From d2dc9a8da799cf2cc8bbe78ac57d14d11d0c6896 Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Mon, 18 Oct 2021 10:46:16 +0800 Subject: [PATCH 01/59] Feat: application operation API implementation (#2478) * Feat: application operation API implementation * Docs: update swagger json * Docs: update swagger config * Feat: improve application management API implementation and testing * Style: change code style * Style: change some code style Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 869 +++++++++++++++++- pkg/apiserver/datastore/datastore.go | 3 + pkg/apiserver/datastore/kubeapi/kubeapi.go | 16 +- pkg/apiserver/datastore/mongodb/mongodb.go | 3 + pkg/apiserver/model/application.go | 71 +- pkg/apiserver/model/model.go | 59 +- pkg/apiserver/model/workflow.go | 8 +- pkg/apiserver/rest/apis/v1/types.go | 126 ++- pkg/apiserver/rest/usecase/application.go | 668 +++++++++++++- .../rest/usecase/application_test.go | 292 ++++++ .../usecase/testdata/example-app-error.yaml | 78 ++ .../rest/usecase/testdata/example-app.yaml | 78 ++ .../rest/usecase/usecase_suite_test.go | 102 ++ pkg/apiserver/rest/usecase/workflow.go | 118 +++ pkg/apiserver/rest/utils/bcode/application.go | 27 + pkg/apiserver/rest/utils/bcode/bcode.go | 11 + .../rest/utils/bcode/bcode_suite_test.go | 29 + pkg/apiserver/rest/utils/bcode/bcode_test.go | 31 + pkg/apiserver/rest/utils/bcode/workflow.go | 23 + pkg/apiserver/rest/utils/utils_suite_test.go | 29 + pkg/apiserver/rest/utils/version.go | 34 + pkg/apiserver/rest/utils/version_test.go | 35 + pkg/apiserver/rest/webservice/application.go | 308 ++++++- pkg/apiserver/rest/webservice/webservice.go | 5 +- pkg/apiserver/rest/webservice/workflow.go | 11 + .../core/scopes/healthscope/healthscope.go | 2 +- test/e2e-apiserver-test/application_test.go | 249 +++++ test/e2e-apiserver-test/suite_test.go | 10 +- .../testdata/example-app.yaml | 78 ++ 29 files changed, 3248 insertions(+), 125 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/application_test.go create mode 100644 pkg/apiserver/rest/usecase/testdata/example-app-error.yaml create mode 100644 pkg/apiserver/rest/usecase/testdata/example-app.yaml create mode 100644 pkg/apiserver/rest/usecase/usecase_suite_test.go create mode 100644 pkg/apiserver/rest/usecase/workflow.go create mode 100644 pkg/apiserver/rest/utils/bcode/bcode_suite_test.go create mode 100644 pkg/apiserver/rest/utils/bcode/bcode_test.go create mode 100644 pkg/apiserver/rest/utils/bcode/workflow.go create mode 100644 pkg/apiserver/rest/utils/utils_suite_test.go create mode 100644 pkg/apiserver/rest/utils/version.go create mode 100644 pkg/apiserver/rest/utils/version_test.go create mode 100644 test/e2e-apiserver-test/testdata/example-app.yaml diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 38dbcaf31..ed9cab8f6 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -29,7 +29,7 @@ "application" ], "summary": "list all applications", - "operationId": "noop", + "operationId": "listApplications", "parameters": [ { "type": "string", @@ -52,7 +52,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.ListApplicationResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } }, @@ -69,7 +76,7 @@ "application" ], "summary": "create one application", - "operationId": "noop", + "operationId": "createApplication", "parameters": [ { "name": "body", @@ -82,7 +89,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.ApplicationBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } @@ -101,7 +115,7 @@ "application" ], "summary": "detail one application", - "operationId": "noop", + "operationId": "detailApplication", "parameters": [ { "type": "string", @@ -113,7 +127,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.DetailApplicationResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } }, @@ -130,7 +151,7 @@ "application" ], "summary": "delete one application", - "operationId": "noop", + "operationId": "deleteApplication", "parameters": [ { "type": "string", @@ -142,7 +163,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } @@ -161,7 +189,7 @@ "application" ], "summary": "gets the component topology of the application", - "operationId": "noop", + "operationId": "listApplicationComponents", "parameters": [ { "type": "string", @@ -180,7 +208,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.ComponentListResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } }, @@ -197,7 +232,7 @@ "application" ], "summary": "create component for application", - "operationId": "noop", + "operationId": "createComponent", "parameters": [ { "type": "string", @@ -217,7 +252,52 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.ComponentBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/components/{componentName}": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail component for application", + "operationId": "detailComponent", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailComponentResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } @@ -236,7 +316,7 @@ "application" ], "summary": "deploy or update the application", - "operationId": "noop", + "operationId": "deployApplication", "parameters": [ { "type": "string", @@ -248,12 +328,55 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.ApplicationBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } }, "/api/v1/applications/{name}/policies": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list policy for application", + "operationId": "listApplicationPolicies", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListApplicationPolicy" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, "post": { "consumes": [ "application/xml", @@ -267,7 +390,7 @@ "application" ], "summary": "create policy for application", - "operationId": "noop", + "operationId": "createApplicationPolicy", "parameters": [ { "type": "string", @@ -287,7 +410,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.PolicyBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } @@ -306,7 +436,7 @@ "application" ], "summary": "detail policy for application", - "operationId": "noop", + "operationId": "detailApplicationPolicy", "parameters": [ { "type": "string", @@ -325,7 +455,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.DetailPolicyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } }, @@ -342,7 +479,7 @@ "application" ], "summary": "detail policy for application", - "operationId": "noop", + "operationId": "deleteApplicationPolicy", "parameters": [ { "type": "string", @@ -361,7 +498,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } @@ -380,7 +524,7 @@ "application" ], "summary": "create one application template", - "operationId": "noop", + "operationId": "publishApplicationTemplate", "parameters": [ { "type": "string", @@ -400,7 +544,14 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.ApplicationTemplateBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } @@ -892,7 +1043,7 @@ } } }, - "/v1/catalogs": { + "/v1/addons": { "get": { "consumes": [ "application/xml", @@ -903,15 +1054,15 @@ "application/xml" ], "tags": [ - "cluster" + "addon" ], - "summary": "list all clusters", + "summary": "list all addons", "operationId": "noop", "parameters": [ { "type": "string", - "description": "Fuzzy search based on name or description", - "name": "query", + "description": "Cluster-based search", + "name": "cluster", "in": "query" } ], @@ -926,6 +1077,187 @@ "description": "Bummer, something went wrong" } } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "create an addon", + "operationId": "noop", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateAddonRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/v1/addons/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "show details of an addon", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the addon", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "delete an addon", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the addon", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/v1/addons/{name}/disable": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "disable an addon on a cluster", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "cluster name", + "name": "cluster", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/v1/addons/{name}/enable": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "enable an addon on a cluster", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "cluster name", + "name": "cluster", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/v1/addons/{name}/status": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "show status of an addon", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the addon", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } } }, "/v1/{namespace}/applications/:appname": { @@ -1050,13 +1382,28 @@ } }, "definitions": { + "bcode.Bcode": { + "required": [ + "BusinessCode", + "Message" + ], + "properties": { + "BusinessCode": { + "type": "integer", + "format": "int32" + }, + "Message": { + "type": "string" + } + } + }, "common.AppRolloutStatus": { "required": [ - "rollingState", - "batchRollingState", "currentBatch", "upgradedReplicas", + "batchRollingState", "upgradedReadyReplicas", + "rollingState", "lastTargetAppRevision" ], "properties": { @@ -1159,6 +1506,12 @@ "type" ], "properties": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, "externalRevision": { "type": "string" }, @@ -1502,6 +1855,118 @@ "type": "string" } }, + "model.ApplicationComponent": { + "required": [ + "createTime", + "updateTime", + "appPrimaryKey", + "creator", + "name", + "type" + ], + "properties": { + "appPrimaryKey": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "externalRevision": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "lables": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ApplicationTrait" + } + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "model.ApplicationTrait": { + "required": [ + "type" + ], + "properties": { + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + } + } + }, + "model.JSONStruct": { + "type": "object" + }, + "model.Model": { + "required": [ + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, "types.Parameter": { "required": [ "name" @@ -1538,6 +2003,49 @@ } }, "types.Parameter.default": {}, + "v1.AddonMeta": { + "required": [ + "name", + "version", + "description", + "icon", + "tags", + "phase" + ], + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "type": "string" + } + } + }, + "v1.AddonStatusResponse": { + "required": [ + "phase" + ], + "properties": { + "phase": { + "type": "string" + } + } + }, "v1.ApplicationBase": { "required": [ "name", @@ -1650,12 +2158,22 @@ }, "v1.ApplicationTemplateBase": { "required": [ - "templateName" + "templateName", + "createTime", + "updateTime" ], "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, "templateName": { "type": "string" }, + "updateTime": { + "type": "string", + "format": "date-time" + }, "versions": { "type": "array", "items": { @@ -1768,7 +2286,9 @@ "componentType", "bindClusters", "dependsOn", - "deployVersion" + "deployVersion", + "createTime", + "updateTime" ], "properties": { "bindClusters": { @@ -1780,6 +2300,10 @@ "componentType": { "type": "string" }, + "createTime": { + "type": "string", + "format": "date-time" + }, "creator": { "type": "string" }, @@ -1806,6 +2330,10 @@ }, "name": { "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" } } }, @@ -1847,6 +2375,43 @@ } } }, + "v1.CreateAddonRequest": { + "required": [ + "name", + "version", + "icon", + "tags" + ], + "properties": { + "deploy_data": { + "type": "string" + }, + "deploy_url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "type": "string" + } + } + }, "v1.CreateApplicationRequest": { "required": [ "name", @@ -2005,16 +2570,58 @@ } } }, + "v1.DetailAddonResponse": { + "required": [ + "name", + "version", + "description", + "icon", + "tags", + "phase" + ], + "properties": { + "deploy_data": { + "type": "string" + }, + "deploy_url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "type": "string" + } + } + }, "v1.DetailApplicationResponse": { "required": [ + "name", "updateTime", "icon", "status", - "gatewayRule", - "name", "namespace", "description", "createTime", + "gatewayRule", "policies", "status", "resourceInfo", @@ -2081,12 +2688,12 @@ }, "v1.DetailClusterResponse": { "required": [ - "name", "description", "icon", "labels", "status", "reason", + "name", "resourceInfo" ], "properties": { @@ -2122,21 +2729,111 @@ } } }, + "v1.DetailComponentResponse": { + "required": [ + "createTime", + "updateTime", + "appPrimaryKey", + "name", + "type", + "creator" + ], + "properties": { + "appPrimaryKey": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "externalRevision": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "lables": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ApplicationTrait" + } + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, "v1.DetailPolicyResponse": { "required": [ "name", "type", - "properties" + "properties", + "createTime", + "updateTime" ], "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, "name": { "type": "string" }, "properties": { - "type": "string" + "$ref": "#/definitions/model.JSONStruct" }, "type": { "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" } } }, @@ -2160,6 +2857,7 @@ } } }, + "v1.EmptyResponse": {}, "v1.GatewayRule": { "required": [ "ruleType", @@ -2187,6 +2885,32 @@ } } }, + "v1.ListAddonResponse": { + "required": [ + "addons" + ], + "properties": { + "addons": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonMeta" + } + } + } + }, + "v1.ListApplicationPolicy": { + "required": [ + "policies" + ], + "properties": { + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PolicyBase" + } + } + } + }, "v1.ListApplicationResponse": { "required": [ "applications" @@ -2273,14 +2997,24 @@ "v1.NamesapceBase": { "required": [ "name", - "description" + "description", + "createTime", + "updateTime" ], "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, "description": { "type": "string" }, "name": { "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" } } }, @@ -2288,6 +3022,8 @@ "required": [ "name", "description", + "createTime", + "updateTime", "clusterBind" ], "properties": { @@ -2297,11 +3033,19 @@ "type": "string" } }, + "createTime": { + "type": "string", + "format": "date-time" + }, "description": { "type": "string" }, "name": { "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" } } }, @@ -2338,6 +3082,34 @@ } } }, + "v1.PolicyBase": { + "required": [ + "name", + "type", + "properties", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, "v1.PolicyDefinition": { "required": [ "name", @@ -2361,12 +3133,20 @@ }, "v1.UpdateWorkflowRequest": { "required": [ + "name", + "namesapce", "enable" ], "properties": { "enable": { "type": "boolean" }, + "name": { + "type": "string" + }, + "namesapce": { + "type": "string" + }, "steps": { "type": "array", "items": { @@ -2379,9 +3159,16 @@ "v1.WorkflowStep": { "required": [ "name", - "type" + "type", + "dependsOn" ], "properties": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, "inputs": { "type": "array", "items": { @@ -2668,6 +3455,12 @@ "type" ], "properties": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, "inputs": { "type": "array", "items": { diff --git a/pkg/apiserver/datastore/datastore.go b/pkg/apiserver/datastore/datastore.go index 5e08fbd51..465b04781 100644 --- a/pkg/apiserver/datastore/datastore.go +++ b/pkg/apiserver/datastore/datastore.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "reflect" + "time" ) var ( @@ -65,6 +66,8 @@ type Config struct { // Entity database data model type Entity interface { + SetCreateTime(time time.Time) + SetUpdateTime(time time.Time) PrimaryKey() string TableName() string Index() map[string]string diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi.go b/pkg/apiserver/datastore/kubeapi/kubeapi.go index fc100f5de..fa7dfab61 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "strings" + "time" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -74,17 +75,17 @@ func generateName(entity datastore.Entity) string { func (m *kubeapi) generateConfigMap(entity datastore.Entity) *corev1.ConfigMap { data, _ := json.Marshal(entity) - lables := entity.Index() - if lables == nil { - lables = make(map[string]string) + labels := entity.Index() + if labels == nil { + labels = make(map[string]string) } - lables["table"] = entity.TableName() - lables["primaryKey"] = entity.PrimaryKey() + labels["table"] = entity.TableName() + labels["primaryKey"] = entity.PrimaryKey() var configMap = corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: generateName(entity), Namespace: m.namespace, - Labels: lables, + Labels: labels, }, BinaryData: map[string][]byte{ "data": data, @@ -101,6 +102,8 @@ func (m *kubeapi) Add(ctx context.Context, entity datastore.Entity) error { if entity.TableName() == "" { return datastore.ErrTableNameEmpty } + entity.SetCreateTime(time.Now()) + entity.SetUpdateTime(time.Now()) configMap := m.generateConfigMap(entity) if err := m.kubeclient.Create(ctx, configMap); err != nil { if apierrors.IsAlreadyExists(err) { @@ -163,6 +166,7 @@ func (m *kubeapi) Put(ctx context.Context, entity datastore.Entity) error { if entity.TableName() == "" { return datastore.ErrTableNameEmpty } + entity.SetUpdateTime(time.Now()) 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) { diff --git a/pkg/apiserver/datastore/mongodb/mongodb.go b/pkg/apiserver/datastore/mongodb/mongodb.go index 270b0c7f4..ddbdac199 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb.go +++ b/pkg/apiserver/datastore/mongodb/mongodb.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "time" "cuelang.org/go/pkg/strings" "go.mongodb.org/mongo-driver/bson" @@ -61,6 +62,7 @@ func (m *mongodb) Add(ctx context.Context, entity datastore.Entity) error { if entity.TableName() == "" { return datastore.ErrTableNameEmpty } + entity.SetCreateTime(time.Now()) if err := m.Get(ctx, entity); err == nil { return datastore.ErrRecordExist } @@ -121,6 +123,7 @@ func (m *mongodb) Put(ctx context.Context, entity datastore.Entity) error { if entity.TableName() == "" { return datastore.ErrTableNameEmpty } + entity.SetUpdateTime(time.Now()) collection := m.client.Database(m.database).Collection(entity.TableName()) _, err := collection.UpdateOne(ctx, makeNameFilter(entity.PrimaryKey()), makeEntityUpdate(entity)) if err != nil { diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 9e0e69cbc..83be4e109 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -24,6 +24,7 @@ import ( // Application database model type Application struct { + Model Name string `json:"name"` Namespace string `json:"namespace"` Description string `json:"description"` @@ -56,9 +57,10 @@ func (a *Application) Index() map[string]string { // ApplicationComponent component database model type ApplicationComponent struct { + Model AppPrimaryKey string `json:"appPrimaryKey"` Description string `json:"description,omitempty"` - Labels map[string]string `json:"lables,omitempty"` + Labels map[string]string `json:"labels,omitempty"` Icon string `json:"icon,omitempty"` Creator string `json:"creator"` Name string `json:"name"` @@ -104,9 +106,12 @@ func (a *ApplicationComponent) Index() map[string]string { // ApplicationPolicy app policy type ApplicationPolicy struct { + Model AppPrimaryKey string `json:"appPrimaryKey"` Name string `json:"name"` + Description string `json:"description"` Type string `json:"type"` + Creator string `json:"creator"` Properties *JSONStruct `json:"properties,omitempty"` } @@ -140,3 +145,67 @@ type ApplicationTrait struct { Type string `json:"type"` Properties *JSONStruct `json:"properties,omitempty"` } + +// DeployEventInit event status init +var DeployEventInit = "init" + +// DeployEventRunning event status running +var DeployEventRunning = "running" + +// DeployEventComplete event status complete +var DeployEventComplete = "complete" + +// DeployEventFail event status failure +var DeployEventFail = "failure" + +// DeployEvent record each application deployment event. +type DeployEvent struct { + Model + AppPrimaryKey string `json:"appPrimaryKey"` + Version string `json:"version"` + // ApplyAppConfig Stores the application configuration during the current deploy. + ApplyAppConfig string `json:"applyAppConfig,omitempty"` + + // Deploy event status + Status string `json:"status"` + Reason string `json:"reason"` + + // The user that triggers the deploy. + DeployUser string `json:"deployUser"` + + // Information that users can note. + Commit string `json:"commit"` + // SourceType the event trigger source, Web or API + SourceType string `json:"sourceType"` +} + +// TableName return custom table name +func (a *DeployEvent) TableName() string { + return tableNamePrefix + "deploy_event" +} + +// PrimaryKey return custom primary key +func (a *DeployEvent) PrimaryKey() string { + return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Version) +} + +// Index return custom index +func (a *DeployEvent) Index() map[string]string { + index := make(map[string]string) + if a.Version != "" { + index["version"] = a.Version + } + if a.AppPrimaryKey != "" { + index["appPrimaryKey"] = a.AppPrimaryKey + } + if a.DeployUser != "" { + index["deployUser"] = a.DeployUser + } + if a.Status != "" { + index["status"] = a.Status + } + if a.SourceType != "" { + index["sourceType"] = a.SourceType + } + return index +} diff --git a/pkg/apiserver/model/model.go b/pkg/apiserver/model/model.go index 54955f316..afedb7bdc 100644 --- a/pkg/apiserver/model/model.go +++ b/pkg/apiserver/model/model.go @@ -19,8 +19,12 @@ package model import ( "encoding/json" "fmt" + "time" "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/pkg/apiserver/log" ) var tableNamePrefix = "vela_" @@ -29,7 +33,7 @@ var tableNamePrefix = "vela_" type JSONStruct map[string]interface{} // NewJSONStruct new jsonstruct from runtime.RawExtension -func NewJSONStruct(raw runtime.RawExtension) (*JSONStruct, error) { +func NewJSONStruct(raw *runtime.RawExtension) (*JSONStruct, error) { var data JSONStruct err := json.Unmarshal(raw.Raw, &data) if err != nil { @@ -37,3 +41,56 @@ func NewJSONStruct(raw runtime.RawExtension) (*JSONStruct, error) { } return &data, nil } + +// NewJSONStructByString new jsonstruct from string +func NewJSONStructByString(source string) (*JSONStruct, error) { + if source == "" { + return nil, nil + } + var data JSONStruct + err := json.Unmarshal([]byte(source), &data) + if err != nil { + return nil, fmt.Errorf("parse raw data failure %w", err) + } + return &data, nil +} + +// JSON Encoded as a JSON string +func (j *JSONStruct) JSON() string { + b, err := json.Marshal(j) + if err != nil { + log.Logger.Errorf("json marshal failure %s", err.Error()) + } + return string(b) +} + +// RawExtension Encoded as a RawExtension +func (j *JSONStruct) RawExtension() *runtime.RawExtension { + yamlByte, err := yaml.Marshal(j) + if err != nil { + log.Logger.Errorf("yaml marshal failure %s", err.Error()) + return nil + } + b, err := yaml.YAMLToJSON(yamlByte) + if err != nil { + log.Logger.Errorf("yaml to json failure %s", err.Error()) + return nil + } + return &runtime.RawExtension{Raw: b} +} + +// Model common model +type Model struct { + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// SetCreateTime set create time +func (m *Model) SetCreateTime(time time.Time) { + m.CreateTime = time +} + +// SetUpdateTime set update time +func (m *Model) SetUpdateTime(time time.Time) { + m.UpdateTime = time +} diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index afa38608d..7d6fe4020 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -17,15 +17,15 @@ limitations under the License. package model import ( - "fmt" - "github.com/oam-dev/kubevela/apis/core.oam.dev/common" ) // Workflow workflow database model type Workflow struct { + Model Name string `json:"name"` Namespace string `json:"namespace"` + Enable bool `json:"enable"` Steps []WorkflowStep `json:"steps,omitempty"` } @@ -34,7 +34,7 @@ type WorkflowStep struct { // Name is the unique name of the workflow step. Name string `json:"name"` Type string `json:"type"` - Properties JSONStruct `json:"properties,omitempty"` + Properties *JSONStruct `json:"properties,omitempty"` DependsOn []string `json:"dependsOn,omitempty"` Inputs common.StepInputs `json:"inputs,omitempty"` Outputs common.StepOutputs `json:"outputs,omitempty"` @@ -47,7 +47,7 @@ func (w *Workflow) TableName() string { // PrimaryKey return custom primary key func (w *Workflow) PrimaryKey() string { - return fmt.Sprintf("%s-%s", w.Namespace, w.Name) + return w.Name } // Index return custom primary key diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 4cc798fb6..755227472 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -21,8 +21,12 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/apiserver/model" ) +// CtxKeyApplication request context key of application +var CtxKeyApplication = "application" + // AddonPhase defines the phase of an addon type AddonPhase string @@ -37,6 +41,9 @@ const ( AddonPhaseEnabling AddonPhase = "enabling" ) +// EmptyResponse empty response, it will used for delete api +type EmptyResponse struct{} + // CreateAddonRequest defines the format for addon create request type CreateAddonRequest struct { Name string `json:"name" validate:"name"` @@ -227,27 +234,36 @@ type ComponentBase struct { DependsOn []string `json:"dependsOn"` Creator string `json:"creator,omitempty"` DeployVersion string `json:"deployVersion"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // ComponentListResponse list component type ComponentListResponse struct { - Components []ComponentBase `json:"components"` + Components []*ComponentBase `json:"components"` } // CreateComponentRequest create component request model type CreateComponentRequest struct { - ApplicationName string `json:"appName" validate:"name"` - Name string `json:"name" validate:"required"` - Description string `json:"description"` - Labels map[string]string `json:"labels,omitempty"` - ComponentType string `json:"componentType" validate:"required"` - BindClusters []string `json:"bindClusters"` - Properties string `json:"properties,omitempty"` + Name string `json:"name" validate:"checkname"` + Description string `json:"description"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels,omitempty"` + ComponentType string `json:"componentType" validate:"checkname"` + BindClusters []string `json:"bindClusters"` + Properties string `json:"properties,omitempty"` + DependsOn []string `json:"dependsOn"` +} + +// DetailComponentResponse detail component model +type DetailComponentResponse struct { + model.ApplicationComponent + //TODO: Status } // CreateApplicationTemplateRequest create app template request model type CreateApplicationTemplateRequest struct { - TemplateName string `json:"templateName" validate:"required"` + TemplateName string `json:"templateName" validate:"checkname"` Version string `json:"version" validate:"required"` Description string `json:"description"` } @@ -256,6 +272,8 @@ type CreateApplicationTemplateRequest struct { type ApplicationTemplateBase struct { TemplateName string `json:"templateName"` Versions []*ApplicationTemplateVersion `json:"versions,omitempty"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // ApplicationTemplateVersion template version model @@ -274,13 +292,15 @@ type ListNamespaceResponse struct { // NamesapceBase namespace base model type NamesapceBase struct { - Name string `json:"name"` - Description string `json:"description"` + Name string `json:"name"` + Description string `json:"description"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // CreateNamespaceRequest create namespace request body type CreateNamespaceRequest struct { - Name string `json:"name" validate:"name"` + Name string `json:"name" validate:"checkname"` Description string `json:"description"` } @@ -306,25 +326,47 @@ type ComponentDefinitionBase struct { // CreatePolicyRequest create app policy type CreatePolicyRequest struct { // Name is the unique name of the policy. - Name string `json:"name" validate:"name"` + Name string `json:"name" validate:"checkname"` - Type string `json:"type" validate:"required"` + Description string `json:"description"` + + Type string `json:"type" validate:"checkname"` // 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"` - +// UpdatePolicyRequest update policy +type UpdatePolicyRequest struct { + Description string `json:"description"` + Type string `json:"type" validate:"checkname"` // Properties json data Properties string `json:"properties"` } +// PolicyBase application policy base info +type PolicyBase struct { + // Name is the unique name of the policy. + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Creator string `json:"creator"` + // Properties json data + Properties *model.JSONStruct `json:"properties"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// DetailPolicyResponse app policy detail model +type DetailPolicyResponse struct { + PolicyBase +} + +// ListApplicationPolicy list app policies +type ListApplicationPolicy struct { + Policies []*PolicyBase `json:"policies"` +} + // ListPolicyDefinitionResponse list available type ListPolicyDefinitionResponse struct { PolicyDefinitions []PolicyDefinition `json:"policyDefinitions"` @@ -339,22 +381,21 @@ type PolicyDefinition struct { // UpdateWorkflowRequest update or create application workflow type UpdateWorkflowRequest struct { - Steps []WorkflowStep `json:"steps,omitempty"` - Enable bool `json:"enable"` + Name string `json:"name" validate:"checkname"` + Namespace string `json:"namesapce" validate:"checkname"` + 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"` + Name string `json:"name" validate:"checkname"` + Type string `json:"type" validate:"checkname"` + DependsOn []string `json:"dependsOn"` + Properties string `json:"properties,omitempty"` + Inputs common.StepInputs `json:"inputs,omitempty"` + Outputs common.StepOutputs `json:"outputs,omitempty"` } // DetailWorkflowResponse detail workflow response @@ -373,3 +414,24 @@ type ListWorkflowRecordsResponse struct { // WorkflowRecord workflow record type WorkflowRecord struct { } + +// ApplicationDeployRequest the application deploy or update event request +type ApplicationDeployRequest struct { + // User note message, optional + Commit string `json:"commit"` + // SourceType the event trigger source, Web or API + SourceType string `json:"sourceType" validate:"oneof=web api"` + // Force set to True to ignore unfinished events. + Force bool `json:"force"` +} + +// ApplicationDeployResponse deploy response +type ApplicationDeployResponse struct { + Version string `json:"version"` + Status string `json:"status"` + Reason string `json:"reason"` + DeployUser string `json:"deployUser"` + Commit string `json:"commit"` + // SourceType the event trigger source, Web or API + SourceType string `json:"sourceType"` +} diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index 783223c68..e1c7ed991 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -18,29 +18,120 @@ package usecase import ( "context" - "encoding/json" "errors" + 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/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" "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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/utils/apply" ) // ApplicationUsecase application usecase type ApplicationUsecase interface { + ListApplications(ctx context.Context) ([]*apisv1.ApplicationBase, error) + GetApplication(ctx context.Context, appName string) (*model.Application, error) + DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) + PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) CreateApplication(context.Context, apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) + DeleteApplication(ctx context.Context, app *model.Application) error + Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) + ListComponents(ctx context.Context, app *model.Application) ([]*apisv1.ComponentBase, error) + AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) + DetailComponent(ctx context.Context, app *model.Application, componentName string) (*apisv1.DetailComponentResponse, error) + DeleteComponent(ctx context.Context, app *model.Application, componentName string) error + ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) + AddPolicy(ctx context.Context, app *model.Application, policy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) + DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) + DeletePolicy(ctx context.Context, app *model.Application, policyName string) error + UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) } type applicationUsecaseImpl struct { - ds datastore.DataStore + ds datastore.DataStore + kubeClient client.Client + apply apply.Applicator + workflowUsecase WorkflowUsecase } -// NewApplicationUsecase new cluster usecase -func NewApplicationUsecase(ds datastore.DataStore) ApplicationUsecase { - return &applicationUsecaseImpl{ds: ds} +// NewApplicationUsecase new application usecase +func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase) ApplicationUsecase { + kubecli, _ := clients.GetKubeClient() + return &applicationUsecaseImpl{ + ds: ds, + workflowUsecase: workflowUsecase, + kubeClient: kubecli, + apply: apply.NewAPIApplicator(kubecli), + } +} + +// ListApplications list applications +func (c *applicationUsecaseImpl) ListApplications(ctx context.Context) ([]*apisv1.ApplicationBase, error) { + var app = model.Application{} + entitys, err := c.ds.List(ctx, &app, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + var list []*apisv1.ApplicationBase + for _, entity := range entitys { + list = append(list, c.converAppModelToBase(entity.(*model.Application))) + } + return list, nil +} + +// GetApplication get application model +func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName string) (*model.Application, error) { + var app = model.Application{ + Name: appName, + } + if err := c.ds.Get(ctx, &app); err != nil { + return nil, err + } + return &app, nil +} + +// DetailApplication detail application info +func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) { + base := c.converAppModelToBase(app) + policys, err := c.queryApplicationPolicys(ctx, app) + if err != nil { + return nil, err + } + components, err := c.ListComponents(ctx, app) + if err != nil { + return nil, err + } + var policyNames []string + for _, p := range policys { + policyNames = append(policyNames, p.Name) + } + var detail = &apisv1.DetailApplicationResponse{ + ApplicationBase: *base, + Policies: policyNames, + ResourceInfo: apisv1.ApplicationResourceInfo{ + ComponentNum: len(components), + }, + WorkflowStatus: []apisv1.WorkflowStepStatus{}, + } + return detail, nil +} + +// PublishApplicationTemplate publish app template +func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) { + //TODO: + return nil, nil } // CreateApplication create application @@ -48,28 +139,76 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis application := model.Application{ Name: req.Name, Description: req.Description, + Namespace: req.Namespace, Icon: req.Icon, Labels: req.Labels, ClusterList: req.ClusterList, } - // check clusters. - + // check app name. + exit, err := c.ds.IsExist(ctx, &application) + if err != nil { + log.Logger.Errorf("check application name is exist failure %s", err.Error()) + return nil, bcode.ErrApplicationExist + } + if exit { + return nil, bcode.ErrApplicationExist + } // check can deploy var canDeploy bool if req.YamlConfig != "" { var oamApp v1beta1.Application - if err := json.Unmarshal([]byte(req.YamlConfig), &oamApp); err != nil { + if err := yaml.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 + // split the configuration and store it in the database. + if err := c.saveApplicationComponent(ctx, &application, oamApp.Spec.Components); err != nil { + log.Logger.Errorf("save applictaion component failure,%s", err.Error()) + return nil, err + } + if len(oamApp.Spec.Policies) > 0 { + if err := c.saveApplicationPolicy(ctx, &application, oamApp.Spec.Policies); err != nil { + log.Logger.Errorf("save applictaion polocies failure,%s", err.Error()) + return nil, err + } + } + if oamApp.Spec.Workflow != nil && len(oamApp.Spec.Workflow.Steps) > 0 { + var steps []apisv1.WorkflowStep + for _, step := range oamApp.Spec.Workflow.Steps { + var propertyStr string + if step.Properties != nil { + properties, err := model.NewJSONStruct(step.Properties) + if err != nil { + log.Logger.Errorf("workflow %s step %s properties is invalid %s", application.Name, step.Name, err.Error()) + continue + } + propertyStr = properties.JSON() + } + steps = append(steps, apisv1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + DependsOn: step.DependsOn, + Properties: propertyStr, + Inputs: step.Inputs, + Outputs: step.Outputs, + }) + } + _, err := c.workflowUsecase.CreateOrUpdateWorkflow(ctx, apisv1.UpdateWorkflowRequest{ + Name: application.Name, + Namespace: application.Namespace, + Steps: steps, + Enable: true, + }) + if err != nil { + return nil, err + } + } + // you can deploy only if the application contains components + canDeploy = len(oamApp.Spec.Components) > 0 } - // add to db. + // add application to db. if err := c.ds.Add(ctx, &application); err != nil { if errors.Is(err, datastore.ErrRecordExist) { return nil, bcode.ErrApplicationExist @@ -77,27 +216,354 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return nil, err } // render app base info. - base := c.renderAppBase(&application) + base := c.converAppModelToBase(&application) // deploy to cluster if need. if req.Deploy && canDeploy { - if err := c.Deploy(ctx, req.Name); err != nil { + if _, err := c.Deploy(ctx, &application, apisv1.ApplicationDeployRequest{ + Commit: "init create auto deploy", + SourceType: "web", + }); err != nil { return nil, err } } return base, nil } +func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, app *model.Application, components []common.ApplicationComponent) error { + var componentModels []datastore.Entity + for _, component := range components { + // TODO: Check whether the component type is supported. + var traits []model.ApplicationTrait + for _, trait := range component.Traits { + properties, err := model.NewJSONStruct(trait.Properties) + if err != nil { + log.Logger.Errorf("parse trait properties failire %w", err) + return bcode.ErrInvalidProperties + } + traits = append(traits, model.ApplicationTrait{ + Type: trait.Type, + Properties: properties, + }) + } + properties, err := model.NewJSONStruct(component.Properties) + if err != nil { + log.Logger.Errorf("parse component properties failire %w", err) + return bcode.ErrInvalidProperties + } + componentModel := model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: component.Name, + Type: component.Type, + ExternalRevision: component.ExternalRevision, + DependsOn: component.DependsOn, + Inputs: component.Inputs, + Outputs: component.Outputs, + Scopes: component.Scopes, + Traits: traits, + Properties: properties, + } + componentModels = append(componentModels, &componentModel) + } + return c.ds.BatchAdd(ctx, componentModels) +} + +func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.Application) ([]*apisv1.ComponentBase, error) { + var component = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + } + components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + var list []*apisv1.ComponentBase + for _, component := range components { + pm := component.(*model.ApplicationComponent) + list = append(list, c.converComponentModelToBase(pm)) + } + return list, nil +} + +// DetailComponent detail app component +// TODO: Add status data about the component. +func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailComponentResponse, error) { + var component = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: policyName, + } + err := c.ds.Get(ctx, &component) + if err != nil { + return nil, err + } + return &apisv1.DetailComponentResponse{ + ApplicationComponent: component, + }, nil +} + +func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.ApplicationComponent) *apisv1.ComponentBase { + return &apisv1.ComponentBase{ + Name: m.Name, + Description: m.Description, + Labels: m.Labels, + ComponentType: m.Type, + Icon: m.Icon, + DependsOn: m.DependsOn, + Creator: m.Creator, + CreateTime: m.CreateTime, + UpdateTime: m.UpdateTime, + } +} + +// ListPolicies list application policies +func (c *applicationUsecaseImpl) ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) { + policies, err := c.queryApplicationPolicys(ctx, app) + if err != nil { + return nil, err + } + var list []*apisv1.PolicyBase + for _, policy := range policies { + list = append(list, c.converPolicyModelToBase(policy)) + } + return list, nil +} + +func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.ApplicationPolicy) *apisv1.PolicyBase { + pb := &apisv1.PolicyBase{ + Name: policy.Name, + Type: policy.Type, + Properties: policy.Properties, + Description: policy.Description, + Creator: policy.Creator, + CreateTime: policy.CreateTime, + UpdateTime: policy.UpdateTime, + } + return pb +} + +func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app *model.Application, policys []v1beta1.AppPolicy) error { + var policyModels []datastore.Entity + for _, policy := range policys { + properties, err := model.NewJSONStruct(policy.Properties) + if err != nil { + log.Logger.Errorf("parse trait properties failire %w", err) + return bcode.ErrInvalidProperties + } + policyModels = append(policyModels, &model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Name: policy.Name, + Type: policy.Type, + Properties: properties, + }) + } + return c.ds.BatchAdd(ctx, policyModels) +} + +func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, app *model.Application) (list []*model.ApplicationPolicy, err error) { + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + } + policys, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + for _, policy := range policys { + pm := policy.(*model.ApplicationPolicy) + list = append(list, pm) + } + return +} + +// DetailPolicy detail app policy +// TODO: Add status data about the policy. +func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) { + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Name: policyName, + } + err := c.ds.Get(ctx, &policy) + if err != nil { + return nil, err + } + return &apisv1.DetailPolicyResponse{ + PolicyBase: *c.converPolicyModelToBase(&policy), + }, 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) Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) { + // step1: Render oam application + version := utils.GenerateVersion("") + oamApp, err := c.renderOAMApplication(ctx, app, version) + if err != nil { + return nil, err + } + configByte, _ := yaml.Marshal(oamApp) + // step2: check and create deploy event + if !req.Force { + var lastEvent = model.DeployEvent{ + AppPrimaryKey: app.PrimaryKey(), + } + list, err := c.ds.List(ctx, &lastEvent, &datastore.ListOptions{PageSize: 1, Page: 1}) + if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { + log.Logger.Errorf("query last app event failure %s", err.Error()) + return nil, bcode.ErrDeployEventConflict + } + if len(list) > 0 && list[0].(*model.DeployEvent).Status != model.DeployEventComplete { + return nil, bcode.ErrDeployEventConflict + } + } + + var deployEvent = &model.DeployEvent{ + AppPrimaryKey: app.PrimaryKey(), + Version: version, + ApplyAppConfig: string(configByte), + Status: model.DeployEventInit, + // TODO: Get user information from ctx and assign a value. + DeployUser: "", + Commit: req.Commit, + SourceType: req.SourceType, + } + + if err := c.ds.Add(ctx, deployEvent); err != nil { + return nil, err + } + // step3: check and create namespace + var namespace corev1.Namespace + if err := c.kubeClient.Get(ctx, types.NamespacedName{Name: oamApp.Namespace}, &namespace); apierrors.IsNotFound(err) { + namespace.Name = oamApp.Namespace + if err := c.kubeClient.Create(ctx, &namespace); err != nil { + log.Logger.Errorf("auto create namesapce failure %s", err.Error()) + return nil, bcode.ErrCreateNamespace + } + } + // step4: apply to controller cluster + err = c.apply.Apply(ctx, oamApp) + if err != nil { + deployEvent.Status = model.DeployEventFail + deployEvent.Reason = err.Error() + if err := c.ds.Put(ctx, deployEvent); err != nil { + log.Logger.Warnf("update deploy event failure %s", err.Error()) + } + log.Logger.Errorf("deploy app %s failure %s", app.PrimaryKey(), err.Error()) + return nil, bcode.ErrDeployApplyFail + } + deployEvent.Status = model.DeployEventRunning + if err := c.ds.Put(ctx, deployEvent); err != nil { + log.Logger.Warnf("update deploy event failure %s", err.Error()) + } + + // step5: update deploy event status + return &apisv1.ApplicationDeployResponse{ + Version: deployEvent.Version, + Status: deployEvent.Status, + Reason: deployEvent.Reason, + DeployUser: deployEvent.DeployUser, + Commit: deployEvent.Commit, + SourceType: deployEvent.SourceType, + }, nil } -func (c *applicationUsecaseImpl) renderAppBase(app *model.Application) *apisv1.ApplicationBase { +func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMoel *model.Application, version string) (*v1beta1.Application, error) { + var app = &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: appMoel.Name, + Namespace: appMoel.Namespace, + Labels: appMoel.Labels, + Annotations: map[string]string{ + "deploy_version": version, + }, + }, + } + var component = model.ApplicationComponent{ + AppPrimaryKey: appMoel.PrimaryKey(), + } + components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + if err != nil || len(components) == 0 { + return nil, bcode.ErrNoComponent + } + + var policy = model.ApplicationPolicy{ + AppPrimaryKey: appMoel.PrimaryKey(), + } + policies, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + + for _, entity := range components { + component := entity.(*model.ApplicationComponent) + var traits []common.ApplicationTrait + for _, trait := range component.Traits { + aTrait := common.ApplicationTrait{ + Type: trait.Type, + } + if trait.Properties != nil { + aTrait.Properties = trait.Properties.RawExtension() + } + traits = append(traits, aTrait) + } + app.Spec.Components = append(app.Spec.Components, common.ApplicationComponent{ + Name: component.Name, + Type: component.Type, + ExternalRevision: component.ExternalRevision, + DependsOn: component.DependsOn, + Inputs: component.Inputs, + Outputs: component.Outputs, + Traits: traits, + Scopes: component.Scopes, + }) + } + + for _, entity := range policies { + policy := entity.(*model.ApplicationPolicy) + apolicy := v1beta1.AppPolicy{ + Name: component.Name, + Type: component.Type, + } + if policy.Properties != nil { + apolicy.Properties = policy.Properties.RawExtension() + } + app.Spec.Policies = append(app.Spec.Policies, apolicy) + } + workflow, err := c.workflowUsecase.GetWorkflow(ctx, appMoel.Name) + if err != nil { + return nil, err + } + if workflow != nil { + var steps []v1beta1.WorkflowStep + for _, step := range workflow.Steps { + var wstep = v1beta1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + DependsOn: step.DependsOn, + Inputs: step.Inputs, + Outputs: step.Outputs, + } + if step.Properties != nil { + wstep.Properties = step.Properties.RawExtension() + } + } + app.Spec.Workflow = &v1beta1.Workflow{ + Steps: steps, + } + } + return app, nil +} + +func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *apisv1.ApplicationBase { appBeas := &apisv1.ApplicationBase{ Name: app.Name, + Namespace: app.Namespace, + CreateTime: app.CreateTime, + UpdateTime: app.UpdateTime, Description: app.Description, Icon: app.Icon, Labels: app.Labels, @@ -105,3 +571,167 @@ func (c *applicationUsecaseImpl) renderAppBase(app *model.Application) *apisv1.A // TODO: get and render app status return appBeas } + +// DeleteApplication delete application +func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *model.Application) error { + // TODO: check app can be deleted + + // query all components to deleted + components, err := c.ListComponents(ctx, app) + if err != nil { + return err + } + // query all policies to deleted + policies, err := c.ListPolicies(ctx, app) + if err != nil { + return err + } + // delete workflow + if err := c.workflowUsecase.DeleteWorkflow(ctx, app.Name); err != nil && !errors.Is(err, bcode.ErrWorkflowNotExist) { + log.Logger.Errorf("delete workflow %s failure %s", app.Name, err.Error()) + } + + for _, component := range components { + err := c.ds.Delete(ctx, &model.ApplicationComponent{AppPrimaryKey: app.PrimaryKey(), Name: component.Name}) + if err != nil && errors.Is(err, datastore.ErrRecordNotExist) { + log.Logger.Errorf("delete component %s in app %s failure %s", component.Name, app.Name, err.Error()) + } + } + + for _, policy := range policies { + err := c.ds.Delete(ctx, &model.ApplicationPolicy{AppPrimaryKey: app.PrimaryKey(), Name: policy.Name}) + if err != nil && errors.Is(err, datastore.ErrRecordNotExist) { + log.Logger.Errorf("delete policy %s in app %s failure %s", policy.Name, app.Name, err.Error()) + } + } + + return c.ds.Delete(ctx, app) +} + +func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) { + componentModel := model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Description: com.Description, + Labels: com.Labels, + Icon: com.Icon, + // TODO: Get user information from ctx and assign a value. + Creator: "", + Name: com.Name, + Type: com.ComponentType, + DependsOn: com.DependsOn, + } + properties, err := model.NewJSONStructByString(com.Properties) + if err != nil { + return nil, bcode.ErrInvalidProperties + } + componentModel.Properties = properties + if err := c.ds.Add(ctx, &componentModel); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrApplicationComponetExist + } + log.Logger.Warnf("add component for app %s failure %s", app.PrimaryKey(), err.Error()) + return nil, err + } + return &apisv1.ComponentBase{ + Name: componentModel.Name, + Description: componentModel.Description, + Labels: componentModel.Labels, + ComponentType: componentModel.Type, + Icon: componentModel.Icon, + DependsOn: componentModel.DependsOn, + Creator: componentModel.Creator, + CreateTime: componentModel.CreateTime, + UpdateTime: componentModel.UpdateTime, + }, nil +} + +func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.Application, componentName string) error { + var component = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: componentName, + } + if err := c.ds.Delete(ctx, &component); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrApplicationComponetNotExist + } + log.Logger.Warnf("delete app component %s failure %s", app.PrimaryKey(), err.Error()) + return err + } + return nil +} + +func (c *applicationUsecaseImpl) AddPolicy(ctx context.Context, app *model.Application, createpolicy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) { + policyModel := model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Description: createpolicy.Description, + // TODO: Get user information from ctx and assign a value. + Creator: "", + Name: createpolicy.Name, + Type: createpolicy.Type, + } + properties, err := model.NewJSONStructByString(createpolicy.Properties) + if err != nil { + return nil, bcode.ErrInvalidProperties + } + policyModel.Properties = properties + if err := c.ds.Add(ctx, &policyModel); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrApplicationPolicyExist + } + log.Logger.Warnf("add policy for app %s failure %s", app.PrimaryKey(), err.Error()) + return nil, err + } + return &apisv1.PolicyBase{ + Name: policyModel.Name, + Description: policyModel.Description, + Type: policyModel.Type, + Creator: policyModel.Creator, + CreateTime: policyModel.CreateTime, + UpdateTime: policyModel.UpdateTime, + Properties: policyModel.Properties, + }, nil +} + +func (c *applicationUsecaseImpl) DeletePolicy(ctx context.Context, app *model.Application, policyName string) error { + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Name: policyName, + } + if err := c.ds.Delete(ctx, &policy); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrApplicationPolicyNotExist + } + log.Logger.Warnf("delete app policy %s failure %s", app.PrimaryKey(), err.Error()) + return err + } + return nil +} + +func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policyUpdate apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) { + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Name: policyName, + } + err := c.ds.Get(ctx, &policy) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationPolicyNotExist + } + log.Logger.Warnf("update app policy %s failure %s", app.PrimaryKey(), err.Error()) + return nil, err + } + policy.Type = policyUpdate.Type + properties, err := model.NewJSONStructByString(policyUpdate.Properties) + if err != nil { + return nil, bcode.ErrInvalidProperties + } + policy.Properties = properties + policy.Description = policyUpdate.Description + + if err := c.ds.Put(ctx, &policy); err != nil { + return nil, err + } + return &apisv1.DetailPolicyResponse{ + PolicyBase: *c.converPolicyModelToBase(&policy), + }, nil +} diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go new file mode 100644 index 000000000..72cdee439 --- /dev/null +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -0,0 +1,292 @@ +/* +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" + "io/ioutil" + "strings" + + "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/types" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/utils/apply" +) + +var _ = Describe("Test application usecase function", func() { + var ( + appUsecase *applicationUsecaseImpl + workflowUsecase *workflowUsecaseImpl + ) + BeforeEach(func() { + workflowUsecase = &workflowUsecaseImpl{ds: ds} + appUsecase = &applicationUsecaseImpl{ + ds: ds, + workflowUsecase: workflowUsecase, + apply: apply.NewAPIApplicator(k8sClient), + kubeClient: k8sClient, + } + }) + It("Test CreateApplication funtion", func() { + By("test sample create") + req := v1.CreateApplicationRequest{ + Name: "test-app", + Namespace: "test-app-namespace", + Description: "this is a test app", + } + base, err := appUsecase.CreateApplication(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) + + _, err = appUsecase.CreateApplication(context.TODO(), req) + equal := cmp.Equal(err, bcode.ErrApplicationExist, cmpopts.EquateErrors()) + Expect(equal).Should(BeTrue()) + + By("test with oam yaml config create") + bs, err := ioutil.ReadFile("./testdata/example-app.yaml") + Expect(err).Should(Succeed()) + req = v1.CreateApplicationRequest{ + Name: "test-app-sadasd", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + YamlConfig: string(bs), + } + base, err = appUsecase.CreateApplication(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) + + req = v1.CreateApplicationRequest{ + Name: "test-app-sadasd2", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + YamlConfig: "asdasdasdasd", + } + base, err = appUsecase.CreateApplication(context.TODO(), req) + equal = cmp.Equal(err, bcode.ErrApplicationConfig, cmpopts.EquateErrors()) + Expect(equal).Should(BeTrue()) + Expect(base).Should(BeNil()) + + bs, err = ioutil.ReadFile("./testdata/example-app-error.yaml") + Expect(err).Should(Succeed()) + req = v1.CreateApplicationRequest{ + Name: "test-app-sadasd3", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + YamlConfig: string(bs), + } + _, err = appUsecase.CreateApplication(context.TODO(), req) + equal = cmp.Equal(err, bcode.ErrInvalidProperties, cmpopts.EquateErrors()) + Expect(equal).Should(BeTrue()) + }) + + It("Test ListApplications funtion", func() { + apps, err := appUsecase.ListApplications(context.TODO()) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(apps), 2)).Should(BeEmpty()) + }) + + It("Test DetailApplication funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + detail, err := appUsecase.DetailApplication(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(detail.ResourceInfo.ComponentNum, 2)).Should(BeEmpty()) + Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) + }) + + It("Test GetWorkflow funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + detail, err := workflowUsecase.GetWorkflow(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(detail.Namespace, "test-app-namespace")).Should(BeEmpty()) + Expect(cmp.Diff(detail.Enable, true)).Should(BeEmpty()) + }) + + It("Test ListPolicies funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + policies, err := appUsecase.ListPolicies(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(policies), 1)).Should(BeEmpty()) + Expect(cmp.Diff(policies[0].Type, "env-binding")).Should(BeEmpty()) + Expect((*policies[0].Properties)["envs"]).ShouldNot(BeEmpty()) + }) + + It("Test ListComponents funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + components, err := appUsecase.ListComponents(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].ComponentType, "worker")).Should(BeEmpty()) + Expect(components[1].UpdateTime).ShouldNot(BeNil()) + }) + + It("Test DetailComponent funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + detail, err := appUsecase.DetailComponent(context.TODO(), appModel, "hello-world-server") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(detail.Traits), 1)).Should(BeEmpty()) + Expect(cmp.Diff(detail.Type, "webservice")).Should(BeEmpty()) + Expect(cmp.Diff(strings.Contains((*detail.Properties)["image"].(string), "crccheck/hello-world"), true)).Should(BeEmpty()) + }) + + It("Test DetailPolicy funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, "example-multi-env-policy") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(detail.Type, "env-binding")).Should(BeEmpty()) + Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) + }) + It("Test AddComponent funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + base, err := appUsecase.AddComponent(context.TODO(), appModel, v1.CreateComponentRequest{ + Name: "test2", + Description: "this is a test2 component", + Labels: map[string]string{}, + ComponentType: "worker", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + DependsOn: []string{"data-worker"}, + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.ComponentType, "worker")).Should(BeEmpty()) + }) + It("Test DetailComponent funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + detailResponse, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(detailResponse.DependsOn[0], "data-worker")).Should(BeEmpty()) + Expect(detailResponse.Properties).ShouldNot(BeNil()) + Expect(cmp.Diff((*detailResponse.Properties)["image"], "busybox")).Should(BeEmpty()) + }) + + It("Test AddPolicy funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ + Name: "example-multi-env-policy", + Description: "this is a test2 policy", + Type: "env-binding", + Properties: ``, + }) + Expect(cmp.Equal(err, bcode.ErrApplicationPolicyExist, cmpopts.EquateErrors())).Should(BeTrue()) + _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ + Name: "env-binding-2", + Description: "this is a test2 policy", + Type: "env-binding", + Properties: `{"envs":{ "name": "test", "placement":{"namespaceSelector":{ "name": "TEST_NAMESPACE"}}, "selector":{ "components": ["data-worker"]}}}`, + }) + Expect(err).Should(BeNil()) + }) + It("Test DetailPolicy funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, "env-binding-2") + Expect(err).Should(BeNil()) + Expect(detail.Properties).ShouldNot(BeNil()) + Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) + }) + It("Test UpdatePolicy funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + base, err := appUsecase.UpdatePolicy(context.TODO(), appModel, "env-binding-2", v1.UpdatePolicyRequest{ + Type: "env-binding", + Properties: `{"envs":{}}`, + }) + Expect(err).Should(BeNil()) + Expect(base.Properties).ShouldNot(BeNil()) + Expect((*base.Properties)["envs"]).Should(BeEmpty()) + }) + It("Test DeletePolicy funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + err = appUsecase.DeletePolicy(context.TODO(), appModel, "env-binding-2") + Expect(err).Should(BeNil()) + }) + It("Test DeleteComponent funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + err = appUsecase.DeleteComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + }) + It("Test Deploy Application funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + res, err := appUsecase.Deploy(context.TODO(), appModel, v1.ApplicationDeployRequest{ + Commit: "unit test deploy", + SourceType: "api", + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(res.Status, model.DeployEventRunning)).Should(BeEmpty()) + + var oam v1beta1.Application + err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: appModel.Name, Namespace: appModel.Namespace}, &oam) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty()) + }) + It("Test DeleteApplication funtion", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + err = appUsecase.DeleteApplication(context.TODO(), appModel) + Expect(err).Should(BeNil()) + components, err := appUsecase.ListComponents(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 0)).Should(BeEmpty()) + policies, err := appUsecase.ListPolicies(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(policies), 0)).Should(BeEmpty()) + }) +}) diff --git a/pkg/apiserver/rest/usecase/testdata/example-app-error.yaml b/pkg/apiserver/rest/usecase/testdata/example-app-error.yaml new file mode 100644 index 000000000..86b2567d3 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/example-app-error.yaml @@ -0,0 +1,78 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: example-app + namespace: default +spec: + components: + - name: hello-world-server + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 1 + - name: data-worker + type: worker + properties: | + image: busybox + cmd: + - sleep + - '1000000' + policies: + - name: example-multi-env-policy + type: env-binding + properties: + envs: + - name: test + placement: # selecting the namespace (in local cluster) to deploy to + namespaceSelector: + name: TEST_NAMESPACE + selector: + components: + - data-worker + + - name: staging + placement: # selecting the cluster to deploy to + clusterSelector: + name: cluster-worker + + - name: prod + placement: # selecting both namespace and cluster to deploy to + clusterSelector: + name: cluster-worker + namespaceSelector: + name: PROD_NAMESPACE + patch: # overlay patch on above components + components: + - name: hello-world-server + type: webservice + traits: + - type: scaler + properties: + replicas: 3 + + workflow: + steps: + # deploy to test env + - name: deploy-test + type: deploy2env + properties: + policy: example-multi-env-policy + env: test + + # deploy to staging env + - name: deploy-staging + type: deploy2env + properties: + policy: example-multi-env-policy + env: staging + + # deploy to prod env + - name: deploy-prod + type: deploy2env + properties: + policy: example-multi-env-policy + env: prod diff --git a/pkg/apiserver/rest/usecase/testdata/example-app.yaml b/pkg/apiserver/rest/usecase/testdata/example-app.yaml new file mode 100644 index 000000000..4e77d8bf3 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/example-app.yaml @@ -0,0 +1,78 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: example-app + namespace: default +spec: + components: + - name: hello-world-server + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 1 + - name: data-worker + type: worker + properties: + image: busybox + cmd: + - sleep + - '1000000' + policies: + - name: example-multi-env-policy + type: env-binding + properties: + envs: + - name: test + placement: # selecting the namespace (in local cluster) to deploy to + namespaceSelector: + name: TEST_NAMESPACE + selector: + components: + - data-worker + + - name: staging + placement: # selecting the cluster to deploy to + clusterSelector: + name: cluster-worker + + - name: prod + placement: # selecting both namespace and cluster to deploy to + clusterSelector: + name: cluster-worker + namespaceSelector: + name: PROD_NAMESPACE + patch: # overlay patch on above components + components: + - name: hello-world-server + type: webservice + traits: + - type: scaler + properties: + replicas: 3 + + workflow: + steps: + # deploy to test env + - name: deploy-test + type: deploy2env + properties: + policy: example-multi-env-policy + env: test + + # deploy to staging env + - name: deploy-staging + type: deploy2env + properties: + policy: example-multi-env-policy + env: staging + + # deploy to prod env + - name: deploy-prod + type: deploy2env + properties: + policy: example-multi-env-policy + env: prod diff --git a/pkg/apiserver/rest/usecase/usecase_suite_test.go b/pkg/apiserver/rest/usecase/usecase_suite_test.go new file mode 100644 index 000000000..a58e48a77 --- /dev/null +++ b/pkg/apiserver/rest/usecase/usecase_suite_test.go @@ -0,0 +1,102 @@ +/* +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" + "fmt" + "math/rand" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "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/datastore/kubeapi" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore/mongodb" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ds datastore.DataStore + +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), + CRDDirectoryPaths: []string{"../../../../charts/vela-core/crds"}, + } + + By("start kube test env") + var err error + cfg, err = testEnv.Start() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + By("new kube client") + cfg.Timeout = time.Minute * 2 + k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) + By("new kube client success") + clients.SetKubeClient(k8sClient) + ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: "kubevela"}) + Expect(err).Should(BeNil()) + Expect(ds).ToNot(BeNil()) + close(done) +}, 240) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +func NewDatastore(cfg datastore.Config) (ds datastore.DataStore, err error) { + switch cfg.Type { + case "mongodb": + ds, err = mongodb.New(context.Background(), cfg) + if err != nil { + return nil, fmt.Errorf("create mongodb datastore instance failure %w", err) + } + case "kubeapi": + ds, err = kubeapi.New(context.Background(), cfg) + if err != nil { + return nil, fmt.Errorf("create mongodb datastore instance failure %w", err) + } + default: + return nil, fmt.Errorf("not support datastore type %s", cfg.Type) + } + return ds, nil +} + +func TestUsecase(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Usecase Suite") +} diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go new file mode 100644 index 000000000..3b4a85c21 --- /dev/null +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -0,0 +1,118 @@ +/* +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" + "errors" + + "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" +) + +// WorkflowUsecase workflow manage api +type WorkflowUsecase interface { + GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) + DeleteWorkflow(ctx context.Context, workflowName string) error + CreateOrUpdateWorkflow(ctx context.Context, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) +} + +// NewWorkflowUsecase new workflow usecase +func NewWorkflowUsecase(ds datastore.DataStore) WorkflowUsecase { + return &workflowUsecaseImpl{ds: ds} +} + +type workflowUsecaseImpl struct { + ds datastore.DataStore +} + +// DeleteWorkflow delete application workflow +func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, workflowName string) error { + var workflow = &model.Workflow{ + Name: workflowName, + } + if err := w.ds.Delete(ctx, workflow); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrWorkflowNotExist + } + return err + } + return nil +} + +func (w *workflowUsecaseImpl) CreateOrUpdateWorkflow(ctx context.Context, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { + var steps []model.WorkflowStep + for _, step := range req.Steps { + properties, err := model.NewJSONStructByString(step.Properties) + if err != nil { + log.Logger.Errorf("parse trait properties failire %w", err) + return nil, bcode.ErrInvalidProperties + } + steps = append(steps, model.WorkflowStep{ + Name: step.Name, + Type: step.Type, + DependsOn: step.DependsOn, + Inputs: step.Inputs, + Outputs: step.Outputs, + Properties: properties, + }) + } + var workflow = model.Workflow{ + Steps: steps, + Name: req.Name, + Namespace: req.Namespace, + Enable: req.Enable, + } + if err := w.ds.Add(ctx, &workflow); err != nil { + return nil, err + } + return w.DetailWorkflow(ctx, &workflow) +} + +// DetailWorkflow detail workflow +func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) { + var steps []apisv1.WorkflowStep + for _, step := range workflow.Steps { + apiStep := apisv1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + DependsOn: step.DependsOn, + Inputs: step.Inputs, + Outputs: step.Outputs, + Properties: step.Properties.JSON(), + } + if step.Properties != nil { + apiStep.Properties = step.Properties.JSON() + } + steps = append(steps, apiStep) + } + return &apisv1.DetailWorkflowResponse{Steps: steps, Enable: workflow.Enable}, nil +} + +// GetWorkflow get workflow model +func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) { + var workflow = model.Workflow{ + Name: workflowName, + } + if err := w.ds.Get(ctx, &workflow); err != nil { + return nil, err + } + return &workflow, nil +} diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index 273e12f3f..2dcb3d142 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -24,3 +24,30 @@ var ErrComponentTypeNotSupport = NewBcode(400, 10001, "An unsupported component // ErrApplicationExist application is exist var ErrApplicationExist = NewBcode(400, 10002, "application name is exist") + +// ErrInvalidProperties properties(trait or component or others) is invalid +var ErrInvalidProperties = NewBcode(400, 10003, "properties is invalid") + +// ErrDeployEventConflict Occurs when a new event is triggered before the last deployment event has completed. +var ErrDeployEventConflict = NewBcode(400, 10004, "application deploy event conflict") + +// ErrDeployApplyFail Failed to update an application to the control cluster. +var ErrDeployApplyFail = NewBcode(500, 10005, "application deploy apply failure") + +// ErrNoComponent no component +var ErrNoComponent = NewBcode(200, 10006, "application not have components, can not deploy") + +// ErrApplicationComponetExist application component is exist +var ErrApplicationComponetExist = NewBcode(400, 10007, "application component is exist") + +// ErrApplicationComponetNotExist application component is not exist +var ErrApplicationComponetNotExist = NewBcode(404, 10008, "application component is not exist") + +// ErrApplicationPolicyExist application policy is exist +var ErrApplicationPolicyExist = NewBcode(400, 10009, "application policy is exist") + +// ErrApplicationPolicyNotExist application policy is not exist +var ErrApplicationPolicyNotExist = NewBcode(404, 10010, "application policy is not exist") + +// ErrCreateNamespace auto create namespace failure before deploy app +var ErrCreateNamespace = NewBcode(500, 10011, "auto create namespace failure") diff --git a/pkg/apiserver/rest/utils/bcode/bcode.go b/pkg/apiserver/rest/utils/bcode/bcode.go index 3fab7ac7d..0454779bf 100644 --- a/pkg/apiserver/rest/utils/bcode/bcode.go +++ b/pkg/apiserver/rest/utils/bcode/bcode.go @@ -23,9 +23,13 @@ import ( restful "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/log" ) +// ErrServer an unexpected mistake. +var ErrServer = NewBcode(500, 500, "The service has lapsed.") + // Bcode business error code type Bcode struct { HTTPCode int32 `json:"-"` @@ -61,6 +65,13 @@ func ReturnError(req *restful.Request, res *restful.Response, err error) { } return } + + if errors.Is(err, datastore.ErrRecordNotExist) { + if err := res.WriteHeaderAndEntity(int(404), 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.WriteHeaderAndEntity(restfulerr.Code, Bcode{HTTPCode: int32(restfulerr.Code), BusinessCode: int32(restfulerr.Code), Message: restfulerr.Message}); err != nil { diff --git a/pkg/apiserver/rest/utils/bcode/bcode_suite_test.go b/pkg/apiserver/rest/utils/bcode/bcode_suite_test.go new file mode 100644 index 000000000..fcbf6c8e2 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/bcode_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 bcode + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestBcode(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Bcode Suite") +} diff --git a/pkg/apiserver/rest/utils/bcode/bcode_test.go b/pkg/apiserver/rest/utils/bcode/bcode_test.go new file mode 100644 index 000000000..7ef5e62c7 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/bcode_test.go @@ -0,0 +1,31 @@ +/* +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 + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test bcode package", func() { + It("Test New bcode funtion", func() { + bcode := NewBcode(400, 4000, "test") + Expect(bcode).ShouldNot(BeNil()) + Expect(bcode.Message).ShouldNot(BeNil()) + Expect(bcode.Error()).ShouldNot(BeNil()) + }) +}) diff --git a/pkg/apiserver/rest/utils/bcode/workflow.go b/pkg/apiserver/rest/utils/bcode/workflow.go new file mode 100644 index 000000000..07b30dfb5 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/workflow.go @@ -0,0 +1,23 @@ +/* +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 + +// ErrWorkflowNotExist application workflow is not exist +var ErrWorkflowNotExist = NewBcode(404, 20002, "application workflow is not exist") + +// ErrWorkflowExist application workflow is exist +var ErrWorkflowExist = NewBcode(404, 20003, "application workflow is exist") diff --git a/pkg/apiserver/rest/utils/utils_suite_test.go b/pkg/apiserver/rest/utils/utils_suite_test.go new file mode 100644 index 000000000..a75298da7 --- /dev/null +++ b/pkg/apiserver/rest/utils/utils_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 utils + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestUtils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} diff --git a/pkg/apiserver/rest/utils/version.go b/pkg/apiserver/rest/utils/version.go new file mode 100644 index 000000000..153eae651 --- /dev/null +++ b/pkg/apiserver/rest/utils/version.go @@ -0,0 +1,34 @@ +/* +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 utils + +import ( + "fmt" + "time" + + "cuelang.org/go/pkg/strings" +) + +// GenerateVersion Generate version numbers by time +func GenerateVersion(pre string) string { + timeStr := time.Now().Format("20060102150405.000") + timeStr = strings.Replace(timeStr, ".", "", 1) + if pre != "" { + return fmt.Sprintf("%s-%s", pre, timeStr) + } + return timeStr +} diff --git a/pkg/apiserver/rest/utils/version_test.go b/pkg/apiserver/rest/utils/version_test.go new file mode 100644 index 000000000..18f38e6a7 --- /dev/null +++ b/pkg/apiserver/rest/utils/version_test.go @@ -0,0 +1,35 @@ +/* +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 utils + +import ( + "strings" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test version utils", func() { + It("Test New version funtion", func() { + s := GenerateVersion("") + Expect(s).ShouldNot(BeNil()) + + s2 := GenerateVersion("pre") + Expect(cmp.Diff(strings.HasPrefix(s2, "pre-"), true)).ShouldNot(BeNil()) + }) +}) diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 3ae3f2f22..f848749ea 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -17,9 +17,13 @@ limitations under the License. package webservice import ( + "context" + restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" 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" @@ -51,76 +55,146 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). Param(ws.QueryParameter("namespace", "Namespace-based search").DataType("string")). Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")). + Returns(200, "", apis.ListApplicationResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ListApplicationResponse{})) ws.Route(ws.POST("/").To(c.createApplication). Doc("create one application"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateApplicationRequest{}). + Returns(200, "", apis.ApplicationBase{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationBase{})) - ws.Route(ws.DELETE("/{name}").To(noop). + ws.Route(ws.DELETE("/{name}").To(c.deleteApplication). Doc("delete one application"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). - Writes(apis.ApplicationBase{})) + Returns(200, "", apis.EmptyResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}").To(noop). + ws.Route(ws.GET("/{name}").To(c.detailApplication). Doc("detail one application"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Returns(200, "", apis.DetailApplicationResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.DetailApplicationResponse{})) - ws.Route(ws.POST("/{name}/template").To(noop). + ws.Route(ws.POST("/{name}/template").To(c.publishApplicationTemplate). Doc("create one application template"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Reads(apis.CreateApplicationTemplateRequest{}). + Returns(200, "", apis.ApplicationTemplateBase{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationTemplateBase{})) - ws.Route(ws.POST("/{name}/deploy").To(noop). - Doc("deploy or update the application"). + ws.Route(ws.POST("/{name}/deploy").To(c.deployApplication). + Doc("deploy or upgrade the application"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). - Writes(apis.ApplicationBase{})) + Returns(200, "", apis.ApplicationDeployRequest{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationDeployResponse{})) - ws.Route(ws.GET("/{name}/components").To(noop). + ws.Route(ws.GET("/{name}/components").To(c.listApplicationComponents). Doc("gets the component topology of the application"). + Filter(c.appCheckFilter). 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). + Returns(200, "", apis.ComponentListResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ComponentListResponse{})) - ws.Route(ws.POST("/{name}/components").To(noop). + ws.Route(ws.POST("/{name}/components").To(c.createComponent). Doc("create component for application"). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateComponentRequest{}). + Returns(200, "", apis.ComponentBase{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ComponentBase{})) - ws.Route(ws.POST("/{name}/policies").To(noop). + ws.Route(ws.GET("/{name}/components/{componentName}").To(c.detailComponent). + Doc("detail component for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.DetailComponentResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailComponentResponse{})) + + ws.Route(ws.GET("/{name}/policies").To(c.listApplicationPolicies). + Doc("list policy for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListApplicationPolicy{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListApplicationPolicy{})) + + ws.Route(ws.POST("/{name}/policies").To(c.createApplicationPolicy). Doc("create policy for application"). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreatePolicyRequest{}). - Writes(apis.DetailPolicyResponse{})) + Returns(200, "", apis.PolicyBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.PolicyBase{})) - ws.Route(ws.GET("/{name}/policies/{policyName}").To(noop). + ws.Route(ws.GET("/{name}/policies/{policyName}").To(c.detailApplicationPolicy). Doc("detail policy for application"). + Filter(c.appCheckFilter). 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). + Returns(200, "", apis.DetailPolicyResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.DetailPolicyResponse{})) - ws.Route(ws.DELETE("/{name}/policies/{policyName}").To(noop). + ws.Route(ws.DELETE("/{name}/policies/{policyName}").To(c.deleteApplicationPolicy). Doc("detail policy for application"). + Filter(c.appCheckFilter). 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). + Returns(200, "", apis.EmptyResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.PUT("/{name}/policies/{policyName}").To(c.updateApplicationPolicy). + Doc("update policy for application"). + Filter(c.appCheckFilter). + 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). + Reads(apis.UpdatePolicyRequest{}). + Returns(200, "", apis.DetailPolicyResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.DetailPolicyResponse{})) return ws } +func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app, err := c.applicationUsecase.GetApplication(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplication, app)) + chain.ProcessFilter(req, res) +} + func (c *applicationWebService) createApplication(req *restful.Request, res *restful.Response) { // Verify the validity of parameters var createReq apis.CreateApplicationRequest @@ -135,6 +209,7 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res // Call the usecase layer code appBase, err := c.applicationUsecase.CreateApplication(req.Request.Context(), createReq) if err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) bcode.ReturnError(req, res, err) return } @@ -147,5 +222,210 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res } func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { - + apps, err := c.applicationUsecase.ListApplications(req.Request.Context()) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListApplicationResponse{Applications: apps}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) detailApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.DetailApplication(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) publishApplicationTemplate(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + base, err := c.applicationUsecase.PublishApplicationTemplate(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +// deployApplication TODO: return event model +func (c *applicationWebService) deployApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var createReq apis.ApplicationDeployRequest + 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 + } + deployRes, err := c.applicationUsecase.Deploy(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(deployRes); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) deleteApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.applicationUsecase.DeleteApplication(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplicationComponents(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + components, err := c.applicationUsecase.ListComponents(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ComponentListResponse{Components: components}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) createComponent(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var createReq apis.CreateComponentRequest + 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 + } + base, err := c.applicationUsecase.AddComponent(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) detailComponent(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.DetailComponent(req.Request.Context(), app, req.PathParameter("componentName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) createApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var createReq apis.CreatePolicyRequest + 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 + } + base, err := c.applicationUsecase.AddPolicy(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplicationPolicies(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + policies, err := c.applicationUsecase.ListPolicies(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListApplicationPolicy{Policies: policies}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) detailApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.DetailPolicy(req.Request.Context(), app, req.PathParameter("policyName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) deleteApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.applicationUsecase.DeletePolicy(req.Request.Context(), app, req.PathParameter("policyName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) updateApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var updateReq apis.UpdatePolicyRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + response, err := c.applicationUsecase.UpdatePolicy(req.Request.Context(), app, req.PathParameter("policyName"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(response); err != nil { + bcode.ReturnError(req, res, err) + return + } } diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 86b11edaa..65bec77f1 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -60,7 +60,8 @@ func returns500(b *restful.RouteBuilder) { // 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) + workflowUsecase := usecase.NewWorkflowUsecase(ds) + applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase) RegistWebService(NewClusterWebService(clusterUsecase)) RegistWebService(NewApplicationWebService(applicationUsecase)) RegistWebService(&namespaceWebService{}) @@ -68,5 +69,5 @@ func Init(ctx context.Context, ds datastore.DataStore) { RegistWebService(&addonWebService{}) RegistWebService(&oamApplicationWebService{}) RegistWebService(&policyDefinitionWebservice{}) - RegistWebService(&workflowWebService{}) + RegistWebService(NewWorkflowWebService(workflowUsecase, applicationUsecase)) } diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 9fef7662b..05730ccaf 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -21,9 +21,20 @@ 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" ) +// NewWorkflowWebService new workflow webservice +func NewWorkflowWebService(workflowUsecase usecase.WorkflowUsecase, applicationUsecase usecase.ApplicationUsecase) WebService { + return &workflowWebService{ + workflowUsecase: workflowUsecase, + applicationUsecase: applicationUsecase, + } +} + type workflowWebService struct { + workflowUsecase usecase.WorkflowUsecase + applicationUsecase usecase.ApplicationUsecase } func (c *workflowWebService) GetWebService() *restful.WebService { diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go index f16150d61..dc1c271b8 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go @@ -307,7 +307,7 @@ func getVersioningPeerWorkloadRefs(ctx context.Context, c client.Reader, wlRef c compName := getComponentNameFromLabel(o) appName := getAppConfigNameFromLabel(o) if compName == "" || appName == "" { - // if missing these lables, cannot get peer workloads + // if missing these labels, cannot get peer workloads return nil, nil } diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index adb82a2d1..c6d67393b 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -18,13 +18,18 @@ package e2e_apiserver_test import ( "bytes" + "context" "encoding/json" + "io/ioutil" "net/http" "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" ) @@ -52,6 +57,250 @@ var _ = Describe("Test application rest api", func() { 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.Namespace, req.Namespace)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Labels["test"], req.Labels["test"])).Should(BeEmpty()) }) + + It("Test delete app", func() { + defer GinkgoRecover() + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd", nil) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + }) + + It("Test create app with oamspec", func() { + defer GinkgoRecover() + bs, err := ioutil.ReadFile("./testdata/example-app.yaml") + Expect(err).Should(Succeed()) + 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"}, + YamlConfig: string(bs), + } + 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.Namespace, req.Namespace)).Should(BeEmpty()) + Expect(cmp.Diff(appBase.Labels["test"], req.Labels["test"])).Should(BeEmpty()) + }) + + It("Test list components", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components") + 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 components apisv1.ComponentListResponse + err = json.NewDecoder(res.Body).Decode(&components) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(len(components.Components), 2)).Should(BeEmpty()) + }) + + It("Test list policies", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies") + 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 policies apisv1.ListApplicationPolicy + err = json.NewDecoder(res.Body).Decode(&policies) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(len(policies.Policies), 1)).Should(BeEmpty()) + }) + + It("Test get workflow", func() { + // defer GinkgoRecover() + // res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies") + // 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 policies apisv1.ListApplicationPolicy + // err = json.NewDecoder(res.Body).Decode(&policies) + // Expect(err).ShouldNot(HaveOccurred()) + // Expect(cmp.Diff(len(policies.Policies), 1)).Should(BeEmpty()) + }) + + It("Test detail application", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd") + 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 detail apisv1.DetailApplicationResponse + err = json.NewDecoder(res.Body).Decode(&detail) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) + }) + + It("Test deploy application", func() { + defer GinkgoRecover() + var req = apisv1.ApplicationDeployRequest{ + Commit: "test apply", + SourceType: "web", + Force: false, + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/deploy", "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 response apisv1.ApplicationDeployResponse + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Status, model.DeployEventRunning)).Should(BeEmpty()) + + var oam v1beta1.Application + err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: "test-app-sadasd", Namespace: "test-app-namesapce"}, &oam) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty()) + }) + + It("Test create component", func() { + defer GinkgoRecover() + var req = apisv1.CreateComponentRequest{ + Name: "test2", + Description: "this is a test2 component", + Labels: map[string]string{}, + ComponentType: "worker", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + DependsOn: []string{"data-worker"}, + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components", "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 response apisv1.ComponentBase + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.ComponentType, "worker")).Should(BeEmpty()) + }) + + It("Test detail component", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2") + 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 response apisv1.DetailComponentResponse + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(len(response.DependsOn), 1)).Should(BeEmpty()) + }) + + It("Test create application policy", func() { + defer GinkgoRecover() + var req = apisv1.CreatePolicyRequest{ + Name: "test2", + Description: "this is a test2 component", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 400)).Should(BeEmpty()) + var req2 = apisv1.CreatePolicyRequest{ + Name: "test2", + Description: "this is a test2 policy", + Type: "wqsdasd", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + } + bodyByte2, err := json.Marshal(req2) + Expect(err).ShouldNot(HaveOccurred()) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte2)) + 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 response apisv1.PolicyBase + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Type, "wqsdasd")).Should(BeEmpty()) + }) + + It("Test detail application policy", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2") + 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 response apisv1.DetailPolicyResponse + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Description, "this is a test2 policy")).Should(BeEmpty()) + }) + + It("Test update application policy", func() { + var req2 = apisv1.UpdatePolicyRequest{ + Description: "this is a test2 policy update", + Type: "wqsdasd", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + } + bodyByte2, err := json.Marshal(req2) + Expect(err).ShouldNot(HaveOccurred()) + req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2", bytes.NewBuffer(bodyByte2)) + Expect(err).ShouldNot(HaveOccurred()) + req.Header.Set("Content-Type", "application/json") + res, err := http.DefaultClient.Do(req) + 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 response apisv1.PolicyBase + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Description, "this is a test2 policy update")).Should(BeEmpty()) + }) + + It("Test delete application policy", func() { + defer GinkgoRecover() + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2", nil) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + }) + }) diff --git a/test/e2e-apiserver-test/suite_test.go b/test/e2e-apiserver-test/suite_test.go index dfe95b2bb..bcc14d355 100644 --- a/test/e2e-apiserver-test/suite_test.go +++ b/test/e2e-apiserver-test/suite_test.go @@ -23,8 +23,6 @@ import ( . "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" @@ -33,12 +31,12 @@ import ( "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" + "github.com/oam-dev/kubevela/pkg/utils/common" ) var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment -var testScheme = runtime.NewScheme() func TestE2eApiserverTest(t *testing.T) { RegisterFailHandler(Fail) @@ -53,6 +51,7 @@ var _ = BeforeSuite(func() { ControlPlaneStartTimeout: time.Minute * 3, ControlPlaneStopTimeout: time.Minute, UseExistingCluster: pointer.BoolPtr(false), + CRDDirectoryPaths: []string{"../../charts/vela-core/crds"}, } By("start kube test env") @@ -61,12 +60,9 @@ var _ = BeforeSuite(func() { 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}) + k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) Expect(err).Should(BeNil()) Expect(k8sClient).ToNot(BeNil()) By("new kube client success") diff --git a/test/e2e-apiserver-test/testdata/example-app.yaml b/test/e2e-apiserver-test/testdata/example-app.yaml new file mode 100644 index 000000000..4e77d8bf3 --- /dev/null +++ b/test/e2e-apiserver-test/testdata/example-app.yaml @@ -0,0 +1,78 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: example-app + namespace: default +spec: + components: + - name: hello-world-server + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 1 + - name: data-worker + type: worker + properties: + image: busybox + cmd: + - sleep + - '1000000' + policies: + - name: example-multi-env-policy + type: env-binding + properties: + envs: + - name: test + placement: # selecting the namespace (in local cluster) to deploy to + namespaceSelector: + name: TEST_NAMESPACE + selector: + components: + - data-worker + + - name: staging + placement: # selecting the cluster to deploy to + clusterSelector: + name: cluster-worker + + - name: prod + placement: # selecting both namespace and cluster to deploy to + clusterSelector: + name: cluster-worker + namespaceSelector: + name: PROD_NAMESPACE + patch: # overlay patch on above components + components: + - name: hello-world-server + type: webservice + traits: + - type: scaler + properties: + replicas: 3 + + workflow: + steps: + # deploy to test env + - name: deploy-test + type: deploy2env + properties: + policy: example-multi-env-policy + env: test + + # deploy to staging env + - name: deploy-staging + type: deploy2env + properties: + policy: example-multi-env-policy + env: staging + + # deploy to prod env + - name: deploy-prod + type: deploy2env + properties: + policy: example-multi-env-policy + env: prod From c7f9cdcbbf6f37bef0dae9ad846bdd7cd73fe9af Mon Sep 17 00:00:00 2001 From: yangsoon Date: Mon, 18 Oct 2021 22:06:51 +0800 Subject: [PATCH 02/59] Feat: add the oam application api in apiserver (#2492) * Feat: oam application api in apiserver * Feat: enable unit-test and e2e-test --- .github/workflows/apiserver-test.yaml | 2 + .../rest/usecase/application_test.go | 34 ++--- pkg/apiserver/rest/usecase/oam_application.go | 109 ++++++++++++++++ .../rest/usecase/oam_application_test.go | 120 ++++++++++++++++++ .../rest/usecase/usecase_suite_test.go | 5 + .../rest/webservice/oam_application.go | 77 ++++++++++- pkg/apiserver/rest/webservice/webservice.go | 3 +- .../oam_application_test.go | 120 ++++++++++++++++++ 8 files changed, 448 insertions(+), 22 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/oam_application.go create mode 100644 pkg/apiserver/rest/usecase/oam_application_test.go create mode 100644 test/e2e-apiserver-test/oam_application_test.go diff --git a/.github/workflows/apiserver-test.yaml b/.github/workflows/apiserver-test.yaml index 2200be360..75dc769b6 100644 --- a/.github/workflows/apiserver-test.yaml +++ b/.github/workflows/apiserver-test.yaml @@ -5,11 +5,13 @@ on: branches: - master - release-* + - apiserver workflow_dispatch: {} pull_request: branches: - master - release-* + - apiserver env: # Common versions diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 72cdee439..94f9285f3 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -48,7 +48,7 @@ var _ = Describe("Test application usecase function", func() { kubeClient: k8sClient, } }) - It("Test CreateApplication funtion", func() { + It("Test CreateApplication function", func() { By("test sample create") req := v1.CreateApplicationRequest{ Name: "test-app", @@ -106,13 +106,13 @@ var _ = Describe("Test application usecase function", func() { Expect(equal).Should(BeTrue()) }) - It("Test ListApplications funtion", func() { + It("Test ListApplications function", func() { apps, err := appUsecase.ListApplications(context.TODO()) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(apps), 2)).Should(BeEmpty()) }) - It("Test DetailApplication funtion", func() { + It("Test DetailApplication function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -123,7 +123,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) }) - It("Test GetWorkflow funtion", func() { + It("Test GetWorkflow function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -134,7 +134,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(detail.Enable, true)).Should(BeEmpty()) }) - It("Test ListPolicies funtion", func() { + It("Test ListPolicies function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -146,7 +146,7 @@ var _ = Describe("Test application usecase function", func() { Expect((*policies[0].Properties)["envs"]).ShouldNot(BeEmpty()) }) - It("Test ListComponents funtion", func() { + It("Test ListComponents function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -158,7 +158,7 @@ var _ = Describe("Test application usecase function", func() { Expect(components[1].UpdateTime).ShouldNot(BeNil()) }) - It("Test DetailComponent funtion", func() { + It("Test DetailComponent function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -170,7 +170,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(strings.Contains((*detail.Properties)["image"].(string), "crccheck/hello-world"), true)).Should(BeEmpty()) }) - It("Test DetailPolicy funtion", func() { + It("Test DetailPolicy function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -180,7 +180,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(detail.Type, "env-binding")).Should(BeEmpty()) Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) }) - It("Test AddComponent funtion", func() { + It("Test AddComponent function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -195,7 +195,7 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(cmp.Diff(base.ComponentType, "worker")).Should(BeEmpty()) }) - It("Test DetailComponent funtion", func() { + It("Test DetailComponent function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -206,7 +206,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff((*detailResponse.Properties)["image"], "busybox")).Should(BeEmpty()) }) - It("Test AddPolicy funtion", func() { + It("Test AddPolicy function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -225,7 +225,7 @@ var _ = Describe("Test application usecase function", func() { }) Expect(err).Should(BeNil()) }) - It("Test DetailPolicy funtion", func() { + It("Test DetailPolicy function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -234,7 +234,7 @@ var _ = Describe("Test application usecase function", func() { Expect(detail.Properties).ShouldNot(BeNil()) Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) }) - It("Test UpdatePolicy funtion", func() { + It("Test UpdatePolicy function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -246,21 +246,21 @@ var _ = Describe("Test application usecase function", func() { Expect(base.Properties).ShouldNot(BeNil()) Expect((*base.Properties)["envs"]).Should(BeEmpty()) }) - It("Test DeletePolicy funtion", func() { + It("Test DeletePolicy function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) err = appUsecase.DeletePolicy(context.TODO(), appModel, "env-binding-2") Expect(err).Should(BeNil()) }) - It("Test DeleteComponent funtion", func() { + It("Test DeleteComponent function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) err = appUsecase.DeleteComponent(context.TODO(), appModel, "test2") Expect(err).Should(BeNil()) }) - It("Test Deploy Application funtion", func() { + It("Test Deploy Application function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -277,7 +277,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty()) Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty()) }) - It("Test DeleteApplication funtion", func() { + It("Test DeleteApplication function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) err = appUsecase.DeleteApplication(context.TODO(), appModel) diff --git a/pkg/apiserver/rest/usecase/oam_application.go b/pkg/apiserver/rest/usecase/oam_application.go new file mode 100644 index 000000000..6005c9f3d --- /dev/null +++ b/pkg/apiserver/rest/usecase/oam_application.go @@ -0,0 +1,109 @@ +/* + 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" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +// OAMApplicationUsecase oam_application usecase +type OAMApplicationUsecase interface { + CreateOrUpdateOAMApplication(context.Context, apisv1.ApplicationRequest, string, string) error + GetOAMApplication(context.Context, string, string) (*apisv1.ApplicationResponse, error) + DeleteOAMApplication(context.Context, string, string) error +} + +// NewOAMApplicationUsecase new oam_application usecase +func NewOAMApplicationUsecase() OAMApplicationUsecase { + kubeClient, _ := clients.GetKubeClient() + return &oamApplicationUsecaseImpl{kubeClient: kubeClient} +} + +type oamApplicationUsecaseImpl struct { + kubeClient client.Client +} + +// CreateOrUpdateOAMApplication create or update application +func (o oamApplicationUsecaseImpl) CreateOrUpdateOAMApplication(ctx context.Context, request apisv1.ApplicationRequest, name, namespace string) error { + ns := new(v1.Namespace) + err := o.kubeClient.Get(ctx, client.ObjectKey{Name: namespace}, ns) + if kerrors.IsNotFound(err) { + ns.Name = namespace + if err = o.kubeClient.Create(ctx, ns); err != nil { + return err + } + } + + app := &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: request.Components, + Policies: request.Policies, + Workflow: request.Workflow, + }, + } + + existApp := new(v1beta1.Application) + err = o.kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, existApp) + if err != nil { + if kerrors.IsNotFound(err) { + return o.kubeClient.Create(ctx, app) + } + return err + } + + existApp.Spec = app.Spec + return o.kubeClient.Update(ctx, existApp) +} + +// GetOAMApplication get application +func (o oamApplicationUsecaseImpl) GetOAMApplication(ctx context.Context, name, namespace string) (*apisv1.ApplicationResponse, error) { + app := new(v1beta1.Application) + if err := o.kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, app); err != nil { + return nil, err + } + return &apisv1.ApplicationResponse{ + APIVersion: app.APIVersion, + Kind: app.Kind, + Spec: app.Spec, + Status: app.Status, + }, nil +} + +// DeleteOAMApplication delete application +func (o oamApplicationUsecaseImpl) DeleteOAMApplication(ctx context.Context, name, namespace string) error { + return client.IgnoreNotFound(o.kubeClient.Delete(ctx, &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + })) +} diff --git a/pkg/apiserver/rest/usecase/oam_application_test.go b/pkg/apiserver/rest/usecase/oam_application_test.go new file mode 100644 index 000000000..a8fff0087 --- /dev/null +++ b/pkg/apiserver/rest/usecase/oam_application_test.go @@ -0,0 +1,120 @@ +/* + 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" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + apiv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var _ = Describe("Test oam application usecase function", func() { + var oamAppUsecase *oamApplicationUsecaseImpl + var ctx context.Context + var baseApp v1beta1.Application + var ns corev1.Namespace + var namespace string + + BeforeEach(func() { + ctx = context.Background() + namespace = randomNamespaceName("test-oam-app") + ns = corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + oamAppUsecase = &oamApplicationUsecaseImpl{ + kubeClient: k8sClient, + } + Expect(common.ReadYamlToObject("./testdata/example-app.yaml", &baseApp)).Should(BeNil()) + + Eventually(func() error { + return k8sClient.Create(ctx, &ns) + }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + baseApp.SetNamespace(namespace) + Eventually(func() error { + return k8sClient.Create(ctx, &baseApp) + }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + }) + + AfterEach(func() { + By("Clean up resources after a test") + k8sClient.DeleteAllOf(ctx, &v1beta1.Application{}, client.InNamespace(namespace)) + baseApp = v1beta1.Application{} + By(fmt.Sprintf("Delete the entire namespaceName %s", ns.Name)) + Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed()) + }) + + It("Test CreateOrUpdateOAMApplication function", func() { + By("test create application") + appName := "test-new-app" + appNs := randomNamespaceName("test-new-app") + req := apiv1.ApplicationRequest{ + Components: baseApp.Spec.Components, + Policies: baseApp.Spec.Policies, + Workflow: baseApp.Spec.Workflow, + } + Expect(oamAppUsecase.CreateOrUpdateOAMApplication(ctx, req, appName, appNs)).Should(BeNil()) + + app := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: appNs, Name: appName}, app)).Should(BeNil()) + Expect(app.Spec.Components).Should(Equal(req.Components)) + Expect(app.Spec.Policies).Should(Equal(req.Policies)) + Expect(app.Spec.Workflow).Should(Equal(req.Workflow)) + + By("test update application") + updateReq := apiv1.ApplicationRequest{ + Components: baseApp.Spec.Components[1:], + } + Expect(oamAppUsecase.CreateOrUpdateOAMApplication(ctx, updateReq, appName, appNs)).Should(BeNil()) + + updatedApp := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: appNs, Name: appName}, updatedApp)).Should(BeNil()) + Expect(updatedApp.Spec.Components).Should(Equal(updateReq.Components)) + Expect(updatedApp.Spec.Policies).Should(BeNil()) + Expect(updatedApp.Spec.Workflow).Should(BeNil()) + }) + + It("Test GetOAMApplication function", func() { + By("test get an existed application") + resp, err := oamAppUsecase.GetOAMApplication(ctx, baseApp.Name, namespace) + Expect(err).Should(BeNil()) + + Expect(resp.Spec.Components).Should(Equal(baseApp.Spec.Components)) + Expect(resp.Spec.Policies).Should(Equal(baseApp.Spec.Policies)) + Expect(resp.Spec.Workflow).Should(Equal(baseApp.Spec.Workflow)) + }) + + It("Test DeleteOAMApplication function", func() { + By("test delete application") + app := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: baseApp.Name}, app)).Should(BeNil()) + + Expect(oamAppUsecase.DeleteOAMApplication(ctx, baseApp.Name, namespace)).Should(BeNil()) + err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: baseApp.Name}, app) + Expect(kerrors.IsNotFound(err)).Should(BeTrue()) + }) +}) diff --git a/pkg/apiserver/rest/usecase/usecase_suite_test.go b/pkg/apiserver/rest/usecase/usecase_suite_test.go index a58e48a77..e77f33b77 100644 --- a/pkg/apiserver/rest/usecase/usecase_suite_test.go +++ b/pkg/apiserver/rest/usecase/usecase_suite_test.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "math/rand" + "strconv" "testing" "time" @@ -100,3 +101,7 @@ func TestUsecase(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Usecase Suite") } + +func randomNamespaceName(basic string) string { + return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16)) +} diff --git a/pkg/apiserver/rest/webservice/oam_application.go b/pkg/apiserver/rest/webservice/oam_application.go index 7e19b3648..55725ae3a 100644 --- a/pkg/apiserver/rest/webservice/oam_application.go +++ b/pkg/apiserver/rest/webservice/oam_application.go @@ -18,12 +18,23 @@ package webservice import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" - restful "github.com/emicklei/go-restful/v3" + "github.com/emicklei/go-restful/v3" + "github.com/oam-dev/kubevela/pkg/apiserver/log" 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 oamApplicationWebService struct { + oamApplicationUsecase usecase.OAMApplicationUsecase +} + +// NewOAMApplication new oam application +func NewOAMApplication(oamApplicationUsecase usecase.OAMApplicationUsecase) WebService { + return &oamApplicationWebService{ + oamApplicationUsecase: oamApplicationUsecase, + } } func (c *oamApplicationWebService) GetWebService() *restful.WebService { @@ -35,24 +46,82 @@ func (c *oamApplicationWebService) GetWebService() *restful.WebService { tags := []string{"oam"} - ws.Route(ws.GET("/{namespace}/applications/:appname").To(noop). + ws.Route(ws.GET("/namespaces/{namespace}/applications/{appname}").To(c.getApplication). Doc("get the specified oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). Writes(apis.ApplicationResponse{})) - ws.Route(ws.POST("/{namespace}/applications/{appname}").To(noop). + ws.Route(ws.POST("/namespaces/{namespace}/applications/{appname}").To(c.createOrUpdateApplication). Doc("create or update oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). Reads(apis.ApplicationRequest{})) - ws.Route(ws.DELETE("/{namespace}/applications/:appname").To(noop). + ws.Route(ws.DELETE("/namespaces/{namespace}/applications/{appname}").To(c.deleteApplication). Doc("create or update oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string"))) + return ws } + +func (c *oamApplicationWebService) getApplication(req *restful.Request, res *restful.Response) { + namespace := req.PathParameter("namespace") + appName := req.PathParameter("appname") + appRes, err := c.oamApplicationUsecase.GetOAMApplication(req.Request.Context(), appName, namespace) + if err != nil { + log.Logger.Errorf("get application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(appRes); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *oamApplicationWebService) createOrUpdateApplication(req *restful.Request, res *restful.Response) { + namespace := req.PathParameter("namespace") + appName := req.PathParameter("appname") + + var createReq apis.ApplicationRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + err := c.oamApplicationUsecase.CreateOrUpdateOAMApplication(req.Request.Context(), createReq, appName, namespace) + if err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *oamApplicationWebService) deleteApplication(req *restful.Request, res *restful.Response) { + namespace := req.PathParameter("namespace") + appName := req.PathParameter("appname") + + err := c.oamApplicationUsecase.DeleteOAMApplication(req.Request.Context(), appName, namespace) + if err != nil { + log.Logger.Errorf("delete application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 65bec77f1..8681550bf 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -62,12 +62,13 @@ func Init(ctx context.Context, ds datastore.DataStore) { clusterUsecase := usecase.NewClusterUsecase(ds) workflowUsecase := usecase.NewWorkflowUsecase(ds) applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase) + oamApplicationUsecase := usecase.NewOAMApplicationUsecase() RegistWebService(NewClusterWebService(clusterUsecase)) RegistWebService(NewApplicationWebService(applicationUsecase)) RegistWebService(&namespaceWebService{}) RegistWebService(&componentDefinitionWebservice{}) RegistWebService(&addonWebService{}) - RegistWebService(&oamApplicationWebService{}) + RegistWebService(NewOAMApplication(oamApplicationUsecase)) RegistWebService(&policyDefinitionWebservice{}) RegistWebService(NewWorkflowWebService(workflowUsecase, applicationUsecase)) } diff --git a/test/e2e-apiserver-test/oam_application_test.go b/test/e2e-apiserver-test/oam_application_test.go new file mode 100644 index 000000000..4048541df --- /dev/null +++ b/test/e2e-apiserver-test/oam_application_test.go @@ -0,0 +1,120 @@ +/* + 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" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + apiv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var _ = Describe("Test oam application rest api", func() { + namespace := "test-oam-app" + appName := "example-app" + var app v1beta1.Application + + It("Test create and update oam app", func() { + defer GinkgoRecover() + By("test create app") + + Expect(common.ReadYamlToObject("./testdata/example-app.yaml", &app)).Should(BeNil()) + req := apiv1.ApplicationRequest{ + Components: app.Spec.Components, + Policies: app.Spec.Policies, + Workflow: app.Spec.Workflow, + } + bodyByte, err := json.Marshal(req) + Expect(err).Should(BeNil()) + res, err := http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + "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() + + ctx := context.Background() + oldApp := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: appName, Namespace: namespace}, oldApp)).Should(BeNil()) + Expect(oldApp.Spec.Components).Should(Equal(req.Components)) + Expect(oldApp.Spec.Policies).Should(Equal(req.Policies)) + Expect(oldApp.Spec.Workflow).Should(Equal(req.Workflow)) + + By("test update app") + updateReq := apiv1.ApplicationRequest{ + Components: app.Spec.Components[1:], + } + bodyByte, err = json.Marshal(updateReq) + Expect(err).Should(BeNil()) + res, err = http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + "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() + + newApp := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: appName, Namespace: namespace}, newApp)).Should(BeNil()) + Expect(newApp.Spec.Components).Should(Equal(updateReq.Components)) + Expect(newApp.Spec.Policies).Should(BeNil()) + Expect(newApp.Spec.Workflow).Should(BeNil()) + }) + + It("Test get oam app", func() { + defer GinkgoRecover() + res, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + ) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + + defer res.Body.Close() + var appResp apiv1.ApplicationResponse + err = json.NewDecoder(res.Body).Decode(&appResp) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(len(appResp.Spec.Components)).Should(Equal(1)) + }) + + It("Test delete oam app", func() { + defer GinkgoRecover() + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), nil) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + }) +}) From b85dda35f3e4b1d8b7fa0b8be7034f2edc6d2ce9 Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Tue, 19 Oct 2021 10:03:14 +0800 Subject: [PATCH 03/59] Feat: add namesapce create and list api (#2514) Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 372 +++++++++--------- pkg/apiserver/datastore/kubeapi/kubeapi.go | 2 +- .../datastore/kubeapi/kubeapi_test.go | 8 +- .../datastore/mongodb/mongodb_test.go | 8 +- pkg/apiserver/rest/apis/v1/types.go | 15 +- pkg/apiserver/rest/usecase/application.go | 7 +- pkg/apiserver/rest/usecase/namespace.go | 106 +++++ pkg/apiserver/rest/usecase/namespace_test.go | 50 +++ pkg/apiserver/rest/utils/bcode/namespace.go | 23 ++ pkg/apiserver/rest/webservice/namespace.go | 82 ++-- .../rest/webservice/validate_test.go | 8 +- pkg/apiserver/rest/webservice/webservice.go | 3 +- .../v1alpha2/application/dispatch/dispatch.go | 2 +- .../dispatch/dispatch_suite_test.go | 2 +- .../v1alpha2/application/suite_test.go | 2 +- pkg/oam/util/helper_test.go | 2 +- test/e2e-apiserver-test/application_test.go | 6 +- test/e2e-apiserver-test/namespace_test.go | 65 +++ test/e2e-test/suite_test.go | 2 +- 19 files changed, 510 insertions(+), 255 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/namespace.go create mode 100644 pkg/apiserver/rest/usecase/namespace_test.go create mode 100644 pkg/apiserver/rest/utils/bcode/namespace.go create mode 100644 test/e2e-apiserver-test/namespace_test.go diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index ed9cab8f6..6ec5ec9fd 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -265,7 +265,7 @@ } }, "/api/v1/applications/{name}/components/{componentName}": { - "post": { + "get": { "consumes": [ "application/xml", "application/json" @@ -315,7 +315,7 @@ "tags": [ "application" ], - "summary": "deploy or update the application", + "summary": "deploy or upgrade the application", "operationId": "deployApplication", "parameters": [ { @@ -329,7 +329,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ApplicationBase" + "$ref": "#/definitions/v1.ApplicationDeployRequest" } }, "400": { @@ -466,6 +466,57 @@ } } }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update policy for application", + "operationId": "updateApplicationPolicy", + "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 + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdatePolicyRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailPolicyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, "delete": { "consumes": [ "application/xml", @@ -703,7 +754,7 @@ "namespace" ], "summary": "list all namespaces", - "operationId": "noop", + "operationId": "listNamespaces", "responses": { "200": { "description": "OK" @@ -723,7 +774,7 @@ "namespace" ], "summary": "create namespace", - "operationId": "noop", + "operationId": "createNamespace", "parameters": [ { "name": "body", @@ -741,155 +792,6 @@ } } }, - "/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": [ @@ -1399,11 +1301,11 @@ }, "common.AppRolloutStatus": { "required": [ + "rollingState", + "upgradedReadyReplicas", + "batchRollingState", "currentBatch", "upgradedReplicas", - "batchRollingState", - "upgradedReadyReplicas", - "rollingState", "lastTargetAppRevision" ], "properties": { @@ -1896,7 +1798,7 @@ "$ref": "#/definitions/common.inputItem" } }, - "lables": { + "labels": { "type": "object", "additionalProperties": { "type": "string" @@ -2101,6 +2003,54 @@ } } }, + "v1.ApplicationDeployRequest": { + "required": [ + "commit", + "sourceType", + "force" + ], + "properties": { + "commit": { + "type": "string" + }, + "force": { + "type": "boolean" + }, + "sourceType": { + "type": "string" + } + } + }, + "v1.ApplicationDeployResponse": { + "required": [ + "version", + "status", + "reason", + "deployUser", + "commit", + "sourceType" + ], + "properties": { + "commit": { + "type": "string" + }, + "deployUser": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "v1.ApplicationRequest": { "required": [ "components" @@ -2502,16 +2452,14 @@ }, "v1.CreateComponentRequest": { "required": [ - "appName", "name", "description", + "icon", "componentType", - "bindClusters" + "bindClusters", + "dependsOn" ], "properties": { - "appName": { - "type": "string" - }, "bindClusters": { "type": "array", "items": { @@ -2521,9 +2469,18 @@ "componentType": { "type": "string" }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, "description": { "type": "string" }, + "icon": { + "type": "string" + }, "labels": { "type": "object", "additionalProperties": { @@ -2555,10 +2512,14 @@ "v1.CreatePolicyRequest": { "required": [ "name", + "description", "type", "properties" ], "properties": { + "description": { + "type": "string" + }, "name": { "type": "string" }, @@ -2572,12 +2533,12 @@ }, "v1.DetailAddonResponse": { "required": [ - "name", "version", "description", "icon", "tags", - "phase" + "phase", + "name" ], "properties": { "deploy_data": { @@ -2615,13 +2576,13 @@ "v1.DetailApplicationResponse": { "required": [ "name", - "updateTime", + "description", "icon", "status", - "namespace", - "description", - "createTime", "gatewayRule", + "namespace", + "createTime", + "updateTime", "policies", "status", "resourceInfo", @@ -2688,12 +2649,12 @@ }, "v1.DetailClusterResponse": { "required": [ + "name", "description", "icon", "labels", "status", "reason", - "name", "resourceInfo" ], "properties": { @@ -2732,11 +2693,11 @@ "v1.DetailComponentResponse": { "required": [ "createTime", - "updateTime", "appPrimaryKey", - "name", "type", - "creator" + "name", + "creator", + "updateTime" ], "properties": { "appPrimaryKey": { @@ -2770,7 +2731,7 @@ "$ref": "#/definitions/common.inputItem" } }, - "lables": { + "labels": { "type": "object", "additionalProperties": { "type": "string" @@ -2811,17 +2772,25 @@ }, "v1.DetailPolicyResponse": { "required": [ - "name", - "type", + "description", + "creator", "properties", "createTime", - "updateTime" + "updateTime", + "name", + "type" ], "properties": { "createTime": { "type": "string", "format": "date-time" }, + "creator": { + "type": "string" + }, + "description": { + "type": "string" + }, "name": { "type": "string" }, @@ -2952,13 +2921,13 @@ }, "v1.ListNamespaceResponse": { "required": [ - "namesapces" + "namespaces" ], "properties": { - "namesapces": { + "namespaces": { "type": "array", "items": { - "$ref": "#/definitions/v1.NamesapceBase" + "$ref": "#/definitions/v1.NamespaceBase" } } } @@ -2994,7 +2963,7 @@ } } }, - "v1.NamesapceBase": { + "v1.NamespaceBase": { "required": [ "name", "description", @@ -3018,21 +2987,14 @@ } } }, - "v1.NamesapceDetailResponse": { + "v1.NamespaceDetailResponse": { "required": [ "name", "description", "createTime", - "updateTime", - "clusterBind" + "updateTime" ], "properties": { - "clusterBind": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, "createTime": { "type": "string", "format": "date-time" @@ -3086,6 +3048,8 @@ "required": [ "name", "type", + "description", + "creator", "properties", "createTime", "updateTime" @@ -3095,6 +3059,12 @@ "type": "string", "format": "date-time" }, + "creator": { + "type": "string" + }, + "description": { + "type": "string" + }, "name": { "type": "string" }, @@ -3131,10 +3101,28 @@ } } }, + "v1.UpdatePolicyRequest": { + "required": [ + "description", + "type", + "properties" + ], + "properties": { + "description": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "v1.UpdateWorkflowRequest": { "required": [ "name", - "namesapce", + "namespace", "enable" ], "properties": { @@ -3144,7 +3132,7 @@ "name": { "type": "string" }, - "namesapce": { + "namespace": { "type": "string" }, "steps": { diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi.go b/pkg/apiserver/datastore/kubeapi/kubeapi.go index fa7dfab61..94a35cfdb 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi.go @@ -59,7 +59,7 @@ func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error) 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 nil, fmt.Errorf("create namespace failure %w", err) } } return &kubeapi{ diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go index 60778d86e..c4840db32 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go @@ -95,14 +95,14 @@ var _ = Describe("Test kubeapi datastore driver", func() { It("Test batch add funtion", func() { var datas = []datastore.Entity{ &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, - &model.Application{Namespace: "test-namesapce", Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.Application{Namespace: "test-namesapce2", Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, + &model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, } err := kubeStore.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) var datas2 = []datastore.Entity{ - &model.Application{Namespace: "test-namesapce", Name: "can-delete", Description: "this is demo can-delete"}, + &model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = kubeStore.BatchAdd(context.TODO(), datas2) @@ -158,7 +158,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { diff = cmp.Diff(len(list), 4) Expect(diff).Should(BeEmpty()) - app.Namespace = "test-namesapce" + app.Namespace = "test-namespace" list, err = kubeStore.List(context.TODO(), &app, nil) Expect(err).ShouldNot(HaveOccurred()) diff = cmp.Diff(len(list), 1) diff --git a/pkg/apiserver/datastore/mongodb/mongodb_test.go b/pkg/apiserver/datastore/mongodb/mongodb_test.go index 19a53c9d9..82a326079 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb_test.go +++ b/pkg/apiserver/datastore/mongodb/mongodb_test.go @@ -63,14 +63,14 @@ var _ = Describe("Test mongodb datastore driver", func() { It("Test batch add funtion", func() { var datas = []datastore.Entity{ &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, - &model.Application{Namespace: "test-namesapce", Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.Application{Namespace: "test-namesapce2", Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, + &model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, } err := mongodbDriver.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) var datas2 = []datastore.Entity{ - &model.Application{Namespace: "test-namesapce", Name: "can-delete", Description: "this is demo can-delete"}, + &model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = mongodbDriver.BatchAdd(context.TODO(), datas2) @@ -112,7 +112,7 @@ var _ = Describe("Test mongodb datastore driver", func() { diff = cmp.Diff(len(list), 4) Expect(diff).Should(BeEmpty()) - app.Namespace = "test-namesapce" + app.Namespace = "test-namespace" list, err = mongodbDriver.List(context.TODO(), &app, nil) Expect(err).ShouldNot(HaveOccurred()) diff = cmp.Diff(len(list), 1) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 755227472..19f6d0ac4 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -287,11 +287,11 @@ type ApplicationTemplateVersion struct { // ListNamespaceResponse namesace list model type ListNamespaceResponse struct { - Namespaces []NamesapceBase `json:"namesapces"` + Namespaces []NamespaceBase `json:"namespaces"` } -// NamesapceBase namespace base model -type NamesapceBase struct { +// NamespaceBase namespace base model +type NamespaceBase struct { Name string `json:"name"` Description string `json:"description"` CreateTime time.Time `json:"createTime"` @@ -304,10 +304,9 @@ type CreateNamespaceRequest struct { Description string `json:"description"` } -// NamesapceDetailResponse namespace detail response -type NamesapceDetailResponse struct { - NamesapceBase - ClusterBind map[string]string `json:"clusterBind"` +// NamespaceDetailResponse namespace detail response +type NamespaceDetailResponse struct { + NamespaceBase } // ListComponentDefinitionResponse list component dedinition response model @@ -382,7 +381,7 @@ type PolicyDefinition struct { // UpdateWorkflowRequest update or create application workflow type UpdateWorkflowRequest struct { Name string `json:"name" validate:"checkname"` - Namespace string `json:"namesapce" validate:"checkname"` + Namespace string `json:"namespace" validate:"checkname"` Steps []WorkflowStep `json:"steps,omitempty"` Enable bool `json:"enable"` } diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index e1c7ed991..f6bd53e81 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -68,7 +68,10 @@ type applicationUsecaseImpl struct { // NewApplicationUsecase new application usecase func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase) ApplicationUsecase { - kubecli, _ := clients.GetKubeClient() + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } return &applicationUsecaseImpl{ ds: ds, workflowUsecase: workflowUsecase, @@ -433,7 +436,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat if err := c.kubeClient.Get(ctx, types.NamespacedName{Name: oamApp.Namespace}, &namespace); apierrors.IsNotFound(err) { namespace.Name = oamApp.Namespace if err := c.kubeClient.Create(ctx, &namespace); err != nil { - log.Logger.Errorf("auto create namesapce failure %s", err.Error()) + log.Logger.Errorf("auto create namespace failure %s", err.Error()) return nil, bcode.ErrCreateNamespace } } diff --git a/pkg/apiserver/rest/usecase/namespace.go b/pkg/apiserver/rest/usecase/namespace.go new file mode 100644 index 000000000..7f7fdf4a1 --- /dev/null +++ b/pkg/apiserver/rest/usecase/namespace.go @@ -0,0 +1,106 @@ +/* +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" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +// NamespaceUsecase namespace manage usecase. +// Namespace acts as the tenant isolation model on the control side. +type NamespaceUsecase interface { + ListNamespaces(ctx context.Context) ([]apisv1.NamespaceBase, error) + CreateNamespace(ctx context.Context, req apisv1.CreateNamespaceRequest) (*apisv1.NamespaceBase, error) +} + +// AnnotationDescription set namespace description in annotation +const AnnotationDescription string = "description" + +// LabelCreator set namesapce creator in labels +const LabelCreator string = "creator" + +type namespaceUsecaseImpl struct { + kubeClient client.Client +} + +// NewNamespaceUsecase new namespace usecase +func NewNamespaceUsecase() NamespaceUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + return &namespaceUsecaseImpl{kubeClient: kubecli} +} + +// ListNamespaces list controller cluster namespaces +func (n *namespaceUsecaseImpl) ListNamespaces(ctx context.Context) ([]apisv1.NamespaceBase, error) { + + // TODO: Consider whether to query only namespaces created by Vela + var kubeNamespaces corev1.NamespaceList + if err := n.kubeClient.List(ctx, &kubeNamespaces, &client.ListOptions{}); err != nil { + log.Logger.Errorf("query namespace list from cluster failure %s", err.Error()) + return nil, bcode.ErrNamespaceQuery + } + var namespaces []apisv1.NamespaceBase + for _, namesapce := range kubeNamespaces.Items { + namespaces = append(namespaces, apisv1.NamespaceBase{ + Name: namesapce.Name, + Description: namesapce.Annotations[AnnotationDescription], + CreateTime: namesapce.CreationTimestamp.Time, + UpdateTime: namesapce.CreationTimestamp.Time, + }) + } + return namespaces, nil +} + +// CreateNamespace create namespace to controller cluster +func (n *namespaceUsecaseImpl) CreateNamespace(ctx context.Context, req apisv1.CreateNamespaceRequest) (*apisv1.NamespaceBase, error) { + if err := n.kubeClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: req.Name, + Labels: map[string]string{ + LabelCreator: "kubevela", + }, + Annotations: map[string]string{ + AnnotationDescription: req.Description, + }, + }, + Spec: corev1.NamespaceSpec{}, + }); err != nil { + if apierrors.IsAlreadyExists(err) { + return nil, bcode.ErrNamespaceIsExist + } + return nil, err + } + return &apisv1.NamespaceBase{ + Name: req.Name, + Description: req.Description, + CreateTime: time.Now(), + UpdateTime: time.Now(), + }, nil +} diff --git a/pkg/apiserver/rest/usecase/namespace_test.go b/pkg/apiserver/rest/usecase/namespace_test.go new file mode 100644 index 000000000..ff6e11d2b --- /dev/null +++ b/pkg/apiserver/rest/usecase/namespace_test.go @@ -0,0 +1,50 @@ +/* +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" + + "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 namespace usecase functions", func() { + var ( + namespaceUsecase *namespaceUsecaseImpl + ) + BeforeEach(func() { + namespaceUsecase = &namespaceUsecaseImpl{kubeClient: k8sClient} + }) + It("Test CreateNamespace function", func() { + req := apisv1.CreateNamespaceRequest{ + Name: "test-namespace", + Description: "this is a namespace description 王二", + } + base, err := namespaceUsecase.CreateNamespace(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) + }) + + It("Test ListNamespace function", func() { + _, err := namespaceUsecase.ListNamespaces(context.TODO()) + Expect(err).Should(BeNil()) + }) +}) diff --git a/pkg/apiserver/rest/utils/bcode/namespace.go b/pkg/apiserver/rest/utils/bcode/namespace.go new file mode 100644 index 000000000..d9145f9c9 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/namespace.go @@ -0,0 +1,23 @@ +/* +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 + +// ErrNamespaceQuery query namespace failure from k8s api +var ErrNamespaceQuery = NewBcode(500, 30001, "query namespace list from cluster failure") + +// ErrNamespaceIsExist namespace name is exist +var ErrNamespaceIsExist = NewBcode(400, 30002, "namespace name is exist") diff --git a/pkg/apiserver/rest/webservice/namespace.go b/pkg/apiserver/rest/webservice/namespace.go index 31af705dc..46a5ccf2b 100644 --- a/pkg/apiserver/rest/webservice/namespace.go +++ b/pkg/apiserver/rest/webservice/namespace.go @@ -20,13 +20,22 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" + "github.com/oam-dev/kubevela/pkg/apiserver/log" 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 namespaceWebService struct { + namespaceUsecase usecase.NamespaceUsecase } -func (c *namespaceWebService) GetWebService() *restful.WebService { +// NewNamespaceWebService new namespace webservice +func NewNamespaceWebService(namespaceUsecase usecase.NamespaceUsecase) WebService { + return &namespaceWebService{namespaceUsecase: namespaceUsecase} +} + +func (n *namespaceWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) ws.Path(versionPrefix+"/namespaces"). Consumes(restful.MIME_XML, restful.MIME_JSON). @@ -35,42 +44,53 @@ func (c *namespaceWebService) GetWebService() *restful.WebService { tags := []string{"namespace"} - ws.Route(ws.GET("/").To(noop). + ws.Route(ws.GET("/").To(n.listNamespaces). Doc("list all namespaces"). Metadata(restfulspec.KeyOpenAPITags, tags). Writes(apis.ListNamespaceResponse{})) - ws.Route(ws.POST("/").To(noop). + ws.Route(ws.POST("/").To(n.createNamespace). Doc("create namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateNamespaceRequest{}). - Writes(apis.NamesapceDetailResponse{})) - - ws.Route(ws.GET("/{namespace}").To(noop). - Doc("get one namespace"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). - Writes(apis.NamesapceDetailResponse{})) - - // Compatible with historical apis - ws.Route(ws.GET("/{namespace}/applications/:appname").To(noop). - Doc("get the specified oam application in the specified namespace"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). - Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). - Writes(apis.ApplicationResponse{})) - - ws.Route(ws.POST("/{namespace}/applications/:appname").To(noop). - Doc("create or update oam application in the specified namespace"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). - Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). - Reads(apis.ApplicationRequest{})) - - ws.Route(ws.DELETE("/{namespace}/applications/:appname").To(noop). - Doc("create or update oam application in the specified namespace"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). - Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string"))) + Writes(apis.NamespaceDetailResponse{})) return ws } + +func (n *namespaceWebService) listNamespaces(req *restful.Request, res *restful.Response) { + namespaces, err := n.namespaceUsecase.ListNamespaces(req.Request.Context()) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListNamespaceResponse{Namespaces: namespaces}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *namespaceWebService) createNamespace(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateNamespaceRequest + 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 + namespaceBase, err := n.namespaceUsecase.CreateNamespace(req.Request.Context(), createReq) + if err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(apis.NamespaceDetailResponse{NamespaceBase: *namespaceBase}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/validate_test.go b/pkg/apiserver/rest/webservice/validate_test.go index f6fd675b7..7b71fab51 100644 --- a/pkg/apiserver/rest/webservice/validate_test.go +++ b/pkg/apiserver/rest/webservice/validate_test.go @@ -29,26 +29,26 @@ var _ = Describe("Test validate function", func() { Expect(cmp.Diff(nameRegexp.MatchString("///Asd asda "), false)).Should(BeEmpty()) var app0 = apisv1.CreateApplicationRequest{ Name: "a", - Namespace: "namesapce", + Namespace: "namespace", } err := validate.Struct(&app0) Expect(err).ShouldNot(BeNil()) var app1 = apisv1.CreateApplicationRequest{ Name: "Asdasd", - Namespace: "namesapce", + Namespace: "namespace", } err = validate.Struct(&app1) Expect(err).ShouldNot(BeNil()) var app2 = apisv1.CreateApplicationRequest{ Name: "asdasd asdasd ++", - Namespace: "namesapce", + Namespace: "namespace", } err = validate.Struct(&app2) Expect(err).ShouldNot(BeNil()) var app3 = apisv1.CreateApplicationRequest{ Name: "asdasd", - Namespace: "namesapce", + Namespace: "namespace", } 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 8681550bf..7c03cd777 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -62,10 +62,11 @@ func Init(ctx context.Context, ds datastore.DataStore) { clusterUsecase := usecase.NewClusterUsecase(ds) workflowUsecase := usecase.NewWorkflowUsecase(ds) applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase) + namespaceUsecase := usecase.NewNamespaceUsecase() oamApplicationUsecase := usecase.NewOAMApplicationUsecase() RegistWebService(NewClusterWebService(clusterUsecase)) RegistWebService(NewApplicationWebService(applicationUsecase)) - RegistWebService(&namespaceWebService{}) + RegistWebService(NewNamespaceWebService(namespaceUsecase)) RegistWebService(&componentDefinitionWebservice{}) RegistWebService(&addonWebService{}) RegistWebService(NewOAMApplication(oamApplicationUsecase)) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch.go b/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch.go index 6ff29d2f8..fd99f856d 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch.go @@ -209,7 +209,7 @@ func (a *AppManifestsDispatcher) retrieveLegacyResourceTrackers(ctx context.Cont oldRtList := &v1beta1.ResourceTrackerList{} if err := a.c.List(ctx, oldRtList, client.MatchingLabels{ oam.LabelAppName: ExtractAppName(a.currentRTName, a.namespace), - "app.oam.dev/namesapce": a.namespace, + "app.oam.dev/namespace": a.namespace, }); err != nil { return errors.Wrap(err, "cannot retrieve legacy resource trackers with miss-spell label") } diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch_suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch_suite_test.go index 8eabf6398..6109988a7 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch_suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch_suite_test.go @@ -490,7 +490,7 @@ var _ = Describe("Test compatibility code", func() { ObjectMeta: metav1.ObjectMeta{ Name: appName + "-v2-" + namespaceName, Labels: map[string]string{ - "app.oam.dev/namesapce": namespaceName, + "app.oam.dev/namespace": namespaceName, oam.LabelAppName: appName, }, }, diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go index 584f9ed20..d00bf6af0 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go @@ -245,7 +245,7 @@ func NewFakeRecorder(bufferSize int) *FakeRecorder { // randomNamespaceName generates a random name based on the basic name. // Running each ginkgo case in a new namespace with a random name can avoid -// waiting a long time to GC namesapce. +// waiting a long time to GC namespace. func randomNamespaceName(basic string) string { return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16)) } diff --git a/pkg/oam/util/helper_test.go b/pkg/oam/util/helper_test.go index bb9dfc3bc..8d35c6de3 100644 --- a/pkg/oam/util/helper_test.go +++ b/pkg/oam/util/helper_test.go @@ -1338,7 +1338,7 @@ func TestGetDefinitionWithClusterScope(t *testing.T) { }, }, } - // old cluster workload trait scope definition crd is cluster scope, the namesapce field is empty + // old cluster workload trait scope definition crd is cluster scope, the namespace field is empty noNs := v1alpha2.TraitDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "noNsDefinition", diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index c6d67393b..36a76c9b2 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -38,7 +38,7 @@ var _ = Describe("Test application rest api", func() { defer GinkgoRecover() var req = apisv1.CreateApplicationRequest{ Name: "test-app-sadasd", - Namespace: "test-app-namesapce", + Namespace: "test-app-namespace", Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, @@ -77,7 +77,7 @@ var _ = Describe("Test application rest api", func() { Expect(err).Should(Succeed()) var req = apisv1.CreateApplicationRequest{ Name: "test-app-sadasd", - Namespace: "test-app-namesapce", + Namespace: "test-app-namespace", Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, @@ -177,7 +177,7 @@ var _ = Describe("Test application rest api", func() { Expect(cmp.Diff(response.Status, model.DeployEventRunning)).Should(BeEmpty()) var oam v1beta1.Application - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: "test-app-sadasd", Namespace: "test-app-namesapce"}, &oam) + err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: "test-app-sadasd", Namespace: "test-app-namespace"}, &oam) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty()) Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty()) diff --git a/test/e2e-apiserver-test/namespace_test.go b/test/e2e-apiserver-test/namespace_test.go new file mode 100644 index 000000000..6fa313610 --- /dev/null +++ b/test/e2e-apiserver-test/namespace_test.go @@ -0,0 +1,65 @@ +/* +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 namespace rest api", func() { + It("Test create namespace", func() { + defer GinkgoRecover() + var req = apisv1.CreateNamespaceRequest{ + Name: "dev-team", + Description: "开发环境租户", + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/namespaces", "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 namespaceBase apisv1.NamespaceBase + err = json.NewDecoder(res.Body).Decode(&namespaceBase) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(namespaceBase.Name, req.Name)).Should(BeEmpty()) + Expect(cmp.Diff(namespaceBase.Description, req.Description)).Should(BeEmpty()) + }) + + It("Test list namespace", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/namespaces") + 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 namespaces apisv1.ListNamespaceResponse + err = json.NewDecoder(res.Body).Decode(&namespaces) + Expect(err).ShouldNot(HaveOccurred()) + }) +}) diff --git a/test/e2e-test/suite_test.go b/test/e2e-test/suite_test.go index 0b893d533..43ac3b3d8 100644 --- a/test/e2e-test/suite_test.go +++ b/test/e2e-test/suite_test.go @@ -302,7 +302,7 @@ func RequestReconcileNow(ctx context.Context, o client.Object) { // randomNamespaceName generates a random name based on the basic name. // Running each ginkgo case in a new namespace with a random name can avoid -// waiting a long time to GC namesapce. +// waiting a long time to GC namespace. func randomNamespaceName(basic string) string { return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16)) } From f0b91ef8d67f09912801f81cb799abfa6acb7e0b Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Wed, 20 Oct 2021 19:47:36 +0800 Subject: [PATCH 04/59] Feat: added environment binding capabilities to the application creation API. (#2523) * Feat: add env binding support in app create api * Feat: add query component types api Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 245 +++++++++-------- pkg/apiserver/model/application.go | 16 +- pkg/apiserver/model/model.go | 16 ++ pkg/apiserver/rest/apis/v1/types.go | 31 ++- pkg/apiserver/rest/usecase/application.go | 90 ++++++- .../rest/usecase/application_test.go | 35 ++- pkg/apiserver/rest/usecase/definition.go | 72 +++++ pkg/apiserver/rest/usecase/definition_test.go | 53 ++++ .../rest/usecase/testdata/webserver-cd.yaml | 255 ++++++++++++++++++ pkg/apiserver/rest/utils/cache.go | 41 +++ .../rest/webservice/component_definition.go | 28 +- pkg/apiserver/rest/webservice/webservice.go | 3 +- test/e2e-apiserver-test/application_test.go | 5 +- test/e2e-apiserver-test/definition_test.go | 44 +++ 14 files changed, 805 insertions(+), 129 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/definition.go create mode 100644 pkg/apiserver/rest/usecase/definition_test.go create mode 100644 pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml create mode 100644 pkg/apiserver/rest/utils/cache.go create mode 100644 test/e2e-apiserver-test/definition_test.go diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 6ec5ec9fd..890e69b0c 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -718,24 +718,20 @@ "componentdefinition" ], "summary": "list all componentdefinition", - "operationId": "noop", + "operationId": "listComponentDefinition", "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", + "description": "if specified, query the componentdefinition supported by the env.", + "name": "envName", "in": "query" } ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.ListComponentDefinitionResponse" + } } } } @@ -1162,7 +1158,7 @@ } } }, - "/v1/{namespace}/applications/:appname": { + "/v1/namespaces/{namespace}/applications/{appname}": { "get": { "consumes": [ "application/xml", @@ -1176,7 +1172,7 @@ "oam" ], "summary": "get the specified oam application in the specified namespace", - "operationId": "noop", + "operationId": "getApplication", "parameters": [ { "type": "string", @@ -1199,44 +1195,6 @@ } } }, - "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", @@ -1250,7 +1208,7 @@ "oam" ], "summary": "create or update oam application in the specified namespace", - "operationId": "noop", + "operationId": "createOrUpdateApplication", "parameters": [ { "type": "string", @@ -1280,6 +1238,42 @@ "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": "deleteApplication", + "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" + } + } } } }, @@ -1302,10 +1296,10 @@ "common.AppRolloutStatus": { "required": [ "rollingState", - "upgradedReadyReplicas", "batchRollingState", - "currentBatch", "upgradedReplicas", + "currentBatch", + "upgradedReadyReplicas", "lastTargetAppRevision" ], "properties": { @@ -1595,7 +1589,8 @@ "required": [ "mode", "suspend", - "terminated" + "terminated", + "finished" ], "properties": { "appRevision": { @@ -1604,9 +1599,15 @@ "contextBackend": { "$ref": "#/definitions/v1.ObjectReference" }, + "finished": { + "type": "boolean" + }, "mode": { "type": "string" }, + "startTime": { + "type": "string" + }, "steps": { "type": "array", "items": { @@ -1626,9 +1627,15 @@ "id" ], "properties": { + "firstExecuteTime": { + "type": "string" + }, "id": { "type": "string" }, + "lastExecuteTime": { + "type": "string" + }, "message": { "type": "string" }, @@ -1960,12 +1967,6 @@ "gatewayRule" ], "properties": { - "clusterList": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ClusterBase" - } - }, "createTime": { "type": "string", "format": "date-time" @@ -1973,6 +1974,12 @@ "description": { "type": "string" }, + "envBind": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBind" + } + }, "gatewayRule": { "type": "array", "items": { @@ -2229,24 +2236,31 @@ } } }, + "v1.ClusterSelector": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, "v1.ComponentBase": { "required": [ "name", "description", "componentType", - "bindClusters", + "envNames", "dependsOn", "deployVersion", "createTime", "updateTime" ], "properties": { - "bindClusters": { - "type": "array", - "items": { - "type": "string" - } - }, "componentType": { "type": "string" }, @@ -2269,6 +2283,12 @@ "description": { "type": "string" }, + "envNames": { + "type": "array", + "items": { + "type": "string" + } + }, "icon": { "type": "string" }, @@ -2291,8 +2311,7 @@ "required": [ "name", "description", - "icon", - "requiredParams" + "icon" ], "properties": { "description": { @@ -2303,12 +2322,6 @@ }, "name": { "type": "string" - }, - "requiredParams": { - "type": "array", - "items": { - "$ref": "#/definitions/types.Parameter" - } } } }, @@ -2370,18 +2383,18 @@ "icon" ], "properties": { - "clusterList": { - "type": "array", - "items": { - "type": "string" - } - }, "deploy": { "type": "boolean" }, "description": { "type": "string" }, + "envBind": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBind" + } + }, "icon": { "type": "string" }, @@ -2456,16 +2469,9 @@ "description", "icon", "componentType", - "bindClusters", "dependsOn" ], "properties": { - "bindClusters": { - "type": "array", - "items": { - "type": "string" - } - }, "componentType": { "type": "string" }, @@ -2478,6 +2484,12 @@ "description": { "type": "string" }, + "envNames": { + "type": "array", + "items": { + "type": "string" + } + }, "icon": { "type": "string" }, @@ -2575,26 +2587,20 @@ }, "v1.DetailApplicationResponse": { "required": [ + "gatewayRule", "name", - "description", + "namespace", + "updateTime", "icon", "status", - "gatewayRule", - "namespace", + "description", "createTime", - "updateTime", "policies", "status", "resourceInfo", "workflowStatus" ], "properties": { - "clusterList": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ClusterBase" - } - }, "createTime": { "type": "string", "format": "date-time" @@ -2602,6 +2608,12 @@ "description": { "type": "string" }, + "envBind": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBind" + } + }, "gatewayRule": { "type": "array", "items": { @@ -2649,12 +2661,12 @@ }, "v1.DetailClusterResponse": { "required": [ + "reason", "name", "description", "icon", "labels", "status", - "reason", "resourceInfo" ], "properties": { @@ -2692,12 +2704,12 @@ }, "v1.DetailComponentResponse": { "required": [ - "createTime", - "appPrimaryKey", + "updateTime", "type", - "name", + "createTime", "creator", - "updateTime" + "appPrimaryKey", + "name" ], "properties": { "appPrimaryKey": { @@ -2772,13 +2784,13 @@ }, "v1.DetailPolicyResponse": { "required": [ + "name", + "type", "description", "creator", "properties", "createTime", - "updateTime", - "name", - "type" + "updateTime" ], "properties": { "createTime": { @@ -2827,6 +2839,23 @@ } }, "v1.EmptyResponse": {}, + "v1.EnvBind": { + "required": [ + "name", + "clusterSelector" + ], + "properties": { + "clusterSelector": { + "$ref": "#/definitions/v1.ClusterSelector" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "v1.GatewayRule": { "required": [ "ruleType", @@ -2989,10 +3018,10 @@ }, "v1.NamespaceDetailResponse": { "required": [ - "name", "description", "createTime", - "updateTime" + "updateTime", + "name" ], "properties": { "createTime": { diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 83be4e109..fc1718662 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -30,7 +30,7 @@ type Application struct { Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` - ClusterList []string `json:"clusterList,omitempty"` + EnvBinds []*EnvBind `json:"envBinds,omitempty"` } // TableName return custom table name @@ -55,6 +55,20 @@ func (a *Application) Index() map[string]string { return index } +// EnvBind application env bind +type EnvBind struct { + Name string `json:"name" validate:"checkname"` + Description string `json:"description,omitempty"` + ClusterSelector *ClusterSelector `json:"clusterSelector"` +} + +// ClusterSelector cluster selector +type ClusterSelector struct { + Name string `json:"name" validate:"checkname"` + // Adapt to a scenario where only one Namespace is available or a user-defined Namespace is available. + Namespace string `json:"namespace,omitempty"` +} + // ApplicationComponent component database model type ApplicationComponent struct { Model diff --git a/pkg/apiserver/model/model.go b/pkg/apiserver/model/model.go index afedb7bdc..7621eb9ef 100644 --- a/pkg/apiserver/model/model.go +++ b/pkg/apiserver/model/model.go @@ -55,6 +55,22 @@ func NewJSONStructByString(source string) (*JSONStruct, error) { return &data, nil } +// NewJSONStructByStruct new jsonstruct from strcut object +func NewJSONStructByStruct(object interface{}) (*JSONStruct, error) { + if object == nil { + return nil, nil + } + var data JSONStruct + out, err := yaml.Marshal(object) + if err != nil { + return nil, fmt.Errorf("marshal object data failure %w", err) + } + if err := yaml.Unmarshal(out, &data); err != nil { + return nil, fmt.Errorf("unmarshal object data failure %w", err) + } + return &data, nil +} + // JSON Encoded as a JSON string func (j *JSONStruct) JSON() string { b, err := json.Marshal(j) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 19f6d0ac4..1662f4859 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -164,8 +164,8 @@ type ApplicationBase struct { UpdateTime time.Time `json:"updateTime"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` - ClusterBindList []ClusterBase `json:"clusterList,omitempty"` Status string `json:"status"` + EnvBind []*EnvBind `json:"envBind,omitempty"` GatewayRuleList []GatewayRule `json:"gatewayRule"` } @@ -195,12 +195,26 @@ type CreateApplicationRequest struct { Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` - ClusterList []string `json:"clusterList,omitempty"` + EnvBind []*EnvBind `json:"envBind,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"` } +// EnvBind application env bind +type EnvBind struct { + Name string `json:"name" validate:"checkname"` + Description string `json:"description,omitempty"` + ClusterSelector *ClusterSelector `json:"clusterSelector"` +} + +// ClusterSelector cluster selector +type ClusterSelector struct { + Name string `json:"name" validate:"checkname"` + // Adapt to a scenario where only one Namespace is available or a user-defined Namespace is available. + Namespace string `json:"namespace,omitempty"` +} + // DetailApplicationResponse application detail type DetailApplicationResponse struct { ApplicationBase @@ -229,7 +243,7 @@ type ComponentBase struct { Description string `json:"description"` Labels map[string]string `json:"labels,omitempty"` ComponentType string `json:"componentType"` - BindClusters []string `json:"bindClusters"` + EnvNames []string `json:"envNames"` Icon string `json:"icon,omitempty"` DependsOn []string `json:"dependsOn"` Creator string `json:"creator,omitempty"` @@ -250,7 +264,7 @@ type CreateComponentRequest struct { Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` ComponentType string `json:"componentType" validate:"checkname"` - BindClusters []string `json:"bindClusters"` + EnvNames []string `json:"envNames,omitempty"` Properties string `json:"properties,omitempty"` DependsOn []string `json:"dependsOn"` } @@ -311,15 +325,14 @@ type NamespaceDetailResponse struct { // ListComponentDefinitionResponse list component dedinition response model type ListComponentDefinitionResponse struct { - ComponentDefinitions []ComponentDefinitionBase `json:"componentDefinitions"` + ComponentDefinitions []*ComponentDefinitionBase `json:"componentDefinitions"` } // ComponentDefinitionBase component definition base model type ComponentDefinitionBase struct { - Name string `json:"name"` - Description string `json:"description"` - Icon string `json:"icon"` - Parameter []types.Parameter `json:"requiredParams"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` } // CreatePolicyRequest create app policy diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index f6bd53e81..e1cd15933 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -18,6 +18,7 @@ package usecase import ( "context" + "encoding/json" "errors" corev1 "k8s.io/api/core/v1" @@ -28,6 +29,7 @@ import ( "sigs.k8s.io/yaml" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" @@ -39,6 +41,14 @@ import ( "github.com/oam-dev/kubevela/pkg/utils/apply" ) +// PolicyType build-in policy type +type PolicyType string + +const ( + // EnvBindPolicy Multiple environment distribution policy + EnvBindPolicy PolicyType = "env-binding" +) + // ApplicationUsecase application usecase type ApplicationUsecase interface { ListApplications(ctx context.Context) ([]*apisv1.ApplicationBase, error) @@ -89,7 +99,7 @@ func (c *applicationUsecaseImpl) ListApplications(ctx context.Context) ([]*apisv } var list []*apisv1.ApplicationBase for _, entity := range entitys { - list = append(list, c.converAppModelToBase(entity.(*model.Application))) + list = append(list, c.converAppModelToBase(ctx, entity.(*model.Application))) } return list, nil } @@ -107,7 +117,7 @@ func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName str // DetailApplication detail application info func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) { - base := c.converAppModelToBase(app) + base := c.converAppModelToBase(ctx, app) policys, err := c.queryApplicationPolicys(ctx, app) if err != nil { return nil, err @@ -145,7 +155,6 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis Namespace: req.Namespace, Icon: req.Icon, Labels: req.Labels, - ClusterList: req.ClusterList, } // check app name. exit, err := c.ds.IsExist(ctx, &application) @@ -211,6 +220,44 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis canDeploy = len(oamApp.Spec.Components) > 0 } + // build-in create env binding policy + if len(req.EnvBind) > 0 { + policy := model.ApplicationPolicy{ + AppPrimaryKey: application.PrimaryKey(), + Name: "env-binds", + Description: "build-in create", + Type: string(EnvBindPolicy), + Creator: "", + } + var envBindingSpec v1alpha1.EnvBindingSpec + for _, envBind := range req.EnvBind { + placement := v1alpha1.EnvPlacement{ + ClusterSelector: &common.ClusterSelector{ + Name: envBind.ClusterSelector.Name, + }, + } + if envBind.ClusterSelector.Namespace != "" { + placement.NamespaceSelector = &v1alpha1.NamespaceSelector{ + Name: envBind.ClusterSelector.Namespace, + } + } + envBindingSpec.Envs = append(envBindingSpec.Envs, v1alpha1.EnvConfig{ + Name: envBind.Name, + Placement: placement, + }) + } + properties, err := model.NewJSONStructByStruct(envBindingSpec) + if err != nil { + log.Logger.Errorf("new env binding properties failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + policy.Properties = properties + if err := c.ds.Add(ctx, &policy); err != nil { + log.Logger.Errorf("save env binding policy failure,%s", err.Error()) + return nil, err + } + } + // add application to db. if err := c.ds.Add(ctx, &application); err != nil { if errors.Is(err, datastore.ErrRecordExist) { @@ -219,7 +266,7 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return nil, err } // render app base info. - base := c.converAppModelToBase(&application) + base := c.converAppModelToBase(ctx, &application) // deploy to cluster if need. if req.Deploy && canDeploy { if _, err := c.Deploy(ctx, &application, apisv1.ApplicationDeployRequest{ @@ -561,7 +608,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return app, nil } -func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *apisv1.ApplicationBase { +func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app *model.Application) *apisv1.ApplicationBase { appBeas := &apisv1.ApplicationBase{ Name: app.Name, Namespace: app.Namespace, @@ -571,6 +618,39 @@ func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *a Icon: app.Icon, Labels: app.Labels, } + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Type: string(EnvBindPolicy), + } + policys, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) + if err != nil { + log.Logger.Errorf("query application env binding policy failure %s", err.Error()) + } + for _, policyEntity := range policys { + policy := policyEntity.(*model.ApplicationPolicy) + if policy.Properties != nil { + var envBindingSpec v1alpha1.EnvBindingSpec + if err := json.Unmarshal([]byte(policy.Properties.JSON()), &envBindingSpec); err != nil { + log.Logger.Errorf("unmarshal env binding policy failure %s", err.Error()) + continue + } + for _, env := range envBindingSpec.Envs { + envBind := &apisv1.EnvBind{ + Name: env.Name, + Description: "", + } + if env.Placement.ClusterSelector != nil { + envBind.ClusterSelector = &apisv1.ClusterSelector{ + Name: env.Placement.ClusterSelector.Name, + } + } + if env.Placement.NamespaceSelector != nil && envBind.ClusterSelector != nil { + envBind.ClusterSelector.Namespace = env.Placement.NamespaceSelector.Name + } + appBeas.EnvBind = append(appBeas.EnvBind, envBind) + } + } + } // TODO: get and render app status return appBeas } diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 94f9285f3..1cddd199e 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -104,12 +104,45 @@ var _ = Describe("Test application usecase function", func() { _, err = appUsecase.CreateApplication(context.TODO(), req) equal = cmp.Equal(err, bcode.ErrInvalidProperties, cmpopts.EquateErrors()) Expect(equal).Should(BeTrue()) + + By("Test create app with env binding") + req = v1.CreateApplicationRequest{ + Name: "test-app-sadasd4", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + EnvBind: []*v1.EnvBind{ + { + Name: "dev", + Description: "This is a dev env", + ClusterSelector: &v1.ClusterSelector{ + Name: "dev-cluster", + }, + }, + { + Name: "prob", + Description: "This is a prob env", + ClusterSelector: &v1.ClusterSelector{ + Name: "prob-cluster", + Namespace: "prob", + }, + }, + }, + } + appBase, err := appUsecase.CreateApplication(context.TODO(), req) + Expect(err).Should(BeNil()) + + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + Expect(cmp.Diff(len(appBase.EnvBind), 2)).Should(BeEmpty()) }) It("Test ListApplications function", func() { apps, err := appUsecase.ListApplications(context.TODO()) Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(apps), 2)).Should(BeEmpty()) + Expect(cmp.Diff(len(apps), 3)).Should(BeEmpty()) }) It("Test DetailApplication function", func() { diff --git a/pkg/apiserver/rest/usecase/definition.go b/pkg/apiserver/rest/usecase/definition.go new file mode 100644 index 000000000..770d4b072 --- /dev/null +++ b/pkg/apiserver/rest/usecase/definition.go @@ -0,0 +1,72 @@ +/* +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" + "time" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" +) + +// DefinitionUsecase definition usecase, Implement the management of ComponentDefinition、TraitDefinition and WorkflowStepDefinition. +type DefinitionUsecase interface { + // ListComponentDefinitions list component definition base info + ListComponentDefinitions(ctx context.Context, envName string) ([]*apisv1.ComponentDefinitionBase, error) +} + +type definitionUsecaseImpl struct { + kubeClient client.Client + caches map[string]*utils.MemoryCache +} + +// NewDefinitionUsecase new definition usecase +func NewDefinitionUsecase() DefinitionUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + return &definitionUsecaseImpl{kubeClient: kubecli, caches: make(map[string]*utils.MemoryCache)} +} + +func (d *definitionUsecaseImpl) ListComponentDefinitions(ctx context.Context, envName string) ([]*apisv1.ComponentDefinitionBase, error) { + // check cache + if mc := d.caches["componentDefinitions"]; mc != nil && !mc.IsExpired() { + return mc.GetData().([]*apisv1.ComponentDefinitionBase), nil + } + var componentDefinitions v1beta1.ComponentDefinitionList + if err := d.kubeClient.List(ctx, &componentDefinitions, &client.ListOptions{}); err != nil { + return nil, err + } + var cdb []*apisv1.ComponentDefinitionBase + for _, cd := range componentDefinitions.Items { + cdb = append(cdb, &apisv1.ComponentDefinitionBase{ + Name: cd.Name, + Description: cd.Annotations[types.AnnDescription], + }) + } + // set cache + d.caches["componentDefinitions"] = utils.NewMemoryCache(cdb, time.Minute*3) + return cdb, nil +} diff --git a/pkg/apiserver/rest/usecase/definition_test.go b/pkg/apiserver/rest/usecase/definition_test.go new file mode 100644 index 000000000..f74c0c8e2 --- /dev/null +++ b/pkg/apiserver/rest/usecase/definition_test.go @@ -0,0 +1,53 @@ +/* +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" + "io/ioutil" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" +) + +var _ = Describe("Test namespace usecase functions", func() { + var ( + definitionUsecase *definitionUsecaseImpl + ) + BeforeEach(func() { + definitionUsecase = &definitionUsecaseImpl{kubeClient: k8sClient, caches: make(map[string]*utils.MemoryCache)} + }) + It("Test ListComponentDefinitions function", func() { + bs, err := ioutil.ReadFile("./testdata/webserver-cd.yaml") + Expect(err).Should(Succeed()) + var test v1beta1.ComponentDefinition + err = yaml.Unmarshal(bs, &test) + Expect(err).Should(Succeed()) + err = k8sClient.Create(context.Background(), &test) + Expect(err).Should(Succeed()) + components, err := definitionUsecase.ListComponentDefinitions(context.TODO(), "") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 1)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].Name, "webservice-test")).Should(BeEmpty()) + Expect(components[0].Description).ShouldNot(BeEmpty()) + }) +}) diff --git a/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml b/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml new file mode 100644 index 000000000..a1b7970ae --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml @@ -0,0 +1,255 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/webservice.cue +apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + annotations: + definition.oam.dev/description: Describes long-running, scalable, containerized services that have a stable network endpoint to receive external network traffic from customers. + name: webservice-test + namespace: default +spec: + schematic: + cue: + template: | + output: { + apiVersion: "apps/v1" + kind: "Deployment" + spec: { + selector: matchLabels: "app.oam.dev/component": context.name + + template: { + metadata: labels: { + "app.oam.dev/component": context.name + if parameter.addRevisionLabel { + "app.oam.dev/appRevision": context.appRevision + } + "app.oam.dev/revision": context.revision + } + + spec: { + containers: [{ + name: context.name + image: parameter.image + ports: [{ + containerPort: parameter.port + }] + + if parameter["imagePullPolicy"] != _|_ { + imagePullPolicy: parameter.imagePullPolicy + } + + if parameter["cmd"] != _|_ { + command: parameter.cmd + } + + if parameter["env"] != _|_ { + env: parameter.env + } + + if context["config"] != _|_ { + env: context.config + } + + if parameter["cpu"] != _|_ { + resources: { + limits: cpu: parameter.cpu + requests: cpu: parameter.cpu + } + } + + if parameter["memory"] != _|_ { + resources: { + limits: memory: parameter.memory + requests: memory: parameter.memory + } + } + + if parameter["volumes"] != _|_ { + volumeMounts: [ for v in parameter.volumes { + { + mountPath: v.mountPath + name: v.name + }}] + } + + if parameter["livenessProbe"] != _|_ { + livenessProbe: parameter.livenessProbe + } + + if parameter["readinessProbe"] != _|_ { + readinessProbe: parameter.readinessProbe + } + + }] + + if parameter["imagePullSecrets"] != _|_ { + imagePullSecrets: [ for v in parameter.imagePullSecrets { + name: v + }, + ] + } + + if parameter["volumes"] != _|_ { + volumes: [ for v in parameter.volumes { + { + name: v.name + if v.type == "pvc" { + persistentVolumeClaim: claimName: v.claimName + } + if v.type == "configMap" { + configMap: { + defaultMode: v.defaultMode + name: v.cmName + if v.items != _|_ { + items: v.items + } + } + } + if v.type == "secret" { + secret: { + defaultMode: v.defaultMode + secretName: v.secretName + if v.items != _|_ { + items: v.items + } + } + } + if v.type == "emptyDir" { + emptyDir: medium: v.medium + } + }}] + } + } + } + } + } + parameter: { + // +usage=Which image would you like to use for your service + // +short=i + image: string + + // +usage=Specify image pull policy for your service + imagePullPolicy?: string + + // +usage=Specify image pull secrets for your service + imagePullSecrets?: [...string] + + // +usage=Which port do you want customer traffic sent to + // +short=p + port: *80 | int + + // +ignore + // +usage=If addRevisionLabel is true, the appRevision label will be added to the underlying pods + addRevisionLabel: *false | bool + + // +usage=Commands to run in the container + cmd?: [...string] + + // +usage=Define arguments by using environment variables + env?: [...{ + // +usage=Environment variable name + name: string + // +usage=The value of the environment variable + value?: string + // +usage=Specifies a source the value of this var should come from + valueFrom?: { + // +usage=Selects a key of a secret in the pod's namespace + secretKeyRef: { + // +usage=The name of the secret in the pod's namespace to select from + name: string + // +usage=The key of the secret to select from. Must be a valid secret key + key: string + } + } + }] + + // +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core) + cpu?: string + + // +usage=Specifies the attributes of the memory resource required for the container. + memory?: string + + // +usage=Declare volumes and volumeMounts + volumes?: [...{ + name: string + mountPath: string + // +usage=Specify volume type, options: "pvc","configMap","secret","emptyDir" + type: "pvc" | "configMap" | "secret" | "emptyDir" + if type == "pvc" { + claimName: string + } + if type == "configMap" { + defaultMode: *420 | int + cmName: string + items?: [...{ + key: string + path: string + mode: *511 | int + }] + } + if type == "secret" { + defaultMode: *420 | int + secretName: string + items?: [...{ + key: string + path: string + mode: *511 | int + }] + } + if type == "emptyDir" { + medium: *"" | "Memory" + } + }] + + // +usage=Instructions for assessing whether the container is alive. + livenessProbe?: #HealthProbe + + // +usage=Instructions for assessing whether the container is in a suitable state to serve traffic. + readinessProbe?: #HealthProbe + } + #HealthProbe: { + + // +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute. + exec?: { + // +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures. + command: [...string] + } + + // +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute. + httpGet?: { + // +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed. + path: string + // +usage=The TCP socket within the container to which the HTTP GET request should be directed. + port: int + httpHeaders?: [...{ + name: string + value: string + }] + } + + // +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute. + tcpSocket?: { + // +usage=The TCP socket within the container that should be probed to assess container health. + port: int + } + + // +usage=Number of seconds after the container is started before the first probe is initiated. + initialDelaySeconds: *0 | int + + // +usage=How often, in seconds, to execute the probe. + periodSeconds: *10 | int + + // +usage=Number of seconds after which the probe times out. + timeoutSeconds: *1 | int + + // +usage=Minimum consecutive successes for the probe to be considered successful after having failed. + successThreshold: *1 | int + + // +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe). + failureThreshold: *3 | int + } + workload: + definition: + apiVersion: apps/v1 + kind: Deployment + diff --git a/pkg/apiserver/rest/utils/cache.go b/pkg/apiserver/rest/utils/cache.go new file mode 100644 index 000000000..a3d6da740 --- /dev/null +++ b/pkg/apiserver/rest/utils/cache.go @@ -0,0 +1,41 @@ +/* +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 utils + +import "time" + +// MemoryCache memory cache, support time expired +type MemoryCache struct { + data interface{} + cacheDuration time.Duration + startTime time.Time +} + +// NewMemoryCache new memory cache instance +func NewMemoryCache(data interface{}, cacheDuration time.Duration) *MemoryCache { + return &MemoryCache{data: data, cacheDuration: cacheDuration, startTime: time.Now()} +} + +// IsExpired whether the cache data expires +func (m *MemoryCache) IsExpired() bool { + return time.Now().Before(m.startTime.Add(m.cacheDuration)) +} + +// GetData get cache data +func (m *MemoryCache) GetData() interface{} { + return m.data +} diff --git a/pkg/apiserver/rest/webservice/component_definition.go b/pkg/apiserver/rest/webservice/component_definition.go index f26d922ea..b5d99d35d 100644 --- a/pkg/apiserver/rest/webservice/component_definition.go +++ b/pkg/apiserver/rest/webservice/component_definition.go @@ -21,9 +21,12 @@ 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 componentDefinitionWebservice struct { + definitionUsecase usecase.DefinitionUsecase } func (c *componentDefinitionWebservice) GetWebService() *restful.WebService { @@ -35,11 +38,30 @@ func (c *componentDefinitionWebservice) GetWebService() *restful.WebService { tags := []string{"componentdefinition"} - ws.Route(ws.GET("/").To(noop). + ws.Route(ws.GET("/").To(c.listComponentDefinition). Doc("list all componentdefinition"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("appName", "if specified, query the componentdefinition supported by the cluster where the application resides.").DataType("string")). - Param(ws.QueryParameter("clusterName", "if specified, query the componentdefinition supported by the cluster.").DataType("string")). + Param(ws.QueryParameter("envName", "if specified, query the componentdefinition supported by the env.").DataType("string")). + Returns(200, "", apis.ListComponentDefinitionResponse{}). Writes(apis.ListComponentDefinitionResponse{})) return ws } + +// NewComponentDefinitionWebservice new componentdefinition webservice +func NewComponentDefinitionWebservice(du usecase.DefinitionUsecase) WebService { + return &componentDefinitionWebservice{ + definitionUsecase: du, + } +} + +func (c *componentDefinitionWebservice) listComponentDefinition(req *restful.Request, res *restful.Response) { + componentDefinitions, err := c.definitionUsecase.ListComponentDefinitions(req.Request.Context(), req.QueryParameter("envName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListComponentDefinitionResponse{ComponentDefinitions: componentDefinitions}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 7c03cd777..d057ce176 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -64,10 +64,11 @@ func Init(ctx context.Context, ds datastore.DataStore) { applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase) namespaceUsecase := usecase.NewNamespaceUsecase() oamApplicationUsecase := usecase.NewOAMApplicationUsecase() + definitionUsecase := usecase.NewDefinitionUsecase() RegistWebService(NewClusterWebService(clusterUsecase)) RegistWebService(NewApplicationWebService(applicationUsecase)) RegistWebService(NewNamespaceWebService(namespaceUsecase)) - RegistWebService(&componentDefinitionWebservice{}) + RegistWebService(NewComponentDefinitionWebservice(definitionUsecase)) RegistWebService(&addonWebService{}) RegistWebService(NewOAMApplication(oamApplicationUsecase)) RegistWebService(&policyDefinitionWebservice{}) diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index 36a76c9b2..ab335074a 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -42,7 +42,9 @@ var _ = Describe("Test application rest api", func() { Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, - ClusterList: []string{}, + EnvBind: []*apisv1.EnvBind{{Name: "dev-env", ClusterSelector: &apisv1.ClusterSelector{ + Name: "dev-cluster", + }}}, } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) @@ -59,6 +61,7 @@ var _ = Describe("Test application rest api", func() { Expect(cmp.Diff(appBase.Description, req.Description)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Namespace, req.Namespace)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Labels["test"], req.Labels["test"])).Should(BeEmpty()) + Expect(cmp.Diff(appBase.EnvBind[0].Name, "dev-env")).Should(BeEmpty()) }) It("Test delete app", func() { diff --git a/test/e2e-apiserver-test/definition_test.go b/test/e2e-apiserver-test/definition_test.go new file mode 100644 index 000000000..31e035262 --- /dev/null +++ b/test/e2e-apiserver-test/definition_test.go @@ -0,0 +1,44 @@ +/* +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 ( + "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 definitions rest api", func() { + + It("Test list component definitions", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/componentdefinitions") + 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 componentdefinitions apisv1.ListComponentDefinitionResponse + err = json.NewDecoder(res.Body).Decode(&componentdefinitions) + Expect(err).ShouldNot(HaveOccurred()) + }) +}) From accf0138f8f679da29e3bfceacb904f6f3ab41fa Mon Sep 17 00:00:00 2001 From: Somefive Date: Thu, 21 Oct 2021 17:01:29 +0800 Subject: [PATCH 05/59] Feat: apiserver cluster api (#2526) * Fix: multicluster api * Feat: add apiserver cluster api & fix codecov * Feat: add pod capacity and resourceused stat * Feat: add cloud cluster manage * Test: add test for cloud cluster * Test: pending cloud resource api * Style: refactor * Fix: apiserver e2e test * Fix: fix application usecase policy bug * Style: add returns for cluster api * Feat: add provider detail info & add cache & add rollback protection * Style: refactor * Style: add error code --- .github/workflows/apiserver-test.yaml | 16 +- cmd/core/main.go | 2 +- go.mod | 3 + go.sum | 37 ++ pkg/apiserver/clients/kubeclient.go | 12 +- pkg/apiserver/model/cluster.go | 67 ++++ pkg/apiserver/model/model.go | 14 + pkg/apiserver/rest/apis/v1/types.go | 65 ++- pkg/apiserver/rest/usecase/application.go | 4 +- pkg/apiserver/rest/usecase/cluster.go | 379 +++++++++++++++++- pkg/apiserver/rest/utils/bcode/cluster.go | 38 ++ pkg/apiserver/rest/utils/params.go | 50 +++ pkg/apiserver/rest/webservice/cluster.go | 202 +++++++++- pkg/cloudprovider/aliyun.go | 128 ++++++ pkg/cloudprovider/cluster.go | 38 ++ pkg/cloudprovider/types.go | 34 ++ pkg/multicluster/cluster_management.go | 268 +++++++++++++ pkg/multicluster/errors.go | 29 ++ pkg/multicluster/utils.go | 26 +- references/cli/cluster.go | 170 +------- test/e2e-apiserver-test/cluster_test.go | 133 ++++++ .../oam_application_test.go | 24 +- test/e2e-apiserver-test/suite_test.go | 34 +- test/e2e-apiserver-test/utils.go | 59 +++ 24 files changed, 1575 insertions(+), 257 deletions(-) create mode 100644 pkg/apiserver/model/cluster.go create mode 100644 pkg/apiserver/rest/utils/bcode/cluster.go create mode 100644 pkg/apiserver/rest/utils/params.go create mode 100644 pkg/cloudprovider/aliyun.go create mode 100644 pkg/cloudprovider/cluster.go create mode 100644 pkg/cloudprovider/types.go create mode 100644 pkg/multicluster/cluster_management.go create mode 100644 pkg/multicluster/errors.go create mode 100644 test/e2e-apiserver-test/cluster_test.go create mode 100644 test/e2e-apiserver-test/utils.go diff --git a/.github/workflows/apiserver-test.yaml b/.github/workflows/apiserver-test.yaml index 75dc769b6..25483be82 100644 --- a/.github/workflows/apiserver-test.yaml +++ b/.github/workflows/apiserver-test.yaml @@ -62,6 +62,15 @@ jobs: version: ${{ env.KIND_VERSION }} skipClusterCreation: true + - name: Setup Kind Cluster (Worker) + run: | + kind delete cluster --name worker + kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4 --name worker + kubectl version + kubectl cluster-info + kind get kubeconfig --name worker --internal > /tmp/worker.kubeconfig + kind get kubeconfig --name worker > /tmp/worker.client.kubeconfig + - name: Setup Kind Cluster (Hub) run: | kind delete cluster @@ -69,7 +78,7 @@ jobs: kubectl version kubectl cluster-info - - name: Load Image to kind cluster (Hub) + - name: Load Image to kind cluster run: make kind-load - name: Cleanup for e2e tests @@ -81,7 +90,10 @@ jobs: run: make unit-test-apiserver - name: Run apiserver e2e test - run: make e2e-apiserver-test + run: | + export ALIYUN_ACCESS_KEY_ID=${{ secrets.ALIYUN_ACCESS_KEY_ID }} + export ALIYUN_ACCESS_KEY_SECRET=${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} + make e2e-apiserver-test - name: Stop kubevela, get profile run: make end-e2e-core diff --git a/cmd/core/main.go b/cmd/core/main.go index cf57ca530..a56da7fb1 100644 --- a/cmd/core/main.go +++ b/cmd/core/main.go @@ -187,7 +187,7 @@ func main() { // wrapper the round tripper by multi cluster rewriter if enableClusterGateway { - if err := multicluster.Initialize(restConfig); err != nil { + if _, err := multicluster.Initialize(restConfig, true); err != nil { klog.ErrorS(err, "failed to enable multicluster") os.Exit(1) } diff --git a/go.mod b/go.mod index 06936e73b..a3c9ccbf4 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,9 @@ require ( github.com/AlecAivazis/survey/v2 v2.1.1 github.com/Masterminds/sprig v2.22.0+incompatible github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 + github.com/alibabacloud-go/cs-20151215/v2 v2.4.5 + github.com/alibabacloud-go/darabonba-openapi v0.1.4 + github.com/alibabacloud-go/tea v1.1.15 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 github.com/briandowns/spinner v1.11.1 diff --git a/go.sum b/go.sum index 52245e12a..9dd0f234a 100644 --- a/go.sum +++ b/go.sum @@ -178,7 +178,29 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alessio/shellescape v1.2.2 h1:8LnL+ncxhWT2TR00dfJRT25JWWrhkMZXneHVWnetDZg= github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alibabacloud-go/cs-20151215/v2 v2.4.5 h1:v7SWYM+3nCfw7L5DjstXgwSo6PLGpMYZHdNdDi6yajU= +github.com/alibabacloud-go/cs-20151215/v2 v2.4.5/go.mod h1:pIg8PCfRO6qSylVbW9BiG6q0zaYCP/aIKCCEwsuvbPg= +github.com/alibabacloud-go/darabonba-openapi v0.1.4 h1:eV4mB+45/QxWFQqghSUVO5H5Ct4c+tCaCp4c57TCTVY= +github.com/alibabacloud-go/darabonba-openapi v0.1.4/go.mod h1:j03z4XUkIC9aBj/w5Bt7H0cygmPNt5sug8NXle68+Og= +github.com/alibabacloud-go/darabonba-string v1.0.0/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.0.7 h1:Kt/9kicJxvq1It739psKFBi1IB9imhqGWA9g4chIbjI= +github.com/alibabacloud-go/openapi-util v0.0.7/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.15 h1:IaBC1Mm5Ss+l7cWnOXSxCmnWoWrEdeHEtDgQzoCCgjY= +github.com/alibabacloud-go/tea v1.1.15/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils v1.3.9 h1:TtbzxS+BXrisA7wzbAMRtlU8A2eWLg0ufm7m/Tl6fc4= +github.com/alibabacloud-go/tea-utils v1.3.9/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aliyun/credentials-go v1.1.2 h1:qU1vwGIBb3UJ8BwunHDRFtAhS6jnQLnde/yk0+Ih2GY= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -803,6 +825,8 @@ github.com/gophercloud/gophercloud v0.10.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU8 github.com/gophercloud/gophercloud v0.11.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw= github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= @@ -971,6 +995,7 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -1446,7 +1471,10 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -1521,6 +1549,8 @@ github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek= github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -1590,6 +1620,7 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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= @@ -1723,10 +1754,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -1944,6 +1977,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2091,6 +2125,7 @@ golang.org/x/tools v0.0.0-20200422205258-72e4a01eba43/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200603131246-cc40288be839/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -2310,6 +2345,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= diff --git a/pkg/apiserver/clients/kubeclient.go b/pkg/apiserver/clients/kubeclient.go index 8e3733022..92c482a53 100644 --- a/pkg/apiserver/clients/kubeclient.go +++ b/pkg/apiserver/clients/kubeclient.go @@ -18,9 +18,8 @@ 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" + "github.com/oam-dev/kubevela/pkg/multicluster" ) var kubeClient client.Client @@ -35,13 +34,10 @@ func GetKubeClient() (client.Client, error) { if kubeClient != nil { return kubeClient, nil } - conf, err := config.GetConfig() + var err error + kubeClient, err = multicluster.GetMulticlusterKubernetesClient() 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 + return kubeClient, nil } diff --git a/pkg/apiserver/model/cluster.go b/pkg/apiserver/model/cluster.go new file mode 100644 index 000000000..de043148b --- /dev/null +++ b/pkg/apiserver/model/cluster.go @@ -0,0 +1,67 @@ +/* +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 + +// ProviderInfo describes the information from provider API +type ProviderInfo struct { + Name string `json:"name"` + ID string `json:"id"` + Zone string `json:"zone"` + Labels map[string]string `json:"labels"` +} + +// Cluster describes the model of cluster in apiserver +type Cluster struct { + Model + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels"` + Status string `json:"status"` + Reason string `json:"reason"` + + Provider ProviderInfo `json:"provider"` + APIServerURL string `json:"apiServerURL"` + DashboardURL string `json:"dashboardURL"` + + KubeConfig string `json:"kubeConfig"` + KubeConfigSecret string `json:"kubeConfigSecret"` +} + +// TableName table name for datastore +func (c *Cluster) TableName() string { + return tableNamePrefix + "cluster" +} + +// PrimaryKey primary key for datastore +func (c *Cluster) PrimaryKey() string { + return c.Name +} + +// Index set to nil for list +func (c *Cluster) Index() map[string]string { + index := make(map[string]string) + if c.Name != "" { + index["name"] = c.Name + } + return index +} + +// DeepCopy create a copy of cluster +func (c *Cluster) DeepCopy() *Cluster { + return deepCopy(c).(*Cluster) +} diff --git a/pkg/apiserver/model/model.go b/pkg/apiserver/model/model.go index 7621eb9ef..bc8f3aacb 100644 --- a/pkg/apiserver/model/model.go +++ b/pkg/apiserver/model/model.go @@ -19,6 +19,7 @@ package model import ( "encoding/json" "fmt" + "reflect" "time" "k8s.io/apimachinery/pkg/runtime" @@ -110,3 +111,16 @@ func (m *Model) SetCreateTime(time time.Time) { func (m *Model) SetUpdateTime(time time.Time) { m.UpdateTime = time } + +func deepCopy(src interface{}) interface{} { + dst := reflect.New(reflect.TypeOf(src).Elem()) + + val := reflect.ValueOf(src).Elem() + nVal := dst.Elem() + for i := 0; i < val.NumField(); i++ { + nvField := nVal.Field(i) + nvField.Set(val.Field(i)) + } + + return dst.Interface() +} diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 1662f4859..80dabc7be 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -22,6 +22,7 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/model" + "github.com/oam-dev/kubevela/pkg/cloudprovider" ) // CtxKeyApplication request context key of application @@ -105,14 +106,47 @@ type AddonStatusResponse struct { Phase AddonPhase `json:"phase"` } +// AccessKeyRequest request parameters to access cloud provider +type AccessKeyRequest struct { + AccessKeyID string `json:"accessKeyID"` + AccessKeySecret string `json:"accessKeySecret"` +} + // CreateClusterRequest request parameters to create a cluster type CreateClusterRequest struct { - Name string `json:"name" validate:"name"` + Name string `json:"name" validate:"checkname"` Description string `json:"description,omitempty"` Icon string `json:"icon"` - KubeConfig string `json:"kubeConfig" validate:"required_without=kubeConfigSecret"` - KubeConfigSecret string `json:"kubeConfigSecret,omitempty" validate:"required_without=kubeConfig"` + KubeConfig string `json:"kubeConfig,omitempty" validate:"required_without=KubeConfigSecret"` + KubeConfigSecret string `json:"kubeConfigSecret,omitempty" validate:"required_without=KubeConfig"` Labels map[string]string `json:"labels,omitempty"` + DashboardURL string `json:"dashboardURL,omitempty"` +} + +// ConnectCloudClusterRequest request parameters to create a cluster from cloud cluster +type ConnectCloudClusterRequest struct { + AccessKeyID string `json:"accessKeyID"` + AccessKeySecret string `json:"accessKeySecret"` + ClusterID string `json:"clusterID"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels,omitempty"` +} + +// ClusterResourceInfo resource info of cluster +type ClusterResourceInfo struct { + WorkerNumber int `json:"workerNumber"` + MasterNumber int `json:"masterNumber"` + MemoryCapacity int64 `json:"memoryCapacity"` + CPUCapacity int64 `json:"cpuCapacity"` + GPUCapacity int64 `json:"gpuCapacity,omitempty"` + PodCapacity int64 `json:"podCapacity"` + MemoryUsed int64 `json:"memoryUsed"` + CPUUsed int64 `json:"cpuUsed"` + GPUUsed int64 `json:"gpuUsed,omitempty"` + PodUsed int64 `json:"podUsed"` + StorageClassList []string `json:"storageClassList,omitempty"` } // DetailClusterResponse cluster detail information model @@ -125,29 +159,30 @@ type DetailClusterResponse struct { DashboardURL string `json:"dashboardURL,omitempty"` } -// ClusterResourceInfo resource info of cluster -type ClusterResourceInfo struct { - WorkerNumber int `json:"workerNumber"` - MasterNumber int `json:"masterNumber"` - MemoryCapacity int64 `json:"memoryCapacity"` - CPUCapacity int64 `json:"cpuCapacity"` - GPUCapacity int64 `json:"gpuCapacity,omitempty"` - StorageClassList []string `json:"storageClassList,omitempty"` -} - // ListClusterResponse list cluster type ListClusterResponse struct { Clusters []ClusterBase `json:"clusters"` } +// ListCloudClusterResponse list cloud clusters +type ListCloudClusterResponse struct { + Clusters []cloudprovider.CloudCluster `json:"clusters"` + Total int `json:"total"` +} + // ClusterBase cluster base model type ClusterBase struct { Name string `json:"name"` Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels"` - Status string `json:"status"` - Reason string `json:"reason"` + + Provider model.ProviderInfo `json:"providerInfo"` + APIServerURL string `json:"apiServerURL"` + DashboardURL string `json:"dashboardURL"` + + Status string `json:"status"` + Reason string `json:"reason"` } // ListApplicationResponse list applications by query params diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index e1cd15933..fd27978df 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -575,8 +575,8 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo for _, entity := range policies { policy := entity.(*model.ApplicationPolicy) apolicy := v1beta1.AppPolicy{ - Name: component.Name, - Type: component.Type, + Name: policy.Name, + Type: policy.Type, } if policy.Properties != nil { apolicy.Properties = policy.Properties.RawExtension() diff --git a/pkg/apiserver/rest/usecase/cluster.go b/pkg/apiserver/rest/usecase/cluster.go index 7c54406c5..bafd67e2e 100644 --- a/pkg/apiserver/rest/usecase/cluster.go +++ b/pkg/apiserver/rest/usecase/cluster.go @@ -18,25 +18,396 @@ package usecase import ( "context" + "fmt" + "io/ioutil" + "os" + "time" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + utils2 "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/cloudprovider" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/utils" ) // ClusterUsecase cluster manage type ClusterUsecase interface { + ListKubeClusters(context.Context, string, int, int) (*apis.ListClusterResponse, error) CreateKubeCluster(context.Context, apis.CreateClusterRequest) (*apis.ClusterBase, error) + GetKubeCluster(context.Context, string) (*apis.DetailClusterResponse, error) + ModifyKubeCluster(context.Context, apis.CreateClusterRequest, string) (*apis.ClusterBase, error) + DeleteKubeCluster(context.Context, string) (*apis.ClusterBase, error) + + ListCloudClusters(context.Context, string, apis.AccessKeyRequest, int, int) (*apis.ListCloudClusterResponse, error) + ConnectCloudCluster(context.Context, string, apis.ConnectCloudClusterRequest) (*apis.ClusterBase, error) } type clusterUsecaseImpl struct { - ds datastore.DataStore + ds datastore.DataStore + caches map[string]*utils2.MemoryCache + k8sClient client.Client } // NewClusterUsecase new cluster usecase func NewClusterUsecase(ds datastore.DataStore) ClusterUsecase { - return &clusterUsecaseImpl{ds: ds} + k8sClient, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get k8sClient failure: %s", err.Error()) + } + return &clusterUsecaseImpl{ds: ds, k8sClient: k8sClient, caches: make(map[string]*utils2.MemoryCache)} } -func (c *clusterUsecaseImpl) CreateKubeCluster(context.Context, apis.CreateClusterRequest) (*apis.ClusterBase, error) { - return nil, nil +func (c *clusterUsecaseImpl) getClusterFromDataStore(ctx context.Context, clusterName string) (*model.Cluster, error) { + cluster := &model.Cluster{ + Name: clusterName, + } + if err := c.ds.Get(ctx, cluster); err != nil { + return nil, err + } + return cluster, nil +} + +func (c *clusterUsecaseImpl) rollbackAddedClusterInDataStore(ctx context.Context, cluster *model.Cluster) { + if e := c.ds.Delete(ctx, cluster); e != nil { + log.Logger.Errorf("failed to rollback added cluster %s in data store: %s", cluster.Name, e.Error()) + } +} + +func (c *clusterUsecaseImpl) rollbackDeletedClusterInDataStore(ctx context.Context, cluster *model.Cluster) { + if e := c.ds.Add(ctx, cluster); e != nil { + log.Logger.Errorf("failed to rollback deleted cluster %s in data store: %s", cluster.Name, e.Error()) + } +} + +func (c *clusterUsecaseImpl) rollbackJoinedKubeCluster(ctx context.Context, cluster *model.Cluster) { + if e := multicluster.DetachCluster(ctx, c.k8sClient, cluster.Name); e != nil { + log.Logger.Errorf("failed to rollback joined cluster %s in kubevela: %s", cluster.Name, e.Error()) + } +} + +func (c *clusterUsecaseImpl) rollbackDetachedKubeCluster(ctx context.Context, cluster *model.Cluster) { + if _, e := joinClusterByKubeConfigString(ctx, c.k8sClient, cluster.Name, cluster.KubeConfig); e != nil { + log.Logger.Errorf("failed to rollback detached cluster %s in kubevela: %s", cluster.Name, e.Error()) + } +} + +func (c *clusterUsecaseImpl) ListKubeClusters(ctx context.Context, query string, page int, pageSize int) (*apis.ListClusterResponse, error) { + // TODO: Fuzzy query + clusters, err := c.ds.List(ctx, &model.Cluster{}, &datastore.ListOptions{Page: page, PageSize: pageSize}) + if err != nil { + return nil, errors.Wrapf(err, "failed to list cluster with query %s in data store", query) + } + resp := &apis.ListClusterResponse{ + Clusters: []apis.ClusterBase{}, + } + for _, raw := range clusters { + cluster, ok := raw.(*model.Cluster) + if ok { + resp.Clusters = append(resp.Clusters, *newClusterBaseFromCluster(cluster)) + } + } + return resp, nil +} + +func joinClusterByKubeConfigString(ctx context.Context, k8sClient client.Client, clusterName string, kubeConfig string) (string, error) { + tmpFileName := fmt.Sprintf("/tmp/cluster-secret-%s-%s-%d.kubeconfig", clusterName, utils.RandomString(8), time.Now().UnixNano()) + if err := ioutil.WriteFile(tmpFileName, []byte(kubeConfig), 0600); err != nil { + return "", errors.Wrapf(err, "failed to write kubeconfig to temp file %s", tmpFileName) + } + defer func() { + _ = os.Remove(tmpFileName) + }() + cluster, err := multicluster.JoinClusterByKubeConfig(ctx, k8sClient, tmpFileName, clusterName) + if err != nil { + if errors.Is(err, multicluster.ErrClusterExists) { + return "", bcode.ErrClusterExistsInKubernetes + } + return "", errors.Wrapf(err, "failed to join cluster") + } + return cluster.Server, nil +} + +func createClusterModelFromRequest(req apis.CreateClusterRequest, oldCluster *model.Cluster) (newCluster *model.Cluster) { + if oldCluster != nil { + newCluster = oldCluster.DeepCopy() + } else { + newCluster = &model.Cluster{} + } + newCluster.Name = req.Name + newCluster.Description = req.Description + newCluster.Icon = req.Icon + newCluster.Labels = req.Labels + newCluster.KubeConfig = req.KubeConfig + newCluster.KubeConfigSecret = req.KubeConfigSecret + newCluster.DashboardURL = req.DashboardURL + return newCluster +} + +func (c *clusterUsecaseImpl) createKubeCluster(ctx context.Context, req apis.CreateClusterRequest, providerCluster *cloudprovider.CloudCluster) (*apis.ClusterBase, error) { + var err error + cluster := createClusterModelFromRequest(req, nil) + t := time.Now() + cluster.SetCreateTime(t) + cluster.SetUpdateTime(t) + if providerCluster != nil { + cluster.Provider = model.ProviderInfo{ + Name: providerCluster.Name, + ID: providerCluster.ID, + Zone: providerCluster.Zone, + Labels: providerCluster.Labels, + } + cluster.DashboardURL = providerCluster.DashBoardURL + } + if req.KubeConfig != "" { + cluster.APIServerURL, err = joinClusterByKubeConfigString(ctx, c.k8sClient, req.Name, req.KubeConfig) + if err != nil { + return nil, err + } + c.setClusterStatusAndResourceInfo(ctx, cluster) + if err = c.ds.Add(ctx, cluster); err != nil { + c.rollbackJoinedKubeCluster(ctx, cluster) + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrClusterAlreadyExistInDataStore + } + return nil, err + } + return newClusterBaseFromCluster(cluster), nil + } + if req.KubeConfigSecret != "" { + return nil, bcode.ErrKubeConfigSecretNotSupport + } + return nil, bcode.ErrKubeConfigAndSecretIsNotSet +} + +func (c *clusterUsecaseImpl) CreateKubeCluster(ctx context.Context, req apis.CreateClusterRequest) (*apis.ClusterBase, error) { + return c.createKubeCluster(ctx, req, nil) +} + +func (c *clusterUsecaseImpl) GetKubeCluster(ctx context.Context, clusterName string) (*apis.DetailClusterResponse, error) { + cluster, err := c.getClusterFromDataStore(ctx, clusterName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to found cluster %s in data store", clusterName) + } + resourceInfo := c.setClusterStatusAndResourceInfo(ctx, cluster) + if err = c.ds.Put(ctx, cluster); err != nil { + return nil, errors.Wrapf(err, "failed to update cluster %s status info", clusterName) + } + return &apis.DetailClusterResponse{ + ClusterBase: *newClusterBaseFromCluster(cluster), + ResourceInfo: resourceInfo, + RemoteManageURL: "NA", + DashboardURL: "NA", + }, nil +} + +func (c *clusterUsecaseImpl) ModifyKubeCluster(ctx context.Context, req apis.CreateClusterRequest, clusterName string) (*apis.ClusterBase, error) { + oldCluster, err := c.getClusterFromDataStore(ctx, clusterName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to found cluster %s in data store", clusterName) + } + + newCluster := createClusterModelFromRequest(req, oldCluster) + newCluster.SetUpdateTime(time.Now()) + if oldCluster.Name != newCluster.Name || oldCluster.KubeConfig != newCluster.KubeConfig || oldCluster.KubeConfigSecret != newCluster.KubeConfigSecret { + if newCluster.KubeConfig == "" && newCluster.KubeConfigSecret != "" { + return nil, bcode.ErrKubeConfigSecretNotSupport + } + newClusterTempName := newCluster.Name + "_tmp_" + utils.RandomString(8) + newCluster.APIServerURL, err = joinClusterByKubeConfigString(ctx, c.k8sClient, newCluster.Name, newCluster.KubeConfig) + if err != nil { + return nil, errors.Wrapf(err, "failed to join new cluster %s", newCluster.Name) + } + c.setClusterStatusAndResourceInfo(ctx, newCluster) + rollbackTempCluster := func() { + rollBackCluster := newCluster.DeepCopy() + rollBackCluster.Name = newClusterTempName + c.rollbackJoinedKubeCluster(ctx, rollBackCluster) + } + if err = multicluster.DetachCluster(ctx, c.k8sClient, oldCluster.Name); err != nil { + rollbackTempCluster() + return nil, errors.Wrapf(err, "failed to detach old cluster %s", oldCluster.Name) + } + if err = c.ds.Delete(ctx, oldCluster); err != nil { + rollbackTempCluster() + c.rollbackDetachedKubeCluster(ctx, oldCluster) + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to delete old cluster %s from datastore", oldCluster.Name) + } + if err = c.ds.Add(ctx, newCluster); err != nil { + rollbackTempCluster() + c.rollbackDetachedKubeCluster(ctx, oldCluster) + c.rollbackDeletedClusterInDataStore(ctx, oldCluster) + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrClusterAlreadyExistInDataStore + } + return nil, errors.Wrapf(err, "failed to add new cluster %s to datastore", newCluster.Name) + } + if err = multicluster.RenameCluster(ctx, c.k8sClient, newClusterTempName, newCluster.Name); err != nil { + rollbackTempCluster() + c.rollbackDetachedKubeCluster(ctx, oldCluster) + c.rollbackDeletedClusterInDataStore(ctx, oldCluster) + c.rollbackAddedClusterInDataStore(ctx, newCluster) + return nil, errors.Wrapf(err, "failed to rename temporary cluster %s to %s", newClusterTempName, newCluster.Name) + } + } else { + newCluster.Status = oldCluster.Status + newCluster.Reason = oldCluster.Reason + if err = c.ds.Put(ctx, newCluster); err != nil { + return nil, errors.Wrapf(err, "failed to update cluster %s", newCluster.Name) + } + } + return newClusterBaseFromCluster(newCluster), nil +} + +func (c *clusterUsecaseImpl) DeleteKubeCluster(ctx context.Context, clusterName string) (*apis.ClusterBase, error) { + cluster, err := c.getClusterFromDataStore(ctx, clusterName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to found cluster %s in data store", clusterName) + } + if err = c.ds.Delete(ctx, cluster); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to delete cluster %s in data store", clusterName) + } + if err = multicluster.DetachCluster(ctx, c.k8sClient, clusterName); err != nil { + c.rollbackDeletedClusterInDataStore(ctx, cluster) + return nil, errors.Wrapf(err, "failed to delete cluster %s in kubernetes", clusterName) + } + return newClusterBaseFromCluster(cluster), nil +} + +func (c *clusterUsecaseImpl) setClusterStatusAndResourceInfo(ctx context.Context, cluster *model.Cluster) apis.ClusterResourceInfo { + resourceInfo, err := c.getClusterResourceInfoFromK8s(ctx, cluster.Name) + if err != nil { + cluster.Status = "Unhealthy" + cluster.Reason = fmt.Sprintf("Failed to get cluster resource info: %s", err.Error()) + } else { + cluster.Status = "Healthy" + cluster.Reason = "" + } + return resourceInfo +} + +func (c *clusterUsecaseImpl) getClusterResourceInfoCacheKey(clusterName string) string { + return "cluster-resource-info::" + clusterName +} + +func (c *clusterUsecaseImpl) getClusterResourceInfoFromK8s(ctx context.Context, clusterName string) (apis.ClusterResourceInfo, error) { + cacheKey := c.getClusterResourceInfoCacheKey(clusterName) + if cache, exists := c.caches[cacheKey]; exists && !cache.IsExpired() { + return cache.GetData().(apis.ClusterResourceInfo), nil + } + clusterInfo, err := multicluster.GetClusterInfo(ctx, c.k8sClient, clusterName) + if err != nil { + return apis.ClusterResourceInfo{}, err + } + var storageClassList []string + for _, cls := range clusterInfo.StorageClasses.Items { + storageClassList = append(storageClassList, cls.Name) + } + getUsed := func(cap resource.Quantity, alloc resource.Quantity) *resource.Quantity { + used := cap.DeepCopy() + used.Sub(alloc) + return &used + } + // TODO add support for gpu capacity + clusterResourceInfo := apis.ClusterResourceInfo{ + WorkerNumber: clusterInfo.WorkerNumber, + MasterNumber: clusterInfo.MasterNumber, + MemoryCapacity: clusterInfo.MemoryCapacity.Value(), + CPUCapacity: clusterInfo.CPUCapacity.Value(), + GPUCapacity: 0, + PodCapacity: clusterInfo.PodCapacity.Value(), + MemoryUsed: getUsed(clusterInfo.MemoryCapacity, clusterInfo.MemoryAllocatable).Value(), + CPUUsed: getUsed(clusterInfo.CPUCapacity, clusterInfo.CPUAllocatable).Value(), + GPUUsed: 0, + PodUsed: getUsed(clusterInfo.PodCapacity, clusterInfo.PodAllocatable).Value(), + StorageClassList: storageClassList, + } + c.caches[cacheKey] = utils2.NewMemoryCache(clusterResourceInfo, time.Minute) + return clusterResourceInfo, nil +} + +func (c *clusterUsecaseImpl) ListCloudClusters(ctx context.Context, provider string, req apis.AccessKeyRequest, pageNumber int, pageSize int) (*apis.ListCloudClusterResponse, error) { + p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret) + if err != nil { + log.Logger.Errorf("failed to get cluster provider: %s", err.Error()) + return nil, bcode.ErrInvalidCloudClusterProvider + } + clusters, total, err := p.ListCloudClusters(pageNumber, pageSize) + if err != nil { + log.Logger.Errorf("failed to list cloud clusters: %s", err.Error()) + return nil, bcode.ErrGetCloudClusterFailure + } + resp := &apis.ListCloudClusterResponse{ + Clusters: []cloudprovider.CloudCluster{}, + Total: total, + } + for _, cluster := range clusters { + resp.Clusters = append(resp.Clusters, *cluster) + } + return resp, nil +} + +func (c *clusterUsecaseImpl) ConnectCloudCluster(ctx context.Context, provider string, req apis.ConnectCloudClusterRequest) (*apis.ClusterBase, error) { + p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret) + if err != nil { + log.Logger.Errorf("failed to get cluster provider: %s", err.Error()) + return nil, bcode.ErrInvalidCloudClusterProvider + } + kubeConfig, err := p.GetClusterKubeConfig(req.ClusterID) + if err != nil { + log.Logger.Errorf("failed to get cluster kubeConfig: %s", err.Error()) + return nil, bcode.ErrGetCloudClusterFailure + } + cluster, err := p.GetClusterInfo(req.ClusterID) + if err != nil { + log.Logger.Errorf("failed to get cluster info: %s", err.Error()) + return nil, bcode.ErrGetCloudClusterFailure + } + createReq := apis.CreateClusterRequest{ + Name: req.Name, + Description: req.Description, + Icon: req.Icon, + Labels: req.Labels, + KubeConfig: kubeConfig, + } + return c.createKubeCluster(ctx, createReq, cluster) +} + +func newClusterBaseFromCluster(cluster *model.Cluster) *apis.ClusterBase { + return &apis.ClusterBase{ + Name: cluster.Name, + Description: cluster.Description, + Icon: cluster.Icon, + Labels: cluster.Labels, + + APIServerURL: cluster.APIServerURL, + DashboardURL: cluster.DashboardURL, + Provider: cluster.Provider, + + Status: cluster.Status, + Reason: cluster.Reason, + } } diff --git a/pkg/apiserver/rest/utils/bcode/cluster.go b/pkg/apiserver/rest/utils/bcode/cluster.go new file mode 100644 index 000000000..463513c71 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/cluster.go @@ -0,0 +1,38 @@ +/* +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 + +// ErrInvalidCloudClusterProvider provider is not support now +var ErrInvalidCloudClusterProvider = NewBcode(400, 40000, "provider is not support") + +// ErrKubeConfigSecretNotSupport kubeConfig secret is not support +var ErrKubeConfigSecretNotSupport = NewBcode(400, 40001, "kubeConfig secret is not supported now") + +// ErrKubeConfigAndSecretIsNotSet kubeConfig and kubeConfigSecret are not set +var ErrKubeConfigAndSecretIsNotSet = NewBcode(400, 40002, "kubeConfig or kubeConfig secret must be provided") + +// ErrClusterNotFoundInDataStore cluster not found in datastore +var ErrClusterNotFoundInDataStore = NewBcode(404, 40003, "cluster not found in data store") + +// ErrClusterAlreadyExistInDataStore cluster exists in datastore +var ErrClusterAlreadyExistInDataStore = NewBcode(400, 40004, "cluster already exists in data store") + +// ErrGetCloudClusterFailure get cloud cluster failed +var ErrGetCloudClusterFailure = NewBcode(500, 40005, "get cloud cluster information failed") + +// ErrClusterExistsInKubernetes cluster exists in kubernetes +var ErrClusterExistsInKubernetes = NewBcode(400, 40006, "cluster already exists in kubernetes") diff --git a/pkg/apiserver/rest/utils/params.go b/pkg/apiserver/rest/utils/params.go new file mode 100644 index 000000000..aff95f34d --- /dev/null +++ b/pkg/apiserver/rest/utils/params.go @@ -0,0 +1,50 @@ +/* +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 utils + +import ( + "strconv" + + "github.com/emicklei/go-restful/v3" + "github.com/pkg/errors" +) + +// ExtractPagingParams extract `page` and `pageSize` params from request +func ExtractPagingParams(req *restful.Request, minPageSize int, maxPageSize int) (int, int, error) { + pageStr := req.QueryParameter("page") + pageSizeStr := req.QueryParameter("pageSize") + page64, err := strconv.ParseInt(pageStr, 10, 32) + if err != nil { + return 0, 0, errors.Errorf("invalid page %s: %v", pageStr, err) + } + pageSize64, err := strconv.ParseInt(pageSizeStr, 10, 32) + if err != nil { + return 0, 0, errors.Errorf("invalid pageSize %s: %v", pageSizeStr, err) + } + page := int(page64) + pageSize := int(pageSize64) + if page < 0 { + page = 0 + } + if pageSize < minPageSize { + pageSize = minPageSize + } + if pageSize > maxPageSize { + pageSize = maxPageSize + } + return page, pageSize, nil +} diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index 9d6fed598..1f4f3e548 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -22,6 +22,7 @@ import ( 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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) @@ -45,39 +46,92 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { tags := []string{"cluster"} - ws.Route(ws.GET("/").To(noop). + ws.Route(ws.GET("/").To(c.listKubeClusters). Doc("list all clusters"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). + Param(ws.QueryParameter("page", "Page for paging").DataType("int").DefaultValue("0")). + Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("int").DefaultValue("20")). + Returns(200, "", apis.ListClusterResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ListClusterResponse{}).Do(returns200, returns500)) ws.Route(ws.POST("/").To(c.createKubeCluster). Doc("create cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(&apis.CreateClusterRequest{}). + Returns(200, "", apis.ClusterBase{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ClusterBase{})) - ws.Route(ws.GET("/{clusterName}").To(noop). + ws.Route(ws.GET("/{clusterName}").To(c.getKubeCluster). Doc("detail cluster info"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). + Returns(200, "", apis.DetailClusterResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.DetailClusterResponse{})) - // Do not implement this dimension for now. - // ws.Route(ws.GET("/{clusterName}/addons").To(noop). - // Doc("list cluster addons info"). - // Metadata(restfulspec.KeyOpenAPITags, tags). - // Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). - // Writes(apis.ListClusterAddonResponse{})) + ws.Route(ws.POST("/{clusterName}").To(c.modifyKubeCluster). + Doc("modify cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). + Reads(&apis.CreateClusterRequest{}). + Returns(200, "", apis.ClusterBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ClusterBase{})) + + ws.Route(ws.DELETE("/{clusterName}").To(c.deleteKubeCluster). + Doc("delete cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). + Returns(200, "", apis.ClusterBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ClusterBase{})) + + ws.Route(ws.POST("/cloud-clusters/{provider}").To(c.listCloudClusters). + Doc("list cloud clusters"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.QueryParameter("page", "Page for paging").DataType("int").DefaultValue("0")). + Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("int").DefaultValue("20")). + Reads(&apis.AccessKeyRequest{}). + Returns(200, "", apis.ListCloudClusterResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListCloudClusterResponse{})) + + ws.Route(ws.POST("/cloud-clusters/{provider}/connect").To(c.connectCloudCluster). + Doc("create cluster from cloud cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(&apis.ConnectCloudClusterRequest{}). + Returns(200, "", apis.ClusterBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ClusterBase{})) - // ws.Route(ws.POST("/{clusterName}/addons").To(noop). - // Doc("add addon for the cluster"). - // Metadata(restfulspec.KeyOpenAPITags, tags). - // Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). - // Writes(apis.DeatilClusterAddonResponse{}).Returns(200, "", apis.DeatilClusterAddonResponse{})) return ws } +func (c *ClusterWebService) listKubeClusters(req *restful.Request, res *restful.Response) { + query := req.QueryParameter("query") + page, pageSize, err := utils.ExtractPagingParams(req, 5, 100) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + clusters, err := c.clusterUsecase.ListKubeClusters(req.Request.Context(), query, page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clusters); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + func (c *ClusterWebService) createKubeCluster(req *restful.Request, res *restful.Response) { // Verify the validity of parameters var createReq apis.CreateClusterRequest @@ -102,3 +156,125 @@ func (c *ClusterWebService) createKubeCluster(req *restful.Request, res *restful return } } + +func (c *ClusterWebService) getKubeCluster(req *restful.Request, res *restful.Response) { + clusterName := req.PathParameter("clusterName") + + // Call the usecase layer code + clusterDetail, err := c.clusterUsecase.GetKubeCluster(req.Request.Context(), clusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clusterDetail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) modifyKubeCluster(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateClusterRequest + 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 + } + clusterName := req.PathParameter("clusterName") + + // Call the usecase layer code + clusterBase, err := c.clusterUsecase.ModifyKubeCluster(req.Request.Context(), createReq, clusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clusterBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) deleteKubeCluster(req *restful.Request, res *restful.Response) { + clusterName := req.PathParameter("clusterName") + + // Call the usecase layer code + clusterBase, err := c.clusterUsecase.DeleteKubeCluster(req.Request.Context(), clusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clusterBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) listCloudClusters(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + page, pageSize, err := utils.ExtractPagingParams(req, 5, 100) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Verify the validity of parameters + var accessKeyRequest apis.AccessKeyRequest + if err := req.ReadEntity(&accessKeyRequest); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&accessKeyRequest); err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + clustersResp, err := c.clusterUsecase.ListCloudClusters(req.Request.Context(), provider, accessKeyRequest, page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clustersResp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) connectCloudCluster(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + + // Verify the validity of parameters + var connectReq apis.ConnectCloudClusterRequest + if err := req.ReadEntity(&connectReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&connectReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + cluster, err := c.clusterUsecase.ConnectCloudCluster(req.Request.Context(), provider, connectReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(cluster); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/cloudprovider/aliyun.go b/pkg/cloudprovider/aliyun.go new file mode 100644 index 000000000..6ccf9985b --- /dev/null +++ b/pkg/cloudprovider/aliyun.go @@ -0,0 +1,128 @@ +/* +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 cloudprovider + +import ( + "encoding/json" + + cs20151215 "github.com/alibabacloud-go/cs-20151215/v2/client" + openapi "github.com/alibabacloud-go/darabonba-openapi/client" + "github.com/alibabacloud-go/tea/tea" + "k8s.io/klog/v2" + "k8s.io/utils/pointer" +) + +const ( + aliyunAPIEndpoint = "cs.cn-hangzhou.aliyuncs.com" +) + +// AliyunCloudProvider describes the cloud provider in aliyun +type AliyunCloudProvider struct { + *cs20151215.Client +} + +// NewAliyunCloudProvider create aliyun cloud provider +func NewAliyunCloudProvider(accessKeyID string, accessKeySecret string) (*AliyunCloudProvider, error) { + config := &openapi.Config{ + AccessKeyId: pointer.String(accessKeyID), + AccessKeySecret: pointer.String(accessKeySecret), + } + config.Endpoint = tea.String(aliyunAPIEndpoint) + c, err := cs20151215.NewClient(config) + if err != nil { + return nil, err + } + return &AliyunCloudProvider{Client: c}, nil +} + +func (provider *AliyunCloudProvider) decodeClusterLabels(tags []*cs20151215.Tag) map[string]string { + labels := map[string]string{} + for _, tag := range tags { + labels[*tag.Key] = *tag.Value + } + return labels +} + +func (provider *AliyunCloudProvider) decodeClusterURL(masterURL string) (url struct { + APIServerEndpoint string `json:"api_server_endpoint"` + DashboardEndpoint string `json:"dashboardEndpoint"` + IntranetAPIServerEndpoint string `json:"intranet_api_server_endpoint"` +}) { + if err := json.Unmarshal([]byte(masterURL), &url); err != nil { + klog.Info("failed to unmarshal masterUrl %s", masterURL) + } + return +} + +// ListCloudClusters list clusters with page info, return clusters, total count and error +func (provider *AliyunCloudProvider) ListCloudClusters(pageNumber int, pageSize int) ([]*CloudCluster, int, error) { + describeClustersV1Request := &cs20151215.DescribeClustersV1Request{ + PageSize: pointer.Int64(int64(pageSize)), + PageNumber: pointer.Int64(int64(pageNumber)), + } + resp, err := provider.DescribeClustersV1(describeClustersV1Request) + if err != nil { + return nil, 0, err + } + var clusters []*CloudCluster + for _, cluster := range resp.Body.Clusters { + labels := provider.decodeClusterLabels(cluster.Tags) + url := provider.decodeClusterURL(*cluster.MasterUrl) + clusters = append(clusters, &CloudCluster{ + ID: *cluster.ClusterId, + Name: *cluster.Name, + Type: *cluster.ClusterType, + Zone: *cluster.ZoneId, + Labels: labels, + Status: *cluster.State, + APIServerURL: url.APIServerEndpoint, + DashBoardURL: url.DashboardEndpoint, + }) + } + return clusters, int(*resp.Body.PageInfo.TotalCount), nil +} + +// GetClusterKubeConfig get cluster kubeconfig by clusterID +func (provider *AliyunCloudProvider) GetClusterKubeConfig(clusterID string) (string, error) { + req := &cs20151215.DescribeClusterUserKubeconfigRequest{} + resp, err := provider.DescribeClusterUserKubeconfig(pointer.String(clusterID), req) + if err != nil { + return "", err + } + return *resp.Body.Config, nil +} + +// GetClusterInfo retrieves cluster info by clusterID +func (provider *AliyunCloudProvider) GetClusterInfo(clusterID string) (*CloudCluster, error) { + resp, err := provider.DescribeClusterDetail(pointer.String(clusterID)) + if err != nil { + return nil, err + } + cluster := resp.Body + labels := provider.decodeClusterLabels(cluster.Tags) + url := provider.decodeClusterURL(*cluster.MasterUrl) + return &CloudCluster{ + ID: *cluster.ClusterId, + Name: *cluster.Name, + Type: *cluster.ClusterType, + Zone: *cluster.ZoneId, + Labels: labels, + Status: *cluster.State, + APIServerURL: url.APIServerEndpoint, + DashBoardURL: url.DashboardEndpoint, + }, nil +} diff --git a/pkg/cloudprovider/cluster.go b/pkg/cloudprovider/cluster.go new file mode 100644 index 000000000..5f1c530cc --- /dev/null +++ b/pkg/cloudprovider/cluster.go @@ -0,0 +1,38 @@ +/* +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 cloudprovider + +import ( + "github.com/pkg/errors" +) + +// CloudClusterProvider abstracts the cloud provider to provide cluster access +type CloudClusterProvider interface { + ListCloudClusters(pageNumber int, pageSize int) ([]*CloudCluster, int, error) + GetClusterKubeConfig(clusterID string) (string, error) + GetClusterInfo(clusterID string) (*CloudCluster, error) +} + +// GetClusterProvider creates interface for getting cloud cluster provider +func GetClusterProvider(provider string, accessKeyID string, accessKeySecret string) (CloudClusterProvider, error) { + switch provider { + case ProviderAliyun: + return NewAliyunCloudProvider(accessKeyID, accessKeySecret) + default: + return nil, errors.Errorf("cluster provider %s is not implemented", provider) + } +} diff --git a/pkg/cloudprovider/types.go b/pkg/cloudprovider/types.go new file mode 100644 index 000000000..319643d06 --- /dev/null +++ b/pkg/cloudprovider/types.go @@ -0,0 +1,34 @@ +/* +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 cloudprovider + +const ( + // ProviderAliyun cloud provider aliyun + ProviderAliyun = "aliyun" +) + +// CloudCluster describes the interface that cloud provider should return +type CloudCluster struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Zone string `json:"zone"` + Labels map[string]string `json:"labels"` + Status string `json:"status"` + APIServerURL string `json:"apiServerURL"` + DashBoardURL string `json:"dashboardURL"` +} diff --git a/pkg/multicluster/cluster_management.go b/pkg/multicluster/cluster_management.go new file mode 100644 index 000000000..8a45de977 --- /dev/null +++ b/pkg/multicluster/cluster_management.go @@ -0,0 +1,268 @@ +/* +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 multicluster + +import ( + "context" + "fmt" + + v1alpha12 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + v14 "k8s.io/api/storage/v1" + v13 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + errors2 "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + types2 "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/oam" + errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" +) + +// ensureResourceTrackerCRDInstalled ensures resourcetracker to be installed in child cluster +func ensureResourceTrackerCRDInstalled(ctx context.Context, c client.Client, clusterName string) error { + remoteCtx := ContextWithClusterName(ctx, clusterName) + crdName := types2.NamespacedName{Name: "resourcetrackers." + v1beta1.Group} + if err := c.Get(remoteCtx, crdName, &v13.CustomResourceDefinition{}); err != nil { + if !errors2.IsNotFound(err) { + return errors.Wrapf(err, "failed to check resourcetracker crd in cluster %s", clusterName) + } + crd := &v13.CustomResourceDefinition{} + if err = c.Get(ctx, crdName, crd); err != nil { + return errors.Wrapf(err, "failed to get resourcetracker crd in hub cluster") + } + crd.ObjectMeta = v12.ObjectMeta{ + Name: crdName.Name, + Annotations: crd.Annotations, + Labels: crd.Labels, + } + if err = c.Create(remoteCtx, crd); err != nil { + return errors.Wrapf(err, "failed to create resourcetracker crd in cluster %s", clusterName) + } + } + return nil +} + +// ensureClusterNotExists checks if child cluster has already been joined, if joined, error is returned +func ensureClusterNotExists(ctx context.Context, c client.Client, clusterName string) error { + secret := &v1.Secret{} + err := c.Get(ctx, types2.NamespacedName{Name: clusterName, Namespace: ClusterGatewaySecretNamespace}, secret) + if err == nil { + return ErrClusterExists + } + if !errors2.IsNotFound(err) { + return errors.Wrapf(err, "failed to check duplicate cluster secret") + } + return nil +} + +// getMutableClusterSecret retrieves the cluster secret and check if any application is using the cluster +func getMutableClusterSecret(ctx context.Context, c client.Client, clusterName string) (*v1.Secret, error) { + clusterSecret := &v1.Secret{} + if err := c.Get(ctx, types2.NamespacedName{Namespace: ClusterGatewaySecretNamespace, Name: clusterName}, clusterSecret); err != nil { + return nil, errors.Wrapf(err, "failed to find target cluster secret %s", clusterName) + } + labels := clusterSecret.GetLabels() + if labels == nil || labels[v1alpha12.LabelKeyClusterCredentialType] == "" { + return nil, fmt.Errorf("invalid cluster secret %s: cluster credential type label %s is not set", clusterName, v1alpha12.LabelKeyClusterCredentialType) + } + ebs := &v1alpha1.EnvBindingList{} + if err := c.List(ctx, ebs); err != nil { + return nil, errors.Wrap(err, "failed to find EnvBindings to check clusters") + } + errs := errors3.ErrorList{} + for _, eb := range ebs.Items { + for _, decision := range eb.Status.ClusterDecisions { + if decision.Cluster == clusterName { + errs.Append(fmt.Errorf("application %s/%s (env: %s, envBinding: %s) is currently using cluster %s", eb.Namespace, eb.Labels[oam.LabelAppName], decision.Env, eb.Name, clusterName)) + } + } + } + if errs.HasError() { + return nil, errors.Wrapf(errs, "cluster %s is in use now", clusterName) + } + return clusterSecret, nil +} + +// JoinClusterByKubeConfig add child cluster by kubeconfig path, return cluster info and error +func JoinClusterByKubeConfig(_ctx context.Context, k8sClient client.Client, kubeconfigPath string, clusterName string) (*api.Cluster, error) { + config, err := clientcmd.LoadFromFile(kubeconfigPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to get kubeconfig") + } + if len(config.CurrentContext) == 0 { + return nil, fmt.Errorf("current-context is not set") + } + ctx, ok := config.Contexts[config.CurrentContext] + if !ok { + return nil, fmt.Errorf("current-context %s not found", config.CurrentContext) + } + cluster, ok := config.Clusters[ctx.Cluster] + if !ok { + return nil, fmt.Errorf("cluster %s not found", ctx.Cluster) + } + authInfo, ok := config.AuthInfos[ctx.AuthInfo] + if !ok { + return nil, fmt.Errorf("authInfo %s not found", ctx.AuthInfo) + } + + if clusterName == "" { + clusterName = ctx.Cluster + } + if clusterName == ClusterLocalName { + return cluster, fmt.Errorf("cannot use `%s` as cluster name, it is reserved as the local cluster", ClusterLocalName) + } + + if err := ensureClusterNotExists(_ctx, k8sClient, clusterName); err != nil { + return cluster, errors.Wrapf(err, "cannot use cluster name %s", clusterName) + } + + var credentialType v1alpha12.CredentialType + data := map[string][]byte{ + "endpoint": []byte(cluster.Server), + "ca.crt": cluster.CertificateAuthorityData, + } + if len(authInfo.Token) > 0 { + credentialType = v1alpha12.CredentialTypeServiceAccountToken + data["token"] = []byte(authInfo.Token) + } else { + credentialType = v1alpha12.CredentialTypeX509Certificate + data["tls.crt"] = authInfo.ClientCertificateData + data["tls.key"] = authInfo.ClientKeyData + } + secret := &v1.Secret{ + ObjectMeta: v12.ObjectMeta{ + Name: clusterName, + Namespace: ClusterGatewaySecretNamespace, + Labels: map[string]string{ + v1alpha12.LabelKeyClusterCredentialType: string(credentialType), + }, + }, + Type: v1.SecretTypeOpaque, + Data: data, + } + + if err := k8sClient.Create(_ctx, secret); err != nil { + return cluster, errors.Wrapf(err, "failed to add cluster to kubernetes") + } + if err := ensureResourceTrackerCRDInstalled(_ctx, k8sClient, clusterName); err != nil { + _ = k8sClient.Delete(_ctx, secret) + return cluster, errors.Wrapf(err, "failed to ensure resourcetracker crd installed in cluster %s", clusterName) + } + return cluster, nil +} + +// DetachCluster detach cluster by name, if cluster is using by application, it will return error +func DetachCluster(ctx context.Context, k8sClient client.Client, clusterName string) error { + if clusterName == ClusterLocalName { + return ErrReservedLocalClusterName + } + clusterSecret, err := getMutableClusterSecret(ctx, k8sClient, clusterName) + if err != nil { + return errors.Wrapf(err, "cluster %s is not mutable now", clusterName) + } + return k8sClient.Delete(ctx, clusterSecret) +} + +// RenameCluster rename cluster +func RenameCluster(ctx context.Context, k8sClient client.Client, oldClusterName string, newClusterName string) error { + if newClusterName == ClusterLocalName { + return ErrReservedLocalClusterName + } + clusterSecret, err := getMutableClusterSecret(ctx, k8sClient, oldClusterName) + if err != nil { + return errors.Wrapf(err, "cluster %s is not mutable now", oldClusterName) + } + if err := ensureClusterNotExists(ctx, k8sClient, newClusterName); err != nil { + return errors.Wrapf(err, "cannot set cluster name to %s", newClusterName) + } + if err := k8sClient.Delete(ctx, clusterSecret); err != nil { + return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName) + } + clusterSecret.ObjectMeta = v12.ObjectMeta{ + Name: newClusterName, + Namespace: ClusterGatewaySecretNamespace, + Labels: clusterSecret.Labels, + Annotations: clusterSecret.Annotations, + } + if err := k8sClient.Create(ctx, clusterSecret); err != nil { + return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName) + } + return nil +} + +// ClusterInfo describes the basic information of a cluster +type ClusterInfo struct { + Nodes *v1.NodeList + WorkerNumber int + MasterNumber int + MemoryCapacity resource.Quantity + CPUCapacity resource.Quantity + PodCapacity resource.Quantity + MemoryAllocatable resource.Quantity + CPUAllocatable resource.Quantity + PodAllocatable resource.Quantity + StorageClasses *v14.StorageClassList +} + +// GetClusterInfo retrieves current cluster info from cluster +func GetClusterInfo(_ctx context.Context, k8sClient client.Client, clusterName string) (*ClusterInfo, error) { + ctx := ContextWithClusterName(_ctx, clusterName) + nodes := &v1.NodeList{} + if err := k8sClient.List(ctx, nodes); err != nil { + return nil, errors.Wrapf(err, "failed to list cluster nodes") + } + var workerNumber, masterNumber int + var memoryCapacity, cpuCapacity, podCapacity, memoryAllocatable, cpuAllocatable, podAllcatable resource.Quantity + for _, node := range nodes.Items { + if _, ok := node.Labels["node-role.kubernetes.io/master"]; ok { + masterNumber++ + } else { + workerNumber++ + } + capacity := node.Status.Capacity + memoryCapacity.Add(*capacity.Memory()) + cpuCapacity.Add(*capacity.Cpu()) + podCapacity.Add(*capacity.Pods()) + allocatable := node.Status.Allocatable + memoryAllocatable.Add(*allocatable.Memory()) + cpuAllocatable.Add(*allocatable.Cpu()) + podAllcatable.Add(*allocatable.Pods()) + } + storageClasses := &v14.StorageClassList{} + if err := k8sClient.List(ctx, storageClasses); err != nil { + return nil, errors.Wrapf(err, "failed to list storage classes") + } + return &ClusterInfo{ + Nodes: nodes, + WorkerNumber: workerNumber, + MasterNumber: masterNumber, + MemoryCapacity: memoryCapacity, + CPUCapacity: cpuCapacity, + PodCapacity: podCapacity, + MemoryAllocatable: memoryAllocatable, + CPUAllocatable: cpuAllocatable, + PodAllocatable: podAllcatable, + StorageClasses: storageClasses, + }, nil +} diff --git a/pkg/multicluster/errors.go b/pkg/multicluster/errors.go new file mode 100644 index 000000000..99b1432e9 --- /dev/null +++ b/pkg/multicluster/errors.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 multicluster + +import "fmt" + +var ( + // ErrClusterExists cluster already exists + ErrClusterExists = ClusterManagementError(fmt.Errorf("cluster already exists")) + // ErrReservedLocalClusterName reserved cluster name is used + ErrReservedLocalClusterName = ClusterManagementError(fmt.Errorf("cluster name `local` is reserved for kubevela hub cluster")) +) + +// ClusterManagementError multicluster management error +type ClusterManagementError error diff --git a/pkg/multicluster/utils.go b/pkg/multicluster/utils.go index 31428e53c..9f8182bdd 100644 --- a/pkg/multicluster/utils.go +++ b/pkg/multicluster/utils.go @@ -33,6 +33,7 @@ import ( "k8s.io/klog/v2" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" "github.com/oam-dev/kubevela/pkg/utils/common" errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" @@ -114,23 +115,25 @@ func WaitUntilClusterGatewayReady(ctx context.Context, c client.Client, maxRetry // Initialize prepare multicluster environment by checking cluster gateway service in clusters and hack rest config to use cluster gateway // if cluster gateway service is not ready, it will wait up to 5 minutes -func Initialize(restConfig *rest.Config) error { +func Initialize(restConfig *rest.Config, autoUpgrade bool) (client.Client, error) { c, err := client.New(restConfig, client.Options{Scheme: common.Scheme}) if err != nil { - return errors2.Wrapf(err, "unable to get client to find cluster gateway service") + return nil, errors2.Wrapf(err, "unable to get client to find cluster gateway service") } svc, err := WaitUntilClusterGatewayReady(context.Background(), c, 60, 5*time.Second) if err != nil { - return errors2.Wrapf(err, "failed to wait for cluster gateway, unable to use multi-cluster") + return nil, errors2.Wrapf(err, "failed to wait for cluster gateway, unable to use multi-cluster") } ClusterGatewaySecretNamespace = svc.Namespace klog.Infof("find cluster gateway service %s/%s:%d", svc.Namespace, svc.Name, *svc.Port) restConfig.Wrap(NewSecretModeMultiClusterRoundTripper) - if err = UpgradeExistingClusterSecret(context.Background(), c); err != nil { - // this error do not affect the running of current version - klog.ErrorS(err, "error encountered while grading existing cluster secret to the latest version") + if autoUpgrade { + if err = UpgradeExistingClusterSecret(context.Background(), c); err != nil { + // this error do not affect the running of current version + klog.ErrorS(err, "error encountered while grading existing cluster secret to the latest version") + } } - return nil + return c, nil } // UpgradeExistingClusterSecret upgrade outdated cluster secrets in v1.1.1 to latest @@ -157,3 +160,12 @@ func UpgradeExistingClusterSecret(ctx context.Context, c client.Client) error { } return nil } + +// GetMulticlusterKubernetesClient get client with multicluster function enabled +func GetMulticlusterKubernetesClient() (client.Client, error) { + k8sConfig, err := config.GetConfig() + if err != nil { + return nil, err + } + return Initialize(k8sConfig, false) +} diff --git a/references/cli/cluster.go b/references/cli/cluster.go index 1e285952d..570e6df2e 100644 --- a/references/cli/cluster.go +++ b/references/cli/cluster.go @@ -18,26 +18,16 @@ package cli import ( "context" - "fmt" v1alpha12 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" "github.com/pkg/errors" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" - v13 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - errors2 "k8s.io/apimachinery/pkg/api/errors" - v12 "k8s.io/apimachinery/pkg/apis/meta/v1" - types2 "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/multicluster" - "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/utils/common" - errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" "github.com/oam-dev/kubevela/references/a/preimport" ) @@ -115,42 +105,6 @@ func NewClusterListCommand(c *common.Args) *cobra.Command { return cmd } -func ensureClusterNotExists(c client.Client, clusterName string) error { - secret := &v1.Secret{} - err := c.Get(context.Background(), types2.NamespacedName{Name: clusterName, Namespace: multicluster.ClusterGatewaySecretNamespace}, secret) - if err == nil { - return fmt.Errorf("cluster %s already exists", clusterName) - } - if !errors2.IsNotFound(err) { - return errors.Wrapf(err, "failed to check duplicate cluster secret") - } - return nil -} - -func ensureResourceTrackerCRDInstalled(c client.Client, clusterName string) error { - ctx := context.Background() - remoteCtx := multicluster.ContextWithClusterName(ctx, clusterName) - crdName := types2.NamespacedName{Name: "resourcetrackers." + v1beta1.Group} - if err := c.Get(remoteCtx, crdName, &v13.CustomResourceDefinition{}); err != nil { - if !errors2.IsNotFound(err) { - return errors.Wrapf(err, "failed to check resourcetracker crd in cluster %s", clusterName) - } - crd := &v13.CustomResourceDefinition{} - if err = c.Get(ctx, crdName, crd); err != nil { - return errors.Wrapf(err, "failed to get resourcetracker crd in hub cluster") - } - crd.ObjectMeta = v12.ObjectMeta{ - Name: crdName.Name, - Annotations: crd.Annotations, - Labels: crd.Labels, - } - if err = c.Create(remoteCtx, crd); err != nil { - return errors.Wrapf(err, "failed to create resourcetracker crd in cluster %s", clusterName) - } - } - return nil -} - // NewClusterJoinCommand create command to help user join cluster to multicluster management func NewClusterJoinCommand(c *common.Args) *cobra.Command { cmd := &cobra.Command{ @@ -161,71 +115,14 @@ func NewClusterJoinCommand(c *common.Args) *cobra.Command { "> vela cluster join my-child-cluster.kubeconfig --name example-cluster", Args: cobra.ExactValidArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - config, err := clientcmd.LoadFromFile(args[0]) - if err != nil { - return errors.Wrapf(err, "failed to get kubeconfig") - } - if len(config.CurrentContext) == 0 { - return fmt.Errorf("current-context is not set") - } - ctx, ok := config.Contexts[config.CurrentContext] - if !ok { - return fmt.Errorf("current-context %s not found", config.CurrentContext) - } - cluster, ok := config.Clusters[ctx.Cluster] - if !ok { - return fmt.Errorf("cluster %s not found", ctx.Cluster) - } - authInfo, ok := config.AuthInfos[ctx.AuthInfo] - if !ok { - return fmt.Errorf("authInfo %s not found", ctx.AuthInfo) - } - // get ClusterName from flag or config clusterName, err := cmd.Flags().GetString(FlagClusterName) if err != nil { return errors.Wrapf(err, "failed to get cluster name flag") } - if clusterName == "" { - clusterName = ctx.Cluster - } - if clusterName == multicluster.ClusterLocalName { - return fmt.Errorf("cannot use `%s` as cluster name, it is reserved as the local cluster", multicluster.ClusterLocalName) - } - - if err := ensureClusterNotExists(c.Client, clusterName); err != nil { - return errors.Wrapf(err, "cannot use cluster name %s", clusterName) - } - var credentialType v1alpha12.CredentialType - data := map[string][]byte{ - "endpoint": []byte(cluster.Server), - "ca.crt": cluster.CertificateAuthorityData, - } - if len(authInfo.Token) > 0 { - credentialType = v1alpha12.CredentialTypeServiceAccountToken - data["token"] = []byte(authInfo.Token) - } else { - credentialType = v1alpha12.CredentialTypeX509Certificate - data["tls.crt"] = authInfo.ClientCertificateData - data["tls.key"] = authInfo.ClientKeyData - } - secret := &v1.Secret{ - ObjectMeta: v12.ObjectMeta{ - Name: clusterName, - Namespace: multicluster.ClusterGatewaySecretNamespace, - Labels: map[string]string{ - v1alpha12.LabelKeyClusterCredentialType: string(credentialType), - }, - }, - Type: v1.SecretTypeOpaque, - Data: data, - } - if err := c.Client.Create(context.Background(), secret); err != nil { - return errors.Wrapf(err, "failed to add cluster to kubernetes") - } - if err := ensureResourceTrackerCRDInstalled(c.Client, clusterName); err != nil { - _ = c.Client.Delete(context.Background(), secret) - return errors.Wrapf(err, "failed to ensure resourcetracker crd installed in cluster %s", clusterName) + cluster, err := multicluster.JoinClusterByKubeConfig(context.Background(), c.Client, args[0], clusterName) + if err != nil { + return err } cmd.Printf("Successfully add cluster %s, endpoint: %s.\n", clusterName, cluster.Server) return nil @@ -235,33 +132,6 @@ func NewClusterJoinCommand(c *common.Args) *cobra.Command { return cmd } -func getMutableClusterSecret(c client.Client, clusterName string) (*v1.Secret, error) { - clusterSecret := &v1.Secret{} - if err := c.Get(context.Background(), types2.NamespacedName{Namespace: multicluster.ClusterGatewaySecretNamespace, Name: clusterName}, clusterSecret); err != nil { - return nil, errors.Wrapf(err, "failed to find target cluster secret %s", clusterName) - } - labels := clusterSecret.GetLabels() - if labels == nil || labels[v1alpha12.LabelKeyClusterCredentialType] == "" { - return nil, fmt.Errorf("invalid cluster secret %s: cluster credential type label %s is not set", clusterName, v1alpha12.LabelKeyClusterCredentialType) - } - ebs := &v1alpha1.EnvBindingList{} - if err := c.List(context.Background(), ebs); err != nil { - return nil, errors.Wrap(err, "failed to find EnvBindings to check clusters") - } - errs := errors3.ErrorList{} - for _, eb := range ebs.Items { - for _, decision := range eb.Status.ClusterDecisions { - if decision.Cluster == clusterName { - errs.Append(fmt.Errorf("application %s/%s (env: %s, envBinding: %s) is currently using cluster %s", eb.Namespace, eb.Labels[oam.LabelAppName], decision.Env, eb.Name, clusterName)) - } - } - } - if errs.HasError() { - return nil, errors.Wrapf(errs, "cluster %s is in use now", clusterName) - } - return clusterSecret, nil -} - // NewClusterRenameCommand create command to help user rename cluster func NewClusterRenameCommand(c *common.Args) *cobra.Command { cmd := &cobra.Command{ @@ -271,27 +141,8 @@ func NewClusterRenameCommand(c *common.Args) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { oldClusterName := args[0] newClusterName := args[1] - if newClusterName == multicluster.ClusterLocalName { - return fmt.Errorf("cannot use `%s` as cluster name, it is reserved as the local cluster", multicluster.ClusterLocalName) - } - clusterSecret, err := getMutableClusterSecret(c.Client, oldClusterName) - if err != nil { - return errors.Wrapf(err, "cluster %s is not mutable now", oldClusterName) - } - if err := ensureClusterNotExists(c.Client, newClusterName); err != nil { - return errors.Wrapf(err, "cannot set cluster name to %s", newClusterName) - } - if err := c.Client.Delete(context.Background(), clusterSecret); err != nil { - return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName) - } - clusterSecret.ObjectMeta = v12.ObjectMeta{ - Name: newClusterName, - Namespace: multicluster.ClusterGatewaySecretNamespace, - Labels: clusterSecret.Labels, - Annotations: clusterSecret.Annotations, - } - if err := c.Client.Create(context.Background(), clusterSecret); err != nil { - return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName) + if err := multicluster.RenameCluster(context.Background(), c.Client, oldClusterName, newClusterName); err != nil { + return err } cmd.Printf("Rename cluster %s to %s successfully.\n", oldClusterName, newClusterName) return nil @@ -308,15 +159,8 @@ func NewClusterDetachCommand(c *common.Args) *cobra.Command { Args: cobra.ExactValidArgs(1), RunE: func(cmd *cobra.Command, args []string) error { clusterName := args[0] - if clusterName == multicluster.ClusterLocalName { - return fmt.Errorf("cannot delete `%s` cluster, it is reserved as the local cluster", multicluster.ClusterLocalName) - } - clusterSecret, err := getMutableClusterSecret(c.Client, clusterName) - if err != nil { - return errors.Wrapf(err, "cluster %s is not mutable now", clusterName) - } - if err := c.Client.Delete(context.Background(), clusterSecret); err != nil { - return errors.Wrapf(err, "failed to detach cluster %s", clusterName) + if err := multicluster.DetachCluster(context.Background(), c.Client, clusterName); err != nil { + return err } cmd.Printf("Detach cluster %s successfully.\n", clusterName) return nil diff --git a/test/e2e-apiserver-test/cluster_test.go b/test/e2e-apiserver-test/cluster_test.go new file mode 100644 index 000000000..180dbb7f1 --- /dev/null +++ b/test/e2e-apiserver-test/cluster_test.go @@ -0,0 +1,133 @@ +/* +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 + +import ( + "io/ioutil" + "net/http" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + util "github.com/oam-dev/kubevela/pkg/utils" +) + +const ( + WorkerClusterName = "cluster-worker" + WorkerClusterKubeConfigPath = "/tmp/worker.kubeconfig" +) + +var _ = Describe("Test cluster rest api", func() { + + Context("Test basic cluster CURD", func() { + + var clusterName string + + BeforeEach(func() { + clusterName = WorkerClusterName + "-" + util.RandomString(8) + kubeconfigBytes, err := ioutil.ReadFile(WorkerClusterKubeConfigPath) + Expect(err).Should(Succeed()) + resp, err := CreateRequest(http.MethodPost, "/clusters", v1.CreateClusterRequest{ + Name: clusterName, + KubeConfig: string(kubeconfigBytes), + }) + Expect(err).Should(Succeed()) + Expect(resp.StatusCode).Should(Equal(200)) + Expect(resp.Body).ShouldNot(BeNil()) + Expect(resp.Body.Close()).Should(Succeed()) + }) + + AfterEach(func() { + resp, err := CreateRequest(http.MethodDelete, "/clusters/"+clusterName, nil) + Expect(err).Should(Succeed()) + Expect(resp.StatusCode).Should(Equal(200)) + Expect(resp.Body).ShouldNot(BeNil()) + Expect(resp.Body.Close()).Should(Succeed()) + }) + + It("Test get cluster", func() { + resp, err := CreateRequest(http.MethodGet, "/clusters/"+clusterName, nil) + clusterResp := &v1.DetailClusterResponse{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(clusterResp.Status).Should(Equal("Healthy")) + }) + + It("Test get clusters", func() { + resp, err := CreateRequest(http.MethodGet, "/clusters/?page=1&pageSize=5", nil) + clusterResp := &v1.ListClusterResponse{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(len(clusterResp.Clusters)).ShouldNot(Equal(0)) + }) + + It("Test modify cluster", func() { + kubeconfigBytes, err := ioutil.ReadFile(WorkerClusterKubeConfigPath) + Expect(err).Should(Succeed()) + resp, err := CreateRequest(http.MethodPost, "/clusters/"+clusterName, v1.CreateClusterRequest{ + Name: clusterName, + KubeConfig: string(kubeconfigBytes), + Description: "Example description", + }) + clusterResp := &v1.ClusterBase{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(clusterResp.Description).ShouldNot(Equal("")) + }) + + }) + + PContext("Test cloud cluster rest api", func() { + + var clusterName string + + BeforeEach(func() { + clusterName = WorkerClusterName + "-" + util.RandomString(8) + }) + + AfterEach(func() { + resp, err := CreateRequest(http.MethodDelete, "/clusters/"+clusterName, nil) + Expect(err).Should(Succeed()) + Expect(resp.StatusCode).Should(Equal(200)) + Expect(resp.Body).ShouldNot(BeNil()) + Expect(resp.Body.Close()).Should(Succeed()) + }) + + It("Test list aliyun cloud cluster and connect", func() { + AccessKeyID := os.Getenv("ALIYUN_ACCESS_KEY_ID") + AccessKeySecret := os.Getenv("ALIYUN_ACCESS_KEY_SECRET") + resp, err := CreateRequest(http.MethodPost, "/clusters/cloud-clusters/aliyun/?page=1&pageSize=5", v1.AccessKeyRequest{ + AccessKeyID: AccessKeyID, + AccessKeySecret: AccessKeySecret, + }) + clusterResp := &v1.ListCloudClusterResponse{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(len(clusterResp.Clusters)).ShouldNot(Equal(0)) + + ClusterID := clusterResp.Clusters[0].ID + resp, err = CreateRequest(http.MethodPost, "/clusters/cloud-clusters/aliyun/connect", v1.ConnectCloudClusterRequest{ + AccessKeyID: AccessKeyID, + AccessKeySecret: AccessKeySecret, + ClusterID: ClusterID, + Name: clusterName, + }) + clusterBase := &v1.ClusterBase{} + Expect(DecodeResponseBody(resp, err, clusterBase)).Should(Succeed()) + Expect(clusterBase.Status).Should(Equal("Healthy")) + }) + + }) +}) diff --git a/test/e2e-apiserver-test/oam_application_test.go b/test/e2e-apiserver-test/oam_application_test.go index 4048541df..06f11cf08 100644 --- a/test/e2e-apiserver-test/oam_application_test.go +++ b/test/e2e-apiserver-test/oam_application_test.go @@ -22,6 +22,7 @@ import ( "encoding/json" "fmt" "net/http" + "time" "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" @@ -74,17 +75,18 @@ var _ = Describe("Test oam application rest api", func() { } bodyByte, err = json.Marshal(updateReq) Expect(err).Should(BeNil()) - res, err = http.Post( - fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), - "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() - + Eventually(func(g Gomega) { + res, err = http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + "application/json", + bytes.NewBuffer(bodyByte), + ) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).ShouldNot(BeNil()) + g.Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + g.Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + }, time.Minute).Should(Succeed()) newApp := new(v1beta1.Application) Expect(k8sClient.Get(ctx, client.ObjectKey{Name: appName, Namespace: namespace}, newApp)).Should(BeNil()) Expect(newApp.Spec.Components).Should(Equal(updateReq.Components)) diff --git a/test/e2e-apiserver-test/suite_test.go b/test/e2e-apiserver-test/suite_test.go index bcc14d355..4014e9af8 100644 --- a/test/e2e-apiserver-test/suite_test.go +++ b/test/e2e-apiserver-test/suite_test.go @@ -23,50 +23,28 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "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" - "github.com/oam-dev/kubevela/pkg/utils/common" ) -var cfg *rest.Config var k8sClient client.Client -var testEnv *envtest.Environment func TestE2eApiserverTest(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "E2eApiserverTest Suite") } +// Suite test in e2e-apiserver-test relies on the pre-setup kubernetes environment var _ = BeforeSuite(func() { - - By("bootstrapping test environment") - - testEnv = &envtest.Environment{ - ControlPlaneStartTimeout: time.Minute * 3, - ControlPlaneStopTimeout: time.Minute, - UseExistingCluster: pointer.BoolPtr(false), - CRDDirectoryPaths: []string{"../../charts/vela-core/crds"}, - } - - By("start kube test env") - var err error - cfg, err = testEnv.Start() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg).ToNot(BeNil()) - By("new kube client") - cfg.Timeout = time.Minute * 2 - k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) + var err error + k8sClient, err = clients.GetKubeClient() Expect(err).Should(BeNil()) Expect(k8sClient).ToNot(BeNil()) By("new kube client success") - clients.SetKubeClient(k8sClient) ctx := context.Background() @@ -86,9 +64,3 @@ var _ = BeforeSuite(func() { 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()) -}) diff --git a/test/e2e-apiserver-test/utils.go b/test/e2e-apiserver-test/utils.go new file mode 100644 index 000000000..c3725b650 --- /dev/null +++ b/test/e2e-apiserver-test/utils.go @@ -0,0 +1,59 @@ +/* +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 + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +// CreateRequest wraps request +func CreateRequest(method string, path string, body interface{}) (*http.Response, error) { + if body == nil { + body = map[string]string{} + } + bs, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest(method, "http://127.0.0.1:8000/api/v1"+path, bytes.NewBuffer(bs)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +// DecodeResponseBody decode response and close response +func DecodeResponseBody(resp *http.Response, err error, dst interface{}) error { + if err != nil { + return err + } + if resp.StatusCode != 200 { + return fmt.Errorf("response code is not 200: %d", resp.StatusCode) + } + if resp.Body == nil { + return fmt.Errorf("response body is nil") + } + err = json.NewDecoder(resp.Body).Decode(dst) + if err != nil { + return err + } + return resp.Body.Close() +} From 964a12bb4427ce8cd337c93b549975a319faa4ec Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Tue, 26 Oct 2021 17:50:30 +0800 Subject: [PATCH 06/59] Feat: support manage multiple workflows in one application. (#2533) * Docs: change swagger json * Feat: support manage multiple workflows in one application. * Docs: update swagger doc * Fix: fix code bug * Update pkg/apiserver/rest/webservice/workflow.go Co-authored-by: Tianxin Dong Co-authored-by: barnettZQG Co-authored-by: Tianxin Dong --- docs/apidoc/swagger.json | 674 ++++++++++++++++-- pkg/apiserver/datastore/kubeapi/kubeapi.go | 2 + pkg/apiserver/model/application.go | 7 + pkg/apiserver/model/cluster.go | 4 + pkg/apiserver/model/model.go | 17 + pkg/apiserver/model/workflow.go | 41 +- pkg/apiserver/rest/apis/v1/types.go | 39 +- pkg/apiserver/rest/usecase/application.go | 58 +- .../rest/usecase/application_test.go | 3 +- pkg/apiserver/rest/usecase/workflow.go | 103 ++- pkg/apiserver/rest/usecase/workflow_test.go | 68 ++ pkg/apiserver/rest/utils/bcode/workflow.go | 6 + pkg/apiserver/rest/webservice/workflow.go | 158 +++- pkg/oam/labels.go | 6 + 14 files changed, 1091 insertions(+), 95 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/workflow_test.go diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 890e69b0c..9f4aa3991 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -621,13 +621,27 @@ "cluster" ], "summary": "list all clusters", - "operationId": "noop", + "operationId": "listKubeClusters", "parameters": [ { "type": "string", "description": "Fuzzy search based on name or description", "name": "query", "in": "query" + }, + { + "type": "int", + "default": 0, + "description": "Page for paging", + "name": "page", + "in": "query" + }, + { + "type": "int", + "default": 20, + "description": "PageSize for paging", + "name": "pageSize", + "in": "query" } ], "responses": { @@ -637,6 +651,11 @@ "$ref": "#/definitions/map[string]string" } }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, "500": { "description": "Bummer, something went wrong" } @@ -668,7 +687,106 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list cloud clusters", + "operationId": "listCloudClusters", + "parameters": [ + { + "type": "int", + "default": 0, + "description": "Page for paging", + "name": "page", + "in": "query" + }, + { + "type": "int", + "default": 20, + "description": "PageSize for paging", + "name": "pageSize", + "in": "query" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/*v1.AccessKeyRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/connect": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create cluster from cloud cluster", + "operationId": "connectCloudCluster", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/*v1.ConnectCloudClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } @@ -687,7 +805,7 @@ "cluster" ], "summary": "detail cluster info", - "operationId": "noop", + "operationId": "getKubeCluster", "parameters": [ { "type": "string", @@ -699,7 +817,94 @@ ], "responses": { "200": { - "description": "OK" + "schema": { + "$ref": "#/definitions/v1.DetailClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "modify cluster", + "operationId": "modifyKubeCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/*v1.CreateClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "delete cluster", + "operationId": "deleteKubeCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } } } } @@ -810,6 +1015,90 @@ } } }, + "/api/v1/workflows": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list application workflow", + "operationId": "listApplicationWorkflows", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "appName", + "in": "query" + }, + { + "type": "boolean", + "description": "query based on enable status", + "name": "enable", + "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 application workflow", + "operationId": "createApplicationWorkflow", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateWorkflowRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "400": { + "description": "create failure", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, "/api/v1/workflows/{name}": { "get": { "consumes": [ @@ -824,11 +1113,11 @@ "cluster" ], "summary": "detail application workflow", - "operationId": "noop", + "operationId": "detailWorkflow", "parameters": [ { "type": "string", - "description": "identifier of the workflow, Currently, the application name is used.", + "description": "identifier of the workflow.", "name": "name", "in": "path", "required": true @@ -858,8 +1147,8 @@ "tags": [ "cluster" ], - "summary": "create or update application workflow config", - "operationId": "noop", + "summary": "update application workflow config", + "operationId": "updateWorkflow", "parameters": [ { "type": "string", @@ -888,6 +1177,41 @@ "description": "Bummer, something went wrong" } } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "deletet workflow", + "operationId": "deleteWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } } }, "/api/v1/workflows/{name}/records": { @@ -1293,13 +1617,54 @@ } } }, + "cloudprovider.CloudCluster": { + "required": [ + "id", + "name", + "type", + "zone", + "labels", + "status", + "apiServerURL", + "dashboardURL" + ], + "properties": { + "apiServerURL": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "zone": { + "type": "string" + } + } + }, "common.AppRolloutStatus": { "required": [ - "rollingState", "batchRollingState", - "upgradedReplicas", "currentBatch", + "upgradedReplicas", "upgradedReadyReplicas", + "rollingState", "lastTargetAppRevision" ], "properties": { @@ -1876,6 +2241,31 @@ } } }, + "model.ProviderInfo": { + "required": [ + "name", + "id", + "zone", + "labels" + ], + "properties": { + "id": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "zone": { + "type": "string" + } + } + }, "types.Parameter": { "required": [ "name" @@ -1912,6 +2302,20 @@ } }, "types.Parameter.default": {}, + "v1.AccessKeyRequest": { + "required": [ + "accessKeyID", + "accessKeySecret" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + } + } + }, "v1.AddonMeta": { "required": [ "name", @@ -2012,6 +2416,7 @@ }, "v1.ApplicationDeployRequest": { "required": [ + "workflowName", "commit", "sourceType", "force" @@ -2025,6 +2430,9 @@ }, "sourceType": { "type": "string" + }, + "workflowName": { + "type": "string" } } }, @@ -2173,10 +2581,19 @@ "description", "icon", "labels", + "providerInfo", + "apiServerURL", + "dashboardURL", "status", "reason" ], "properties": { + "apiServerURL": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, "description": { "type": "string" }, @@ -2192,6 +2609,9 @@ "name": { "type": "string" }, + "providerInfo": { + "$ref": "#/definitions/model.ProviderInfo" + }, "reason": { "type": "string" }, @@ -2205,17 +2625,29 @@ "workerNumber", "masterNumber", "memoryCapacity", - "cpuCapacity" + "cpuCapacity", + "podCapacity", + "memoryUsed", + "cpuUsed", + "podUsed" ], "properties": { "cpuCapacity": { "type": "integer", "format": "int64" }, + "cpuUsed": { + "type": "integer", + "format": "int64" + }, "gpuCapacity": { "type": "integer", "format": "int64" }, + "gpuUsed": { + "type": "integer", + "format": "int64" + }, "masterNumber": { "type": "integer", "format": "int32" @@ -2224,6 +2656,18 @@ "type": "integer", "format": "int64" }, + "memoryUsed": { + "type": "integer", + "format": "int64" + }, + "podCapacity": { + "type": "integer", + "format": "int64" + }, + "podUsed": { + "type": "integer", + "format": "int64" + }, "storageClassList": { "type": "array", "items": { @@ -2338,6 +2782,41 @@ } } }, + "v1.ConnectCloudClusterRequest": { + "required": [ + "accessKeyID", + "accessKeySecret", + "clusterID", + "name", + "icon" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + }, + "clusterID": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, "v1.CreateAddonRequest": { "required": [ "name", @@ -2436,10 +2915,12 @@ "v1.CreateClusterRequest": { "required": [ "name", - "icon", - "kubeConfig" + "icon" ], "properties": { + "dashboardURL": { + "type": "string" + }, "description": { "type": "string" }, @@ -2543,14 +3024,46 @@ } } }, + "v1.CreateWorkflowRequest": { + "required": [ + "appName", + "name", + "description", + "enable", + "default" + ], + "properties": { + "appName": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + } + } + }, "v1.DetailAddonResponse": { "required": [ - "version", "description", "icon", "tags", "phase", - "name" + "name", + "version" ], "properties": { "deploy_data": { @@ -2587,14 +3100,14 @@ }, "v1.DetailApplicationResponse": { "required": [ + "description", + "createTime", + "status", "gatewayRule", "name", "namespace", "updateTime", "icon", - "status", - "description", - "createTime", "policies", "status", "resourceInfo", @@ -2661,15 +3174,21 @@ }, "v1.DetailClusterResponse": { "required": [ + "status", + "icon", + "description", + "labels", + "providerInfo", + "apiServerURL", + "dashboardURL", "reason", "name", - "description", - "icon", - "labels", - "status", "resourceInfo" ], "properties": { + "apiServerURL": { + "type": "string" + }, "dashboardURL": { "type": "string" }, @@ -2688,6 +3207,9 @@ "name": { "type": "string" }, + "providerInfo": { + "$ref": "#/definitions/model.ProviderInfo" + }, "reason": { "type": "string" }, @@ -2704,12 +3226,12 @@ }, "v1.DetailComponentResponse": { "required": [ - "updateTime", - "type", - "createTime", - "creator", "appPrimaryKey", - "name" + "type", + "updateTime", + "creator", + "name", + "createTime" ], "properties": { "appPrimaryKey": { @@ -2784,13 +3306,13 @@ }, "v1.DetailPolicyResponse": { "required": [ - "name", - "type", - "description", "creator", "properties", "createTime", - "updateTime" + "updateTime", + "name", + "type", + "description" ], "properties": { "createTime": { @@ -2820,19 +3342,41 @@ }, "v1.DetailWorkflowResponse": { "required": [ + "default", + "createTime", + "updateTime", + "name", + "description", "enable", "workflowRecord" ], "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, "enable": { "type": "boolean" }, + "name": { + "type": "string" + }, "steps": { "type": "array", "items": { "$ref": "#/definitions/v1.WorkflowStep" } }, + "updateTime": { + "type": "string", + "format": "date-time" + }, "workflowRecord": { "$ref": "#/definitions/v1.WorkflowRecord" } @@ -2922,6 +3466,24 @@ } } }, + "v1.ListCloudClusterResponse": { + "required": [ + "clusters", + "total" + ], + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/cloudprovider.CloudCluster" + } + }, + "total": { + "type": "integer", + "format": "int32" + } + } + }, "v1.ListClusterResponse": { "required": [ "clusters" @@ -3018,10 +3580,10 @@ }, "v1.NamespaceDetailResponse": { "required": [ + "name", "description", "createTime", - "updateTime", - "name" + "updateTime" ], "properties": { "createTime": { @@ -3150,19 +3712,19 @@ }, "v1.UpdateWorkflowRequest": { "required": [ - "name", - "namespace", - "enable" + "description", + "enable", + "default" ], "properties": { - "enable": { + "default": { "type": "boolean" }, - "name": { + "description": { "type": "string" }, - "namespace": { - "type": "string" + "enable": { + "type": "boolean" }, "steps": { "type": "array", @@ -3172,6 +3734,38 @@ } } }, + "v1.WorkflowBase": { + "required": [ + "name", + "description", + "enable", + "default", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, "v1.WorkflowRecord": {}, "v1.WorkflowStep": { "required": [ diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi.go b/pkg/apiserver/datastore/kubeapi/kubeapi.go index 94a35cfdb..d720b3a21 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi.go @@ -239,6 +239,7 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto } options := &client.ListOptions{ LabelSelector: selector, + Namespace: m.namespace, } var skip, limit int64 if op != nil && op.PageSize > 0 && op.Page > 0 { @@ -268,6 +269,7 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto } } var list []datastore.Entity + log.Logger.Debugf("query %s result count %d", selector, len(items)) for _, item := range items { ent, err := datastore.NewEntity(entity) if err != nil { diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index fc1718662..7ff36c523 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -22,6 +22,10 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/common" ) +func init() { + RegistModel(&ApplicationComponent{}, &ApplicationPolicy{}, &Application{}, &DeployEvent{}) +} + // Application database model type Application struct { Model @@ -191,6 +195,9 @@ type DeployEvent struct { Commit string `json:"commit"` // SourceType the event trigger source, Web or API SourceType string `json:"sourceType"` + + // WorkflowName deploy controller by workflow + WorkflowName string `json:"workflowName"` } // TableName return custom table name diff --git a/pkg/apiserver/model/cluster.go b/pkg/apiserver/model/cluster.go index de043148b..1df2b2b96 100644 --- a/pkg/apiserver/model/cluster.go +++ b/pkg/apiserver/model/cluster.go @@ -16,6 +16,10 @@ limitations under the License. package model +func init() { + RegistModel(&Cluster{}) +} + // ProviderInfo describes the information from provider API type ProviderInfo struct { Name string `json:"name"` diff --git a/pkg/apiserver/model/model.go b/pkg/apiserver/model/model.go index bc8f3aacb..dd6bf5702 100644 --- a/pkg/apiserver/model/model.go +++ b/pkg/apiserver/model/model.go @@ -30,6 +30,23 @@ import ( var tableNamePrefix = "vela_" +var registedModels = map[string]Interface{} + +// Interface model interface +type Interface interface { + TableName() string +} + +// RegistModel regist model +func RegistModel(models ...Interface) { + for _, model := range models { + if _, exist := registedModels[model.TableName()]; exist { + panic(fmt.Errorf("model table name %s conflict", model.TableName())) + } + registedModels[model.TableName()] = model + } +} + // JSONStruct json struct, same with runtime.RawExtension type JSONStruct map[string]interface{} diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index 7d6fe4020..356dfb308 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -17,32 +17,43 @@ limitations under the License. package model import ( + "strconv" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" ) -// Workflow workflow database model +func init() { + RegistModel(&Workflow{}) +} + +// Workflow application delivery plan database model type Workflow struct { Model - Name string `json:"name"` - Namespace string `json:"namespace"` - Enable bool `json:"enable"` - Steps []WorkflowStep `json:"steps,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Enable bool `json:"enable"` + // Workflow used by the default + Default bool `json:"default"` + AppPrimaryKey string `json:"appPrimaryKey"` + Steps []WorkflowStep `json:"steps,omitempty"` } // WorkflowStep defines how to execute a workflow step. type WorkflowStep struct { // Name is the unique name of the workflow step. - Name string `json:"name"` - Type string `json:"type"` - Properties *JSONStruct `json:"properties,omitempty"` - DependsOn []string `json:"dependsOn,omitempty"` - Inputs common.StepInputs `json:"inputs,omitempty"` - Outputs common.StepOutputs `json:"outputs,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Group string `json:"group"` + Description string `json:"description"` + OrderIndex int `json:"orderIndex"` + Inputs common.StepInputs `json:"inputs,omitempty"` + Outputs common.StepOutputs `json:"outputs,omitempty"` + Properties *JSONStruct `json:"properties,omitempty"` } // TableName return custom table name func (w *Workflow) TableName() string { - return tableNamePrefix + "application_component" + return tableNamePrefix + "workflow" } // PrimaryKey return custom primary key @@ -56,8 +67,10 @@ func (w *Workflow) Index() map[string]string { if w.Name != "" { index["name"] = w.Name } - if w.Namespace != "" { - index["namespace"] = w.Namespace + if w.AppPrimaryKey != "" { + index["appPrimaryKey"] = w.AppPrimaryKey } + index["default"] = strconv.FormatBool(w.Default) + index["enable"] = strconv.FormatBool(w.Enable) return index } diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 80dabc7be..4457f50a2 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -28,6 +28,9 @@ import ( // CtxKeyApplication request context key of application var CtxKeyApplication = "application" +// CtxKeyWorkflow request context key of workflow +var CtxKeyWorkflow = "workflow" + // AddonPhase defines the phase of an addon type AddonPhase string @@ -426,12 +429,22 @@ type PolicyDefinition struct { Parameters []types.Parameter `json:"parameters"` } +// CreateWorkflowRequest create workflow request +type CreateWorkflowRequest struct { + AppName string `json:"appName" validate:"checkname"` + Name string `json:"name" validate:"checkname"` + Description string `json:"description"` + Steps []WorkflowStep `json:"steps,omitempty"` + Enable bool `json:"enable"` + Default bool `json:"default"` +} + // UpdateWorkflowRequest update or create application workflow type UpdateWorkflowRequest struct { - Name string `json:"name" validate:"checkname"` - Namespace string `json:"namespace" validate:"checkname"` - Steps []WorkflowStep `json:"steps,omitempty"` - Enable bool `json:"enable"` + Description string `json:"description"` + Steps []WorkflowStep `json:"steps,omitempty"` + Enable bool `json:"enable"` + Default bool `json:"default"` } // WorkflowStep workflow step config @@ -447,11 +460,26 @@ type WorkflowStep struct { // DetailWorkflowResponse detail workflow response type DetailWorkflowResponse struct { + WorkflowBase Steps []WorkflowStep `json:"steps,omitempty"` - Enable bool `json:"enable"` LastRecord *WorkflowRecord `json:"workflowRecord"` } +// ListWorkflowResponse list application workflows +type ListWorkflowResponse struct { + Workflows []*WorkflowBase `json:"workflows"` +} + +// WorkflowBase workflow base model +type WorkflowBase struct { + Name string `json:"name"` + Description string `json:"description"` + Enable bool `json:"enable"` + Default bool `json:"default"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + // ListWorkflowRecordsResponse list workflow execution record type ListWorkflowRecordsResponse struct { Records []WorkflowRecord `json:"records"` @@ -464,6 +492,7 @@ type WorkflowRecord struct { // ApplicationDeployRequest the application deploy or update event request type ApplicationDeployRequest struct { + WorkflowName string `json:"workflowName"` // User note message, optional Commit string `json:"commit"` // SourceType the event trigger source, Web or API diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index fd27978df..f08e00b16 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -38,6 +38,7 @@ import ( apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/utils/apply" ) @@ -206,11 +207,13 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis Outputs: step.Outputs, }) } - _, err := c.workflowUsecase.CreateOrUpdateWorkflow(ctx, apisv1.UpdateWorkflowRequest{ - Name: application.Name, - Namespace: application.Namespace, - Steps: steps, - Enable: true, + _, err := c.workflowUsecase.CreateWorkflow(ctx, &application, apisv1.CreateWorkflowRequest{ + AppName: application.PrimaryKey(), + Name: application.Name, + Description: "Created automatically.", + Steps: steps, + Enable: true, + Default: true, }) if err != nil { return nil, err @@ -314,6 +317,7 @@ func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, a } componentModels = append(componentModels, &componentModel) } + log.Logger.Infof("batch add %d components for app %s", len(componentModels), app.PrimaryKey()) return c.ds.BatchAdd(ctx, componentModels) } @@ -327,6 +331,7 @@ func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model. } var list []*apisv1.ComponentBase for _, component := range components { + log.Logger.Infof("component name %s", component.PrimaryKey()) pm := component.(*model.ApplicationComponent) list = append(list, c.converComponentModelToBase(pm)) } @@ -444,7 +449,7 @@ func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Ap func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) { // step1: Render oam application version := utils.GenerateVersion("") - oamApp, err := c.renderOAMApplication(ctx, app, version) + oamApp, err := c.renderOAMApplication(ctx, app, req.WorkflowName, version) if err != nil { return nil, err } @@ -470,9 +475,10 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat ApplyAppConfig: string(configByte), Status: model.DeployEventInit, // TODO: Get user information from ctx and assign a value. - DeployUser: "", - Commit: req.Commit, - SourceType: req.SourceType, + DeployUser: "", + Commit: req.Commit, + SourceType: req.SourceType, + WorkflowName: oamApp.Annotations[oam.AnnotationWorkflowName], } if err := c.ds.Add(ctx, deployEvent); err != nil { @@ -514,7 +520,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat }, nil } -func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMoel *model.Application, version string) (*v1beta1.Application, error) { +func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMoel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) { var app = &v1beta1.Application{ TypeMeta: metav1.TypeMeta{ Kind: "Application", @@ -525,7 +531,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo Namespace: appMoel.Namespace, Labels: appMoel.Labels, Annotations: map[string]string{ - "deploy_version": version, + oam.AnnotationDeployVersion: version, }, }, } @@ -583,19 +589,31 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } app.Spec.Policies = append(app.Spec.Policies, apolicy) } - workflow, err := c.workflowUsecase.GetWorkflow(ctx, appMoel.Name) - if err != nil { - return nil, err + + // Priority 1 uses the requested workflow as release plan. + // Priority 2 uses the default workflow as release plan. + var workflow *model.Workflow + if reqWorkflowName != "" { + workflow, err = c.workflowUsecase.GetWorkflow(ctx, reqWorkflowName) + if err != nil { + return nil, err + } + } else { + workflow, err = c.workflowUsecase.GetApplicationDefaultWorkflow(ctx, appMoel) + if err != nil && !errors.Is(err, bcode.ErrWorkflowNoDefault) { + return nil, err + } } + if workflow != nil { + app.Annotations[oam.AnnotationWorkflowName] = workflow.AppPrimaryKey var steps []v1beta1.WorkflowStep for _, step := range workflow.Steps { var wstep = v1beta1.WorkflowStep{ - Name: step.Name, - Type: step.Type, - DependsOn: step.DependsOn, - Inputs: step.Inputs, - Outputs: step.Outputs, + Name: step.Name, + Type: step.Type, + Inputs: step.Inputs, + Outputs: step.Outputs, } if step.Properties != nil { wstep.Properties = step.Properties.RawExtension() @@ -676,7 +694,7 @@ func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *mod for _, component := range components { err := c.ds.Delete(ctx, &model.ApplicationComponent{AppPrimaryKey: app.PrimaryKey(), Name: component.Name}) - if err != nil && errors.Is(err, datastore.ErrRecordNotExist) { + if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { log.Logger.Errorf("delete component %s in app %s failure %s", component.Name, app.Name, err.Error()) } } diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 1cddd199e..bd5c81aa3 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -163,7 +163,6 @@ var _ = Describe("Test application usecase function", func() { detail, err := workflowUsecase.GetWorkflow(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) - Expect(cmp.Diff(detail.Namespace, "test-app-namespace")).Should(BeEmpty()) Expect(cmp.Diff(detail.Enable, true)).Should(BeEmpty()) }) @@ -213,6 +212,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(detail.Type, "env-binding")).Should(BeEmpty()) Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) }) + It("Test AddComponent function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) @@ -228,6 +228,7 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(cmp.Diff(base.ComponentType, "worker")).Should(BeEmpty()) }) + It("Test DetailComponent function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 3b4a85c21..25960fcef 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -29,9 +29,13 @@ import ( // WorkflowUsecase workflow manage api type WorkflowUsecase interface { + ListApplicationWorkflow(ctx context.Context, app *model.Application, enable *bool) ([]*apisv1.WorkflowBase, error) GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) + DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) + GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) DeleteWorkflow(ctx context.Context, workflowName string) error - CreateOrUpdateWorkflow(ctx context.Context, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) } // NewWorkflowUsecase new workflow usecase @@ -57,7 +61,7 @@ func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, workflowName s return nil } -func (w *workflowUsecaseImpl) CreateOrUpdateWorkflow(ctx context.Context, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { +func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { var steps []model.WorkflowStep for _, step := range req.Steps { properties, err := model.NewJSONStructByString(step.Properties) @@ -68,17 +72,19 @@ func (w *workflowUsecaseImpl) CreateOrUpdateWorkflow(ctx context.Context, req ap steps = append(steps, model.WorkflowStep{ Name: step.Name, Type: step.Type, - DependsOn: step.DependsOn, Inputs: step.Inputs, Outputs: step.Outputs, Properties: properties, }) } + // It is allowed to set multiple workflows as default, and only one takes effect. var workflow = model.Workflow{ - Steps: steps, - Name: req.Name, - Namespace: req.Namespace, - Enable: req.Enable, + Steps: steps, + Name: req.Name, + Enable: req.Enable, + Description: req.Description, + Default: req.Default, + AppPrimaryKey: app.PrimaryKey(), } if err := w.ds.Add(ctx, &workflow); err != nil { return nil, err @@ -86,6 +92,33 @@ func (w *workflowUsecaseImpl) CreateOrUpdateWorkflow(ctx context.Context, req ap return w.DetailWorkflow(ctx, &workflow) } +func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { + var steps []model.WorkflowStep + for _, step := range req.Steps { + properties, err := model.NewJSONStructByString(step.Properties) + if err != nil { + log.Logger.Errorf("parse trait properties failire %w", err) + return nil, bcode.ErrInvalidProperties + } + steps = append(steps, model.WorkflowStep{ + Name: step.Name, + Type: step.Type, + Inputs: step.Inputs, + Outputs: step.Outputs, + Properties: properties, + }) + } + workflow.Steps = steps + workflow.Description = req.Description + // It is allowed to set multiple workflows as default, and only one takes effect. + workflow.Default = req.Default + workflow.Enable = req.Enable + if err := w.ds.Put(ctx, workflow); err != nil { + return nil, err + } + return w.DetailWorkflow(ctx, workflow) +} + // DetailWorkflow detail workflow func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) { var steps []apisv1.WorkflowStep @@ -93,7 +126,6 @@ func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *mode apiStep := apisv1.WorkflowStep{ Name: step.Name, Type: step.Type, - DependsOn: step.DependsOn, Inputs: step.Inputs, Outputs: step.Outputs, Properties: step.Properties.JSON(), @@ -103,7 +135,17 @@ func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *mode } steps = append(steps, apiStep) } - return &apisv1.DetailWorkflowResponse{Steps: steps, Enable: workflow.Enable}, nil + return &apisv1.DetailWorkflowResponse{ + WorkflowBase: apisv1.WorkflowBase{ + Name: workflow.Name, + Description: workflow.Description, + Enable: workflow.Enable, + Default: workflow.Default, + CreateTime: workflow.CreateTime, + UpdateTime: workflow.UpdateTime, + }, + Steps: steps, + }, nil } // GetWorkflow get workflow model @@ -116,3 +158,46 @@ func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName stri } return &workflow, nil } + +// ListApplicationWorkflow list application workflows +func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app *model.Application, enable *bool) ([]*apisv1.WorkflowBase, error) { + var workflow = model.Workflow{ + AppPrimaryKey: app.PrimaryKey(), + } + if enable != nil { + workflow.Enable = *enable + } + workflows, err := w.ds.List(ctx, &workflow, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + var list []*apisv1.WorkflowBase + for _, workflow := range workflows { + wm := workflow.(*model.Workflow) + list = append(list, &apisv1.WorkflowBase{ + Name: wm.Name, + Description: wm.Description, + Enable: wm.Enable, + Default: wm.Default, + CreateTime: wm.CreateTime, + UpdateTime: wm.UpdateTime, + }) + } + return list, nil +} + +// GetApplicationDefaultWorkflow get application default workflow +func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) { + var workflow = model.Workflow{ + AppPrimaryKey: app.PrimaryKey(), + Default: true, + } + workflows, err := w.ds.List(ctx, &workflow, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + if len(workflows) > 0 { + return workflows[0].(*model.Workflow), nil + } + return nil, bcode.ErrWorkflowNoDefault +} diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go new file mode 100644 index 000000000..1f05e36a4 --- /dev/null +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -0,0 +1,68 @@ +/* +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" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +var _ = Describe("Test workflow usecase functions", func() { + var ( + workflowUsecase *workflowUsecaseImpl + ) + BeforeEach(func() { + workflowUsecase = &workflowUsecaseImpl{ds: ds} + }) + It("Test CreateNamespace function", func() { + req := apisv1.CreateWorkflowRequest{ + Name: "test-workflow-1", + Description: "this is a workflow", + } + base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + Name: "test-app", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + req = apisv1.CreateWorkflowRequest{ + Name: "test-workflow-2", + Description: "this is test workflow", + Default: true, + } + base, err = workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + Name: "test-app", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + }) + + It("Test GetApplicationDefaultWorkflow function", func() { + workflow, err := workflowUsecase.GetApplicationDefaultWorkflow(context.TODO(), &model.Application{ + Name: "test-app", + }) + Expect(err).Should(BeNil()) + Expect(workflow).ShouldNot(BeNil()) + Expect(cmp.Diff(workflow.Name, "test-workflow-2")).Should(BeEmpty()) + }) +}) diff --git a/pkg/apiserver/rest/utils/bcode/workflow.go b/pkg/apiserver/rest/utils/bcode/workflow.go index 07b30dfb5..9b4059cf3 100644 --- a/pkg/apiserver/rest/utils/bcode/workflow.go +++ b/pkg/apiserver/rest/utils/bcode/workflow.go @@ -21,3 +21,9 @@ var ErrWorkflowNotExist = NewBcode(404, 20002, "application workflow is not exis // ErrWorkflowExist application workflow is exist var ErrWorkflowExist = NewBcode(404, 20003, "application workflow is exist") + +// ErrWorkflowNoDefault application default workflow is not exist +var ErrWorkflowNoDefault = NewBcode(404, 20004, "application default workflow is not exist") + +// ErrMustQueryByApp you can only query the Workflow list based on applications. +var ErrMustQueryByApp = NewBcode(404, 20005, "you can only query the Workflow list based on applications.") diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 05730ccaf..a1d08c1d4 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -17,11 +17,17 @@ limitations under the License. package webservice import ( + "context" + "strconv" + restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" 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" ) // NewWorkflowWebService new workflow webservice @@ -37,7 +43,7 @@ type workflowWebService struct { applicationUsecase usecase.ApplicationUsecase } -func (c *workflowWebService) GetWebService() *restful.WebService { +func (w *workflowWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) ws.Path(versionPrefix+"/workflows"). Consumes(restful.MIME_XML, restful.MIME_JSON). @@ -46,26 +52,166 @@ func (c *workflowWebService) GetWebService() *restful.WebService { 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")). + ws.Route(ws.GET("/").To(w.listApplicationWorkflows). + Doc("list application workflow"). + Param(ws.QueryParameter("appName", "identifier of the application.").DataType("string")). + Param(ws.QueryParameter("enable", "query based on enable status").DataType("boolean")). Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(apis.ListWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.POST("/").To(w.createApplicationWorkflow). + Doc("create application workflow"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateWorkflowRequest{}). + Returns(200, "create success", apis.DetailWorkflowResponse{}). + Returns(400, "create failure", bcode.Bcode{}). Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - ws.Route(ws.PUT("/{name}").To(noop). - Doc("create or update application workflow config"). + ws.Route(ws.GET("/{name}").To(w.detailWorkflow). + Doc("detail application workflow"). + Param(ws.PathParameter("name", "identifier of the workflow.").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(w.workflowCheckFilter). + Returns(200, "create success", apis.DetailWorkflowResponse{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.PUT("/{name}").To(w.updateWorkflow). + Doc("update application workflow config"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(w.workflowCheckFilter). Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). Reads(apis.UpdateWorkflowRequest{}). Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + ws.Route(ws.DELETE("/{name}").To(w.deleteWorkflow). + Doc("deletet workflow"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(w.workflowCheckFilter). + Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). + Writes(apis.EmptyResponse{}).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). + Filter(w.workflowCheckFilter). 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 } + +func (w *workflowWebService) workflowCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + workflow, err := w.workflowUsecase.GetWorkflow(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyWorkflow, workflow)) + chain.ProcessFilter(req, res) +} + +func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res *restful.Response) { + if req.QueryParameter("appName") == "" { + bcode.ReturnError(req, res, bcode.ErrMustQueryByApp) + return + } + app, err := w.applicationUsecase.GetApplication(req.Request.Context(), req.QueryParameter("appName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + var enableQuery *bool + enable, err := strconv.ParseBool(req.QueryParameter("enable")) + if err == nil { + enableQuery = &enable + } + workflows, err := w.workflowUsecase.ListApplicationWorkflow(req.Request.Context(), app, enableQuery) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListWorkflowResponse{Workflows: workflows}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateWorkflowRequest + 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 + } + app, err := w.applicationUsecase.GetApplication(req.Request.Context(), createReq.AppName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + workflowDetail, err := w.workflowUsecase.CreateWorkflow(req.Request.Context(), app, createReq) + if err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(workflowDetail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) detailWorkflow(req *restful.Request, res *restful.Response) { + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + detail, err := w.workflowUsecase.DetailWorkflow(req.Request.Context(), workflow) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) updateWorkflow(req *restful.Request, res *restful.Response) { + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + // Verify the validity of parameters + var updateReq apis.UpdateWorkflowRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + detail, err := w.workflowUsecase.UpdateWorkflow(req.Request.Context(), workflow, updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) deleteWorkflow(req *restful.Request, res *restful.Response) { + if err := w.workflowUsecase.DeleteWorkflow(req.Request.Context(), req.PathParameter("name")); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index f879b5e2b..86bf0181f 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -125,4 +125,10 @@ const ( // AnnotationLastAppliedConfiguration is kubectl annotations for 3-way merge AnnotationLastAppliedConfiguration = "kubectl.kubernetes.io/last-applied-configuration" + + // AnnotationDeployVersion know the version number of the deployment. + AnnotationDeployVersion = "app.oam.dev/deployVersion" + + // AnnotationWorkflowName specifies the workflow name for execution. + AnnotationWorkflowName = "app.oam.dev/workflowName" ) From 3ebc94394c81a6736062eb39c093e46ac33d0a30 Mon Sep 17 00:00:00 2001 From: Hongchao Deng Date: Tue, 26 Oct 2021 05:52:34 -0400 Subject: [PATCH 07/59] Feat: addon service impl (#2515) * Feat: addon service impl * get addon from git/configmap * add ListAddonRegistries * add GetAddonModel * add CreateAddonRegistry and bcode/addon.go * add applyAddonData * update * Fix: getAddonFromGit * Fix: getAddonFromGit, remove trailing .git * add comment * add enable/disable/status impl * add deleteAddonRegistry and check dup addon * read addon without accessing database * change to query parameter, add addon detail * Feat: add addon readme for apiserver * Make enable/disable/status runnable * chore: fix bcode * Fix: refactor parse to util * Fix: refactor addonutil to pkg * add addon test for create and delete addon registry * fix version prefix * add post func * add enable/disable test * add provider aws readme * done testing * fix comment and refactor statusAddon * move enable/disable logic to usecase * add GITHUB_TOKEN env * Fix: Add github token support and use it in test * add license Co-authored-by: qiaozp --- .github/workflows/apiserver-test.yaml | 1 + charts/vela-core/templates/addons/fluxcd.yaml | 6 + charts/vela-core/templates/addons/istio.yaml | 4 + charts/vela-core/templates/addons/keda.yaml | 4 + charts/vela-core/templates/addons/kruise.yaml | 4 + .../templates/addons/observability.yaml | 4 + .../templates/addons/ocm-cluster-manager.yaml | 4 + .../templates/addons/prometheus.yaml | 4 + .../addons/terraform-provider-alibaba.yaml | 4 + .../addons/terraform-provider-aws.yaml | 4 + .../addons/terraform-provider-azure.yaml | 4 + .../vela-core/templates/addons/terraform.yaml | 2 + pkg/apiserver/model/addon.go | 51 +++ pkg/apiserver/model/catalog.go | 31 -- pkg/apiserver/rest/apis/v1/types.go | 56 +-- pkg/apiserver/rest/rest_server.go | 5 +- pkg/apiserver/rest/usecase/addon.go | 403 ++++++++++++++++++ pkg/apiserver/rest/utils/bcode/addon.go | 47 ++ pkg/apiserver/rest/utils/convert.go | 14 + pkg/apiserver/rest/webservice/addon.go | 158 +++++-- .../rest/webservice/addon_registry.go | 109 +++++ pkg/apiserver/rest/webservice/webservice.go | 4 +- pkg/utils/addon/addon.go | 281 ++++++++++++ pkg/utils/parse.go | 120 ++++++ references/cli/addon.go | 269 +----------- references/plugins/capcenter.go | 125 +----- references/plugins/capcenter_test.go | 18 +- references/plugins/registry.go | 14 +- test/e2e-apiserver-test/addon_test.go | 125 ++++++ test/e2e-apiserver-test/suite_test.go | 15 +- vela-templates/addons/fluxcd/readme.md | 18 + vela-templates/addons/istio/readme.md | 3 + vela-templates/addons/keda/readme.md | 3 + vela-templates/addons/kruise/readme.md | 3 + vela-templates/addons/observability/readme.md | 3 + .../addons/ocm-cluster-manager/readme.md | 3 + vela-templates/addons/prometheus/readme.md | 3 + .../terraform-provider-alibaba/readme.md | 3 + .../addons/terraform-provider-aws/readme.md | 3 + .../addons/terraform-provider-azure/readme.md | 3 + vela-templates/addons/terraform/readme.md | 4 + vela-templates/gen_addons.go | 59 ++- 42 files changed, 1499 insertions(+), 499 deletions(-) create mode 100644 pkg/apiserver/model/addon.go delete mode 100644 pkg/apiserver/model/catalog.go create mode 100644 pkg/apiserver/rest/usecase/addon.go create mode 100644 pkg/apiserver/rest/utils/bcode/addon.go create mode 100644 pkg/apiserver/rest/utils/convert.go create mode 100644 pkg/apiserver/rest/webservice/addon_registry.go create mode 100644 pkg/utils/addon/addon.go create mode 100644 pkg/utils/parse.go create mode 100644 test/e2e-apiserver-test/addon_test.go create mode 100644 vela-templates/addons/fluxcd/readme.md create mode 100644 vela-templates/addons/istio/readme.md create mode 100644 vela-templates/addons/keda/readme.md create mode 100644 vela-templates/addons/kruise/readme.md create mode 100644 vela-templates/addons/observability/readme.md create mode 100644 vela-templates/addons/ocm-cluster-manager/readme.md create mode 100644 vela-templates/addons/prometheus/readme.md create mode 100644 vela-templates/addons/terraform-provider-alibaba/readme.md create mode 100644 vela-templates/addons/terraform-provider-aws/readme.md create mode 100644 vela-templates/addons/terraform-provider-azure/readme.md create mode 100644 vela-templates/addons/terraform/readme.md diff --git a/.github/workflows/apiserver-test.yaml b/.github/workflows/apiserver-test.yaml index 25483be82..66e8e0275 100644 --- a/.github/workflows/apiserver-test.yaml +++ b/.github/workflows/apiserver-test.yaml @@ -93,6 +93,7 @@ jobs: run: | export ALIYUN_ACCESS_KEY_ID=${{ secrets.ALIYUN_ACCESS_KEY_ID }} export ALIYUN_ACCESS_KEY_SECRET=${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} + export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} make e2e-apiserver-test - name: Stop kubevela, get profile diff --git a/charts/vela-core/templates/addons/fluxcd.yaml b/charts/vela-core/templates/addons/fluxcd.yaml index ed1737aa8..35021e140 100644 --- a/charts/vela-core/templates/addons/fluxcd.yaml +++ b/charts/vela-core/templates/addons/fluxcd.yaml @@ -6020,6 +6020,12 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: "# fluxcd\n\nThis addon is built based [FluxCD](https://fluxcd.io/) \n\n## + install\n\n```shell\nvela addon enable fluxcd\n```\n\n## X-Definitions\n\nEnable + fluxcd addon to use these X-definitions\n\n- [helm](https://kubevela.io/docs/end-user/components/helm) + helps to deploy a helm chart from everywhere:\ngit repo / helm repo / S3 compatible + bucket.\n\n- [kustomize](https://kubevela.io/docs/end-user/components/kustomize) + helps to deploy a kustomize style artifact.\n" kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/istio.yaml b/charts/vela-core/templates/addons/istio.yaml index f3d96695d..8d809d270 100644 --- a/charts/vela-core/templates/addons/istio.yaml +++ b/charts/vela-core/templates/addons/istio.yaml @@ -245,6 +245,10 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: |- + # istio + + This addon provides istio support for vela rollout. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/keda.yaml b/charts/vela-core/templates/addons/keda.yaml index db035c2fb..6936a196f 100644 --- a/charts/vela-core/templates/addons/keda.yaml +++ b/charts/vela-core/templates/addons/keda.yaml @@ -26,6 +26,10 @@ data: - name: apply-resources type: apply-application status: {} + detail: |- + # keda + + keda kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/kruise.yaml b/charts/vela-core/templates/addons/kruise.yaml index 45fed21c4..2a57c3a77 100644 --- a/charts/vela-core/templates/addons/kruise.yaml +++ b/charts/vela-core/templates/addons/kruise.yaml @@ -170,6 +170,10 @@ data: - name: apply-resources type: apply-application status: {} + detail: |- + # kruise + + This addon provides [open-kruise](https://github.com/openkruise/kruise) workload. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/observability.yaml b/charts/vela-core/templates/addons/observability.yaml index 8bf60330e..faaa1f327 100644 --- a/charts/vela-core/templates/addons/observability.yaml +++ b/charts/vela-core/templates/addons/observability.yaml @@ -231,6 +231,10 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: |- + # observability + + This addon expose system and application level metrics for KubeVela. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/ocm-cluster-manager.yaml b/charts/vela-core/templates/addons/ocm-cluster-manager.yaml index 0c6f4f05f..9b6d35e24 100644 --- a/charts/vela-core/templates/addons/ocm-cluster-manager.yaml +++ b/charts/vela-core/templates/addons/ocm-cluster-manager.yaml @@ -503,6 +503,10 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: |- + # ocm-cluster-manager + + This addon aims to support multi-cluster application deployment. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/prometheus.yaml b/charts/vela-core/templates/addons/prometheus.yaml index 1af8c7791..8c6f9e314 100644 --- a/charts/vela-core/templates/addons/prometheus.yaml +++ b/charts/vela-core/templates/addons/prometheus.yaml @@ -27,6 +27,10 @@ data: - name: apply-resources type: apply-application status: {} + detail: | + # prometheus + + prometheus kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml b/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml index abeeceeed..7bef5dae5 100644 --- a/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml @@ -43,6 +43,10 @@ data: region: '[[ index .Args "ALICLOUD_REGION" ]]' type: raw status: {} + detail: |- + # terraform/provider-alibaba + + This addon contains terraform provider for Alibaba Cloud. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/terraform-provider-aws.yaml b/charts/vela-core/templates/addons/terraform-provider-aws.yaml index 7ecaf8275..d47bc477d 100644 --- a/charts/vela-core/templates/addons/terraform-provider-aws.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-aws.yaml @@ -43,6 +43,10 @@ data: region: '[[ index .Args "AWS_DEFAULT_REGION" ]]' type: raw status: {} + detail: |- + # terraform/provider-aws + + This addon contains terraform provider for AWS kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/terraform-provider-azure.yaml b/charts/vela-core/templates/addons/terraform-provider-azure.yaml index a8c60409b..c74593283 100644 --- a/charts/vela-core/templates/addons/terraform-provider-azure.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-azure.yaml @@ -43,6 +43,10 @@ data: provider: azure type: raw status: {} + detail: |- + # terraform/provider-azure + + This addon contains terraform provider for Azure. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/terraform.yaml b/charts/vela-core/templates/addons/terraform.yaml index 0d6fbf5ce..6c1e84893 100644 --- a/charts/vela-core/templates/addons/terraform.yaml +++ b/charts/vela-core/templates/addons/terraform.yaml @@ -522,6 +522,8 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: "# Terraform\n\nThis addon contains terraform operation kit, which allows + you to arrange, \ngenerate and use cloud service from different cloud vendor." kind: ConfigMap metadata: annotations: diff --git a/pkg/apiserver/model/addon.go b/pkg/apiserver/model/addon.go new file mode 100644 index 000000000..fffd52e38 --- /dev/null +++ b/pkg/apiserver/model/addon.go @@ -0,0 +1,51 @@ +/* +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 + +// AddonRegistry defines the data model of a AddonRegistry +type AddonRegistry struct { + Model + Name string `json:"name"` + + Git *GitAddonSource `json:"git,omitempty"` +} + +// GitAddonSource defines the information about the Git as addon source +type GitAddonSource struct { + URL string `json:"url,omitempty"` + Path string `json:"path,omitempty"` + Token string `json:"token,omitempty"` +} + +// TableName return custom table name +func (a *AddonRegistry) TableName() string { + return tableNamePrefix + "addon_registry" +} + +// PrimaryKey return custom primary key +func (a *AddonRegistry) PrimaryKey() string { + return a.Name +} + +// Index return custom index +func (a *AddonRegistry) Index() map[string]string { + index := make(map[string]string) + if a.Name != "" { + index["name"] = a.Name + } + return index +} diff --git a/pkg/apiserver/model/catalog.go b/pkg/apiserver/model/catalog.go deleted file mode 100644 index 2e334d1ab..000000000 --- a/pkg/apiserver/model/catalog.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -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 - -// Catalog defines the data model of a Catalog -type Catalog struct { - Name string `json:"name,omitempty"` - Desc string `json:"desc,omitempty"` - // UpdatedAt is the unix time of the last time when the catalog is updated. - UpdatedAt int64 `json:"updated_at,omitempty"` - // Type of the Catalog, such as "github" for a github repo. - Type string `json:"type,omitempty"` - // URL of the Catalog. - URL string `json:"url,omitempty"` - // Auth token used to sync Catalog. - Token string `json:"token,omitempty"` -} diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 4457f50a2..047611ae6 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -25,8 +25,10 @@ import ( "github.com/oam-dev/kubevela/pkg/cloudprovider" ) -// CtxKeyApplication request context key of application -var CtxKeyApplication = "application" +var ( + // CtxKeyApplication request context key of application + CtxKeyApplication = "application" +) // CtxKeyWorkflow request context key of workflow var CtxKeyWorkflow = "workflow" @@ -37,8 +39,6 @@ type AddonPhase string const ( // AddonPhaseDisabled indicates the addon is disabled AddonPhaseDisabled AddonPhase = "disabled" - // AddonPhaseDisabling indicates the addon is disabling - AddonPhaseDisabling AddonPhase = "disabling" // AddonPhaseEnabled indicates the addon is enabled AddonPhaseEnabled AddonPhase = "enabled" // AddonPhaseEnabling indicates the addon is enabling @@ -48,32 +48,30 @@ const ( // EmptyResponse empty response, it will used for delete api type EmptyResponse struct{} -// CreateAddonRequest defines the format for addon create request -type CreateAddonRequest struct { - Name string `json:"name" validate:"name"` +// CreateAddonRegistryRequest defines the format for addon registry create request +type CreateAddonRegistryRequest struct { + Name string `json:"name" validate:"required"` - Version string `json:"version" validate:"required"` + Git *model.GitAddonSource `json:"git,omitempty"` +} - // Short description about the addon. - Description string `json:"description,omitempty"` +// AddonRegistryMeta defines the format for a single addon registry +type AddonRegistryMeta struct { + Name string `json:"name" validate:"required"` - Icon string `json:"icon"` + Git *model.GitAddonSource `json:"git,omitempty"` +} - Tags []string `json:"tags"` +// EnableAddonRequest defines the format for enable addon request +type EnableAddonRequest struct { - // The detail of the addon. Could be the entire README data. - Detail string `json:"detail,omitempty"` - - // DeployData is the object to deploy to the cluster to enable addon - DeployData string `json:"deploy_data,omitempty" validate:"required_without=deploy_url"` - - // DeployURL is the URL to the data file location in a Git repository - DeployURL string `json:"deploy_url,omitempty" validate:"required_without=deploy_data"` + // Args is the key-value environment variables, e.g. AK/SK credentials. + Args map[string]string `json:"args,omitempty"` } // ListAddonResponse defines the format for addon list response type ListAddonResponse struct { - Addons []AddonMeta `json:"addons"` + Addons []*AddonMeta `json:"addons"` } // AddonMeta defines the format for a single addon @@ -87,26 +85,30 @@ type AddonMeta struct { Icon string `json:"icon"` Tags []string `json:"tags"` - - Phase AddonPhase `json:"phase"` } // DetailAddonResponse defines the format for showing the addon details type DetailAddonResponse struct { AddonMeta + // More details about the addon, e.g. README Detail string `json:"detail,omitempty"` - // DeployData is the object to deploy to the cluster to enable addon + // DeployData is the object to apply to enable addon, e.g. Application DeployData string `json:"deploy_data,omitempty"` - - // DeployURL is the URL to the data file location in a Git repository - DeployURL string `json:"deploy_url,omitempty"` } // AddonStatusResponse defines the format of addon status response type AddonStatusResponse struct { Phase AddonPhase `json:"phase"` + + EnablingProgress *EnablingProgress `json:"enabling_progress,omitempty"` +} + +// EnablingProgress defines the progress of enabling an addon +type EnablingProgress struct { + EnabledComponents int `json:"enabled_components"` + TotalComponents int `json:"total_components"` } // AccessKeyRequest request parameters to access cloud provider diff --git a/pkg/apiserver/rest/rest_server.go b/pkg/apiserver/rest/rest_server.go index 8685f2166..0203fb7b1 100644 --- a/pkg/apiserver/rest/rest_server.go +++ b/pkg/apiserver/rest/rest_server.go @@ -22,7 +22,7 @@ import ( "net/http" restfulspec "github.com/emicklei/go-restful-openapi/v2" - restful "github.com/emicklei/go-restful/v3" + "github.com/emicklei/go-restful/v3" "github.com/go-openapi/spec" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" @@ -68,11 +68,12 @@ func New(cfg Config) (a APIServer, err error) { case "kubeapi": ds, err = kubeapi.New(context.Background(), cfg.Datastore) if err != nil { - return nil, fmt.Errorf("create mongodb datastore instance failure %w", err) + return nil, fmt.Errorf("create kubeapi datastore instance failure %w", err) } default: return nil, fmt.Errorf("not support datastore type %s", cfg.Datastore.Type) } + s := &restServer{ webContainer: restful.NewContainer(), cfg: cfg, diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go new file mode 100644 index 000000000..0b4e38dfa --- /dev/null +++ b/pkg/apiserver/rest/usecase/addon.go @@ -0,0 +1,403 @@ +package usecase + +import ( + "bytes" + "context" + "errors" + "fmt" + "github.com/Masterminds/sprig" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "golang.org/x/oauth2" + "net/http" + "net/url" + "path" + "sort" + "strings" + "text/template" + + errors2 "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/google/go-github/v32/github" + common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + restutils "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/utils" + addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" + "github.com/oam-dev/kubevela/pkg/utils/apply" +) + +const ( + // AddonFileName is the addon file name + AddonFileName string = "addon.yaml" + // AddonReadmeFileName is the addon readme file name + AddonReadmeFileName string = "readme.md" +) + +// AddonUsecase addon usecase +type AddonUsecase interface { + GetAddonRegistryModel(ctx context.Context, name string) (*model.AddonRegistry, error) + CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) + ListAddons(ctx context.Context, detailed bool) ([]*apis.DetailAddonResponse, error) + StatusAddon(name string) (*apis.AddonStatusResponse, error) + GetAddon(ctx context.Context, name string) (*apis.DetailAddonResponse, error) + EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error + DisableAddon(ctx context.Context, name string) error +} + +// NewAddonUsecase returns a addon usecase +func NewAddonUsecase(ds datastore.DataStore) AddonUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + panic(err) + } + return &addonUsecaseImpl{ + ds: ds, + kubeClient: kubecli, + apply: apply.NewAPIApplicator(kubecli), + } +} + +type addonUsecaseImpl struct { + ds datastore.DataStore + kubeClient client.Client + apply apply.Applicator +} + +func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string) (*apis.DetailAddonResponse, error) { + addons, err := u.ListAddons(ctx, true) + if err != nil { + return nil, err + } + + for _, addon := range addons { + if addon.Name == name { + return addon, nil + } + } + return nil, bcode.ErrAddonNotExist +} + +func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, error) { + _, err := u.GetAddon(context.TODO(), name) + if err != nil { + return nil, err + } + + var app v1beta1.Application + err = u.kubeClient.Get(context.Background(), client.ObjectKey{ + Namespace: types.DefaultKubeVelaNS, + Name: addonutil.TransAddonName(name), + }, &app) + if err != nil { + if errors2.IsNotFound(err) { + return &apis.AddonStatusResponse{ + Phase: apis.AddonPhaseDisabled, + EnablingProgress: nil, + }, nil + } + return nil, bcode.ErrGetApplicationFail + } + + switch app.Status.Phase { + case common2.ApplicationRunning, common2.ApplicationWorkflowFinished: + return &apis.AddonStatusResponse{ + Phase: apis.AddonPhaseEnabled, + EnablingProgress: nil, + }, nil + default: + return &apis.AddonStatusResponse{ + Phase: apis.AddonPhaseEnabling, + EnablingProgress: nil, + }, nil + } +} + +func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool) ([]*apis.DetailAddonResponse, error) { + // Backward compatibility with ConfigMap addons. + // We will deprecate ConfigMap and use Git based registry. + addons, err := getAddonsFromConfigMap(detailed) + if err != nil { + return nil, err + } + + rs, err := u.listAddonRegistries(ctx) + if err != nil { + return nil, err + } + for _, r := range rs { + gitAddons, err := getAddonsFromGit(r.Git.URL, r.Git.Path, r.Git.Token, detailed) + if err != nil { + return nil, err + } + addons = mergeAddons(addons, gitAddons) + } + sort.Slice(addons, func(i, j int) bool { + return addons[i].Name < addons[j].Name + }) + return addons, nil +} + +func (u *addonUsecaseImpl) CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) { + r := addonRegistryModelFromCreateAddonRegistryRequest(req) + + err := u.ds.Add(ctx, r) + if err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrAddonRegistryExist + } + return nil, err + } + + return &apis.AddonRegistryMeta{ + Name: r.Name, + Git: r.Git, + }, nil + +} + +func (u *addonUsecaseImpl) GetAddonRegistryModel(ctx context.Context, name string) (*model.AddonRegistry, error) { + var r = model.AddonRegistry{ + Name: name, + } + err := u.ds.Get(ctx, &r) + if err != nil { + return nil, err + } + return &r, nil +} + +func (u *addonUsecaseImpl) listAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) { + var r = model.AddonRegistry{} + entities, err := u.ds.List(ctx, &r, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + var list []*apis.AddonRegistryMeta + for _, entity := range entities { + list = append(list, restutils.ConvertAddonRegistryModel2AddonRegistryMeta(entity.(*model.AddonRegistry))) + } + return list, nil +} + +func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error { + addon, err := u.GetAddon(ctx, name) + if err != nil { + return err + } + err = u.applyAddonData(addon.DeployData, args) + if err != nil { + return err + } + return nil + +} + +func (u *addonUsecaseImpl) DisableAddon(ctx context.Context, name string) error { + addon, err := u.GetAddon(ctx, name) + if err != nil { + return err + } + err = u.deleteAddonData(addon.DeployData) + if err != nil { + return err + } + return nil +} + +func (u *addonUsecaseImpl) applyAddonData(data string, request apis.EnableAddonRequest) error { + app, err := renderAddonApp(data, &request) + if err != nil { + return err + } + applicator := apply.NewAPIApplicator(u.kubeClient) + err = applicator.Apply(context.TODO(), app) + if err != nil { + log.Logger.Errorf("apply application fail: %s", err.Error()) + return bcode.ErrAddonApplyFail + } + return nil +} + +func (u *addonUsecaseImpl) deleteAddonData(data string) error { + app, err := renderAddonApp(data, nil) + if err != nil { + return err + } + err = u.kubeClient.Get(context.Background(), client.ObjectKey{ + Namespace: app.GetNamespace(), + Name: app.GetName(), + }, app) + if err != nil { + return bcode.ErrAddonNotEnabled + } + err = u.kubeClient.Delete(context.Background(), app) + if err != nil { + return bcode.ErrAddonDisableFail + } + return nil + +} + +// renderAddonApp can render string to unstructured, args can be nil +func renderAddonApp(data string, args *apis.EnableAddonRequest) (*unstructured.Unstructured, error) { + if args == nil { + args = &apis.EnableAddonRequest{Args: map[string]string{}} + } + + t, err := template.New("addon-template").Delims("[[", "]]").Funcs(sprig.TxtFuncMap()).Parse(data) + if err != nil { + return nil, bcode.ErrAddonRenderFail + } + buf := bytes.Buffer{} + err = t.Execute(&buf, args) + if err != nil { + return nil, bcode.ErrAddonRenderFail + } + dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + obj := &unstructured.Unstructured{} + _, _, err = dec.Decode(buf.Bytes(), nil, obj) + if err != nil { + return nil, bcode.ErrAddonRenderFail + } + return obj, nil +} + +func addonRegistryModelFromCreateAddonRegistryRequest(req apis.CreateAddonRegistryRequest) *model.AddonRegistry { + return &model.AddonRegistry{ + Name: req.Name, + Git: req.Git, + } +} + +func mergeAddons(a1, a2 []*apis.DetailAddonResponse) []*apis.DetailAddonResponse { + for _, item := range a2 { + if hasAddon(a1, item.Name) { + continue + } + a1 = append(a1, item) + } + return a1 +} + +func hasAddon(addons []*apis.DetailAddonResponse, name string) bool { + for _, addon := range addons { + if addon.Name == name { + return true + } + } + return false +} + +func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.DetailAddonResponse, error) { + addons := []*apis.DetailAddonResponse{} + dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + var tc *http.Client + if token != "" { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc = oauth2.NewClient(context.Background(), ts) + } + clt := github.NewClient(tc) + // TODO add error handling + baseURL = strings.TrimSuffix(baseURL, ".git") + u, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + u.Path = path.Join(u.Path, dir) + _, content, err := utils.Parse(u.String()) + if err != nil { + return nil, err + } + _, dirs, _, err := clt.Repositories.GetContents(context.Background(), content.Owner, content.Repo, content.Path, nil) + if err != nil { + return nil, err + } + for _, subItems := range dirs { + if *subItems.Type == "file" { + continue + } + addonRes := apis.DetailAddonResponse{ + AddonMeta: apis.AddonMeta{ + Name: *subItems.Name, + }, + } + var err error + _, files, _, err := clt.Repositories.GetContents(context.Background(), content.Owner, content.Repo, *subItems.Path, nil) + // get addon.yaml and readme.md + for _, file := range files { + switch *file.Name { + case AddonFileName: + addonContent, _, _, err := clt.Repositories.GetContents(context.Background(), content.Owner, content.Repo, *file.Path, nil) + if err != nil { + break + } + addonStr, _ := addonContent.GetContent() + obj := &unstructured.Unstructured{} + _, _, err = dec.Decode([]byte(addonStr), nil, obj) + if err != nil { + break + } + addonRes.AddonMeta.Description = obj.GetAnnotations()[addonutil.DescAnnotation] + addonRes.DeployData = addonStr + case AddonReadmeFileName: + if detailed { + detailContent, _, _, err := clt.Repositories.GetContents(context.Background(), content.Owner, content.Repo, *file.Path, nil) + if err != nil { + break + } + addonRes.Detail, err = detailContent.GetContent() + if err != nil { + break + } + } + default: + continue + } + + } + if err != nil { + continue + } + addons = append(addons, &addonRes) + } + return addons, nil +} + +func getAddonsFromConfigMap(detailed bool) ([]*apis.DetailAddonResponse, error) { + repo, err := addonutil.NewAddonRepo() + if err != nil { + return nil, fmt.Errorf("failed to get configMap addon repo: %w", err) + } + cliAddons := repo.ListAddons() + addons := []*apis.DetailAddonResponse{} + for _, addon := range cliAddons { + d := &apis.DetailAddonResponse{ + AddonMeta: apis.AddonMeta{ + Name: addon.Name, + // TODO add actual Version, Icon, tags + Version: "v1alpha1", + Description: addon.Description, + Icon: "", + Tags: nil, + }, + DeployData: addon.Data, + } + if detailed { + d.Detail = addon.Detail + } + addons = append(addons, d) + } + return addons, nil + +} diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go new file mode 100644 index 000000000..4b1ef71a0 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/addon.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 bcode + +var ( + + // ErrAddonNotExist addon not exist + ErrAddonNotExist = NewBcode(400, 50001, "addon not exist") + + // ErrAddonRegistryExist application is exist + ErrAddonRegistryExist = NewBcode(400, 50002, "addon name already exists") + + // ErrAddonRenderFail fail to render addon application + ErrAddonRenderFail = NewBcode(500, 50010, "addon render fail") + + // ErrAddonApplyFail fail to apply application to cluster + ErrAddonApplyFail = NewBcode(500, 50011, "fail to apply addon application") + + // ErrGetClientFail fail to get k8s client + ErrGetClientFail = NewBcode(500, 50012, "fail to initialize kubernetes client") + + // ErrGetApplicationFail fail to get addon application + ErrGetApplicationFail = NewBcode(500, 50013, "fail to get addon application") + + // ErrGetConfigMapAddonFail fail to get addon info in configmap + ErrGetConfigMapAddonFail = NewBcode(500, 50014, "fail to get addon information in ConfigMap") + + // ErrAddonDisableFail fail to disable addon + ErrAddonDisableFail = NewBcode(500, 50016, "fail to disable addon") + + // ErrAddonNotEnabled means addon can't be disable because it's not enabled + ErrAddonNotEnabled = NewBcode(400, 50017, "addon not enabled") +) diff --git a/pkg/apiserver/rest/utils/convert.go b/pkg/apiserver/rest/utils/convert.go new file mode 100644 index 000000000..9da4c237f --- /dev/null +++ b/pkg/apiserver/rest/utils/convert.go @@ -0,0 +1,14 @@ +package utils + +import ( + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +// ConvertAddonRegistryModel2AddonRegistryMeta will convert from model to AddonRegistryMeta +func ConvertAddonRegistryModel2AddonRegistryMeta(r *model.AddonRegistry) *apisv1.AddonRegistryMeta { + return &apisv1.AddonRegistryMeta{ + Name: r.Name, + Git: r.Git, + } +} diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 182266c31..52e18f988 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -19,16 +19,25 @@ package webservice import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" "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 addonWebService struct { +// NewAddonWebService returns addon web service +func NewAddonWebService(u usecase.AddonUsecase) WebService { + return &addonWebService{ + addonUsecase: u, + } } -func (c *addonWebService) GetWebService() *restful.WebService { +type addonWebService struct { + addonUsecase usecase.AddonUsecase +} + +func (s *addonWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) - ws.Path("/v1/addons"). + ws.Path(versionPrefix+"/addons"). Consumes(restful.MIME_XML, restful.MIME_JSON). Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for addon management") @@ -36,53 +45,132 @@ func (c *addonWebService) GetWebService() *restful.WebService { tags := []string{"addon"} // List - ws.Route(ws.GET("/").To(noop). + ws.Route(ws.GET("/").To(s.listAddons). Doc("list all addons"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")). - Writes(apis.ListAddonResponse{}).Do(returns200, returns500)) - - // Create - ws.Route(ws.POST("/").To(noop). - Doc("create an addon"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateAddonRequest{}). - Writes(apis.AddonMeta{})) - - // Delete - ws.Route(ws.DELETE("/{name}").To(noop). - Doc("delete an addon"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the addon").DataType("string")). - Writes(apis.AddonMeta{})) + Returns(200, "", apis.ListAddonResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListAddonResponse{})) // GET - ws.Route(ws.GET("/{name}").To(noop). + ws.Route(ws.GET("/{name}").To(s.detailAddon). Doc("show details of an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the addon").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.DetailAddonResponse{}). + Returns(400, "", bcode.Bcode{}). + Param(ws.QueryParameter("name", "addon name to query detail").DataType("string").Required(true)). Writes(apis.DetailAddonResponse{})) // GET status - ws.Route(ws.GET("/{name}/status").To(noop). + ws.Route(ws.GET("/status").To(s.statusAddon). Doc("show status of an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the addon").DataType("string")). + Returns(200, "", apis.AddonStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Param(ws.QueryParameter("name", "addon name to query status").DataType("string").Required(true)). Writes(apis.AddonStatusResponse{})) - // vela enable addon - ws.Route(ws.POST("/{name}/enable").To(noop). - Doc("enable an addon on a cluster"). + // enable addon + ws.Route(ws.POST("/enable").To(s.enableAddon). + Doc("enable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("cluster", "cluster name").DataType("string")). - Writes(apis.AddonMeta{})) + Returns(200, "", apis.AddonStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Param(ws.QueryParameter("name", "addon name to enable").DataType("string").Required(true)). + Writes(apis.AddonStatusResponse{})) - // vela disable addon - ws.Route(ws.POST("/{name}/disable").To(noop). - Doc("disable an addon on a cluster"). + // disable addon + ws.Route(ws.POST("/disable").To(s.disableAddon). + Doc("disable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("cluster", "cluster name").DataType("string")). - Writes(apis.AddonMeta{})) + Returns(200, "", apis.AddonStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Param(ws.QueryParameter("name", "addon name to enable").DataType("string").Required(true)). + Writes(apis.AddonStatusResponse{})) return ws } + +func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response) { + detailAddons, err := s.addonUsecase.ListAddons(req.Request.Context(), false) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + var addons []*apis.AddonMeta + + for _, d := range detailAddons { + addons = append(addons, &d.AddonMeta) + } + + err = res.WriteEntity(apis.ListAddonResponse{Addons: addons}) + if err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (s *addonWebService) detailAddon(req *restful.Request, res *restful.Response) { + name := req.QueryParameter("name") + addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + err = res.WriteEntity(addon) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + +} + +func (s *addonWebService) enableAddon(req *restful.Request, res *restful.Response) { + var createReq apis.EnableAddonRequest + err := req.ReadEntity(&createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err = validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + name := req.QueryParameter("name") + err = s.addonUsecase.EnableAddon(req.Request.Context(), name, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + s.statusAddon(req, res) +} + +func (s *addonWebService) disableAddon(req *restful.Request, res *restful.Response) { + name := req.QueryParameter("name") + err := s.addonUsecase.DisableAddon(req.Request.Context(), name) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + s.statusAddon(req, res) +} + +func (s *addonWebService) statusAddon(req *restful.Request, res *restful.Response) { + name := req.QueryParameter("name") + status, err := s.addonUsecase.StatusAddon(name) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + err = res.WriteEntity(*status) + if err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/addon_registry.go b/pkg/apiserver/rest/webservice/addon_registry.go new file mode 100644 index 000000000..8b45fb84f --- /dev/null +++ b/pkg/apiserver/rest/webservice/addon_registry.go @@ -0,0 +1,109 @@ +/* +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" + "github.com/emicklei/go-restful/v3" + + "github.com/oam-dev/kubevela/pkg/apiserver/log" + 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" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +// NewAddonRegistryWebService returns addon registry web service +func NewAddonRegistryWebService(u usecase.AddonUsecase) WebService { + return &addonRegistryWebService{ + addonUsecase: u, + } +} + +type addonRegistryWebService struct { + addonUsecase usecase.AddonUsecase +} + +func (s *addonRegistryWebService) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/addon_registries"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for addon registry management") + + tags := []string{"addon_registry"} + + // Create + ws.Route(ws.POST("/").To(s.createAddonRegistry). + Doc("create an addon registry"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateAddonRegistryRequest{}). + Returns(200, "", apis.AddonRegistryMeta{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.AddonRegistryMeta{})) + + // Delete + ws.Route(ws.DELETE("/{name}").To(s.deleteAddonRegistry). + Doc("delete an addon registry"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("name", "identifier of the addon registry").DataType("string")). + Returns(200, "", apis.AddonRegistryMeta{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.AddonRegistryMeta{})) + + return ws +} + +func (s *addonRegistryWebService) createAddonRegistry(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateAddonRegistryRequest + 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 + meta, err := s.addonUsecase.CreateAddonRegistry(req.Request.Context(), createReq) + if err != nil { + log.Logger.Errorf("create addon registry failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(meta); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (s *addonRegistryWebService) deleteAddonRegistry(req *restful.Request, res *restful.Response) { + r, err := s.addonUsecase.GetAddonRegistryModel(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(*utils.ConvertAddonRegistryModel2AddonRegistryMeta(r)); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index d057ce176..a7e92227f 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -65,11 +65,13 @@ func Init(ctx context.Context, ds datastore.DataStore) { namespaceUsecase := usecase.NewNamespaceUsecase() oamApplicationUsecase := usecase.NewOAMApplicationUsecase() definitionUsecase := usecase.NewDefinitionUsecase() + addonUsecase := usecase.NewAddonUsecase(ds) RegistWebService(NewClusterWebService(clusterUsecase)) RegistWebService(NewApplicationWebService(applicationUsecase)) RegistWebService(NewNamespaceWebService(namespaceUsecase)) RegistWebService(NewComponentDefinitionWebservice(definitionUsecase)) - RegistWebService(&addonWebService{}) + RegistWebService(NewAddonWebService(addonUsecase)) + RegistWebService(NewAddonRegistryWebService(addonUsecase)) RegistWebService(NewOAMApplication(oamApplicationUsecase)) RegistWebService(&policyDefinitionWebservice{}) RegistWebService(NewWorkflowWebService(workflowUsecase, applicationUsecase)) diff --git a/pkg/utils/addon/addon.go b/pkg/utils/addon/addon.go new file mode 100644 index 000000000..403ba95ec --- /dev/null +++ b/pkg/utils/addon/addon.go @@ -0,0 +1,281 @@ +package addon + +import ( + "bytes" + "context" + "fmt" + "strings" + "text/template" + "time" + + "github.com/Masterminds/sprig" + "github.com/pkg/errors" + + "github.com/oam-dev/kubevela/pkg/utils/common" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + types2 "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/controller-runtime/pkg/client" + + common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/utils/apply" +) + +const ( + // DescAnnotation records the Description of addon + DescAnnotation = "addons.oam.dev/description" +) + +var ( + // StatusUninstalled means addon not installed + StatusUninstalled = "uninstalled" + // StatusInstalled means addon installed + StatusInstalled = "installed" + clt client.Client + clientArgs common.Args +) + +func init() { + clientArgs, _ = common.InitBaseRestConfig() + clt, _ = clientArgs.GetClient() + +} + +func newAddon(data *v1.ConfigMap) *Addon { + description := data.ObjectMeta.Annotations[DescAnnotation] + a := Addon{ + Name: data.Annotations[oam.AnnotationAddonsName], + Description: description, + Detail: data.Data["detail"], + Data: data.Data["application"], + } + return &a +} + +// Repo is a place to store addon info +type Repo interface { + GetAddon(name string) (Addon, error) + ListAddons() []Addon +} + +// NewAddonRepo create new addon repo,now only support ConfigMap +func NewAddonRepo() (Repo, error) { + list := v1.ConfigMapList{} + matchLabels := metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: oam.LabelAddonsName, + Operator: metav1.LabelSelectorOpExists, + }}, + } + selector, err := metav1.LabelSelectorAsSelector(&matchLabels) + if err != nil { + return nil, err + } + err = clt.List(context.Background(), &list, &client.ListOptions{LabelSelector: selector}) + if err != nil { + return nil, errors.Wrap(err, "Get addon list failed") + } + return configMapAddonRepo{maps: list.Items}, nil +} + +type configMapAddonRepo struct { + maps []v1.ConfigMap +} + +// NotFoundErr means addon not found +type NotFoundErr struct { + addonName string +} + +func (e NotFoundErr) Error() string { + return fmt.Sprintf("addon %s not found", e.addonName) +} + +// GetAddon will get addon from ConfigMap +func (c configMapAddonRepo) GetAddon(name string) (Addon, error) { + for i := range c.maps { + if addonName, ok := c.maps[i].Annotations[oam.AnnotationAddonsName]; ok && name == addonName { + return *newAddon(&c.maps[i]), nil + } + } + return Addon{}, NotFoundErr{addonName: name} +} + +// ListAddons will list addons from ConfigMap +func (c configMapAddonRepo) ListAddons() []Addon { + var addons []Addon + for i := range c.maps { + addon := newAddon(&c.maps[i]) + addons = append(addons, *addon) + } + return addons +} + +// Addon consist of a Initializer resource to Enable an addon +type Addon struct { + Name string + Description string + Data string + // Args is map for renderInitializer + Args map[string]string + application *unstructured.Unstructured + gvk *schema.GroupVersionKind + + // Detail is doc for addon + Detail string +} + +// GetGVK will return addon's application's GVK +func (a *Addon) GetGVK() (*schema.GroupVersionKind, error) { + if a.gvk == nil { + if a.application == nil { + _, err := a.RenderApplication() + if err != nil { + return nil, err + } + } + gvk := schema.FromAPIVersionAndKind(a.application.GetAPIVersion(), a.application.GetKind()) + a.gvk = &gvk + } + return a.gvk, nil +} + +// RenderApplication will render addon application +// this will use addon's Data and Args +func (a *Addon) RenderApplication() (*unstructured.Unstructured, error) { + if a.Args == nil { + a.Args = map[string]string{} + } + t, err := template.New("addon-template").Delims("[[", "]]").Funcs(sprig.TxtFuncMap()).Parse(a.Data) + if err != nil { + return nil, errors.Wrap(err, "parsing addon initializer template error") + } + buf := bytes.Buffer{} + err = t.Execute(&buf, a) + if err != nil { + return nil, errors.Wrap(err, "application template render fail") + } + dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + obj := &unstructured.Unstructured{} + _, gvk, err := dec.Decode(buf.Bytes(), nil, obj) + if err != nil { + return nil, err + } + a.application = obj + a.gvk = gvk + return a.application, nil +} + +// Enable will enable an addon by apply application +func (a *Addon) Enable() error { + applicator := apply.NewAPIApplicator(clt) + ctx := context.Background() + obj, err := a.RenderApplication() + if err != nil { + return err + } + err = applicator.Apply(ctx, obj) + if err != nil { + return errors.Wrapf(err, "Error occurs when apply addon application: %s\n", a.Name) + } + err = waitApplicationRunning(a.application) + if err != nil { + return errors.Wrap(err, "Error occurs when waiting addon applicatoin running") + } + return nil +} + +func waitApplicationRunning(obj *unstructured.Unstructured) error { + ctx := context.Background() + period := 20 * time.Second + timeout := 10 * time.Minute + var app v1beta1.Application + return wait.PollImmediate(period, timeout, func() (done bool, err error) { + err = clt.Get(ctx, types2.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, &app) + if err != nil { + return false, client.IgnoreNotFound(err) + } + phase := app.Status.Phase + if phase == common2.ApplicationRunning { + return true, nil + } + fmt.Printf("Application %s is in phase:%s...\n", obj.GetName(), phase) + return false, nil + }) +} + +// Disable will delete addon's application +func (a *Addon) Disable() error { + dynamicClient, err := dynamic.NewForConfig(clientArgs.Config) + if err != nil { + return err + } + mapper, err := discoverymapper.New(clientArgs.Config) + if err != nil { + return err + } + obj, err := a.RenderApplication() + if err != nil { + return err + } + gvk, err := a.GetGVK() + if err != nil { + return err + } + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return err + } + var resourceREST dynamic.ResourceInterface + if mapping.Scope.Name() == meta.RESTScopeNameNamespace { + // namespaced resources should specify the namespace + resourceREST = dynamicClient.Resource(mapping.Resource).Namespace(obj.GetNamespace()) + } else { + // for cluster-wide resources + resourceREST = dynamicClient.Resource(mapping.Resource) + } + deletePolicy := metav1.DeletePropagationForeground + deleteOptions := metav1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + } + fmt.Println("Deleting all resources...") + err = resourceREST.Delete(context.TODO(), obj.GetName(), deleteOptions) + if err != nil { + return err + } + return nil +} + +// GetStatus will return if an Addon is enabled +func (a *Addon) GetStatus() string { + var application v1beta1.Application + err := clt.Get(context.Background(), client.ObjectKey{ + Namespace: types.DefaultKubeVelaNS, + Name: TransAddonName(a.Name), + }, &application) + if err != nil { + return StatusUninstalled + } + return StatusInstalled +} + +// SetArgs will set Args for application render +func (a *Addon) SetArgs(args map[string]string) { + a.Args = args +} + +// TransAddonName will turn addon's name from xxx/yyy to xxx-yyy +func TransAddonName(name string) string { + return strings.ReplaceAll(name, "/", "-") +} diff --git a/pkg/utils/parse.go b/pkg/utils/parse.go new file mode 100644 index 000000000..fb1872957 --- /dev/null +++ b/pkg/utils/parse.go @@ -0,0 +1,120 @@ +package utils + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// TypeLocal represents github +const TypeLocal = "local" + +// TypeOss represent oss +const TypeOss = "oss" + +// TypeGithub represents github +const TypeGithub = "github" + +// TypeUnknown represents parse failed +const TypeUnknown = "unknown" + +// Content contains different type of content needed when building Registry +type Content struct { + OssContent + GithubContent + LocalContent +} + +// LocalContent for local registry +type LocalContent struct { + AbsDir string `json:"abs_dir"` +} + +// OssContent for oss registry +type OssContent struct { + BucketURL string `json:"bucket_url"` +} + +// GithubContent for cap center +type GithubContent struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + Path string `json:"path"` + Ref string `json:"ref"` +} + +// Parse will parse config from address +func Parse(addr string) (string, *Content, error) { + URL, err := url.Parse(addr) + if err != nil { + return "", nil, err + } + l := strings.Split(strings.TrimPrefix(URL.Path, "/"), "/") + switch URL.Scheme { + case "http", "https": + switch URL.Host { + case "github.com": + // We support two valid format: + // 1. https://github.com///tree// + // 2. https://github.com/// + if len(l) < 3 { + return "", nil, errors.New("invalid format " + addr) + } + if l[2] == "tree" { + // https://github.com///tree// + if len(l) < 5 { + return "", nil, errors.New("invalid format " + addr) + } + return TypeGithub, &Content{ + GithubContent: GithubContent{ + Owner: l[0], + Repo: l[1], + Path: strings.Join(l[4:], "/"), + Ref: l[3], + }, + }, nil + } + // https://github.com/// + return TypeGithub, &Content{ + GithubContent: GithubContent{ + Owner: l[0], + Repo: l[1], + Path: strings.Join(l[2:], "/"), + Ref: "", // use default branch + }, + }, + nil + case "api.github.com": + if len(l) != 5 { + return "", nil, errors.New("invalid format " + addr) + } + //https://api.github.com/repos///contents/ + return TypeGithub, &Content{ + GithubContent: GithubContent{ + Owner: l[1], + Repo: l[2], + Path: l[4], + Ref: URL.Query().Get("ref"), + }, + }, + nil + default: + } + case "oss": + return TypeOss, &Content{ + OssContent: OssContent{ + BucketURL: URL.Host, + }, + }, nil + case "file": + return TypeLocal, &Content{ + LocalContent: LocalContent{ + AbsDir: URL.Path, + }, + }, nil + + } + + return TypeUnknown, nil, nil +} diff --git a/references/cli/addon.go b/references/cli/addon.go index d5c8f1570..f687eb5b7 100644 --- a/references/cli/addon.go +++ b/references/cli/addon.go @@ -17,53 +17,27 @@ limitations under the License. package cli import ( - "bytes" "context" "fmt" "strings" - "text/template" - "time" - common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - - "github.com/Masterminds/sprig" "github.com/gosuri/uitable" "github.com/pkg/errors" "github.com/spf13/cobra" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer/yaml" - types2 "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/dynamic" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/oam" - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" - "github.com/oam-dev/kubevela/pkg/utils/apply" + addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/common" cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" ) -const ( - // DescAnnotation records the description of addon - DescAnnotation = "addons.oam.dev/description" -) - -var statusUninstalled = "uninstalled" -var statusInstalled = "installed" -var clt client.Client -var clientArgs common.Args - var legacyAddonNamespace map[string]string +var clt client.Client func init() { - clientArgs, _ = common.InitBaseRestConfig() + clientArgs, _ := common.InitBaseRestConfig() clt, _ = clientArgs.GetClient() legacyAddonNamespace = map[string]string{ "fluxcd": types.DefaultKubeVelaNS, @@ -182,31 +156,31 @@ func NewAddonDisableCommand(ioStream cmdutil.IOStreams) *cobra.Command { } func listAddons() error { - repo, err := NewAddonRepo() + repo, err := addonutil.NewAddonRepo() if err != nil { return err } - addons := repo.listAddons() + addons := repo.ListAddons() table := uitable.New() table.AddRow("NAME", "DESCRIPTION", "STATUS") for _, addon := range addons { - table.AddRow(addon.name, addon.description, addon.getStatus()) + table.AddRow(addon.Name, addon.Description, addon.GetStatus()) } fmt.Println(table.String()) return nil } func enableAddon(name string, args map[string]string) error { - repo, err := NewAddonRepo() + repo, err := addonutil.NewAddonRepo() if err != nil { return err } - addon, err := repo.getAddon(name) + addon, err := repo.GetAddon(name) if err != nil { return err } - addon.setArgs(args) - err = addon.enable() + addon.SetArgs(args) + err = addon.Enable() return err } @@ -214,25 +188,25 @@ func disableAddon(name string) error { if isLegacyAddonExist(name) { return tryDisableInitializerAddon(name) } - repo, err := NewAddonRepo() + repo, err := addonutil.NewAddonRepo() if err != nil { return err } - addon, err := repo.getAddon(name) + addon, err := repo.GetAddon(name) if err != nil { return errors.Wrap(err, "get addon err") } - if addon.getStatus() == statusUninstalled { - fmt.Printf("Addon %s is not installed\n", addon.name) + if addon.GetStatus() == addonutil.StatusUninstalled { + fmt.Printf("Addon %s is not installed\n", addon.Name) return nil } - return addon.disable() + return addon.Disable() } func isLegacyAddonExist(name string) bool { if namespace, ok := legacyAddonNamespace[name]; ok { - convertedAddonName := TransAddonName(name) + convertedAddonName := addonutil.TransAddonName(name) init := unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "core.oam.dev/v1beta1", @@ -255,7 +229,7 @@ func tryDisableInitializerAddon(addonName string) error { "apiVersion": "core.oam.dev/v1beta1", "kind": "Initializer", "metadata": map[string]interface{}{ - "name": TransAddonName(addonName), + "name": addonutil.TransAddonName(addonName), "namespace": legacyAddonNamespace[addonName], }, }, @@ -263,212 +237,3 @@ func tryDisableInitializerAddon(addonName string) error { return clt.Delete(context.TODO(), &init) } -func newAddon(data *v1.ConfigMap) *Addon { - description := data.ObjectMeta.Annotations[DescAnnotation] - a := Addon{name: data.Annotations[oam.AnnotationAddonsName], description: description, data: data.Data["application"]} - return &a -} - -// AddonRepo is a place to store addon info -type AddonRepo interface { - getAddon(name string) (Addon, error) - listAddons() []Addon -} - -// NewAddonRepo create new addon repo,now only support ConfigMap -func NewAddonRepo() (AddonRepo, error) { - list := v1.ConfigMapList{} - matchLabels := metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{{ - Key: oam.LabelAddonsName, - Operator: metav1.LabelSelectorOpExists, - }}, - } - selector, err := metav1.LabelSelectorAsSelector(&matchLabels) - if err != nil { - return nil, err - } - err = clt.List(context.Background(), &list, &client.ListOptions{LabelSelector: selector}) - if err != nil { - return nil, errors.Wrap(err, "Get addon list failed") - } - return configMapAddonRepo{maps: list.Items}, nil -} - -type configMapAddonRepo struct { - maps []v1.ConfigMap -} - -// AddonNotFoundErr means addon not found -type AddonNotFoundErr struct { - addonName string -} - -func (e AddonNotFoundErr) Error() string { - return fmt.Sprintf("addon %s not found", e.addonName) -} - -func (c configMapAddonRepo) getAddon(name string) (Addon, error) { - for i := range c.maps { - if addonName, ok := c.maps[i].Annotations[oam.AnnotationAddonsName]; ok && name == addonName { - return *newAddon(&c.maps[i]), nil - } - } - return Addon{}, AddonNotFoundErr{addonName: name} -} - -func (c configMapAddonRepo) listAddons() []Addon { - var addons []Addon - for i := range c.maps { - addon := newAddon(&c.maps[i]) - addons = append(addons, *addon) - } - return addons -} - -// Addon consist of a Initializer resource to enable an addon -type Addon struct { - name string - description string - data string - // Args is map for renderInitializer - Args map[string]string - application *unstructured.Unstructured - gvk *schema.GroupVersionKind -} - -func (a *Addon) getGVK() (*schema.GroupVersionKind, error) { - if a.gvk == nil { - if a.application == nil { - _, err := a.renderApplication() - if err != nil { - return nil, err - } - } - gvk := schema.FromAPIVersionAndKind(a.application.GetAPIVersion(), a.application.GetKind()) - a.gvk = &gvk - } - return a.gvk, nil -} - -func (a *Addon) renderApplication() (*unstructured.Unstructured, error) { - if a.Args == nil { - a.Args = map[string]string{} - } - t, err := template.New("addon-template").Delims("[[", "]]").Funcs(sprig.TxtFuncMap()).Parse(a.data) - if err != nil { - return nil, errors.Wrap(err, "parsing addon initializer template error") - } - buf := bytes.Buffer{} - err = t.Execute(&buf, a) - if err != nil { - return nil, errors.Wrap(err, "application template render fail") - } - dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) - obj := &unstructured.Unstructured{} - _, gvk, err := dec.Decode(buf.Bytes(), nil, obj) - if err != nil { - return nil, err - } - a.application = obj - a.gvk = gvk - return a.application, nil -} - -func (a *Addon) enable() error { - applicator := apply.NewAPIApplicator(clt) - ctx := context.Background() - obj, err := a.renderApplication() - if err != nil { - return err - } - err = applicator.Apply(ctx, obj) - if err != nil { - return errors.Wrapf(err, "Error occurs when apply addon application: %s\n", a.name) - } - err = waitApplicationRunning(a.application) - if err != nil { - return errors.Wrap(err, "Error occurs when waiting addon applicatoin running") - } - return nil -} - -func waitApplicationRunning(obj *unstructured.Unstructured) error { - ctx := context.Background() - period := 20 * time.Second - timeout := 10 * time.Minute - var app v1beta1.Application - return wait.PollImmediate(period, timeout, func() (done bool, err error) { - err = clt.Get(ctx, types2.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, &app) - if err != nil { - return false, client.IgnoreNotFound(err) - } - phase := app.Status.Phase - if phase == common2.ApplicationRunning { - return true, nil - } - fmt.Printf("Application %s is in phase:%s...\n", obj.GetName(), phase) - return false, nil - }) -} -func (a *Addon) disable() error { - dynamicClient, err := dynamic.NewForConfig(clientArgs.Config) - if err != nil { - return err - } - mapper, err := discoverymapper.New(clientArgs.Config) - if err != nil { - return err - } - obj, err := a.renderApplication() - if err != nil { - return err - } - gvk, err := a.getGVK() - if err != nil { - return err - } - mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) - if err != nil { - return err - } - var resourceREST dynamic.ResourceInterface - if mapping.Scope.Name() == meta.RESTScopeNameNamespace { - // namespaced resources should specify the namespace - resourceREST = dynamicClient.Resource(mapping.Resource).Namespace(obj.GetNamespace()) - } else { - // for cluster-wide resources - resourceREST = dynamicClient.Resource(mapping.Resource) - } - deletePolicy := metav1.DeletePropagationForeground - deleteOptions := metav1.DeleteOptions{ - PropagationPolicy: &deletePolicy, - } - fmt.Println("Deleting all resources...") - err = resourceREST.Delete(context.TODO(), obj.GetName(), deleteOptions) - if err != nil { - return err - } - return nil -} - -func (a *Addon) getStatus() string { - var application v1beta1.Application - err := clt.Get(context.Background(), client.ObjectKey{ - Namespace: types.DefaultKubeVelaNS, - Name: TransAddonName(a.name), - }, &application) - if err != nil { - return statusUninstalled - } - return statusInstalled -} - -func (a *Addon) setArgs(args map[string]string) { - a.Args = args -} - -// TransAddonName will turn addon's name from xxx/yyy to xxx-yyy -func TransAddonName(name string) string { - return strings.ReplaceAll(name, "/", "-") -} diff --git a/references/plugins/capcenter.go b/references/plugins/capcenter.go index 01c4038ac..55afcc306 100644 --- a/references/plugins/capcenter.go +++ b/references/plugins/capcenter.go @@ -20,17 +20,18 @@ import ( "context" "errors" "fmt" + "net/http" - "net/url" "os" "path/filepath" - "strings" "github.com/google/go-github/v32/github" "golang.org/x/oauth2" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" + "github.com/oam-dev/kubevela/pkg/utils" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" @@ -38,31 +39,6 @@ import ( "github.com/oam-dev/kubevela/pkg/utils/system" ) -// Content contains different type of content needed when building Registry -type Content struct { - OssContent - GithubContent - LocalContent -} - -// LocalContent for local registry -type LocalContent struct { - AbsDir string `json:"abs_dir"` -} - -// OssContent for oss registry -type OssContent struct { - BucketURL string `json:"bucket_url"` -} - -// GithubContent for cap center -type GithubContent struct { - Owner string `json:"owner"` - Repo string `json:"repo"` - Path string `json:"path"` - Ref string `json:"ref"` -} - // CapCenterConfig is used to store cap center config in file type CapCenterConfig struct { Name string `json:"name"` @@ -77,107 +53,20 @@ type CenterClient interface { // NewCenterClient create a client from type func NewCenterClient(ctx context.Context, name, address, token string) (CenterClient, error) { - Type, cfg, err := Parse(address) + Type, cfg, err := utils.Parse(address) if err != nil { return nil, err } switch Type { - case TypeGithub: + case utils.TypeGithub: return NewGithubCenter(ctx, token, name, &cfg.GithubContent) - case TypeOss: + case utils.TypeOss: return NewOssCenter(fmt.Sprintf("https://%s/", cfg.BucketURL), name), nil default: } return nil, errors.New("we only support github as repository now") } -// TypeLocal represents github -const TypeLocal = "local" - -// TypeOss represent oss -const TypeOss = "oss" - -// TypeGithub represents github -const TypeGithub = "github" - -// TypeUnknown represents parse failed -const TypeUnknown = "unknown" - -// Parse will parse config from address -func Parse(addr string) (string, *Content, error) { - URL, err := url.Parse(addr) - if err != nil { - return "", nil, err - } - l := strings.Split(strings.TrimPrefix(URL.Path, "/"), "/") - switch URL.Scheme { - case "http", "https": - switch URL.Host { - case "github.com": - // We support two valid format: - // 1. https://github.com///tree// - // 2. https://github.com/// - if len(l) < 3 { - return "", nil, errors.New("invalid format " + addr) - } - if l[2] == "tree" { - // https://github.com///tree// - if len(l) < 5 { - return "", nil, errors.New("invalid format " + addr) - } - return TypeGithub, &Content{ - GithubContent: GithubContent{ - Owner: l[0], - Repo: l[1], - Path: strings.Join(l[4:], "/"), - Ref: l[3], - }, - }, nil - } - // https://github.com/// - return TypeGithub, &Content{ - GithubContent: GithubContent{ - Owner: l[0], - Repo: l[1], - Path: strings.Join(l[2:], "/"), - Ref: "", // use default branch - }, - }, - nil - case "api.github.com": - if len(l) != 5 { - return "", nil, errors.New("invalid format " + addr) - } - //https://api.github.com/repos///contents/ - return TypeGithub, &Content{ - GithubContent: GithubContent{ - Owner: l[1], - Repo: l[2], - Path: l[4], - Ref: URL.Query().Get("ref"), - }, - }, - nil - default: - } - case "oss": - return TypeOss, &Content{ - OssContent: OssContent{ - BucketURL: URL.Host, - }, - }, nil - case "file": - return TypeLocal, &Content{ - LocalContent: LocalContent{ - AbsDir: URL.Path, - }, - }, nil - - } - - return TypeUnknown, nil, nil -} - // LoadRepos will load all cap center repos func LoadRepos() ([]CapCenterConfig, error) { defaultRepo := CapCenterConfig{ @@ -259,7 +148,7 @@ func ParseCapability(mapper discoverymapper.DiscoveryMapper, data []byte) (types } // NewGithubCenter will create client by github center implementation -func NewGithubCenter(ctx context.Context, token, centerName string, r *GithubContent) (*GithubRegistry, error) { +func NewGithubCenter(ctx context.Context, token, centerName string, r *utils.GithubContent) (*GithubRegistry, error) { var tc *http.Client if token != "" { ts := oauth2.StaticTokenSource( diff --git a/references/plugins/capcenter_test.go b/references/plugins/capcenter_test.go index 3a53047a5..38875995e 100644 --- a/references/plugins/capcenter_test.go +++ b/references/plugins/capcenter_test.go @@ -19,19 +19,21 @@ package plugins import ( "testing" + "github.com/oam-dev/kubevela/pkg/utils" + "github.com/stretchr/testify/assert" ) func TestParseURL(t *testing.T) { cases := map[string]struct { url string - exp *GithubContent + exp *utils.GithubContent expType string }{ "api-github": { url: "https://api.github.com/repos/zzxwill/catalog/contents/repository?ref=plugin", - expType: TypeGithub, - exp: &GithubContent{ + expType: utils.TypeGithub, + exp: &utils.GithubContent{ Owner: "zzxwill", Repo: "catalog", Path: "repository", @@ -40,8 +42,8 @@ func TestParseURL(t *testing.T) { }, "github-copy-path": { url: "https://github.com/zzxwill/catalog/tree/plugin/repository", - expType: TypeGithub, - exp: &GithubContent{ + expType: utils.TypeGithub, + exp: &utils.GithubContent{ Owner: "zzxwill", Repo: "catalog", Path: "repository", @@ -50,8 +52,8 @@ func TestParseURL(t *testing.T) { }, "github-manuel-write-path": { url: "https://github.com/zzxwill/catalog/repository", - expType: TypeGithub, - exp: &GithubContent{ + expType: utils.TypeGithub, + exp: &utils.GithubContent{ Owner: "zzxwill", Repo: "catalog", Path: "repository", @@ -59,7 +61,7 @@ func TestParseURL(t *testing.T) { }, } for caseName, c := range cases { - tp, content, err := Parse(c.url) + tp, content, err := utils.Parse(c.url) assert.NoError(t, err, caseName) assert.Equal(t, c.exp, &content.GithubContent, caseName) assert.Equal(t, c.expType, tp, caseName) diff --git a/references/plugins/registry.go b/references/plugins/registry.go index f60eefcfa..5c24e8ccc 100644 --- a/references/plugins/registry.go +++ b/references/plugins/registry.go @@ -27,6 +27,8 @@ import ( "path" "path/filepath" + "github.com/oam-dev/kubevela/pkg/utils" + "github.com/oam-dev/kubevela/apis/types" "github.com/google/go-github/v32/github" @@ -45,19 +47,19 @@ type Registry interface { // GithubRegistry is Registry's implementation treat github url as resource type GithubRegistry struct { client *github.Client - cfg *GithubContent + cfg *utils.GithubContent ctx context.Context centerName string // to be used to cache registry } // NewRegistry will create a registry implementation func NewRegistry(ctx context.Context, token, registryName string, regURL string) (Registry, error) { - tp, cfg, err := Parse(regURL) + tp, cfg, err := utils.Parse(regURL) if err != nil { return nil, err } switch tp { - case TypeGithub: + case utils.TypeGithub: var tc *http.Client if token != "" { ts := oauth2.StaticTokenSource( @@ -66,19 +68,19 @@ func NewRegistry(ctx context.Context, token, registryName string, regURL string) tc = oauth2.NewClient(ctx, ts) } return GithubRegistry{client: github.NewClient(tc), cfg: &cfg.GithubContent, ctx: ctx, centerName: registryName}, nil - case TypeOss: + case utils.TypeOss: var tc http.Client return OssRegistry{ Client: &tc, bucketURL: fmt.Sprintf("https://%s/", cfg.BucketURL), }, nil - case TypeLocal: + case utils.TypeLocal: _, err := os.Stat(cfg.AbsDir) if os.IsNotExist(err) { return LocalRegistry{}, err } return LocalRegistry{absPath: cfg.AbsDir}, nil - case TypeUnknown: + case utils.TypeUnknown: return nil, fmt.Errorf("not supported url") } diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go new file mode 100644 index 000000000..d31549847 --- /dev/null +++ b/test/e2e-apiserver-test/addon_test.go @@ -0,0 +1,125 @@ +package e2e_apiserver + +import ( + "bytes" + "encoding/json" + "net/http" + "os" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +const baseURL = "http://127.0.0.1:8000" + +func post(path string, body interface{}) *http.Response { + b, err := json.Marshal(body) + Expect(err).Should(BeNil()) + + res, err := http.Post(baseURL+path, "application/json", bytes.NewBuffer(b)) + Expect(err).Should(BeNil()) + return res +} + +func get(path string) *http.Response { + res, err := http.Get(baseURL + path) + Expect(err).Should(BeNil()) + return res +} + +var _ = Describe("Test addon rest api", func() { + It("should add a registry and list addons from it and delete the registry", func() { + defer GinkgoRecover() + + By("add registry") + createReq := apis.CreateAddonRegistryRequest{ + Name: "test-addon-registry-1", + Git: &model.GitAddonSource{ + URL: "https://github.com/oam-dev/catalog", + Path: "addon/", + Token: os.Getenv("GITHUB_TOKEN"), + }, + } + createRes := post("/api/v1/addon_registries", createReq) + Expect(createRes).ShouldNot(BeNil()) + Expect(createRes.StatusCode).Should(Equal(200)) + Expect(createRes.Body).ShouldNot(BeNil()) + + defer createRes.Body.Close() + + var rmeta apis.AddonRegistryMeta + err := json.NewDecoder(createRes.Body).Decode(&rmeta) + Expect(err).Should(BeNil()) + Expect(rmeta.Name).Should(Equal(createReq.Name)) + Expect(rmeta.Git).Should(Equal(createReq.Git)) + + By("list addons") + listRes := get("/api/v1/addons/") + defer listRes.Body.Close() + + var lres apis.ListAddonResponse + err = json.NewDecoder(listRes.Body).Decode(&lres) + Expect(err).Should(BeNil()) + Expect(lres.Addons).ShouldNot(BeZero()) + firstAddon := lres.Addons[0] + Expect(firstAddon.Name).Should(Equal("fluxcd")) + + By("delete registry") + deleteReq, err := http.NewRequest(http.MethodDelete, baseURL+"/api/v1/addon_registries/"+createReq.Name, nil) + Expect(err).Should(BeNil()) + deleteRes, err := http.DefaultClient.Do(deleteReq) + Expect(err).Should(BeNil()) + Expect(deleteRes).ShouldNot(BeNil()) + Expect(deleteRes.StatusCode).Should(Equal(200)) + }) + + It("should enable and disable an addon", func() { + defer GinkgoRecover() + req := apis.EnableAddonRequest{ + Args: map[string]string{}, + } + testAddon := "fluxcd" + res := post("/api/v1/addons/enable?name="+testAddon, req) + Expect(res).ShouldNot(BeNil()) + Expect(res.StatusCode).Should(Equal(200)) + Expect(res.Body).ShouldNot(BeNil()) + + defer res.Body.Close() + + var statusRes apis.AddonStatusResponse + err := json.NewDecoder(res.Body).Decode(&statusRes) + + Expect(err).Should(BeNil()) + Expect(statusRes.Phase).Should(Equal(apis.AddonPhaseEnabling)) + + // Wait for addon enabled + + period := 20 * time.Second + timeout := 5 * time.Minute + err = wait.PollImmediate(period, timeout, func() (done bool, err error) { + res = get("/api/v1/addons/status?name=" + testAddon) + err = json.NewDecoder(res.Body).Decode(&statusRes) + Expect(err).Should(BeNil()) + if statusRes.Phase == apis.AddonPhaseEnabled { + return true, nil + } + return false, nil + }) + Expect(err).Should(BeNil()) + + res = post("/api/v1/addons/disable?name="+testAddon, req) + Expect(res).ShouldNot(BeNil()) + Expect(res.StatusCode).Should(Equal(200)) + Expect(res.Body).ShouldNot(BeNil()) + + err = json.NewDecoder(res.Body).Decode(&statusRes) + Expect(err).Should(BeNil()) + + }) +}) diff --git a/test/e2e-apiserver-test/suite_test.go b/test/e2e-apiserver-test/suite_test.go index 4014e9af8..02a879bb7 100644 --- a/test/e2e-apiserver-test/suite_test.go +++ b/test/e2e-apiserver-test/suite_test.go @@ -18,6 +18,8 @@ package e2e_apiserver_test import ( "context" + "errors" + "net/http" "testing" "time" @@ -61,6 +63,17 @@ var _ = BeforeSuite(func() { err = server.Run(ctx) Expect(err).ShouldNot(HaveOccurred()) }() + By("wait for api server to start") + Eventually( + func() error { + res, err := http.Get("http://127.0.0.1:8000/api/v1/namespaces") + if err != nil { + return err + } + if res.StatusCode == http.StatusOK { + return nil + } + return errors.New("rest service not ready") + }, time.Second*5, time.Millisecond*200).Should(BeNil()) By("api server started") - time.Sleep(time.Second * 2) }) diff --git a/vela-templates/addons/fluxcd/readme.md b/vela-templates/addons/fluxcd/readme.md new file mode 100644 index 000000000..d0ea0da55 --- /dev/null +++ b/vela-templates/addons/fluxcd/readme.md @@ -0,0 +1,18 @@ +# fluxcd + +This addon is built based [FluxCD](https://fluxcd.io/) + +## install + +```shell +vela addon enable fluxcd +``` + +## X-Definitions + +Enable fluxcd addon to use these X-definitions + +- [helm](https://kubevela.io/docs/end-user/components/helm) helps to deploy a helm chart from everywhere: +git repo / helm repo / S3 compatible bucket. + +- [kustomize](https://kubevela.io/docs/end-user/components/kustomize) helps to deploy a kustomize style artifact. diff --git a/vela-templates/addons/istio/readme.md b/vela-templates/addons/istio/readme.md new file mode 100644 index 000000000..de9bdab97 --- /dev/null +++ b/vela-templates/addons/istio/readme.md @@ -0,0 +1,3 @@ +# istio + +This addon provides istio support for vela rollout. \ No newline at end of file diff --git a/vela-templates/addons/keda/readme.md b/vela-templates/addons/keda/readme.md new file mode 100644 index 000000000..8cb7a9f0f --- /dev/null +++ b/vela-templates/addons/keda/readme.md @@ -0,0 +1,3 @@ +# keda + +keda \ No newline at end of file diff --git a/vela-templates/addons/kruise/readme.md b/vela-templates/addons/kruise/readme.md new file mode 100644 index 000000000..775584c2c --- /dev/null +++ b/vela-templates/addons/kruise/readme.md @@ -0,0 +1,3 @@ +# kruise + +This addon provides [open-kruise](https://github.com/openkruise/kruise) workload. \ No newline at end of file diff --git a/vela-templates/addons/observability/readme.md b/vela-templates/addons/observability/readme.md new file mode 100644 index 000000000..de09d6dd8 --- /dev/null +++ b/vela-templates/addons/observability/readme.md @@ -0,0 +1,3 @@ +# observability + +This addon expose system and application level metrics for KubeVela. \ No newline at end of file diff --git a/vela-templates/addons/ocm-cluster-manager/readme.md b/vela-templates/addons/ocm-cluster-manager/readme.md new file mode 100644 index 000000000..f9b214285 --- /dev/null +++ b/vela-templates/addons/ocm-cluster-manager/readme.md @@ -0,0 +1,3 @@ +# ocm-cluster-manager + +This addon aims to support multi-cluster application deployment. \ No newline at end of file diff --git a/vela-templates/addons/prometheus/readme.md b/vela-templates/addons/prometheus/readme.md new file mode 100644 index 000000000..4917cfd76 --- /dev/null +++ b/vela-templates/addons/prometheus/readme.md @@ -0,0 +1,3 @@ +# prometheus + +prometheus diff --git a/vela-templates/addons/terraform-provider-alibaba/readme.md b/vela-templates/addons/terraform-provider-alibaba/readme.md new file mode 100644 index 000000000..b092c1804 --- /dev/null +++ b/vela-templates/addons/terraform-provider-alibaba/readme.md @@ -0,0 +1,3 @@ +# terraform/provider-alibaba + +This addon contains terraform provider for Alibaba Cloud. \ No newline at end of file diff --git a/vela-templates/addons/terraform-provider-aws/readme.md b/vela-templates/addons/terraform-provider-aws/readme.md new file mode 100644 index 000000000..82ddc7e7c --- /dev/null +++ b/vela-templates/addons/terraform-provider-aws/readme.md @@ -0,0 +1,3 @@ +# terraform/provider-aws + +This addon contains terraform provider for AWS \ No newline at end of file diff --git a/vela-templates/addons/terraform-provider-azure/readme.md b/vela-templates/addons/terraform-provider-azure/readme.md new file mode 100644 index 000000000..dfc36ff1f --- /dev/null +++ b/vela-templates/addons/terraform-provider-azure/readme.md @@ -0,0 +1,3 @@ +# terraform/provider-azure + +This addon contains terraform provider for Azure. \ No newline at end of file diff --git a/vela-templates/addons/terraform/readme.md b/vela-templates/addons/terraform/readme.md new file mode 100644 index 000000000..55016b4ea --- /dev/null +++ b/vela-templates/addons/terraform/readme.md @@ -0,0 +1,4 @@ +# Terraform + +This addon contains terraform operation kit, which allows you to arrange, +generate and use cloud service from different cloud vendor. \ No newline at end of file diff --git a/vela-templates/gen_addons.go b/vela-templates/gen_addons.go index 3643b2033..4fd68ea32 100644 --- a/vela-templates/gen_addons.go +++ b/vela-templates/gen_addons.go @@ -29,6 +29,8 @@ import ( "strings" "text/template" + addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" + "github.com/Masterminds/sprig" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -39,20 +41,22 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/references/cli" ) const ( + // DetailFileName is readme for each addon + DetailFileName = "readme.md" + // TemplateName represents the Application template file of addons TemplateName = "template.yaml" // ApplicationFileDir is where we store generated application & component definition ApplicationFileDir = "auto-gen" - // ComponentDefDir is where we store correspond componentDefinition for addon - ComponentDefDir = "definitions" + // DefinitionDir is where we store correspond X-Definition for addon + DefinitionDir = "definitions" - // ResourceDir is where we store correspond componentDefinition for addon + // ResourceDir is where we store correspond resources for addon ResourceDir = "resource" // DescAnnotation records the description of addon @@ -66,6 +70,12 @@ const ( // NameAnnotation marked the addon's name if exist, or application's name NameAnnotation = "addons.oam.dev/name" + + // ApplicationKey is the key to store application in ConfigMap + ApplicationKey = "application" + + // DetailKey is the key to store detail information in ConfigMap + DetailKey = "detail" ) // DefaultEnableAddons is default enabled addons @@ -77,11 +87,11 @@ type velaFile struct { Content string } -// AddonInfo records addon's metadata -type AddonInfo struct { +// AddonGenerateInfo records addon's metadata used in addon generation +type AddonGenerateInfo struct { ResourceFiles []velaFile DefinitionFiles []velaFile - HasDefs bool + DetailFile velaFile Name string StoreName string Description string @@ -134,13 +144,14 @@ func newWalkFn(files *[]velaFile) filepath.WalkFunc { } } -func getAddonInfo(addon string, addonsPath string) (*AddonInfo, error) { +func getAddonInfo(addon string, addonsPath string) (*AddonGenerateInfo, error) { addonRoot := filepath.Clean(addonsPath + "/" + addon) resourceRoot := filepath.Clean(addonRoot + "/" + ResourceDir) - defRoot := filepath.Clean(addonRoot + "/" + ComponentDefDir) + defRoot := filepath.Clean(addonRoot + "/" + DefinitionDir) + detailFile := filepath.Clean(addonRoot + "/" + DetailFileName) resourcesFiles := make([]velaFile, 0) defFiles := make([]velaFile, 0) - addInfo := &AddonInfo{ + addInfo := &AddonGenerateInfo{ TemplatePath: filepath.Join(addonRoot, TemplateName), } // raw resources directory @@ -155,9 +166,20 @@ func getAddonInfo(addon string, addonsPath string) (*AddonInfo, error) { if err := filepath.Walk(defRoot, newWalkFn(&defFiles)); err != nil { return nil, err } - addInfo.HasDefs = true addInfo.DefinitionFiles = defFiles } + + if pathExist(detailFile) { + content, err := os.ReadFile(detailFile) + if err != nil { + return nil, errors.Wrapf(err, "read %s detail file fail", addon) + } + addInfo.DetailFile = velaFile{ + RelativePath: detailFile, + Name: filepath.Base(detailFile), + Content: string(content), + } + } return addInfo, nil } @@ -180,7 +202,7 @@ func WriteToFile(filename string, data string) error { return file.Sync() } -func generateApplication(addon *AddonInfo) (*v1beta1.Application, error) { +func generateApplication(addon *AddonGenerateInfo) (*v1beta1.Application, error) { templatePath := strings.Split(addon.TemplatePath, "/") templateName := templatePath[len(templatePath)-1] t, err := template.New(templateName).Funcs(sprig.TxtFuncMap()).ParseFiles(addon.TemplatePath) @@ -202,12 +224,12 @@ func generateApplication(addon *AddonInfo) (*v1beta1.Application, error) { return app, err } -func setConfigMapLabels(addonInfo *AddonInfo) map[string]string { +func setConfigMapLabels(addonInfo *AddonGenerateInfo) map[string]string { return map[string]string{ MarkLabel: addonInfo.StoreName, } } -func setConfigMapAnnotations(addonInfo *AddonInfo) map[string]string { +func setConfigMapAnnotations(addonInfo *AddonGenerateInfo) map[string]string { return map[string]string{ NameAnnotation: addonInfo.Name, DescAnnotation: addonInfo.Description, @@ -229,7 +251,7 @@ func removeUselessInplace(s *string) { } // storeConfigMap store configMap in helm chart -func storeConfigMap(addonInfo *AddonInfo, application *v1beta1.Application, storePath string) error { +func storeConfigMap(addonInfo *AddonGenerateInfo, application *v1beta1.Application, storePath string) error { configMap := &corev1.ConfigMap{ TypeMeta: v1.TypeMeta{ APIVersion: "v1", @@ -247,7 +269,8 @@ func storeConfigMap(addonInfo *AddonInfo, application *v1beta1.Application, stor if err != nil { return err } - data["application"] = string(initContent) + data[ApplicationKey] = string(initContent) + data[DetailKey] = addonInfo.DetailFile.Content configMap.Data = data content, err := yaml.Marshal(configMap) if err != nil { @@ -329,7 +352,7 @@ func main() { } } -func setAddonName(addInfo *AddonInfo, app *v1beta1.Application) { +func setAddonName(addInfo *AddonGenerateInfo, app *v1beta1.Application) { var name string if val, ok := app.Annotations[NameAnnotation]; ok { name = val @@ -337,5 +360,5 @@ func setAddonName(addInfo *AddonInfo, app *v1beta1.Application) { name = app.Name } addInfo.Name = name - addInfo.StoreName = cli.TransAddonName(name) + addInfo.StoreName = addonutil.TransAddonName(name) } From 9563391952cb80be681d4ac00393915c502aa292 Mon Sep 17 00:00:00 2001 From: Somefive Date: Wed, 27 Oct 2021 11:37:47 +0800 Subject: [PATCH 08/59] Fix: add local cluster & fix query parameter default value bug (#2561) --- pkg/apiserver/clients/kubeclient.go | 15 +++++- pkg/apiserver/model/cluster.go | 7 +++ pkg/apiserver/rest/apis/v1/types.go | 6 +-- pkg/apiserver/rest/usecase/cluster.go | 57 ++++++++++++++++++++--- pkg/apiserver/rest/utils/bcode/cluster.go | 6 +++ pkg/apiserver/rest/utils/params.go | 8 +++- pkg/apiserver/rest/webservice/cluster.go | 6 ++- pkg/multicluster/utils.go | 7 +-- 8 files changed, 93 insertions(+), 19 deletions(-) diff --git a/pkg/apiserver/clients/kubeclient.go b/pkg/apiserver/clients/kubeclient.go index 92c482a53..a396df633 100644 --- a/pkg/apiserver/clients/kubeclient.go +++ b/pkg/apiserver/clients/kubeclient.go @@ -17,12 +17,15 @@ limitations under the License. package clients import ( + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" "github.com/oam-dev/kubevela/pkg/multicluster" ) var kubeClient client.Client +var kubeConfig *rest.Config // SetKubeClient for test func SetKubeClient(c client.Client) { @@ -35,9 +38,19 @@ func GetKubeClient() (client.Client, error) { return kubeClient, nil } var err error - kubeClient, err = multicluster.GetMulticlusterKubernetesClient() + kubeClient, kubeConfig, err = multicluster.GetMulticlusterKubernetesClient() if err != nil { return nil, err } return kubeClient, nil } + +// GetKubeConfig create/get kube runtime config +func GetKubeConfig() (*rest.Config, error) { + var err error + if kubeConfig == nil { + kubeConfig, err = config.GetConfig() + return kubeConfig, err + } + return kubeConfig, nil +} diff --git a/pkg/apiserver/model/cluster.go b/pkg/apiserver/model/cluster.go index 1df2b2b96..34517de99 100644 --- a/pkg/apiserver/model/cluster.go +++ b/pkg/apiserver/model/cluster.go @@ -28,6 +28,13 @@ type ProviderInfo struct { Labels map[string]string `json:"labels"` } +const ( + // ClusterStatusHealthy healthy cluster + ClusterStatusHealthy = "Healthy" + // ClusterStatusUnhealthy unhealthy cluster + ClusterStatusUnhealthy = "Unhealthy" +) + // Cluster describes the model of cluster in apiserver type Cluster struct { Model diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 047611ae6..eee425f20 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -133,7 +133,7 @@ type ConnectCloudClusterRequest struct { AccessKeyID string `json:"accessKeyID"` AccessKeySecret string `json:"accessKeySecret"` ClusterID string `json:"clusterID"` - Name string `json:"name"` + Name string `json:"name" validate:"checkname"` Description string `json:"description,omitempty"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` @@ -158,10 +158,6 @@ type ClusterResourceInfo struct { type DetailClusterResponse struct { ClusterBase ResourceInfo ClusterResourceInfo `json:"resourceInfo"` - // remote manage url, eg. ACK cluster manage url. - RemoteManageURL string `json:"remoteManageURL,omitempty"` - // Dashboard URL - DashboardURL string `json:"dashboardURL,omitempty"` } // ListClusterResponse list cluster diff --git a/pkg/apiserver/rest/usecase/cluster.go b/pkg/apiserver/rest/usecase/cluster.go index bafd67e2e..31f405fa1 100644 --- a/pkg/apiserver/rest/usecase/cluster.go +++ b/pkg/apiserver/rest/usecase/cluster.go @@ -63,7 +63,11 @@ func NewClusterUsecase(ds datastore.DataStore) ClusterUsecase { if err != nil { log.Logger.Fatalf("get k8sClient failure: %s", err.Error()) } - return &clusterUsecaseImpl{ds: ds, k8sClient: k8sClient, caches: make(map[string]*utils2.MemoryCache)} + c := &clusterUsecaseImpl{ds: ds, k8sClient: k8sClient, caches: make(map[string]*utils2.MemoryCache)} + if err = c.preAddLocalCluster(context.Background()); err != nil { + log.Logger.Fatalf("preAdd local cluster failure: %s", err.Error()) + } + return c } func (c *clusterUsecaseImpl) getClusterFromDataStore(ctx context.Context, clusterName string) (*model.Cluster, error) { @@ -100,6 +104,33 @@ func (c *clusterUsecaseImpl) rollbackDetachedKubeCluster(ctx context.Context, cl } } +func (c *clusterUsecaseImpl) preAddLocalCluster(ctx context.Context) error { + cfg, err := clients.GetKubeConfig() + if err != nil { + return err + } + localCluster := &model.Cluster{ + Name: multicluster.ClusterLocalName, + Description: "The hub manage cluster where KubeVela runs on.", + Status: model.ClusterStatusHealthy, + APIServerURL: cfg.Host + cfg.APIPath, + } + if err = c.ds.Get(ctx, localCluster); err != nil { + // no local cluster in datastore + if errors.Is(err, datastore.ErrRecordNotExist) { + if err = c.ds.Add(ctx, localCluster); err != nil { + // local cluster already added in datastore + if errors.Is(err, datastore.ErrRecordExist) { + return nil + } + return err + } + } + return err + } + return nil +} + func (c *clusterUsecaseImpl) ListKubeClusters(ctx context.Context, query string, page int, pageSize int) (*apis.ListClusterResponse, error) { // TODO: Fuzzy query clusters, err := c.ds.List(ctx, &model.Cluster{}, &datastore.ListOptions{Page: page, PageSize: pageSize}) @@ -155,6 +186,9 @@ func createClusterModelFromRequest(req apis.CreateClusterRequest, oldCluster *mo func (c *clusterUsecaseImpl) createKubeCluster(ctx context.Context, req apis.CreateClusterRequest, providerCluster *cloudprovider.CloudCluster) (*apis.ClusterBase, error) { var err error cluster := createClusterModelFromRequest(req, nil) + if cluster.Name == multicluster.ClusterLocalName { + return nil, bcode.ErrLocalClusterReserved + } t := time.Now() cluster.SetCreateTime(t) cluster.SetUpdateTime(t) @@ -167,6 +201,11 @@ func (c *clusterUsecaseImpl) createKubeCluster(ctx context.Context, req apis.Cre } cluster.DashboardURL = providerCluster.DashBoardURL } + if err = c.ds.Get(ctx, cluster); err == nil { + return nil, bcode.ErrClusterAlreadyExistInDataStore + } else if !errors.Is(err, datastore.ErrRecordNotExist) { + return nil, err + } if req.KubeConfig != "" { cluster.APIServerURL, err = joinClusterByKubeConfigString(ctx, c.k8sClient, req.Name, req.KubeConfig) if err != nil { @@ -205,10 +244,8 @@ func (c *clusterUsecaseImpl) GetKubeCluster(ctx context.Context, clusterName str return nil, errors.Wrapf(err, "failed to update cluster %s status info", clusterName) } return &apis.DetailClusterResponse{ - ClusterBase: *newClusterBaseFromCluster(cluster), - ResourceInfo: resourceInfo, - RemoteManageURL: "NA", - DashboardURL: "NA", + ClusterBase: *newClusterBaseFromCluster(cluster), + ResourceInfo: resourceInfo, }, nil } @@ -224,6 +261,9 @@ func (c *clusterUsecaseImpl) ModifyKubeCluster(ctx context.Context, req apis.Cre newCluster := createClusterModelFromRequest(req, oldCluster) newCluster.SetUpdateTime(time.Now()) if oldCluster.Name != newCluster.Name || oldCluster.KubeConfig != newCluster.KubeConfig || oldCluster.KubeConfigSecret != newCluster.KubeConfigSecret { + if clusterName == multicluster.ClusterLocalName || newCluster.Name == multicluster.ClusterLocalName { + return nil, bcode.ErrLocalClusterImmutable + } if newCluster.KubeConfig == "" && newCluster.KubeConfigSecret != "" { return nil, bcode.ErrKubeConfigSecretNotSupport } @@ -277,6 +317,9 @@ func (c *clusterUsecaseImpl) ModifyKubeCluster(ctx context.Context, req apis.Cre } func (c *clusterUsecaseImpl) DeleteKubeCluster(ctx context.Context, clusterName string) (*apis.ClusterBase, error) { + if clusterName == multicluster.ClusterLocalName { + return nil, bcode.ErrLocalClusterImmutable + } cluster, err := c.getClusterFromDataStore(ctx, clusterName) if err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { @@ -300,10 +343,10 @@ func (c *clusterUsecaseImpl) DeleteKubeCluster(ctx context.Context, clusterName func (c *clusterUsecaseImpl) setClusterStatusAndResourceInfo(ctx context.Context, cluster *model.Cluster) apis.ClusterResourceInfo { resourceInfo, err := c.getClusterResourceInfoFromK8s(ctx, cluster.Name) if err != nil { - cluster.Status = "Unhealthy" + cluster.Status = model.ClusterStatusUnhealthy cluster.Reason = fmt.Sprintf("Failed to get cluster resource info: %s", err.Error()) } else { - cluster.Status = "Healthy" + cluster.Status = model.ClusterStatusHealthy cluster.Reason = "" } return resourceInfo diff --git a/pkg/apiserver/rest/utils/bcode/cluster.go b/pkg/apiserver/rest/utils/bcode/cluster.go index 463513c71..4e8a15611 100644 --- a/pkg/apiserver/rest/utils/bcode/cluster.go +++ b/pkg/apiserver/rest/utils/bcode/cluster.go @@ -36,3 +36,9 @@ var ErrGetCloudClusterFailure = NewBcode(500, 40005, "get cloud cluster informat // ErrClusterExistsInKubernetes cluster exists in kubernetes var ErrClusterExistsInKubernetes = NewBcode(400, 40006, "cluster already exists in kubernetes") + +// ErrLocalClusterReserved cluster name reserved for local +var ErrLocalClusterReserved = NewBcode(400, 40007, "local cluster is reserved") + +// ErrLocalClusterImmutable local cluster kubeConfig is immutable +var ErrLocalClusterImmutable = NewBcode(400, 40008, "local cluster is immutable") diff --git a/pkg/apiserver/rest/utils/params.go b/pkg/apiserver/rest/utils/params.go index aff95f34d..098a7aabf 100644 --- a/pkg/apiserver/rest/utils/params.go +++ b/pkg/apiserver/rest/utils/params.go @@ -24,9 +24,15 @@ import ( ) // ExtractPagingParams extract `page` and `pageSize` params from request -func ExtractPagingParams(req *restful.Request, minPageSize int, maxPageSize int) (int, int, error) { +func ExtractPagingParams(req *restful.Request, minPageSize int, maxPageSize int, defaultPageSize int) (int, int, error) { pageStr := req.QueryParameter("page") pageSizeStr := req.QueryParameter("pageSize") + if pageStr == "" { + pageStr = "0" + } + if pageSizeStr == "" { + pageSizeStr = strconv.Itoa(defaultPageSize) + } page64, err := strconv.ParseInt(pageStr, 10, 32) if err != nil { return 0, 0, errors.Errorf("invalid page %s: %v", pageStr, err) diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index 1f4f3e548..7fc9c9408 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -92,6 +92,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/cloud-clusters/{provider}").To(c.listCloudClusters). Doc("list cloud clusters"). Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). Param(ws.QueryParameter("page", "Page for paging").DataType("int").DefaultValue("0")). Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("int").DefaultValue("20")). Reads(&apis.AccessKeyRequest{}). @@ -102,6 +103,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/cloud-clusters/{provider}/connect").To(c.connectCloudCluster). Doc("create cluster from cloud cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). Reads(&apis.ConnectCloudClusterRequest{}). Returns(200, "", apis.ClusterBase{}). Returns(400, "", bcode.Bcode{}). @@ -112,7 +114,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { func (c *ClusterWebService) listKubeClusters(req *restful.Request, res *restful.Response) { query := req.QueryParameter("query") - page, pageSize, err := utils.ExtractPagingParams(req, 5, 100) + page, pageSize, err := utils.ExtractPagingParams(req, 5, 100, 20) if err != nil { bcode.ReturnError(req, res, err) return @@ -220,7 +222,7 @@ func (c *ClusterWebService) deleteKubeCluster(req *restful.Request, res *restful func (c *ClusterWebService) listCloudClusters(req *restful.Request, res *restful.Response) { provider := req.PathParameter("provider") - page, pageSize, err := utils.ExtractPagingParams(req, 5, 100) + page, pageSize, err := utils.ExtractPagingParams(req, 5, 100, 20) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/multicluster/utils.go b/pkg/multicluster/utils.go index 9f8182bdd..4cff7f6ff 100644 --- a/pkg/multicluster/utils.go +++ b/pkg/multicluster/utils.go @@ -162,10 +162,11 @@ func UpgradeExistingClusterSecret(ctx context.Context, c client.Client) error { } // GetMulticlusterKubernetesClient get client with multicluster function enabled -func GetMulticlusterKubernetesClient() (client.Client, error) { +func GetMulticlusterKubernetesClient() (client.Client, *rest.Config, error) { k8sConfig, err := config.GetConfig() if err != nil { - return nil, err + return nil, nil, err } - return Initialize(k8sConfig, false) + k8sClient, err := Initialize(k8sConfig, false) + return k8sClient, k8sConfig, err } From edf42074b932ff41dfd7bc83711952e94bc4db74 Mon Sep 17 00:00:00 2001 From: yangsoon Date: Thu, 28 Oct 2021 12:05:12 +0800 Subject: [PATCH 09/59] Feat: add velaql webservice (#2532) --- Makefile | 2 +- apis/core.oam.dev/common/types.go | 4 +- pkg/apiserver/clients/kubeclient.go | 30 ++++ pkg/apiserver/rest/apis/v1/types.go | 3 + pkg/apiserver/rest/usecase/velaql.go | 85 ++++++++++++ pkg/apiserver/rest/utils/bcode/velaql.go | 26 ++++ pkg/apiserver/rest/webservice/velaql.go | 73 ++++++++++ pkg/apiserver/rest/webservice/webservice.go | 2 + pkg/velaql/context.go | 96 +++++++++++++ pkg/velaql/parse.go | 146 ++++++++++++++++++++ pkg/velaql/parse_test.go | 129 +++++++++++++++++ pkg/velaql/suit_test.go | 105 ++++++++++++++ pkg/velaql/testdata/apply-object.yaml | 33 +++++ pkg/velaql/testdata/example-pod.yaml | 58 ++++++++ pkg/velaql/testdata/read-object.yaml | 61 ++++++++ pkg/velaql/view.go | 133 ++++++++++++++++++ pkg/velaql/view_test.go | 95 +++++++++++++ test/e2e-apiserver-test/velaql_test.go | 84 +++++++++++ 18 files changed, 1161 insertions(+), 4 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/velaql.go create mode 100644 pkg/apiserver/rest/utils/bcode/velaql.go create mode 100644 pkg/apiserver/rest/webservice/velaql.go create mode 100644 pkg/velaql/context.go create mode 100644 pkg/velaql/parse.go create mode 100644 pkg/velaql/parse_test.go create mode 100644 pkg/velaql/suit_test.go create mode 100644 pkg/velaql/testdata/apply-object.yaml create mode 100644 pkg/velaql/testdata/example-pod.yaml create mode 100644 pkg/velaql/testdata/read-object.yaml create mode 100644 pkg/velaql/view.go create mode 100644 pkg/velaql/view_test.go create mode 100644 test/e2e-apiserver-test/velaql_test.go diff --git a/Makefile b/Makefile index affbfddc6..982c26d98 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ unit-test-core: go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... | grep -v apiserver) go test $(shell go list ./references/... | grep -v apiserver) unit-test-apiserver: - go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... | grep apiserver) + go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... | grep -E 'apiserver|velaql') # Build vela cli binary build: fmt vet lint staticcheck vela-cli kubectl-vela diff --git a/apis/core.oam.dev/common/types.go b/apis/core.oam.dev/common/types.go index f64a2669d..9a5e6367a 100644 --- a/apis/core.oam.dev/common/types.go +++ b/apis/core.oam.dev/common/types.go @@ -19,14 +19,12 @@ package common import ( "encoding/json" + types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime" - "github.com/oam-dev/kubevela/apis/core.oam.dev/condition" - "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1" ) diff --git a/pkg/apiserver/clients/kubeclient.go b/pkg/apiserver/clients/kubeclient.go index a396df633..0f880b7b3 100644 --- a/pkg/apiserver/clients/kubeclient.go +++ b/pkg/apiserver/clients/kubeclient.go @@ -21,7 +21,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" + "github.com/oam-dev/kubevela/pkg/cue/packages" "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" ) var kubeClient client.Client @@ -54,3 +56,31 @@ func GetKubeConfig() (*rest.Config, error) { } return kubeConfig, nil } + +// GetDiscoverMapper get discover mapper +func GetDiscoverMapper() (discoverymapper.DiscoveryMapper, error) { + conf, err := GetKubeConfig() + if err != nil { + return nil, err + } + dm, err := discoverymapper.New(conf) + if err != nil { + return nil, err + } + return dm, nil +} + +// GetPackageDiscover get package discover +func GetPackageDiscover() (*packages.PackageDiscover, error) { + conf, err := GetKubeConfig() + if err != nil { + return nil, err + } + pd, err := packages.NewPackageDiscover(conf) + if err != nil { + if !packages.IsCUEParseErr(err) { + return nil, err + } + } + return pd, nil +} diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index eee425f20..c202e903e 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -509,3 +509,6 @@ type ApplicationDeployResponse struct { // SourceType the event trigger source, Web or API SourceType string `json:"sourceType"` } + +// VelaQLViewResponse query response +type VelaQLViewResponse map[string]interface{} diff --git a/pkg/apiserver/rest/usecase/velaql.go b/pkg/apiserver/rest/usecase/velaql.go new file mode 100644 index 000000000..065469ced --- /dev/null +++ b/pkg/apiserver/rest/usecase/velaql.go @@ -0,0 +1,85 @@ +/* + 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" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/velaql" +) + +// VelaQLUsecase velaQL usecase +type VelaQLUsecase interface { + QueryView(context.Context, string) (*apis.VelaQLViewResponse, error) +} + +type velaQLUsecaseImpl struct { + kubeClient client.Client + dm discoverymapper.DiscoveryMapper + pd *packages.PackageDiscover +} + +// NewVelaQLUsecase new velaQL usecase +func NewVelaQLUsecase() VelaQLUsecase { + k8sClient, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + + dm, err := clients.GetDiscoverMapper() + if err != nil { + log.Logger.Fatalf("get discover mapper failure %s", err.Error()) + } + + pd, err := clients.GetPackageDiscover() + if err != nil { + log.Logger.Fatalf("get package discover failure %s", err.Error()) + } + return &velaQLUsecaseImpl{ + kubeClient: k8sClient, + dm: dm, + pd: pd, + } +} + +// QueryView get the view query results +func (v *velaQLUsecaseImpl) QueryView(ctx context.Context, velaQL string) (*apis.VelaQLViewResponse, error) { + query, err := velaql.ParseVelaQL(velaQL) + if err != nil { + return nil, bcode.ErrParseVelaQL + } + + queryValue, err := velaql.NewViewHandler(v.kubeClient, v.dm, v.pd).QueryView(ctx, query) + if err != nil { + return nil, bcode.ErrViewQuery + } + + resp := apis.VelaQLViewResponse{} + err = queryValue.UnmarshalTo(&resp) + if err != nil { + return nil, bcode.ErrParseQuery2Json + } + return &resp, err +} diff --git a/pkg/apiserver/rest/utils/bcode/velaql.go b/pkg/apiserver/rest/utils/bcode/velaql.go new file mode 100644 index 000000000..89c77e025 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/velaql.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 + +// ErrParseVelaQL failed to parse velaQL +var ErrParseVelaQL = NewBcode(400, 60001, "fail to parse the velaQL") + +// ErrViewQuery failed to query view +var ErrViewQuery = NewBcode(400, 60002, "view query failed") + +// ErrParseQuery2Json failed to parse query result to response +var ErrParseQuery2Json = NewBcode(400, 60003, "fail to parse query result to json format") diff --git a/pkg/apiserver/rest/webservice/velaql.go b/pkg/apiserver/rest/webservice/velaql.go new file mode 100644 index 000000000..533675cdb --- /dev/null +++ b/pkg/apiserver/rest/webservice/velaql.go @@ -0,0 +1,73 @@ +/* + 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" + "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 velaQLWebService struct { + velaQLUsecase usecase.VelaQLUsecase +} + +// NewVelaQLWebService new velaQL webservice +func NewVelaQLWebService(velaQLUsecase usecase.VelaQLUsecase) WebService { + return &velaQLWebService{ + velaQLUsecase: velaQLUsecase, + } +} + +func (v *velaQLWebService) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/query"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for velaQL") + + tags := []string{"velaQL"} + + ws.Route(ws.GET("/").To(v.queryView). + Doc("use velaQL to query resource status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.QueryParameter("velaql", "velaql query statement").DataType("string")). + Returns(200, "", apis.VelaQLViewResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.VelaQLViewResponse{})) + + return ws +} + +func (v *velaQLWebService) queryView(req *restful.Request, res *restful.Response) { + velaQL := req.QueryParameter("velaql") + + qlResp, err := v.velaQLUsecase.QueryView(req.Request.Context(), velaQL) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err = res.WriteEntity(qlResp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index a7e92227f..3effd3eaa 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -64,6 +64,7 @@ func Init(ctx context.Context, ds datastore.DataStore) { applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase) namespaceUsecase := usecase.NewNamespaceUsecase() oamApplicationUsecase := usecase.NewOAMApplicationUsecase() + velaQLUsecase := usecase.NewVelaQLUsecase() definitionUsecase := usecase.NewDefinitionUsecase() addonUsecase := usecase.NewAddonUsecase(ds) RegistWebService(NewClusterWebService(clusterUsecase)) @@ -75,4 +76,5 @@ func Init(ctx context.Context, ds datastore.DataStore) { RegistWebService(NewOAMApplication(oamApplicationUsecase)) RegistWebService(&policyDefinitionWebservice{}) RegistWebService(NewWorkflowWebService(workflowUsecase, applicationUsecase)) + RegistWebService(NewVelaQLWebService(velaQLUsecase)) } diff --git a/pkg/velaql/context.go b/pkg/velaql/context.go new file mode 100644 index 000000000..38d3273f5 --- /dev/null +++ b/pkg/velaql/context.go @@ -0,0 +1,96 @@ +/* + 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 velaql + +import ( + "encoding/json" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + + "github.com/oam-dev/kubevela/pkg/cue/model/value" + wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" +) + +// NewViewContext new view context +func NewViewContext() (wfContext.Context, error) { + viewContext := &ViewContext{} + var err error + viewContext.vars, err = value.NewValue("", nil, "") + return viewContext, err +} + +// ViewContext is view context +type ViewContext struct { + vars *value.Value +} + +// GetComponent Get ComponentManifest from workflow context. +func (c ViewContext) GetComponent(name string) (*wfContext.ComponentManifest, error) { + return nil, errors.New("not support func GetComponent") +} + +// GetComponents Get All ComponentManifest from workflow context. +func (c ViewContext) GetComponents() map[string]*wfContext.ComponentManifest { + return nil +} + +// PatchComponent patch component with value. +func (c ViewContext) PatchComponent(name string, patchValue *value.Value) error { + return errors.New("not support func PatchComponent") +} + +// GetVar get variable from workflow context. +func (c ViewContext) GetVar(paths ...string) (*value.Value, error) { + return c.vars.LookupValue(paths...) +} + +// SetVar set variable to workflow context. +func (c ViewContext) SetVar(v *value.Value, paths ...string) error { + str, err := v.String() + if err != nil { + return errors.WithMessage(err, "compile var") + } + if err := c.vars.FillRaw(str, paths...); err != nil { + return err + } + return c.vars.Error() +} + +// Commit the workflow context and persist it's content. +func (c ViewContext) Commit() error { + return errors.New("not support func Commit") +} + +// MakeParameter make 'value' with interface{} +func (c ViewContext) MakeParameter(parameter interface{}) (*value.Value, error) { + var s = "{}" + if parameter != nil { + bt, err := json.Marshal(parameter) + if err != nil { + return nil, err + } + s = string(bt) + } + + return c.vars.MakeValue(s) +} + +// StoreRef return the store reference of workflow context. +func (c ViewContext) StoreRef() *corev1.ObjectReference { + return nil +} diff --git a/pkg/velaql/parse.go b/pkg/velaql/parse.go new file mode 100644 index 000000000..2b2b3d8e1 --- /dev/null +++ b/pkg/velaql/parse.go @@ -0,0 +1,146 @@ +/* + 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 velaql + +import ( + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// Query contains query data +type Query struct { + View string + Parameter map[string]interface{} + Export string +} + +const ( + // PatternQL is the pattern string of velaQL, velaQL's query syntax is `ViewName{key1=value1 ,key2="value2",}.Export` + PatternQL = `(?P[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?)(?P{.*?})?\.?(?P[_a-zA-Z][\._a-zA-Z0-9]*)?` + // PatternKV is the pattern string of parameter + PatternKV = `(?P[^=]+)=(?P[^=]*?)(?:,|$)` + // KeyWordView represent view keyword + KeyWordView = "view" + // KeyWordParameter represent parameter keyword + KeyWordParameter = "parameter" + // KeyWordExport represent export keyword + KeyWordExport = "export" + // DefaultExportValue is the default Export value + DefaultExportValue = "status" +) + +var ( + qlRegexp *regexp.Regexp + kvRegexp *regexp.Regexp +) + +func init() { + qlRegexp = regexp.MustCompile(PatternQL) + kvRegexp = regexp.MustCompile(PatternKV) +} + +// ParseVelaQL parse velaQL to Query +func ParseVelaQL(ql string) (Query, error) { + query := Query{ + Export: DefaultExportValue, + } + + groupNames := qlRegexp.SubexpNames() + matched := qlRegexp.FindStringSubmatch(ql) + if len(matched) != len(groupNames) || (len(matched) != 0 && matched[0] != ql) { + return query, errors.New("fail to parse the velaQL") + } + + result := make(map[string]string, len(groupNames)) + for i, name := range groupNames { + if i != 0 && name != "" { + result[name] = strings.TrimSpace(matched[i]) + } + } + + if len(result["view"]) == 0 { + return query, errors.New("view name shouldn't be empty") + } + + query.View = result[KeyWordView] + if len(result[KeyWordExport]) != 0 { + query.Export = result[KeyWordExport] + } + var err error + query.Parameter, err = ParseParameter(result[KeyWordParameter]) + if err != nil { + return query, err + } + return query, nil +} + +// ParseParameter parse parameter to map[string]interface{} +func ParseParameter(parameter string) (map[string]interface{}, error) { + parameter = strings.TrimLeft(parameter, "{") + parameter = strings.TrimRight(parameter, "}") + parameter = strings.TrimSpace(parameter) + + if len(parameter) == 0 { + return nil, errors.New("parameter shouldn't be empty") + } + + groupNames := kvRegexp.SubexpNames() + matchKVs := kvRegexp.FindAllStringSubmatch(parameter, -1) + + result := make(map[string]interface{}, len(matchKVs)) + for _, kv := range matchKVs { + kvMap := make(map[string]string, 2) + if len(kv) != len(groupNames) { + return nil, errors.New("failed to parse the parameter") + } + + for i, name := range groupNames { + if i != 0 && name != "" { + kvMap[name] = strings.TrimSpace(kv[i]) + } + } + + if len(kvMap["key"]) == 0 || len(kvMap["value"]) == 0 { + return nil, errors.New("key or value in parameter shouldn't be empty") + } + result[kvMap["key"]] = string2OtherType(kvMap["value"]) + } + + return result, nil +} + +// string2OtherType convert string to other type +func string2OtherType(s string) interface{} { + i, err := strconv.ParseInt(s, 10, 64) + if err == nil { + return i + } + + b, err := strconv.ParseBool(s) + if err == nil { + return b + } + + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + return strings.Trim(s, "\"") +} diff --git a/pkg/velaql/parse_test.go b/pkg/velaql/parse_test.go new file mode 100644 index 000000000..2088a8a5a --- /dev/null +++ b/pkg/velaql/parse_test.go @@ -0,0 +1,129 @@ +/* + 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 velaql + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestParseVelaQL(t *testing.T) { + testcases := []struct { + ql string + query Query + err error + }{{ + ql: `view{test=,test1=hello}.output`, + err: errors.New("key or value in parameter shouldn't be empty"), + }, { + ql: `{test=1,app="name"}.Export`, + err: errors.New("fail to parse the velaQL"), + }, { + ql: `view.{test=true}.output.value.spec"`, + err: errors.New("fail to parse the velaQL"), + }, { + ql: `view{test=1,app="name"}`, + query: Query{ + View: "view", + Export: "status", + }, + err: nil, + }, { + ql: `view{test=1,app="name"}.Export`, + query: Query{ + View: "view", + Export: "Export", + }, + err: nil, + }, { + ql: `view{test=true}.output.value.spec`, + query: Query{ + View: "view", + Export: "output.value.spec", + }, + err: nil, + }} + + for _, testcase := range testcases { + q, err := ParseVelaQL(testcase.ql) + assert.Equal(t, testcase.err != nil, err != nil) + if err == nil { + assert.Equal(t, testcase.query.View, q.View) + assert.Equal(t, testcase.query.Export, q.Export) + } else { + assert.Equal(t, testcase.err.Error(), err.Error()) + } + } +} + +func TestParseParameter(t *testing.T) { + testcases := []struct { + parameter string + parameterMap map[string]interface{} + err error + }{{ + parameter: `{ }`, + err: errors.New("parameter shouldn't be empty"), + }, { + parameter: `{}`, + err: errors.New("parameter shouldn't be empty"), + }, { + parameter: `{ testString = "pod" , testFloat= , testBoolean=true}`, + err: errors.New("key or value in parameter shouldn't be empty"), + }, { + parameter: `{testString="pod",testFloat=1000.10,testBoolean=true,testInt=1}`, + parameterMap: map[string]interface{}{ + "testString": "pod", + "testFloat": 1000.1, + "testBoolean": true, + "testInt": int64(1), + }, + err: nil, + }, { + parameter: `{testString="pod",testFloat=1000.10,testBoolean=true,testInt=1,}`, + parameterMap: map[string]interface{}{ + "testString": "pod", + "testFloat": 1000.1, + "testBoolean": true, + "testInt": int64(1), + }, + err: nil, + }, { + parameter: `{ testString = "pod" , testFloat=1000.10 , testBoolean=true , testInt=1, }`, + parameterMap: map[string]interface{}{ + "testString": "pod", + "testFloat": 1000.1, + "testBoolean": true, + "testInt": int64(1), + }, + err: nil, + }} + + for _, testcase := range testcases { + result, err := ParseParameter(testcase.parameter) + assert.Equal(t, testcase.err != nil, err != nil) + if err == nil { + for k, v := range result { + assert.Equal(t, testcase.parameterMap[k], v) + } + } else { + assert.Equal(t, testcase.err.Error(), err.Error()) + } + } +} diff --git a/pkg/velaql/suit_test.go b/pkg/velaql/suit_test.go new file mode 100644 index 000000000..e1cdb20b3 --- /dev/null +++ b/pkg/velaql/suit_test.go @@ -0,0 +1,105 @@ +/* + 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 velaql + +import ( + "context" + "math/rand" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var viewHandler *ViewHandler +var pod corev1.Pod +var readView v1beta1.WorkflowStepDefinition +var applyView v1beta1.WorkflowStepDefinition + +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), + CRDDirectoryPaths: []string{"../../charts/vela-core/crds"}, + } + + By("start kube test env") + var err error + cfg, err = testEnv.Start() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + By("new kube client") + cfg.Timeout = time.Minute * 2 + k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) + By("new kube client success") + clients.SetKubeClient(k8sClient) + + dm, err := discoverymapper.New(cfg) + Expect(err).To(BeNil()) + pd, err := packages.NewPackageDiscover(cfg) + Expect(err).To(BeNil()) + + viewHandler = NewViewHandler(k8sClient, dm, pd) + ctx := context.Background() + + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}} + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + + Expect(common.ReadYamlToObject("./testdata/example-pod.yaml", &pod)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &pod)).Should(BeNil()) + + Expect(common.ReadYamlToObject("./testdata/read-object.yaml", &readView)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &readView)).Should(BeNil()) + + Expect(common.ReadYamlToObject("./testdata/apply-object.yaml", &applyView)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &applyView)).Should(BeNil()) + close(done) +}, 240) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +func TestVelaQL(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VelaQL Suite") +} diff --git a/pkg/velaql/testdata/apply-object.yaml b/pkg/velaql/testdata/apply-object.yaml new file mode 100644 index 000000000..9e5b24f82 --- /dev/null +++ b/pkg/velaql/testdata/apply-object.yaml @@ -0,0 +1,33 @@ +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Apply raw kubernetes objects for your workflow steps + name: apply-object + namespace: vela-system +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + apply: op.#Apply & { + value: { + apiVersion: parameter.apiVersion + kind: parameter.kind + metadata: { + name: parameter.name + } + } + } + + objStatus: { + apply.value + } + parameter: { + apiVersion: string + kind: string + name: string + } \ No newline at end of file diff --git a/pkg/velaql/testdata/example-pod.yaml b/pkg/velaql/testdata/example-pod.yaml new file mode 100644 index 000000000..b858d4b87 --- /dev/null +++ b/pkg/velaql/testdata/example-pod.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: Pod +metadata: + name: hello-world-server-55559d5dd9-t8wt2 + namespace: default +spec: + containers: + - image: crccheck/hello-world + imagePullPolicy: Always + name: hello-world-server + ports: + - containerPort: 8000 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-9wklg + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + nodeName: kind-control-plane + preemptionPolicy: PreemptLowerPriority + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccountName: default + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - name: kube-api-access-9wklg + projected: + defaultMode: 420 + sources: + - serviceAccountToken: + expirationSeconds: 3607 + path: token + - configMap: + items: + - key: ca.crt + path: ca.crt + name: kube-root-ca.crt + - downwardAPI: + items: + - fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + path: namespace \ No newline at end of file diff --git a/pkg/velaql/testdata/read-object.yaml b/pkg/velaql/testdata/read-object.yaml new file mode 100644 index 000000000..d701b63c9 --- /dev/null +++ b/pkg/velaql/testdata/read-object.yaml @@ -0,0 +1,61 @@ +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Read objects for your workflow steps + name: read-object + namespace: vela-system +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + output: { + if parameter.apiVersion == _|_ && parameter.kind == _|_ { + op.#Read & { + value: { + apiVersion: "core.oam.dev/v1beta1" + kind: "Application" + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } + } + if parameter.apiVersion != _|_ || parameter.kind != _|_ { + op.#Read & { + value: { + apiVersion: parameter.apiVersion + kind: parameter.kind + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } + } + } + + objStatus: { + output.value.status + } + + parameter: { + // +usage=Specify the apiVersion of the object, defaults to core.oam.dev/v1beta1 + apiVersion?: string + // +usage=Specify the kind of the object, defaults to Application + kind?: string + // +usage=Specify the name of the object + name: string + // +usage=Specify the namespace of the object + namespace?: string + } + diff --git a/pkg/velaql/view.go b/pkg/velaql/view.go new file mode 100644 index 000000000..bb3597877 --- /dev/null +++ b/pkg/velaql/view.go @@ -0,0 +1,133 @@ +/* + 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 velaql + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + oamutil "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils" + "github.com/oam-dev/kubevela/pkg/utils/apply" + "github.com/oam-dev/kubevela/pkg/workflow/providers" + "github.com/oam-dev/kubevela/pkg/workflow/providers/kube" + "github.com/oam-dev/kubevela/pkg/workflow/tasks" + wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" +) + +const qlNs = "vela-system" + +// ViewHandler view handler +type ViewHandler struct { + cli client.Client + workflowStep v1beta1.WorkflowStep + dm discoverymapper.DiscoveryMapper + pd *packages.PackageDiscover + namespace string +} + +// NewViewHandler new view handler +func NewViewHandler(cli client.Client, dm discoverymapper.DiscoveryMapper, pd *packages.PackageDiscover) *ViewHandler { + return &ViewHandler{ + cli: cli, + dm: dm, + pd: pd, + namespace: qlNs, + } +} + +// QueryView generate view step +func (v *ViewHandler) QueryView(ctx context.Context, query Query) (*value.Value, error) { + outputsTemplate := fmt.Sprintf(OutputsTemplate, query.Export, query.Export) + queryKey := QueryParameterKey{} + if err := json.Unmarshal([]byte(outputsTemplate), &queryKey); err != nil { + return nil, err + } + + v.workflowStep = v1beta1.WorkflowStep{ + Name: fmt.Sprintf("%s-%s", query.View, query.Export), + Type: query.View, + Properties: oamutil.Object2RawExtension(query.Parameter), + Outputs: queryKey.Outputs, + } + + ctx = oamutil.SetNamespaceInCtx(ctx, v.namespace) + handlerProviders := providers.NewProviders() + kube.Install(handlerProviders, v.cli, v.dispatch) + taskDiscover := tasks.NewTaskDiscover(handlerProviders, v.pd, v.cli, v.dm) + genTask, err := taskDiscover.GetTaskGenerator(ctx, v.workflowStep.Type) + if err != nil { + return nil, err + } + + runner, err := genTask(v.workflowStep, &wfTypes.GeneratorOptions{ + ID: utils.RandomString(10), + }) + if err != nil { + return nil, err + } + + viewCtx, err := NewViewContext() + if err != nil { + return nil, err + } + status, _, err := runner.Run(viewCtx, &wfTypes.TaskRunOptions{}) + if err != nil { + return nil, err + } + if status.Phase != common.WorkflowStepPhaseSucceeded { + return nil, errors.Errorf("failed to query the view %s", status.Message) + } + return viewCtx.GetVar(query.Export) +} + +func (v *ViewHandler) dispatch(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifests ...*unstructured.Unstructured) error { + applicator := apply.NewAPIApplicator(v.cli) + for _, manifest := range manifests { + if err := applicator.Apply(ctx, manifest); err != nil { + return err + } + } + return nil +} + +// QueryParameterKey query parameter key +type QueryParameterKey struct { + Outputs common.StepOutputs `json:"outputs"` +} + +// OutputsTemplate output template +var OutputsTemplate = ` +{ + "outputs": [ + { + "valueFrom": "%s", + "name": "%s" + } + ] +} +` diff --git a/pkg/velaql/view_test.go b/pkg/velaql/view_test.go new file mode 100644 index 000000000..d72d98448 --- /dev/null +++ b/pkg/velaql/view_test.go @@ -0,0 +1,95 @@ +/* + 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 velaql + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("Test VelaQL View", func() { + var ctx = context.Background() + + It("Test query a sample view", func() { + parameter := map[string]string{ + "apiVersion": "v1", + "kind": "Pod", + "name": pod.Name, + } + + velaQL := fmt.Sprintf("%s{%s}.%s", readView.Name, Map2URLParameter(parameter), "objStatus") + query, err := ParseVelaQL(velaQL) + Expect(err).ShouldNot(HaveOccurred()) + + queryValue, err := viewHandler.QueryView(context.Background(), query) + Expect(err).Should(BeNil()) + + podStatus := corev1.PodStatus{} + Expect(queryValue.UnmarshalTo(&podStatus)).Should(BeNil()) + }) + + It("Test query view with wrong request", func() { + parameter := map[string]string{ + "apiVersion": "v1", + "kind": "Pod", + "name": pod.Name, + } + + By("query view with an non-existent result") + velaQL := fmt.Sprintf("%s{%s}.%s", readView.Name, Map2URLParameter(parameter), "appStatus") + query, err := ParseVelaQL(velaQL) + Expect(err).ShouldNot(HaveOccurred()) + _, err = viewHandler.QueryView(context.Background(), query) + Expect(err).Should(HaveOccurred()) + + By("query an non-existent view") + velaQL = fmt.Sprintf("%s{%s}.%s", "view-resource", Map2URLParameter(parameter), "objStatus") + query, err = ParseVelaQL(velaQL) + Expect(err).ShouldNot(HaveOccurred()) + _, err = viewHandler.QueryView(context.Background(), query) + Expect(err).Should(HaveOccurred()) + }) + + It("Test apply resource in view", func() { + parameter := map[string]string{ + "apiVersion": "v1", + "kind": "Namespace", + "name": "test-namespace", + } + velaQL := fmt.Sprintf("%s{%s}.%s", applyView.Name, Map2URLParameter(parameter), "objStatus") + query, err := ParseVelaQL(velaQL) + Expect(err).ShouldNot(HaveOccurred()) + _, err = viewHandler.QueryView(context.Background(), query) + Expect(err).ShouldNot(HaveOccurred()) + + ns := corev1.Namespace{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "test-namespace"}, &ns)).Should(BeNil()) + }) +}) + +func Map2URLParameter(parameter map[string]string) string { + var res string + for k, v := range parameter { + res += fmt.Sprintf("%s=\"%s\",", k, v) + } + return res +} diff --git a/test/e2e-apiserver-test/velaql_test.go b/test/e2e-apiserver-test/velaql_test.go new file mode 100644 index 000000000..fd53cdca6 --- /dev/null +++ b/test/e2e-apiserver-test/velaql_test.go @@ -0,0 +1,84 @@ +/* + 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" + "context" + "encoding/json" + "fmt" + "net/http" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + apiv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var _ = Describe("Test velaQL rest api", func() { + namespace := "test-velaql" + appName := "example-app" + var app v1beta1.Application + + It("Test query application status via view", func() { + Expect(common.ReadYamlToObject("./testdata/example-app.yaml", &app)).Should(BeNil()) + req := apiv1.ApplicationRequest{ + Components: app.Spec.Components, + Policies: app.Spec.Policies, + Workflow: app.Spec.Workflow, + } + bodyByte, err := json.Marshal(req) + Expect(err).Should(BeNil()) + res, err := http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + "application/json", + bytes.NewBuffer(bodyByte), + ) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(res.StatusCode).Should(Equal(200)) + + Expect(err).Should(BeNil()) + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s}.%s", "read-object", appName, namespace, "output.value.spec"), + ) + Expect(err).Should(BeNil()) + Expect(queryRes.StatusCode).Should(Equal(200)) + + defer queryRes.Body.Close() + var appSpec v1beta1.ApplicationSpec + err = json.NewDecoder(queryRes.Body).Decode(&appSpec) + Expect(err).ShouldNot(HaveOccurred()) + + var existApp v1beta1.Application + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, &existApp)).Should(BeNil()) + + Expect(len(appSpec.Components)).Should(Equal(len(existApp.Spec.Components))) + Expect(len(appSpec.Workflow.Steps)).Should(Equal(len(existApp.Spec.Workflow.Steps))) + }) + + It("Test query application status with wrong velaQL", func() { + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{err=,name=%s,namespace=%s}.%s", "read-object", appName, namespace, "output.value.spec"), + ) + Expect(err).Should(BeNil()) + Expect(queryRes.StatusCode).Should(Equal(400)) + }) +}) From 6666c3a2bb7e926f18af50d7c55508c73a7df512 Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Fri, 29 Oct 2021 10:39:50 +0800 Subject: [PATCH 10/59] Feat: add workflow record api (#2567) * Feat: add workflow record api * resolve comments * add count function --- pkg/apiserver/datastore/datastore.go | 5 +- pkg/apiserver/datastore/kubeapi/kubeapi.go | 33 ++++++ .../datastore/kubeapi/kubeapi_test.go | 16 ++- pkg/apiserver/datastore/mongodb/mongodb.go | 22 ++++ .../datastore/mongodb/mongodb_test.go | 12 ++ pkg/apiserver/model/workflow.go | 38 ++++++ pkg/apiserver/rest/apis/v1/types.go | 16 +++ pkg/apiserver/rest/usecase/application.go | 2 +- pkg/apiserver/rest/usecase/workflow.go | 99 ++++++++++++++++ pkg/apiserver/rest/usecase/workflow_test.go | 108 +++++++++++++++++- pkg/apiserver/rest/utils/params.go | 6 +- pkg/apiserver/rest/webservice/cluster.go | 4 +- pkg/apiserver/rest/webservice/validate.go | 5 + pkg/apiserver/rest/webservice/workflow.go | 42 ++++++- 14 files changed, 398 insertions(+), 10 deletions(-) diff --git a/pkg/apiserver/datastore/datastore.go b/pkg/apiserver/datastore/datastore.go index 465b04781..2dc550191 100644 --- a/pkg/apiserver/datastore/datastore.go +++ b/pkg/apiserver/datastore/datastore.go @@ -109,9 +109,12 @@ type DataStore interface { // Get entity from database, Name() and TableName() can't return zero value. Get(ctx context.Context, entity Entity) error - // TableName() can't return zero value. + // List entities from database, TableName() can't return zero value. List(ctx context.Context, query Entity, options *ListOptions) ([]Entity, error) + // Count entities from database, TableName() can't return zero value. + Count(ctx context.Context, entity Entity) (int64, error) + // IsExist Name() and TableName() can't return zero value. IsExist(ctx context.Context, entity Entity) (bool, error) } diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi.go b/pkg/apiserver/datastore/kubeapi/kubeapi.go index d720b3a21..08b75f422 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi.go @@ -282,3 +282,36 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto } return list, nil } + +// Count counts entities +func (m *kubeapi) Count(ctx context.Context, entity datastore.Entity) (int64, error) { + if entity.TableName() == "" { + return 0, datastore.ErrTableNameEmpty + } + + selector, err := labels.Parse(fmt.Sprintf("table=%s", entity.TableName())) + if err != nil { + return 0, datastore.NewDBError(err) + } + for k, v := range entity.Index() { + rq, err := labels.NewRequirement(k, selection.Equals, []string{v}) + if err != nil { + return 0, datastore.ErrIndexInvalid + } + selector = selector.Add(*rq) + } + options := &client.ListOptions{ + LabelSelector: selector, + Namespace: m.namespace, + } + + var configMaps corev1.ConfigMapList + if err := m.kubeclient.List(ctx, &configMaps, options); err != nil { + if apierrors.IsNotFound(err) { + return 0, nil + } + return 0, datastore.NewDBError(err) + } + + return int64(len(configMaps.Items)), nil +} diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go index c4840db32..bdb52f62f 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go @@ -136,7 +136,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { } Expect(cmp.Diff(selector.String(), "namespace=test,table=vela_application")).Should(BeEmpty()) }) - It("Test list funtion", func() { + It("Test list function", func() { var app model.Application list, err := kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) Expect(err).ShouldNot(HaveOccurred()) @@ -165,7 +165,19 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test isExist funtion", func() { + It("Test count function", func() { + var app model.Application + count, err := kubeStore.Count(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(4))) + + app.Namespace = "test-namespace" + count, err = kubeStore.Count(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(1))) + }) + + It("Test isExist function", func() { var app model.Application app.Name = "kubevela-app-3" exist, err := kubeStore.IsExist(context.TODO(), &app) diff --git a/pkg/apiserver/datastore/mongodb/mongodb.go b/pkg/apiserver/datastore/mongodb/mongodb.go index ddbdac199..256ea9907 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb.go +++ b/pkg/apiserver/datastore/mongodb/mongodb.go @@ -229,6 +229,28 @@ func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datasto return list, nil } +// Count counts entities +func (m *mongodb) Count(ctx context.Context, entity datastore.Entity) (int64, error) { + if entity.TableName() == "" { + return 0, datastore.ErrTableNameEmpty + } + collection := m.client.Database(m.database).Collection(entity.TableName()) + filter := bson.D{} + if entity.Index() != nil { + for k, v := range entity.Index() { + filter = append(filter, bson.E{ + Key: k, + Value: v, + }) + } + } + count, err := collection.CountDocuments(ctx, filter) + if err != nil { + return 0, datastore.NewDBError(err) + } + return count, nil +} + func makeNameFilter(name string) bson.D { return bson.D{{Key: "name", Value: name}} } diff --git a/pkg/apiserver/datastore/mongodb/mongodb_test.go b/pkg/apiserver/datastore/mongodb/mongodb_test.go index 82a326079..3e947d028 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb_test.go +++ b/pkg/apiserver/datastore/mongodb/mongodb_test.go @@ -119,6 +119,18 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(diff).Should(BeEmpty()) }) + It("Test count function", func() { + var app model.Application + count, err := mongodbDriver.Count(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(4))) + + app.Namespace = "test-namespace" + count, err = mongodbDriver.Count(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(1))) + }) + It("Test isExist funtion", func() { var app model.Application app.Name = "kubevela-app-3" diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index 356dfb308..eed20cca7 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -18,12 +18,14 @@ package model import ( "strconv" + "time" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" ) func init() { RegistModel(&Workflow{}) + RegistModel(&WorkflowRecord{}) } // Workflow application delivery plan database model @@ -74,3 +76,39 @@ func (w *Workflow) Index() map[string]string { index["enable"] = strconv.FormatBool(w.Enable) return index } + +// WorkflowRecord is the workflow record database model +type WorkflowRecord struct { + Model + WorkflowPrimaryKey string `json:"workflowPrimaryKey"` + AppPrimaryKey string `json:"appPrimaryKey"` + // name is `appName-version`, which is the same as the primary key of deploy event + Name string `json:"name"` + Namespace string `json:"namespace"` + StartTime time.Time `json:"startTime,omitempty"` + Suspend bool `json:"suspend"` + Terminated bool `json:"terminated"` + Steps []common.WorkflowStepStatus `json:"steps,omitempty"` +} + +// TableName return custom table name +func (w *WorkflowRecord) TableName() string { + return tableNamePrefix + "workflow_record" +} + +// PrimaryKey return custom primary key +func (w *WorkflowRecord) PrimaryKey() string { + return w.Name +} + +// Index return custom primary key +func (w *WorkflowRecord) Index() map[string]string { + index := make(map[string]string) + if w.Name != "" { + index["name"] = w.Name + } + if w.WorkflowPrimaryKey != "" { + index["workflowPrimaryKey"] = w.WorkflowPrimaryKey + } + return index +} diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index c202e903e..fa214a81e 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -484,8 +484,24 @@ type ListWorkflowRecordsResponse struct { Total int64 `json:"total"` } +// DetailWorkflowRecordResponse get workflow record detail +type DetailWorkflowRecordResponse struct { + WorkflowRecord + DeployTime time.Time `json:"deployTime"` + DeployUser string `json:"deployUser"` + Commit string `json:"commit"` + // SourceType the event trigger source, Web or API + SourceType string `json:"sourceType"` +} + // WorkflowRecord workflow record type WorkflowRecord struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + StartTime time.Time `json:"startTime,omitempty"` + Suspend bool `json:"suspend"` + Terminated bool `json:"terminated"` + Steps []common.WorkflowStepStatus `json:"steps,omitempty"` } // ApplicationDeployRequest the application deploy or update event request diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index f08e00b16..279627196 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -606,7 +606,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } if workflow != nil { - app.Annotations[oam.AnnotationWorkflowName] = workflow.AppPrimaryKey + app.Annotations[oam.AnnotationWorkflowName] = workflow.Name var steps []v1beta1.WorkflowStep for _, step := range workflow.Steps { var wstep = v1beta1.WorkflowStep{ diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 25960fcef..7df6b40fa 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -19,12 +19,18 @@ package usecase import ( "context" "errors" + "fmt" + "strings" + + appsv1 "k8s.io/api/apps/v1" "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" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" ) // WorkflowUsecase workflow manage api @@ -36,6 +42,8 @@ type WorkflowUsecase interface { DeleteWorkflow(ctx context.Context, workflowName string) error CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + ListWorkflowRecords(ctx context.Context, workflowName string, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) + DetailWorkflowRecord(ctx context.Context, workflowName, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) } // NewWorkflowUsecase new workflow usecase @@ -201,3 +209,94 @@ func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, } return nil, bcode.ErrWorkflowNoDefault } + +// ListWorkflowRecords list workflow record +func (w *workflowUsecaseImpl) ListWorkflowRecords(ctx context.Context, workflowName string, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) { + var record = model.WorkflowRecord{ + WorkflowPrimaryKey: workflowName, + } + records, err := w.ds.List(ctx, &record, &datastore.ListOptions{Page: page, PageSize: pageSize}) + if err != nil { + return nil, err + } + + resp := &apisv1.ListWorkflowRecordsResponse{ + Records: []apisv1.WorkflowRecord{}, + } + for _, raw := range records { + record, ok := raw.(*model.WorkflowRecord) + if ok { + resp.Records = append(resp.Records, *convertFromRecordModel(record)) + } + } + count, err := w.ds.Count(ctx, &record) + if err != nil { + return nil, err + } + resp.Total = count + + return resp, nil +} + +// DetailWorkflowRecord get workflow record detail with name +func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflowName, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) { + var record = model.WorkflowRecord{ + WorkflowPrimaryKey: workflowName, + Name: recordName, + } + err := w.ds.Get(ctx, &record) + if err != nil { + return nil, err + } + + version := strings.TrimPrefix(recordName, fmt.Sprintf("%s-", record.AppPrimaryKey)) + var deployEvent = model.DeployEvent{ + AppPrimaryKey: record.AppPrimaryKey, + Version: version, + } + err = w.ds.Get(ctx, &deployEvent) + if err != nil { + return nil, err + } + + return &apisv1.DetailWorkflowRecordResponse{ + WorkflowRecord: *convertFromRecordModel(&record), + DeployTime: deployEvent.CreateTime, + DeployUser: deployEvent.DeployUser, + Commit: deployEvent.Commit, + SourceType: deployEvent.SourceType, + }, nil +} + +func (w *workflowUsecaseImpl) createWorkflowRecord(ctx context.Context, revision *appsv1.ControllerRevision) error { + app, err := util.RawExtension2Application(revision.Data) + if err != nil { + return err + } + if app.Annotations == nil || app.Annotations[oam.AnnotationWorkflowName] == "" { + return fmt.Errorf("missing workflow name") + } + status := app.Status.Workflow + + return w.ds.Add(ctx, &model.WorkflowRecord{ + WorkflowPrimaryKey: app.Annotations[oam.AnnotationWorkflowName], + AppPrimaryKey: app.Name, + Name: strings.TrimPrefix(revision.Name, "record-"), + Namespace: revision.Namespace, + StartTime: status.StartTime.Time, + Suspend: status.Suspend, + Terminated: status.Terminated, + Steps: status.Steps, + }) +} + +func convertFromRecordModel(record *model.WorkflowRecord) *apisv1.WorkflowRecord { + return &apisv1.WorkflowRecord{ + Name: record.Name, + Namespace: record.Namespace, + StartTime: record.StartTime, + Suspend: record.Suspend, + Terminated: record.Terminated, + Steps: record.Steps, + } +} diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 1f05e36a4..37c36a491 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -18,10 +18,15 @@ package usecase import ( "context" + "fmt" "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" "github.com/oam-dev/kubevela/pkg/apiserver/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" @@ -34,7 +39,7 @@ var _ = Describe("Test workflow usecase functions", func() { BeforeEach(func() { workflowUsecase = &workflowUsecaseImpl{ds: ds} }) - It("Test CreateNamespace function", func() { + It("Test CreateWorkflow function", func() { req := apisv1.CreateWorkflowRequest{ Name: "test-workflow-1", Description: "this is a workflow", @@ -65,4 +70,105 @@ var _ = Describe("Test workflow usecase functions", func() { Expect(workflow).ShouldNot(BeNil()) Expect(cmp.Diff(workflow.Name, "test-workflow-2")).Should(BeEmpty()) }) + + It("Test ListWorkflowRecords function", func() { + By("create some controller revisions to test list workflow records") + raw, err := yaml.YAMLToJSON([]byte(yamlStr)) + Expect(err).Should(BeNil()) + for i := 0; i < 3; i++ { + err := workflowUsecase.createWorkflowRecord(context.TODO(), &appsv1.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("record-test-%v", i), + Namespace: "default", + }, + Data: runtime.RawExtension{ + Raw: raw, + }, + }) + Expect(err).Should(BeNil()) + } + + resp, err := workflowUsecase.ListWorkflowRecords(context.TODO(), "test-workflow-name", 0, 10) + Expect(err).Should(BeNil()) + Expect(resp.Total).Should(Equal(int64(3))) + }) + + It("Test DetailWorkflowRecord function", func() { + By("create one controller revision to test detail workflow record") + raw, err := yaml.YAMLToJSON([]byte(yamlStr)) + Expect(err).Should(BeNil()) + err = workflowUsecase.createWorkflowRecord(context.TODO(), &appsv1.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "record-test-123", + Namespace: "default", + }, + Data: runtime.RawExtension{ + Raw: raw, + }, + }) + Expect(err).Should(BeNil()) + + var deployEvent = &model.DeployEvent{ + AppPrimaryKey: "test", + Version: "123", + Status: model.DeployEventInit, + DeployUser: "test-user", + Commit: "test-commit", + SourceType: "API", + WorkflowName: "test-workflow-name", + } + + err = workflowUsecase.createTestDeployEvent(context.TODO(), deployEvent) + Expect(err).Should(BeNil()) + + detail, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-123") + Expect(err).Should(BeNil()) + Expect(detail.WorkflowRecord.Name).Should(Equal("test-123")) + Expect(detail.DeployUser).Should(Equal("test-user")) + }) }) + +var yamlStr = `apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + annotations: + app.oam.dev/workflowName: test-workflow-name + name: test + namespace: default +spec: + components: + - name: express-server + properties: + image: crccheck/hello-world + port: 8000 + type: webservice + workflow: + steps: + - name: apply-server + properties: + component: express-server + type: apply-component +status: + workflow: + steps: + - firstExecuteTime: "2021-10-26T11:19:33Z" + id: t8bpvi88d1 + lastExecuteTime: "2021-10-26T11:19:33Z" + name: apply-pvc + phase: succeeded + type: apply-object + - firstExecuteTime: "2021-10-26T11:19:33Z" + id: 9fou7rbq9r + lastExecuteTime: "2021-10-26T11:19:33Z" + name: apply-server + phase: succeeded + type: apply-component + suspend: false + terminated: false` + +func (w *workflowUsecaseImpl) createTestDeployEvent(ctx context.Context, deployEvent *model.DeployEvent) error { + if err := w.ds.Add(ctx, deployEvent); err != nil { + return err + } + return nil +} diff --git a/pkg/apiserver/rest/utils/params.go b/pkg/apiserver/rest/utils/params.go index 098a7aabf..141721aab 100644 --- a/pkg/apiserver/rest/utils/params.go +++ b/pkg/apiserver/rest/utils/params.go @@ -23,15 +23,17 @@ import ( "github.com/pkg/errors" ) +const defaultPageSize = "10" + // ExtractPagingParams extract `page` and `pageSize` params from request -func ExtractPagingParams(req *restful.Request, minPageSize int, maxPageSize int, defaultPageSize int) (int, int, error) { +func ExtractPagingParams(req *restful.Request, minPageSize, maxPageSize int) (int, int, error) { pageStr := req.QueryParameter("page") pageSizeStr := req.QueryParameter("pageSize") if pageStr == "" { pageStr = "0" } if pageSizeStr == "" { - pageSizeStr = strconv.Itoa(defaultPageSize) + pageSizeStr = defaultPageSize } page64, err := strconv.ParseInt(pageStr, 10, 32) if err != nil { diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index 7fc9c9408..a4c78fb05 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -114,7 +114,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { func (c *ClusterWebService) listKubeClusters(req *restful.Request, res *restful.Response) { query := req.QueryParameter("query") - page, pageSize, err := utils.ExtractPagingParams(req, 5, 100, 20) + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) if err != nil { bcode.ReturnError(req, res, err) return @@ -222,7 +222,7 @@ func (c *ClusterWebService) deleteKubeCluster(req *restful.Request, res *restful func (c *ClusterWebService) listCloudClusters(req *restful.Request, res *restful.Response) { provider := req.PathParameter("provider") - page, pageSize, err := utils.ExtractPagingParams(req, 5, 100, 20) + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/validate.go b/pkg/apiserver/rest/webservice/validate.go index ff4f063ea..c0419df89 100644 --- a/pkg/apiserver/rest/webservice/validate.go +++ b/pkg/apiserver/rest/webservice/validate.go @@ -26,6 +26,11 @@ 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])?)*$`) +const ( + minPageSize = 5 + maxPageSize = 100 +) + func init() { if err := validate.RegisterValidation("checkname", ValidateName); err != nil { panic(err) diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index a1d08c1d4..1f9e4718f 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -27,6 +27,7 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/model" 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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) @@ -90,7 +91,7 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). Writes(apis.EmptyResponse{}).Do(returns200, returns500)) - ws.Route(ws.GET("/{name}/records").To(noop). + ws.Route(ws.GET("/{name}/records").To(w.listWorkflowRecords). Doc("query application workflow execution record"). Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -99,6 +100,13 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Param(ws.PathParameter("pageSize", "Query the page size number.").DataType("integer")). Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) + ws.Route(ws.GET("/{name}/records/{record}").To(w.detailWorkflowRecord). + Doc("query application workflow execution record detail"). + Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(apis.DetailWorkflowRecordResponse{}).Do(returns200, returns500)) + return ws } @@ -215,3 +223,35 @@ func (w *workflowWebService) deleteWorkflow(req *restful.Request, res *restful.R return } } + +func (w *workflowWebService) listWorkflowRecords(req *restful.Request, res *restful.Response) { + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + records, err := w.workflowUsecase.ListWorkflowRecords(req.Request.Context(), req.PathParameter("name"), page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(records); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) detailWorkflowRecord(req *restful.Request, res *restful.Response) { + record, err := w.workflowUsecase.DetailWorkflowRecord(req.Request.Context(), req.PathParameter("name"), req.PathParameter("record")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(record); err != nil { + bcode.ReturnError(req, res, err) + return + } +} From 9519d2443ae03987d1c609e858390861ccc160db Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Sun, 31 Oct 2021 18:23:55 +0800 Subject: [PATCH 11/59] Feat: add alias field and fix some code bug (#2589) * Feat: add alias field and fix some code bug * Fix: fix alias check rule bug * Fix: fix addon e2e test bug Co-authored-by: barnettZQG --- Dockerfile | 7 +-- Dockerfile.apiserver | 48 +++++++++++++++ Makefile | 7 ++- pkg/apiserver/model/application.go | 2 + pkg/apiserver/model/cluster.go | 10 ++-- pkg/apiserver/model/workflow.go | 1 + pkg/apiserver/rest/apis/v1/types.go | 60 ++++++++++++++----- pkg/apiserver/rest/usecase/addon.go | 42 +++++++++---- pkg/apiserver/rest/usecase/application.go | 24 +++++++- .../rest/usecase/application_test.go | 2 +- pkg/apiserver/rest/usecase/cluster.go | 12 ++-- pkg/apiserver/rest/utils/bcode/addon.go | 4 +- pkg/apiserver/rest/utils/uiswagger.go | 52 ++++++++++++++++ pkg/apiserver/rest/webservice/addon.go | 26 ++++---- .../rest/webservice/addon_registry.go | 19 ++++++ pkg/apiserver/rest/webservice/application.go | 6 +- pkg/apiserver/rest/webservice/validate.go | 12 ++++ pkg/cloudprovider/aliyun.go | 1 + pkg/cloudprovider/types.go | 1 + test/e2e-apiserver-test/addon_test.go | 6 +- 20 files changed, 276 insertions(+), 66 deletions(-) create mode 100644 Dockerfile.apiserver create mode 100644 pkg/apiserver/rest/utils/uiswagger.go diff --git a/Dockerfile b/Dockerfile index 0882cc6cb..8a5318e68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +ARG BASE_IMAGE="alpine:latest" # Build the manager binary FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.16-alpine as builder @@ -24,15 +25,10 @@ RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ go build -a -ldflags "-s -w -X github.com/oam-dev/kubevela/version.VelaVersion=${VERSION:-undefined} -X github.com/oam-dev/kubevela/version.GitRevision=${GITVERSION:-undefined}" \ -o manager-${TARGETARCH} main.go -RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ - go build -a -ldflags "-s -w -X github.com/oam-dev/kubevela/version.VelaVersion=${VERSION:-undefined} -X github.com/oam-dev/kubevela/version.GitRevision=${GITVERSION:-undefined}" \ - -o apiserver-${TARGETARCH} cmd/apiserver/main.go - # Use alpine as base image due to the discussion in issue #1448 # You can replace distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details # Overwrite `BASE_IMAGE` by passing `--build-arg=BASE_IMAGE=gcr.io/distroless/static:nonroot` -ARG BASE_IMAGE FROM ${BASE_IMAGE:-alpine:latest} # This is required by daemon connnecting with cri RUN apk add --no-cache ca-certificates bash @@ -41,7 +37,6 @@ WORKDIR / ARG TARGETARCH COPY --from=builder /workspace/manager-${TARGETARCH} /usr/local/bin/manager -COPY --from=builder /workspace/apiserver-${TARGETARCH} /usr/local/bin/apiserver COPY entrypoint.sh /usr/local/bin/ diff --git a/Dockerfile.apiserver b/Dockerfile.apiserver new file mode 100644 index 000000000..4b39b7130 --- /dev/null +++ b/Dockerfile.apiserver @@ -0,0 +1,48 @@ +ARG BASE_IMAGE="alpine:latest" +# Build the manager binary +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.16-alpine as builder +ARG GOPROXY +ENV GOPROXY=${GOPROXY:-https://goproxy.cn} +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/core/main.go main.go +COPY cmd/apiserver/main.go cmd/apiserver/main.go +COPY apis/ apis/ +COPY pkg/ pkg/ +COPY version/ version/ + +# Build +ARG TARGETARCH +ARG VERSION +ARG GITVERSION + +RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ + go build -a -ldflags "-s -w -X github.com/oam-dev/kubevela/version.VelaVersion=${VERSION:-undefined} -X github.com/oam-dev/kubevela/version.GitRevision=${GITVERSION:-undefined}" \ + -o apiserver-${TARGETARCH} cmd/apiserver/main.go + +# Use alpine as base image due to the discussion in issue #1448 +# You can replace distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +# Overwrite `BASE_IMAGE` by passing `--build-arg=BASE_IMAGE=gcr.io/distroless/static:nonroot` + +FROM ${BASE_IMAGE:-alpine:latest} +# This is required by daemon connnecting with cri +RUN apk add --no-cache ca-certificates bash + +WORKDIR / + +ARG TARGETARCH +COPY --from=builder /workspace/apiserver-${TARGETARCH} /usr/local/bin/apiserver + +COPY entrypoint.sh /usr/local/bin/ + +ENTRYPOINT ["entrypoint.sh"] + +CMD ["apiserver"] diff --git a/Makefile b/Makefile index 85d8f3fe4..d21c62896 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ endif # Image URL to use all building/pushing image targets VELA_CORE_IMAGE ?= vela-core:latest VELA_CORE_TEST_IMAGE ?= vela-core-test:$(GIT_COMMIT) +VELA_APISERVER_IMAGE ?= apiserver:latest VELA_RUNTIME_ROLLOUT_IMAGE ?= vela-runtime-rollout:latest VELA_RUNTIME_ROLLOUT_TEST_IMAGE ?= vela-runtime-rollout-test:$(GIT_COMMIT) RUNTIME_CLUSTER_CONFIG ?= /tmp/worker.kubeconfig @@ -133,8 +134,12 @@ check-diff: reviewable @$(OK) branch is clean # Build the docker image -docker-build: +docker-build: docker-build-core docker-build-apiserver + @$(OK) +docker-build-core: docker build --build-arg=VERSION=$(VELA_VERSION) --build-arg=GITVERSION=$(GIT_COMMIT) -t $(VELA_CORE_IMAGE) . +docker-build-apiserver: + docker build --build-arg=VERSION=$(VELA_VERSION) --build-arg=GITVERSION=$(GIT_COMMIT) -t $(VELA_APISERVER_IMAGE) -f Dockerfile.apiserver . # Build the runtime docker image docker-build-runtime-rollout: diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 7ff36c523..9a3f1be8b 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -30,6 +30,7 @@ func init() { type Application struct { Model Name string `json:"name"` + Alias string `json:"alias"` Namespace string `json:"namespace"` Description string `json:"description"` Icon string `json:"icon"` @@ -82,6 +83,7 @@ type ApplicationComponent struct { Icon string `json:"icon,omitempty"` Creator string `json:"creator"` Name string `json:"name"` + Alias string `json:"alias"` Type string `json:"type"` // ExternalRevision specified the component revisionName diff --git a/pkg/apiserver/model/cluster.go b/pkg/apiserver/model/cluster.go index 34517de99..553f6b3cf 100644 --- a/pkg/apiserver/model/cluster.go +++ b/pkg/apiserver/model/cluster.go @@ -22,10 +22,11 @@ func init() { // ProviderInfo describes the information from provider API type ProviderInfo struct { - Name string `json:"name"` - ID string `json:"id"` - Zone string `json:"zone"` - Labels map[string]string `json:"labels"` + Provider string `json:"provider"` + ClusterName string `json:"name"` + ID string `json:"id"` + Zone string `json:"zone"` + Labels map[string]string `json:"labels"` } const ( @@ -39,6 +40,7 @@ const ( type Cluster struct { Model Name string `json:"name"` + Alias string `json:"alias"` Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels"` diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index eed20cca7..c0395f54d 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -32,6 +32,7 @@ func init() { type Workflow struct { Model Name string `json:"name"` + Alias string `json:"alias"` Description string `json:"description"` Enable bool `json:"enable"` // Workflow used by the default diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index fa214a81e..bbfca1356 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -50,21 +50,23 @@ type EmptyResponse struct{} // CreateAddonRegistryRequest defines the format for addon registry create request type CreateAddonRegistryRequest struct { - Name string `json:"name" validate:"required"` - - Git *model.GitAddonSource `json:"git,omitempty"` + Name string `json:"name" validate:"checkname"` + Git *model.GitAddonSource `json:"git,omitempty"` } // AddonRegistryMeta defines the format for a single addon registry type AddonRegistryMeta struct { - Name string `json:"name" validate:"required"` + Name string `json:"name" validate:"required"` + Git *model.GitAddonSource `json:"git,omitempty"` +} - Git *model.GitAddonSource `json:"git,omitempty"` +// ListAddonRegistryResponse list addon registry +type ListAddonRegistryResponse struct { + Registrys []*AddonRegistryMeta `json:"registrys"` } // EnableAddonRequest defines the format for enable addon request type EnableAddonRequest struct { - // Args is the key-value environment variables, e.g. AK/SK credentials. Args map[string]string `json:"args,omitempty"` } @@ -76,15 +78,11 @@ type ListAddonResponse struct { // AddonMeta defines the format for a single addon type AddonMeta struct { - Name string `json:"name"` - - Version string `json:"version"` - - Description string `json:"description"` - - Icon string `json:"icon"` - - Tags []string `json:"tags"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Icon string `json:"icon"` + Tags []string `json:"tags"` } // DetailAddonResponse defines the format for showing the addon details @@ -120,6 +118,7 @@ type AccessKeyRequest struct { // CreateClusterRequest request parameters to create a cluster type CreateClusterRequest struct { Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias"` Description string `json:"description,omitempty"` Icon string `json:"icon"` KubeConfig string `json:"kubeConfig,omitempty" validate:"required_without=KubeConfigSecret"` @@ -134,6 +133,7 @@ type ConnectCloudClusterRequest struct { AccessKeySecret string `json:"accessKeySecret"` ClusterID string `json:"clusterID"` Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias"` Description string `json:"description,omitempty"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` @@ -174,6 +174,7 @@ type ListCloudClusterResponse struct { // ClusterBase cluster base model type ClusterBase struct { Name string `json:"name"` + Alias string `json:"alias" validate:"checkalias"` Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels"` @@ -186,14 +187,35 @@ type ClusterBase struct { Reason string `json:"reason"` } +// ListApplicatioOptions list application query options +type ListApplicatioOptions struct { + Namespace string `json:"namespace"` + Cluster string `json:"cluster"` + Query string `json:"query"` +} + // ListApplicationResponse list applications by query params type ListApplicationResponse struct { Applications []*ApplicationBase `json:"applications"` } +// EnvBindList env bind list +type EnvBindList []*EnvBind + +// ContainCluster contain cluster name +func (e EnvBindList) ContainCluster(name string) bool { + for _, eb := range e { + if eb.ClusterSelector != nil && eb.ClusterSelector.Name == name { + return true + } + } + return false +} + // ApplicationBase application base model type ApplicationBase struct { Name string `json:"name"` + Alias string `json:"alias"` Namespace string `json:"namespace"` Description string `json:"description"` CreateTime time.Time `json:"createTime"` @@ -201,7 +223,7 @@ type ApplicationBase struct { Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` Status string `json:"status"` - EnvBind []*EnvBind `json:"envBind,omitempty"` + EnvBind EnvBindList `json:"envBind,omitempty"` GatewayRuleList []GatewayRule `json:"gatewayRule"` } @@ -227,6 +249,7 @@ type GatewayRule struct { // CreateApplicationRequest create application request body type CreateApplicationRequest struct { Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias"` Namespace string `json:"namespace" validate:"checkname"` Description string `json:"description"` Icon string `json:"icon"` @@ -276,6 +299,7 @@ type ApplicationResourceInfo struct { // ComponentBase component base model type ComponentBase struct { Name string `json:"name"` + Alias string `json:"alias"` Description string `json:"description"` Labels map[string]string `json:"labels,omitempty"` ComponentType string `json:"componentType"` @@ -296,6 +320,7 @@ type ComponentListResponse struct { // CreateComponentRequest create component request model type CreateComponentRequest struct { Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias"` Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` @@ -431,6 +456,7 @@ type PolicyDefinition struct { type CreateWorkflowRequest struct { AppName string `json:"appName" validate:"checkname"` Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias"` Description string `json:"description"` Steps []WorkflowStep `json:"steps,omitempty"` Enable bool `json:"enable"` @@ -439,6 +465,7 @@ type CreateWorkflowRequest struct { // UpdateWorkflowRequest update or create application workflow type UpdateWorkflowRequest struct { + Alias string `json:"alias" validate:"checkalias"` Description string `json:"description"` Steps []WorkflowStep `json:"steps,omitempty"` Enable bool `json:"enable"` @@ -471,6 +498,7 @@ type ListWorkflowResponse struct { // WorkflowBase workflow base model type WorkflowBase struct { Name string `json:"name"` + Alias string `json:"alias"` Description string `json:"description"` Enable bool `json:"enable"` Default bool `json:"default"` diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 0b4e38dfa..44cb83f1e 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -5,27 +5,28 @@ import ( "context" "errors" "fmt" - "github.com/Masterminds/sprig" - "github.com/oam-dev/kubevela/pkg/apiserver/log" - "golang.org/x/oauth2" "net/http" "net/url" "path" "sort" "strings" "text/template" + "time" + "github.com/Masterminds/sprig" + "github.com/google/go-github/v32/github" + "golang.org/x/oauth2" errors2 "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/serializer/yaml" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/google/go-github/v32/github" common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" restutils "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" @@ -46,7 +47,8 @@ const ( type AddonUsecase interface { GetAddonRegistryModel(ctx context.Context, name string) (*model.AddonRegistry, error) CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) - ListAddons(ctx context.Context, detailed bool) ([]*apis.DetailAddonResponse, error) + ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) + ListAddons(ctx context.Context, detailed bool, query string) ([]*apis.DetailAddonResponse, error) StatusAddon(name string) (*apis.AddonStatusResponse, error) GetAddon(ctx context.Context, name string) (*apis.DetailAddonResponse, error) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error @@ -73,7 +75,7 @@ type addonUsecaseImpl struct { } func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string) (*apis.DetailAddonResponse, error) { - addons, err := u.ListAddons(ctx, true) + addons, err := u.ListAddons(ctx, true, "") if err != nil { return nil, err } @@ -121,7 +123,7 @@ func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, } } -func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool) ([]*apis.DetailAddonResponse, error) { +func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, query string) ([]*apis.DetailAddonResponse, error) { // Backward compatibility with ConfigMap addons. // We will deprecate ConfigMap and use Git based registry. addons, err := getAddonsFromConfigMap(detailed) @@ -129,17 +131,27 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool) ([]*ap return nil, err } - rs, err := u.listAddonRegistries(ctx) + rs, err := u.ListAddonRegistries(ctx) if err != nil { return nil, err } for _, r := range rs { gitAddons, err := getAddonsFromGit(r.Git.URL, r.Git.Path, r.Git.Token, detailed) if err != nil { - return nil, err + log.Logger.Errorf("list addons from registry %s failure %s", r.Name, err.Error()) + continue } addons = mergeAddons(addons, gitAddons) } + if query != "" { + var new []*apis.DetailAddonResponse + for i, addon := range addons { + if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) { + new = append(new, addons[i]) + } + } + addons = new + } sort.Slice(addons, func(i, j int) bool { return addons[i].Name < addons[j].Name }) @@ -175,7 +187,7 @@ func (u *addonUsecaseImpl) GetAddonRegistryModel(ctx context.Context, name strin return &r, nil } -func (u *addonUsecaseImpl) listAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) { +func (u *addonUsecaseImpl) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) { var r = model.AddonRegistry{} entities, err := u.ds.List(ctx, &r, &datastore.ListOptions{}) if err != nil { @@ -307,6 +319,7 @@ func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.Detail ) tc = oauth2.NewClient(context.Background(), ts) } + tc.Timeout = time.Second * 10 clt := github.NewClient(tc) // TODO add error handling baseURL = strings.TrimSuffix(baseURL, ".git") @@ -329,7 +342,7 @@ func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.Detail } addonRes := apis.DetailAddonResponse{ AddonMeta: apis.AddonMeta{ - Name: *subItems.Name, + Name: converAddonName(*subItems.Name), }, } var err error @@ -384,7 +397,7 @@ func getAddonsFromConfigMap(detailed bool) ([]*apis.DetailAddonResponse, error) for _, addon := range cliAddons { d := &apis.DetailAddonResponse{ AddonMeta: apis.AddonMeta{ - Name: addon.Name, + Name: converAddonName(addon.Name), // TODO add actual Version, Icon, tags Version: "v1alpha1", Description: addon.Description, @@ -399,5 +412,8 @@ func getAddonsFromConfigMap(detailed bool) ([]*apis.DetailAddonResponse, error) addons = append(addons, d) } return addons, nil - +} + +func converAddonName(name string) string { + return strings.ReplaceAll(name, "/", "-") } diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index 279627196..c39fbe8d7 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "errors" + "strings" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -52,7 +53,7 @@ const ( // ApplicationUsecase application usecase type ApplicationUsecase interface { - ListApplications(ctx context.Context) ([]*apisv1.ApplicationBase, error) + ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) GetApplication(ctx context.Context, appName string) (*model.Application, error) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) @@ -92,15 +93,28 @@ func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUseca } // ListApplications list applications -func (c *applicationUsecaseImpl) ListApplications(ctx context.Context) ([]*apisv1.ApplicationBase, error) { +func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) { var app = model.Application{} + if listOptions.Namespace != "" { + app.Namespace = listOptions.Namespace + } entitys, err := c.ds.List(ctx, &app, &datastore.ListOptions{}) if err != nil { return nil, err } var list []*apisv1.ApplicationBase for _, entity := range entitys { - list = append(list, c.converAppModelToBase(ctx, entity.(*model.Application))) + appBase := c.converAppModelToBase(ctx, entity.(*model.Application)) + if listOptions.Query != "" && + !(strings.Contains(appBase.Alias, listOptions.Query) || + strings.Contains(appBase.Name, listOptions.Query) || + strings.Contains(appBase.Description, listOptions.Query)) { + continue + } + if listOptions.Cluster != "" && !appBase.EnvBind.ContainCluster(listOptions.Cluster) { + continue + } + list = append(list, appBase) } return list, nil } @@ -152,6 +166,7 @@ func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) { application := model.Application{ Name: req.Name, + Alias: req.Alias, Description: req.Description, Namespace: req.Namespace, Icon: req.Icon, @@ -357,6 +372,7 @@ func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.ApplicationComponent) *apisv1.ComponentBase { return &apisv1.ComponentBase{ Name: m.Name, + Alias: m.Alias, Description: m.Description, Labels: m.Labels, ComponentType: m.Type, @@ -629,6 +645,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app *model.Application) *apisv1.ApplicationBase { appBeas := &apisv1.ApplicationBase{ Name: app.Name, + Alias: app.Alias, Namespace: app.Namespace, CreateTime: app.CreateTime, UpdateTime: app.UpdateTime, @@ -720,6 +737,7 @@ func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Ap Name: com.Name, Type: com.ComponentType, DependsOn: com.DependsOn, + Alias: com.Alias, } properties, err := model.NewJSONStructByString(com.Properties) if err != nil { diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index bd5c81aa3..b1e20e961 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -140,7 +140,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test ListApplications function", func() { - apps, err := appUsecase.ListApplications(context.TODO()) + apps, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioOptions{}) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(apps), 3)).Should(BeEmpty()) }) diff --git a/pkg/apiserver/rest/usecase/cluster.go b/pkg/apiserver/rest/usecase/cluster.go index 31f405fa1..29c46988f 100644 --- a/pkg/apiserver/rest/usecase/cluster.go +++ b/pkg/apiserver/rest/usecase/cluster.go @@ -174,6 +174,7 @@ func createClusterModelFromRequest(req apis.CreateClusterRequest, oldCluster *mo newCluster = &model.Cluster{} } newCluster.Name = req.Name + newCluster.Alias = req.Alias newCluster.Description = req.Description newCluster.Icon = req.Icon newCluster.Labels = req.Labels @@ -194,10 +195,11 @@ func (c *clusterUsecaseImpl) createKubeCluster(ctx context.Context, req apis.Cre cluster.SetUpdateTime(t) if providerCluster != nil { cluster.Provider = model.ProviderInfo{ - Name: providerCluster.Name, - ID: providerCluster.ID, - Zone: providerCluster.Zone, - Labels: providerCluster.Labels, + Provider: providerCluster.Provider, + ClusterName: providerCluster.Name, + ID: providerCluster.ID, + Zone: providerCluster.Zone, + Labels: providerCluster.Labels, } cluster.DashboardURL = providerCluster.DashBoardURL } @@ -431,6 +433,7 @@ func (c *clusterUsecaseImpl) ConnectCloudCluster(ctx context.Context, provider s } createReq := apis.CreateClusterRequest{ Name: req.Name, + Alias: req.Alias, Description: req.Description, Icon: req.Icon, Labels: req.Labels, @@ -442,6 +445,7 @@ func (c *clusterUsecaseImpl) ConnectCloudCluster(ctx context.Context, provider s func newClusterBaseFromCluster(cluster *model.Cluster) *apis.ClusterBase { return &apis.ClusterBase{ Name: cluster.Name, + Alias: cluster.Alias, Description: cluster.Description, Icon: cluster.Icon, Labels: cluster.Labels, diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go index 4b1ef71a0..a7ba3cf29 100644 --- a/pkg/apiserver/rest/utils/bcode/addon.go +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -19,9 +19,9 @@ package bcode var ( // ErrAddonNotExist addon not exist - ErrAddonNotExist = NewBcode(400, 50001, "addon not exist") + ErrAddonNotExist = NewBcode(404, 50001, "addon not exist") - // ErrAddonRegistryExist application is exist + // ErrAddonRegistryExist addon is exist ErrAddonRegistryExist = NewBcode(400, 50002, "addon name already exists") // ErrAddonRenderFail fail to render addon application diff --git a/pkg/apiserver/rest/utils/uiswagger.go b/pkg/apiserver/rest/utils/uiswagger.go new file mode 100644 index 000000000..fca084eaf --- /dev/null +++ b/pkg/apiserver/rest/utils/uiswagger.go @@ -0,0 +1,52 @@ +/* +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 utils + +// UIParameter Structured import table simple UI model +type UIParameter struct { + Label string `json:"label"` + Description string `json:"description"` + Validate *Validate `json:"validete,omitempty"` + JSONKey string `json:"jsonKey"` + UIType string `json:"uiType"` + // means only can be read. + Disable bool `json:"disable"` + SubParameters []*UIParameter `json:"subParameters,omitempty"` +} + +// Validate parameter validate rule +type Validate struct { + Required bool `json:"required,omitempty"` + Max int `json:"max,omitempty"` + Min int `json:"min,omitempty"` + Regular string `json:"regular,omitempty"` + Options []*Options `json:"options,omitempty"` + DefaultValue interface{} `json:"defaultValue,omitempty"` +} + +// Options select option +type Options struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// ParseUIParameterFromDefinition cue of parameter in Definitions was analyzed to obtain the form description model. +func ParseUIParameterFromDefinition(definition []byte) ([]*UIParameter, error) { + var params []*UIParameter + + return params, nil +} diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 52e18f988..57f0469ca 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -19,6 +19,7 @@ package webservice import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" "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" @@ -48,6 +49,7 @@ func (s *addonWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(s.listAddons). Doc("list all addons"). Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")). Returns(200, "", apis.ListAddonResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.ListAddonResponse{})) @@ -59,41 +61,41 @@ func (s *addonWebService) GetWebService() *restful.WebService { Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.DetailAddonResponse{}). Returns(400, "", bcode.Bcode{}). - Param(ws.QueryParameter("name", "addon name to query detail").DataType("string").Required(true)). + Param(ws.PathParameter("name", "addon name to query detail").DataType("string").Required(true)). Writes(apis.DetailAddonResponse{})) // GET status - ws.Route(ws.GET("/status").To(s.statusAddon). + ws.Route(ws.GET("/{name}/status").To(s.statusAddon). Doc("show status of an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.AddonStatusResponse{}). Returns(400, "", bcode.Bcode{}). - Param(ws.QueryParameter("name", "addon name to query status").DataType("string").Required(true)). + Param(ws.PathParameter("name", "addon name to query status").DataType("string").Required(true)). Writes(apis.AddonStatusResponse{})) // enable addon - ws.Route(ws.POST("/enable").To(s.enableAddon). + ws.Route(ws.POST("/{name}/enable").To(s.enableAddon). Doc("enable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.AddonStatusResponse{}). Returns(400, "", bcode.Bcode{}). - Param(ws.QueryParameter("name", "addon name to enable").DataType("string").Required(true)). + Param(ws.PathParameter("name", "addon name to enable").DataType("string").Required(true)). Writes(apis.AddonStatusResponse{})) // disable addon - ws.Route(ws.POST("/disable").To(s.disableAddon). + ws.Route(ws.POST("/{name}/disable").To(s.disableAddon). Doc("disable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.AddonStatusResponse{}). Returns(400, "", bcode.Bcode{}). - Param(ws.QueryParameter("name", "addon name to enable").DataType("string").Required(true)). + Param(ws.PathParameter("name", "addon name to enable").DataType("string").Required(true)). Writes(apis.AddonStatusResponse{})) return ws } func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response) { - detailAddons, err := s.addonUsecase.ListAddons(req.Request.Context(), false) + detailAddons, err := s.addonUsecase.ListAddons(req.Request.Context(), false, req.QueryParameter("query")) if err != nil { bcode.ReturnError(req, res, err) return @@ -113,7 +115,7 @@ func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response } func (s *addonWebService) detailAddon(req *restful.Request, res *restful.Response) { - name := req.QueryParameter("name") + name := req.PathParameter("name") addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name) if err != nil { bcode.ReturnError(req, res, err) @@ -140,7 +142,7 @@ func (s *addonWebService) enableAddon(req *restful.Request, res *restful.Respons return } - name := req.QueryParameter("name") + name := req.PathParameter("name") err = s.addonUsecase.EnableAddon(req.Request.Context(), name, createReq) if err != nil { bcode.ReturnError(req, res, err) @@ -151,7 +153,7 @@ func (s *addonWebService) enableAddon(req *restful.Request, res *restful.Respons } func (s *addonWebService) disableAddon(req *restful.Request, res *restful.Response) { - name := req.QueryParameter("name") + name := req.PathParameter("name") err := s.addonUsecase.DisableAddon(req.Request.Context(), name) if err != nil { bcode.ReturnError(req, res, err) @@ -161,7 +163,7 @@ func (s *addonWebService) disableAddon(req *restful.Request, res *restful.Respon } func (s *addonWebService) statusAddon(req *restful.Request, res *restful.Response) { - name := req.QueryParameter("name") + name := req.PathParameter("name") status, err := s.addonUsecase.StatusAddon(name) if err != nil { bcode.ReturnError(req, res, err) diff --git a/pkg/apiserver/rest/webservice/addon_registry.go b/pkg/apiserver/rest/webservice/addon_registry.go index 8b45fb84f..d6d0eab3b 100644 --- a/pkg/apiserver/rest/webservice/addon_registry.go +++ b/pkg/apiserver/rest/webservice/addon_registry.go @@ -56,6 +56,13 @@ func (s *addonRegistryWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.AddonRegistryMeta{})) + ws.Route(ws.GET("/").To(s.listAddonRegistry). + Doc("list all addon registry"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListAddonRegistryResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListAddonRegistryResponse{})) + // Delete ws.Route(ws.DELETE("/{name}").To(s.deleteAddonRegistry). Doc("delete an addon registry"). @@ -107,3 +114,15 @@ func (s *addonRegistryWebService) deleteAddonRegistry(req *restful.Request, res return } } + +func (s *addonRegistryWebService) listAddonRegistry(req *restful.Request, res *restful.Response) { + registrys, err := s.addonUsecase.ListAddonRegistries(req.Request.Context()) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListAddonRegistryResponse{Registrys: registrys}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index f848749ea..77d06273b 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -222,7 +222,11 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res } func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { - apps, err := c.applicationUsecase.ListApplications(req.Request.Context()) + apps, err := c.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioOptions{ + Namespace: req.QueryParameter("namespace"), + Cluster: req.QueryParameter("cluster"), + Query: req.QueryParameter("query"), + }) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/validate.go b/pkg/apiserver/rest/webservice/validate.go index c0419df89..f5714e80b 100644 --- a/pkg/apiserver/rest/webservice/validate.go +++ b/pkg/apiserver/rest/webservice/validate.go @@ -35,6 +35,9 @@ func init() { if err := validate.RegisterValidation("checkname", ValidateName); err != nil { panic(err) } + if err := validate.RegisterValidation("checkalias", ValidateAlias); err != nil { + panic(err) + } } // ValidateName custom check name field @@ -45,3 +48,12 @@ func ValidateName(fl validator.FieldLevel) bool { } return nameRegexp.MatchString(value) } + +// ValidateAlias custom check alias field +func ValidateAlias(fl validator.FieldLevel) bool { + value := fl.Field().String() + if value != "" && (len(value) > 64 || len(value) < 2) { + return false + } + return true +} diff --git a/pkg/cloudprovider/aliyun.go b/pkg/cloudprovider/aliyun.go index 6ccf9985b..fd7e8cd28 100644 --- a/pkg/cloudprovider/aliyun.go +++ b/pkg/cloudprovider/aliyun.go @@ -116,6 +116,7 @@ func (provider *AliyunCloudProvider) GetClusterInfo(clusterID string) (*CloudClu labels := provider.decodeClusterLabels(cluster.Tags) url := provider.decodeClusterURL(*cluster.MasterUrl) return &CloudCluster{ + Provider: ProviderAliyun, ID: *cluster.ClusterId, Name: *cluster.Name, Type: *cluster.ClusterType, diff --git a/pkg/cloudprovider/types.go b/pkg/cloudprovider/types.go index 319643d06..5898ee433 100644 --- a/pkg/cloudprovider/types.go +++ b/pkg/cloudprovider/types.go @@ -23,6 +23,7 @@ const ( // CloudCluster describes the interface that cloud provider should return type CloudCluster struct { + Provider string `json:"provider"` ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go index d31549847..3b8e40220 100644 --- a/test/e2e-apiserver-test/addon_test.go +++ b/test/e2e-apiserver-test/addon_test.go @@ -85,7 +85,7 @@ var _ = Describe("Test addon rest api", func() { Args: map[string]string{}, } testAddon := "fluxcd" - res := post("/api/v1/addons/enable?name="+testAddon, req) + res := post("/api/v1/addons/"+testAddon+"/enable", req) Expect(res).ShouldNot(BeNil()) Expect(res.StatusCode).Should(Equal(200)) Expect(res.Body).ShouldNot(BeNil()) @@ -103,7 +103,7 @@ var _ = Describe("Test addon rest api", func() { period := 20 * time.Second timeout := 5 * time.Minute err = wait.PollImmediate(period, timeout, func() (done bool, err error) { - res = get("/api/v1/addons/status?name=" + testAddon) + res = get("/api/v1/addons/" + testAddon + "/status") err = json.NewDecoder(res.Body).Decode(&statusRes) Expect(err).Should(BeNil()) if statusRes.Phase == apis.AddonPhaseEnabled { @@ -113,7 +113,7 @@ var _ = Describe("Test addon rest api", func() { }) Expect(err).Should(BeNil()) - res = post("/api/v1/addons/disable?name="+testAddon, req) + res = post("/api/v1/addons/"+testAddon+"/disable", req) Expect(res).ShouldNot(BeNil()) Expect(res.StatusCode).Should(Equal(200)) Expect(res.Body).ShouldNot(BeNil()) From 90e5fd9ed61e94ae28ae88653240cf58e9033be4 Mon Sep 17 00:00:00 2001 From: Somefive Date: Tue, 2 Nov 2021 09:49:52 +0800 Subject: [PATCH 12/59] Fix: add fixes to cluster api (#2597) --- pkg/apiserver/rest/apis/v1/types.go | 2 +- pkg/apiserver/rest/usecase/cluster.go | 8 +++++++- pkg/apiserver/rest/utils/bcode/cluster.go | 3 +++ pkg/apiserver/rest/webservice/cluster.go | 2 +- pkg/cloudprovider/aliyun.go | 6 ++++++ pkg/cloudprovider/cluster.go | 1 + test/e2e-apiserver-test/cluster_test.go | 2 +- 7 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index bbfca1356..9f6b0c059 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -156,7 +156,7 @@ type ClusterResourceInfo struct { // DetailClusterResponse cluster detail information model type DetailClusterResponse struct { - ClusterBase + model.Cluster ResourceInfo ClusterResourceInfo `json:"resourceInfo"` } diff --git a/pkg/apiserver/rest/usecase/cluster.go b/pkg/apiserver/rest/usecase/cluster.go index 29c46988f..88439e7ce 100644 --- a/pkg/apiserver/rest/usecase/cluster.go +++ b/pkg/apiserver/rest/usecase/cluster.go @@ -246,7 +246,7 @@ func (c *clusterUsecaseImpl) GetKubeCluster(ctx context.Context, clusterName str return nil, errors.Wrapf(err, "failed to update cluster %s status info", clusterName) } return &apis.DetailClusterResponse{ - ClusterBase: *newClusterBaseFromCluster(cluster), + Cluster: *cluster, ResourceInfo: resourceInfo, }, nil } @@ -402,6 +402,9 @@ func (c *clusterUsecaseImpl) ListCloudClusters(ctx context.Context, provider str } clusters, total, err := p.ListCloudClusters(pageNumber, pageSize) if err != nil { + if p.IsInvalidKey(err) { + return nil, bcode.ErrInvalidAccessKeyOrSecretKey + } log.Logger.Errorf("failed to list cloud clusters: %s", err.Error()) return nil, bcode.ErrGetCloudClusterFailure } @@ -428,6 +431,9 @@ func (c *clusterUsecaseImpl) ConnectCloudCluster(ctx context.Context, provider s } cluster, err := p.GetClusterInfo(req.ClusterID) if err != nil { + if p.IsInvalidKey(err) { + return nil, bcode.ErrInvalidAccessKeyOrSecretKey + } log.Logger.Errorf("failed to get cluster info: %s", err.Error()) return nil, bcode.ErrGetCloudClusterFailure } diff --git a/pkg/apiserver/rest/utils/bcode/cluster.go b/pkg/apiserver/rest/utils/bcode/cluster.go index 4e8a15611..0b1a8647f 100644 --- a/pkg/apiserver/rest/utils/bcode/cluster.go +++ b/pkg/apiserver/rest/utils/bcode/cluster.go @@ -42,3 +42,6 @@ var ErrLocalClusterReserved = NewBcode(400, 40007, "local cluster is reserved") // ErrLocalClusterImmutable local cluster kubeConfig is immutable var ErrLocalClusterImmutable = NewBcode(400, 40008, "local cluster is immutable") + +// ErrInvalidAccessKeyOrSecretKey access key or secret key is invalid +var ErrInvalidAccessKeyOrSecretKey = NewBcode(400, 40013, "access key or secret key is invalid") diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index a4c78fb05..fbb1045d7 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -72,7 +72,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.DetailClusterResponse{})) - ws.Route(ws.POST("/{clusterName}").To(c.modifyKubeCluster). + ws.Route(ws.PUT("/{clusterName}").To(c.modifyKubeCluster). Doc("modify cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). diff --git a/pkg/cloudprovider/aliyun.go b/pkg/cloudprovider/aliyun.go index fd7e8cd28..943700351 100644 --- a/pkg/cloudprovider/aliyun.go +++ b/pkg/cloudprovider/aliyun.go @@ -18,6 +18,7 @@ package cloudprovider import ( "encoding/json" + "strings" cs20151215 "github.com/alibabacloud-go/cs-20151215/v2/client" openapi "github.com/alibabacloud-go/darabonba-openapi/client" @@ -49,6 +50,11 @@ func NewAliyunCloudProvider(accessKeyID string, accessKeySecret string) (*Aliyun return &AliyunCloudProvider{Client: c}, nil } +// IsInvalidKey check if error is InvalidAccessKey or InvalidSecretKey +func (provider *AliyunCloudProvider) IsInvalidKey(err error) bool { + return strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "Code: SignatureDoesNotMatch") +} + func (provider *AliyunCloudProvider) decodeClusterLabels(tags []*cs20151215.Tag) map[string]string { labels := map[string]string{} for _, tag := range tags { diff --git a/pkg/cloudprovider/cluster.go b/pkg/cloudprovider/cluster.go index 5f1c530cc..a18873396 100644 --- a/pkg/cloudprovider/cluster.go +++ b/pkg/cloudprovider/cluster.go @@ -22,6 +22,7 @@ import ( // CloudClusterProvider abstracts the cloud provider to provide cluster access type CloudClusterProvider interface { + IsInvalidKey(err error) bool ListCloudClusters(pageNumber int, pageSize int) ([]*CloudCluster, int, error) GetClusterKubeConfig(clusterID string) (string, error) GetClusterInfo(clusterID string) (*CloudCluster, error) diff --git a/test/e2e-apiserver-test/cluster_test.go b/test/e2e-apiserver-test/cluster_test.go index 180dbb7f1..8e2dd0684 100644 --- a/test/e2e-apiserver-test/cluster_test.go +++ b/test/e2e-apiserver-test/cluster_test.go @@ -78,7 +78,7 @@ var _ = Describe("Test cluster rest api", func() { It("Test modify cluster", func() { kubeconfigBytes, err := ioutil.ReadFile(WorkerClusterKubeConfigPath) Expect(err).Should(Succeed()) - resp, err := CreateRequest(http.MethodPost, "/clusters/"+clusterName, v1.CreateClusterRequest{ + resp, err := CreateRequest(http.MethodPut, "/clusters/"+clusterName, v1.CreateClusterRequest{ Name: clusterName, KubeConfig: string(kubeconfigBytes), Description: "Example description", From 5f09faeff011f4e7f08645be30ad2d2539676b9c Mon Sep 17 00:00:00 2001 From: Hongchao Deng Date: Mon, 1 Nov 2021 23:30:30 -0400 Subject: [PATCH 13/59] Fix: delete addon registry (#2594) * Fix: delete addon registry * add validate Git should not be empty * fix token could be nil * GitSource Repo url is a must --- pkg/apiserver/model/addon.go | 2 +- pkg/apiserver/rest/apis/v1/types.go | 2 +- pkg/apiserver/rest/usecase/addon.go | 14 ++++++++------ pkg/apiserver/rest/webservice/addon_registry.go | 5 +++++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/pkg/apiserver/model/addon.go b/pkg/apiserver/model/addon.go index fffd52e38..0088a858c 100644 --- a/pkg/apiserver/model/addon.go +++ b/pkg/apiserver/model/addon.go @@ -26,7 +26,7 @@ type AddonRegistry struct { // GitAddonSource defines the information about the Git as addon source type GitAddonSource struct { - URL string `json:"url,omitempty"` + URL string `json:"url,omitempty" validate:"required"` Path string `json:"path,omitempty"` Token string `json:"token,omitempty"` } diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 9f6b0c059..197e366a4 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -51,7 +51,7 @@ type EmptyResponse struct{} // CreateAddonRegistryRequest defines the format for addon registry create request type CreateAddonRegistryRequest struct { Name string `json:"name" validate:"checkname"` - Git *model.GitAddonSource `json:"git,omitempty"` + Git *model.GitAddonSource `json:"git,omitempty" validate:"required"` } // AddonRegistryMeta defines the format for a single addon registry diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 44cb83f1e..0a1c3af8f 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "net/http" "net/url" "path" "sort" @@ -47,6 +46,7 @@ const ( type AddonUsecase interface { GetAddonRegistryModel(ctx context.Context, name string) (*model.AddonRegistry, error) CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) + DeleteAddonRegistry(ctx context.Context, name string) error ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) ListAddons(ctx context.Context, detailed bool, query string) ([]*apis.DetailAddonResponse, error) StatusAddon(name string) (*apis.AddonStatusResponse, error) @@ -158,6 +158,10 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, query return addons, nil } +func (u *addonUsecaseImpl) DeleteAddonRegistry(ctx context.Context, name string) error { + return u.ds.Delete(ctx, &model.AddonRegistry{Name: name}) +} + func (u *addonUsecaseImpl) CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) { r := addonRegistryModelFromCreateAddonRegistryRequest(req) @@ -312,13 +316,11 @@ func hasAddon(addons []*apis.DetailAddonResponse, name string) bool { func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.DetailAddonResponse, error) { addons := []*apis.DetailAddonResponse{} dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) - var tc *http.Client + var ts oauth2.TokenSource if token != "" { - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - tc = oauth2.NewClient(context.Background(), ts) + ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) } + tc := oauth2.NewClient(context.Background(), ts) tc.Timeout = time.Second * 10 clt := github.NewClient(tc) // TODO add error handling diff --git a/pkg/apiserver/rest/webservice/addon_registry.go b/pkg/apiserver/rest/webservice/addon_registry.go index d6d0eab3b..209110f1b 100644 --- a/pkg/apiserver/rest/webservice/addon_registry.go +++ b/pkg/apiserver/rest/webservice/addon_registry.go @@ -108,6 +108,11 @@ func (s *addonRegistryWebService) deleteAddonRegistry(req *restful.Request, res bcode.ReturnError(req, res, err) return } + err = s.addonUsecase.DeleteAddonRegistry(req.Request.Context(), r.Name) + if err != nil { + bcode.ReturnError(req, res, err) + return + } if err := res.WriteEntity(*utils.ConvertAddonRegistryModel2AddonRegistryMeta(r)); err != nil { bcode.ReturnError(req, res, err) From 8e874bc3a9eac112fd424da9d2bb67834265dadf Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Wed, 3 Nov 2021 17:25:37 +0800 Subject: [PATCH 14/59] Feat: change application to applicationplan (#2616) * Feat: change application to applicationplan * Feat: update swagger config * Feat: change api spec * Fix : fix e2e test case Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 1207 +++++++++++------ pkg/apiserver/datastore/datastore_test.go | 4 +- .../datastore/kubeapi/kubeapi_test.go | 26 +- .../datastore/mongodb/mongodb_test.go | 24 +- pkg/apiserver/model/application.go | 40 +- pkg/apiserver/model/workflow.go | 12 +- pkg/apiserver/rest/apis/v1/types.go | 68 +- pkg/apiserver/rest/usecase/application.go | 158 +-- .../rest/usecase/application_test.go | 14 +- pkg/apiserver/rest/usecase/workflow.go | 46 +- pkg/apiserver/rest/usecase/workflow_test.go | 10 +- pkg/apiserver/rest/webservice/application.go | 78 +- .../rest/webservice/validate_test.go | 8 +- pkg/apiserver/rest/webservice/workflow.go | 28 +- test/e2e-apiserver-test/application_test.go | 50 +- 15 files changed, 1106 insertions(+), 667 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 9f4aa3991..cc9f77b13 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -15,7 +15,300 @@ "version": "v1beta1" }, "paths": { - "/api/v1/applications": { + "/api/v1/addon_registries": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "list all addon registry", + "operationId": "listAddonRegistry", + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListAddonRegistryResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "create an addon registry", + "operationId": "createAddonRegistry", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateAddonRegistryRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addon_registries/{name}": { + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "delete an addon registry", + "operationId": "deleteAddonRegistry", + "parameters": [ + { + "type": "string", + "description": "identifier of the addon registry", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "list all addons", + "operationId": "listAddons", + "parameters": [ + { + "type": "string", + "description": "Fuzzy search based on name and description.", + "name": "query", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListAddonResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "show details of an addon", + "operationId": "detailAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to query detail", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailAddonResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/disable": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "disable an addon", + "operationId": "disableAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to enable", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/enable": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "enable an addon", + "operationId": "enableAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to enable", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/status": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "show status of an addon", + "operationId": "statusAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to query status", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans": { "get": { "consumes": [ "application/xml", @@ -53,7 +346,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ListApplicationResponse" + "$ref": "#/definitions/v1.ListApplicationPlanResponse" } }, "400": { @@ -83,14 +376,14 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateApplicationRequest" + "$ref": "#/definitions/v1.CreateApplicationPlanRequest" } } ], "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ApplicationBase" + "$ref": "#/definitions/v1.ApplicationPlanBase" } }, "400": { @@ -101,7 +394,7 @@ } } }, - "/api/v1/applications/{name}": { + "/api/v1/applicationplans/{name}": { "get": { "consumes": [ "application/xml", @@ -128,7 +421,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.DetailApplicationResponse" + "$ref": "#/definitions/v1.DetailApplicationPlanResponse" } }, "400": { @@ -175,7 +468,7 @@ } } }, - "/api/v1/applications/{name}/components": { + "/api/v1/applicationplans/{name}/componentplans": { "get": { "consumes": [ "application/xml", @@ -188,7 +481,7 @@ "tags": [ "application" ], - "summary": "gets the component topology of the application", + "summary": "gets the componentplan topology of the application", "operationId": "listApplicationComponents", "parameters": [ { @@ -200,16 +493,15 @@ }, { "type": "string", - "description": "list components that deployed in define cluster", - "name": "cluster", - "in": "path", - "required": true + "description": "list components that deployed in define env", + "name": "envName", + "in": "query" } ], "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ComponentListResponse" + "$ref": "#/definitions/v1.ComponentPlanListResponse" } }, "400": { @@ -231,7 +523,7 @@ "tags": [ "application" ], - "summary": "create component for application", + "summary": "create component plan for application plan", "operationId": "createComponent", "parameters": [ { @@ -246,14 +538,14 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateComponentRequest" + "$ref": "#/definitions/v1.CreateComponentPlanRequest" } } ], "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ComponentBase" + "$ref": "#/definitions/v1.ComponentPlanBase" } }, "400": { @@ -264,7 +556,7 @@ } } }, - "/api/v1/applications/{name}/components/{componentName}": { + "/api/v1/applicationplans/{name}/componentplans/{componentName}": { "get": { "consumes": [ "application/xml", @@ -277,7 +569,7 @@ "tags": [ "application" ], - "summary": "detail component for application", + "summary": "detail component plan for application plan", "operationId": "detailComponent", "parameters": [ { @@ -291,7 +583,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.DetailComponentResponse" + "$ref": "#/definitions/v1.DetailComponentPlanResponse" } }, "400": { @@ -302,7 +594,7 @@ } } }, - "/api/v1/applications/{name}/deploy": { + "/api/v1/applicationplans/{name}/deploy": { "post": { "consumes": [ "application/xml", @@ -340,7 +632,7 @@ } } }, - "/api/v1/applications/{name}/policies": { + "/api/v1/applicationplans/{name}/policies": { "get": { "consumes": [ "application/xml", @@ -422,7 +714,7 @@ } } }, - "/api/v1/applications/{name}/policies/{policyName}": { + "/api/v1/applicationplans/{name}/policies/{policyName}": { "get": { "consumes": [ "application/xml", @@ -561,7 +853,7 @@ } } }, - "/api/v1/applications/{name}/template": { + "/api/v1/applicationplans/{name}/template": { "post": { "consumes": [ "application/xml", @@ -715,6 +1007,13 @@ "summary": "list cloud clusters", "operationId": "listCloudClusters", "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, { "type": "int", "default": 0, @@ -768,6 +1067,13 @@ "summary": "create cluster from cloud cluster", "operationId": "connectCloudCluster", "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, { "name": "body", "in": "body", @@ -828,7 +1134,7 @@ } } }, - "post": { + "put": { "consumes": [ "application/xml", "application/json" @@ -1015,7 +1321,44 @@ } } }, - "/api/v1/workflows": { + "/api/v1/query": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "velaQL" + ], + "summary": "use velaQL to query resource status", + "operationId": "queryView", + "parameters": [ + { + "type": "string", + "description": "velaql query statement", + "name": "velaql", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.VelaQLViewResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/workflowplans": { "get": { "consumes": [ "application/xml", @@ -1076,7 +1419,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateWorkflowRequest" + "$ref": "#/definitions/v1.CreateWorkflowPlanRequest" } } ], @@ -1099,7 +1442,7 @@ } } }, - "/api/v1/workflows/{name}": { + "/api/v1/workflowplans/{name}": { "get": { "consumes": [ "application/xml", @@ -1162,7 +1505,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpdateWorkflowRequest" + "$ref": "#/definitions/v1.UpdateWorkflowPlanRequest" } } ], @@ -1214,7 +1557,7 @@ } } }, - "/api/v1/workflows/{name}/records": { + "/api/v1/workflowplans/{name}/records": { "get": { "consumes": [ "application/xml", @@ -1228,7 +1571,7 @@ "cluster" ], "summary": "query application workflow execution record", - "operationId": "noop", + "operationId": "listWorkflowRecords", "parameters": [ { "type": "string", @@ -1265,7 +1608,7 @@ } } }, - "/v1/addons": { + "/api/v1/workflowplans/{name}/records/{record}": { "get": { "consumes": [ "application/xml", @@ -1276,16 +1619,24 @@ "application/xml" ], "tags": [ - "addon" + "cluster" ], - "summary": "list all addons", - "operationId": "noop", + "summary": "query application workflow execution record detail", + "operationId": "detailWorkflowRecord", "parameters": [ { "type": "string", - "description": "Cluster-based search", - "name": "cluster", - "in": "query" + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true } ], "responses": { @@ -1299,187 +1650,6 @@ "description": "Bummer, something went wrong" } } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "create an addon", - "operationId": "noop", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateAddonRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/addons/{name}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "show details of an addon", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the addon", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "delete an addon", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the addon", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/addons/{name}/disable": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "disable an addon on a cluster", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "cluster name", - "name": "cluster", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/addons/{name}/enable": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "enable an addon on a cluster", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "cluster name", - "name": "cluster", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/addons/{name}/status": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "show status of an addon", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the addon", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } } }, "/v1/namespaces/{namespace}/applications/{appname}": { @@ -1619,6 +1789,7 @@ }, "cloudprovider.CloudCluster": { "required": [ + "provider", "id", "name", "type", @@ -1647,6 +1818,9 @@ "name": { "type": "string" }, + "provider": { + "type": "string" + }, "status": { "type": "string" }, @@ -1661,10 +1835,10 @@ "common.AppRolloutStatus": { "required": [ "batchRollingState", - "currentBatch", "upgradedReplicas", - "upgradedReadyReplicas", "rollingState", + "currentBatch", + "upgradedReadyReplicas", "lastTargetAppRevision" ], "properties": { @@ -2129,16 +2303,20 @@ "type": "string" } }, - "model.ApplicationComponent": { + "model.ApplicationComponentPlan": { "required": [ "createTime", "updateTime", "appPrimaryKey", "creator", "name", + "alias", "type" ], "properties": { + "alias": { + "type": "string" + }, "appPrimaryKey": { "type": "string" }, @@ -2197,7 +2375,7 @@ "traits": { "type": "array", "items": { - "$ref": "#/definitions/model.ApplicationTrait" + "$ref": "#/definitions/model.ApplicationTraitPlan" } }, "type": { @@ -2209,7 +2387,7 @@ } } }, - "model.ApplicationTrait": { + "model.ApplicationTraitPlan": { "required": [ "type" ], @@ -2222,6 +2400,86 @@ } } }, + "model.Cluster": { + "required": [ + "createTime", + "updateTime", + "name", + "alias", + "description", + "icon", + "labels", + "status", + "reason", + "provider", + "apiServerURL", + "dashboardURL", + "kubeConfig", + "kubeConfigSecret" + ], + "properties": { + "alias": { + "type": "string" + }, + "apiServerURL": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/model.ProviderInfo" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "model.GitAddonSource": { + "properties": { + "path": { + "type": "string" + }, + "token": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "model.JSONStruct": { "type": "object" }, @@ -2243,6 +2501,7 @@ }, "model.ProviderInfo": { "required": [ + "provider", "name", "id", "zone", @@ -2261,6 +2520,9 @@ "name": { "type": "string" }, + "provider": { + "type": "string" + }, "zone": { "type": "string" } @@ -2322,8 +2584,7 @@ "version", "description", "icon", - "tags", - "phase" + "tags" ], "properties": { "description": { @@ -2335,9 +2596,6 @@ "name": { "type": "string" }, - "phase": { - "type": "string" - }, "tags": { "type": "array", "items": { @@ -2349,71 +2607,32 @@ } } }, + "v1.AddonRegistryMeta": { + "required": [ + "name" + ], + "properties": { + "git": { + "$ref": "#/definitions/model.GitAddonSource" + }, + "name": { + "type": "string" + } + } + }, "v1.AddonStatusResponse": { "required": [ "phase" ], "properties": { + "enabling_progress": { + "$ref": "#/definitions/v1.EnablingProgress" + }, "phase": { "type": "string" } } }, - "v1.ApplicationBase": { - "required": [ - "name", - "namespace", - "description", - "createTime", - "updateTime", - "icon", - "status", - "gatewayRule" - ], - "properties": { - "createTime": { - "type": "string", - "format": "date-time" - }, - "description": { - "type": "string" - }, - "envBind": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.EnvBind" - } - }, - "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.ApplicationDeployRequest": { "required": [ "workflowName", @@ -2466,6 +2685,65 @@ } } }, + "v1.ApplicationPlanBase": { + "required": [ + "name", + "alias", + "namespace", + "description", + "createTime", + "updateTime", + "icon", + "status", + "gatewayRule" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "envBind": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBind" + } + }, + "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" @@ -2578,6 +2856,7 @@ "v1.ClusterBase": { "required": [ "name", + "alias", "description", "icon", "labels", @@ -2588,6 +2867,9 @@ "reason" ], "properties": { + "alias": { + "type": "string" + }, "apiServerURL": { "type": "string" }, @@ -2693,10 +2975,29 @@ } } }, - "v1.ComponentBase": { + "v1.ComponentDefinitionBase": { "required": [ "name", "description", + "icon" + ], + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.ComponentPlanBase": { + "required": [ + "name", + "alias", + "description", "componentType", "envNames", "dependsOn", @@ -2705,6 +3006,9 @@ "updateTime" ], "properties": { + "alias": { + "type": "string" + }, "componentType": { "type": "string" }, @@ -2751,33 +3055,15 @@ } } }, - "v1.ComponentDefinitionBase": { + "v1.ComponentPlanListResponse": { "required": [ - "name", - "description", - "icon" + "componentplans" ], "properties": { - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "v1.ComponentListResponse": { - "required": [ - "components" - ], - "properties": { - "components": { + "componentplans": { "type": "array", "items": { - "$ref": "#/definitions/v1.ComponentBase" + "$ref": "#/definitions/v1.ComponentPlanBase" } } } @@ -2788,6 +3074,7 @@ "accessKeySecret", "clusterID", "name", + "alias", "icon" ], "properties": { @@ -2797,6 +3084,9 @@ "accessKeySecret": { "type": "string" }, + "alias": { + "type": "string" + }, "clusterID": { "type": "string" }, @@ -2817,51 +3107,31 @@ } } }, - "v1.CreateAddonRequest": { + "v1.CreateAddonRegistryRequest": { "required": [ - "name", - "version", - "icon", - "tags" + "name" ], "properties": { - "deploy_data": { - "type": "string" - }, - "deploy_url": { - "type": "string" - }, - "description": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "icon": { - "type": "string" + "git": { + "$ref": "#/definitions/model.GitAddonSource" }, "name": { "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "version": { - "type": "string" } } }, - "v1.CreateApplicationRequest": { + "v1.CreateApplicationPlanRequest": { "required": [ "name", + "alias", "namespace", "description", "icon" ], "properties": { + "alias": { + "type": "string" + }, "deploy": { "type": "boolean" }, @@ -2915,9 +3185,13 @@ "v1.CreateClusterRequest": { "required": [ "name", + "alias", "icon" ], "properties": { + "alias": { + "type": "string" + }, "dashboardURL": { "type": "string" }, @@ -2944,15 +3218,19 @@ } } }, - "v1.CreateComponentRequest": { + "v1.CreateComponentPlanRequest": { "required": [ "name", + "alias", "description", "icon", "componentType", "dependsOn" ], "properties": { + "alias": { + "type": "string" + }, "componentType": { "type": "string" }, @@ -3024,15 +3302,19 @@ } } }, - "v1.CreateWorkflowRequest": { + "v1.CreateWorkflowPlanRequest": { "required": [ "appName", "name", + "alias", "description", "enable", "default" ], "properties": { + "alias": { + "type": "string" + }, "appName": { "type": "string" }, @@ -3058,20 +3340,16 @@ }, "v1.DetailAddonResponse": { "required": [ + "version", "description", "icon", "tags", - "phase", - "name", - "version" + "name" ], "properties": { "deploy_data": { "type": "string" }, - "deploy_url": { - "type": "string" - }, "description": { "type": "string" }, @@ -3084,9 +3362,6 @@ "name": { "type": "string" }, - "phase": { - "type": "string" - }, "tags": { "type": "array", "items": { @@ -3098,22 +3373,26 @@ } } }, - "v1.DetailApplicationResponse": { + "v1.DetailApplicationPlanResponse": { "required": [ - "description", - "createTime", + "icon", "status", "gatewayRule", - "name", "namespace", + "description", + "createTime", "updateTime", - "icon", + "name", + "alias", "policies", "status", "resourceInfo", "workflowStatus" ], "properties": { + "alias": { + "type": "string" + }, "createTime": { "type": "string", "format": "date-time" @@ -3174,21 +3453,33 @@ }, "v1.DetailClusterResponse": { "required": [ - "status", - "icon", + "createTime", "description", - "labels", - "providerInfo", - "apiServerURL", - "dashboardURL", + "status", "reason", + "kubeConfig", + "kubeConfigSecret", "name", + "alias", + "provider", + "labels", + "dashboardURL", + "updateTime", + "icon", + "apiServerURL", "resourceInfo" ], "properties": { + "alias": { + "type": "string" + }, "apiServerURL": { "type": "string" }, + "createTime": { + "type": "string", + "format": "date-time" + }, "dashboardURL": { "type": "string" }, @@ -3198,6 +3489,12 @@ "icon": { "type": "string" }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, "labels": { "type": "object", "additionalProperties": { @@ -3207,26 +3504,28 @@ "name": { "type": "string" }, - "providerInfo": { + "provider": { "$ref": "#/definitions/model.ProviderInfo" }, "reason": { "type": "string" }, - "remoteManageURL": { - "type": "string" - }, "resourceInfo": { "$ref": "#/definitions/v1.ClusterResourceInfo" }, "status": { "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" } } }, - "v1.DetailComponentResponse": { + "v1.DetailComponentPlanResponse": { "required": [ "appPrimaryKey", + "alias", "type", "updateTime", "creator", @@ -3234,6 +3533,9 @@ "createTime" ], "properties": { + "alias": { + "type": "string" + }, "appPrimaryKey": { "type": "string" }, @@ -3292,7 +3594,7 @@ "traits": { "type": "array", "items": { - "$ref": "#/definitions/model.ApplicationTrait" + "$ref": "#/definitions/model.ApplicationTraitPlan" } }, "type": { @@ -3306,13 +3608,13 @@ }, "v1.DetailPolicyResponse": { "required": [ + "description", "creator", "properties", "createTime", "updateTime", "name", - "type", - "description" + "type" ], "properties": { "createTime": { @@ -3340,17 +3642,21 @@ } } }, - "v1.DetailWorkflowResponse": { + "v1.DetailWorkflowPlanResponse": { "required": [ + "description", + "enable", "default", "createTime", "updateTime", "name", - "description", - "enable", + "alias", "workflowRecord" ], "properties": { + "alias": { + "type": "string" + }, "createTime": { "type": "string", "format": "date-time" @@ -3382,7 +3688,72 @@ } } }, + "v1.DetailWorkflowRecordResponse": { + "required": [ + "name", + "namespace", + "suspend", + "terminated", + "deployTime", + "deployUser", + "commit", + "sourceType" + ], + "properties": { + "commit": { + "type": "string" + }, + "deployTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + }, + "suspend": { + "type": "boolean" + }, + "terminated": { + "type": "boolean" + } + } + }, "v1.EmptyResponse": {}, + "v1.EnablingProgress": { + "required": [ + "enabled_components", + "total_components" + ], + "properties": { + "enabled_components": { + "type": "integer", + "format": "int32" + }, + "total_components": { + "type": "integer", + "format": "int32" + } + } + }, "v1.EnvBind": { "required": [ "name", @@ -3427,6 +3798,19 @@ } } }, + "v1.ListAddonRegistryResponse": { + "required": [ + "registrys" + ], + "properties": { + "registrys": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + } + } + }, "v1.ListAddonResponse": { "required": [ "addons" @@ -3440,6 +3824,19 @@ } } }, + "v1.ListApplicationPlanResponse": { + "required": [ + "applicationplans" + ], + "properties": { + "applicationplans": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationPlanBase" + } + } + } + }, "v1.ListApplicationPolicy": { "required": [ "policies" @@ -3453,19 +3850,6 @@ } } }, - "v1.ListApplicationResponse": { - "required": [ - "applications" - ], - "properties": { - "applications": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ApplicationBase" - } - } - } - }, "v1.ListCloudClusterResponse": { "required": [ "clusters", @@ -3536,6 +3920,19 @@ } } }, + "v1.ListWorkflowPlanResponse": { + "required": [ + "workflowplans" + ], + "properties": { + "workflowplans": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowPlanBase" + } + } + } + }, "v1.ListWorkflowRecordsResponse": { "required": [ "records", @@ -3710,13 +4107,17 @@ } } }, - "v1.UpdateWorkflowRequest": { + "v1.UpdateWorkflowPlanRequest": { "required": [ + "alias", "description", "enable", "default" ], "properties": { + "alias": { + "type": "string" + }, "default": { "type": "boolean" }, @@ -3734,9 +4135,13 @@ } } }, - "v1.WorkflowBase": { + "v1.VelaQLViewResponse": { + "type": "object" + }, + "v1.WorkflowPlanBase": { "required": [ "name", + "alias", "description", "enable", "default", @@ -3744,6 +4149,9 @@ "updateTime" ], "properties": { + "alias": { + "type": "string" + }, "createTime": { "type": "string", "format": "date-time" @@ -3766,7 +4174,38 @@ } } }, - "v1.WorkflowRecord": {}, + "v1.WorkflowRecord": { + "required": [ + "name", + "namespace", + "suspend", + "terminated" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + }, + "suspend": { + "type": "boolean" + }, + "terminated": { + "type": "boolean" + } + } + }, "v1.WorkflowStep": { "required": [ "name", diff --git a/pkg/apiserver/datastore/datastore_test.go b/pkg/apiserver/datastore/datastore_test.go index 26feeff76..c6fd219c3 100644 --- a/pkg/apiserver/datastore/datastore_test.go +++ b/pkg/apiserver/datastore/datastore_test.go @@ -30,7 +30,7 @@ import ( var _ = Describe("Test new entity function", func() { It("Test new application entity", func() { - var app model.Application + var app model.ApplicationPlan new, err := NewEntity(&app) Expect(err).To(BeNil()) json.Unmarshal([]byte(`{"name":"demo"}`), new) @@ -40,7 +40,7 @@ var _ = Describe("Test new entity function", func() { }) It("Test new multiple application entity", func() { - var app model.Application + var app model.ApplicationPlan var list []Entity var n = 3 for n > 0 { diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go index bdb52f62f..b5d8c0e63 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go @@ -88,22 +88,22 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(kubeStore).ToNot(BeNil()) It("Test add funtion", func() { - err := kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) + err := kubeStore.Add(context.TODO(), &model.ApplicationPlan{Name: "kubevela-app", Description: "default"}) Expect(err).ToNot(HaveOccurred()) }) It("Test batch add funtion", func() { var datas = []datastore.Entity{ - &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, - &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.ApplicationPlan{Name: "kubevela-app-2", Description: "this is demo 2"}, + &model.ApplicationPlan{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, + &model.ApplicationPlan{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, } err := kubeStore.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) var datas2 = []datastore.Entity{ - &model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, - &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, + &model.ApplicationPlan{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, + &model.ApplicationPlan{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = kubeStore.BatchAdd(context.TODO(), datas2) equal := cmp.Diff(strings.Contains(err.Error(), "save components occur error"), true) @@ -111,7 +111,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test get funtion", func() { - app := &model.Application{Name: "kubevela-app"} + app := &model.ApplicationPlan{Name: "kubevela-app"} err := kubeStore.Get(context.TODO(), app) Expect(err).Should(BeNil()) diff := cmp.Diff(app.Description, "default") @@ -119,11 +119,11 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test put funtion", func() { - err := kubeStore.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) + err := kubeStore.Put(context.TODO(), &model.ApplicationPlan{Name: "kubevela-app", Description: "this is demo"}) Expect(err).ToNot(HaveOccurred()) }) It("Test index", func() { - var app = model.Application{ + var app = model.ApplicationPlan{ Namespace: "test", } selector, err := labels.Parse(fmt.Sprintf("table=%s", app.TableName())) @@ -137,7 +137,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(cmp.Diff(selector.String(), "namespace=test,table=vela_application")).Should(BeEmpty()) }) It("Test list function", func() { - var app model.Application + var app model.ApplicationPlan list, err := kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) Expect(err).ShouldNot(HaveOccurred()) diff := cmp.Diff(len(list), 4) @@ -166,7 +166,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test count function", func() { - var app model.Application + var app model.ApplicationPlan count, err := kubeStore.Count(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) Expect(count).Should(Equal(int64(4))) @@ -178,7 +178,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test isExist function", func() { - var app model.Application + var app model.ApplicationPlan app.Name = "kubevela-app-3" exist, err := kubeStore.IsExist(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) @@ -193,7 +193,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test delete funtion", func() { - var app model.Application + var app model.ApplicationPlan app.Name = "kubevela-app" err := kubeStore.Delete(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/apiserver/datastore/mongodb/mongodb_test.go b/pkg/apiserver/datastore/mongodb/mongodb_test.go index 3e947d028..ac5f884f6 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb_test.go +++ b/pkg/apiserver/datastore/mongodb/mongodb_test.go @@ -56,22 +56,22 @@ var _ = BeforeSuite(func(done Done) { var _ = Describe("Test mongodb datastore driver", func() { It("Test add funtion", func() { - err := mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) + err := mongodbDriver.Add(context.TODO(), &model.ApplicationPlan{Name: "kubevela-app", Description: "default"}) Expect(err).ToNot(HaveOccurred()) }) It("Test batch add funtion", func() { var datas = []datastore.Entity{ - &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, - &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.ApplicationPlan{Name: "kubevela-app-2", Description: "this is demo 2"}, + &model.ApplicationPlan{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, + &model.ApplicationPlan{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, } err := mongodbDriver.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) var datas2 = []datastore.Entity{ - &model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, - &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, + &model.ApplicationPlan{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, + &model.ApplicationPlan{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = mongodbDriver.BatchAdd(context.TODO(), datas2) equal := cmp.Diff(strings.Contains(err.Error(), "save components occur error"), true) @@ -79,7 +79,7 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test get funtion", func() { - app := &model.Application{Name: "kubevela-app"} + app := &model.ApplicationPlan{Name: "kubevela-app"} err := mongodbDriver.Get(context.TODO(), app) Expect(err).Should(BeNil()) diff := cmp.Diff(app.Description, "default") @@ -87,11 +87,11 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test put funtion", func() { - err := mongodbDriver.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) + err := mongodbDriver.Put(context.TODO(), &model.ApplicationPlan{Name: "kubevela-app", Description: "this is demo"}) Expect(err).ToNot(HaveOccurred()) }) It("Test list funtion", func() { - var app model.Application + var app model.ApplicationPlan list, err := mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) Expect(err).ShouldNot(HaveOccurred()) diff := cmp.Diff(len(list), 4) @@ -120,7 +120,7 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test count function", func() { - var app model.Application + var app model.ApplicationPlan count, err := mongodbDriver.Count(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) Expect(count).Should(Equal(int64(4))) @@ -132,7 +132,7 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test isExist funtion", func() { - var app model.Application + var app model.ApplicationPlan app.Name = "kubevela-app-3" exist, err := mongodbDriver.IsExist(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) @@ -147,7 +147,7 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test delete funtion", func() { - var app model.Application + var app model.ApplicationPlan app.Name = "kubevela-app" err := mongodbDriver.Delete(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 9a3f1be8b..5095f1cc7 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -23,11 +23,11 @@ import ( ) func init() { - RegistModel(&ApplicationComponent{}, &ApplicationPolicy{}, &Application{}, &DeployEvent{}) + RegistModel(&ApplicationComponentPlan{}, &ApplicationPolicyPlan{}, &ApplicationPlan{}, &DeployEvent{}) } -// Application database model -type Application struct { +// ApplicationPlan application delivery plan model +type ApplicationPlan struct { Model Name string `json:"name"` Alias string `json:"alias"` @@ -39,17 +39,17 @@ type Application struct { } // TableName return custom table name -func (a *Application) TableName() string { +func (a *ApplicationPlan) TableName() string { return tableNamePrefix + "application" } // PrimaryKey return custom primary key -func (a *Application) PrimaryKey() string { +func (a *ApplicationPlan) PrimaryKey() string { return a.Name } // Index return custom index -func (a *Application) Index() map[string]string { +func (a *ApplicationPlan) Index() map[string]string { index := make(map[string]string) if a.Name != "" { index["name"] = a.Name @@ -74,8 +74,8 @@ type ClusterSelector struct { Namespace string `json:"namespace,omitempty"` } -// ApplicationComponent component database model -type ApplicationComponent struct { +// ApplicationComponentPlan component database model +type ApplicationComponentPlan struct { Model AppPrimaryKey string `json:"appPrimaryKey"` Description string `json:"description,omitempty"` @@ -93,24 +93,24 @@ type ApplicationComponent struct { Inputs common.StepInputs `json:"inputs,omitempty"` Outputs common.StepOutputs `json:"outputs,omitempty"` // Traits define the trait of one component, the type must be array to keep the order. - Traits []ApplicationTrait `json:"traits,omitempty"` - // scopes in ApplicationComponent defines the component-level scopes + Traits []ApplicationTraitPlan `json:"traits,omitempty"` + // scopes in ApplicationComponentPlan defines the component-level scopes // the format is pairs, the key represents type of `ScopeDefinition` while the value represent the name of scope instance. Scopes map[string]string `json:"scopes,omitempty"` } // TableName return custom table name -func (a *ApplicationComponent) TableName() string { +func (a *ApplicationComponentPlan) TableName() string { return tableNamePrefix + "application_component" } // PrimaryKey return custom primary key -func (a *ApplicationComponent) PrimaryKey() string { +func (a *ApplicationComponentPlan) PrimaryKey() string { return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Name) } // Index return custom index -func (a *ApplicationComponent) Index() map[string]string { +func (a *ApplicationComponentPlan) Index() map[string]string { index := make(map[string]string) if a.Name != "" { index["name"] = a.Name @@ -124,8 +124,8 @@ func (a *ApplicationComponent) Index() map[string]string { return index } -// ApplicationPolicy app policy -type ApplicationPolicy struct { +// ApplicationPolicyPlan app policy +type ApplicationPolicyPlan struct { Model AppPrimaryKey string `json:"appPrimaryKey"` Name string `json:"name"` @@ -136,17 +136,17 @@ type ApplicationPolicy struct { } // TableName return custom table name -func (a *ApplicationPolicy) TableName() string { +func (a *ApplicationPolicyPlan) TableName() string { return tableNamePrefix + "application_policy" } // PrimaryKey return custom primary key -func (a *ApplicationPolicy) PrimaryKey() string { +func (a *ApplicationPolicyPlan) PrimaryKey() string { return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Name) } // Index return custom index -func (a *ApplicationPolicy) Index() map[string]string { +func (a *ApplicationPolicyPlan) Index() map[string]string { index := make(map[string]string) if a.Name != "" { index["name"] = a.Name @@ -160,8 +160,8 @@ func (a *ApplicationPolicy) Index() map[string]string { return index } -// ApplicationTrait application trait -type ApplicationTrait struct { +// ApplicationTraitPlan application trait +type ApplicationTraitPlan struct { Type string `json:"type"` Properties *JSONStruct `json:"properties,omitempty"` } diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index c0395f54d..261f76070 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -24,12 +24,12 @@ import ( ) func init() { - RegistModel(&Workflow{}) + RegistModel(&WorkflowPlan{}) RegistModel(&WorkflowRecord{}) } -// Workflow application delivery plan database model -type Workflow struct { +// WorkflowPlan application delivery plan database model +type WorkflowPlan struct { Model Name string `json:"name"` Alias string `json:"alias"` @@ -55,17 +55,17 @@ type WorkflowStep struct { } // TableName return custom table name -func (w *Workflow) TableName() string { +func (w *WorkflowPlan) TableName() string { return tableNamePrefix + "workflow" } // PrimaryKey return custom primary key -func (w *Workflow) PrimaryKey() string { +func (w *WorkflowPlan) PrimaryKey() string { return w.Name } // Index return custom primary key -func (w *Workflow) Index() map[string]string { +func (w *WorkflowPlan) Index() map[string]string { index := make(map[string]string) if w.Name != "" { index["name"] = w.Name diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 197e366a4..fb4c0e24c 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -187,16 +187,16 @@ type ClusterBase struct { Reason string `json:"reason"` } -// ListApplicatioOptions list application query options -type ListApplicatioOptions struct { +// ListApplicatioPlanOptions list application plan query options +type ListApplicatioPlanOptions struct { Namespace string `json:"namespace"` Cluster string `json:"cluster"` Query string `json:"query"` } -// ListApplicationResponse list applications by query params -type ListApplicationResponse struct { - Applications []*ApplicationBase `json:"applications"` +// ListApplicationPlanResponse list applications by query params +type ListApplicationPlanResponse struct { + ApplicationPlans []*ApplicationPlanBase `json:"applicationplans"` } // EnvBindList env bind list @@ -212,8 +212,8 @@ func (e EnvBindList) ContainCluster(name string) bool { return false } -// ApplicationBase application base model -type ApplicationBase struct { +// ApplicationPlanBase application base model +type ApplicationPlanBase struct { Name string `json:"name"` Alias string `json:"alias"` Namespace string `json:"namespace"` @@ -246,8 +246,8 @@ type GatewayRule struct { ComponentPort int32 `json:"componentPort"` } -// CreateApplicationRequest create application request body -type CreateApplicationRequest struct { +// CreateApplicationPlanRequest create application plan request body +type CreateApplicationPlanRequest struct { Name string `json:"name" validate:"checkname"` Alias string `json:"alias" validate:"checkalias"` Namespace string `json:"namespace" validate:"checkname"` @@ -274,9 +274,9 @@ type ClusterSelector struct { Namespace string `json:"namespace,omitempty"` } -// DetailApplicationResponse application detail -type DetailApplicationResponse struct { - ApplicationBase +// DetailApplicationPlanResponse application plan detail +type DetailApplicationPlanResponse struct { + ApplicationPlanBase Policies []string `json:"policies"` Status string `json:"status"` ResourceInfo ApplicationResourceInfo `json:"resourceInfo"` @@ -296,8 +296,8 @@ type ApplicationResourceInfo struct { // Others, such as: Memory、CPU、GPU、Storage } -// ComponentBase component base model -type ComponentBase struct { +// ComponentPlanBase component plan base model +type ComponentPlanBase struct { Name string `json:"name"` Alias string `json:"alias"` Description string `json:"description"` @@ -312,13 +312,13 @@ type ComponentBase struct { UpdateTime time.Time `json:"updateTime"` } -// ComponentListResponse list component -type ComponentListResponse struct { - Components []*ComponentBase `json:"components"` +// ComponentPlanListResponse list component plan +type ComponentPlanListResponse struct { + ComponentPlans []*ComponentPlanBase `json:"componentplans"` } -// CreateComponentRequest create component request model -type CreateComponentRequest struct { +// CreateComponentPlanRequest create component plan request model +type CreateComponentPlanRequest struct { Name string `json:"name" validate:"checkname"` Alias string `json:"alias" validate:"checkalias"` Description string `json:"description"` @@ -330,9 +330,9 @@ type CreateComponentRequest struct { DependsOn []string `json:"dependsOn"` } -// DetailComponentResponse detail component model -type DetailComponentResponse struct { - model.ApplicationComponent +// DetailComponentPlanResponse detail component plan model +type DetailComponentPlanResponse struct { + model.ApplicationComponentPlan //TODO: Status } @@ -452,8 +452,8 @@ type PolicyDefinition struct { Parameters []types.Parameter `json:"parameters"` } -// CreateWorkflowRequest create workflow request -type CreateWorkflowRequest struct { +// CreateWorkflowPlanRequest create workflow plan request +type CreateWorkflowPlanRequest struct { AppName string `json:"appName" validate:"checkname"` Name string `json:"name" validate:"checkname"` Alias string `json:"alias" validate:"checkalias"` @@ -463,8 +463,8 @@ type CreateWorkflowRequest struct { Default bool `json:"default"` } -// UpdateWorkflowRequest update or create application workflow -type UpdateWorkflowRequest struct { +// UpdateWorkflowPlanRequest update or create application workflow +type UpdateWorkflowPlanRequest struct { Alias string `json:"alias" validate:"checkalias"` Description string `json:"description"` Steps []WorkflowStep `json:"steps,omitempty"` @@ -483,20 +483,20 @@ type WorkflowStep struct { Outputs common.StepOutputs `json:"outputs,omitempty"` } -// DetailWorkflowResponse detail workflow response -type DetailWorkflowResponse struct { - WorkflowBase +// DetailWorkflowPlanResponse detail workflow response +type DetailWorkflowPlanResponse struct { + WorkflowPlanBase Steps []WorkflowStep `json:"steps,omitempty"` LastRecord *WorkflowRecord `json:"workflowRecord"` } -// ListWorkflowResponse list application workflows -type ListWorkflowResponse struct { - Workflows []*WorkflowBase `json:"workflows"` +// ListWorkflowPlanResponse list application workflows +type ListWorkflowPlanResponse struct { + WorkflowPlans []*WorkflowPlanBase `json:"workflowplans"` } -// WorkflowBase workflow base model -type WorkflowBase struct { +// WorkflowPlanBase workflow base model +type WorkflowPlanBase struct { Name string `json:"name"` Alias string `json:"alias"` Description string `json:"description"` diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index c39fbe8d7..64a6f4b33 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -53,22 +53,22 @@ const ( // ApplicationUsecase application usecase type ApplicationUsecase interface { - ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) - GetApplication(ctx context.Context, appName string) (*model.Application, error) - DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) - PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) - CreateApplication(context.Context, apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) - DeleteApplication(ctx context.Context, app *model.Application) error - Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) - ListComponents(ctx context.Context, app *model.Application) ([]*apisv1.ComponentBase, error) - AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) - DetailComponent(ctx context.Context, app *model.Application, componentName string) (*apisv1.DetailComponentResponse, error) - DeleteComponent(ctx context.Context, app *model.Application, componentName string) error - ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) - AddPolicy(ctx context.Context, app *model.Application, policy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) - DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) - DeletePolicy(ctx context.Context, app *model.Application, policyName string) error - UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) + ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioPlanOptions) ([]*apisv1.ApplicationPlanBase, error) + GetApplication(ctx context.Context, appName string) (*model.ApplicationPlan, error) + DetailApplication(ctx context.Context, app *model.ApplicationPlan) (*apisv1.DetailApplicationPlanResponse, error) + PublishApplicationTemplate(ctx context.Context, app *model.ApplicationPlan) (*apisv1.ApplicationTemplateBase, error) + CreateApplication(context.Context, apisv1.CreateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) + DeleteApplication(ctx context.Context, app *model.ApplicationPlan) error + Deploy(ctx context.Context, app *model.ApplicationPlan, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) + ListComponents(ctx context.Context, app *model.ApplicationPlan) ([]*apisv1.ComponentPlanBase, error) + AddComponent(ctx context.Context, app *model.ApplicationPlan, com apisv1.CreateComponentPlanRequest) (*apisv1.ComponentPlanBase, error) + DetailComponent(ctx context.Context, app *model.ApplicationPlan, componentName string) (*apisv1.DetailComponentPlanResponse, error) + DeleteComponent(ctx context.Context, app *model.ApplicationPlan, componentName string) error + ListPolicies(ctx context.Context, app *model.ApplicationPlan) ([]*apisv1.PolicyBase, error) + AddPolicy(ctx context.Context, app *model.ApplicationPlan, policy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) + DetailPolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) (*apisv1.DetailPolicyResponse, error) + DeletePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) error + UpdatePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) } type applicationUsecaseImpl struct { @@ -93,8 +93,8 @@ func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUseca } // ListApplications list applications -func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) { - var app = model.Application{} +func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioPlanOptions) ([]*apisv1.ApplicationPlanBase, error) { + var app = model.ApplicationPlan{} if listOptions.Namespace != "" { app.Namespace = listOptions.Namespace } @@ -102,9 +102,9 @@ func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptio if err != nil { return nil, err } - var list []*apisv1.ApplicationBase + var list []*apisv1.ApplicationPlanBase for _, entity := range entitys { - appBase := c.converAppModelToBase(ctx, entity.(*model.Application)) + appBase := c.converAppModelToBase(ctx, entity.(*model.ApplicationPlan)) if listOptions.Query != "" && !(strings.Contains(appBase.Alias, listOptions.Query) || strings.Contains(appBase.Name, listOptions.Query) || @@ -120,8 +120,8 @@ func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptio } // GetApplication get application model -func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName string) (*model.Application, error) { - var app = model.Application{ +func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName string) (*model.ApplicationPlan, error) { + var app = model.ApplicationPlan{ Name: appName, } if err := c.ds.Get(ctx, &app); err != nil { @@ -131,7 +131,7 @@ func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName str } // DetailApplication detail application info -func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) { +func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.ApplicationPlan) (*apisv1.DetailApplicationPlanResponse, error) { base := c.converAppModelToBase(ctx, app) policys, err := c.queryApplicationPolicys(ctx, app) if err != nil { @@ -145,9 +145,9 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod for _, p := range policys { policyNames = append(policyNames, p.Name) } - var detail = &apisv1.DetailApplicationResponse{ - ApplicationBase: *base, - Policies: policyNames, + var detail = &apisv1.DetailApplicationPlanResponse{ + ApplicationPlanBase: *base, + Policies: policyNames, ResourceInfo: apisv1.ApplicationResourceInfo{ ComponentNum: len(components), }, @@ -157,14 +157,14 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod } // PublishApplicationTemplate publish app template -func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) { +func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, app *model.ApplicationPlan) (*apisv1.ApplicationTemplateBase, error) { //TODO: return nil, nil } // CreateApplication create application -func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) { - application := model.Application{ +func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apisv1.CreateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) { + application := model.ApplicationPlan{ Name: req.Name, Alias: req.Alias, Description: req.Description, @@ -222,7 +222,7 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis Outputs: step.Outputs, }) } - _, err := c.workflowUsecase.CreateWorkflow(ctx, &application, apisv1.CreateWorkflowRequest{ + _, err := c.workflowUsecase.CreateWorkflow(ctx, &application, apisv1.CreateWorkflowPlanRequest{ AppName: application.PrimaryKey(), Name: application.Name, Description: "Created automatically.", @@ -240,7 +240,7 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis // build-in create env binding policy if len(req.EnvBind) > 0 { - policy := model.ApplicationPolicy{ + policy := model.ApplicationPolicyPlan{ AppPrimaryKey: application.PrimaryKey(), Name: "env-binds", Description: "build-in create", @@ -297,18 +297,18 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return base, nil } -func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, app *model.Application, components []common.ApplicationComponent) error { +func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, app *model.ApplicationPlan, components []common.ApplicationComponent) error { var componentModels []datastore.Entity for _, component := range components { // TODO: Check whether the component type is supported. - var traits []model.ApplicationTrait + var traits []model.ApplicationTraitPlan for _, trait := range component.Traits { properties, err := model.NewJSONStruct(trait.Properties) if err != nil { log.Logger.Errorf("parse trait properties failire %w", err) return bcode.ErrInvalidProperties } - traits = append(traits, model.ApplicationTrait{ + traits = append(traits, model.ApplicationTraitPlan{ Type: trait.Type, Properties: properties, }) @@ -318,7 +318,7 @@ func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, a log.Logger.Errorf("parse component properties failire %w", err) return bcode.ErrInvalidProperties } - componentModel := model.ApplicationComponent{ + componentModel := model.ApplicationComponentPlan{ AppPrimaryKey: app.PrimaryKey(), Name: component.Name, Type: component.Type, @@ -336,18 +336,18 @@ func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, a return c.ds.BatchAdd(ctx, componentModels) } -func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.Application) ([]*apisv1.ComponentBase, error) { - var component = model.ApplicationComponent{ +func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.ApplicationPlan) ([]*apisv1.ComponentPlanBase, error) { + var component = model.ApplicationComponentPlan{ AppPrimaryKey: app.PrimaryKey(), } components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) if err != nil { return nil, err } - var list []*apisv1.ComponentBase + var list []*apisv1.ComponentPlanBase for _, component := range components { log.Logger.Infof("component name %s", component.PrimaryKey()) - pm := component.(*model.ApplicationComponent) + pm := component.(*model.ApplicationComponentPlan) list = append(list, c.converComponentModelToBase(pm)) } return list, nil @@ -355,8 +355,8 @@ func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model. // DetailComponent detail app component // TODO: Add status data about the component. -func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailComponentResponse, error) { - var component = model.ApplicationComponent{ +func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model.ApplicationPlan, policyName string) (*apisv1.DetailComponentPlanResponse, error) { + var component = model.ApplicationComponentPlan{ AppPrimaryKey: app.PrimaryKey(), Name: policyName, } @@ -364,13 +364,13 @@ func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model if err != nil { return nil, err } - return &apisv1.DetailComponentResponse{ - ApplicationComponent: component, + return &apisv1.DetailComponentPlanResponse{ + ApplicationComponentPlan: component, }, nil } -func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.ApplicationComponent) *apisv1.ComponentBase { - return &apisv1.ComponentBase{ +func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.ApplicationComponentPlan) *apisv1.ComponentPlanBase { + return &apisv1.ComponentPlanBase{ Name: m.Name, Alias: m.Alias, Description: m.Description, @@ -385,7 +385,7 @@ func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.Application } // ListPolicies list application policies -func (c *applicationUsecaseImpl) ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) { +func (c *applicationUsecaseImpl) ListPolicies(ctx context.Context, app *model.ApplicationPlan) ([]*apisv1.PolicyBase, error) { policies, err := c.queryApplicationPolicys(ctx, app) if err != nil { return nil, err @@ -397,7 +397,7 @@ func (c *applicationUsecaseImpl) ListPolicies(ctx context.Context, app *model.Ap return list, nil } -func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.ApplicationPolicy) *apisv1.PolicyBase { +func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.ApplicationPolicyPlan) *apisv1.PolicyBase { pb := &apisv1.PolicyBase{ Name: policy.Name, Type: policy.Type, @@ -410,7 +410,7 @@ func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.Applicati return pb } -func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app *model.Application, policys []v1beta1.AppPolicy) error { +func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app *model.ApplicationPlan, policys []v1beta1.AppPolicy) error { var policyModels []datastore.Entity for _, policy := range policys { properties, err := model.NewJSONStruct(policy.Properties) @@ -418,7 +418,7 @@ func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app log.Logger.Errorf("parse trait properties failire %w", err) return bcode.ErrInvalidProperties } - policyModels = append(policyModels, &model.ApplicationPolicy{ + policyModels = append(policyModels, &model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), Name: policy.Name, Type: policy.Type, @@ -428,8 +428,8 @@ func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app return c.ds.BatchAdd(ctx, policyModels) } -func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, app *model.Application) (list []*model.ApplicationPolicy, err error) { - var policy = model.ApplicationPolicy{ +func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, app *model.ApplicationPlan) (list []*model.ApplicationPolicyPlan, err error) { + var policy = model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), } policys, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) @@ -437,7 +437,7 @@ func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, ap return nil, err } for _, policy := range policys { - pm := policy.(*model.ApplicationPolicy) + pm := policy.(*model.ApplicationPolicyPlan) list = append(list, pm) } return @@ -445,8 +445,8 @@ func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, ap // DetailPolicy detail app policy // TODO: Add status data about the policy. -func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) { - var policy = model.ApplicationPolicy{ +func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) (*apisv1.DetailPolicyResponse, error) { + var policy = model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), Name: policyName, } @@ -462,7 +462,7 @@ func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Ap // 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, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) { +func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.ApplicationPlan, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) { // step1: Render oam application version := utils.GenerateVersion("") oamApp, err := c.renderOAMApplication(ctx, app, req.WorkflowName, version) @@ -536,7 +536,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat }, nil } -func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMoel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) { +func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMoel *model.ApplicationPlan, reqWorkflowName, version string) (*v1beta1.Application, error) { var app = &v1beta1.Application{ TypeMeta: metav1.TypeMeta{ Kind: "Application", @@ -551,7 +551,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo }, }, } - var component = model.ApplicationComponent{ + var component = model.ApplicationComponentPlan{ AppPrimaryKey: appMoel.PrimaryKey(), } components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) @@ -562,7 +562,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return nil, bcode.ErrNoComponent } - var policy = model.ApplicationPolicy{ + var policy = model.ApplicationPolicyPlan{ AppPrimaryKey: appMoel.PrimaryKey(), } policies, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) @@ -571,7 +571,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } for _, entity := range components { - component := entity.(*model.ApplicationComponent) + component := entity.(*model.ApplicationComponentPlan) var traits []common.ApplicationTrait for _, trait := range component.Traits { aTrait := common.ApplicationTrait{ @@ -595,7 +595,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } for _, entity := range policies { - policy := entity.(*model.ApplicationPolicy) + policy := entity.(*model.ApplicationPolicyPlan) apolicy := v1beta1.AppPolicy{ Name: policy.Name, Type: policy.Type, @@ -608,7 +608,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo // Priority 1 uses the requested workflow as release plan. // Priority 2 uses the default workflow as release plan. - var workflow *model.Workflow + var workflow *model.WorkflowPlan if reqWorkflowName != "" { workflow, err = c.workflowUsecase.GetWorkflow(ctx, reqWorkflowName) if err != nil { @@ -642,8 +642,8 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return app, nil } -func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app *model.Application) *apisv1.ApplicationBase { - appBeas := &apisv1.ApplicationBase{ +func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app *model.ApplicationPlan) *apisv1.ApplicationPlanBase { + appBeas := &apisv1.ApplicationPlanBase{ Name: app.Name, Alias: app.Alias, Namespace: app.Namespace, @@ -653,7 +653,7 @@ func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app * Icon: app.Icon, Labels: app.Labels, } - var policy = model.ApplicationPolicy{ + var policy = model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), Type: string(EnvBindPolicy), } @@ -662,7 +662,7 @@ func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app * log.Logger.Errorf("query application env binding policy failure %s", err.Error()) } for _, policyEntity := range policys { - policy := policyEntity.(*model.ApplicationPolicy) + policy := policyEntity.(*model.ApplicationPolicyPlan) if policy.Properties != nil { var envBindingSpec v1alpha1.EnvBindingSpec if err := json.Unmarshal([]byte(policy.Properties.JSON()), &envBindingSpec); err != nil { @@ -691,7 +691,7 @@ func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app * } // DeleteApplication delete application -func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *model.Application) error { +func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *model.ApplicationPlan) error { // TODO: check app can be deleted // query all components to deleted @@ -710,14 +710,14 @@ func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *mod } for _, component := range components { - err := c.ds.Delete(ctx, &model.ApplicationComponent{AppPrimaryKey: app.PrimaryKey(), Name: component.Name}) + err := c.ds.Delete(ctx, &model.ApplicationComponentPlan{AppPrimaryKey: app.PrimaryKey(), Name: component.Name}) if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { log.Logger.Errorf("delete component %s in app %s failure %s", component.Name, app.Name, err.Error()) } } for _, policy := range policies { - err := c.ds.Delete(ctx, &model.ApplicationPolicy{AppPrimaryKey: app.PrimaryKey(), Name: policy.Name}) + err := c.ds.Delete(ctx, &model.ApplicationPolicyPlan{AppPrimaryKey: app.PrimaryKey(), Name: policy.Name}) if err != nil && errors.Is(err, datastore.ErrRecordNotExist) { log.Logger.Errorf("delete policy %s in app %s failure %s", policy.Name, app.Name, err.Error()) } @@ -726,8 +726,8 @@ func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *mod return c.ds.Delete(ctx, app) } -func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) { - componentModel := model.ApplicationComponent{ +func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.ApplicationPlan, com apisv1.CreateComponentPlanRequest) (*apisv1.ComponentPlanBase, error) { + componentModel := model.ApplicationComponentPlan{ AppPrimaryKey: app.PrimaryKey(), Description: com.Description, Labels: com.Labels, @@ -751,7 +751,7 @@ func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Ap log.Logger.Warnf("add component for app %s failure %s", app.PrimaryKey(), err.Error()) return nil, err } - return &apisv1.ComponentBase{ + return &apisv1.ComponentPlanBase{ Name: componentModel.Name, Description: componentModel.Description, Labels: componentModel.Labels, @@ -764,8 +764,8 @@ func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Ap }, nil } -func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.Application, componentName string) error { - var component = model.ApplicationComponent{ +func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.ApplicationPlan, componentName string) error { + var component = model.ApplicationComponentPlan{ AppPrimaryKey: app.PrimaryKey(), Name: componentName, } @@ -779,8 +779,8 @@ func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model return nil } -func (c *applicationUsecaseImpl) AddPolicy(ctx context.Context, app *model.Application, createpolicy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) { - policyModel := model.ApplicationPolicy{ +func (c *applicationUsecaseImpl) AddPolicy(ctx context.Context, app *model.ApplicationPlan, createpolicy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) { + policyModel := model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), Description: createpolicy.Description, // TODO: Get user information from ctx and assign a value. @@ -811,8 +811,8 @@ func (c *applicationUsecaseImpl) AddPolicy(ctx context.Context, app *model.Appli }, nil } -func (c *applicationUsecaseImpl) DeletePolicy(ctx context.Context, app *model.Application, policyName string) error { - var policy = model.ApplicationPolicy{ +func (c *applicationUsecaseImpl) DeletePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) error { + var policy = model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), Name: policyName, } @@ -826,8 +826,8 @@ func (c *applicationUsecaseImpl) DeletePolicy(ctx context.Context, app *model.Ap return nil } -func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policyUpdate apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) { - var policy = model.ApplicationPolicy{ +func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string, policyUpdate apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) { + var policy = model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), Name: policyName, } diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index b1e20e961..3270aec1f 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -50,7 +50,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test CreateApplication function", func() { By("test sample create") - req := v1.CreateApplicationRequest{ + req := v1.CreateApplicationPlanRequest{ Name: "test-app", Namespace: "test-app-namespace", Description: "this is a test app", @@ -66,7 +66,7 @@ var _ = Describe("Test application usecase function", func() { By("test with oam yaml config create") bs, err := ioutil.ReadFile("./testdata/example-app.yaml") Expect(err).Should(Succeed()) - req = v1.CreateApplicationRequest{ + req = v1.CreateApplicationPlanRequest{ Name: "test-app-sadasd", Namespace: "test-app-namespace", Description: "this is a test app", @@ -78,7 +78,7 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) - req = v1.CreateApplicationRequest{ + req = v1.CreateApplicationPlanRequest{ Name: "test-app-sadasd2", Namespace: "test-app-namespace", Description: "this is a test app", @@ -93,7 +93,7 @@ var _ = Describe("Test application usecase function", func() { bs, err = ioutil.ReadFile("./testdata/example-app-error.yaml") Expect(err).Should(Succeed()) - req = v1.CreateApplicationRequest{ + req = v1.CreateApplicationPlanRequest{ Name: "test-app-sadasd3", Namespace: "test-app-namespace", Description: "this is a test app", @@ -106,7 +106,7 @@ var _ = Describe("Test application usecase function", func() { Expect(equal).Should(BeTrue()) By("Test create app with env binding") - req = v1.CreateApplicationRequest{ + req = v1.CreateApplicationPlanRequest{ Name: "test-app-sadasd4", Namespace: "test-app-namespace", Description: "this is a test app", @@ -140,7 +140,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test ListApplications function", func() { - apps, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioOptions{}) + apps, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioPlanOptions{}) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(apps), 3)).Should(BeEmpty()) }) @@ -217,7 +217,7 @@ var _ = Describe("Test application usecase function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - base, err := appUsecase.AddComponent(context.TODO(), appModel, v1.CreateComponentRequest{ + base, err := appUsecase.AddComponent(context.TODO(), appModel, v1.CreateComponentPlanRequest{ Name: "test2", Description: "this is a test2 component", Labels: map[string]string{}, diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 7df6b40fa..9a228bac5 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -35,13 +35,13 @@ import ( // WorkflowUsecase workflow manage api type WorkflowUsecase interface { - ListApplicationWorkflow(ctx context.Context, app *model.Application, enable *bool) ([]*apisv1.WorkflowBase, error) - GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) - DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) - GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) + ListApplicationWorkflow(ctx context.Context, app *model.ApplicationPlan, enable *bool) ([]*apisv1.WorkflowPlanBase, error) + GetWorkflow(ctx context.Context, workflowName string) (*model.WorkflowPlan, error) + DetailWorkflow(ctx context.Context, workflow *model.WorkflowPlan) (*apisv1.DetailWorkflowPlanResponse, error) + GetApplicationDefaultWorkflow(ctx context.Context, app *model.ApplicationPlan) (*model.WorkflowPlan, error) DeleteWorkflow(ctx context.Context, workflowName string) error - CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) - UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + CreateWorkflow(ctx context.Context, app *model.ApplicationPlan, req apisv1.CreateWorkflowPlanRequest) (*apisv1.DetailWorkflowPlanResponse, error) + UpdateWorkflow(ctx context.Context, workflow *model.WorkflowPlan, req apisv1.UpdateWorkflowPlanRequest) (*apisv1.DetailWorkflowPlanResponse, error) ListWorkflowRecords(ctx context.Context, workflowName string, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) DetailWorkflowRecord(ctx context.Context, workflowName, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) } @@ -57,7 +57,7 @@ type workflowUsecaseImpl struct { // DeleteWorkflow delete application workflow func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, workflowName string) error { - var workflow = &model.Workflow{ + var workflow = &model.WorkflowPlan{ Name: workflowName, } if err := w.ds.Delete(ctx, workflow); err != nil { @@ -69,7 +69,7 @@ func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, workflowName s return nil } -func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { +func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.ApplicationPlan, req apisv1.CreateWorkflowPlanRequest) (*apisv1.DetailWorkflowPlanResponse, error) { var steps []model.WorkflowStep for _, step := range req.Steps { properties, err := model.NewJSONStructByString(step.Properties) @@ -86,7 +86,7 @@ func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.App }) } // It is allowed to set multiple workflows as default, and only one takes effect. - var workflow = model.Workflow{ + var workflow = model.WorkflowPlan{ Steps: steps, Name: req.Name, Enable: req.Enable, @@ -100,7 +100,7 @@ func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.App return w.DetailWorkflow(ctx, &workflow) } -func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { +func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *model.WorkflowPlan, req apisv1.UpdateWorkflowPlanRequest) (*apisv1.DetailWorkflowPlanResponse, error) { var steps []model.WorkflowStep for _, step := range req.Steps { properties, err := model.NewJSONStructByString(step.Properties) @@ -128,7 +128,7 @@ func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *mode } // DetailWorkflow detail workflow -func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) { +func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.WorkflowPlan) (*apisv1.DetailWorkflowPlanResponse, error) { var steps []apisv1.WorkflowStep for _, step := range workflow.Steps { apiStep := apisv1.WorkflowStep{ @@ -143,8 +143,8 @@ func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *mode } steps = append(steps, apiStep) } - return &apisv1.DetailWorkflowResponse{ - WorkflowBase: apisv1.WorkflowBase{ + return &apisv1.DetailWorkflowPlanResponse{ + WorkflowPlanBase: apisv1.WorkflowPlanBase{ Name: workflow.Name, Description: workflow.Description, Enable: workflow.Enable, @@ -157,8 +157,8 @@ func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *mode } // GetWorkflow get workflow model -func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) { - var workflow = model.Workflow{ +func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName string) (*model.WorkflowPlan, error) { + var workflow = model.WorkflowPlan{ Name: workflowName, } if err := w.ds.Get(ctx, &workflow); err != nil { @@ -168,8 +168,8 @@ func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName stri } // ListApplicationWorkflow list application workflows -func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app *model.Application, enable *bool) ([]*apisv1.WorkflowBase, error) { - var workflow = model.Workflow{ +func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app *model.ApplicationPlan, enable *bool) ([]*apisv1.WorkflowPlanBase, error) { + var workflow = model.WorkflowPlan{ AppPrimaryKey: app.PrimaryKey(), } if enable != nil { @@ -179,10 +179,10 @@ func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app * if err != nil { return nil, err } - var list []*apisv1.WorkflowBase + var list []*apisv1.WorkflowPlanBase for _, workflow := range workflows { - wm := workflow.(*model.Workflow) - list = append(list, &apisv1.WorkflowBase{ + wm := workflow.(*model.WorkflowPlan) + list = append(list, &apisv1.WorkflowPlanBase{ Name: wm.Name, Description: wm.Description, Enable: wm.Enable, @@ -195,8 +195,8 @@ func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app * } // GetApplicationDefaultWorkflow get application default workflow -func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) { - var workflow = model.Workflow{ +func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, app *model.ApplicationPlan) (*model.WorkflowPlan, error) { + var workflow = model.WorkflowPlan{ AppPrimaryKey: app.PrimaryKey(), Default: true, } @@ -205,7 +205,7 @@ func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, return nil, err } if len(workflows) > 0 { - return workflows[0].(*model.Workflow), nil + return workflows[0].(*model.WorkflowPlan), nil } return nil, bcode.ErrWorkflowNoDefault } diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 37c36a491..9477d2f77 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -40,22 +40,22 @@ var _ = Describe("Test workflow usecase functions", func() { workflowUsecase = &workflowUsecaseImpl{ds: ds} }) It("Test CreateWorkflow function", func() { - req := apisv1.CreateWorkflowRequest{ + req := apisv1.CreateWorkflowPlanRequest{ Name: "test-workflow-1", Description: "this is a workflow", } - base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.ApplicationPlan{ Name: "test-app", }, req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) - req = apisv1.CreateWorkflowRequest{ + req = apisv1.CreateWorkflowPlanRequest{ Name: "test-workflow-2", Description: "this is test workflow", Default: true, } - base, err = workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + base, err = workflowUsecase.CreateWorkflow(context.TODO(), &model.ApplicationPlan{ Name: "test-app", }, req) Expect(err).Should(BeNil()) @@ -63,7 +63,7 @@ var _ = Describe("Test workflow usecase functions", func() { }) It("Test GetApplicationDefaultWorkflow function", func() { - workflow, err := workflowUsecase.GetApplicationDefaultWorkflow(context.TODO(), &model.Application{ + workflow, err := workflowUsecase.GetApplicationDefaultWorkflow(context.TODO(), &model.ApplicationPlan{ Name: "test-app", }) Expect(err).Should(BeNil()) diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 77d06273b..b23f15da5 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -42,7 +42,7 @@ func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase) Web func (c *applicationWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) - ws.Path(versionPrefix+"/applications"). + ws.Path(versionPrefix+"/applicationplans"). Consumes(restful.MIME_XML, restful.MIME_JSON). Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for application manage") @@ -55,17 +55,17 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). Param(ws.QueryParameter("namespace", "Namespace-based search").DataType("string")). Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")). - Returns(200, "", apis.ListApplicationResponse{}). + Returns(200, "", apis.ListApplicationPlanResponse{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ListApplicationResponse{})) + Writes(apis.ListApplicationPlanResponse{})) ws.Route(ws.POST("/").To(c.createApplication). Doc("create one application"). Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateApplicationRequest{}). - Returns(200, "", apis.ApplicationBase{}). + Reads(apis.CreateApplicationPlanRequest{}). + Returns(200, "", apis.ApplicationPlanBase{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ApplicationBase{})) + Writes(apis.ApplicationPlanBase{})) ws.Route(ws.DELETE("/{name}").To(c.deleteApplication). Doc("delete one application"). @@ -81,9 +81,9 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). - Returns(200, "", apis.DetailApplicationResponse{}). + Returns(200, "", apis.DetailApplicationPlanResponse{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.DetailApplicationResponse{})) + Writes(apis.DetailApplicationPlanResponse{})) ws.Route(ws.POST("/{name}/template").To(c.publishApplicationTemplate). Doc("create one application template"). @@ -104,34 +104,34 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationDeployResponse{})) - ws.Route(ws.GET("/{name}/components").To(c.listApplicationComponents). - Doc("gets the component topology of the application"). + ws.Route(ws.GET("/{name}/componentplans").To(c.listApplicationComponents). + Doc("gets the componentplan topology of the application"). Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). - Param(ws.PathParameter("cluster", "list components that deployed in define cluster").DataType("string")). + Param(ws.QueryParameter("envName", "list components that deployed in define env").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). - Returns(200, "", apis.ComponentListResponse{}). + Returns(200, "", apis.ComponentPlanListResponse{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ComponentListResponse{})) + Writes(apis.ComponentPlanListResponse{})) - ws.Route(ws.POST("/{name}/components").To(c.createComponent). - Doc("create component for application"). + ws.Route(ws.POST("/{name}/componentplans").To(c.createComponent). + Doc("create component plan for application plan"). Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateComponentRequest{}). - Returns(200, "", apis.ComponentBase{}). + Reads(apis.CreateComponentPlanRequest{}). + Returns(200, "", apis.ComponentPlanBase{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ComponentBase{})) + Writes(apis.ComponentPlanBase{})) - ws.Route(ws.GET("/{name}/components/{componentName}").To(c.detailComponent). - Doc("detail component for application"). + ws.Route(ws.GET("/{name}/componentplans/{componentName}").To(c.detailComponent). + Doc("detail component plan for application plan"). Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). - Returns(200, "", apis.DetailComponentResponse{}). + Returns(200, "", apis.DetailComponentPlanResponse{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.DetailComponentResponse{})) + Writes(apis.DetailComponentPlanResponse{})) ws.Route(ws.GET("/{name}/policies").To(c.listApplicationPolicies). Doc("list policy for application"). @@ -197,7 +197,7 @@ func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restfu func (c *applicationWebService) createApplication(req *restful.Request, res *restful.Response) { // Verify the validity of parameters - var createReq apis.CreateApplicationRequest + var createReq apis.CreateApplicationPlanRequest if err := req.ReadEntity(&createReq); err != nil { bcode.ReturnError(req, res, err) return @@ -222,7 +222,7 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res } func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { - apps, err := c.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioOptions{ + apps, err := c.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioPlanOptions{ Namespace: req.QueryParameter("namespace"), Cluster: req.QueryParameter("cluster"), Query: req.QueryParameter("query"), @@ -231,14 +231,14 @@ func (c *applicationWebService) listApplications(req *restful.Request, res *rest bcode.ReturnError(req, res, err) return } - if err := res.WriteEntity(apis.ListApplicationResponse{Applications: apps}); err != nil { + if err := res.WriteEntity(apis.ListApplicationPlanResponse{ApplicationPlans: apps}); err != nil { bcode.ReturnError(req, res, err) return } } func (c *applicationWebService) detailApplication(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) detail, err := c.applicationUsecase.DetailApplication(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) @@ -251,7 +251,7 @@ func (c *applicationWebService) detailApplication(req *restful.Request, res *res } func (c *applicationWebService) publishApplicationTemplate(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) base, err := c.applicationUsecase.PublishApplicationTemplate(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) @@ -265,7 +265,7 @@ func (c *applicationWebService) publishApplicationTemplate(req *restful.Request, // deployApplication TODO: return event model func (c *applicationWebService) deployApplication(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) // Verify the validity of parameters var createReq apis.ApplicationDeployRequest if err := req.ReadEntity(&createReq); err != nil { @@ -288,7 +288,7 @@ func (c *applicationWebService) deployApplication(req *restful.Request, res *res } func (c *applicationWebService) deleteApplication(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) err := c.applicationUsecase.DeleteApplication(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) @@ -301,22 +301,22 @@ func (c *applicationWebService) deleteApplication(req *restful.Request, res *res } func (c *applicationWebService) listApplicationComponents(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) components, err := c.applicationUsecase.ListComponents(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) return } - if err := res.WriteEntity(apis.ComponentListResponse{Components: components}); err != nil { + if err := res.WriteEntity(apis.ComponentPlanListResponse{ComponentPlans: components}); err != nil { bcode.ReturnError(req, res, err) return } } func (c *applicationWebService) createComponent(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) // Verify the validity of parameters - var createReq apis.CreateComponentRequest + var createReq apis.CreateComponentPlanRequest if err := req.ReadEntity(&createReq); err != nil { bcode.ReturnError(req, res, err) return @@ -337,7 +337,7 @@ func (c *applicationWebService) createComponent(req *restful.Request, res *restf } func (c *applicationWebService) detailComponent(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) detail, err := c.applicationUsecase.DetailComponent(req.Request.Context(), app, req.PathParameter("componentName")) if err != nil { bcode.ReturnError(req, res, err) @@ -350,7 +350,7 @@ func (c *applicationWebService) detailComponent(req *restful.Request, res *restf } func (c *applicationWebService) createApplicationPolicy(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) // Verify the validity of parameters var createReq apis.CreatePolicyRequest if err := req.ReadEntity(&createReq); err != nil { @@ -373,7 +373,7 @@ func (c *applicationWebService) createApplicationPolicy(req *restful.Request, re } func (c *applicationWebService) listApplicationPolicies(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) policies, err := c.applicationUsecase.ListPolicies(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) @@ -386,7 +386,7 @@ func (c *applicationWebService) listApplicationPolicies(req *restful.Request, re } func (c *applicationWebService) detailApplicationPolicy(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) detail, err := c.applicationUsecase.DetailPolicy(req.Request.Context(), app, req.PathParameter("policyName")) if err != nil { bcode.ReturnError(req, res, err) @@ -399,7 +399,7 @@ func (c *applicationWebService) detailApplicationPolicy(req *restful.Request, re } func (c *applicationWebService) deleteApplicationPolicy(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) err := c.applicationUsecase.DeletePolicy(req.Request.Context(), app, req.PathParameter("policyName")) if err != nil { bcode.ReturnError(req, res, err) @@ -412,7 +412,7 @@ func (c *applicationWebService) deleteApplicationPolicy(req *restful.Request, re } func (c *applicationWebService) updateApplicationPolicy(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) // Verify the validity of parameters var updateReq apis.UpdatePolicyRequest if err := req.ReadEntity(&updateReq); err != nil { diff --git a/pkg/apiserver/rest/webservice/validate_test.go b/pkg/apiserver/rest/webservice/validate_test.go index 7b71fab51..1113e6b5e 100644 --- a/pkg/apiserver/rest/webservice/validate_test.go +++ b/pkg/apiserver/rest/webservice/validate_test.go @@ -27,26 +27,26 @@ import ( 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{ + var app0 = apisv1.CreateApplicationPlanRequest{ Name: "a", Namespace: "namespace", } err := validate.Struct(&app0) Expect(err).ShouldNot(BeNil()) - var app1 = apisv1.CreateApplicationRequest{ + var app1 = apisv1.CreateApplicationPlanRequest{ Name: "Asdasd", Namespace: "namespace", } err = validate.Struct(&app1) Expect(err).ShouldNot(BeNil()) - var app2 = apisv1.CreateApplicationRequest{ + var app2 = apisv1.CreateApplicationPlanRequest{ Name: "asdasd asdasd ++", Namespace: "namespace", } err = validate.Struct(&app2) Expect(err).ShouldNot(BeNil()) - var app3 = apisv1.CreateApplicationRequest{ + var app3 = apisv1.CreateApplicationPlanRequest{ Name: "asdasd", Namespace: "namespace", } diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 1f9e4718f..30bcedd26 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -46,7 +46,7 @@ type workflowWebService struct { func (w *workflowWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) - ws.Path(versionPrefix+"/workflows"). + ws.Path(versionPrefix+"/workflowplans"). Consumes(restful.MIME_XML, restful.MIME_JSON). Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for cluster manage") @@ -58,31 +58,31 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Param(ws.QueryParameter("appName", "identifier of the application.").DataType("string")). Param(ws.QueryParameter("enable", "query based on enable status").DataType("boolean")). Metadata(restfulspec.KeyOpenAPITags, tags). - Writes(apis.ListWorkflowResponse{}).Do(returns200, returns500)) + Writes(apis.ListWorkflowPlanResponse{}).Do(returns200, returns500)) ws.Route(ws.POST("/").To(w.createApplicationWorkflow). Doc("create application workflow"). Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateWorkflowRequest{}). - Returns(200, "create success", apis.DetailWorkflowResponse{}). + Reads(apis.CreateWorkflowPlanRequest{}). + Returns(200, "create success", apis.DetailWorkflowPlanResponse{}). Returns(400, "create failure", bcode.Bcode{}). - Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + Writes(apis.DetailWorkflowPlanResponse{}).Do(returns200, returns500)) ws.Route(ws.GET("/{name}").To(w.detailWorkflow). Doc("detail application workflow"). Param(ws.PathParameter("name", "identifier of the workflow.").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(w.workflowCheckFilter). - Returns(200, "create success", apis.DetailWorkflowResponse{}). - Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + Returns(200, "create success", apis.DetailWorkflowPlanResponse{}). + Writes(apis.DetailWorkflowPlanResponse{}).Do(returns200, returns500)) ws.Route(ws.PUT("/{name}").To(w.updateWorkflow). Doc("update application workflow config"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(w.workflowCheckFilter). Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Reads(apis.UpdateWorkflowRequest{}). - Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + Reads(apis.UpdateWorkflowPlanRequest{}). + Writes(apis.DetailWorkflowPlanResponse{}).Do(returns200, returns500)) ws.Route(ws.DELETE("/{name}").To(w.deleteWorkflow). Doc("deletet workflow"). @@ -140,7 +140,7 @@ func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res bcode.ReturnError(req, res, err) return } - if err := res.WriteEntity(apis.ListWorkflowResponse{Workflows: workflows}); err != nil { + if err := res.WriteEntity(apis.ListWorkflowPlanResponse{WorkflowPlans: workflows}); err != nil { bcode.ReturnError(req, res, err) return } @@ -148,7 +148,7 @@ func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res *restful.Response) { // Verify the validity of parameters - var createReq apis.CreateWorkflowRequest + var createReq apis.CreateWorkflowPlanRequest if err := req.ReadEntity(&createReq); err != nil { bcode.ReturnError(req, res, err) return @@ -178,7 +178,7 @@ func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res } func (w *workflowWebService) detailWorkflow(req *restful.Request, res *restful.Response) { - workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.WorkflowPlan) detail, err := w.workflowUsecase.DetailWorkflow(req.Request.Context(), workflow) if err != nil { bcode.ReturnError(req, res, err) @@ -191,9 +191,9 @@ func (w *workflowWebService) detailWorkflow(req *restful.Request, res *restful.R } func (w *workflowWebService) updateWorkflow(req *restful.Request, res *restful.Response) { - workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.WorkflowPlan) // Verify the validity of parameters - var updateReq apis.UpdateWorkflowRequest + var updateReq apis.UpdateWorkflowPlanRequest if err := req.ReadEntity(&updateReq); err != nil { bcode.ReturnError(req, res, err) return diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index ab335074a..5bf6b078b 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -36,7 +36,7 @@ import ( var _ = Describe("Test application rest api", func() { It("Test create app", func() { defer GinkgoRecover() - var req = apisv1.CreateApplicationRequest{ + var req = apisv1.CreateApplicationPlanRequest{ Name: "test-app-sadasd", Namespace: "test-app-namespace", Description: "this is a test app", @@ -48,13 +48,13 @@ var _ = Describe("Test application rest api", func() { } 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)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans", "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 + var appBase apisv1.ApplicationPlanBase err = json.NewDecoder(res.Body).Decode(&appBase) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(appBase.Name, req.Name)).Should(BeEmpty()) @@ -66,7 +66,7 @@ var _ = Describe("Test application rest api", func() { It("Test delete app", func() { defer GinkgoRecover() - req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd", nil) + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd", nil) Expect(err).ShouldNot(HaveOccurred()) res, err := http.DefaultClient.Do(req) Expect(err).ShouldNot(HaveOccurred()) @@ -78,7 +78,7 @@ var _ = Describe("Test application rest api", func() { defer GinkgoRecover() bs, err := ioutil.ReadFile("./testdata/example-app.yaml") Expect(err).Should(Succeed()) - var req = apisv1.CreateApplicationRequest{ + var req = apisv1.CreateApplicationPlanRequest{ Name: "test-app-sadasd", Namespace: "test-app-namespace", Description: "this is a test app", @@ -88,13 +88,13 @@ var _ = Describe("Test application rest api", func() { } 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)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans", "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 + var appBase apisv1.ApplicationPlanBase err = json.NewDecoder(res.Body).Decode(&appBase) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(appBase.Name, req.Name)).Should(BeEmpty()) @@ -105,21 +105,21 @@ var _ = Describe("Test application rest api", func() { It("Test list components", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/componentplans") 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 components apisv1.ComponentListResponse + var components apisv1.ComponentPlanListResponse err = json.NewDecoder(res.Body).Decode(&components) Expect(err).ShouldNot(HaveOccurred()) - Expect(cmp.Diff(len(components.Components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(len(components.ComponentPlans), 2)).Should(BeEmpty()) }) It("Test list policies", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies") Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -133,7 +133,7 @@ var _ = Describe("Test application rest api", func() { It("Test get workflow", func() { // defer GinkgoRecover() - // res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies") + // res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies") // Expect(err).ShouldNot(HaveOccurred()) // Expect(res).ShouldNot(BeNil()) // Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -147,13 +147,13 @@ var _ = Describe("Test application rest api", func() { It("Test detail application", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd") 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 detail apisv1.DetailApplicationResponse + var detail apisv1.DetailApplicationPlanResponse err = json.NewDecoder(res.Body).Decode(&detail) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) @@ -168,7 +168,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/deploy", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/deploy", "application/json", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -188,7 +188,7 @@ var _ = Describe("Test application rest api", func() { It("Test create component", func() { defer GinkgoRecover() - var req = apisv1.CreateComponentRequest{ + var req = apisv1.CreateComponentPlanRequest{ Name: "test2", Description: "this is a test2 component", Labels: map[string]string{}, @@ -198,13 +198,13 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/componentplans", "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 response apisv1.ComponentBase + var response apisv1.ComponentPlanBase err = json.NewDecoder(res.Body).Decode(&response) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(response.ComponentType, "worker")).Should(BeEmpty()) @@ -212,13 +212,13 @@ var _ = Describe("Test application rest api", func() { It("Test detail component", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/componentplans/test2") 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 response apisv1.DetailComponentResponse + var response apisv1.DetailComponentPlanResponse err = json.NewDecoder(res.Body).Decode(&response) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(len(response.DependsOn), 1)).Should(BeEmpty()) @@ -233,7 +233,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 400)).Should(BeEmpty()) @@ -245,7 +245,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte2, err := json.Marshal(req2) Expect(err).ShouldNot(HaveOccurred()) - res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte2)) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte2)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -260,7 +260,7 @@ var _ = Describe("Test application rest api", func() { It("Test detail application policy", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies/test2") Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -280,7 +280,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte2, err := json.Marshal(req2) Expect(err).ShouldNot(HaveOccurred()) - req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2", bytes.NewBuffer(bodyByte2)) + req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies/test2", bytes.NewBuffer(bodyByte2)) Expect(err).ShouldNot(HaveOccurred()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) @@ -298,7 +298,7 @@ var _ = Describe("Test application rest api", func() { It("Test delete application policy", func() { defer GinkgoRecover() - req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2", nil) + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies/test2", nil) Expect(err).ShouldNot(HaveOccurred()) res, err := http.DefaultClient.Do(req) Expect(err).ShouldNot(HaveOccurred()) From e8969e4d17695446a324d5826c4488dcb5e22433 Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Thu, 4 Nov 2021 16:38:34 +0800 Subject: [PATCH 15/59] Feat: add definition API (#2602) * Feat: add definition API * fix struct * fix e2e * optimize the code * fix return * fix const --- pkg/apiserver/rest/apis/v1/types.go | 28 +++++- pkg/apiserver/rest/usecase/definition.go | 92 +++++++++++++++---- pkg/apiserver/rest/usecase/definition_test.go | 86 +++++++++++++++-- .../usecase/testdata/applyapplication-sd.yaml | 20 ++++ .../rest/usecase/testdata/myingress-td.yaml | 65 +++++++++++++ .../rest/usecase/testdata/webserver-cd.yaml | 2 +- .../rest/webservice/component_definition.go | 67 -------------- pkg/apiserver/rest/webservice/definition.go | 88 ++++++++++++++++++ pkg/apiserver/rest/webservice/webservice.go | 2 +- test/e2e-apiserver-test/definition_test.go | 9 +- 10 files changed, 358 insertions(+), 101 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/testdata/applyapplication-sd.yaml create mode 100644 pkg/apiserver/rest/usecase/testdata/myingress-td.yaml delete mode 100644 pkg/apiserver/rest/webservice/component_definition.go create mode 100644 pkg/apiserver/rest/webservice/definition.go diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index fb4c0e24c..ea24469d2 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -384,13 +384,31 @@ type NamespaceDetailResponse struct { NamespaceBase } -// ListComponentDefinitionResponse list component dedinition response model -type ListComponentDefinitionResponse struct { - ComponentDefinitions []*ComponentDefinitionBase `json:"componentDefinitions"` +// ListDefinitionResponse list definition response model +type ListDefinitionResponse struct { + Definitions []*DefinitionBase `json:"definitions"` } -// ComponentDefinitionBase component definition base model -type ComponentDefinitionBase struct { +// DetailDefinitionResponse get definition detail +type DetailDefinitionResponse struct { + Schema *DefinitionSchema `json:"schema"` +} + +type DefinitionSchema struct { + Properties map[string]DefinitionProperties `json:"properties"` + Required []string `json:"required"` + Type string `json:"type"` +} + +type DefinitionProperties struct { + Default string `json:"default"` + Description string `json:"description"` + Title string `json:"title"` + Type string `json:"type"` +} + +// DefinitionBase is the definition base model +type DefinitionBase struct { Name string `json:"name"` Description string `json:"description"` Icon string `json:"icon"` diff --git a/pkg/apiserver/rest/usecase/definition.go b/pkg/apiserver/rest/usecase/definition.go index 770d4b072..f24076b01 100644 --- a/pkg/apiserver/rest/usecase/definition.go +++ b/pkg/apiserver/rest/usecase/definition.go @@ -18,11 +18,15 @@ package usecase import ( "context" + "encoding/json" + "fmt" "time" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/log" @@ -32,8 +36,10 @@ import ( // DefinitionUsecase definition usecase, Implement the management of ComponentDefinition、TraitDefinition and WorkflowStepDefinition. type DefinitionUsecase interface { - // ListComponentDefinitions list component definition base info - ListComponentDefinitions(ctx context.Context, envName string) ([]*apisv1.ComponentDefinitionBase, error) + // ListDefinitions list definition base info + ListDefinitions(ctx context.Context, envName, defType string) ([]*apisv1.DefinitionBase, error) + // DetailDefinition get definition detail + DetailDefinition(ctx context.Context, name, defType string) (*apisv1.DetailDefinitionResponse, error) } type definitionUsecaseImpl struct { @@ -41,6 +47,13 @@ type definitionUsecaseImpl struct { caches map[string]*utils.MemoryCache } +const ( + definitionAPIVersion = "core.oam.dev/v1beta1" + kindComponentDefinition = "ComponentDefinition" + kindTraitDefinition = "TraitDefinition" + kindWorkflowStepDefinition = "WorkflowStepDefinition" +) + // NewDefinitionUsecase new definition usecase func NewDefinitionUsecase() DefinitionUsecase { kubecli, err := clients.GetKubeClient() @@ -50,23 +63,68 @@ func NewDefinitionUsecase() DefinitionUsecase { return &definitionUsecaseImpl{kubeClient: kubecli, caches: make(map[string]*utils.MemoryCache)} } -func (d *definitionUsecaseImpl) ListComponentDefinitions(ctx context.Context, envName string) ([]*apisv1.ComponentDefinitionBase, error) { - // check cache - if mc := d.caches["componentDefinitions"]; mc != nil && !mc.IsExpired() { - return mc.GetData().([]*apisv1.ComponentDefinitionBase), nil +func (d *definitionUsecaseImpl) ListDefinitions(ctx context.Context, envName, defType string) ([]*apisv1.DefinitionBase, error) { + defs := &unstructured.UnstructuredList{} + switch defType { + case "component": + defs.SetAPIVersion(definitionAPIVersion) + defs.SetKind(kindComponentDefinition) + return d.listDefinitions(ctx, defs, kindComponentDefinition) + + case "trait": + defs.SetAPIVersion(definitionAPIVersion) + defs.SetKind(kindTraitDefinition) + return d.listDefinitions(ctx, defs, kindTraitDefinition) + + case "workflowstep": + defs.SetAPIVersion(definitionAPIVersion) + defs.SetKind(kindWorkflowStepDefinition) + return d.listDefinitions(ctx, defs, kindWorkflowStepDefinition) + + default: + return nil, fmt.Errorf("invalid definition type") } - var componentDefinitions v1beta1.ComponentDefinitionList - if err := d.kubeClient.List(ctx, &componentDefinitions, &client.ListOptions{}); err != nil { +} + +func (d *definitionUsecaseImpl) listDefinitions(ctx context.Context, list *unstructured.UnstructuredList, cache string) ([]*apisv1.DefinitionBase, error) { + if mc := d.caches[cache]; mc != nil && !mc.IsExpired() { + return mc.GetData().([]*apisv1.DefinitionBase), nil + } + if err := d.kubeClient.List(ctx, list, &client.ListOptions{ + Namespace: types.DefaultKubeVelaNS, + }); err != nil { return nil, err } - var cdb []*apisv1.ComponentDefinitionBase - for _, cd := range componentDefinitions.Items { - cdb = append(cdb, &apisv1.ComponentDefinitionBase{ - Name: cd.Name, - Description: cd.Annotations[types.AnnDescription], + var defs []*apisv1.DefinitionBase + for _, def := range list.Items { + defs = append(defs, &apisv1.DefinitionBase{ + Name: def.GetName(), + Description: def.GetAnnotations()[types.AnnDescription], }) } - // set cache - d.caches["componentDefinitions"] = utils.NewMemoryCache(cdb, time.Minute*3) - return cdb, nil + d.caches[cache] = utils.NewMemoryCache(defs, time.Minute*3) + return defs, nil +} + +// DetailDefinition get definition detail +func (d *definitionUsecaseImpl) DetailDefinition(ctx context.Context, name, defType string) (*apisv1.DetailDefinitionResponse, error) { + var cm v1.ConfigMap + if err := d.kubeClient.Get(ctx, k8stypes.NamespacedName{ + Namespace: types.DefaultKubeVelaNS, + Name: fmt.Sprintf("%s-schema-%s", defType, name), + }, &cm); err != nil { + return nil, err + } + + data, ok := cm.Data["openapi-v3-json-schema"] + if !ok { + return nil, fmt.Errorf("failed to get definition schema") + } + schema := &apisv1.DefinitionSchema{} + if err := json.Unmarshal([]byte(data), schema); err != nil { + return nil, err + } + return &apisv1.DetailDefinitionResponse{ + Schema: schema, + }, nil } diff --git a/pkg/apiserver/rest/usecase/definition_test.go b/pkg/apiserver/rest/usecase/definition_test.go index f74c0c8e2..37756fe92 100644 --- a/pkg/apiserver/rest/usecase/definition_test.go +++ b/pkg/apiserver/rest/usecase/definition_test.go @@ -23,31 +23,105 @@ import ( "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/oam/util" ) var _ = Describe("Test namespace usecase functions", func() { var ( definitionUsecase *definitionUsecaseImpl ) + BeforeEach(func() { definitionUsecase = &definitionUsecaseImpl{kubeClient: k8sClient, caches: make(map[string]*utils.MemoryCache)} + err := k8sClient.Create(context.Background(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vela-system", + }, + }) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) }) - It("Test ListComponentDefinitions function", func() { - bs, err := ioutil.ReadFile("./testdata/webserver-cd.yaml") + It("Test ListDefinitions function", func() { + By("List component definitions") + webserver, err := ioutil.ReadFile("./testdata/webserver-cd.yaml") Expect(err).Should(Succeed()) - var test v1beta1.ComponentDefinition - err = yaml.Unmarshal(bs, &test) + var cd v1beta1.ComponentDefinition + err = yaml.Unmarshal(webserver, &cd) Expect(err).Should(Succeed()) - err = k8sClient.Create(context.Background(), &test) + err = k8sClient.Create(context.Background(), &cd) Expect(err).Should(Succeed()) - components, err := definitionUsecase.ListComponentDefinitions(context.TODO(), "") + components, err := definitionUsecase.ListDefinitions(context.TODO(), "", "component") Expect(err).Should(BeNil()) Expect(cmp.Diff(len(components), 1)).Should(BeEmpty()) Expect(cmp.Diff(components[0].Name, "webservice-test")).Should(BeEmpty()) Expect(components[0].Description).ShouldNot(BeEmpty()) + + By("List trait definitions") + myingress, err := ioutil.ReadFile("./testdata/myingress-td.yaml") + Expect(err).Should(Succeed()) + var td v1beta1.TraitDefinition + err = yaml.Unmarshal(myingress, &td) + Expect(err).Should(Succeed()) + err = k8sClient.Create(context.Background(), &td) + Expect(err).Should(Succeed()) + traits, err := definitionUsecase.ListDefinitions(context.TODO(), "", "trait") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(traits), 1)).Should(BeEmpty()) + Expect(cmp.Diff(traits[0].Name, "myingress")).Should(BeEmpty()) + Expect(traits[0].Description).ShouldNot(BeEmpty()) + + By("List workflow step definitions") + step, err := ioutil.ReadFile("./testdata/applyapplication-sd.yaml") + Expect(err).Should(Succeed()) + var sd v1beta1.WorkflowStepDefinition + err = yaml.Unmarshal(step, &sd) + Expect(err).Should(Succeed()) + err = k8sClient.Create(context.Background(), &sd) + Expect(err).Should(Succeed()) + wfstep, err := definitionUsecase.ListDefinitions(context.TODO(), "", "workflowstep") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(wfstep), 1)).Should(BeEmpty()) + Expect(cmp.Diff(wfstep[0].Name, "apply-application")).Should(BeEmpty()) + Expect(wfstep[0].Description).ShouldNot(BeEmpty()) + }) + + It("Test DetailDefinition function", func() { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "workflowstep-schema-apply-object", + Namespace: "vela-system", + }, + Data: map[string]string{ + "openapi-v3-json-schema": `{"properties":{"cluster":{"default":"","description":"Specify the cluster of the object","title":"cluster","type":"string"},"value":{"description":"Specify the value of the object","title":"value","type":"object"}},"required":["value","cluster"],"type":"object"}`, + }, + } + err := k8sClient.Create(context.Background(), cm) + Expect(err).Should(Succeed()) + schema, err := definitionUsecase.DetailDefinition(context.TODO(), "apply-object", "workflowstep") + Expect(err).Should(BeNil()) + Expect(schema.Schema).Should(Equal(&v1.DefinitionSchema{ + Properties: map[string]v1.DefinitionProperties{ + "value": { + Default: "", + Description: "Specify the value of the object", + Title: "value", + Type: "object", + }, + "cluster": { + Default: "", + Description: "Specify the cluster of the object", + Title: "cluster", + Type: "string", + }, + }, + Required: []string{"value", "cluster"}, + Type: "object", + })) }) }) diff --git a/pkg/apiserver/rest/usecase/testdata/applyapplication-sd.yaml b/pkg/apiserver/rest/usecase/testdata/applyapplication-sd.yaml new file mode 100644 index 000000000..a7af45553 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/applyapplication-sd.yaml @@ -0,0 +1,20 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/apply-application.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Apply application for your workflow steps + name: apply-application + namespace: vela-system +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + // apply application + output: op.#ApplyApplication & {} + diff --git a/pkg/apiserver/rest/usecase/testdata/myingress-td.yaml b/pkg/apiserver/rest/usecase/testdata/myingress-td.yaml new file mode 100644 index 000000000..9345a04a3 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/myingress-td.yaml @@ -0,0 +1,65 @@ +apiVersion: core.oam.dev/v1beta1 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: Enable public web traffic for the component. + name: myingress + namespace: vela-system +spec: + appliesToWorkloads: + - "*" + podDisruptive: false + schematic: + cue: + template: | + import ( + kubev1 "kube/v1" + network "kube/networking.k8s.io/v1beta1" + ) + + parameter: { + domain: string + http: [string]: int + } + + outputs: { + service: kubev1.#Service + ingress: network.#Ingress + } + + // trait template can have multiple outputs in one trait + outputs: service: { + metadata: + name: context.name + spec: { + selector: + "app.oam.dev/component": context.name + ports: [ + for k, v in parameter.http { + port: v + targetPort: v + }, + ] + } + } + + outputs: ingress: { + metadata: + name: context.name + spec: { + rules: [{ + host: parameter.domain + http: { + paths: [ + for k, v in parameter.http { + path: k + backend: { + serviceName: context.name + servicePort: v + } + }, + ] + } + }] + } + } diff --git a/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml b/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml index a1b7970ae..b407974a8 100644 --- a/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml +++ b/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml @@ -6,7 +6,7 @@ metadata: annotations: definition.oam.dev/description: Describes long-running, scalable, containerized services that have a stable network endpoint to receive external network traffic from customers. name: webservice-test - namespace: default + namespace: vela-system spec: schematic: cue: diff --git a/pkg/apiserver/rest/webservice/component_definition.go b/pkg/apiserver/rest/webservice/component_definition.go deleted file mode 100644 index b5d99d35d..000000000 --- a/pkg/apiserver/rest/webservice/component_definition.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -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" - "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" - "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" -) - -type componentDefinitionWebservice struct { - definitionUsecase usecase.DefinitionUsecase -} - -func (c *componentDefinitionWebservice) GetWebService() *restful.WebService { - ws := new(restful.WebService) - ws.Path(versionPrefix+"/componentdefinitions"). - Consumes(restful.MIME_XML, restful.MIME_JSON). - Produces(restful.MIME_JSON, restful.MIME_XML). - Doc("api for componentdefinition manage") - - tags := []string{"componentdefinition"} - - ws.Route(ws.GET("/").To(c.listComponentDefinition). - Doc("list all componentdefinition"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("envName", "if specified, query the componentdefinition supported by the env.").DataType("string")). - Returns(200, "", apis.ListComponentDefinitionResponse{}). - Writes(apis.ListComponentDefinitionResponse{})) - return ws -} - -// NewComponentDefinitionWebservice new componentdefinition webservice -func NewComponentDefinitionWebservice(du usecase.DefinitionUsecase) WebService { - return &componentDefinitionWebservice{ - definitionUsecase: du, - } -} - -func (c *componentDefinitionWebservice) listComponentDefinition(req *restful.Request, res *restful.Response) { - componentDefinitions, err := c.definitionUsecase.ListComponentDefinitions(req.Request.Context(), req.QueryParameter("envName")) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - if err := res.WriteEntity(apis.ListComponentDefinitionResponse{ComponentDefinitions: componentDefinitions}); err != nil { - bcode.ReturnError(req, res, err) - return - } -} diff --git a/pkg/apiserver/rest/webservice/definition.go b/pkg/apiserver/rest/webservice/definition.go new file mode 100644 index 000000000..8eee097fa --- /dev/null +++ b/pkg/apiserver/rest/webservice/definition.go @@ -0,0 +1,88 @@ +/* +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" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +type definitionWebservice struct { + definitionUsecase usecase.DefinitionUsecase +} + +func (d *definitionWebservice) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/definitions"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for definition manage") + + tags := []string{"definition"} + + ws.Route(ws.GET("/").To(d.listDefinitions). + Doc("list all definitions"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.QueryParameter("type", "query the definition type").DataType("string")). + Param(ws.QueryParameter("envName", "if specified, query the definition supported by the env.").DataType("string")). + Returns(200, "", apis.ListDefinitionResponse{}). + Writes(apis.ListDefinitionResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}").To(d.detailDefinition). + Doc("detail definition"). + Param(ws.PathParameter("name", "identifier of the definition").DataType("string")). + Param(ws.QueryParameter("type", "query the definition type").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "create success", apis.DetailDefinitionResponse{}). + Writes(apis.DetailDefinitionResponse{}).Do(returns200, returns500)) + return ws +} + +// NewDefinitionWebservice new definition webservice +func NewDefinitionWebservice(du usecase.DefinitionUsecase) WebService { + return &definitionWebservice{ + definitionUsecase: du, + } +} + +func (d *definitionWebservice) listDefinitions(req *restful.Request, res *restful.Response) { + definitions, err := d.definitionUsecase.ListDefinitions(req.Request.Context(), req.QueryParameter("envName"), req.QueryParameter("type")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListDefinitionResponse{Definitions: definitions}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (d *definitionWebservice) detailDefinition(req *restful.Request, res *restful.Response) { + definition, err := d.definitionUsecase.DetailDefinition(req.Request.Context(), req.PathParameter("name"), req.QueryParameter("type")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(definition); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 3effd3eaa..6915f595c 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -70,7 +70,7 @@ func Init(ctx context.Context, ds datastore.DataStore) { RegistWebService(NewClusterWebService(clusterUsecase)) RegistWebService(NewApplicationWebService(applicationUsecase)) RegistWebService(NewNamespaceWebService(namespaceUsecase)) - RegistWebService(NewComponentDefinitionWebservice(definitionUsecase)) + RegistWebService(NewDefinitionWebservice(definitionUsecase)) RegistWebService(NewAddonWebService(addonUsecase)) RegistWebService(NewAddonRegistryWebService(addonUsecase)) RegistWebService(NewOAMApplication(oamApplicationUsecase)) diff --git a/test/e2e-apiserver-test/definition_test.go b/test/e2e-apiserver-test/definition_test.go index 31e035262..c0abf49f9 100644 --- a/test/e2e-apiserver-test/definition_test.go +++ b/test/e2e-apiserver-test/definition_test.go @@ -29,16 +29,17 @@ import ( var _ = Describe("Test definitions rest api", func() { - It("Test list component definitions", func() { + It("Test list definitions", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/componentdefinitions") + res, err := http.Get("http://127.0.0.1:8000/api/v1/definitions?type=component") 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 componentdefinitions apisv1.ListComponentDefinitionResponse - err = json.NewDecoder(res.Body).Decode(&componentdefinitions) + var definitions apisv1.ListDefinitionResponse + err = json.NewDecoder(res.Body).Decode(&definitions) Expect(err).ShouldNot(HaveOccurred()) }) + }) From 2337c82c3d9367c8732a3f7bffa61b46c790094d Mon Sep 17 00:00:00 2001 From: Somefive Date: Thu, 4 Nov 2021 16:39:32 +0800 Subject: [PATCH 16/59] Feat: add create ack api (#2596) --- go.mod | 2 +- pkg/apiserver/rest/apis/v1/types.go | 22 ++++ pkg/apiserver/rest/usecase/cluster.go | 99 +++++++++++++++++- pkg/apiserver/rest/utils/bcode/cluster.go | 12 +++ pkg/apiserver/rest/webservice/cluster.go | 116 ++++++++++++++++++++++ pkg/cloudprovider/aliyun.go | 74 +++++++++++++- pkg/cloudprovider/cluster.go | 13 ++- pkg/cloudprovider/terraform.go | 94 ++++++++++++++++++ pkg/utils/util/k8s.go | 32 ++++++ 9 files changed, 457 insertions(+), 7 deletions(-) create mode 100644 pkg/cloudprovider/terraform.go create mode 100644 pkg/utils/util/k8s.go diff --git a/go.mod b/go.mod index 2c5bf9821..c765a3a31 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ require ( github.com/AlecAivazis/survey/v2 v2.1.1 github.com/Masterminds/sprig v2.22.0+incompatible github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 + github.com/agiledragon/gomonkey/v2 v2.3.0 github.com/alibabacloud-go/cs-20151215/v2 v2.4.5 github.com/alibabacloud-go/darabonba-openapi v0.1.4 github.com/alibabacloud-go/tea v1.1.15 - github.com/agiledragon/gomonkey/v2 v2.3.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 github.com/briandowns/spinner v1.11.1 diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index ea24469d2..035315d8a 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -139,6 +139,17 @@ type ConnectCloudClusterRequest struct { Labels map[string]string `json:"labels,omitempty"` } +// CreateCloudClusterRequest request parameters to create a cloud cluster (buy one) +type CreateCloudClusterRequest struct { + AccessKeyID string `json:"accessKeyID"` + AccessKeySecret string `json:"accessKeySecret"` + Name string `json:"name" validate:"checkname"` + Zone string `json:"zone"` + WorkerNumber int `json:"workerNumber"` + CPUCoresPerWorker int64 `json:"cpuCoresPerWorker"` + MemoryPerWorker int64 `json:"memoryPerWorker"` +} + // ClusterResourceInfo resource info of cluster type ClusterResourceInfo struct { WorkerNumber int `json:"workerNumber"` @@ -171,6 +182,17 @@ type ListCloudClusterResponse struct { Total int `json:"total"` } +// CreateCloudClusterResponse return values for cloud cluster create request +type CreateCloudClusterResponse struct { + ClusterID string `json:"clusterID"` + Status string `json:"status"` +} + +// ListCloudClusterCreationResponse return the cluster names of creation process of cloud clusters +type ListCloudClusterCreationResponse struct { + Creations []string `json:"creations"` +} + // ClusterBase cluster base model type ClusterBase struct { Name string `json:"name"` diff --git a/pkg/apiserver/rest/usecase/cluster.go b/pkg/apiserver/rest/usecase/cluster.go index 88439e7ce..44021a873 100644 --- a/pkg/apiserver/rest/usecase/cluster.go +++ b/pkg/apiserver/rest/usecase/cluster.go @@ -21,10 +21,15 @@ import ( "fmt" "io/ioutil" "os" + "strings" "time" + "github.com/oam-dev/terraform-controller/api/types" + "github.com/oam-dev/terraform-controller/api/v1beta1" "github.com/pkg/errors" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/pkg/apiserver/clients" @@ -37,6 +42,7 @@ import ( "github.com/oam-dev/kubevela/pkg/cloudprovider" "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/utils" + "github.com/oam-dev/kubevela/pkg/utils/util" ) // ClusterUsecase cluster manage @@ -49,6 +55,10 @@ type ClusterUsecase interface { ListCloudClusters(context.Context, string, apis.AccessKeyRequest, int, int) (*apis.ListCloudClusterResponse, error) ConnectCloudCluster(context.Context, string, apis.ConnectCloudClusterRequest) (*apis.ClusterBase, error) + CreateCloudCluster(context.Context, string, apis.CreateCloudClusterRequest) (*apis.CreateCloudClusterResponse, error) + GetCloudClusterCreationStatus(context.Context, string, string) (*apis.CreateCloudClusterResponse, error) + ListCloudClusterCreation(context.Context, string) (*apis.ListCloudClusterCreationResponse, error) + DeleteCloudClusterCreation(context.Context, string, string) (*apis.CreateCloudClusterResponse, error) } type clusterUsecaseImpl struct { @@ -395,7 +405,7 @@ func (c *clusterUsecaseImpl) getClusterResourceInfoFromK8s(ctx context.Context, } func (c *clusterUsecaseImpl) ListCloudClusters(ctx context.Context, provider string, req apis.AccessKeyRequest, pageNumber int, pageSize int) (*apis.ListCloudClusterResponse, error) { - p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret) + p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret, c.k8sClient) if err != nil { log.Logger.Errorf("failed to get cluster provider: %s", err.Error()) return nil, bcode.ErrInvalidCloudClusterProvider @@ -419,7 +429,7 @@ func (c *clusterUsecaseImpl) ListCloudClusters(ctx context.Context, provider str } func (c *clusterUsecaseImpl) ConnectCloudCluster(ctx context.Context, provider string, req apis.ConnectCloudClusterRequest) (*apis.ClusterBase, error) { - p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret) + p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret, c.k8sClient) if err != nil { log.Logger.Errorf("failed to get cluster provider: %s", err.Error()) return nil, bcode.ErrInvalidCloudClusterProvider @@ -448,6 +458,91 @@ func (c *clusterUsecaseImpl) ConnectCloudCluster(ctx context.Context, provider s return c.createKubeCluster(ctx, createReq, cluster) } +func (c *clusterUsecaseImpl) CreateCloudCluster(ctx context.Context, provider string, req apis.CreateCloudClusterRequest) (*apis.CreateCloudClusterResponse, error) { + p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret, c.k8sClient) + if err != nil { + log.Logger.Errorf("failed to get cluster provider: %s", err.Error()) + return nil, bcode.ErrInvalidCloudClusterProvider + } + _, err = p.CreateCloudCluster(ctx, req.Name, req.Zone, req.WorkerNumber, req.CPUCoresPerWorker, req.MemoryPerWorker) + if err != nil { + if kerrors.IsAlreadyExists(err) { + return nil, bcode.ErrCloudClusterAlreadyExists + } + log.Logger.Errorf("failed to bootstrap terraform configuration: %s", err.Error()) + return nil, bcode.ErrBootstrapTerraformConfiguration + } + return c.GetCloudClusterCreationStatus(ctx, provider, req.Name) +} + +func (c *clusterUsecaseImpl) getCloudClusterCreationStatus(ctx context.Context, provider string, cloudClusterName string) (*apis.CreateCloudClusterResponse, *v1beta1.Configuration, error) { + terraformConfigurationName := cloudprovider.GetCloudClusterFullName(provider, cloudClusterName) + cfg := &v1beta1.Configuration{ + ObjectMeta: v1.ObjectMeta{ + Name: terraformConfigurationName, + Namespace: util.GetRuntimeNamespace(), + }, + } + if err := c.k8sClient.Get(ctx, client.ObjectKeyFromObject(cfg), cfg); err != nil { + if kerrors.IsNotFound(err) { + return nil, nil, bcode.ErrTerraformConfigurationNotFound + } + return nil, nil, err + } + status := string(cfg.Status.Apply.State) + if status == "" { + status = "Initializing" + } + if cfg.DeletionTimestamp != nil { + status = "Deleting" + } + if status == string(types.Available) { + cid, ok := cfg.Status.Apply.Outputs["CLUSTER_ID"] + if !ok { + return nil, nil, bcode.ErrClusterIDNotFoundInTerraformConfiguration + } + return &apis.CreateCloudClusterResponse{ + Status: status, + ClusterID: cid.Value, + }, cfg, nil + } + return &apis.CreateCloudClusterResponse{Status: status}, cfg, nil +} + +func (c *clusterUsecaseImpl) GetCloudClusterCreationStatus(ctx context.Context, provider string, cloudClusterName string) (*apis.CreateCloudClusterResponse, error) { + resp, _, err := c.getCloudClusterCreationStatus(ctx, provider, cloudClusterName) + return resp, err +} + +func (c *clusterUsecaseImpl) ListCloudClusterCreation(ctx context.Context, provider string) (*apis.ListCloudClusterCreationResponse, error) { + cfgs := v1beta1.ConfigurationList{} + if err := c.k8sClient.List(ctx, &cfgs, client.HasLabels{cloudprovider.CloudClusterCreatorLabelKey}, client.InNamespace(util.GetRuntimeNamespace())); err != nil { + return nil, err + } + var creations []string + for _, cfg := range cfgs.Items { + prefix := "cloud-cluster-" + provider + "-" + if strings.HasPrefix(cfg.Name, prefix) { + creations = append(creations, strings.TrimPrefix(cfg.Name, prefix)) + } + } + return &apis.ListCloudClusterCreationResponse{Creations: creations}, nil +} + +func (c *clusterUsecaseImpl) DeleteCloudClusterCreation(ctx context.Context, provider string, cloudClusterName string) (*apis.CreateCloudClusterResponse, error) { + resp, cfg, err := c.getCloudClusterCreationStatus(ctx, provider, cloudClusterName) + if err != nil { + return resp, err + } + if err = c.k8sClient.Delete(ctx, cfg); err != nil { + if kerrors.IsNotFound(err) { + return resp, nil + } + return nil, err + } + return resp, err +} + func newClusterBaseFromCluster(cluster *model.Cluster) *apis.ClusterBase { return &apis.ClusterBase{ Name: cluster.Name, diff --git a/pkg/apiserver/rest/utils/bcode/cluster.go b/pkg/apiserver/rest/utils/bcode/cluster.go index 0b1a8647f..5b7478cb6 100644 --- a/pkg/apiserver/rest/utils/bcode/cluster.go +++ b/pkg/apiserver/rest/utils/bcode/cluster.go @@ -43,5 +43,17 @@ var ErrLocalClusterReserved = NewBcode(400, 40007, "local cluster is reserved") // ErrLocalClusterImmutable local cluster kubeConfig is immutable var ErrLocalClusterImmutable = NewBcode(400, 40008, "local cluster is immutable") +// ErrCloudClusterAlreadyExists cloud cluster already exists +var ErrCloudClusterAlreadyExists = NewBcode(400, 40009, "cloud cluster already exists") + +// ErrTerraformConfigurationNotFound cannot find terraform configuration +var ErrTerraformConfigurationNotFound = NewBcode(404, 40010, "cannot find terraform configuration") + +// ErrClusterIDNotFoundInTerraformConfiguration cannot find cluster_id in terraform configuration +var ErrClusterIDNotFoundInTerraformConfiguration = NewBcode(500, 40011, "cannot find cluster_id in terraform configuration") + +// ErrBootstrapTerraformConfiguration failed to bootstrap terraform configuration +var ErrBootstrapTerraformConfiguration = NewBcode(500, 40012, "failed to bootstrap terraform configuration") + // ErrInvalidAccessKeyOrSecretKey access key or secret key is invalid var ErrInvalidAccessKeyOrSecretKey = NewBcode(400, 40013, "access key or secret key is invalid") diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index fbb1045d7..b3f2e2648 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -109,6 +109,41 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.ClusterBase{})) + ws.Route(ws.POST("/cloud-clusters/{provider}/create").To(c.createCloudCluster). + Doc("create cloud cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Reads(&apis.CreateCloudClusterRequest{}). + Returns(200, "", apis.CreateCloudClusterResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.CreateCloudClusterResponse{})) + + ws.Route(ws.GET("/cloud-clusters/{provider}/creation/{cloudClusterName}").To(c.getCloudClusterCreationStatus). + Doc("check cloud cluster create status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Param(ws.PathParameter("cloudClusterName", "identifier for cloud cluster which is creating").DataType("string")). + Returns(200, "", apis.CreateCloudClusterResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.CreateCloudClusterResponse{})) + + ws.Route(ws.GET("/cloud-clusters/{provider}/creation").To(c.listCloudClusterCreation). + Doc("list cloud cluster creation"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Returns(200, "", apis.ListCloudClusterCreationResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListCloudClusterCreationResponse{})) + + ws.Route(ws.DELETE("/cloud-clusters/{provider}/creation/{cloudClusterName}").To(c.deleteCloudClusterCreation). + Doc("delete cloud cluster creation"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Param(ws.PathParameter("cloudClusterName", "identifier for cloud cluster which is creating").DataType("string")). + Returns(200, "", apis.CreateCloudClusterResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.CreateCloudClusterResponse{})) + return ws } @@ -280,3 +315,84 @@ func (c *ClusterWebService) connectCloudCluster(req *restful.Request, res *restf return } } + +func (c *ClusterWebService) createCloudCluster(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + + // Verify the validity of parameters + var createReq apis.CreateCloudClusterRequest + 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 + resp, err := c.clusterUsecase.CreateCloudCluster(req.Request.Context(), provider, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) getCloudClusterCreationStatus(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + cloudClusterName := req.PathParameter("cloudClusterName") + + // Call the usecase layer code + resp, err := c.clusterUsecase.GetCloudClusterCreationStatus(req.Request.Context(), provider, cloudClusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) listCloudClusterCreation(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + + // Call the usecase layer code + resp, err := c.clusterUsecase.ListCloudClusterCreation(req.Request.Context(), provider) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) deleteCloudClusterCreation(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + cloudClusterName := req.PathParameter("cloudClusterName") + + // Call the usecase layer code + resp, err := c.clusterUsecase.DeleteCloudClusterCreation(req.Request.Context(), provider, cloudClusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/cloudprovider/aliyun.go b/pkg/cloudprovider/aliyun.go index 943700351..0f379fdce 100644 --- a/pkg/cloudprovider/aliyun.go +++ b/pkg/cloudprovider/aliyun.go @@ -17,14 +17,23 @@ limitations under the License. package cloudprovider import ( + "context" "encoding/json" "strings" cs20151215 "github.com/alibabacloud-go/cs-20151215/v2/client" openapi "github.com/alibabacloud-go/darabonba-openapi/client" "github.com/alibabacloud-go/tea/tea" + types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime" + v1beta12 "github.com/oam-dev/terraform-controller/api/v1beta1" + "github.com/pkg/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/utils/util" ) const ( @@ -34,10 +43,13 @@ const ( // AliyunCloudProvider describes the cloud provider in aliyun type AliyunCloudProvider struct { *cs20151215.Client + k8sClient client.Client + accessKeyID string + accessKeySecret string } // NewAliyunCloudProvider create aliyun cloud provider -func NewAliyunCloudProvider(accessKeyID string, accessKeySecret string) (*AliyunCloudProvider, error) { +func NewAliyunCloudProvider(accessKeyID string, accessKeySecret string, k8sClient client.Client) (*AliyunCloudProvider, error) { config := &openapi.Config{ AccessKeyId: pointer.String(accessKeyID), AccessKeySecret: pointer.String(accessKeySecret), @@ -47,7 +59,7 @@ func NewAliyunCloudProvider(accessKeyID string, accessKeySecret string) (*Aliyun if err != nil { return nil, err } - return &AliyunCloudProvider{Client: c}, nil + return &AliyunCloudProvider{Client: c, k8sClient: k8sClient, accessKeyID: accessKeyID, accessKeySecret: accessKeySecret}, nil } // IsInvalidKey check if error is InvalidAccessKey or InvalidSecretKey @@ -133,3 +145,61 @@ func (provider *AliyunCloudProvider) GetClusterInfo(clusterID string) (*CloudClu DashBoardURL: url.DashboardEndpoint, }, nil } + +// CreateCloudCluster create cloud cluster +func (provider *AliyunCloudProvider) CreateCloudCluster(ctx context.Context, clusterName string, zone string, worker int, cpu int64, mem int64) (string, error) { + name := GetCloudClusterFullName(ProviderAliyun, clusterName) + ns := util.GetRuntimeNamespace() + terraformProviderName, err := bootstrapTerraformProvider(ctx, provider.k8sClient, ns, ProviderAliyun, "alibaba", provider.accessKeyID, provider.accessKeySecret, "cn-hongkong") + if err != nil { + return "", errors.Wrapf(err, "failed to bootstrap terraform provider") + } + properties := map[string]interface{}{ + "k8s_name_prefix": clusterName, + } + if zone != "" { + properties["zone_id"] = zone + } + if cpu != 0 { + properties["cpu_core_count"] = cpu + } + if mem != 0 { + properties["memory_size"] = mem + } + if worker != 0 { + properties["k8s_worker_number"] = worker + } + bs, err := json.Marshal(properties) + if err != nil { + return name, errors.Wrapf(err, "failed to marshal cloud cluster app properties") + } + + cfg := v1beta12.Configuration{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: ns, + Labels: map[string]string{ + CloudClusterCreatorLabelKey: ProviderAliyun, + }, + }, + Spec: v1beta12.ConfigurationSpec{ + Path: "alibaba/cs/dedicated-kubernetes", + Remote: "https://github.com/kubevela-contrib/terraform-modules.git", + Variable: &runtime.RawExtension{Raw: bs}, + ProviderReference: &types.Reference{ + Name: terraformProviderName, + Namespace: ns, + }, + WriteConnectionSecretToReference: &types.SecretReference{ + Name: name, + Namespace: ns, + }, + }, + } + + if err = provider.k8sClient.Create(ctx, &cfg); err != nil { + return name, errors.Wrapf(err, "failed to create cloud cluster terraform configuration") + } + + return name, nil +} diff --git a/pkg/cloudprovider/cluster.go b/pkg/cloudprovider/cluster.go index a18873396..eb0324551 100644 --- a/pkg/cloudprovider/cluster.go +++ b/pkg/cloudprovider/cluster.go @@ -17,7 +17,15 @@ limitations under the License. package cloudprovider import ( + "context" + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // CloudClusterCreatorLabelKey labels the creator of cloud cluster + CloudClusterCreatorLabelKey = "api.core.oam.dev/cloud-cluster-creator" ) // CloudClusterProvider abstracts the cloud provider to provide cluster access @@ -26,13 +34,14 @@ type CloudClusterProvider interface { ListCloudClusters(pageNumber int, pageSize int) ([]*CloudCluster, int, error) GetClusterKubeConfig(clusterID string) (string, error) GetClusterInfo(clusterID string) (*CloudCluster, error) + CreateCloudCluster(ctx context.Context, clusterName string, zone string, worker int, cpu int64, mem int64) (string, error) } // GetClusterProvider creates interface for getting cloud cluster provider -func GetClusterProvider(provider string, accessKeyID string, accessKeySecret string) (CloudClusterProvider, error) { +func GetClusterProvider(provider string, accessKeyID string, accessKeySecret string, k8sClient client.Client) (CloudClusterProvider, error) { switch provider { case ProviderAliyun: - return NewAliyunCloudProvider(accessKeyID, accessKeySecret) + return NewAliyunCloudProvider(accessKeyID, accessKeySecret, k8sClient) default: return nil, errors.Errorf("cluster provider %s is not implemented", provider) } diff --git a/pkg/cloudprovider/terraform.go b/pkg/cloudprovider/terraform.go new file mode 100644 index 000000000..36b7e10d9 --- /dev/null +++ b/pkg/cloudprovider/terraform.go @@ -0,0 +1,94 @@ +/* +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 cloudprovider + +import ( + "context" + "crypto/sha256" + "fmt" + "strings" + + types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime" + v1beta12 "github.com/oam-dev/terraform-controller/api/v1beta1" + "github.com/pkg/errors" + v12 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func computeProviderHashKey(provider string, accessKeyID string, accessKeySecret string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join([]string{provider, accessKeyID, accessKeySecret}, "::"))))[:8] // #nosec +} + +// GetCloudClusterFullName construct the full name of cloud cluster which will be used as the name of terraform configuration +func GetCloudClusterFullName(provider string, clusterName string) string { + return fmt.Sprintf("cloud-cluster-%s-%s", provider, clusterName) +} + +func bootstrapTerraformProvider(ctx context.Context, k8sClient client.Client, ns string, provider string, tfProvider string, accessKeyID string, accessKeySecret string, region string) (string, error) { + hashKey := computeProviderHashKey(provider, accessKeyID, accessKeySecret) + secretName := fmt.Sprintf("tf-provider-cred-%s-%s", provider, hashKey) + terraformProviderName := fmt.Sprintf("tf-provider-%s-%s", provider, hashKey) + secret := v12.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: secretName, + Namespace: ns, + }, + StringData: map[string]string{"credentials": fmt.Sprintf("accessKeyID: %s\naccessKeySecret: %s\nsecurityToken:\n", accessKeyID, accessKeySecret)}, + Type: v12.SecretTypeOpaque, + } + var err error + if err = k8sClient.Get(ctx, client.ObjectKeyFromObject(&secret), &v12.Secret{}); err != nil { + if kerrors.IsNotFound(err) { + err = k8sClient.Create(ctx, &secret) + } + if err != nil { + return "", errors.Wrapf(err, "failed to upsert terraform provider secret") + } + } + + terraformProvider := v1beta12.Provider{ + ObjectMeta: v1.ObjectMeta{ + Name: terraformProviderName, + Namespace: ns, + }, + Spec: v1beta12.ProviderSpec{ + Credentials: v1beta12.ProviderCredentials{ + SecretRef: &types.SecretKeySelector{ + Key: "credentials", + SecretReference: types.SecretReference{ + Name: secretName, + Namespace: ns, + }, + }, + Source: types.CredentialsSourceSecret, + }, + Provider: tfProvider, + Region: region, + }, + } + if err = k8sClient.Get(ctx, client.ObjectKeyFromObject(&terraformProvider), &v1beta12.Provider{}); err != nil { + if kerrors.IsNotFound(err) { + err = k8sClient.Create(ctx, &terraformProvider) + } + if err != nil { + return "", errors.Wrapf(err, "failed to upsert terraform provider") + } + } + return terraformProviderName, nil +} diff --git a/pkg/utils/util/k8s.go b/pkg/utils/util/k8s.go new file mode 100644 index 000000000..a8d1765b2 --- /dev/null +++ b/pkg/utils/util/k8s.go @@ -0,0 +1,32 @@ +/* +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 util + +import ( + "io/ioutil" + + "github.com/oam-dev/kubevela/apis/types" +) + +// GetRuntimeNamespace get namespace of the current running pod, fall back to default vela system +func GetRuntimeNamespace() string { + ns, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil { + return types.DefaultKubeVelaNS + } + return string(ns) +} From cd686fbb24d9d2ce3f97820ba7481e8f2d0a3ca7 Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Fri, 5 Nov 2021 10:32:54 +0800 Subject: [PATCH 17/59] Fix: fix definition schema struct (#2632) * Fix: fix definition schema struct * add more fields --- pkg/apiserver/rest/apis/v1/types.go | 29 +++++++++--- pkg/apiserver/rest/usecase/definition_test.go | 47 +++++++++++++------ 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 035315d8a..92fa93c36 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + "regexp" "time" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" @@ -417,16 +418,30 @@ type DetailDefinitionResponse struct { } type DefinitionSchema struct { - Properties map[string]DefinitionProperties `json:"properties"` - Required []string `json:"required"` - Type string `json:"type"` + Properties map[string]*DefinitionProperties `json:"properties"` + Required []string `json:"required"` + Type string `json:"type"` } type DefinitionProperties struct { - Default string `json:"default"` - Description string `json:"description"` - Title string `json:"title"` - Type string `json:"type"` + Items *DefinitionSchema `json:"items,omitempty"` + Enum []interface{} `json:"enum,omitempty"` + Default interface{} `json:"default,omitempty"` + Example interface{} `json:"example,omitempty"` + Description string `json:"description,omitempty"` + Title string `json:"title"` + Type string `json:"type"` + + // Number + Min *float64 `json:"minimum,omitempty"` + Max *float64 `json:"maximum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty"` + + // String + MinLength uint64 `json:"minLength,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + compiledPattern *regexp.Regexp } // DefinitionBase is the definition base model diff --git a/pkg/apiserver/rest/usecase/definition_test.go b/pkg/apiserver/rest/usecase/definition_test.go index 37756fe92..546f1eda4 100644 --- a/pkg/apiserver/rest/usecase/definition_test.go +++ b/pkg/apiserver/rest/usecase/definition_test.go @@ -98,29 +98,48 @@ var _ = Describe("Test namespace usecase functions", func() { Namespace: "vela-system", }, Data: map[string]string{ - "openapi-v3-json-schema": `{"properties":{"cluster":{"default":"","description":"Specify the cluster of the object","title":"cluster","type":"string"},"value":{"description":"Specify the value of the object","title":"value","type":"object"}},"required":["value","cluster"],"type":"object"}`, + "openapi-v3-json-schema": `{"properties":{"batchPartition":{"title":"batchPartition","type":"integer"},"volumes": {"description":"Specify volume type, options: pvc, configMap, secret, emptyDir","enum":["pvc","configMap","secret","emptyDir"],"title":"volumes","type":"string"}, "rolloutBatches":{"items":{"properties":{"replicas":{"title":"replicas","type":"integer"}},"required":["replicas"],"type":"object"},"title":"rolloutBatches","type":"array"},"targetRevision":{"title":"targetRevision","type":"string"},"targetSize":{"title":"targetSize","type":"integer"}},"required":["targetRevision","targetSize"],"type":"object"}`, }, } err := k8sClient.Create(context.Background(), cm) Expect(err).Should(Succeed()) schema, err := definitionUsecase.DetailDefinition(context.TODO(), "apply-object", "workflowstep") - Expect(err).Should(BeNil()) Expect(schema.Schema).Should(Equal(&v1.DefinitionSchema{ - Properties: map[string]v1.DefinitionProperties{ - "value": { - Default: "", - Description: "Specify the value of the object", - Title: "value", - Type: "object", - }, - "cluster": { - Default: "", - Description: "Specify the cluster of the object", - Title: "cluster", + Properties: map[string]*v1.DefinitionProperties{ + "volumes": { + Title: "volumes", Type: "string", + Description: "Specify volume type, options: pvc, configMap, secret, emptyDir", + Enum: []interface{}{"pvc", "configMap", "secret", "emptyDir"}, + }, + "batchPartition": { + Title: "batchPartition", + Type: "integer", + }, + "rolloutBatches": { + Items: &v1.DefinitionSchema{ + Properties: map[string]*v1.DefinitionProperties{ + "replicas": { + Title: "replicas", + Type: "integer", + }, + }, + Required: []string{"replicas"}, + Type: "object", + }, + Title: "rolloutBatches", + Type: "array", + }, + "targetSize": { + Title: "targetSize", + Type: "integer", + }, + "targetRevision": { + Title: "targetRevision", + Type: "string", }, }, - Required: []string{"value", "cluster"}, + Required: []string{"targetRevision", "targetSize"}, Type: "object", })) }) From df5bc2727edf2b6252d637010fc2bf2c618c7917 Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Fri, 5 Nov 2021 10:42:18 +0800 Subject: [PATCH 18/59] Feat: supports setting environment differences for application plan (#2624) * Feat: supports setting environment differences for application plan * Feat: update swagger config * Feat: CRUD of application env binding plan * Style: change code style Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 753 ++++++++++++++++-- pkg/apiserver/model/application.go | 15 +- pkg/apiserver/rest/apis/v1/types.go | 50 +- .../{application.go => applicationplan.go} | 446 ++++++++--- ...cation_test.go => applicationplan_test.go} | 169 +++- pkg/apiserver/rest/utils/bcode/application.go | 15 +- .../{application.go => applicationplan.go} | 226 +++++- .../rest/webservice/validate_test.go | 14 + pkg/apiserver/rest/webservice/webservice.go | 2 +- pkg/apiserver/rest/webservice/workflow.go | 4 +- test/e2e-apiserver-test/application_test.go | 2 +- .../e2e-apiserver-test/testdata/workflow.json | 14 + 12 files changed, 1459 insertions(+), 251 deletions(-) rename pkg/apiserver/rest/usecase/{application.go => applicationplan.go} (65%) rename pkg/apiserver/rest/usecase/{application_test.go => applicationplan_test.go} (64%) rename pkg/apiserver/rest/webservice/{application.go => applicationplan.go} (59%) create mode 100644 test/e2e-apiserver-test/testdata/workflow.json diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index cc9f77b13..a9493a2f1 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -321,8 +321,8 @@ "tags": [ "application" ], - "summary": "list all applications", - "operationId": "listApplications", + "summary": "list all application plans", + "operationId": "listApplicationPlans", "parameters": [ { "type": "string", @@ -368,8 +368,8 @@ "tags": [ "application" ], - "summary": "create one application", - "operationId": "createApplication", + "summary": "create one application plan", + "operationId": "createApplicationPlan", "parameters": [ { "name": "body", @@ -407,12 +407,12 @@ "tags": [ "application" ], - "summary": "detail one application", - "operationId": "detailApplication", + "summary": "detail one application plan", + "operationId": "detailApplicationPlan", "parameters": [ { "type": "string", - "description": "identifier of the application", + "description": "identifier of the application plan", "name": "name", "in": "path", "required": true @@ -431,6 +431,50 @@ } } }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update one application plan", + "operationId": "updateApplicationPlan", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateApplicationPlanRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationPlanBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, "delete": { "consumes": [ "application/xml", @@ -444,11 +488,11 @@ "application" ], "summary": "delete one application", - "operationId": "deleteApplication", + "operationId": "deleteApplicationPlan", "parameters": [ { "type": "string", - "description": "identifier of the application", + "description": "identifier of the application plan", "name": "name", "in": "path", "required": true @@ -486,7 +530,7 @@ "parameters": [ { "type": "string", - "description": "identifier of the application", + "description": "identifier of the application plan", "name": "name", "in": "path", "required": true @@ -528,7 +572,7 @@ "parameters": [ { "type": "string", - "description": "identifier of the application", + "description": "identifier of the application plan", "name": "name", "in": "path", "required": true @@ -574,7 +618,7 @@ "parameters": [ { "type": "string", - "description": "identifier of the application", + "description": "identifier of the application plan", "name": "name", "in": "path", "required": true @@ -612,7 +656,7 @@ "parameters": [ { "type": "string", - "description": "identifier of the application", + "description": "identifier of the application plan", "name": "name", "in": "path", "required": true @@ -632,6 +676,148 @@ } } }, + "/api/v1/applicationplans/{name}/envs": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "creating an application environment plan", + "operationId": "createApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationEnvPlanRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EnvBind" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/envs/{envName}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "set application plan differences in the specified environment", + "operationId": "updateApplicationEnvBinding", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application plan", + "name": "envName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.PutApplicationPlanEnvRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EnvBind" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "delete an application environment plan", + "operationId": "deleteApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application plan", + "name": "envName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "404": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/applicationplans/{name}/policies": { "get": { "consumes": [ @@ -650,7 +836,7 @@ "parameters": [ { "type": "string", - "description": "identifier of the application", + "description": "identifier of the application plan", "name": "name", "in": "path", "required": true @@ -871,7 +1057,7 @@ "parameters": [ { "type": "string", - "description": "identifier of the application", + "description": "identifier of the application plan", "name": "name", "in": "path", "required": true @@ -1097,6 +1283,178 @@ } } }, + "/api/v1/clusters/cloud-clusters/{provider}/create": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create cloud cluster", + "operationId": "createCloudCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/*v1.CreateCloudClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/creation": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list cloud cluster creation", + "operationId": "listCloudClusterCreation", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListCloudClusterCreationResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/creation/{cloudClusterName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "check cloud cluster create status", + "operationId": "getCloudClusterCreationStatus", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier for cloud cluster which is creating", + "name": "cloudClusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "delete cloud cluster creation", + "operationId": "deleteCloudClusterCreation", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier for cloud cluster which is creating", + "name": "cloudClusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/clusters/{clusterName}": { "get": { "consumes": [ @@ -1215,7 +1573,7 @@ } } }, - "/api/v1/componentdefinitions": { + "/api/v1/definitions": { "get": { "consumes": [ "application/xml", @@ -1226,23 +1584,76 @@ "application/xml" ], "tags": [ - "componentdefinition" + "definition" ], - "summary": "list all componentdefinition", - "operationId": "listComponentDefinition", + "summary": "list all definitions", + "operationId": "listDefinitions", "parameters": [ { "type": "string", - "description": "if specified, query the componentdefinition supported by the env.", + "description": "query the definition type", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "if specified, query the definition supported by the env.", "name": "envName", "in": "query" } ], "responses": { "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/v1.ListComponentDefinitionResponse" + "$ref": "#/definitions/map[string]string" } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/definitions/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "definition" + ], + "summary": "detail definition", + "operationId": "detailDefinition", + "parameters": [ + { + "type": "string", + "description": "identifier of the definition", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "query the definition type", + "name": "type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" } } } @@ -1834,9 +2245,9 @@ }, "common.AppRolloutStatus": { "required": [ + "rollingState", "batchRollingState", "upgradedReplicas", - "rollingState", "currentBatch", "upgradedReadyReplicas", "lastTargetAppRevision" @@ -2975,24 +3386,6 @@ } } }, - "v1.ComponentDefinitionBase": { - "required": [ - "name", - "description", - "icon" - ], - "properties": { - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, "v1.ComponentPlanBase": { "required": [ "name", @@ -3068,6 +3461,19 @@ } } }, + "v1.ComponentSelector": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1.ConnectCloudClusterRequest": { "required": [ "accessKeyID", @@ -3120,6 +3526,31 @@ } } }, + "v1.CreateApplicationEnvPlanRequest": { + "required": [ + "componentSelector", + "name", + "alias", + "clusterSelector" + ], + "properties": { + "alias": { + "type": "string" + }, + "clusterSelector": { + "$ref": "#/definitions/v1.ClusterSelector" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "v1.CreateApplicationPlanRequest": { "required": [ "name", @@ -3182,6 +3613,57 @@ } } }, + "v1.CreateCloudClusterRequest": { + "required": [ + "accessKeyID", + "accessKeySecret", + "name", + "zone", + "workerNumber", + "cpuCoresPerWorker", + "memoryPerWorker" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + }, + "cpuCoresPerWorker": { + "type": "integer", + "format": "int64" + }, + "memoryPerWorker": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "workerNumber": { + "type": "integer", + "format": "int32" + }, + "zone": { + "type": "string" + } + } + }, + "v1.CreateCloudClusterResponse": { + "required": [ + "clusterID", + "status" + ], + "properties": { + "clusterID": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "v1.CreateClusterRequest": { "required": [ "name", @@ -3338,13 +3820,77 @@ } } }, + "v1.DefinitionBase": { + "required": [ + "name", + "description", + "icon" + ], + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.DefinitionProperties": { + "required": [ + "default", + "description", + "title", + "type" + ], + "properties": { + "default": { + "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.DefinitionSchema": { + "required": [ + "properties", + "required", + "type" + ], + "properties": { + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/v1.DefinitionProperties" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + } + }, "v1.DetailAddonResponse": { "required": [ + "name", "version", "description", "icon", - "tags", - "name" + "tags" ], "properties": { "deploy_data": { @@ -3375,15 +3921,15 @@ }, "v1.DetailApplicationPlanResponse": { "required": [ - "icon", - "status", - "gatewayRule", + "name", "namespace", + "icon", + "gatewayRule", + "alias", "description", "createTime", "updateTime", - "name", - "alias", + "status", "policies", "status", "resourceInfo", @@ -3453,20 +3999,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "createTime", - "description", - "status", "reason", - "kubeConfig", - "kubeConfigSecret", - "name", - "alias", "provider", - "labels", - "dashboardURL", "updateTime", + "description", + "dashboardURL", + "kubeConfigSecret", + "createTime", + "name", "icon", "apiServerURL", + "alias", + "labels", + "status", + "kubeConfig", "resourceInfo" ], "properties": { @@ -3524,13 +4070,13 @@ }, "v1.DetailComponentPlanResponse": { "required": [ - "appPrimaryKey", - "alias", - "type", + "createTime", "updateTime", + "alias", + "appPrimaryKey", "creator", "name", - "createTime" + "type" ], "properties": { "alias": { @@ -3606,15 +4152,25 @@ } } }, + "v1.DetailDefinitionResponse": { + "required": [ + "schema" + ], + "properties": { + "schema": { + "$ref": "#/definitions/v1.DefinitionSchema" + } + } + }, "v1.DetailPolicyResponse": { "required": [ - "description", - "creator", "properties", "createTime", "updateTime", "name", - "type" + "type", + "description", + "creator" ], "properties": { "createTime": { @@ -3757,12 +4313,20 @@ "v1.EnvBind": { "required": [ "name", - "clusterSelector" + "alias", + "clusterSelector", + "componentSelector" ], "properties": { + "alias": { + "type": "string" + }, "clusterSelector": { "$ref": "#/definitions/v1.ClusterSelector" }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, "description": { "type": "string" }, @@ -3850,6 +4414,19 @@ } } }, + "v1.ListCloudClusterCreationResponse": { + "required": [ + "creations" + ], + "properties": { + "creations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1.ListCloudClusterResponse": { "required": [ "clusters", @@ -3881,15 +4458,15 @@ } } }, - "v1.ListComponentDefinitionResponse": { + "v1.ListDefinitionResponse": { "required": [ - "componentDefinitions" + "definitions" ], "properties": { - "componentDefinitions": { + "definitions": { "type": "array", "items": { - "$ref": "#/definitions/v1.ComponentDefinitionBase" + "$ref": "#/definitions/v1.DefinitionBase" } } } @@ -4089,6 +4666,46 @@ } } }, + "v1.PutApplicationPlanEnvRequest": { + "properties": { + "alias": { + "type": "string" + }, + "clusterSelector": { + "$ref": "#/definitions/v1.ClusterSelector" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "description": { + "type": "string" + } + } + }, + "v1.UpdateApplicationPlanRequest": { + "required": [ + "alias", + "description", + "icon" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "v1.UpdatePolicyRequest": { "required": [ "description", diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 5095f1cc7..397660bf7 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -62,18 +62,25 @@ func (a *ApplicationPlan) Index() map[string]string { // EnvBind application env bind type EnvBind struct { - Name string `json:"name" validate:"checkname"` - Description string `json:"description,omitempty"` - ClusterSelector *ClusterSelector `json:"clusterSelector"` + Name string `json:"name"` + Alias string `json:"alias"` + Description string `json:"description,omitempty"` + ClusterSelector ClusterSelector `json:"clusterSelector"` + ComponentSelector *ComponentSelector `json:"componentSelector"` } // ClusterSelector cluster selector type ClusterSelector struct { - Name string `json:"name" validate:"checkname"` + Name string `json:"name"` // Adapt to a scenario where only one Namespace is available or a user-defined Namespace is available. Namespace string `json:"namespace,omitempty"` } +// ComponentSelector component selector +type ComponentSelector struct { + Components []string `json:"components"` +} + // ApplicationComponentPlan component database model type ApplicationComponentPlan struct { Model diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 92fa93c36..b970e6a50 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -29,11 +29,12 @@ import ( var ( // CtxKeyApplication request context key of application CtxKeyApplication = "application" + // CtxKeyWorkflow request context key of workflow + CtxKeyWorkflow = "workflow" + // CtxKeyApplicationEnvBinding request context key of env binding + CtxKeyApplicationEnvBinding = "envbinding-policy" ) -// CtxKeyWorkflow request context key of workflow -var CtxKeyWorkflow = "workflow" - // AddonPhase defines the phase of an addon type AddonPhase string @@ -228,7 +229,7 @@ type EnvBindList []*EnvBind // ContainCluster contain cluster name func (e EnvBindList) ContainCluster(name string) bool { for _, eb := range e { - if eb.ClusterSelector != nil && eb.ClusterSelector.Name == name { + if eb.ClusterSelector.Name == name { return true } } @@ -283,11 +284,21 @@ type CreateApplicationPlanRequest struct { Deploy bool `json:"deploy,omitempty"` } +// UpdateApplicationPlanRequest update application plan base config +type UpdateApplicationPlanRequest struct { + Alias string `json:"alias" validate:"checkalias"` + Description string `json:"description"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels,omitempty"` +} + // EnvBind application env bind type EnvBind struct { - Name string `json:"name" validate:"checkname"` - Description string `json:"description,omitempty"` - ClusterSelector *ClusterSelector `json:"clusterSelector"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias"` + Description string `json:"description,omitempty"` + ClusterSelector ClusterSelector `json:"clusterSelector"` + ComponentSelector *ComponentSelector `json:"componentSelector"` } // ClusterSelector cluster selector @@ -297,6 +308,11 @@ type ClusterSelector struct { Namespace string `json:"namespace,omitempty"` } +// ComponentSelector component selector +type ComponentSelector struct { + Components []string `json:"components"` +} + // DetailApplicationPlanResponse application plan detail type DetailApplicationPlanResponse struct { ApplicationPlanBase @@ -359,6 +375,11 @@ type DetailComponentPlanResponse struct { //TODO: Status } +// ListApplicationComponentOptions list app plan component list +type ListApplicationComponentOptions struct { + EnvName string `json:"envName"` +} + // CreateApplicationTemplateRequest create app template request model type CreateApplicationTemplateRequest struct { TemplateName string `json:"templateName" validate:"checkname"` @@ -417,12 +438,14 @@ type DetailDefinitionResponse struct { Schema *DefinitionSchema `json:"schema"` } +// DefinitionSchema definition schema info type DefinitionSchema struct { Properties map[string]*DefinitionProperties `json:"properties"` Required []string `json:"required"` Type string `json:"type"` } +// DefinitionProperties definition properties type DefinitionProperties struct { Items *DefinitionSchema `json:"items,omitempty"` Enum []interface{} `json:"enum,omitempty"` @@ -611,3 +634,16 @@ type ApplicationDeployResponse struct { // VelaQLViewResponse query response type VelaQLViewResponse map[string]interface{} + +// PutApplicationPlanEnvRequest set diff request +type PutApplicationPlanEnvRequest struct { + ComponentSelector *ComponentSelector `json:"componentSelector,omitempty"` + Alias *string `json:"alias,omitempty" validate:"checkalias"` + Description *string `json:"description,omitempty"` + ClusterSelector *ClusterSelector `json:"clusterSelector,omitempty"` +} + +// CreateApplicationEnvPlanRequest new application env plan +type CreateApplicationEnvPlanRequest struct { + EnvBind +} diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/applicationplan.go similarity index 65% rename from pkg/apiserver/rest/usecase/application.go rename to pkg/apiserver/rest/usecase/applicationplan.go index 64a6f4b33..a22465bc0 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/applicationplan.go @@ -20,6 +20,8 @@ import ( "context" "encoding/json" "errors" + "fmt" + "sort" "strings" corev1 "k8s.io/api/core/v1" @@ -49,18 +51,22 @@ type PolicyType string const ( // EnvBindPolicy Multiple environment distribution policy EnvBindPolicy PolicyType = "env-binding" + + // EnvBindPolicyDefaultName default policy name + EnvBindPolicyDefaultName string = "env-bindings" ) // ApplicationUsecase application usecase type ApplicationUsecase interface { - ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioPlanOptions) ([]*apisv1.ApplicationPlanBase, error) - GetApplication(ctx context.Context, appName string) (*model.ApplicationPlan, error) - DetailApplication(ctx context.Context, app *model.ApplicationPlan) (*apisv1.DetailApplicationPlanResponse, error) + ListApplicationPlans(ctx context.Context, listOptions apisv1.ListApplicatioPlanOptions) ([]*apisv1.ApplicationPlanBase, error) + GetApplicationPlan(ctx context.Context, appName string) (*model.ApplicationPlan, error) + DetailApplicationPlan(ctx context.Context, app *model.ApplicationPlan) (*apisv1.DetailApplicationPlanResponse, error) PublishApplicationTemplate(ctx context.Context, app *model.ApplicationPlan) (*apisv1.ApplicationTemplateBase, error) - CreateApplication(context.Context, apisv1.CreateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) - DeleteApplication(ctx context.Context, app *model.ApplicationPlan) error + CreateApplicationPlan(context.Context, apisv1.CreateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) + UpdateApplicationPlan(context.Context, *model.ApplicationPlan, apisv1.UpdateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) + DeleteApplicationPlan(ctx context.Context, app *model.ApplicationPlan) error Deploy(ctx context.Context, app *model.ApplicationPlan, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) - ListComponents(ctx context.Context, app *model.ApplicationPlan) ([]*apisv1.ComponentPlanBase, error) + ListComponents(ctx context.Context, app *model.ApplicationPlan, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentPlanBase, error) AddComponent(ctx context.Context, app *model.ApplicationPlan, com apisv1.CreateComponentPlanRequest) (*apisv1.ComponentPlanBase, error) DetailComponent(ctx context.Context, app *model.ApplicationPlan, componentName string) (*apisv1.DetailComponentPlanResponse, error) DeleteComponent(ctx context.Context, app *model.ApplicationPlan, componentName string) error @@ -69,6 +75,10 @@ type ApplicationUsecase interface { DetailPolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) (*apisv1.DetailPolicyResponse, error) DeletePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) error UpdatePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) + GetApplicationPlanEnvBindingPolicy(ctx context.Context, app *model.ApplicationPlan) (*v1alpha1.EnvBindingSpec, error) + UpdateApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, envName string, diff apisv1.PutApplicationPlanEnvRequest) (*apisv1.EnvBind, error) + CreateApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, env apisv1.CreateApplicationEnvPlanRequest) (*apisv1.EnvBind, error) + DeleteApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, envName string) error } type applicationUsecaseImpl struct { @@ -92,8 +102,8 @@ func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUseca } } -// ListApplications list applications -func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioPlanOptions) ([]*apisv1.ApplicationPlanBase, error) { +// ListApplicationPlans list applications +func (c *applicationUsecaseImpl) ListApplicationPlans(ctx context.Context, listOptions apisv1.ListApplicatioPlanOptions) ([]*apisv1.ApplicationPlanBase, error) { var app = model.ApplicationPlan{} if listOptions.Namespace != "" { app.Namespace = listOptions.Namespace @@ -104,7 +114,7 @@ func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptio } var list []*apisv1.ApplicationPlanBase for _, entity := range entitys { - appBase := c.converAppModelToBase(ctx, entity.(*model.ApplicationPlan)) + appBase := c.converAppModelToBase(entity.(*model.ApplicationPlan)) if listOptions.Query != "" && !(strings.Contains(appBase.Alias, listOptions.Query) || strings.Contains(appBase.Name, listOptions.Query) || @@ -116,28 +126,34 @@ func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptio } list = append(list, appBase) } + sort.Slice(list, func(i, j int) bool { + return list[i].UpdateTime.Unix() > list[j].UpdateTime.Unix() + }) return list, nil } -// GetApplication get application model -func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName string) (*model.ApplicationPlan, error) { +// GetApplicationPlan get application model +func (c *applicationUsecaseImpl) GetApplicationPlan(ctx context.Context, appName string) (*model.ApplicationPlan, error) { var app = model.ApplicationPlan{ Name: appName, } if err := c.ds.Get(ctx, &app); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationNotExist + } return nil, err } return &app, nil } -// DetailApplication detail application info -func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.ApplicationPlan) (*apisv1.DetailApplicationPlanResponse, error) { - base := c.converAppModelToBase(ctx, app) +// DetailApplicationPlan detail application plan info +func (c *applicationUsecaseImpl) DetailApplicationPlan(ctx context.Context, app *model.ApplicationPlan) (*apisv1.DetailApplicationPlanResponse, error) { + base := c.converAppModelToBase(app) policys, err := c.queryApplicationPolicys(ctx, app) if err != nil { return nil, err } - components, err := c.ListComponents(ctx, app) + components, err := c.ListComponents(ctx, app, apisv1.ListApplicationComponentOptions{}) if err != nil { return nil, err } @@ -162,8 +178,8 @@ func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, return nil, nil } -// CreateApplication create application -func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apisv1.CreateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) { +// CreateApplicationPlan create application +func (c *applicationUsecaseImpl) CreateApplicationPlan(ctx context.Context, req apisv1.CreateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) { application := model.ApplicationPlan{ Name: req.Name, Alias: req.Alias, @@ -240,38 +256,7 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis // build-in create env binding policy if len(req.EnvBind) > 0 { - policy := model.ApplicationPolicyPlan{ - AppPrimaryKey: application.PrimaryKey(), - Name: "env-binds", - Description: "build-in create", - Type: string(EnvBindPolicy), - Creator: "", - } - var envBindingSpec v1alpha1.EnvBindingSpec - for _, envBind := range req.EnvBind { - placement := v1alpha1.EnvPlacement{ - ClusterSelector: &common.ClusterSelector{ - Name: envBind.ClusterSelector.Name, - }, - } - if envBind.ClusterSelector.Namespace != "" { - placement.NamespaceSelector = &v1alpha1.NamespaceSelector{ - Name: envBind.ClusterSelector.Namespace, - } - } - envBindingSpec.Envs = append(envBindingSpec.Envs, v1alpha1.EnvConfig{ - Name: envBind.Name, - Placement: placement, - }) - } - properties, err := model.NewJSONStructByStruct(envBindingSpec) - if err != nil { - log.Logger.Errorf("new env binding properties failure,%s", err.Error()) - return nil, bcode.ErrInvalidProperties - } - policy.Properties = properties - if err := c.ds.Add(ctx, &policy); err != nil { - log.Logger.Errorf("save env binding policy failure,%s", err.Error()) + if _, err := c.createApplictionPlanEnvBindingPolicy(ctx, &application, req.EnvBind); err != nil { return nil, err } } @@ -284,7 +269,7 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return nil, err } // render app base info. - base := c.converAppModelToBase(ctx, &application) + base := c.converAppModelToBase(&application) // deploy to cluster if need. if req.Deploy && canDeploy { if _, err := c.Deploy(ctx, &application, apisv1.ApplicationDeployRequest{ @@ -297,6 +282,17 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return base, nil } +func (c *applicationUsecaseImpl) UpdateApplicationPlan(ctx context.Context, app *model.ApplicationPlan, req apisv1.UpdateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) { + app.Alias = req.Alias + app.Description = req.Description + app.Labels = req.Labels + app.Icon = req.Icon + if err := c.ds.Put(ctx, app); err != nil { + return nil, err + } + return c.converAppModelToBase(app), nil +} + func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, app *model.ApplicationPlan, components []common.ApplicationComponent) error { var componentModels []datastore.Entity for _, component := range components { @@ -336,7 +332,7 @@ func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, a return c.ds.BatchAdd(ctx, componentModels) } -func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.ApplicationPlan) ([]*apisv1.ComponentPlanBase, error) { +func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.ApplicationPlan, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentPlanBase, error) { var component = model.ApplicationComponentPlan{ AppPrimaryKey: app.PrimaryKey(), } @@ -344,11 +340,31 @@ func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model. if err != nil { return nil, err } + envComponents := map[string]bool{} + componentSelectorDefine := false + if op.EnvName != "" { + envbinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) + if err != nil && !errors.Is(err, bcode.ErrApplicationNotEnv) { + log.Logger.Errorf("query app plan env binding policy config failure %s", err.Error()) + } + if envbinding != nil { + for _, env := range envbinding.Envs { + if env.Selector != nil && env.Name == op.EnvName { + componentSelectorDefine = true + for _, componentName := range env.Selector.Components { + envComponents[componentName] = true + } + } + } + } + } + var list []*apisv1.ComponentPlanBase for _, component := range components { - log.Logger.Infof("component name %s", component.PrimaryKey()) pm := component.(*model.ApplicationComponentPlan) - list = append(list, c.converComponentModelToBase(pm)) + if !componentSelectorDefine || envComponents[pm.Name] { + list = append(list, c.converComponentModelToBase(pm)) + } } return list, nil } @@ -412,18 +428,49 @@ func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.Applicati func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app *model.ApplicationPlan, policys []v1beta1.AppPolicy) error { var policyModels []datastore.Entity + var envbindingPolicy *model.ApplicationPolicyPlan for _, policy := range policys { properties, err := model.NewJSONStruct(policy.Properties) if err != nil { log.Logger.Errorf("parse trait properties failire %w", err) return bcode.ErrInvalidProperties } - policyModels = append(policyModels, &model.ApplicationPolicyPlan{ + appPolicyPlan := &model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), Name: policy.Name, Type: policy.Type, Properties: properties, - }) + } + if policy.Type != string(EnvBindPolicy) { + policyModels = append(policyModels, appPolicyPlan) + } else { + envbindingPolicy = appPolicyPlan + } + } + // If multiple configurations are configured, enable only the last one. + if envbindingPolicy != nil { + envbindingPolicy.Name = EnvBindPolicyDefaultName + policyModels = append(policyModels, envbindingPolicy) + var envBindingSpec v1alpha1.EnvBindingSpec + if err := json.Unmarshal([]byte(envbindingPolicy.Properties.JSON()), &envBindingSpec); err != nil { + return fmt.Errorf("unmarshal env binding policy failure %w", err) + } + for _, env := range envBindingSpec.Envs { + envBind := &model.EnvBind{ + Name: env.Name, + Description: "", + } + if env.Selector != nil { + envBind.ComponentSelector = (*model.ComponentSelector)(env.Selector) + } + if env.Placement.ClusterSelector != nil { + envBind.ClusterSelector.Name = env.Placement.ClusterSelector.Name + } + if env.Placement.NamespaceSelector != nil { + envBind.ClusterSelector.Namespace = env.Placement.NamespaceSelector.Name + } + app.EnvBinds = append(app.EnvBinds, envBind) + } } return c.ds.BatchAdd(ctx, policyModels) } @@ -443,6 +490,52 @@ func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, ap return } +func (c *applicationUsecaseImpl) GetApplicationPlanEnvBindingPolicy(ctx context.Context, app *model.ApplicationPlan) (*v1alpha1.EnvBindingSpec, error) { + var policy = model.ApplicationPolicyPlan{ + AppPrimaryKey: app.PrimaryKey(), + Type: string(EnvBindPolicy), + Name: EnvBindPolicyDefaultName, + } + err := c.ds.Get(ctx, &policy) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationNotEnv + } + return nil, err + } + var envBindingSpec v1alpha1.EnvBindingSpec + if err := json.Unmarshal([]byte(policy.Properties.JSON()), &envBindingSpec); err != nil { + return nil, err + } + return &envBindingSpec, nil +} + +func (c *applicationUsecaseImpl) createApplictionPlanEnvBindingPolicy(ctx context.Context, app *model.ApplicationPlan, envbinds apisv1.EnvBindList) (*model.ApplicationPolicyPlan, error) { + policy := &model.ApplicationPolicyPlan{ + AppPrimaryKey: app.PrimaryKey(), + Name: EnvBindPolicyDefaultName, + Description: "build-in create", + Type: string(EnvBindPolicy), + Creator: "", + } + var envBindingSpec v1alpha1.EnvBindingSpec + for _, envBind := range envbinds { + envBindingSpec.Envs = append(envBindingSpec.Envs, createEnvBind(*envBind)) + app.EnvBinds = append(app.EnvBinds, createModelEnvBind(*envBind)) + } + properties, err := model.NewJSONStructByStruct(envBindingSpec) + if err != nil { + log.Logger.Errorf("new env binding properties failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + policy.Properties = properties + if err := c.ds.Add(ctx, policy); err != nil { + log.Logger.Errorf("save env binding policy failure,%s", err.Error()) + return nil, err + } + return policy, nil +} + // DetailPolicy detail app policy // TODO: Add status data about the policy. func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) (*apisv1.DetailPolicyResponse, error) { @@ -642,8 +735,8 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return app, nil } -func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app *model.ApplicationPlan) *apisv1.ApplicationPlanBase { - appBeas := &apisv1.ApplicationPlanBase{ +func (c *applicationUsecaseImpl) converAppModelToBase(app *model.ApplicationPlan) *apisv1.ApplicationPlanBase { + appBase := &apisv1.ApplicationPlanBase{ Name: app.Name, Alias: app.Alias, Namespace: app.Namespace, @@ -653,49 +746,28 @@ func (c *applicationUsecaseImpl) converAppModelToBase(ctx context.Context, app * Icon: app.Icon, Labels: app.Labels, } - var policy = model.ApplicationPolicyPlan{ - AppPrimaryKey: app.PrimaryKey(), - Type: string(EnvBindPolicy), - } - policys, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) - if err != nil { - log.Logger.Errorf("query application env binding policy failure %s", err.Error()) - } - for _, policyEntity := range policys { - policy := policyEntity.(*model.ApplicationPolicyPlan) - if policy.Properties != nil { - var envBindingSpec v1alpha1.EnvBindingSpec - if err := json.Unmarshal([]byte(policy.Properties.JSON()), &envBindingSpec); err != nil { - log.Logger.Errorf("unmarshal env binding policy failure %s", err.Error()) - continue - } - for _, env := range envBindingSpec.Envs { - envBind := &apisv1.EnvBind{ - Name: env.Name, - Description: "", - } - if env.Placement.ClusterSelector != nil { - envBind.ClusterSelector = &apisv1.ClusterSelector{ - Name: env.Placement.ClusterSelector.Name, - } - } - if env.Placement.NamespaceSelector != nil && envBind.ClusterSelector != nil { - envBind.ClusterSelector.Namespace = env.Placement.NamespaceSelector.Name - } - appBeas.EnvBind = append(appBeas.EnvBind, envBind) - } + for _, envBind := range app.EnvBinds { + apiEnvBind := &apisv1.EnvBind{ + Name: envBind.Name, + Alias: envBind.Alias, + Description: envBind.Description, + ClusterSelector: apisv1.ClusterSelector(envBind.ClusterSelector), } + if envBind.ComponentSelector != nil { + apiEnvBind.ComponentSelector = (*apisv1.ComponentSelector)(envBind.ComponentSelector) + } + appBase.EnvBind = append(appBase.EnvBind, apiEnvBind) } // TODO: get and render app status - return appBeas + return appBase } -// DeleteApplication delete application -func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *model.ApplicationPlan) error { +// DeleteApplicationPlan delete application plan +func (c *applicationUsecaseImpl) DeleteApplicationPlan(ctx context.Context, app *model.ApplicationPlan) error { // TODO: check app can be deleted // query all components to deleted - components, err := c.ListComponents(ctx, app) + components, err := c.ListComponents(ctx, app, apisv1.ListApplicationComponentOptions{}) if err != nil { return err } @@ -854,3 +926,193 @@ func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.Ap PolicyBase: *c.converPolicyModelToBase(&policy), }, nil } + +// UpdateApplicationEnvBindingPlan update application env binding diff +func (c *applicationUsecaseImpl) UpdateApplicationEnvBindingPlan( + ctx context.Context, + app *model.ApplicationPlan, + envName string, + envUpdate apisv1.PutApplicationPlanEnvRequest) (*apisv1.EnvBind, error) { + // update env-binding policy + envBinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) + if err != nil { + return nil, err + } + for i, env := range envBinding.Envs { + if env.Name == envName { + if envUpdate.ComponentSelector == nil { + envBinding.Envs[i].Selector = nil + } else { + envBinding.Envs[i].Selector = &v1alpha1.EnvSelector{ + Components: envUpdate.ComponentSelector.Components, + } + } + } + } + properties, err := model.NewJSONStructByStruct(envBinding) + if err != nil { + log.Logger.Errorf("new env binding properties failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + policy := &model.ApplicationPolicyPlan{ + AppPrimaryKey: app.PrimaryKey(), + Name: EnvBindPolicyDefaultName, + } + if err := c.ds.Get(ctx, policy); err != nil { + return nil, err + } + policy.Properties = properties + if err := c.ds.Put(ctx, policy); err != nil { + return nil, err + } + var envBind model.EnvBind + // update env-binding base + for i, env := range app.EnvBinds { + if env.Name == envName { + if envUpdate.Description != nil { + app.EnvBinds[i].Description = *envUpdate.Description + } + if envUpdate.Alias != nil { + app.EnvBinds[i].Alias = *envUpdate.Alias + } + if envUpdate.ClusterSelector != nil { + app.EnvBinds[i].ClusterSelector = model.ClusterSelector{ + Name: envUpdate.ClusterSelector.Name, + Namespace: envUpdate.ClusterSelector.Namespace, + } + } + if envUpdate.ComponentSelector == nil { + app.EnvBinds[i].ComponentSelector = nil + } else { + app.EnvBinds[i].ComponentSelector = &model.ComponentSelector{ + Components: envUpdate.ComponentSelector.Components, + } + } + envBind = *app.EnvBinds[i] + } + } + if err := c.ds.Put(ctx, app); err != nil { + return nil, err + } + re := &apisv1.EnvBind{ + Name: envBind.Name, + Alias: envBind.Alias, + Description: envBind.Description, + ClusterSelector: apisv1.ClusterSelector(envBind.ClusterSelector), + } + if envBind.ComponentSelector != nil { + re.ComponentSelector = (*apisv1.ComponentSelector)(envBind.ComponentSelector) + } + return re, nil +} + +// CreateApplicationEnvBindingPlan create application env plan +func (c *applicationUsecaseImpl) CreateApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, envReq apisv1.CreateApplicationEnvPlanRequest) (*apisv1.EnvBind, error) { + for _, env := range app.EnvBinds { + if env.Name == envReq.Name { + return nil, bcode.ErrApplicationEnvExist + } + } + app.EnvBinds = append(app.EnvBinds, createModelEnvBind(envReq.EnvBind)) + envBinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) + if err != nil { + return nil, err + } + envBinding.Envs = append(envBinding.Envs, createEnvBind(envReq.EnvBind)) + properties, err := model.NewJSONStructByStruct(envBinding) + if err != nil { + log.Logger.Errorf("new env binding properties failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + policy := &model.ApplicationPolicyPlan{ + AppPrimaryKey: app.PrimaryKey(), + Name: EnvBindPolicyDefaultName, + } + if err := c.ds.Get(ctx, policy); err != nil { + return nil, err + } + policy.Properties = properties + if err := c.ds.Put(ctx, policy); err != nil { + return nil, err + } + if err := c.ds.Put(ctx, app); err != nil { + return nil, err + } + return &envReq.EnvBind, nil +} + +// DeleteApplicationEnvBindingPlan delete application env binding plan +func (c *applicationUsecaseImpl) DeleteApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, envName string) error { + + for i, envBind := range app.EnvBinds { + if envBind.Name == envName { + app.EnvBinds = append(app.EnvBinds[0:i], app.EnvBinds[i+1:]...) + } + } + envBinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) + if err != nil { + return err + } + for i, envBind := range envBinding.Envs { + if envBind.Name == envName { + envBinding.Envs = append(envBinding.Envs[0:i], envBinding.Envs[i+1:]...) + } + } + properties, err := model.NewJSONStructByStruct(envBinding) + if err != nil { + log.Logger.Errorf("new env binding properties failure,%s", err.Error()) + return bcode.ErrInvalidProperties + } + policy := &model.ApplicationPolicyPlan{ + AppPrimaryKey: app.PrimaryKey(), + Name: EnvBindPolicyDefaultName, + } + if err := c.ds.Get(ctx, policy); err != nil { + return err + } + policy.Properties = properties + if err := c.ds.Put(ctx, policy); err != nil { + return err + } + if err := c.ds.Put(ctx, app); err != nil { + return err + } + return nil +} + +func createEnvBind(envBind apisv1.EnvBind) v1alpha1.EnvConfig { + placement := v1alpha1.EnvPlacement{ + ClusterSelector: &common.ClusterSelector{ + Name: envBind.ClusterSelector.Name, + }, + } + if envBind.ClusterSelector.Namespace != "" { + placement.NamespaceSelector = &v1alpha1.NamespaceSelector{ + Name: envBind.ClusterSelector.Namespace, + } + } + var componentSelector *v1alpha1.EnvSelector + if envBind.ComponentSelector != nil { + componentSelector = &v1alpha1.EnvSelector{ + Components: envBind.ComponentSelector.Components, + } + } + return v1alpha1.EnvConfig{ + Name: envBind.Name, + Placement: placement, + Selector: componentSelector, + } +} + +func createModelEnvBind(envBind apisv1.EnvBind) *model.EnvBind { + re := model.EnvBind{ + Name: envBind.Name, + Description: envBind.Description, + Alias: envBind.Alias, + ClusterSelector: model.ClusterSelector(envBind.ClusterSelector), + } + if envBind.ComponentSelector != nil { + re.ComponentSelector = (*model.ComponentSelector)(envBind.ComponentSelector) + } + return &re +} diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/applicationplan_test.go similarity index 64% rename from pkg/apiserver/rest/usecase/application_test.go rename to pkg/apiserver/rest/usecase/applicationplan_test.go index 3270aec1f..321335cdb 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/applicationplan_test.go @@ -54,12 +54,27 @@ var _ = Describe("Test application usecase function", func() { Name: "test-app", Namespace: "test-app-namespace", Description: "this is a test app", + EnvBind: []*v1.EnvBind{{ + Name: "dev", + Description: "dev env", + ClusterSelector: v1.ClusterSelector{ + Name: "dev", + Namespace: "devnamespace", + }, + }, { + Name: "test", + Description: "test env", + ClusterSelector: v1.ClusterSelector{ + Name: "dev", + Namespace: "testnamespace", + }, + }}, } - base, err := appUsecase.CreateApplication(context.TODO(), req) + base, err := appUsecase.CreateApplicationPlan(context.TODO(), req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) - _, err = appUsecase.CreateApplication(context.TODO(), req) + _, err = appUsecase.CreateApplicationPlan(context.TODO(), req) equal := cmp.Equal(err, bcode.ErrApplicationExist, cmpopts.EquateErrors()) Expect(equal).Should(BeTrue()) @@ -74,7 +89,7 @@ var _ = Describe("Test application usecase function", func() { Labels: map[string]string{"test": "true"}, YamlConfig: string(bs), } - base, err = appUsecase.CreateApplication(context.TODO(), req) + base, err = appUsecase.CreateApplicationPlan(context.TODO(), req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) @@ -86,7 +101,7 @@ var _ = Describe("Test application usecase function", func() { Labels: map[string]string{"test": "true"}, YamlConfig: "asdasdasdasd", } - base, err = appUsecase.CreateApplication(context.TODO(), req) + base, err = appUsecase.CreateApplicationPlan(context.TODO(), req) equal = cmp.Equal(err, bcode.ErrApplicationConfig, cmpopts.EquateErrors()) Expect(equal).Should(BeTrue()) Expect(base).Should(BeNil()) @@ -101,7 +116,7 @@ var _ = Describe("Test application usecase function", func() { Labels: map[string]string{"test": "true"}, YamlConfig: string(bs), } - _, err = appUsecase.CreateApplication(context.TODO(), req) + _, err = appUsecase.CreateApplicationPlan(context.TODO(), req) equal = cmp.Equal(err, bcode.ErrInvalidProperties, cmpopts.EquateErrors()) Expect(equal).Should(BeTrue()) @@ -115,49 +130,68 @@ var _ = Describe("Test application usecase function", func() { EnvBind: []*v1.EnvBind{ { Name: "dev", + Alias: "Chinese Word", Description: "This is a dev env", - ClusterSelector: &v1.ClusterSelector{ + ClusterSelector: v1.ClusterSelector{ Name: "dev-cluster", }, }, { Name: "prob", Description: "This is a prob env", - ClusterSelector: &v1.ClusterSelector{ + ClusterSelector: v1.ClusterSelector{ Name: "prob-cluster", Namespace: "prob", }, }, }, } - appBase, err := appUsecase.CreateApplication(context.TODO(), req) + appBase, err := appUsecase.CreateApplicationPlan(context.TODO(), req) Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(appBase.EnvBind), 2)).Should(BeEmpty()) - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd4") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - Expect(cmp.Diff(len(appBase.EnvBind), 2)).Should(BeEmpty()) + + }) + + It("Test GetApplicationPlanEnvBindingPolicy", func() { + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd4") + Expect(err).Should(BeNil()) + envBinding, err := appUsecase.GetApplicationPlanEnvBindingPolicy(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(envBinding.Envs), 2)).Should(BeEmpty()) + }) + + It("Test UpdateApplicationPlanEnvBindingDiff", func() { + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + _, err = appUsecase.UpdateApplicationEnvBindingPlan(context.TODO(), appModel, "staging", v1.PutApplicationPlanEnvRequest{ + ComponentSelector: &v1.ComponentSelector{Components: []string{"hello-world-server"}}, + }) + Expect(err).Should(BeNil()) }) It("Test ListApplications function", func() { - apps, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioPlanOptions{}) + apps, err := appUsecase.ListApplicationPlans(context.TODO(), v1.ListApplicatioPlanOptions{}) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(apps), 3)).Should(BeEmpty()) }) It("Test DetailApplication function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - detail, err := appUsecase.DetailApplication(context.TODO(), appModel) + detail, err := appUsecase.DetailApplicationPlan(context.TODO(), appModel) Expect(err).Should(BeNil()) Expect(cmp.Diff(detail.ResourceInfo.ComponentNum, 2)).Should(BeEmpty()) Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) }) It("Test GetWorkflow function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -167,7 +201,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test ListPolicies function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -179,19 +213,33 @@ var _ = Describe("Test application usecase function", func() { }) It("Test ListComponents function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - components, err := appUsecase.ListComponents(context.TODO(), appModel) + components, err := appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{}) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(components), 2)).Should(BeEmpty()) Expect(cmp.Diff(components[0].ComponentType, "worker")).Should(BeEmpty()) Expect(components[1].UpdateTime).ShouldNot(BeNil()) + + components, err = appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{ + EnvName: "test", + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 1)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].Name, "data-worker")).Should(BeEmpty()) + + components, err = appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{ + EnvName: "staging", + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 1)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].Name, "hello-world-server")).Should(BeEmpty()) }) It("Test DetailComponent function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -203,18 +251,18 @@ var _ = Describe("Test application usecase function", func() { }) It("Test DetailPolicy function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, "example-multi-env-policy") + detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, EnvBindPolicyDefaultName) Expect(err).Should(BeNil()) Expect(cmp.Diff(detail.Type, "env-binding")).Should(BeEmpty()) Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) }) It("Test AddComponent function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) base, err := appUsecase.AddComponent(context.TODO(), appModel, v1.CreateComponentPlanRequest{ @@ -230,7 +278,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test DetailComponent function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) detailResponse, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") @@ -241,11 +289,11 @@ var _ = Describe("Test application usecase function", func() { }) It("Test AddPolicy function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ - Name: "example-multi-env-policy", + Name: EnvBindPolicyDefaultName, Description: "this is a test2 policy", Type: "env-binding", Properties: ``, @@ -259,8 +307,9 @@ var _ = Describe("Test application usecase function", func() { }) Expect(err).Should(BeNil()) }) + It("Test DetailPolicy function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, "env-binding-2") @@ -268,8 +317,9 @@ var _ = Describe("Test application usecase function", func() { Expect(detail.Properties).ShouldNot(BeNil()) Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) }) + It("Test UpdatePolicy function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) base, err := appUsecase.UpdatePolicy(context.TODO(), appModel, "env-binding-2", v1.UpdatePolicyRequest{ @@ -281,21 +331,75 @@ var _ = Describe("Test application usecase function", func() { Expect((*base.Properties)["envs"]).Should(BeEmpty()) }) It("Test DeletePolicy function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) err = appUsecase.DeletePolicy(context.TODO(), appModel, "env-binding-2") Expect(err).Should(BeNil()) }) It("Test DeleteComponent function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) err = appUsecase.DeleteComponent(context.TODO(), appModel, "test2") Expect(err).Should(BeNil()) }) + + It("Test CreateApplicationEnvBindingPlan function", func() { + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + env, err := appUsecase.CreateApplicationEnvBindingPlan(context.TODO(), appModel, v1.CreateApplicationEnvPlanRequest{ + EnvBind: v1.EnvBind{ + Name: "prod2", + Alias: "生产环境", + Description: "这是一个用户某客户的生产环境", + ClusterSelector: v1.ClusterSelector{ + Name: "prob", + }, + }, + }) + Expect(err).Should(BeNil()) + Expect(env).ShouldNot(BeNil()) + + appModelNew, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(appModelNew.EnvBinds), 4)).Should(BeEmpty()) + + spec, err := appUsecase.GetApplicationPlanEnvBindingPolicy(context.TODO(), appModelNew) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(spec.Envs), 4)).Should(BeEmpty()) + }) + + It("Test CreateApplicationEnvBindingPlan function", func() { + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + env, err := appUsecase.UpdateApplicationEnvBindingPlan(context.TODO(), appModel, "prod2", v1.PutApplicationPlanEnvRequest{ + ComponentSelector: &v1.ComponentSelector{ + Components: []string{}, + }, + }) + Expect(err).Should(BeNil()) + Expect(env).ShouldNot(BeNil()) + + components, err := appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{ + EnvName: "prod2", + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 0)).Should(BeEmpty()) + }) + + It("Test DeleteApplicationEnvBindingPlan function", func() { + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + err = appUsecase.DeleteApplicationEnvBindingPlan(context.TODO(), appModel, "prod2") + Expect(err).Should(BeNil()) + }) + It("Test Deploy Application function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) res, err := appUsecase.Deploy(context.TODO(), appModel, v1.ApplicationDeployRequest{ @@ -311,12 +415,13 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty()) Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty()) }) + It("Test DeleteApplication function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) - err = appUsecase.DeleteApplication(context.TODO(), appModel) + err = appUsecase.DeleteApplicationPlan(context.TODO(), appModel) Expect(err).Should(BeNil()) - components, err := appUsecase.ListComponents(context.TODO(), appModel) + components, err := appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{}) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(components), 0)).Should(BeEmpty()) policies, err := appUsecase.ListPolicies(context.TODO(), appModel) diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index 2dcb3d142..3d2f06037 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -23,7 +23,7 @@ var ErrApplicationConfig = NewBcode(400, 10000, "application config does not com var ErrComponentTypeNotSupport = NewBcode(400, 10001, "An unsupported component type was used.") // ErrApplicationExist application is exist -var ErrApplicationExist = NewBcode(400, 10002, "application name is exist") +var ErrApplicationExist = NewBcode(400, 10002, "application plan name is exist") // ErrInvalidProperties properties(trait or component or others) is invalid var ErrInvalidProperties = NewBcode(400, 10003, "properties is invalid") @@ -38,10 +38,10 @@ var ErrDeployApplyFail = NewBcode(500, 10005, "application deploy apply failure" var ErrNoComponent = NewBcode(200, 10006, "application not have components, can not deploy") // ErrApplicationComponetExist application component is exist -var ErrApplicationComponetExist = NewBcode(400, 10007, "application component is exist") +var ErrApplicationComponetExist = NewBcode(400, 10007, "application component plan is exist") // ErrApplicationComponetNotExist application component is not exist -var ErrApplicationComponetNotExist = NewBcode(404, 10008, "application component is not exist") +var ErrApplicationComponetNotExist = NewBcode(404, 10008, "application component plan is not exist") // ErrApplicationPolicyExist application policy is exist var ErrApplicationPolicyExist = NewBcode(400, 10009, "application policy is exist") @@ -51,3 +51,12 @@ var ErrApplicationPolicyNotExist = NewBcode(404, 10010, "application policy is n // ErrCreateNamespace auto create namespace failure before deploy app var ErrCreateNamespace = NewBcode(500, 10011, "auto create namespace failure") + +// ErrApplicationNotExist application is not exist +var ErrApplicationNotExist = NewBcode(404, 10012, "application plan name is not exist") + +// ErrApplicationNotEnv no env binding policy +var ErrApplicationNotEnv = NewBcode(404, 10013, "application plan not set env binding policy") + +// ErrApplicationEnvExist application env is exist +var ErrApplicationEnvExist = NewBcode(400, 10014, "application env plan is exist") diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/applicationplan.go similarity index 59% rename from pkg/apiserver/rest/webservice/application.go rename to pkg/apiserver/rest/webservice/applicationplan.go index b23f15da5..f7e4268d9 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/applicationplan.go @@ -29,18 +29,18 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) -type applicationWebService struct { +type applicationPlanWebService struct { applicationUsecase usecase.ApplicationUsecase } -// NewApplicationWebService new application manage webservice -func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase) WebService { - return &applicationWebService{ +// NewApplicationPlanWebService new application manage webservice +func NewApplicationPlanWebService(applicationUsecase usecase.ApplicationUsecase) WebService { + return &applicationPlanWebService{ applicationUsecase: applicationUsecase, } } -func (c *applicationWebService) GetWebService() *restful.WebService { +func (c *applicationPlanWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) ws.Path(versionPrefix+"/applicationplans"). Consumes(restful.MIME_XML, restful.MIME_JSON). @@ -49,8 +49,8 @@ func (c *applicationWebService) GetWebService() *restful.WebService { tags := []string{"application"} - ws.Route(ws.GET("/").To(c.listApplications). - Doc("list all applications"). + ws.Route(ws.GET("/").To(c.listApplicationPlans). + Doc("list all application plans"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). Param(ws.QueryParameter("namespace", "Namespace-based search").DataType("string")). @@ -59,37 +59,80 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.ListApplicationPlanResponse{})) - ws.Route(ws.POST("/").To(c.createApplication). - Doc("create one application"). + ws.Route(ws.POST("/").To(c.createApplicationPlan). + Doc("create one application plan"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateApplicationPlanRequest{}). Returns(200, "", apis.ApplicationPlanBase{}). Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationPlanBase{})) - ws.Route(ws.DELETE("/{name}").To(c.deleteApplication). + ws.Route(ws.DELETE("/{name}").To(c.deleteApplicationPlan). Doc("delete one application"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). Returns(200, "", apis.EmptyResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}").To(c.detailApplication). - Doc("detail one application"). + ws.Route(ws.GET("/{name}").To(c.detailApplicationPlan). + Doc("detail one application plan"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). Returns(200, "", apis.DetailApplicationPlanResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.DetailApplicationPlanResponse{})) + ws.Route(ws.PUT("/{name}").To(c.updateApplicationPlan). + Doc("update one application plan"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Reads(apis.UpdateApplicationPlanRequest{}). + Returns(200, "", apis.ApplicationPlanBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationPlanBase{})) + + ws.Route(ws.PUT("/{name}/envs/{envName}").To(c.updateApplicationEnvBinding). + Doc("set application plan differences in the specified environment"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the application plan").DataType("string")). + Reads(apis.PutApplicationPlanEnvRequest{}). + Returns(200, "", apis.EnvBind{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EnvBind{})) + + ws.Route(ws.POST("/{name}/envs").To(c.createApplicationEnv). + Doc("creating an application environment plan"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Reads(apis.CreateApplicationEnvPlanRequest{}). + Returns(200, "", apis.EnvBind{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.DELETE("/{name}/envs/{envName}").To(c.deleteApplicationEnv). + Doc("delete an application environment plan"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the application plan").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). + Returns(404, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + ws.Route(ws.POST("/{name}/template").To(c.publishApplicationTemplate). Doc("create one application template"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). Reads(apis.CreateApplicationTemplateRequest{}). Returns(200, "", apis.ApplicationTemplateBase{}). Returns(400, "", bcode.Bcode{}). @@ -99,7 +142,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Doc("deploy or upgrade the application"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). Returns(200, "", apis.ApplicationDeployRequest{}). Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationDeployResponse{})) @@ -107,7 +150,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/{name}/componentplans").To(c.listApplicationComponents). Doc("gets the componentplan topology of the application"). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). Param(ws.QueryParameter("envName", "list components that deployed in define env").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.ComponentPlanListResponse{}). @@ -117,7 +160,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/{name}/componentplans").To(c.createComponent). Doc("create component plan for application plan"). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateComponentPlanRequest{}). Returns(200, "", apis.ComponentPlanBase{}). @@ -127,7 +170,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/{name}/componentplans/{componentName}").To(c.detailComponent). Doc("detail component plan for application plan"). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.DetailComponentPlanResponse{}). Returns(400, "", bcode.Bcode{}). @@ -136,7 +179,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/{name}/policies").To(c.listApplicationPolicies). Doc("list policy for application"). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.ListApplicationPolicy{}). Returns(400, "", bcode.Bcode{}). @@ -185,8 +228,8 @@ func (c *applicationWebService) GetWebService() *restful.WebService { return ws } -func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - app, err := c.applicationUsecase.GetApplication(req.Request.Context(), req.PathParameter("name")) +func (c *applicationPlanWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app, err := c.applicationUsecase.GetApplicationPlan(req.Request.Context(), req.PathParameter("name")) if err != nil { bcode.ReturnError(req, res, err) return @@ -195,7 +238,24 @@ func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restfu chain.ProcessFilter(req, res) } -func (c *applicationWebService) createApplication(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) envCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) + envBinding, err := c.applicationUsecase.GetApplicationPlanEnvBindingPolicy(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + for _, env := range envBinding.Envs { + if env.Name == req.PathParameter("envName") { + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplicationEnvBinding, env)) + chain.ProcessFilter(req, res) + return + } + } + bcode.ReturnError(req, res, bcode.ErrApplicationNotEnv) +} + +func (c *applicationPlanWebService) createApplicationPlan(req *restful.Request, res *restful.Response) { // Verify the validity of parameters var createReq apis.CreateApplicationPlanRequest if err := req.ReadEntity(&createReq); err != nil { @@ -207,7 +267,7 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res return } // Call the usecase layer code - appBase, err := c.applicationUsecase.CreateApplication(req.Request.Context(), createReq) + appBase, err := c.applicationUsecase.CreateApplicationPlan(req.Request.Context(), createReq) if err != nil { log.Logger.Errorf("create application failure %s", err.Error()) bcode.ReturnError(req, res, err) @@ -221,8 +281,8 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res } } -func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { - apps, err := c.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioPlanOptions{ +func (c *applicationPlanWebService) listApplicationPlans(req *restful.Request, res *restful.Response) { + apps, err := c.applicationUsecase.ListApplicationPlans(req.Request.Context(), apis.ListApplicatioPlanOptions{ Namespace: req.QueryParameter("namespace"), Cluster: req.QueryParameter("cluster"), Query: req.QueryParameter("query"), @@ -237,9 +297,9 @@ func (c *applicationWebService) listApplications(req *restful.Request, res *rest } } -func (c *applicationWebService) detailApplication(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) detailApplicationPlan(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) - detail, err := c.applicationUsecase.DetailApplication(req.Request.Context(), app) + detail, err := c.applicationUsecase.DetailApplicationPlan(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) return @@ -250,7 +310,7 @@ func (c *applicationWebService) detailApplication(req *restful.Request, res *res } } -func (c *applicationWebService) publishApplicationTemplate(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) publishApplicationTemplate(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) base, err := c.applicationUsecase.PublishApplicationTemplate(req.Request.Context(), app) if err != nil { @@ -264,7 +324,7 @@ func (c *applicationWebService) publishApplicationTemplate(req *restful.Request, } // deployApplication TODO: return event model -func (c *applicationWebService) deployApplication(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) deployApplication(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) // Verify the validity of parameters var createReq apis.ApplicationDeployRequest @@ -287,9 +347,9 @@ func (c *applicationWebService) deployApplication(req *restful.Request, res *res } } -func (c *applicationWebService) deleteApplication(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) deleteApplicationPlan(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) - err := c.applicationUsecase.DeleteApplication(req.Request.Context(), app) + err := c.applicationUsecase.DeleteApplicationPlan(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) return @@ -300,9 +360,11 @@ func (c *applicationWebService) deleteApplication(req *restful.Request, res *res } } -func (c *applicationWebService) listApplicationComponents(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) listApplicationComponents(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) - components, err := c.applicationUsecase.ListComponents(req.Request.Context(), app) + components, err := c.applicationUsecase.ListComponents(req.Request.Context(), app, apis.ListApplicationComponentOptions{ + EnvName: req.QueryParameter("envName"), + }) if err != nil { bcode.ReturnError(req, res, err) return @@ -313,7 +375,7 @@ func (c *applicationWebService) listApplicationComponents(req *restful.Request, } } -func (c *applicationWebService) createComponent(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) createComponent(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) // Verify the validity of parameters var createReq apis.CreateComponentPlanRequest @@ -336,7 +398,7 @@ func (c *applicationWebService) createComponent(req *restful.Request, res *restf } } -func (c *applicationWebService) detailComponent(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) detailComponent(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) detail, err := c.applicationUsecase.DetailComponent(req.Request.Context(), app, req.PathParameter("componentName")) if err != nil { @@ -349,7 +411,7 @@ func (c *applicationWebService) detailComponent(req *restful.Request, res *restf } } -func (c *applicationWebService) createApplicationPolicy(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) createApplicationPolicy(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) // Verify the validity of parameters var createReq apis.CreatePolicyRequest @@ -372,7 +434,7 @@ func (c *applicationWebService) createApplicationPolicy(req *restful.Request, re } } -func (c *applicationWebService) listApplicationPolicies(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) listApplicationPolicies(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) policies, err := c.applicationUsecase.ListPolicies(req.Request.Context(), app) if err != nil { @@ -385,7 +447,7 @@ func (c *applicationWebService) listApplicationPolicies(req *restful.Request, re } } -func (c *applicationWebService) detailApplicationPolicy(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) detailApplicationPolicy(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) detail, err := c.applicationUsecase.DetailPolicy(req.Request.Context(), app, req.PathParameter("policyName")) if err != nil { @@ -398,7 +460,7 @@ func (c *applicationWebService) detailApplicationPolicy(req *restful.Request, re } } -func (c *applicationWebService) deleteApplicationPolicy(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) deleteApplicationPolicy(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) err := c.applicationUsecase.DeletePolicy(req.Request.Context(), app, req.PathParameter("policyName")) if err != nil { @@ -411,7 +473,7 @@ func (c *applicationWebService) deleteApplicationPolicy(req *restful.Request, re } } -func (c *applicationWebService) updateApplicationPolicy(req *restful.Request, res *restful.Response) { +func (c *applicationPlanWebService) updateApplicationPolicy(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) // Verify the validity of parameters var updateReq apis.UpdatePolicyRequest @@ -433,3 +495,85 @@ func (c *applicationWebService) updateApplicationPolicy(req *restful.Request, re return } } + +func (c *applicationPlanWebService) updateApplicationEnvBinding(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) + // Verify the validity of parameters + var updateReq apis.PutApplicationPlanEnvRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + diff, err := c.applicationUsecase.UpdateApplicationEnvBindingPlan(req.Request.Context(), app, req.PathParameter("envName"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(diff); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationPlanWebService) updateApplicationPlan(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) + // Verify the validity of parameters + var updateReq apis.UpdateApplicationPlanRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + base, err := c.applicationUsecase.UpdateApplicationPlan(req.Request.Context(), app, updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationPlanWebService) createApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) + // Verify the validity of parameters + var createReq apis.CreateApplicationEnvPlanRequest + 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 + } + base, err := c.applicationUsecase.CreateApplicationEnvBindingPlan(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationPlanWebService) deleteApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) + err := c.applicationUsecase.DeleteApplicationEnvBindingPlan(req.Request.Context(), app, req.PathParameter("envName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/validate_test.go b/pkg/apiserver/rest/webservice/validate_test.go index 1113e6b5e..8605450f7 100644 --- a/pkg/apiserver/rest/webservice/validate_test.go +++ b/pkg/apiserver/rest/webservice/validate_test.go @@ -52,5 +52,19 @@ var _ = Describe("Test validate function", func() { } err = validate.Struct(&app3) Expect(err).Should(BeNil()) + + var app4 = apisv1.CreateApplicationPlanRequest{ + Name: "asdasd-asdasd", + Namespace: "namespace", + } + err = validate.Struct(&app4) + Expect(err).Should(BeNil()) + + var component = apisv1.CreateComponentPlanRequest{ + Name: "asdasd-asdasd", + ComponentType: "alibaba-ack", + } + err = validate.Struct(&component) + Expect(err).Should(BeNil()) }) }) diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 6915f595c..f7c93553e 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -68,7 +68,7 @@ func Init(ctx context.Context, ds datastore.DataStore) { definitionUsecase := usecase.NewDefinitionUsecase() addonUsecase := usecase.NewAddonUsecase(ds) RegistWebService(NewClusterWebService(clusterUsecase)) - RegistWebService(NewApplicationWebService(applicationUsecase)) + RegistWebService(NewApplicationPlanWebService(applicationUsecase)) RegistWebService(NewNamespaceWebService(namespaceUsecase)) RegistWebService(NewDefinitionWebservice(definitionUsecase)) RegistWebService(NewAddonWebService(addonUsecase)) diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 30bcedd26..4ee1cf41e 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -125,7 +125,7 @@ func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res bcode.ReturnError(req, res, bcode.ErrMustQueryByApp) return } - app, err := w.applicationUsecase.GetApplication(req.Request.Context(), req.QueryParameter("appName")) + app, err := w.applicationUsecase.GetApplicationPlan(req.Request.Context(), req.QueryParameter("appName")) if err != nil { bcode.ReturnError(req, res, err) return @@ -157,7 +157,7 @@ func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res bcode.ReturnError(req, res, err) return } - app, err := w.applicationUsecase.GetApplication(req.Request.Context(), createReq.AppName) + app, err := w.applicationUsecase.GetApplicationPlan(req.Request.Context(), createReq.AppName) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index 5bf6b078b..71a0b2fb6 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -42,7 +42,7 @@ var _ = Describe("Test application rest api", func() { Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, - EnvBind: []*apisv1.EnvBind{{Name: "dev-env", ClusterSelector: &apisv1.ClusterSelector{ + EnvBind: []*apisv1.EnvBind{{Name: "dev-env", ClusterSelector: apisv1.ClusterSelector{ Name: "dev-cluster", }}}, } diff --git a/test/e2e-apiserver-test/testdata/workflow.json b/test/e2e-apiserver-test/testdata/workflow.json new file mode 100644 index 000000000..e218e2bc1 --- /dev/null +++ b/test/e2e-apiserver-test/testdata/workflow.json @@ -0,0 +1,14 @@ +{ + "appName": "appplanName", + "name": "workflowName", + "alias": "workflowAlias", + "description": "workflow plan description", + "enable": true, + "default": true, + "steps": [ + { "name": "deploy-test", + "type": "deploy2env", + "properties":"" + } + ] +} \ No newline at end of file From c821f2a929b9c8b4da1517d97dc91d0c3521b40c Mon Sep 17 00:00:00 2001 From: Hongchao Deng Date: Fri, 5 Nov 2021 05:39:31 -0400 Subject: [PATCH 19/59] Feat: rewrite Addon API to support new format (#2605) * Feat: rewrite Addon API to support new format fix test fix fix add registry query parameter skip enable test add Definition meta fix fix ext add cue rendering return component name update swagger.json refactor addon cue/yaml tmpl to addonElementFile apply app in enable fix cue parse comment swagger enable test update fix bug fix cue render fix apply fail, decode object fix disable addon, todo: fix status api * avoid to render a whole addon when check status * fix * fix * add label * remove todo * fluxcd * fix args * add all path to addon component name * reorder test * add addon application prefix * add * add ns when test * fix Co-authored-by: qiaozp --- docs/apidoc/swagger.json | 412 +++++++++++++-- pkg/apiserver/rest/apis/v1/types.go | 46 +- pkg/apiserver/rest/usecase/addon.go | 490 +++++++++++------- pkg/apiserver/rest/utils/bcode/addon.go | 7 - pkg/apiserver/rest/utils/convert.go | 10 + pkg/apiserver/rest/webservice/addon.go | 6 +- .../rest/webservice/addon_registry.go | 2 - pkg/oam/labels.go | 3 + test/e2e-apiserver-test/addon_test.go | 63 ++- 9 files changed, 793 insertions(+), 246 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index a9493a2f1..212415471 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -135,6 +135,12 @@ "summary": "list all addons", "operationId": "listAddons", "parameters": [ + { + "type": "string", + "description": "filter addons from given registry", + "name": "registry", + "in": "query" + }, { "type": "string", "description": "Fuzzy search based on name and description.", @@ -248,6 +254,14 @@ "summary": "enable an addon", "operationId": "enableAddon", "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.EnableAddonRequest" + } + }, { "type": "string", "description": "addon name to enable", @@ -2247,8 +2261,8 @@ "required": [ "rollingState", "batchRollingState", - "upgradedReplicas", "currentBatch", + "upgradedReplicas", "upgradedReadyReplicas", "lastTargetAppRevision" ], @@ -2939,6 +2953,196 @@ } } }, + "regexp.Regexp": { + "required": [ + "expr", + "prog", + "onepass", + "numSubexp", + "maxBitStateLen", + "subexpNames", + "prefix", + "prefixBytes", + "prefixRune", + "prefixEnd", + "mpool", + "matchcap", + "prefixComplete", + "cond", + "minInputLen", + "longest" + ], + "properties": { + "cond": { + "type": "integer", + "format": "byte" + }, + "expr": { + "type": "string" + }, + "longest": { + "type": "boolean" + }, + "matchcap": { + "type": "integer", + "format": "int32" + }, + "maxBitStateLen": { + "type": "integer", + "format": "int32" + }, + "minInputLen": { + "type": "integer", + "format": "int32" + }, + "mpool": { + "type": "integer", + "format": "int32" + }, + "numSubexp": { + "type": "integer", + "format": "int32" + }, + "onepass": { + "$ref": "#/definitions/regexp.onePassProg" + }, + "prefix": { + "type": "string" + }, + "prefixBytes": { + "type": "string" + }, + "prefixComplete": { + "type": "boolean" + }, + "prefixEnd": { + "type": "integer", + "format": "integer" + }, + "prefixRune": { + "type": "integer", + "format": "int32" + }, + "prog": { + "$ref": "#/definitions/syntax.Prog" + }, + "subexpNames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "regexp.onePassInst": { + "required": [ + "Op", + "Out", + "Arg", + "Rune", + "Next" + ], + "properties": { + "Arg": { + "type": "integer", + "format": "integer" + }, + "Next": { + "type": "array", + "items": { + "type": "integer" + } + }, + "Op": { + "type": "integer", + "format": "byte" + }, + "Out": { + "type": "integer", + "format": "integer" + }, + "Rune": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "regexp.onePassProg": { + "required": [ + "Inst", + "Start", + "NumCap" + ], + "properties": { + "Inst": { + "type": "array", + "items": { + "$ref": "#/definitions/regexp.onePassInst" + } + }, + "NumCap": { + "type": "integer", + "format": "int32" + }, + "Start": { + "type": "integer", + "format": "int32" + } + } + }, + "syntax.Inst": { + "required": [ + "Op", + "Out", + "Arg", + "Rune" + ], + "properties": { + "Arg": { + "type": "integer", + "format": "integer" + }, + "Op": { + "type": "integer", + "format": "byte" + }, + "Out": { + "type": "integer", + "format": "integer" + }, + "Rune": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "syntax.Prog": { + "required": [ + "Inst", + "Start", + "NumCap" + ], + "properties": { + "Inst": { + "type": "array", + "items": { + "$ref": "#/definitions/syntax.Inst" + } + }, + "NumCap": { + "type": "integer", + "format": "int32" + }, + "Start": { + "type": "integer", + "format": "int32" + } + } + }, "types.Parameter": { "required": [ "name" @@ -2989,15 +3193,58 @@ } } }, + "v1.AddonDependency": { + "properties": { + "name": { + "type": "string" + } + } + }, + "v1.AddonDeployTo": { + "required": [ + "control_plane", + "runtime_cluster" + ], + "properties": { + "control_plane": { + "type": "boolean" + }, + "runtime_cluster": { + "type": "boolean" + } + } + }, + "v1.AddonElementFile": { + "required": [ + "Data", + "Name" + ], + "properties": { + "Data": { + "type": "string" + }, + "Name": { + "type": "string" + } + } + }, "v1.AddonMeta": { "required": [ "name", "version", "description", - "icon", - "tags" + "icon" ], "properties": { + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonDependency" + } + }, + "deploy_to": { + "$ref": "#/definitions/v1.AddonDeployTo" + }, "description": { "type": "string" }, @@ -3013,6 +3260,9 @@ "type": "string" } }, + "url": { + "type": "string" + }, "version": { "type": "string" } @@ -3528,10 +3778,10 @@ }, "v1.CreateApplicationEnvPlanRequest": { "required": [ + "clusterSelector", "componentSelector", "name", - "alias", - "clusterSelector" + "alias" ], "properties": { "alias": { @@ -3820,6 +4070,24 @@ } } }, + "v1.Definition": { + "required": [ + "kind", + "name", + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "v1.DefinitionBase": { "required": [ "name", @@ -3840,18 +4108,55 @@ }, "v1.DefinitionProperties": { "required": [ - "default", - "description", "title", - "type" + "type", + "compiledPattern" ], "properties": { + "compiledPattern": { + "$ref": "#/definitions/regexp.Regexp" + }, "default": { - "type": "string" + "$ref": "#/definitions/v1.DefinitionProperties.default" }, "description": { "type": "string" }, + "enum": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.DefinitionProperties.enum" + } + }, + "example": { + "$ref": "#/definitions/v1.DefinitionProperties.example" + }, + "items": { + "$ref": "#/definitions/v1.DefinitionSchema" + }, + "maxLength": { + "type": "integer", + "format": "integer" + }, + "maximum": { + "type": "number", + "format": "double" + }, + "minLength": { + "type": "integer", + "format": "integer" + }, + "minimum": { + "type": "number", + "format": "double" + }, + "multipleOf": { + "type": "number", + "format": "double" + }, + "pattern": { + "type": "string" + }, "title": { "type": "string" }, @@ -3860,6 +4165,9 @@ } } }, + "v1.DefinitionProperties.default": {}, + "v1.DefinitionProperties.enum": {}, + "v1.DefinitionProperties.example": {}, "v1.DefinitionSchema": { "required": [ "properties", @@ -3890,11 +4198,31 @@ "version", "description", "icon", - "tags" + "definitions", + "parameters", + "cue_templates" ], "properties": { - "deploy_data": { - "type": "string" + "cue_templates": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonElementFile" + } + }, + "definitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.Definition" + } + }, + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonDependency" + } + }, + "deploy_to": { + "$ref": "#/definitions/v1.AddonDeployTo" }, "description": { "type": "string" @@ -3908,28 +4236,40 @@ "name": { "type": "string" }, + "parameters": { + "type": "string" + }, "tags": { "type": "array", "items": { "type": "string" } }, + "url": { + "type": "string" + }, "version": { "type": "string" + }, + "yaml_templates": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonElementFile" + } } } }, "v1.DetailApplicationPlanResponse": { "required": [ - "name", + "createTime", + "status", "namespace", - "icon", - "gatewayRule", "alias", "description", - "createTime", "updateTime", - "status", + "icon", + "gatewayRule", + "name", "policies", "status", "resourceInfo", @@ -3999,20 +4339,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "reason", - "provider", - "updateTime", - "description", - "dashboardURL", - "kubeConfigSecret", - "createTime", "name", "icon", - "apiServerURL", + "provider", + "kubeConfigSecret", "alias", - "labels", "status", + "labels", + "description", + "reason", + "apiServerURL", + "dashboardURL", "kubeConfig", + "createTime", + "updateTime", "resourceInfo" ], "properties": { @@ -4070,12 +4410,12 @@ }, "v1.DetailComponentPlanResponse": { "required": [ + "creator", "createTime", "updateTime", + "name", "alias", "appPrimaryKey", - "creator", - "name", "type" ], "properties": { @@ -4164,13 +4504,13 @@ }, "v1.DetailPolicyResponse": { "required": [ + "creator", "properties", "createTime", "updateTime", "name", "type", - "description", - "creator" + "description" ], "properties": { "createTime": { @@ -4200,13 +4540,13 @@ }, "v1.DetailWorkflowPlanResponse": { "required": [ + "alias", "description", "enable", "default", "createTime", "updateTime", "name", - "alias", "workflowRecord" ], "properties": { @@ -4294,6 +4634,16 @@ } }, "v1.EmptyResponse": {}, + "v1.EnableAddonRequest": { + "properties": { + "args": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "v1.EnablingProgress": { "required": [ "enabled_components", diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index b970e6a50..01eaaf57f 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -78,13 +78,41 @@ type ListAddonResponse struct { Addons []*AddonMeta `json:"addons"` } +// AddonDeployTo defines where the addon to deploy to +type AddonDeployTo struct { + ControlPlane bool `json:"control_plane"` + RuntimeCluster bool `json:"runtime_cluster"` +} + +// AddonDependency defines the other addons it depends on +type AddonDependency struct { + Name string `json:"name,omitempty"` +} + // AddonMeta defines the format for a single addon type AddonMeta struct { - Name string `json:"name"` - Version string `json:"version"` - Description string `json:"description"` - Icon string `json:"icon"` - Tags []string `json:"tags"` + Name string `json:"name" validate:"required"` + Version string `json:"version"` + Description string `json:"description"` + Icon string `json:"icon"` + URL string `json:"url,omitempty"` + Tags []string `json:"tags,omitempty"` + DeployTo *AddonDeployTo `json:"deploy_to,omitempty"` + Dependencies []*AddonDependency `json:"dependencies,omitempty"` +} + +// Definition defines the metadata for a single X-Definition +type Definition struct { + Kind string `json:"kind"` + Name string `json:"name"` + Description string `json:"description"` +} + +// AddonElementFile can be addon's definition or addon's component +type AddonElementFile struct { + Data string + Name string + Path []string } // DetailAddonResponse defines the format for showing the addon details @@ -94,8 +122,12 @@ type DetailAddonResponse struct { // More details about the addon, e.g. README Detail string `json:"detail,omitempty"` - // DeployData is the object to apply to enable addon, e.g. Application - DeployData string `json:"deploy_data,omitempty"` + Definitions []*Definition `json:"definitions"` + + Parameters string `json:"parameters"` + + CUETemplates []AddonElementFile `json:"cue_templates"` + YAMLTemplates []AddonElementFile `json:"yaml_templates,omitempty"` } // AddonStatusResponse defines the format of addon status response diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 0a1c3af8f..64ad60db1 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -1,24 +1,26 @@ package usecase import ( - "bytes" "context" + "encoding/json" "errors" "fmt" "net/url" "path" + "path/filepath" "sort" "strings" - "text/template" "time" - "github.com/Masterminds/sprig" + cueyaml "cuelang.org/go/encoding/yaml" "github.com/google/go-github/v32/github" "golang.org/x/oauth2" errors2 "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" @@ -28,18 +30,30 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + restapis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" restutils "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + cuemodel "github.com/oam-dev/kubevela/pkg/cue/model" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils" addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/apply" ) const ( - // AddonFileName is the addon file name - AddonFileName string = "addon.yaml" // AddonReadmeFileName is the addon readme file name AddonReadmeFileName string = "readme.md" + + // AddonMetadataFileName is the addon meatadata.yaml file name + AddonMetadataFileName string = "metadata.yaml" + + // AddonTemplateDirName is the addon template/ dir name + AddonTemplateDirName string = "template" + + // AddonDefinitionsDirName is the addon definitions/ dir name + AddonDefinitionsDirName string = "definitions" ) // AddonUsecase addon usecase @@ -48,9 +62,9 @@ type AddonUsecase interface { CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) DeleteAddonRegistry(ctx context.Context, name string) error ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) - ListAddons(ctx context.Context, detailed bool, query string) ([]*apis.DetailAddonResponse, error) + ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) StatusAddon(name string) (*apis.AddonStatusResponse, error) - GetAddon(ctx context.Context, name string) (*apis.DetailAddonResponse, error) + GetAddon(ctx context.Context, name string, registry string, detailed bool) (*apis.DetailAddonResponse, error) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error DisableAddon(ctx context.Context, name string) error } @@ -74,8 +88,9 @@ type addonUsecaseImpl struct { apply apply.Applicator } -func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string) (*apis.DetailAddonResponse, error) { - addons, err := u.ListAddons(ctx, true, "") +// GetAddon will get addon information, if detailed is not set, addon's componennt and internal definition won't be returned +func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string, registry string, detailed bool) (*apis.DetailAddonResponse, error) { + addons, err := u.ListAddons(ctx, detailed, registry, "") if err != nil { return nil, err } @@ -89,15 +104,10 @@ func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string) (*apis.Det } func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, error) { - _, err := u.GetAddon(context.TODO(), name) - if err != nil { - return nil, err - } - var app v1beta1.Application - err = u.kubeClient.Get(context.Background(), client.ObjectKey{ + err := u.kubeClient.Get(context.Background(), client.ObjectKey{ Namespace: types.DefaultKubeVelaNS, - Name: addonutil.TransAddonName(name), + Name: restutils.AddonName2AppName(name), }, &app) if err != nil { if errors2.IsNotFound(err) { @@ -123,35 +133,33 @@ func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, } } -func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, query string) ([]*apis.DetailAddonResponse, error) { - // Backward compatibility with ConfigMap addons. - // We will deprecate ConfigMap and use Git based registry. - addons, err := getAddonsFromConfigMap(detailed) - if err != nil { - return nil, err - } - +func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) { + var addons []*apis.DetailAddonResponse rs, err := u.ListAddonRegistries(ctx) if err != nil { return nil, err } for _, r := range rs { + if registry != "" && r.Name != registry { + continue + } gitAddons, err := getAddonsFromGit(r.Git.URL, r.Git.Path, r.Git.Token, detailed) if err != nil { - log.Logger.Errorf("list addons from registry %s failure %s", r.Name, err.Error()) - continue + return nil, err } addons = mergeAddons(addons, gitAddons) } + if query != "" { - var new []*apis.DetailAddonResponse + var filtered []*apis.DetailAddonResponse for i, addon := range addons { if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) { - new = append(new, addons[i]) + filtered = append(filtered, addons[i]) } } - addons = new + addons = filtered } + sort.Slice(addons, func(i, j int) bool { return addons[i].Name < addons[j].Name }) @@ -177,7 +185,6 @@ func (u *addonUsecaseImpl) CreateAddonRegistry(ctx context.Context, req apis.Cre Name: r.Name, Git: r.Git, }, nil - } func (u *addonUsecaseImpl) GetAddonRegistryModel(ctx context.Context, name string) (*model.AddonRegistry, error) { @@ -204,87 +211,115 @@ func (u *addonUsecaseImpl) ListAddonRegistries(ctx context.Context) ([]*apis.Add return list, nil } +func renderApplication(addon *restapis.DetailAddonResponse, args *apis.EnableAddonRequest) (*v1beta1.Application, error) { + if args == nil { + args = &apis.EnableAddonRequest{Args: map[string]string{}} + } + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, + ObjectMeta: metav1.ObjectMeta{ + Name: restutils.AddonName2AppName(addon.Name), + Namespace: types.DefaultKubeVelaNS, + Labels: map[string]string{ + oam.LabelAddonName: addon.Name, + }, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common2.ApplicationComponent{}, + }, + } + for _, tmpl := range addon.YAMLTemplates { + comp, err := renderRawComponent(tmpl) + if err != nil { + return nil, err + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + for _, tmpl := range addon.CUETemplates { + yamlData, err := renderCUETemplate(tmpl.Data, addon.Parameters, args.Args) + if err != nil { + log.Logger.Errorf("failed to render CUE template: %v", err) + return nil, bcode.ErrAddonRenderFail + } + comp, err := renderRawComponent(apis.AddonElementFile{Data: yamlData, Name: tmpl.Name, Path: tmpl.Path}) + if err != nil { + return nil, err + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + return app, nil +} + +func renderCUETemplate(template string, parameters string, args map[string]string) (string, error) { + bt, err := json.Marshal(args) + if err != nil { + return "", err + } + var paramFile = cuemodel.ParameterFieldName + ": {}" + if string(bt) != "null" { + paramFile = fmt.Sprintf("%s: %s", cuemodel.ParameterFieldName, string(bt)) + } + param := fmt.Sprintf("%s\n%s", paramFile, parameters) + v, err := value.NewValue(param, nil, "") + if err != nil { + return "", err + } + out, err := v.LookupByScript(fmt.Sprintf("{%s}", template)) + if err != nil { + return "", err + } + b, err := cueyaml.Encode(out.CueValue()) + return string(b), err +} + func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error { - addon, err := u.GetAddon(ctx, name) + addon, err := u.GetAddon(ctx, name, "", true) if err != nil { return err } - err = u.applyAddonData(addon.DeployData, args) + app, err := renderApplication(addon, &args) if err != nil { return err } + err = u.kubeClient.Create(ctx, app) + if err != nil { + log.Logger.Errorf("apply application fail: %s", err.Error()) + return bcode.ErrAddonApplyFail + } return nil } func (u *addonUsecaseImpl) DisableAddon(ctx context.Context, name string) error { - addon, err := u.GetAddon(ctx, name) - if err != nil { - return err + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, + ObjectMeta: metav1.ObjectMeta{ + Name: restutils.AddonName2AppName(name), + Namespace: types.DefaultKubeVelaNS, + }, } - err = u.deleteAddonData(addon.DeployData) + err := u.kubeClient.Delete(ctx, app) if err != nil { + log.Logger.Errorf("delete application fail: %s", err.Error()) return err } return nil } -func (u *addonUsecaseImpl) applyAddonData(data string, request apis.EnableAddonRequest) error { - app, err := renderAddonApp(data, &request) - if err != nil { - return err +// renderRawComponent will return a component in raw type from string +func renderRawComponent(elem apis.AddonElementFile) (*common2.ApplicationComponent, error) { + baseRawComponent := common2.ApplicationComponent{ + Type: "raw", + Name: strings.Join(append(elem.Path, elem.Name), "-"), } - applicator := apply.NewAPIApplicator(u.kubeClient) - err = applicator.Apply(context.TODO(), app) - if err != nil { - log.Logger.Errorf("apply application fail: %s", err.Error()) - return bcode.ErrAddonApplyFail - } - return nil -} - -func (u *addonUsecaseImpl) deleteAddonData(data string) error { - app, err := renderAddonApp(data, nil) - if err != nil { - return err - } - err = u.kubeClient.Get(context.Background(), client.ObjectKey{ - Namespace: app.GetNamespace(), - Name: app.GetName(), - }, app) - if err != nil { - return bcode.ErrAddonNotEnabled - } - err = u.kubeClient.Delete(context.Background(), app) - if err != nil { - return bcode.ErrAddonDisableFail - } - return nil - -} - -// renderAddonApp can render string to unstructured, args can be nil -func renderAddonApp(data string, args *apis.EnableAddonRequest) (*unstructured.Unstructured, error) { - if args == nil { - args = &apis.EnableAddonRequest{Args: map[string]string{}} - } - - t, err := template.New("addon-template").Delims("[[", "]]").Funcs(sprig.TxtFuncMap()).Parse(data) - if err != nil { - return nil, bcode.ErrAddonRenderFail - } - buf := bytes.Buffer{} - err = t.Execute(&buf, args) - if err != nil { - return nil, bcode.ErrAddonRenderFail - } - dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) obj := &unstructured.Unstructured{} - _, _, err = dec.Decode(buf.Bytes(), nil, obj) + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + _, _, err := dec.Decode([]byte(elem.Data), nil, obj) if err != nil { - return nil, bcode.ErrAddonRenderFail + fmt.Println(err) } - return obj, nil + baseRawComponent.Properties = util.Object2RawExtension(obj) + return &baseRawComponent, nil } func addonRegistryModelFromCreateAddonRegistryRequest(req apis.CreateAddonRegistryRequest) *model.AddonRegistry { @@ -313,109 +348,214 @@ func hasAddon(addons []*apis.DetailAddonResponse, name string) bool { return false } +type gitHelper struct { + Client *github.Client + Meta *utils.Content +} + func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.DetailAddonResponse, error) { - addons := []*apis.DetailAddonResponse{} - dec := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + var addons []*apis.DetailAddonResponse + + gith, err := createGitHelper(baseURL, dir, token) + if err != nil { + return nil, err + } + dirs, err := readRepo(gith) + if err != nil { + return nil, err + } + + for _, subItems := range dirs { + if subItems.GetType() != "dir" { + continue + } + addonRes := &apis.DetailAddonResponse{} + _, files, _, err := gith.Client.Repositories.GetContents(context.Background(), gith.Meta.Owner, gith.Meta.Repo, subItems.GetPath(), nil) + if err != nil { + log.Logger.Errorf("failed to read dir %s: %v", subItems.GetPath(), err) + continue + } + for _, file := range files { + var err error + + switch strings.ToLower(file.GetName()) { + case AddonReadmeFileName: + if !detailed { + break + } + err = readReadme(addonRes, gith, file) + case AddonMetadataFileName: + err = readMetadata(addonRes, gith, file) + addonRes.Name = addonutil.TransAddonName(addonRes.Name) + case AddonDefinitionsDirName: + if !detailed { + break + } + err = readDefinitions(addonRes, gith, file) + case AddonTemplateDirName: + if !detailed { + break + } + err = readTemplates(addonRes, gith, file) + } + + if err != nil { + log.Logger.Errorf("failed to read file %s: %v", file.GetPath(), err) + continue + } + } + + addons = append(addons, addonRes) + } + return addons, nil +} + +func readTemplates(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.RepositoryContent) error { + dirPath := strings.Split(dir.GetPath(), "/") + // remove + for i, d := range dirPath { + if d == AddonTemplateDirName { + dirPath = dirPath[i:] + break + } + dirPath = dirPath[i:] + } + + _, files, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *dir.Path, nil) + if err != nil { + return err + } + for _, file := range files { + switch file.GetType() { + case "file": + content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) + if err != nil { + return err + } + b, err := content.GetContent() + if err != nil { + return err + } + + if file.GetName() == "parameter.cue" { + addon.Parameters = b + break + } + switch filepath.Ext(file.GetName()) { + case ".cue": + addon.CUETemplates = append(addon.CUETemplates, apis.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) + default: + addon.YAMLTemplates = append(addon.YAMLTemplates, apis.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) + } + case "dir": + err = readTemplates(addon, h, file) + if err != nil { + return err + } + } + } + return nil +} + +func readDefinitions(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.RepositoryContent) error { + _, files, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *dir.Path, nil) + if err != nil { + return err + } + for _, file := range files { + switch file.GetType() { + case "file": + content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) + if err != nil { + return err + } + b, err := content.GetContent() + if err != nil { + return err + } + d, err := getDefinitionMetaFromYAML(b) + if err != nil { + return err + } + addon.Definitions = append(addon.Definitions, d) + case "dir": + err = readDefinitions(addon, h, file) + if err != nil { + return err + } + } + } + return nil +} + +func getDefinitionMetaFromYAML(data string) (*apis.Definition, error) { + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + obj := &unstructured.Unstructured{} + _, _, err := dec.Decode([]byte(data), nil, obj) + if err != nil { + return nil, bcode.ErrAddonRenderFail + } + d := &apis.Definition{ + Name: obj.GetName(), + Kind: obj.GetKind(), + } + if ann := obj.GetAnnotations(); ann != nil { + d.Description = ann[types.AnnDescription] + } + return d, nil +} + +func readMetadata(addon *apis.DetailAddonResponse, h *gitHelper, file *github.RepositoryContent) error { + content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) + if err != nil { + return err + } + b, err := content.GetContent() + if err != nil { + return err + } + return yaml.Unmarshal([]byte(b), &addon.AddonMeta) +} + +func readReadme(addon *apis.DetailAddonResponse, h *gitHelper, file *github.RepositoryContent) error { + content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) + if err != nil { + return err + } + addon.Detail, err = content.GetContent() + return err +} + +func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { var ts oauth2.TokenSource if token != "" { ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) } tc := oauth2.NewClient(context.Background(), ts) tc.Timeout = time.Second * 10 - clt := github.NewClient(tc) - // TODO add error handling + cli := github.NewClient(tc) + baseURL = strings.TrimSuffix(baseURL, ".git") u, err := url.Parse(baseURL) if err != nil { return nil, err } u.Path = path.Join(u.Path, dir) - _, content, err := utils.Parse(u.String()) + _, gitmeta, err := utils.Parse(u.String()) if err != nil { return nil, err } - _, dirs, _, err := clt.Repositories.GetContents(context.Background(), content.Owner, content.Repo, content.Path, nil) + + return &gitHelper{ + Client: cli, + Meta: gitmeta, + }, nil +} + +func readRepo(h *gitHelper) ([]*github.RepositoryContent, error) { + _, dirs, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, h.Meta.Path, nil) if err != nil { return nil, err } - for _, subItems := range dirs { - if *subItems.Type == "file" { - continue - } - addonRes := apis.DetailAddonResponse{ - AddonMeta: apis.AddonMeta{ - Name: converAddonName(*subItems.Name), - }, - } - var err error - _, files, _, err := clt.Repositories.GetContents(context.Background(), content.Owner, content.Repo, *subItems.Path, nil) - // get addon.yaml and readme.md - for _, file := range files { - switch *file.Name { - case AddonFileName: - addonContent, _, _, err := clt.Repositories.GetContents(context.Background(), content.Owner, content.Repo, *file.Path, nil) - if err != nil { - break - } - addonStr, _ := addonContent.GetContent() - obj := &unstructured.Unstructured{} - _, _, err = dec.Decode([]byte(addonStr), nil, obj) - if err != nil { - break - } - addonRes.AddonMeta.Description = obj.GetAnnotations()[addonutil.DescAnnotation] - addonRes.DeployData = addonStr - case AddonReadmeFileName: - if detailed { - detailContent, _, _, err := clt.Repositories.GetContents(context.Background(), content.Owner, content.Repo, *file.Path, nil) - if err != nil { - break - } - addonRes.Detail, err = detailContent.GetContent() - if err != nil { - break - } - } - default: - continue - } - - } - if err != nil { - continue - } - addons = append(addons, &addonRes) - } - return addons, nil -} - -func getAddonsFromConfigMap(detailed bool) ([]*apis.DetailAddonResponse, error) { - repo, err := addonutil.NewAddonRepo() - if err != nil { - return nil, fmt.Errorf("failed to get configMap addon repo: %w", err) - } - cliAddons := repo.ListAddons() - addons := []*apis.DetailAddonResponse{} - for _, addon := range cliAddons { - d := &apis.DetailAddonResponse{ - AddonMeta: apis.AddonMeta{ - Name: converAddonName(addon.Name), - // TODO add actual Version, Icon, tags - Version: "v1alpha1", - Description: addon.Description, - Icon: "", - Tags: nil, - }, - DeployData: addon.Data, - } - if detailed { - d.Detail = addon.Detail - } - addons = append(addons, d) - } - return addons, nil -} - -func converAddonName(name string) string { - return strings.ReplaceAll(name, "/", "-") + return dirs, nil } diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go index a7ba3cf29..a1415bf6a 100644 --- a/pkg/apiserver/rest/utils/bcode/addon.go +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -17,7 +17,6 @@ limitations under the License. package bcode var ( - // ErrAddonNotExist addon not exist ErrAddonNotExist = NewBcode(404, 50001, "addon not exist") @@ -30,15 +29,9 @@ var ( // ErrAddonApplyFail fail to apply application to cluster ErrAddonApplyFail = NewBcode(500, 50011, "fail to apply addon application") - // ErrGetClientFail fail to get k8s client - ErrGetClientFail = NewBcode(500, 50012, "fail to initialize kubernetes client") - // ErrGetApplicationFail fail to get addon application ErrGetApplicationFail = NewBcode(500, 50013, "fail to get addon application") - // ErrGetConfigMapAddonFail fail to get addon info in configmap - ErrGetConfigMapAddonFail = NewBcode(500, 50014, "fail to get addon information in ConfigMap") - // ErrAddonDisableFail fail to disable addon ErrAddonDisableFail = NewBcode(500, 50016, "fail to disable addon") diff --git a/pkg/apiserver/rest/utils/convert.go b/pkg/apiserver/rest/utils/convert.go index 9da4c237f..09c3f2b64 100644 --- a/pkg/apiserver/rest/utils/convert.go +++ b/pkg/apiserver/rest/utils/convert.go @@ -3,6 +3,7 @@ package utils import ( "github.com/oam-dev/kubevela/pkg/apiserver/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "strings" ) // ConvertAddonRegistryModel2AddonRegistryMeta will convert from model to AddonRegistryMeta @@ -12,3 +13,12 @@ func ConvertAddonRegistryModel2AddonRegistryMeta(r *model.AddonRegistry) *apisv1 Git: r.Git, } } + +const addonAppPrefix = "addon-" + +func AddonName2AppName(name string) string { + return addonAppPrefix + name +} +func AppName2addonName(name string) string { + return strings.TrimPrefix(name, addonAppPrefix) +} diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 57f0469ca..79aa99123 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -49,6 +49,7 @@ func (s *addonWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(s.listAddons). Doc("list all addons"). Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")). Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")). Returns(200, "", apis.ListAddonResponse{}). Returns(400, "", bcode.Bcode{}). @@ -77,6 +78,7 @@ func (s *addonWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/{name}/enable").To(s.enableAddon). Doc("enable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.EnableAddonRequest{}). Returns(200, "", apis.AddonStatusResponse{}). Returns(400, "", bcode.Bcode{}). Param(ws.PathParameter("name", "addon name to enable").DataType("string").Required(true)). @@ -95,7 +97,7 @@ func (s *addonWebService) GetWebService() *restful.WebService { } func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response) { - detailAddons, err := s.addonUsecase.ListAddons(req.Request.Context(), false, req.QueryParameter("query")) + detailAddons, err := s.addonUsecase.ListAddons(req.Request.Context(), false, req.QueryParameter("registry"), req.QueryParameter("query")) if err != nil { bcode.ReturnError(req, res, err) return @@ -116,7 +118,7 @@ func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response func (s *addonWebService) detailAddon(req *restful.Request, res *restful.Response) { name := req.PathParameter("name") - addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name) + addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name, "", true) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/addon_registry.go b/pkg/apiserver/rest/webservice/addon_registry.go index 209110f1b..1ba902dc7 100644 --- a/pkg/apiserver/rest/webservice/addon_registry.go +++ b/pkg/apiserver/rest/webservice/addon_registry.go @@ -20,7 +20,6 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" "github.com/emicklei/go-restful/v3" - "github.com/oam-dev/kubevela/pkg/apiserver/log" 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" @@ -90,7 +89,6 @@ func (s *addonRegistryWebService) createAddonRegistry(req *restful.Request, res // Call the usecase layer code meta, err := s.addonUsecase.CreateAddonRegistry(req.Request.Context(), createReq) if err != nil { - log.Logger.Errorf("create addon registry failure %s", err.Error()) bcode.ReturnError(req, res, err) return } diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index fb1ce91d3..348da8f0f 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -61,6 +61,9 @@ const ( // LabelAddonsName records the name of initializer stored in configMap LabelAddonsName = "addons.oam.dev/type" + + // LabelAddonName indicates the name of the corresponding Addon + LabelAddonName = "addons.oam.dev/name" ) const ( diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go index 3b8e40220..5a005b8af 100644 --- a/test/e2e-apiserver-test/addon_test.go +++ b/test/e2e-apiserver-test/addon_test.go @@ -2,15 +2,19 @@ package e2e_apiserver import ( "bytes" + "context" "encoding/json" "net/http" "os" "time" - "k8s.io/apimachinery/pkg/util/wait" - + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils/common" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" "github.com/oam-dev/kubevela/pkg/apiserver/model" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" @@ -34,18 +38,18 @@ func get(path string) *http.Response { } var _ = Describe("Test addon rest api", func() { - It("should add a registry and list addons from it and delete the registry", func() { + createReq := apis.CreateAddonRegistryRequest{ + Name: "test-addon-registry-1", + Git: &model.GitAddonSource{ + URL: "https://github.com/oam-dev/catalog", + Path: "addons/", + Token: os.Getenv("GITHUB_TOKEN"), + }, + } + It("should add a registry and list addons from it", func() { defer GinkgoRecover() By("add registry") - createReq := apis.CreateAddonRegistryRequest{ - Name: "test-addon-registry-1", - Git: &model.GitAddonSource{ - URL: "https://github.com/oam-dev/catalog", - Path: "addon/", - Token: os.Getenv("GITHUB_TOKEN"), - }, - } createRes := post("/api/v1/addon_registries", createReq) Expect(createRes).ShouldNot(BeNil()) Expect(createRes.StatusCode).Should(Equal(200)) @@ -68,23 +72,29 @@ var _ = Describe("Test addon rest api", func() { Expect(err).Should(BeNil()) Expect(lres.Addons).ShouldNot(BeZero()) firstAddon := lres.Addons[0] - Expect(firstAddon.Name).Should(Equal("fluxcd")) + Expect(firstAddon.Name).Should(Equal("example")) - By("delete registry") - deleteReq, err := http.NewRequest(http.MethodDelete, baseURL+"/api/v1/addon_registries/"+createReq.Name, nil) - Expect(err).Should(BeNil()) - deleteRes, err := http.DefaultClient.Do(deleteReq) - Expect(err).Should(BeNil()) - Expect(deleteRes).ShouldNot(BeNil()) - Expect(deleteRes.StatusCode).Should(Equal(200)) }) It("should enable and disable an addon", func() { + // todo(qiaozp) we should remove this namespace creation. This should be solved with a application template. + ns := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + }, + } + args := common.Args{} + client, err := args.GetClient() + Expect(err).Should(BeNil()) + Expect(client.Create(context.Background(), &ns)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + defer GinkgoRecover() req := apis.EnableAddonRequest{ - Args: map[string]string{}, + Args: map[string]string{ + "example": "test-args", + }, } - testAddon := "fluxcd" + testAddon := "example" res := post("/api/v1/addons/"+testAddon+"/enable", req) Expect(res).ShouldNot(BeNil()) Expect(res.StatusCode).Should(Equal(200)) @@ -93,7 +103,7 @@ var _ = Describe("Test addon rest api", func() { defer res.Body.Close() var statusRes apis.AddonStatusResponse - err := json.NewDecoder(res.Body).Decode(&statusRes) + err = json.NewDecoder(res.Body).Decode(&statusRes) Expect(err).Should(BeNil()) Expect(statusRes.Phase).Should(Equal(apis.AddonPhaseEnabling)) @@ -120,6 +130,15 @@ var _ = Describe("Test addon rest api", func() { err = json.NewDecoder(res.Body).Decode(&statusRes) Expect(err).Should(BeNil()) + }) + It("should delete test registry", func() { + defer GinkgoRecover() + deleteReq, err := http.NewRequest(http.MethodDelete, baseURL+"/api/v1/addon_registries/"+createReq.Name, nil) + Expect(err).Should(BeNil()) + deleteRes, err := http.DefaultClient.Do(deleteReq) + Expect(err).Should(BeNil()) + Expect(deleteRes).ShouldNot(BeNil()) + Expect(deleteRes.StatusCode).Should(Equal(200)) }) }) From 5590c3d7b5b4fc41e58c9817b2d0e92ab21e844b Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Fri, 5 Nov 2021 21:54:31 +0800 Subject: [PATCH 20/59] Fix: fix apiserver definition schema struct (#2644) * Fix: fix apiserver definition schema struct * use open api schema --- pkg/apiserver/rest/apis/v1/types.go | 33 +------------ pkg/apiserver/rest/usecase/definition.go | 6 +-- pkg/apiserver/rest/usecase/definition_test.go | 49 ++++--------------- 3 files changed, 14 insertions(+), 74 deletions(-) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 01eaaf57f..96a895c5f 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -17,9 +17,9 @@ limitations under the License. package v1 import ( - "regexp" "time" + "github.com/getkin/kin-openapi/openapi3" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/model" @@ -467,36 +467,7 @@ type ListDefinitionResponse struct { // DetailDefinitionResponse get definition detail type DetailDefinitionResponse struct { - Schema *DefinitionSchema `json:"schema"` -} - -// DefinitionSchema definition schema info -type DefinitionSchema struct { - Properties map[string]*DefinitionProperties `json:"properties"` - Required []string `json:"required"` - Type string `json:"type"` -} - -// DefinitionProperties definition properties -type DefinitionProperties struct { - Items *DefinitionSchema `json:"items,omitempty"` - Enum []interface{} `json:"enum,omitempty"` - Default interface{} `json:"default,omitempty"` - Example interface{} `json:"example,omitempty"` - Description string `json:"description,omitempty"` - Title string `json:"title"` - Type string `json:"type"` - - // Number - Min *float64 `json:"minimum,omitempty"` - Max *float64 `json:"maximum,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty"` - - // String - MinLength uint64 `json:"minLength,omitempty"` - MaxLength *uint64 `json:"maxLength,omitempty"` - Pattern string `json:"pattern,omitempty"` - compiledPattern *regexp.Regexp + Schema *openapi3.Schema `json:"schema"` } // DefinitionBase is the definition base model diff --git a/pkg/apiserver/rest/usecase/definition.go b/pkg/apiserver/rest/usecase/definition.go index f24076b01..9b1c1707d 100644 --- a/pkg/apiserver/rest/usecase/definition.go +++ b/pkg/apiserver/rest/usecase/definition.go @@ -18,7 +18,6 @@ package usecase import ( "context" - "encoding/json" "fmt" "time" @@ -27,6 +26,7 @@ import ( k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/getkin/kin-openapi/openapi3" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/log" @@ -120,8 +120,8 @@ func (d *definitionUsecaseImpl) DetailDefinition(ctx context.Context, name, defT if !ok { return nil, fmt.Errorf("failed to get definition schema") } - schema := &apisv1.DefinitionSchema{} - if err := json.Unmarshal([]byte(data), schema); err != nil { + schema := &openapi3.Schema{} + if err := schema.UnmarshalJSON([]byte(data)); err != nil { return nil, err } return &apisv1.DetailDefinitionResponse{ diff --git a/pkg/apiserver/rest/usecase/definition_test.go b/pkg/apiserver/rest/usecase/definition_test.go index 546f1eda4..4923cabb1 100644 --- a/pkg/apiserver/rest/usecase/definition_test.go +++ b/pkg/apiserver/rest/usecase/definition_test.go @@ -20,6 +20,7 @@ import ( "context" "io/ioutil" + "github.com/getkin/kin-openapi/openapi3" "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -28,7 +29,6 @@ import ( "sigs.k8s.io/yaml" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/oam/util" ) @@ -98,49 +98,18 @@ var _ = Describe("Test namespace usecase functions", func() { Namespace: "vela-system", }, Data: map[string]string{ - "openapi-v3-json-schema": `{"properties":{"batchPartition":{"title":"batchPartition","type":"integer"},"volumes": {"description":"Specify volume type, options: pvc, configMap, secret, emptyDir","enum":["pvc","configMap","secret","emptyDir"],"title":"volumes","type":"string"}, "rolloutBatches":{"items":{"properties":{"replicas":{"title":"replicas","type":"integer"}},"required":["replicas"],"type":"object"},"title":"rolloutBatches","type":"array"},"targetRevision":{"title":"targetRevision","type":"string"},"targetSize":{"title":"targetSize","type":"integer"}},"required":["targetRevision","targetSize"],"type":"object"}`, + "openapi-v3-json-schema": `{"properties":{"batchPartition":{"title":"batchPartition","type":"integer"},"volumes":{"description":"Specify volume type, options: pvc, configMap, secret, emptyDir","enum":["pvc","configMap","secret","emptyDir"],"title":"volumes","type":"string"}, "rolloutBatches":{"items":{"properties":{"replicas":{"title":"replicas","type":"integer"}},"required":["replicas"],"type":"object"},"title":"rolloutBatches","type":"array"},"targetRevision":{"title":"targetRevision","type":"string"},"targetSize":{"title":"targetSize","type":"integer"}},"required":["targetRevision","targetSize"],"type":"object"}`, }, } err := k8sClient.Create(context.Background(), cm) Expect(err).Should(Succeed()) schema, err := definitionUsecase.DetailDefinition(context.TODO(), "apply-object", "workflowstep") - Expect(schema.Schema).Should(Equal(&v1.DefinitionSchema{ - Properties: map[string]*v1.DefinitionProperties{ - "volumes": { - Title: "volumes", - Type: "string", - Description: "Specify volume type, options: pvc, configMap, secret, emptyDir", - Enum: []interface{}{"pvc", "configMap", "secret", "emptyDir"}, - }, - "batchPartition": { - Title: "batchPartition", - Type: "integer", - }, - "rolloutBatches": { - Items: &v1.DefinitionSchema{ - Properties: map[string]*v1.DefinitionProperties{ - "replicas": { - Title: "replicas", - Type: "integer", - }, - }, - Required: []string{"replicas"}, - Type: "object", - }, - Title: "rolloutBatches", - Type: "array", - }, - "targetSize": { - Title: "targetSize", - Type: "integer", - }, - "targetRevision": { - Title: "targetRevision", - Type: "string", - }, - }, - Required: []string{"targetRevision", "targetSize"}, - Type: "object", - })) + Expect(err).Should(Succeed()) + + schemaFromCM := &openapi3.Schema{} + err = schemaFromCM.UnmarshalJSON([]byte(cm.Data["openapi-v3-json-schema"])) + Expect(err).Should(Succeed()) + + Expect(schema.Schema).Should(Equal(schemaFromCM)) }) }) From eb258fae6663b0ff4592fd4653e96a97ca64bf08 Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Mon, 8 Nov 2021 14:13:22 +0800 Subject: [PATCH 21/59] Feat: support ui schema (#2647) * Docs: change swagger api config * Docs: change swagger api config * Docs: change swagger api config * Feat: support ui schema * Fix: distinguish between structs and arrays * Feat: support build swagger config * Feat: support update ui schema Co-authored-by: barnettZQG --- Makefile | 4 +- apis/types/capability.go | 2 + cmd/apiserver/main.go | 42 + docs/apidoc/swagger.json | 10863 ++++++++-------- pkg/apiserver/rest/apis/v1/types.go | 19 +- pkg/apiserver/rest/rest_server.go | 14 +- pkg/apiserver/rest/usecase/addon.go | 34 +- pkg/apiserver/rest/usecase/definition.go | 179 +- pkg/apiserver/rest/usecase/definition_test.go | 61 +- .../rest/usecase/testdata/api-schema.json | 386 + .../usecase/testdata/ui-custom-schema.yaml | 56 + .../usecase/testdata/ui-default-schema.yaml | 132 + .../rest/usecase/testdata/ui-schema.yaml | 428 + pkg/apiserver/rest/utils/bcode/definition.go | 29 + pkg/apiserver/rest/utils/convert.go | 24 - pkg/apiserver/rest/utils/uiswagger.go | 96 +- .../rest/webservice/addon_registry.go | 3 +- .../rest/webservice/applicationplan.go | 2 +- pkg/apiserver/rest/webservice/cluster.go | 10 +- pkg/apiserver/rest/webservice/definition.go | 2 +- pkg/apiserver/rest/webservice/namespace.go | 2 + .../rest/webservice/oam_application.go | 3 +- .../rest/webservice/policy_definition.go | 3 +- pkg/apiserver/rest/webservice/webservice.go | 3 +- pkg/apiserver/rest/webservice/workflow.go | 9 +- test/e2e-test/helm_app_test.go | 2 +- test/e2e-test/kube_app_test.go | 3 +- 27 files changed, 6825 insertions(+), 5586 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/testdata/api-schema.json create mode 100755 pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml create mode 100755 pkg/apiserver/rest/usecase/testdata/ui-default-schema.yaml create mode 100755 pkg/apiserver/rest/usecase/testdata/ui-schema.yaml create mode 100644 pkg/apiserver/rest/utils/bcode/definition.go delete mode 100644 pkg/apiserver/rest/utils/convert.go diff --git a/Makefile b/Makefile index d21c62896..55f28d3d7 100644 --- a/Makefile +++ b/Makefile @@ -172,12 +172,14 @@ e2e-setup: kubectl wait --for=condition=Ready pod -l app=source-controller -n flux-system --timeout=600s kubectl wait --for=condition=Ready pod -l app=helm-controller -n flux-system --timeout=600s +build-swagger: + go run ./cmd/apiserver/main.go build-swagger ./docs/apidoc/swagger.json e2e-api-test: # Run e2e test ginkgo -v -skipPackage capability,setup,application -r e2e ginkgo -v -r e2e/application -e2e-apiserver-test: +e2e-apiserver-test: build-swagger go test -v -coverpkg=./... -coverprofile=/tmp/e2e_apiserver_test.out ./test/e2e-apiserver-test @$(OK) tests pass diff --git a/apis/types/capability.go b/apis/types/capability.go index 375b0d72e..a2c96650c 100644 --- a/apis/types/capability.go +++ b/apis/types/capability.go @@ -79,6 +79,8 @@ const CapabilityConfigMapNamePrefix = "schema-" const ( // OpenapiV3JSONSchema is the key to store OpenAPI v3 JSON schema in ConfigMap OpenapiV3JSONSchema string = "openapi-v3-json-schema" + // UISchema is the key to store ui custom schema + UISchema string = "ui-schema" ) // CapabilityCategory defines the category of a capability diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index d702c0c2c..4e9714ddf 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -18,12 +18,16 @@ package main import ( "context" + "encoding/json" "flag" "fmt" "os" "os/signal" "syscall" + restfulspec "github.com/emicklei/go-restful-openapi/v2" + "github.com/go-openapi/spec" + "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/rest" "github.com/oam-dev/kubevela/version" @@ -38,6 +42,34 @@ func main() { flag.StringVar(&s.restCfg.Datastore.URL, "datastore-url", "", "Metadata storage database url,takes effect when the storage driver is mongodb.") flag.Parse() + if len(os.Args) > 2 && os.Args[1] == "build-swagger" { + func() { + swagger, err := s.buildSwagger() + if err != nil { + log.Logger.Fatal(err.Error()) + } + outData, err := json.MarshalIndent(swagger, "", "\t") + if err != nil { + log.Logger.Fatal(err.Error()) + } + swaggerFile, err := os.OpenFile(os.Args[2], os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + log.Logger.Fatal(err.Error()) + } + defer func() { + if err := swaggerFile.Close(); err != nil { + log.Logger.Errorf("close swagger file failure %s", err.Error()) + } + }() + _, err = swaggerFile.Write(outData) + if err != nil { + log.Logger.Fatal(err.Error()) + } + fmt.Println("build swagger config file success") + }() + return + } + srvc := make(chan struct{}) go func() { @@ -48,6 +80,7 @@ func main() { }() var term = make(chan os.Signal, 1) signal.Notify(term, os.Interrupt, syscall.SIGTERM) + select { case <-term: log.Logger.Infof("Received SIGTERM, exiting gracefully...") @@ -71,5 +104,14 @@ func (s *Server) run() error { if err != nil { return fmt.Errorf("create apiserver failed : %w ", err) } + return server.Run(ctx) } + +func (s *Server) buildSwagger() (*spec.Swagger, error) { + server, err := rest.New(s.restCfg) + if err != nil { + return nil, fmt.Errorf("create apiserver failed : %w ", err) + } + return restfulspec.BuildSwagger(server.RegisterServices()), nil +} diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 212415471..bb9e23b5c 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -1,5505 +1,5362 @@ { - "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/addon_registries": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon_registry" - ], - "summary": "list all addon registry", - "operationId": "listAddonRegistry", - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ListAddonRegistryResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon_registry" - ], - "summary": "create an addon registry", - "operationId": "createAddonRegistry", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateAddonRegistryRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.AddonRegistryMeta" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/addon_registries/{name}": { - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon_registry" - ], - "summary": "delete an addon registry", - "operationId": "deleteAddonRegistry", - "parameters": [ - { - "type": "string", - "description": "identifier of the addon registry", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.AddonRegistryMeta" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/addons": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "list all addons", - "operationId": "listAddons", - "parameters": [ - { - "type": "string", - "description": "filter addons from given registry", - "name": "registry", - "in": "query" - }, - { - "type": "string", - "description": "Fuzzy search based on name and description.", - "name": "query", - "in": "query" - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ListAddonResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/addons/{name}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "show details of an addon", - "operationId": "detailAddon", - "parameters": [ - { - "type": "string", - "description": "addon name to query detail", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.DetailAddonResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/addons/{name}/disable": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "disable an addon", - "operationId": "disableAddon", - "parameters": [ - { - "type": "string", - "description": "addon name to enable", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.AddonStatusResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/addons/{name}/enable": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "enable an addon", - "operationId": "enableAddon", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.EnableAddonRequest" - } - }, - { - "type": "string", - "description": "addon name to enable", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.AddonStatusResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/addons/{name}/status": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "addon" - ], - "summary": "show status of an addon", - "operationId": "statusAddon", - "parameters": [ - { - "type": "string", - "description": "addon name to query status", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.AddonStatusResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "list all application plans", - "operationId": "listApplicationPlans", - "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": { - "schema": { - "$ref": "#/definitions/v1.ListApplicationPlanResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "create one application plan", - "operationId": "createApplicationPlan", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateApplicationPlanRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ApplicationPlanBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "detail one application plan", - "operationId": "detailApplicationPlan", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.DetailApplicationPlanResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "put": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "update one application plan", - "operationId": "updateApplicationPlan", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.UpdateApplicationPlanRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ApplicationPlanBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "delete one application", - "operationId": "deleteApplicationPlan", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.EmptyResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}/componentplans": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "gets the componentplan topology of the application", - "operationId": "listApplicationComponents", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "list components that deployed in define env", - "name": "envName", - "in": "query" - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ComponentPlanListResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "create component plan for application plan", - "operationId": "createComponent", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateComponentPlanRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ComponentPlanBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}/componentplans/{componentName}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "detail component plan for application plan", - "operationId": "detailComponent", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.DetailComponentPlanResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}/deploy": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "deploy or upgrade the application", - "operationId": "deployApplication", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ApplicationDeployRequest" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}/envs": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "creating an application environment plan", - "operationId": "createApplicationEnv", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateApplicationEnvPlanRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.EnvBind" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}/envs/{envName}": { - "put": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "set application plan differences in the specified environment", - "operationId": "updateApplicationEnvBinding", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the application plan", - "name": "envName", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.PutApplicationPlanEnvRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.EnvBind" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "delete an application environment plan", - "operationId": "deleteApplicationEnv", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the application plan", - "name": "envName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.EmptyResponse" - } - }, - "404": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}/policies": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "list policy for application", - "operationId": "listApplicationPolicies", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ListApplicationPolicy" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "create policy for application", - "operationId": "createApplicationPolicy", - "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": { - "schema": { - "$ref": "#/definitions/v1.PolicyBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}/policies/{policyName}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "detail policy for application", - "operationId": "detailApplicationPolicy", - "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": { - "schema": { - "$ref": "#/definitions/v1.DetailPolicyResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "put": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "update policy for application", - "operationId": "updateApplicationPolicy", - "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 - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.UpdatePolicyRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.DetailPolicyResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "detail policy for application", - "operationId": "deleteApplicationPolicy", - "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": { - "schema": { - "$ref": "#/definitions/v1.EmptyResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/applicationplans/{name}/template": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "create one application template", - "operationId": "publishApplicationTemplate", - "parameters": [ - { - "type": "string", - "description": "identifier of the application plan", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateApplicationTemplateRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ApplicationTemplateBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/clusters": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "list all clusters", - "operationId": "listKubeClusters", - "parameters": [ - { - "type": "string", - "description": "Fuzzy search based on name or description", - "name": "query", - "in": "query" - }, - { - "type": "int", - "default": 0, - "description": "Page for paging", - "name": "page", - "in": "query" - }, - { - "type": "int", - "default": 20, - "description": "PageSize for paging", - "name": "pageSize", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - }, - "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": { - "schema": { - "$ref": "#/definitions/v1.ClusterBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/clusters/cloud-clusters/{provider}": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "list cloud clusters", - "operationId": "listCloudClusters", - "parameters": [ - { - "type": "string", - "description": "identifier of the cloud provider", - "name": "provider", - "in": "path", - "required": true - }, - { - "type": "int", - "default": 0, - "description": "Page for paging", - "name": "page", - "in": "query" - }, - { - "type": "int", - "default": 20, - "description": "PageSize for paging", - "name": "pageSize", - "in": "query" - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/*v1.AccessKeyRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ListCloudClusterResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/clusters/cloud-clusters/{provider}/connect": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "create cluster from cloud cluster", - "operationId": "connectCloudCluster", - "parameters": [ - { - "type": "string", - "description": "identifier of the cloud provider", - "name": "provider", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/*v1.ConnectCloudClusterRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ClusterBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/clusters/cloud-clusters/{provider}/create": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "create cloud cluster", - "operationId": "createCloudCluster", - "parameters": [ - { - "type": "string", - "description": "identifier of the cloud provider", - "name": "provider", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/*v1.CreateCloudClusterRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.CreateCloudClusterResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/clusters/cloud-clusters/{provider}/creation": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "list cloud cluster creation", - "operationId": "listCloudClusterCreation", - "parameters": [ - { - "type": "string", - "description": "identifier of the cloud provider", - "name": "provider", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ListCloudClusterCreationResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/clusters/cloud-clusters/{provider}/creation/{cloudClusterName}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "check cloud cluster create status", - "operationId": "getCloudClusterCreationStatus", - "parameters": [ - { - "type": "string", - "description": "identifier of the cloud provider", - "name": "provider", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier for cloud cluster which is creating", - "name": "cloudClusterName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.CreateCloudClusterResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "delete cloud cluster creation", - "operationId": "deleteCloudClusterCreation", - "parameters": [ - { - "type": "string", - "description": "identifier of the cloud provider", - "name": "provider", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier for cloud cluster which is creating", - "name": "cloudClusterName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.CreateCloudClusterResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/clusters/{clusterName}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "detail cluster info", - "operationId": "getKubeCluster", - "parameters": [ - { - "type": "string", - "description": "identifier of the cluster", - "name": "clusterName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.DetailClusterResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "put": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "modify cluster", - "operationId": "modifyKubeCluster", - "parameters": [ - { - "type": "string", - "description": "identifier of the cluster", - "name": "clusterName", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/*v1.CreateClusterRequest" - } - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ClusterBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "delete cluster", - "operationId": "deleteKubeCluster", - "parameters": [ - { - "type": "string", - "description": "identifier of the cluster", - "name": "clusterName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.ClusterBase" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/definitions": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "definition" - ], - "summary": "list all definitions", - "operationId": "listDefinitions", - "parameters": [ - { - "type": "string", - "description": "query the definition type", - "name": "type", - "in": "query" - }, - { - "type": "string", - "description": "if specified, query the definition supported by the env.", - "name": "envName", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/api/v1/definitions/{name}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "definition" - ], - "summary": "detail definition", - "operationId": "detailDefinition", - "parameters": [ - { - "type": "string", - "description": "identifier of the definition", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "query the definition type", - "name": "type", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/api/v1/namespaces": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "namespace" - ], - "summary": "list all namespaces", - "operationId": "listNamespaces", - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "namespace" - ], - "summary": "create namespace", - "operationId": "createNamespace", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateNamespaceRequest" - } - } - ], - "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/query": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "velaQL" - ], - "summary": "use velaQL to query resource status", - "operationId": "queryView", - "parameters": [ - { - "type": "string", - "description": "velaql query statement", - "name": "velaql", - "in": "query" - } - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/v1.VelaQLViewResponse" - } - }, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/workflowplans": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "list application workflow", - "operationId": "listApplicationWorkflows", - "parameters": [ - { - "type": "string", - "description": "identifier of the application.", - "name": "appName", - "in": "query" - }, - { - "type": "boolean", - "description": "query based on enable status", - "name": "enable", - "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 application workflow", - "operationId": "createApplicationWorkflow", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateWorkflowPlanRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "400": { - "description": "create failure", - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/api/v1/workflowplans/{name}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "detail application workflow", - "operationId": "detailWorkflow", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow.", - "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": "update application workflow config", - "operationId": "updateWorkflow", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.UpdateWorkflowPlanRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "deletet workflow", - "operationId": "deleteWorkflow", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/api/v1/workflowplans/{name}/records": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "query application workflow execution record", - "operationId": "listWorkflowRecords", - "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" - } - } - } - }, - "/api/v1/workflowplans/{name}/records/{record}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "query application workflow execution record detail", - "operationId": "detailWorkflowRecord", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the workflow record", - "name": "record", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/v1/namespaces/{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": "getApplication", - "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": [ - "oam" - ], - "summary": "create or update oam application in the specified namespace", - "operationId": "createOrUpdateApplication", - "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": [ - "oam" - ], - "summary": "create or update oam application in the specified namespace", - "operationId": "deleteApplication", - "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" - } - } - } - } - }, - "definitions": { - "bcode.Bcode": { - "required": [ - "BusinessCode", - "Message" - ], - "properties": { - "BusinessCode": { - "type": "integer", - "format": "int32" - }, - "Message": { - "type": "string" - } - } - }, - "cloudprovider.CloudCluster": { - "required": [ - "provider", - "id", - "name", - "type", - "zone", - "labels", - "status", - "apiServerURL", - "dashboardURL" - ], - "properties": { - "apiServerURL": { - "type": "string" - }, - "dashboardURL": { - "type": "string" - }, - "id": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "status": { - "type": "string" - }, - "type": { - "type": "string" - }, - "zone": { - "type": "string" - } - } - }, - "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": { - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "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", - "finished" - ], - "properties": { - "appRevision": { - "type": "string" - }, - "contextBackend": { - "$ref": "#/definitions/v1.ObjectReference" - }, - "finished": { - "type": "boolean" - }, - "mode": { - "type": "string" - }, - "startTime": { - "type": "string" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/common.WorkflowStepStatus" - } - }, - "suspend": { - "type": "boolean" - }, - "terminated": { - "type": "boolean" - } - } - }, - "common.WorkflowStepStatus": { - "required": [ - "id" - ], - "properties": { - "firstExecuteTime": { - "type": "string" - }, - "id": { - "type": "string" - }, - "lastExecuteTime": { - "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" - } - }, - "model.ApplicationComponentPlan": { - "required": [ - "createTime", - "updateTime", - "appPrimaryKey", - "creator", - "name", - "alias", - "type" - ], - "properties": { - "alias": { - "type": "string" - }, - "appPrimaryKey": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "creator": { - "type": "string" - }, - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "externalRevision": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "inputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.inputItem" - } - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "outputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.outputItem" - } - }, - "properties": { - "$ref": "#/definitions/model.JSONStruct" - }, - "scopes": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "traits": { - "type": "array", - "items": { - "$ref": "#/definitions/model.ApplicationTraitPlan" - } - }, - "type": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "model.ApplicationTraitPlan": { - "required": [ - "type" - ], - "properties": { - "properties": { - "$ref": "#/definitions/model.JSONStruct" - }, - "type": { - "type": "string" - } - } - }, - "model.Cluster": { - "required": [ - "createTime", - "updateTime", - "name", - "alias", - "description", - "icon", - "labels", - "status", - "reason", - "provider", - "apiServerURL", - "dashboardURL", - "kubeConfig", - "kubeConfigSecret" - ], - "properties": { - "alias": { - "type": "string" - }, - "apiServerURL": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "dashboardURL": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "kubeConfig": { - "type": "string" - }, - "kubeConfigSecret": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "provider": { - "$ref": "#/definitions/model.ProviderInfo" - }, - "reason": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "model.GitAddonSource": { - "properties": { - "path": { - "type": "string" - }, - "token": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "model.JSONStruct": { - "type": "object" - }, - "model.Model": { - "required": [ - "createTime", - "updateTime" - ], - "properties": { - "createTime": { - "type": "string", - "format": "date-time" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "model.ProviderInfo": { - "required": [ - "provider", - "name", - "id", - "zone", - "labels" - ], - "properties": { - "id": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "provider": { - "type": "string" - }, - "zone": { - "type": "string" - } - } - }, - "regexp.Regexp": { - "required": [ - "expr", - "prog", - "onepass", - "numSubexp", - "maxBitStateLen", - "subexpNames", - "prefix", - "prefixBytes", - "prefixRune", - "prefixEnd", - "mpool", - "matchcap", - "prefixComplete", - "cond", - "minInputLen", - "longest" - ], - "properties": { - "cond": { - "type": "integer", - "format": "byte" - }, - "expr": { - "type": "string" - }, - "longest": { - "type": "boolean" - }, - "matchcap": { - "type": "integer", - "format": "int32" - }, - "maxBitStateLen": { - "type": "integer", - "format": "int32" - }, - "minInputLen": { - "type": "integer", - "format": "int32" - }, - "mpool": { - "type": "integer", - "format": "int32" - }, - "numSubexp": { - "type": "integer", - "format": "int32" - }, - "onepass": { - "$ref": "#/definitions/regexp.onePassProg" - }, - "prefix": { - "type": "string" - }, - "prefixBytes": { - "type": "string" - }, - "prefixComplete": { - "type": "boolean" - }, - "prefixEnd": { - "type": "integer", - "format": "integer" - }, - "prefixRune": { - "type": "integer", - "format": "int32" - }, - "prog": { - "$ref": "#/definitions/syntax.Prog" - }, - "subexpNames": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "regexp.onePassInst": { - "required": [ - "Op", - "Out", - "Arg", - "Rune", - "Next" - ], - "properties": { - "Arg": { - "type": "integer", - "format": "integer" - }, - "Next": { - "type": "array", - "items": { - "type": "integer" - } - }, - "Op": { - "type": "integer", - "format": "byte" - }, - "Out": { - "type": "integer", - "format": "integer" - }, - "Rune": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "regexp.onePassProg": { - "required": [ - "Inst", - "Start", - "NumCap" - ], - "properties": { - "Inst": { - "type": "array", - "items": { - "$ref": "#/definitions/regexp.onePassInst" - } - }, - "NumCap": { - "type": "integer", - "format": "int32" - }, - "Start": { - "type": "integer", - "format": "int32" - } - } - }, - "syntax.Inst": { - "required": [ - "Op", - "Out", - "Arg", - "Rune" - ], - "properties": { - "Arg": { - "type": "integer", - "format": "integer" - }, - "Op": { - "type": "integer", - "format": "byte" - }, - "Out": { - "type": "integer", - "format": "integer" - }, - "Rune": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "syntax.Prog": { - "required": [ - "Inst", - "Start", - "NumCap" - ], - "properties": { - "Inst": { - "type": "array", - "items": { - "$ref": "#/definitions/syntax.Inst" - } - }, - "NumCap": { - "type": "integer", - "format": "int32" - }, - "Start": { - "type": "integer", - "format": "int32" - } - } - }, - "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.AccessKeyRequest": { - "required": [ - "accessKeyID", - "accessKeySecret" - ], - "properties": { - "accessKeyID": { - "type": "string" - }, - "accessKeySecret": { - "type": "string" - } - } - }, - "v1.AddonDependency": { - "properties": { - "name": { - "type": "string" - } - } - }, - "v1.AddonDeployTo": { - "required": [ - "control_plane", - "runtime_cluster" - ], - "properties": { - "control_plane": { - "type": "boolean" - }, - "runtime_cluster": { - "type": "boolean" - } - } - }, - "v1.AddonElementFile": { - "required": [ - "Data", - "Name" - ], - "properties": { - "Data": { - "type": "string" - }, - "Name": { - "type": "string" - } - } - }, - "v1.AddonMeta": { - "required": [ - "name", - "version", - "description", - "icon" - ], - "properties": { - "dependencies": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonDependency" - } - }, - "deploy_to": { - "$ref": "#/definitions/v1.AddonDeployTo" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "url": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "v1.AddonRegistryMeta": { - "required": [ - "name" - ], - "properties": { - "git": { - "$ref": "#/definitions/model.GitAddonSource" - }, - "name": { - "type": "string" - } - } - }, - "v1.AddonStatusResponse": { - "required": [ - "phase" - ], - "properties": { - "enabling_progress": { - "$ref": "#/definitions/v1.EnablingProgress" - }, - "phase": { - "type": "string" - } - } - }, - "v1.ApplicationDeployRequest": { - "required": [ - "workflowName", - "commit", - "sourceType", - "force" - ], - "properties": { - "commit": { - "type": "string" - }, - "force": { - "type": "boolean" - }, - "sourceType": { - "type": "string" - }, - "workflowName": { - "type": "string" - } - } - }, - "v1.ApplicationDeployResponse": { - "required": [ - "version", - "status", - "reason", - "deployUser", - "commit", - "sourceType" - ], - "properties": { - "commit": { - "type": "string" - }, - "deployUser": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "sourceType": { - "type": "string" - }, - "status": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "v1.ApplicationPlanBase": { - "required": [ - "name", - "alias", - "namespace", - "description", - "createTime", - "updateTime", - "icon", - "status", - "gatewayRule" - ], - "properties": { - "alias": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "description": { - "type": "string" - }, - "envBind": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.EnvBind" - } - }, - "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", - "createTime", - "updateTime" - ], - "properties": { - "createTime": { - "type": "string", - "format": "date-time" - }, - "templateName": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - }, - "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", - "alias", - "description", - "icon", - "labels", - "providerInfo", - "apiServerURL", - "dashboardURL", - "status", - "reason" - ], - "properties": { - "alias": { - "type": "string" - }, - "apiServerURL": { - "type": "string" - }, - "dashboardURL": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "providerInfo": { - "$ref": "#/definitions/model.ProviderInfo" - }, - "reason": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "v1.ClusterResourceInfo": { - "required": [ - "workerNumber", - "masterNumber", - "memoryCapacity", - "cpuCapacity", - "podCapacity", - "memoryUsed", - "cpuUsed", - "podUsed" - ], - "properties": { - "cpuCapacity": { - "type": "integer", - "format": "int64" - }, - "cpuUsed": { - "type": "integer", - "format": "int64" - }, - "gpuCapacity": { - "type": "integer", - "format": "int64" - }, - "gpuUsed": { - "type": "integer", - "format": "int64" - }, - "masterNumber": { - "type": "integer", - "format": "int32" - }, - "memoryCapacity": { - "type": "integer", - "format": "int64" - }, - "memoryUsed": { - "type": "integer", - "format": "int64" - }, - "podCapacity": { - "type": "integer", - "format": "int64" - }, - "podUsed": { - "type": "integer", - "format": "int64" - }, - "storageClassList": { - "type": "array", - "items": { - "type": "string" - } - }, - "workerNumber": { - "type": "integer", - "format": "int32" - } - } - }, - "v1.ClusterSelector": { - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - } - } - }, - "v1.ComponentPlanBase": { - "required": [ - "name", - "alias", - "description", - "componentType", - "envNames", - "dependsOn", - "deployVersion", - "createTime", - "updateTime" - ], - "properties": { - "alias": { - "type": "string" - }, - "componentType": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "creator": { - "type": "string" - }, - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "deployVersion": { - "type": "string" - }, - "description": { - "type": "string" - }, - "envNames": { - "type": "array", - "items": { - "type": "string" - } - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "v1.ComponentPlanListResponse": { - "required": [ - "componentplans" - ], - "properties": { - "componentplans": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ComponentPlanBase" - } - } - } - }, - "v1.ComponentSelector": { - "required": [ - "components" - ], - "properties": { - "components": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "v1.ConnectCloudClusterRequest": { - "required": [ - "accessKeyID", - "accessKeySecret", - "clusterID", - "name", - "alias", - "icon" - ], - "properties": { - "accessKeyID": { - "type": "string" - }, - "accessKeySecret": { - "type": "string" - }, - "alias": { - "type": "string" - }, - "clusterID": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - } - } - }, - "v1.CreateAddonRegistryRequest": { - "required": [ - "name" - ], - "properties": { - "git": { - "$ref": "#/definitions/model.GitAddonSource" - }, - "name": { - "type": "string" - } - } - }, - "v1.CreateApplicationEnvPlanRequest": { - "required": [ - "clusterSelector", - "componentSelector", - "name", - "alias" - ], - "properties": { - "alias": { - "type": "string" - }, - "clusterSelector": { - "$ref": "#/definitions/v1.ClusterSelector" - }, - "componentSelector": { - "$ref": "#/definitions/v1.ComponentSelector" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "v1.CreateApplicationPlanRequest": { - "required": [ - "name", - "alias", - "namespace", - "description", - "icon" - ], - "properties": { - "alias": { - "type": "string" - }, - "deploy": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "envBind": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.EnvBind" - } - }, - "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.CreateCloudClusterRequest": { - "required": [ - "accessKeyID", - "accessKeySecret", - "name", - "zone", - "workerNumber", - "cpuCoresPerWorker", - "memoryPerWorker" - ], - "properties": { - "accessKeyID": { - "type": "string" - }, - "accessKeySecret": { - "type": "string" - }, - "cpuCoresPerWorker": { - "type": "integer", - "format": "int64" - }, - "memoryPerWorker": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - }, - "workerNumber": { - "type": "integer", - "format": "int32" - }, - "zone": { - "type": "string" - } - } - }, - "v1.CreateCloudClusterResponse": { - "required": [ - "clusterID", - "status" - ], - "properties": { - "clusterID": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "v1.CreateClusterRequest": { - "required": [ - "name", - "alias", - "icon" - ], - "properties": { - "alias": { - "type": "string" - }, - "dashboardURL": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "kubeConfig": { - "type": "string" - }, - "kubeConfigSecret": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - } - } - }, - "v1.CreateComponentPlanRequest": { - "required": [ - "name", - "alias", - "description", - "icon", - "componentType", - "dependsOn" - ], - "properties": { - "alias": { - "type": "string" - }, - "componentType": { - "type": "string" - }, - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "envNames": { - "type": "array", - "items": { - "type": "string" - } - }, - "icon": { - "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", - "description", - "type", - "properties" - ], - "properties": { - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "v1.CreateWorkflowPlanRequest": { - "required": [ - "appName", - "name", - "alias", - "description", - "enable", - "default" - ], - "properties": { - "alias": { - "type": "string" - }, - "appName": { - "type": "string" - }, - "default": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "enable": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowStep" - } - } - } - }, - "v1.Definition": { - "required": [ - "kind", - "name", - "description" - ], - "properties": { - "description": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "v1.DefinitionBase": { - "required": [ - "name", - "description", - "icon" - ], - "properties": { - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "v1.DefinitionProperties": { - "required": [ - "title", - "type", - "compiledPattern" - ], - "properties": { - "compiledPattern": { - "$ref": "#/definitions/regexp.Regexp" - }, - "default": { - "$ref": "#/definitions/v1.DefinitionProperties.default" - }, - "description": { - "type": "string" - }, - "enum": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.DefinitionProperties.enum" - } - }, - "example": { - "$ref": "#/definitions/v1.DefinitionProperties.example" - }, - "items": { - "$ref": "#/definitions/v1.DefinitionSchema" - }, - "maxLength": { - "type": "integer", - "format": "integer" - }, - "maximum": { - "type": "number", - "format": "double" - }, - "minLength": { - "type": "integer", - "format": "integer" - }, - "minimum": { - "type": "number", - "format": "double" - }, - "multipleOf": { - "type": "number", - "format": "double" - }, - "pattern": { - "type": "string" - }, - "title": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "v1.DefinitionProperties.default": {}, - "v1.DefinitionProperties.enum": {}, - "v1.DefinitionProperties.example": {}, - "v1.DefinitionSchema": { - "required": [ - "properties", - "required", - "type" - ], - "properties": { - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/v1.DefinitionProperties" - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string" - } - } - }, - "v1.DetailAddonResponse": { - "required": [ - "name", - "version", - "description", - "icon", - "definitions", - "parameters", - "cue_templates" - ], - "properties": { - "cue_templates": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonElementFile" - } - }, - "definitions": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.Definition" - } - }, - "dependencies": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonDependency" - } - }, - "deploy_to": { - "$ref": "#/definitions/v1.AddonDeployTo" - }, - "description": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parameters": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "url": { - "type": "string" - }, - "version": { - "type": "string" - }, - "yaml_templates": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonElementFile" - } - } - } - }, - "v1.DetailApplicationPlanResponse": { - "required": [ - "createTime", - "status", - "namespace", - "alias", - "description", - "updateTime", - "icon", - "gatewayRule", - "name", - "policies", - "status", - "resourceInfo", - "workflowStatus" - ], - "properties": { - "alias": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "description": { - "type": "string" - }, - "envBind": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.EnvBind" - } - }, - "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", - "icon", - "provider", - "kubeConfigSecret", - "alias", - "status", - "labels", - "description", - "reason", - "apiServerURL", - "dashboardURL", - "kubeConfig", - "createTime", - "updateTime", - "resourceInfo" - ], - "properties": { - "alias": { - "type": "string" - }, - "apiServerURL": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "dashboardURL": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "kubeConfig": { - "type": "string" - }, - "kubeConfigSecret": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "provider": { - "$ref": "#/definitions/model.ProviderInfo" - }, - "reason": { - "type": "string" - }, - "resourceInfo": { - "$ref": "#/definitions/v1.ClusterResourceInfo" - }, - "status": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "v1.DetailComponentPlanResponse": { - "required": [ - "creator", - "createTime", - "updateTime", - "name", - "alias", - "appPrimaryKey", - "type" - ], - "properties": { - "alias": { - "type": "string" - }, - "appPrimaryKey": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "creator": { - "type": "string" - }, - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "externalRevision": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "inputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.inputItem" - } - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "outputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.outputItem" - } - }, - "properties": { - "$ref": "#/definitions/model.JSONStruct" - }, - "scopes": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "traits": { - "type": "array", - "items": { - "$ref": "#/definitions/model.ApplicationTraitPlan" - } - }, - "type": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "v1.DetailDefinitionResponse": { - "required": [ - "schema" - ], - "properties": { - "schema": { - "$ref": "#/definitions/v1.DefinitionSchema" - } - } - }, - "v1.DetailPolicyResponse": { - "required": [ - "creator", - "properties", - "createTime", - "updateTime", - "name", - "type", - "description" - ], - "properties": { - "createTime": { - "type": "string", - "format": "date-time" - }, - "creator": { - "type": "string" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "$ref": "#/definitions/model.JSONStruct" - }, - "type": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "v1.DetailWorkflowPlanResponse": { - "required": [ - "alias", - "description", - "enable", - "default", - "createTime", - "updateTime", - "name", - "workflowRecord" - ], - "properties": { - "alias": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "default": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "enable": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowStep" - } - }, - "updateTime": { - "type": "string", - "format": "date-time" - }, - "workflowRecord": { - "$ref": "#/definitions/v1.WorkflowRecord" - } - } - }, - "v1.DetailWorkflowRecordResponse": { - "required": [ - "name", - "namespace", - "suspend", - "terminated", - "deployTime", - "deployUser", - "commit", - "sourceType" - ], - "properties": { - "commit": { - "type": "string" - }, - "deployTime": { - "type": "string", - "format": "date-time" - }, - "deployUser": { - "type": "string" - }, - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "sourceType": { - "type": "string" - }, - "startTime": { - "type": "string", - "format": "date-time" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/common.WorkflowStepStatus" - } - }, - "suspend": { - "type": "boolean" - }, - "terminated": { - "type": "boolean" - } - } - }, - "v1.EmptyResponse": {}, - "v1.EnableAddonRequest": { - "properties": { - "args": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - }, - "v1.EnablingProgress": { - "required": [ - "enabled_components", - "total_components" - ], - "properties": { - "enabled_components": { - "type": "integer", - "format": "int32" - }, - "total_components": { - "type": "integer", - "format": "int32" - } - } - }, - "v1.EnvBind": { - "required": [ - "name", - "alias", - "clusterSelector", - "componentSelector" - ], - "properties": { - "alias": { - "type": "string" - }, - "clusterSelector": { - "$ref": "#/definitions/v1.ClusterSelector" - }, - "componentSelector": { - "$ref": "#/definitions/v1.ComponentSelector" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "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.ListAddonRegistryResponse": { - "required": [ - "registrys" - ], - "properties": { - "registrys": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonRegistryMeta" - } - } - } - }, - "v1.ListAddonResponse": { - "required": [ - "addons" - ], - "properties": { - "addons": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonMeta" - } - } - } - }, - "v1.ListApplicationPlanResponse": { - "required": [ - "applicationplans" - ], - "properties": { - "applicationplans": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ApplicationPlanBase" - } - } - } - }, - "v1.ListApplicationPolicy": { - "required": [ - "policies" - ], - "properties": { - "policies": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.PolicyBase" - } - } - } - }, - "v1.ListCloudClusterCreationResponse": { - "required": [ - "creations" - ], - "properties": { - "creations": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "v1.ListCloudClusterResponse": { - "required": [ - "clusters", - "total" - ], - "properties": { - "clusters": { - "type": "array", - "items": { - "$ref": "#/definitions/cloudprovider.CloudCluster" - } - }, - "total": { - "type": "integer", - "format": "int32" - } - } - }, - "v1.ListClusterResponse": { - "required": [ - "clusters" - ], - "properties": { - "clusters": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ClusterBase" - } - } - } - }, - "v1.ListDefinitionResponse": { - "required": [ - "definitions" - ], - "properties": { - "definitions": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.DefinitionBase" - } - } - } - }, - "v1.ListNamespaceResponse": { - "required": [ - "namespaces" - ], - "properties": { - "namespaces": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.NamespaceBase" - } - } - } - }, - "v1.ListPolicyDefinitionResponse": { - "required": [ - "policyDefinitions" - ], - "properties": { - "policyDefinitions": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.PolicyDefinition" - } - } - } - }, - "v1.ListWorkflowPlanResponse": { - "required": [ - "workflowplans" - ], - "properties": { - "workflowplans": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowPlanBase" - } - } - } - }, - "v1.ListWorkflowRecordsResponse": { - "required": [ - "records", - "total" - ], - "properties": { - "records": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowRecord" - } - }, - "total": { - "type": "integer", - "format": "int64" - } - } - }, - "v1.NamespaceBase": { - "required": [ - "name", - "description", - "createTime", - "updateTime" - ], - "properties": { - "createTime": { - "type": "string", - "format": "date-time" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "v1.NamespaceDetailResponse": { - "required": [ - "name", - "description", - "createTime", - "updateTime" - ], - "properties": { - "createTime": { - "type": "string", - "format": "date-time" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "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.PolicyBase": { - "required": [ - "name", - "type", - "description", - "creator", - "properties", - "createTime", - "updateTime" - ], - "properties": { - "createTime": { - "type": "string", - "format": "date-time" - }, - "creator": { - "type": "string" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "properties": { - "$ref": "#/definitions/model.JSONStruct" - }, - "type": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "v1.PolicyDefinition": { - "required": [ - "name", - "description", - "parameters" - ], - "properties": { - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parameters": { - "type": "array", - "items": { - "$ref": "#/definitions/types.Parameter" - } - } - } - }, - "v1.PutApplicationPlanEnvRequest": { - "properties": { - "alias": { - "type": "string" - }, - "clusterSelector": { - "$ref": "#/definitions/v1.ClusterSelector" - }, - "componentSelector": { - "$ref": "#/definitions/v1.ComponentSelector" - }, - "description": { - "type": "string" - } - } - }, - "v1.UpdateApplicationPlanRequest": { - "required": [ - "alias", - "description", - "icon" - ], - "properties": { - "alias": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - }, - "v1.UpdatePolicyRequest": { - "required": [ - "description", - "type", - "properties" - ], - "properties": { - "description": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "v1.UpdateWorkflowPlanRequest": { - "required": [ - "alias", - "description", - "enable", - "default" - ], - "properties": { - "alias": { - "type": "string" - }, - "default": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "enable": { - "type": "boolean" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowStep" - } - } - } - }, - "v1.VelaQLViewResponse": { - "type": "object" - }, - "v1.WorkflowPlanBase": { - "required": [ - "name", - "alias", - "description", - "enable", - "default", - "createTime", - "updateTime" - ], - "properties": { - "alias": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "default": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "enable": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "v1.WorkflowRecord": { - "required": [ - "name", - "namespace", - "suspend", - "terminated" - ], - "properties": { - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "startTime": { - "type": "string", - "format": "date-time" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/common.WorkflowStepStatus" - } - }, - "suspend": { - "type": "boolean" - }, - "terminated": { - "type": "boolean" - } - } - }, - "v1.WorkflowStep": { - "required": [ - "name", - "type", - "dependsOn" - ], - "properties": { - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "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": { - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "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" - } - } - } - } + "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/addon_registries": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "list all addon registry", + "operationId": "listAddonRegistry", + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListAddonRegistryResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "create an addon registry", + "operationId": "createAddonRegistry", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateAddonRegistryRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addon_registries/{name}": { + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "delete an addon registry", + "operationId": "deleteAddonRegistry", + "parameters": [ + { + "type": "string", + "description": "identifier of the addon registry", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "list all addons", + "operationId": "listAddons", + "parameters": [ + { + "type": "string", + "description": "filter addons from given registry", + "name": "registry", + "in": "query" + }, + { + "type": "string", + "description": "Fuzzy search based on name and description.", + "name": "query", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListAddonResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "show details of an addon", + "operationId": "detailAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to query detail", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailAddonResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/disable": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "disable an addon", + "operationId": "disableAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to enable", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/enable": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "enable an addon", + "operationId": "enableAddon", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.EnableAddonRequest" + } + }, + { + "type": "string", + "description": "addon name to enable", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/status": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "show status of an addon", + "operationId": "statusAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to query status", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "list all application plans", + "operationId": "listApplicationPlans", + "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": { + "schema": { + "$ref": "#/definitions/v1.ListApplicationPlanResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "create one application plan", + "operationId": "createApplicationPlan", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationPlanRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationPlanBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "detail one application plan", + "operationId": "detailApplicationPlan", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailApplicationPlanResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "update one application plan", + "operationId": "updateApplicationPlan", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateApplicationPlanRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationPlanBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "delete one application", + "operationId": "deleteApplicationPlan", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/componentplans": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "gets the componentplan topology of the application", + "operationId": "listApplicationComponents", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "list components that deployed in define env", + "name": "envName", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ComponentPlanListResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "create component plan for application plan", + "operationId": "createComponent", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateComponentPlanRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ComponentPlanBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/componentplans/{componentName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "detail component plan for application plan", + "operationId": "detailComponent", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailComponentPlanResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/deploy": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "deploy or upgrade the application", + "operationId": "deployApplication", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationDeployRequest" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/envs": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "creating an application environment plan", + "operationId": "createApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationEnvPlanRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EnvBind" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/envs/{envName}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "set application plan differences in the specified environment", + "operationId": "updateApplicationEnvBinding", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application plan", + "name": "envName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.PutApplicationPlanEnvRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EnvBind" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "delete an application environment plan", + "operationId": "deleteApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application plan", + "name": "envName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "404": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/policies": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "list policy for application", + "operationId": "listApplicationPolicies", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListApplicationPolicy" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "create policy for application", + "operationId": "createApplicationPolicy", + "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": { + "schema": { + "$ref": "#/definitions/v1.PolicyBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/policies/{policyName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "detail policy for application", + "operationId": "detailApplicationPolicy", + "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": { + "schema": { + "$ref": "#/definitions/v1.DetailPolicyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "update policy for application", + "operationId": "updateApplicationPolicy", + "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 + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdatePolicyRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailPolicyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "detail policy for application", + "operationId": "deleteApplicationPolicy", + "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": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applicationplans/{name}/template": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "applicationplan" + ], + "summary": "create one application template", + "operationId": "publishApplicationTemplate", + "parameters": [ + { + "type": "string", + "description": "identifier of the application plan", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationTemplateRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationTemplateBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list all clusters", + "operationId": "listKubeClusters", + "parameters": [ + { + "type": "string", + "description": "Fuzzy search based on name or description", + "name": "query", + "in": "query" + }, + { + "type": "int", + "default": 0, + "description": "Page for paging", + "name": "page", + "in": "query" + }, + { + "type": "int", + "default": 20, + "description": "PageSize for paging", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "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": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list cloud clusters", + "operationId": "listCloudClusters", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "int", + "default": 0, + "description": "Page for paging", + "name": "page", + "in": "query" + }, + { + "type": "int", + "default": 20, + "description": "PageSize for paging", + "name": "pageSize", + "in": "query" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.AccessKeyRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/connect": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create cluster from cloud cluster", + "operationId": "connectCloudCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.ConnectCloudClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/create": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create cloud cluster", + "operationId": "createCloudCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/creation": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list cloud cluster creation", + "operationId": "listCloudClusterCreation", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListCloudClusterCreationResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/creation/{cloudClusterName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "check cloud cluster create status", + "operationId": "getCloudClusterCreationStatus", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier for cloud cluster which is creating", + "name": "cloudClusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "delete cloud cluster creation", + "operationId": "deleteCloudClusterCreation", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier for cloud cluster which is creating", + "name": "cloudClusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/{clusterName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "detail cluster info", + "operationId": "getKubeCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "modify cluster", + "operationId": "modifyKubeCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "delete cluster", + "operationId": "deleteKubeCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/definitions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "definition" + ], + "summary": "list all definitions", + "operationId": "listDefinitions", + "parameters": [ + { + "enum": [ + "component", + "trait", + "workflowstep" + ], + "type": "string", + "description": "query the definition type", + "name": "type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "if specified, query the definition supported by the env.", + "name": "envName", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/definitions/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "definition" + ], + "summary": "detail definition", + "operationId": "detailDefinition", + "parameters": [ + { + "type": "string", + "description": "identifier of the definition", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "query the definition type", + "name": "type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/namespaces": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "list all namespaces", + "operationId": "listNamespaces", + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListNamespaceResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "create namespace", + "operationId": "createNamespace", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateNamespaceRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.NamespaceDetailResponse" + } + } + } + } + }, + "/api/v1/policydefinitions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "definition" + ], + "summary": "list all policydefinition", + "operationId": "noop", + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListPolicyDefinitionResponse" + } + } + } + } + }, + "/api/v1/query": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "velaQL" + ], + "summary": "use velaQL to query resource status", + "operationId": "queryView", + "parameters": [ + { + "type": "string", + "description": "velaql query statement", + "name": "velaql", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.VelaQLViewResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/workflowplans": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "workflowplan" + ], + "summary": "list application workflow", + "operationId": "listApplicationWorkflows", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "appName", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "query based on enable status", + "name": "enable", + "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": [ + "workflowplan" + ], + "summary": "create application workflow", + "operationId": "createApplicationWorkflow", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateWorkflowPlanRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "400": { + "description": "create failure", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/workflowplans/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "workflowplan" + ], + "summary": "detail application workflow", + "operationId": "detailWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow.", + "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": [ + "workflowplan" + ], + "summary": "update application workflow config", + "operationId": "updateWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateWorkflowPlanRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "workflowplan" + ], + "summary": "deletet workflow", + "operationId": "deleteWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/workflowplans/{name}/records": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "workflowplan" + ], + "summary": "query application workflow execution record", + "operationId": "listWorkflowRecords", + "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" + } + } + } + }, + "/api/v1/workflowplans/{name}/records/{record}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "workflowplan" + ], + "summary": "query application workflow execution record detail", + "operationId": "detailWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/v1/namespaces/{namespace}/applications/{appname}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "oam-application" + ], + "summary": "get the specified oam application in the specified namespace", + "operationId": "getApplication", + "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": { + "schema": { + "$ref": "#/definitions/v1.ApplicationResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "oam-application" + ], + "summary": "create or update oam application in the specified namespace", + "operationId": "createOrUpdateApplication", + "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": [ + "oam-application" + ], + "summary": "create or update oam application in the specified namespace", + "operationId": "deleteApplication", + "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" + } + } + } + } + }, + "definitions": { + "bcode.Bcode": { + "required": [ + "BusinessCode", + "Message" + ], + "properties": { + "BusinessCode": { + "type": "integer", + "format": "int32" + }, + "Message": { + "type": "string" + } + } + }, + "cloudprovider.CloudCluster": { + "required": [ + "provider", + "id", + "name", + "type", + "zone", + "labels", + "status", + "apiServerURL", + "dashboardURL" + ], + "properties": { + "apiServerURL": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "zone": { + "type": "string" + } + } + }, + "common.AppRolloutStatus": { + "required": [ + "rollingState", + "batchRollingState", + "currentBatch", + "upgradedReadyReplicas", + "upgradedReplicas", + "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": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "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", + "finished" + ], + "properties": { + "appRevision": { + "type": "string" + }, + "contextBackend": { + "$ref": "#/definitions/v1.ObjectReference" + }, + "finished": { + "type": "boolean" + }, + "mode": { + "type": "string" + }, + "startTime": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + }, + "suspend": { + "type": "boolean" + }, + "terminated": { + "type": "boolean" + } + } + }, + "common.WorkflowStepStatus": { + "required": [ + "id" + ], + "properties": { + "firstExecuteTime": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastExecuteTime": { + "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" + } + }, + "model.ApplicationComponentPlan": { + "required": [ + "createTime", + "updateTime", + "appPrimaryKey", + "creator", + "name", + "alias", + "type" + ], + "properties": { + "alias": { + "type": "string" + }, + "appPrimaryKey": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "externalRevision": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ApplicationTraitPlan" + } + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "model.ApplicationTraitPlan": { + "required": [ + "type" + ], + "properties": { + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + } + } + }, + "model.Cluster": { + "required": [ + "createTime", + "updateTime", + "name", + "alias", + "description", + "icon", + "labels", + "status", + "reason", + "provider", + "apiServerURL", + "dashboardURL", + "kubeConfig", + "kubeConfigSecret" + ], + "properties": { + "alias": { + "type": "string" + }, + "apiServerURL": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/model.ProviderInfo" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "model.GitAddonSource": { + "properties": { + "path": { + "type": "string" + }, + "token": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "model.JSONStruct": { + "type": "object" + }, + "model.Model": { + "required": [ + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "model.ProviderInfo": { + "required": [ + "provider", + "name", + "id", + "zone", + "labels" + ], + "properties": { + "id": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "zone": { + "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": {}, + "utils.Option": { + "required": [ + "label", + "value" + ], + "properties": { + "label": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/utils.Option.value" + } + } + }, + "utils.Option.value": {}, + "utils.UIParameter": { + "required": [ + "sort", + "label", + "description", + "jsonKey", + "uiType" + ], + "properties": { + "description": { + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "jsonKey": { + "type": "string" + }, + "label": { + "type": "string" + }, + "sort": { + "type": "integer", + "format": "integer" + }, + "subParameterGroupOption": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.UIParameter.subParameterGroupOption" + } + }, + "subParameters": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.UIParameter" + } + }, + "uiType": { + "type": "string" + }, + "validate": { + "$ref": "#/definitions/utils.Validate" + } + } + }, + "utils.Validate": { + "properties": { + "defaultValue": { + "$ref": "#/definitions/utils.Validate.defaultValue" + }, + "max": { + "type": "number", + "format": "double" + }, + "maxLength": { + "type": "integer", + "format": "integer" + }, + "min": { + "type": "number", + "format": "double" + }, + "minLength": { + "type": "integer", + "format": "integer" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.Option" + } + }, + "pattern": { + "type": "string" + }, + "required": { + "type": "boolean" + } + } + }, + "utils.Validate.defaultValue": {}, + "v1.AccessKeyRequest": { + "required": [ + "accessKeyID", + "accessKeySecret" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + } + } + }, + "v1.AddonDependency": { + "properties": { + "name": { + "type": "string" + } + } + }, + "v1.AddonDeployTo": { + "required": [ + "control_plane", + "runtime_cluster" + ], + "properties": { + "control_plane": { + "type": "boolean" + }, + "runtime_cluster": { + "type": "boolean" + } + } + }, + "v1.AddonElementFile": { + "required": [ + "Data", + "Name", + "Path" + ], + "properties": { + "Data": { + "type": "string" + }, + "Name": { + "type": "string" + }, + "Path": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.AddonMeta": { + "required": [ + "name", + "version", + "description", + "icon" + ], + "properties": { + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonDependency" + } + }, + "deploy_to": { + "$ref": "#/definitions/v1.AddonDeployTo" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "url": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "v1.AddonRegistryMeta": { + "required": [ + "name" + ], + "properties": { + "git": { + "$ref": "#/definitions/model.GitAddonSource" + }, + "name": { + "type": "string" + } + } + }, + "v1.AddonStatusResponse": { + "required": [ + "phase" + ], + "properties": { + "enabling_progress": { + "$ref": "#/definitions/v1.EnablingProgress" + }, + "phase": { + "type": "string" + } + } + }, + "v1.ApplicationDeployRequest": { + "required": [ + "workflowName", + "commit", + "sourceType", + "force" + ], + "properties": { + "commit": { + "type": "string" + }, + "force": { + "type": "boolean" + }, + "sourceType": { + "type": "string" + }, + "workflowName": { + "type": "string" + } + } + }, + "v1.ApplicationDeployResponse": { + "required": [ + "version", + "status", + "reason", + "deployUser", + "commit", + "sourceType" + ], + "properties": { + "commit": { + "type": "string" + }, + "deployUser": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "v1.ApplicationPlanBase": { + "required": [ + "name", + "alias", + "namespace", + "description", + "createTime", + "updateTime", + "icon", + "status", + "gatewayRule" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "envBind": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBind" + } + }, + "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", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "templateName": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "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", + "alias", + "description", + "icon", + "labels", + "providerInfo", + "apiServerURL", + "dashboardURL", + "status", + "reason" + ], + "properties": { + "alias": { + "type": "string" + }, + "apiServerURL": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "providerInfo": { + "$ref": "#/definitions/model.ProviderInfo" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "v1.ClusterResourceInfo": { + "required": [ + "workerNumber", + "masterNumber", + "memoryCapacity", + "cpuCapacity", + "podCapacity", + "memoryUsed", + "cpuUsed", + "podUsed" + ], + "properties": { + "cpuCapacity": { + "type": "integer", + "format": "int64" + }, + "cpuUsed": { + "type": "integer", + "format": "int64" + }, + "gpuCapacity": { + "type": "integer", + "format": "int64" + }, + "gpuUsed": { + "type": "integer", + "format": "int64" + }, + "masterNumber": { + "type": "integer", + "format": "int32" + }, + "memoryCapacity": { + "type": "integer", + "format": "int64" + }, + "memoryUsed": { + "type": "integer", + "format": "int64" + }, + "podCapacity": { + "type": "integer", + "format": "int64" + }, + "podUsed": { + "type": "integer", + "format": "int64" + }, + "storageClassList": { + "type": "array", + "items": { + "type": "string" + } + }, + "workerNumber": { + "type": "integer", + "format": "int32" + } + } + }, + "v1.ClusterSelector": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "v1.ComponentPlanBase": { + "required": [ + "name", + "alias", + "description", + "componentType", + "envNames", + "dependsOn", + "deployVersion", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "componentType": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "deployVersion": { + "type": "string" + }, + "description": { + "type": "string" + }, + "envNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.ComponentPlanListResponse": { + "required": [ + "componentplans" + ], + "properties": { + "componentplans": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ComponentPlanBase" + } + } + } + }, + "v1.ComponentSelector": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.ConnectCloudClusterRequest": { + "required": [ + "accessKeyID", + "accessKeySecret", + "clusterID", + "name", + "alias", + "icon" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "clusterID": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "v1.CreateAddonRegistryRequest": { + "required": [ + "name" + ], + "properties": { + "git": { + "$ref": "#/definitions/model.GitAddonSource" + }, + "name": { + "type": "string" + } + } + }, + "v1.CreateApplicationEnvPlanRequest": { + "required": [ + "name", + "alias", + "clusterSelector", + "componentSelector" + ], + "properties": { + "alias": { + "type": "string" + }, + "clusterSelector": { + "$ref": "#/definitions/v1.ClusterSelector" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.CreateApplicationPlanRequest": { + "required": [ + "name", + "alias", + "namespace", + "description", + "icon" + ], + "properties": { + "alias": { + "type": "string" + }, + "deploy": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "envBind": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBind" + } + }, + "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.CreateCloudClusterRequest": { + "required": [ + "accessKeyID", + "accessKeySecret", + "name", + "zone", + "workerNumber", + "cpuCoresPerWorker", + "memoryPerWorker" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + }, + "cpuCoresPerWorker": { + "type": "integer", + "format": "int64" + }, + "memoryPerWorker": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "workerNumber": { + "type": "integer", + "format": "int32" + }, + "zone": { + "type": "string" + } + } + }, + "v1.CreateCloudClusterResponse": { + "required": [ + "clusterID", + "status" + ], + "properties": { + "clusterID": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "v1.CreateClusterRequest": { + "required": [ + "name", + "alias", + "icon" + ], + "properties": { + "alias": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "v1.CreateComponentPlanRequest": { + "required": [ + "name", + "alias", + "description", + "icon", + "componentType", + "dependsOn" + ], + "properties": { + "alias": { + "type": "string" + }, + "componentType": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "envNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "icon": { + "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", + "description", + "type", + "properties" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.CreateWorkflowPlanRequest": { + "required": [ + "appName", + "name", + "alias", + "description", + "enable", + "default" + ], + "properties": { + "alias": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + } + } + }, + "v1.Definition": { + "required": [ + "kind", + "name", + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.DefinitionBase": { + "required": [ + "name", + "description", + "icon" + ], + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.DetailAddonResponse": { + "required": [ + "icon", + "name", + "version", + "description", + "definitions", + "parameters", + "cue_templates" + ], + "properties": { + "cue_templates": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonElementFile" + } + }, + "definitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.Definition" + } + }, + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonDependency" + } + }, + "deploy_to": { + "$ref": "#/definitions/v1.AddonDeployTo" + }, + "description": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "url": { + "type": "string" + }, + "version": { + "type": "string" + }, + "yaml_templates": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonElementFile" + } + } + } + }, + "v1.DetailApplicationPlanResponse": { + "required": [ + "updateTime", + "icon", + "status", + "name", + "alias", + "description", + "createTime", + "namespace", + "gatewayRule", + "policies", + "status", + "resourceInfo", + "workflowStatus" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "envBind": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBind" + } + }, + "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": [ + "createTime", + "icon", + "labels", + "reason", + "description", + "status", + "dashboardURL", + "name", + "apiServerURL", + "kubeConfigSecret", + "updateTime", + "alias", + "provider", + "kubeConfig", + "resourceInfo" + ], + "properties": { + "alias": { + "type": "string" + }, + "apiServerURL": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/model.ProviderInfo" + }, + "reason": { + "type": "string" + }, + "resourceInfo": { + "$ref": "#/definitions/v1.ClusterResourceInfo" + }, + "status": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.DetailComponentPlanResponse": { + "required": [ + "appPrimaryKey", + "updateTime", + "name", + "creator", + "type", + "createTime", + "alias" + ], + "properties": { + "alias": { + "type": "string" + }, + "appPrimaryKey": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "externalRevision": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ApplicationTraitPlan" + } + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.DetailDefinitionResponse": { + "required": [ + "schema", + "uiSchema" + ], + "properties": { + "schema": { + "type": "string" + }, + "uiSchema": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.UIParameter" + } + } + } + }, + "v1.DetailPolicyResponse": { + "required": [ + "name", + "type", + "description", + "creator", + "properties", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.DetailWorkflowPlanResponse": { + "required": [ + "description", + "enable", + "default", + "createTime", + "updateTime", + "name", + "alias", + "workflowRecord" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "workflowRecord": { + "$ref": "#/definitions/v1.WorkflowRecord" + } + } + }, + "v1.DetailWorkflowRecordResponse": { + "required": [ + "name", + "namespace", + "suspend", + "terminated", + "deployTime", + "deployUser", + "commit", + "sourceType" + ], + "properties": { + "commit": { + "type": "string" + }, + "deployTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + }, + "suspend": { + "type": "boolean" + }, + "terminated": { + "type": "boolean" + } + } + }, + "v1.EmptyResponse": {}, + "v1.EnableAddonRequest": { + "properties": { + "args": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "v1.EnablingProgress": { + "required": [ + "enabled_components", + "total_components" + ], + "properties": { + "enabled_components": { + "type": "integer", + "format": "int32" + }, + "total_components": { + "type": "integer", + "format": "int32" + } + } + }, + "v1.EnvBind": { + "required": [ + "name", + "alias", + "clusterSelector", + "componentSelector" + ], + "properties": { + "alias": { + "type": "string" + }, + "clusterSelector": { + "$ref": "#/definitions/v1.ClusterSelector" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "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.ListAddonRegistryResponse": { + "required": [ + "registrys" + ], + "properties": { + "registrys": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + } + } + }, + "v1.ListAddonResponse": { + "required": [ + "addons" + ], + "properties": { + "addons": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonMeta" + } + } + } + }, + "v1.ListApplicationPlanResponse": { + "required": [ + "applicationplans" + ], + "properties": { + "applicationplans": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationPlanBase" + } + } + } + }, + "v1.ListApplicationPolicy": { + "required": [ + "policies" + ], + "properties": { + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PolicyBase" + } + } + } + }, + "v1.ListCloudClusterCreationResponse": { + "required": [ + "creations" + ], + "properties": { + "creations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.ListCloudClusterResponse": { + "required": [ + "clusters", + "total" + ], + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/cloudprovider.CloudCluster" + } + }, + "total": { + "type": "integer", + "format": "int32" + } + } + }, + "v1.ListClusterResponse": { + "required": [ + "clusters" + ], + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ClusterBase" + } + } + } + }, + "v1.ListDefinitionResponse": { + "required": [ + "definitions" + ], + "properties": { + "definitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.DefinitionBase" + } + } + } + }, + "v1.ListNamespaceResponse": { + "required": [ + "namespaces" + ], + "properties": { + "namespaces": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.NamespaceBase" + } + } + } + }, + "v1.ListPolicyDefinitionResponse": { + "required": [ + "policyDefinitions" + ], + "properties": { + "policyDefinitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PolicyDefinition" + } + } + } + }, + "v1.ListWorkflowPlanResponse": { + "required": [ + "workflowplans" + ], + "properties": { + "workflowplans": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowPlanBase" + } + } + } + }, + "v1.ListWorkflowRecordsResponse": { + "required": [ + "records", + "total" + ], + "properties": { + "records": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowRecord" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.NamespaceBase": { + "required": [ + "name", + "description", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.NamespaceDetailResponse": { + "required": [ + "updateTime", + "name", + "description", + "createTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "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.PolicyBase": { + "required": [ + "name", + "type", + "description", + "creator", + "properties", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.PolicyDefinition": { + "required": [ + "name", + "description", + "parameters" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Parameter" + } + } + } + }, + "v1.PutApplicationPlanEnvRequest": { + "properties": { + "alias": { + "type": "string" + }, + "clusterSelector": { + "$ref": "#/definitions/v1.ClusterSelector" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "description": { + "type": "string" + } + } + }, + "v1.UpdateApplicationPlanRequest": { + "required": [ + "alias", + "description", + "icon" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "v1.UpdatePolicyRequest": { + "required": [ + "description", + "type", + "properties" + ], + "properties": { + "description": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.UpdateWorkflowPlanRequest": { + "required": [ + "alias", + "description", + "enable", + "default" + ], + "properties": { + "alias": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + } + } + }, + "v1.VelaQLViewResponse": { + "type": "object" + }, + "v1.WorkflowPlanBase": { + "required": [ + "name", + "alias", + "description", + "enable", + "default", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.WorkflowRecord": { + "required": [ + "name", + "namespace", + "suspend", + "terminated" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + }, + "suspend": { + "type": "boolean" + }, + "terminated": { + "type": "boolean" + } + } + }, + "v1.WorkflowStep": { + "required": [ + "name", + "alias", + "type", + "description", + "dependsOn" + ], + "properties": { + "alias": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "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": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "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/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 96a895c5f..f14a4acb7 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -20,9 +20,11 @@ import ( "time" "github.com/getkin/kin-openapi/openapi3" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/model" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/cloudprovider" ) @@ -467,7 +469,8 @@ type ListDefinitionResponse struct { // DetailDefinitionResponse get definition detail type DetailDefinitionResponse struct { - Schema *openapi3.Schema `json:"schema"` + APISchema *openapi3.Schema `json:"schema"` + UISchema []*utils.UIParameter `json:"uiSchema"` } // DefinitionBase is the definition base model @@ -556,12 +559,14 @@ type UpdateWorkflowPlanRequest struct { // WorkflowStep workflow step config type WorkflowStep struct { // Name is the unique name of the workflow step. - Name string `json:"name" validate:"checkname"` - Type string `json:"type" validate:"checkname"` - DependsOn []string `json:"dependsOn"` - Properties string `json:"properties,omitempty"` - Inputs common.StepInputs `json:"inputs,omitempty"` - Outputs common.StepOutputs `json:"outputs,omitempty"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias"` + Type string `json:"type" validate:"checkname"` + Description string `json:"description"` + DependsOn []string `json:"dependsOn"` + Properties string `json:"properties,omitempty"` + Inputs common.StepInputs `json:"inputs,omitempty"` + Outputs common.StepOutputs `json:"outputs,omitempty"` } // DetailWorkflowPlanResponse detail workflow response diff --git a/pkg/apiserver/rest/rest_server.go b/pkg/apiserver/rest/rest_server.go index 0203fb7b1..8ee324af0 100644 --- a/pkg/apiserver/rest/rest_server.go +++ b/pkg/apiserver/rest/rest_server.go @@ -48,6 +48,7 @@ type Config struct { // APIServer interface for call api server type APIServer interface { Run(context.Context) error + RegisterServices() restfulspec.Config } type restServer struct { @@ -83,16 +84,13 @@ func New(cfg Config) (a APIServer, err error) { } func (s *restServer) Run(ctx context.Context) error { - webservice.Init(ctx, s.dataStore) - err := s.registerServices() - if err != nil { - return err - } + s.RegisterServices() return s.startHTTP(ctx) } -func (s *restServer) registerServices() error { - +// RegisterServices register web service +func (s *restServer) RegisterServices() restfulspec.Config { + webservice.Init(s.dataStore) /* ************************************************************** */ /* ************* Open API Route Group ***************** */ /* ************************************************************** */ @@ -119,7 +117,7 @@ func (s *restServer) registerServices() error { APIPath: "/apidocs.json", PostBuildSwaggerObjectHandler: enrichSwaggerObject} s.webContainer.Add(restfulspec.NewOpenAPIService(config)) - return nil + return config } func enrichSwaggerObject(swo *spec.Swagger) { diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 64ad60db1..d12682a11 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -30,8 +30,6 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" - restapis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" - restutils "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" cuemodel "github.com/oam-dev/kubevela/pkg/cue/model" "github.com/oam-dev/kubevela/pkg/cue/model/value" @@ -107,7 +105,7 @@ func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, var app v1beta1.Application err := u.kubeClient.Get(context.Background(), client.ObjectKey{ Namespace: types.DefaultKubeVelaNS, - Name: restutils.AddonName2AppName(name), + Name: AddonName2AppName(name), }, &app) if err != nil { if errors2.IsNotFound(err) { @@ -206,19 +204,19 @@ func (u *addonUsecaseImpl) ListAddonRegistries(ctx context.Context) ([]*apis.Add } var list []*apis.AddonRegistryMeta for _, entity := range entities { - list = append(list, restutils.ConvertAddonRegistryModel2AddonRegistryMeta(entity.(*model.AddonRegistry))) + list = append(list, ConvertAddonRegistryModel2AddonRegistryMeta(entity.(*model.AddonRegistry))) } return list, nil } -func renderApplication(addon *restapis.DetailAddonResponse, args *apis.EnableAddonRequest) (*v1beta1.Application, error) { +func renderApplication(addon *apis.DetailAddonResponse, args *apis.EnableAddonRequest) (*v1beta1.Application, error) { if args == nil { args = &apis.EnableAddonRequest{Args: map[string]string{}} } app := &v1beta1.Application{ TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, ObjectMeta: metav1.ObjectMeta{ - Name: restutils.AddonName2AppName(addon.Name), + Name: AddonName2AppName(addon.Name), Namespace: types.DefaultKubeVelaNS, Labels: map[string]string{ oam.LabelAddonName: addon.Name, @@ -294,7 +292,7 @@ func (u *addonUsecaseImpl) DisableAddon(ctx context.Context, name string) error app := &v1beta1.Application{ TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, ObjectMeta: metav1.ObjectMeta{ - Name: restutils.AddonName2AppName(name), + Name: AddonName2AppName(name), Namespace: types.DefaultKubeVelaNS, }, } @@ -316,7 +314,7 @@ func renderRawComponent(elem apis.AddonElementFile) (*common2.ApplicationCompone dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) _, _, err := dec.Decode([]byte(elem.Data), nil, obj) if err != nil { - fmt.Println(err) + return nil, err } baseRawComponent.Properties = util.Object2RawExtension(obj) return &baseRawComponent, nil @@ -559,3 +557,23 @@ func readRepo(h *gitHelper) ([]*github.RepositoryContent, error) { } return dirs, nil } + +// ConvertAddonRegistryModel2AddonRegistryMeta will convert from model to AddonRegistryMeta +func ConvertAddonRegistryModel2AddonRegistryMeta(r *model.AddonRegistry) *apis.AddonRegistryMeta { + return &apis.AddonRegistryMeta{ + Name: r.Name, + Git: r.Git, + } +} + +const addonAppPrefix = "addon-" + +// AddonName2AppName - +func AddonName2AppName(name string) string { + return addonAppPrefix + name +} + +// AppName2addonName - +func AppName2addonName(name string) string { + return strings.TrimPrefix(name, addonAppPrefix) +} diff --git a/pkg/apiserver/rest/usecase/definition.go b/pkg/apiserver/rest/usecase/definition.go index 9b1c1707d..bd32c48eb 100644 --- a/pkg/apiserver/rest/usecase/definition.go +++ b/pkg/apiserver/rest/usecase/definition.go @@ -18,20 +18,26 @@ package usecase import ( "context" + "encoding/json" "fmt" + "sort" "time" + "github.com/getkin/kin-openapi/openapi3" v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" - "github.com/getkin/kin-openapi/openapi3" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/log" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) // DefinitionUsecase definition usecase, Implement the management of ComponentDefinition、TraitDefinition and WorkflowStepDefinition. @@ -40,6 +46,8 @@ type DefinitionUsecase interface { ListDefinitions(ctx context.Context, envName, defType string) ([]*apisv1.DefinitionBase, error) // DetailDefinition get definition detail DetailDefinition(ctx context.Context, name, defType string) (*apisv1.DetailDefinitionResponse, error) + // AddDefinitionUISchema add or update custom definition ui schema + AddDefinitionUISchema(ctx context.Context, name, defType, configRaw string) ([]*utils.UIParameter, error) } type definitionUsecaseImpl struct { @@ -82,7 +90,7 @@ func (d *definitionUsecaseImpl) ListDefinitions(ctx context.Context, envName, de return d.listDefinitions(ctx, defs, kindWorkflowStepDefinition) default: - return nil, fmt.Errorf("invalid definition type") + return nil, bcode.ErrDefinitionTypeNotSupport } } @@ -108,23 +116,184 @@ func (d *definitionUsecaseImpl) listDefinitions(ctx context.Context, list *unstr // DetailDefinition get definition detail func (d *definitionUsecaseImpl) DetailDefinition(ctx context.Context, name, defType string) (*apisv1.DetailDefinitionResponse, error) { + if !utils.StringsContain([]string{"component", "trait", "workflowstep"}, defType) { + return nil, bcode.ErrDefinitionTypeNotSupport + } var cm v1.ConfigMap if err := d.kubeClient.Get(ctx, k8stypes.NamespacedName{ Namespace: types.DefaultKubeVelaNS, Name: fmt.Sprintf("%s-schema-%s", defType, name), }, &cm); err != nil { + if apierrors.IsNotFound(err) { + return nil, bcode.ErrDefinitionNoSchema + } return nil, err } - data, ok := cm.Data["openapi-v3-json-schema"] + data, ok := cm.Data[types.OpenapiV3JSONSchema] if !ok { - return nil, fmt.Errorf("failed to get definition schema") + return nil, bcode.ErrDefinitionNoSchema } schema := &openapi3.Schema{} if err := schema.UnmarshalJSON([]byte(data)); err != nil { return nil, err } + // render default ui schema + defaultUISchema := renderDefaultUISchema(schema) + // patch from custom ui schema + customUISchema := d.renderCustomUISchema(ctx, name, defType, defaultUISchema) return &apisv1.DetailDefinitionResponse{ - Schema: schema, + APISchema: schema, + UISchema: customUISchema, }, nil } + +func (d *definitionUsecaseImpl) renderCustomUISchema(ctx context.Context, name, defType string, defaultSchema []*utils.UIParameter) []*utils.UIParameter { + var cm v1.ConfigMap + if err := d.kubeClient.Get(ctx, k8stypes.NamespacedName{ + Namespace: types.DefaultKubeVelaNS, + Name: fmt.Sprintf("%s-uischema-%s", defType, name), + }, &cm); err != nil { + if !apierrors.IsNotFound(err) { + log.Logger.Errorf("find uischema configmap from cluster failure %s", err.Error()) + } + return defaultSchema + } + data, ok := cm.Data[types.UISchema] + if !ok { + return defaultSchema + } + schema := []*utils.UIParameter{} + if err := json.Unmarshal([]byte(data), &schema); err != nil { + log.Logger.Errorf("unmarshal ui schema failure %s", err.Error()) + return defaultSchema + } + return patchSchema(defaultSchema, schema) +} + +// AddDefinitionUISchema add definition custom ui schema config +func (d *definitionUsecaseImpl) AddDefinitionUISchema(ctx context.Context, name, defType, configRaw string) ([]*utils.UIParameter, error) { + var uiParameters []*utils.UIParameter + err := yaml.Unmarshal([]byte(configRaw), &uiParameters) + if err != nil { + log.Logger.Errorf("yaml unmarshal failure %s", err.Error()) + return nil, bcode.ErrInvalidDefinitionUISchema + } + dataBate, err := json.Marshal(uiParameters) + if err != nil { + log.Logger.Errorf("json marshal failure %s", err.Error()) + return nil, bcode.ErrInvalidDefinitionUISchema + } + var cm v1.ConfigMap + if err := d.kubeClient.Get(ctx, k8stypes.NamespacedName{ + Namespace: types.DefaultKubeVelaNS, + Name: fmt.Sprintf("%s-uischema-%s", defType, name), + }, &cm); err != nil { + if apierrors.IsNotFound(err) { + err = d.kubeClient.Create(ctx, &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: types.DefaultKubeVelaNS, + Name: fmt.Sprintf("%s-uischema-%s", defType, name), + }, + Data: map[string]string{ + types.UISchema: string(dataBate), + }, + }) + } + if err != nil { + return nil, err + } + } else { + cm.Data[types.UISchema] = string(dataBate) + err := d.kubeClient.Update(ctx, &cm) + if err != nil { + return nil, err + } + } + return uiParameters, nil +} + +func patchSchema(defaultSchema, customSchema []*utils.UIParameter) []*utils.UIParameter { + var customSchemaMap = make(map[string]*utils.UIParameter, len(customSchema)) + for i, custom := range customSchema { + customSchemaMap[custom.JSONKey] = customSchema[i] + } + for i := range defaultSchema { + dSchema := defaultSchema[i] + if cusSchema, exist := customSchemaMap[dSchema.JSONKey]; exist { + if cusSchema.Description != "" { + dSchema.Description = cusSchema.Description + } + if cusSchema.Label != "" { + dSchema.Label = cusSchema.Label + } + if cusSchema.SubParameterGroupOption != nil { + dSchema.SubParameterGroupOption = cusSchema.SubParameterGroupOption + } + if cusSchema.Validate != nil { + dSchema.Validate = cusSchema.Validate + } + if cusSchema.UIType != "" { + dSchema.UIType = cusSchema.UIType + } + if cusSchema.Disable != nil { + dSchema.Disable = cusSchema.Disable + } + if cusSchema.SubParameters != nil { + dSchema.SubParameters = patchSchema(dSchema.SubParameters, cusSchema.SubParameters) + } + if cusSchema.Sort != 0 { + dSchema.Sort = cusSchema.Sort + } + } + } + sort.Slice(defaultSchema, func(i, j int) bool { + return defaultSchema[i].Sort < defaultSchema[j].Sort + }) + return defaultSchema +} + +func renderDefaultUISchema(apiSchema *openapi3.Schema) []*utils.UIParameter { + if apiSchema == nil { + return nil + } + var params []*utils.UIParameter + for key, property := range apiSchema.Properties { + if property.Value != nil { + param := renderUIParameter(key, utils.FirstUpper(key), property, apiSchema.Required) + params = append(params, param) + } + } + return params +} + +func renderUIParameter(key, label string, property *openapi3.SchemaRef, required []string) *utils.UIParameter { + var parameter utils.UIParameter + subType := "" + if property.Value.Items != nil { + if property.Value.Items.Value != nil { + subType = property.Value.Items.Value.Type + } + parameter.SubParameters = renderDefaultUISchema(property.Value.Items.Value) + } + if property.Value.Properties != nil { + parameter.SubParameters = renderDefaultUISchema(property.Value) + } + parameter.Validate = &utils.Validate{} + parameter.Validate.DefaultValue = property.Value.Default + for _, enum := range property.Value.Enum { + parameter.Validate.Options = append(parameter.Validate.Options, utils.Option{Label: utils.RenderLabel(enum), Value: enum}) + } + parameter.JSONKey = key + parameter.Description = property.Value.Description + parameter.Label = label + parameter.UIType = utils.GetDefaultUIType(property.Value.Type, len(parameter.Validate.Options) != 0, subType) + parameter.Validate.Max = property.Value.Max + parameter.Validate.MaxLength = property.Value.MaxLength + parameter.Validate.Min = property.Value.Min + parameter.Validate.MinLength = property.Value.MinLength + parameter.Validate.Pattern = property.Value.Pattern + parameter.Validate.Required = utils.StringsContain(required, property.Value.Title) + parameter.Sort = 100 + return ¶meter +} diff --git a/pkg/apiserver/rest/usecase/definition_test.go b/pkg/apiserver/rest/usecase/definition_test.go index 4923cabb1..c640ca42b 100644 --- a/pkg/apiserver/rest/usecase/definition_test.go +++ b/pkg/apiserver/rest/usecase/definition_test.go @@ -18,7 +18,10 @@ package usecase import ( "context" + "encoding/json" + "fmt" "io/ioutil" + "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/google/go-cmp/cmp" @@ -29,6 +32,8 @@ import ( "sigs.k8s.io/yaml" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/oam/util" ) @@ -98,7 +103,7 @@ var _ = Describe("Test namespace usecase functions", func() { Namespace: "vela-system", }, Data: map[string]string{ - "openapi-v3-json-schema": `{"properties":{"batchPartition":{"title":"batchPartition","type":"integer"},"volumes":{"description":"Specify volume type, options: pvc, configMap, secret, emptyDir","enum":["pvc","configMap","secret","emptyDir"],"title":"volumes","type":"string"}, "rolloutBatches":{"items":{"properties":{"replicas":{"title":"replicas","type":"integer"}},"required":["replicas"],"type":"object"},"title":"rolloutBatches","type":"array"},"targetRevision":{"title":"targetRevision","type":"string"},"targetSize":{"title":"targetSize","type":"integer"}},"required":["targetRevision","targetSize"],"type":"object"}`, + types.OpenapiV3JSONSchema: `{"properties":{"batchPartition":{"title":"batchPartition","type":"integer"},"volumes": {"description":"Specify volume type, options: pvc, configMap, secret, emptyDir","enum":["pvc","configMap","secret","emptyDir"],"title":"volumes","type":"string"}, "rolloutBatches":{"items":{"properties":{"replicas":{"title":"replicas","type":"integer"}},"required":["replicas"],"type":"object"},"title":"rolloutBatches","type":"array"},"targetRevision":{"title":"targetRevision","type":"string"},"targetSize":{"title":"targetSize","type":"integer"}},"required":["targetRevision","targetSize"],"type":"object"}`, }, } err := k8sClient.Create(context.Background(), cm) @@ -110,6 +115,58 @@ var _ = Describe("Test namespace usecase functions", func() { err = schemaFromCM.UnmarshalJSON([]byte(cm.Data["openapi-v3-json-schema"])) Expect(err).Should(Succeed()) - Expect(schema.Schema).Should(Equal(schemaFromCM)) + Expect(schema.APISchema).Should(Equal(schemaFromCM)) + }) + + It("Test renderDefaultUISchema", func() { + schema := &v1.DetailDefinitionResponse{} + data, err := ioutil.ReadFile("./testdata/api-schema.json") + Expect(err).Should(Succeed()) + err = json.Unmarshal(data, schema) + Expect(err).Should(Succeed()) + Expect(cmp.Diff(len(schema.APISchema.Required), 3)).Should(BeEmpty()) + uiSchema := renderDefaultUISchema(schema.APISchema) + Expect(cmp.Diff(len(uiSchema), 12)).Should(BeEmpty()) + }) + + It("Test patchSchema", func() { + ddr := &v1.DetailDefinitionResponse{} + data, err := ioutil.ReadFile("./testdata/api-schema.json") + Expect(err).Should(Succeed()) + err = json.Unmarshal(data, ddr) + Expect(err).Should(Succeed()) + Expect(cmp.Diff(len(ddr.APISchema.Required), 3)).Should(BeEmpty()) + defaultschema := renderDefaultUISchema(ddr.APISchema) + + customschema := []*utils.UIParameter{} + cdata, err := ioutil.ReadFile("./testdata/ui-custom-schema.yaml") + Expect(err).Should(Succeed()) + err = yaml.Unmarshal(cdata, &customschema) + Expect(err).Should(Succeed()) + + uiSchema := patchSchema(defaultschema, customschema) + for _, schema := range uiSchema { + fmt.Printf("%s=> %d", schema.JSONKey, schema.Sort) + } + Expect(cmp.Diff(len(uiSchema), 12)).Should(BeEmpty()) + Expect(cmp.Diff(uiSchema[3].JSONKey, "readinessProbe")).Should(BeEmpty()) + Expect(cmp.Diff(len(uiSchema[3].SubParameters), 8)).Should(BeEmpty()) + + outdata, err := yaml.Marshal(uiSchema) + Expect(err).Should(Succeed()) + err = ioutil.WriteFile("./testdata/ui-schema.yaml", outdata, 0755) + Expect(err).Should(Succeed()) }) }) + +func TestAddDefinitionUISchema(t *testing.T) { + du := NewDefinitionUsecase() + cdata, err := ioutil.ReadFile("./testdata/ui-custom-schema.yaml") + if err != nil { + t.Fatal(err) + } + _, err = du.AddDefinitionUISchema(context.TODO(), "webservice", "component", string(cdata)) + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/apiserver/rest/usecase/testdata/api-schema.json b/pkg/apiserver/rest/usecase/testdata/api-schema.json new file mode 100644 index 000000000..0ba443081 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/api-schema.json @@ -0,0 +1,386 @@ +{ + "schema": { + "properties": { + "addRevisionLabel": { + "type": "boolean", + "default": false, + "description": "If addRevisionLabel is true, the appRevision label will be added to the underlying pods", + "title": "addRevisionLabel" + }, + "cmd": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Commands to run in the container", + "title": "cmd" + }, + "cpu": { + "type": "string", + "description": "Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)", + "title": "cpu" + }, + "env": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "description": "Environment variable name", + "title": "name" + }, + "value": { + "type": "string", + "description": "The value of the environment variable", + "title": "value" + }, + "valueFrom": { + "properties": { + "secretKeyRef": { + "properties": { + "key": { + "type": "string", + "description": "The key of the secret to select from. Must be a valid secret key", + "title": "key" + }, + "name": { + "type": "string", + "description": "The name of the secret in the pod's namespace to select from", + "title": "name" + } + }, + "required": [ + "name", + "key" + ], + "type": "object", + "description": "Selects a key of a secret in the pod's namespace", + "title": "secretKeyRef" + } + }, + "required": [ + "secretKeyRef" + ], + "type": "object", + "description": "Specifies a source the value of this var should come from", + "title": "valueFrom" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "description": "Define arguments by using environment variables", + "title": "env" + }, + "image": { + "type": "string", + "description": "Which image would you like to use for your service", + "title": "image" + }, + "imagePullPolicy": { + "type": "string", + "description": "Specify image pull policy for your service", + "title": "imagePullPolicy" + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Specify image pull secrets for your service", + "title": "imagePullSecrets" + }, + "livenessProbe": { + "properties": { + "exec": { + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", + "title": "command" + } + }, + "required": [ + "command" + ], + "type": "object", + "description": "Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.", + "title": "exec" + }, + "failureThreshold": { + "type": "integer", + "default": 3, + "description": "Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).", + "title": "failureThreshold" + }, + "httpGet": { + "properties": { + "httpHeaders": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "title": "name" + }, + "value": { + "type": "string", + "title": "value" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "title": "httpHeaders" + }, + "path": { + "type": "string", + "description": "The endpoint, relative to the port, to which the HTTP GET request should be directed.", + "title": "path" + }, + "port": { + "type": "integer", + "description": "The TCP socket within the container to which the HTTP GET request should be directed.", + "title": "port" + } + }, + "required": [ + "path", + "port" + ], + "type": "object", + "description": "Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.", + "title": "httpGet" + }, + "initialDelaySeconds": { + "type": "integer", + "default": 0, + "description": "Number of seconds after the container is started before the first probe is initiated.", + "title": "initialDelaySeconds" + }, + "periodSeconds": { + "type": "integer", + "default": 10, + "description": "How often, in seconds, to execute the probe.", + "title": "periodSeconds" + }, + "successThreshold": { + "type": "integer", + "default": 1, + "description": "Minimum consecutive successes for the probe to be considered successful after having failed.", + "title": "successThreshold" + }, + "tcpSocket": { + "properties": { + "port": { + "type": "integer", + "description": "The TCP socket within the container that should be probed to assess container health.", + "title": "port" + } + }, + "required": [ + "port" + ], + "type": "object", + "description": "Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.", + "title": "tcpSocket" + }, + "timeoutSeconds": { + "type": "integer", + "default": 1, + "description": "Number of seconds after which the probe times out.", + "title": "timeoutSeconds" + } + }, + "required": [ + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "successThreshold", + "failureThreshold" + ], + "type": "object", + "description": "Instructions for assessing whether the container is alive.", + "title": "livenessProbe" + }, + "memory": { + "type": "string", + "description": "Specifies the attributes of the memory resource required for the container.", + "title": "memory" + }, + "port": { + "type": "integer", + "default": 80, + "description": "Which port do you want customer traffic sent to", + "title": "port" + }, + "readinessProbe": { + "properties": { + "exec": { + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.", + "title": "command" + } + }, + "required": [ + "command" + ], + "type": "object", + "description": "Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.", + "title": "exec" + }, + "failureThreshold": { + "type": "integer", + "default": 3, + "description": "Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).", + "title": "failureThreshold" + }, + "httpGet": { + "properties": { + "httpHeaders": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "title": "name" + }, + "value": { + "type": "string", + "title": "value" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "title": "httpHeaders" + }, + "path": { + "type": "string", + "description": "The endpoint, relative to the port, to which the HTTP GET request should be directed.", + "title": "path" + }, + "port": { + "type": "integer", + "description": "The TCP socket within the container to which the HTTP GET request should be directed.", + "title": "port" + } + }, + "required": [ + "path", + "port" + ], + "type": "object", + "description": "Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.", + "title": "httpGet" + }, + "initialDelaySeconds": { + "type": "integer", + "default": 0, + "description": "Number of seconds after the container is started before the first probe is initiated.", + "title": "initialDelaySeconds" + }, + "periodSeconds": { + "type": "integer", + "default": 10, + "description": "How often, in seconds, to execute the probe.", + "title": "periodSeconds" + }, + "successThreshold": { + "type": "integer", + "default": 1, + "description": "Minimum consecutive successes for the probe to be considered successful after having failed.", + "title": "successThreshold" + }, + "tcpSocket": { + "properties": { + "port": { + "type": "integer", + "description": "The TCP socket within the container that should be probed to assess container health.", + "title": "port" + } + }, + "required": [ + "port" + ], + "type": "object", + "description": "Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.", + "title": "tcpSocket" + }, + "timeoutSeconds": { + "type": "integer", + "default": 1, + "description": "Number of seconds after which the probe times out.", + "title": "timeoutSeconds" + } + }, + "required": [ + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "successThreshold", + "failureThreshold" + ], + "type": "object", + "description": "Instructions for assessing whether the container is in a suitable state to serve traffic.", + "title": "readinessProbe" + }, + "volumes": { + "type": "array", + "items": { + "properties": { + "mountPath": { + "type": "string", + "title": "mountPath" + }, + "name": { + "type": "string", + "title": "name" + }, + "type": { + "type": "string", + "enum": [ + "pvc", + "configMap", + "secret", + "emptyDir" + ], + "description": "Specify volume type, options: \"pvc\",\"configMap\",\"secret\",\"emptyDir\"", + "title": "type" + } + }, + "required": [ + "name", + "mountPath", + "type" + ], + "type": "object" + }, + "description": "Declare volumes and volumeMounts", + "title": "volumes" + } + }, + "required": [ + "addRevisionLabel", + "image", + "port" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml new file mode 100755 index 000000000..cc65ecbdf --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml @@ -0,0 +1,56 @@ +- description: Specify image pull policy for your service + disable: false + jsonKey: imagePullPolicy + label: 镜像更新策略 + uiType: Select + validate: + options: + - label: 镜像不存在时更新 + value: IfNotPresent + - label: 总是更新 + value: Always + - label: 永不更新 + value: Never + sort: 2 +- description: Specifies the attributes of the memory resource required for the container. + disable: false + jsonKey: memory + label: Memory + uiType: MemoryNumber + sort: 3 +- uiType: CPUNumber + jsonKey: cpu +- description: Define arguments by using environment variables + disable: false + jsonKey: env + label: Env + subParameterGroupOption: + - - name + - value + - - name + - valueFrom + subParameters: + - description: Specifies a source the value of this var should come from + disable: false + jsonKey: valueFrom + label: Secret选择器 + uiType: InnerGroup + subParameters: + - jsonKey: secretKeyRef + uiType: Ignore + subParameters: + - jsonKey: name + label: Secret选择 + uiType: SecretSelect + - jsonKey: key + label: SecretKey选择 + uiType: SecretKeySelect + uiType: Structs + validate: {} +- uiType: ImageInput + jsonKey: image + sort: 1 +- jsonKey: readinessProbe + uiType: Group + label: ReadinessProbe检测 + sort: 4 \ No newline at end of file diff --git a/pkg/apiserver/rest/usecase/testdata/ui-default-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-default-schema.yaml new file mode 100755 index 000000000..ef7d72dd8 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/ui-default-schema.yaml @@ -0,0 +1,132 @@ +- description: Which image would you like to use for your service + jsonKey: image + label: Image + uiType: Input + validete: + required: true +- description: Specify image pull policy for your service + jsonKey: imagePullPolicy + label: ImagePullPolicy + uiType: Input + validete: {} +- description: Instructions for assessing whether the container is alive. + jsonKey: livenessProbe + label: LivenessProbe + uiType: KV + validete: {} +- description: If addRevisionLabel is true, the appRevision label will be added to + the underlying pods + jsonKey: addRevisionLabel + label: AddRevisionLabel + uiType: Switch + validete: + defaultValue: false + required: true +- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` + (1 CPU core) + jsonKey: cpu + label: Cpu + uiType: Input + validete: {} +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + uiType: Structs + validete: {} +- description: Specifies the attributes of the memory resource required for the container. + jsonKey: memory + label: Memory + uiType: Input + validete: {} +- description: Which port do you want customer traffic sent to + jsonKey: port + label: Port + uiType: Number + validete: + defaultValue: 80 + required: true +- description: Instructions for assessing whether the container is in a suitable state + to serve traffic. + jsonKey: readinessProbe + label: ReadinessProbe + uiType: KV + validete: {} +- description: Declare volumes and volumeMounts + jsonKey: volumes + label: Volumes + subParameters: + - description: "" + jsonKey: volumes.mountPath + label: MountPath + uiType: Input + validete: + required: true + - description: "" + jsonKey: volumes.name + label: Name + uiType: Input + validete: + required: true + - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' + jsonKey: volumes.type + label: Type + uiType: Select + validete: + options: + - label: Pvc + value: pvc + - label: ConfigMap + value: configMap + - label: Secret + value: secret + - label: EmptyDir + value: emptyDir + required: true + uiType: Structs + validete: {} +- description: Commands to run in the container + jsonKey: cmd + label: Cmd + uiType: Structs + validete: {} +- description: Define arguments by using environment variables + jsonKey: env + label: Env + subParameters: + - description: The value of the environment variable + jsonKey: env.value + label: Value + uiType: Input + validete: {} + - description: Specifies a source the value of this var should come from + jsonKey: env.valueFrom + label: ValueFrom + subParameters: + - description: "" + jsonKey: env.valueFrom.secretKeyRef + label: SecretKeyRef + subParameters: + - description: secret name + jsonKey: env.valueFrom.secretKeyRef.name + label: Name + uiType: Input + validete: + required: true + - description: secret key + jsonKey: env.valueFrom.secretKeyRef.key + label: Key + uiType: Input + validete: + required: true + uiType: KV + validete: {} + uiType: KV + validete: {} + - description: Environment variable name + jsonKey: env.name + label: Name + uiType: Input + validete: + required: true + uiType: Structs + validete: {} diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml new file mode 100755 index 000000000..d026df893 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -0,0 +1,428 @@ +- description: Instructions for assessing whether the container is in a suitable state + to serve traffic. + jsonKey: readinessProbe + label: ReadinessProbe检测 + sort: 1 + subParameters: + - description: Number of seconds after the container is started before the first + probe is initiated. + jsonKey: readinessProbe.initialDelaySeconds + label: InitialDelaySeconds + sort: 100 + uiType: Number + validete: + defaultValue: 0 + required: true + - description: How often, in seconds, to execute the probe. + jsonKey: readinessProbe.periodSeconds + label: PeriodSeconds + sort: 100 + uiType: Number + validete: + defaultValue: 10 + required: true + - description: Minimum consecutive successes for the probe to be considered successful + after having failed. + jsonKey: readinessProbe.successThreshold + label: SuccessThreshold + sort: 100 + uiType: Number + validete: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: readinessProbe.tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: readinessProbe.tcpSocket.port + label: Port + sort: 100 + uiType: Number + validete: + required: true + uiType: KV + validete: {} + - description: Number of seconds after which the probe times out. + jsonKey: readinessProbe.timeoutSeconds + label: TimeoutSeconds + sort: 100 + uiType: Number + validete: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by executing a command. + Either this attribute or the httpGet attribute or the tcpSocket attribute MUST + be specified. This attribute is mutually exclusive with both the httpGet attribute + and the tcpSocket attribute. + jsonKey: readinessProbe.exec + label: Exec + sort: 100 + subParameters: + - description: A command to be executed inside the container to assess its health. + Each space delimited token of the command is a separate array element. Commands + exiting 0 are considered to be successful probes, whilst all other exit codes + are considered failures. + jsonKey: readinessProbe.exec.command + label: Command + sort: 100 + uiType: Strings + validete: + required: true + uiType: KV + validete: {} + - description: Number of consecutive failures required to determine the container + is not alive (liveness probe) or not ready (readiness probe). + jsonKey: readinessProbe.failureThreshold + label: FailureThreshold + sort: 100 + uiType: Number + validete: + defaultValue: 3 + required: true + - description: Instructions for assessing container health by executing an HTTP + GET request. Either this attribute or the exec attribute or the tcpSocket attribute + MUST be specified. This attribute is mutually exclusive with both the exec attribute + and the tcpSocket attribute. + jsonKey: readinessProbe.httpGet + label: HttpGet + sort: 100 + subParameters: + - description: "" + jsonKey: readinessProbe.httpGet.httpHeaders + label: HttpHeaders + sort: 100 + subParameters: + - description: "" + jsonKey: readinessProbe.httpGet.httpHeaders.[].name + label: Name + sort: 100 + uiType: Input + validete: + required: true + - description: "" + jsonKey: readinessProbe.httpGet.httpHeaders.[].value + label: Value + sort: 100 + uiType: Input + validete: + required: true + uiType: Structs + validete: {} + - description: The endpoint, relative to the port, to which the HTTP GET request + should be directed. + jsonKey: readinessProbe.httpGet.path + label: Path + sort: 100 + uiType: Input + validete: + required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: readinessProbe.httpGet.port + label: Port + sort: 100 + uiType: Number + validete: + required: true + uiType: KV + validete: {} + uiType: Group + validete: {} +- description: Which image would you like to use for your service + jsonKey: image + label: Image + sort: 2 + uiType: ImageInput + validete: + required: true +- description: Specify image pull policy for your service + disable: false + jsonKey: imagePullPolicy + label: 镜像更新策略 + sort: 2 + uiType: Select + validete: + options: + - label: 镜像不存在时更新 + value: IfNotPresent + - label: 总是更新 + value: Always + - label: 永不更新 + value: Never +- description: Specifies the attributes of the memory resource required for the container. + disable: false + jsonKey: memory + label: Memory + sort: 3 + uiType: MemoryNumber + validete: {} +- description: Commands to run in the container + jsonKey: cmd + label: Cmd + sort: 100 + uiType: Strings + validete: {} +- description: Which port do you want customer traffic sent to + jsonKey: port + label: Port + sort: 100 + uiType: Number + validete: + defaultValue: 80 + required: true +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + sort: 100 + uiType: Strings + validete: {} +- description: Instructions for assessing whether the container is alive. + jsonKey: livenessProbe + label: LivenessProbe + sort: 100 + subParameters: + - description: Number of seconds after which the probe times out. + jsonKey: livenessProbe.timeoutSeconds + label: TimeoutSeconds + sort: 100 + uiType: Number + validete: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by executing a command. + Either this attribute or the httpGet attribute or the tcpSocket attribute MUST + be specified. This attribute is mutually exclusive with both the httpGet attribute + and the tcpSocket attribute. + jsonKey: livenessProbe.exec + label: Exec + sort: 100 + subParameters: + - description: A command to be executed inside the container to assess its health. + Each space delimited token of the command is a separate array element. Commands + exiting 0 are considered to be successful probes, whilst all other exit codes + are considered failures. + jsonKey: livenessProbe.exec.command + label: Command + sort: 100 + uiType: Strings + validete: + required: true + uiType: KV + validete: {} + - description: Number of consecutive failures required to determine the container + is not alive (liveness probe) or not ready (readiness probe). + jsonKey: livenessProbe.failureThreshold + label: FailureThreshold + sort: 100 + uiType: Number + validete: + defaultValue: 3 + required: true + - description: Instructions for assessing container health by executing an HTTP + GET request. Either this attribute or the exec attribute or the tcpSocket attribute + MUST be specified. This attribute is mutually exclusive with both the exec attribute + and the tcpSocket attribute. + jsonKey: livenessProbe.httpGet + label: HttpGet + sort: 100 + subParameters: + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: livenessProbe.httpGet.port + label: Port + sort: 100 + uiType: Number + validete: + required: true + - description: "" + jsonKey: livenessProbe.httpGet.httpHeaders + label: HttpHeaders + sort: 100 + subParameters: + - description: "" + jsonKey: livenessProbe.httpGet.httpHeaders.[].name + label: Name + sort: 100 + uiType: Input + validete: + required: true + - description: "" + jsonKey: livenessProbe.httpGet.httpHeaders.[].value + label: Value + sort: 100 + uiType: Input + validete: + required: true + uiType: Structs + validete: {} + - description: The endpoint, relative to the port, to which the HTTP GET request + should be directed. + jsonKey: livenessProbe.httpGet.path + label: Path + sort: 100 + uiType: Input + validete: + required: true + uiType: KV + validete: {} + - description: Number of seconds after the container is started before the first + probe is initiated. + jsonKey: livenessProbe.initialDelaySeconds + label: InitialDelaySeconds + sort: 100 + uiType: Number + validete: + defaultValue: 0 + required: true + - description: How often, in seconds, to execute the probe. + jsonKey: livenessProbe.periodSeconds + label: PeriodSeconds + sort: 100 + uiType: Number + validete: + defaultValue: 10 + required: true + - description: Minimum consecutive successes for the probe to be considered successful + after having failed. + jsonKey: livenessProbe.successThreshold + label: SuccessThreshold + sort: 100 + uiType: Number + validete: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: livenessProbe.tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: livenessProbe.tcpSocket.port + label: Port + sort: 100 + uiType: Number + validete: + required: true + uiType: KV + validete: {} + uiType: KV + validete: {} +- description: Declare volumes and volumeMounts + jsonKey: volumes + label: Volumes + sort: 100 + subParameters: + - description: "" + jsonKey: volumes.[].mountPath + label: MountPath + sort: 100 + uiType: Input + validete: + required: true + - description: "" + jsonKey: volumes.[].name + label: Name + sort: 100 + uiType: Input + validete: + required: true + - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' + jsonKey: volumes.[].type + label: Type + sort: 100 + uiType: Select + validete: + options: + - label: Pvc + value: pvc + - label: ConfigMap + value: configMap + - label: Secret + value: secret + - label: EmptyDir + value: emptyDir + required: true + uiType: Structs + validete: {} +- description: If addRevisionLabel is true, the appRevision label will be added to + the underlying pods + jsonKey: addRevisionLabel + label: AddRevisionLabel + sort: 100 + uiType: Switch + validete: + defaultValue: false + required: true +- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` + (1 CPU core) + jsonKey: cpu + label: Cpu + sort: 100 + uiType: CPUNumber + validete: {} +- description: Define arguments by using environment variables + disable: false + jsonKey: env + label: Env + sort: 100 + subParameterGroupOption: + - - env.name + - env.value + - - env.name + - env.valueFrom + subParameters: + - description: The value of the environment variable + jsonKey: env.[].value + label: Value + sort: 100 + uiType: Input + validete: {} + - description: Specifies a source the value of this var should come from + jsonKey: env.[].valueFrom + label: ValueFrom + sort: 100 + subParameters: + - description: Selects a key of a secret in the pod's namespace + jsonKey: env.[].valueFrom.secretKeyRef + label: SecretKeyRef + sort: 100 + subParameters: + - description: The key of the secret to select from. Must be a valid secret + key + jsonKey: env.[].valueFrom.secretKeyRef.key + label: Key + sort: 100 + uiType: Input + validete: + required: true + - description: The name of the secret in the pod's namespace to select from + jsonKey: env.[].valueFrom.secretKeyRef.name + label: Name + sort: 100 + uiType: Input + validete: + required: true + uiType: KV + validete: + required: true + uiType: KV + validete: {} + - description: Environment variable name + jsonKey: env.[].name + label: Name + sort: 100 + uiType: Input + validete: + required: true + uiType: Structs + validete: {} diff --git a/pkg/apiserver/rest/utils/bcode/definition.go b/pkg/apiserver/rest/utils/bcode/definition.go new file mode 100644 index 000000000..3a89f6b5f --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/definition.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 bcode + +// ErrDefinitionNotFound definition is not exist +var ErrDefinitionNotFound = NewBcode(404, 70001, "definition is not exist") + +// ErrDefinitionNoSchema definition not have schema +var ErrDefinitionNoSchema = NewBcode(400, 70002, "definition not have schema") + +// ErrDefinitionTypeNotSupport definition type not support +var ErrDefinitionTypeNotSupport = NewBcode(400, 70003, "definition type not support") + +// ErrInvalidDefinitionUISchema invalid custom definition ui schema +var ErrInvalidDefinitionUISchema = NewBcode(400, 70004, "invalid custom defnition ui schema") diff --git a/pkg/apiserver/rest/utils/convert.go b/pkg/apiserver/rest/utils/convert.go deleted file mode 100644 index 09c3f2b64..000000000 --- a/pkg/apiserver/rest/utils/convert.go +++ /dev/null @@ -1,24 +0,0 @@ -package utils - -import ( - "github.com/oam-dev/kubevela/pkg/apiserver/model" - apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" - "strings" -) - -// ConvertAddonRegistryModel2AddonRegistryMeta will convert from model to AddonRegistryMeta -func ConvertAddonRegistryModel2AddonRegistryMeta(r *model.AddonRegistry) *apisv1.AddonRegistryMeta { - return &apisv1.AddonRegistryMeta{ - Name: r.Name, - Git: r.Git, - } -} - -const addonAppPrefix = "addon-" - -func AddonName2AppName(name string) string { - return addonAppPrefix + name -} -func AppName2addonName(name string) string { - return strings.TrimPrefix(name, addonAppPrefix) -} diff --git a/pkg/apiserver/rest/utils/uiswagger.go b/pkg/apiserver/rest/utils/uiswagger.go index fca084eaf..b58b5622d 100644 --- a/pkg/apiserver/rest/utils/uiswagger.go +++ b/pkg/apiserver/rest/utils/uiswagger.go @@ -16,32 +16,41 @@ limitations under the License. package utils +import ( + "fmt" + "strings" +) + // UIParameter Structured import table simple UI model type UIParameter struct { + Sort uint `json:"sort"` Label string `json:"label"` Description string `json:"description"` - Validate *Validate `json:"validete,omitempty"` + Validate *Validate `json:"validate,omitempty"` JSONKey string `json:"jsonKey"` UIType string `json:"uiType"` // means only can be read. - Disable bool `json:"disable"` - SubParameters []*UIParameter `json:"subParameters,omitempty"` + Disable *bool `json:"disable,omitempty"` + SubParameterGroupOption [][]string `json:"subParameterGroupOption,omitempty"` + SubParameters []*UIParameter `json:"subParameters,omitempty"` } // Validate parameter validate rule type Validate struct { Required bool `json:"required,omitempty"` - Max int `json:"max,omitempty"` - Min int `json:"min,omitempty"` - Regular string `json:"regular,omitempty"` - Options []*Options `json:"options,omitempty"` + Max *float64 `json:"max,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty"` + Min *float64 `json:"min,omitempty"` + MinLength uint64 `json:"minLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + Options []Option `json:"options,omitempty"` DefaultValue interface{} `json:"defaultValue,omitempty"` } -// Options select option -type Options struct { - Label string `json:"label"` - Value string `json:"value"` +// Option select option +type Option struct { + Label string `json:"label"` + Value interface{} `json:"value"` } // ParseUIParameterFromDefinition cue of parameter in Definitions was analyzed to obtain the form description model. @@ -50,3 +59,68 @@ func ParseUIParameterFromDefinition(definition []byte) ([]*UIParameter, error) { return params, nil } + +// FirstUpper Sets the first letter of the string to upper. +func FirstUpper(s string) string { + if s == "" { + return "" + } + return strings.ToUpper(s[:1]) + s[1:] +} + +// FirstLower Sets the first letter of the string to lowercase. +func FirstLower(s string) string { + if s == "" { + return "" + } + return strings.ToLower(s[:1]) + s[1:] +} + +// GetDefaultUIType Set the default mapping for API Schema Type +func GetDefaultUIType(apiType string, haveOptions bool, subType string) string { + switch apiType { + case "string": + if haveOptions { + return "Select" + } + return "Input" + case "number", "integer": + return "Number" + case "boolean": + return "Switch" + case "array": + if subType == "string" { + return "Strings" + } + if subType == "number" || subType == "integer" { + return "Numbers" + } + return "Structs" + case "object": + return "KV" + default: + return "Input" + } +} + +// RenderLabel render option label +func RenderLabel(source interface{}) string { + switch v := source.(type) { + case int: + return fmt.Sprintf("%d", v) + case string: + return FirstUpper(v) + default: + return FirstUpper(fmt.Sprintf("%v", v)) + } +} + +// StringsContain strings contain +func StringsContain(items []string, source string) bool { + for _, item := range items { + if item == source { + return true + } + } + return false +} diff --git a/pkg/apiserver/rest/webservice/addon_registry.go b/pkg/apiserver/rest/webservice/addon_registry.go index 1ba902dc7..2b44d9690 100644 --- a/pkg/apiserver/rest/webservice/addon_registry.go +++ b/pkg/apiserver/rest/webservice/addon_registry.go @@ -22,7 +22,6 @@ import ( 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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) @@ -112,7 +111,7 @@ func (s *addonRegistryWebService) deleteAddonRegistry(req *restful.Request, res return } - if err := res.WriteEntity(*utils.ConvertAddonRegistryModel2AddonRegistryMeta(r)); err != nil { + if err := res.WriteEntity(*usecase.ConvertAddonRegistryModel2AddonRegistryMeta(r)); err != nil { bcode.ReturnError(req, res, err) return } diff --git a/pkg/apiserver/rest/webservice/applicationplan.go b/pkg/apiserver/rest/webservice/applicationplan.go index f7e4268d9..709faca8c 100644 --- a/pkg/apiserver/rest/webservice/applicationplan.go +++ b/pkg/apiserver/rest/webservice/applicationplan.go @@ -47,7 +47,7 @@ func (c *applicationPlanWebService) GetWebService() *restful.WebService { Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for application manage") - tags := []string{"application"} + tags := []string{"applicationplan"} ws.Route(ws.GET("/").To(c.listApplicationPlans). Doc("list all application plans"). diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index b3f2e2648..42dc3f3db 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -76,7 +76,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { Doc("modify cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). - Reads(&apis.CreateClusterRequest{}). + Reads(apis.CreateClusterRequest{}). Returns(200, "", apis.ClusterBase{}). Returns(400, "", bcode.Bcode{}). Writes(apis.ClusterBase{})) @@ -95,7 +95,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). Param(ws.QueryParameter("page", "Page for paging").DataType("int").DefaultValue("0")). Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("int").DefaultValue("20")). - Reads(&apis.AccessKeyRequest{}). + Reads(apis.AccessKeyRequest{}). Returns(200, "", apis.ListCloudClusterResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.ListCloudClusterResponse{})) @@ -104,7 +104,7 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { Doc("create cluster from cloud cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). - Reads(&apis.ConnectCloudClusterRequest{}). + Reads(apis.ConnectCloudClusterRequest{}). Returns(200, "", apis.ClusterBase{}). Returns(400, "", bcode.Bcode{}). Writes(apis.ClusterBase{})) @@ -112,8 +112,8 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/cloud-clusters/{provider}/create").To(c.createCloudCluster). Doc("create cloud cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). - Reads(&apis.CreateCloudClusterRequest{}). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string").Required(true)). + Reads(apis.CreateCloudClusterRequest{}). Returns(200, "", apis.CreateCloudClusterResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.CreateCloudClusterResponse{})) diff --git a/pkg/apiserver/rest/webservice/definition.go b/pkg/apiserver/rest/webservice/definition.go index 8eee097fa..d5e508045 100644 --- a/pkg/apiserver/rest/webservice/definition.go +++ b/pkg/apiserver/rest/webservice/definition.go @@ -41,7 +41,7 @@ func (d *definitionWebservice) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(d.listDefinitions). Doc("list all definitions"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("type", "query the definition type").DataType("string")). + Param(ws.QueryParameter("type", "query the definition type").DataType("string").Required(true).AllowableValues(map[string]string{"component": "", "trait": "", "workflowstep": ""})). Param(ws.QueryParameter("envName", "if specified, query the definition supported by the env.").DataType("string")). Returns(200, "", apis.ListDefinitionResponse{}). Writes(apis.ListDefinitionResponse{}).Do(returns200, returns500)) diff --git a/pkg/apiserver/rest/webservice/namespace.go b/pkg/apiserver/rest/webservice/namespace.go index 46a5ccf2b..013d0d2a1 100644 --- a/pkg/apiserver/rest/webservice/namespace.go +++ b/pkg/apiserver/rest/webservice/namespace.go @@ -47,12 +47,14 @@ func (n *namespaceWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(n.listNamespaces). Doc("list all namespaces"). Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListNamespaceResponse{}). Writes(apis.ListNamespaceResponse{})) ws.Route(ws.POST("/").To(n.createNamespace). Doc("create namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateNamespaceRequest{}). + Returns(200, "", apis.NamespaceDetailResponse{}). Writes(apis.NamespaceDetailResponse{})) return ws } diff --git a/pkg/apiserver/rest/webservice/oam_application.go b/pkg/apiserver/rest/webservice/oam_application.go index 55725ae3a..c842868de 100644 --- a/pkg/apiserver/rest/webservice/oam_application.go +++ b/pkg/apiserver/rest/webservice/oam_application.go @@ -44,13 +44,14 @@ func (c *oamApplicationWebService) GetWebService() *restful.WebService { Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for oam application manage") - tags := []string{"oam"} + tags := []string{"oam-application"} ws.Route(ws.GET("/namespaces/{namespace}/applications/{appname}").To(c.getApplication). Doc("get the specified oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). + Returns(200, "", apis.ApplicationResponse{}). Writes(apis.ApplicationResponse{})) ws.Route(ws.POST("/namespaces/{namespace}/applications/{appname}").To(c.createOrUpdateApplication). diff --git a/pkg/apiserver/rest/webservice/policy_definition.go b/pkg/apiserver/rest/webservice/policy_definition.go index 01f54ca17..e389abc03 100644 --- a/pkg/apiserver/rest/webservice/policy_definition.go +++ b/pkg/apiserver/rest/webservice/policy_definition.go @@ -33,11 +33,12 @@ func (c *policyDefinitionWebservice) GetWebService() *restful.WebService { Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for policydefinition manage") - tags := []string{"policydefinition"} + tags := []string{"definition"} ws.Route(ws.GET("/").To(noop). Doc("list all policydefinition"). Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListPolicyDefinitionResponse{}). Writes(apis.ListPolicyDefinitionResponse{})) return ws } diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index f7c93553e..1f06da88f 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -17,7 +17,6 @@ limitations under the License. package webservice import ( - "context" "net/http" "github.com/emicklei/go-restful/v3" @@ -58,7 +57,7 @@ func returns500(b *restful.RouteBuilder) { // Init init all webservice, pass in the required parameter object. // It can be implemented using the idea of dependency injection. -func Init(ctx context.Context, ds datastore.DataStore) { +func Init(ds datastore.DataStore) { clusterUsecase := usecase.NewClusterUsecase(ds) workflowUsecase := usecase.NewWorkflowUsecase(ds) applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase) diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 4ee1cf41e..e831e956a 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -51,13 +51,14 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for cluster manage") - tags := []string{"cluster"} + tags := []string{"workflowplan"} ws.Route(ws.GET("/").To(w.listApplicationWorkflows). Doc("list application workflow"). - Param(ws.QueryParameter("appName", "identifier of the application.").DataType("string")). + Param(ws.QueryParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.QueryParameter("enable", "query based on enable status").DataType("boolean")). Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListWorkflowPlanResponse{}). Writes(apis.ListWorkflowPlanResponse{}).Do(returns200, returns500)) ws.Route(ws.POST("/").To(w.createApplicationWorkflow). @@ -82,6 +83,7 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Filter(w.workflowCheckFilter). Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). Reads(apis.UpdateWorkflowPlanRequest{}). + Returns(200, "", apis.DetailWorkflowPlanResponse{}). Writes(apis.DetailWorkflowPlanResponse{}).Do(returns200, returns500)) ws.Route(ws.DELETE("/{name}").To(w.deleteWorkflow). @@ -89,6 +91,7 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Metadata(restfulspec.KeyOpenAPITags, tags). Filter(w.workflowCheckFilter). Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). Writes(apis.EmptyResponse{}).Do(returns200, returns500)) ws.Route(ws.GET("/{name}/records").To(w.listWorkflowRecords). @@ -98,6 +101,7 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Filter(w.workflowCheckFilter). Param(ws.PathParameter("page", "Query the page number.").DataType("integer")). Param(ws.PathParameter("pageSize", "Query the page size number.").DataType("integer")). + Returns(200, "", apis.ListWorkflowRecordsResponse{}). Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) ws.Route(ws.GET("/{name}/records/{record}").To(w.detailWorkflowRecord). @@ -105,6 +109,7 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.DetailWorkflowRecordResponse{}). Writes(apis.DetailWorkflowRecordResponse{}).Do(returns200, returns500)) return ws diff --git a/test/e2e-test/helm_app_test.go b/test/e2e-test/helm_app_test.go index 527bf87ff..3792f3df9 100644 --- a/test/e2e-test/helm_app_test.go +++ b/test/e2e-test/helm_app_test.go @@ -378,7 +378,7 @@ var _ = Describe("Test application containing helm module", func() { if err := k8sClient.Get(ctx, client.ObjectKey{Name: cmName, Namespace: namespace}, cm); err != nil { return err } - if cm.Data["openapi-v3-json-schema"] == "" { + if cm.Data[types.OpenapiV3JSONSchema] == "" { return errors.New("json schema is not found in the ConfigMap") } return nil diff --git a/test/e2e-test/kube_app_test.go b/test/e2e-test/kube_app_test.go index adf115ece..4c18a22ab 100644 --- a/test/e2e-test/kube_app_test.go +++ b/test/e2e-test/kube_app_test.go @@ -32,6 +32,7 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/oam/util" . "github.com/onsi/ginkgo" @@ -347,7 +348,7 @@ spec: if err := k8sClient.Get(ctx, client.ObjectKey{Name: cmName, Namespace: namespace}, cm); err != nil { return err } - if cm.Data["openapi-v3-json-schema"] == "" { + if cm.Data[types.OpenapiV3JSONSchema] == "" { return errors.New("json schema is not found in the ConfigMap") } return nil From b98cec127ee206dbf34e5e76834ab5dabf91aade Mon Sep 17 00:00:00 2001 From: qiaozp <47812250+chivalryq@users.noreply.github.com> Date: Mon, 8 Nov 2021 16:34:03 +0800 Subject: [PATCH 22/59] add addon template reading (#2648) render definition into application new format fix cue render fix one bad registry make all addon unusable --- pkg/apiserver/rest/apis/v1/types.go | 22 ++-- pkg/apiserver/rest/usecase/addon.go | 182 +++++++++++++++++----------- 2 files changed, 118 insertions(+), 86 deletions(-) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index f14a4acb7..5bd90ecf5 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -22,6 +22,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/model" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" @@ -103,13 +104,6 @@ type AddonMeta struct { Dependencies []*AddonDependency `json:"dependencies,omitempty"` } -// Definition defines the metadata for a single X-Definition -type Definition struct { - Kind string `json:"kind"` - Name string `json:"name"` - Description string `json:"description"` -} - // AddonElementFile can be addon's definition or addon's component type AddonElementFile struct { Data string @@ -122,14 +116,12 @@ type DetailAddonResponse struct { AddonMeta // More details about the addon, e.g. README - Detail string `json:"detail,omitempty"` - - Definitions []*Definition `json:"definitions"` - - Parameters string `json:"parameters"` - - CUETemplates []AddonElementFile `json:"cue_templates"` - YAMLTemplates []AddonElementFile `json:"yaml_templates,omitempty"` + Detail string `json:"detail,omitempty"` + Definitions []AddonElementFile `json:"definitions"` + Parameters string `json:"parameters"` + CUETemplates []AddonElementFile `json:"cue_templates"` + YAMLTemplates []AddonElementFile `json:"yaml_templates,omitempty"` + AppTemplate *v1beta1.Application `json:"app_template"` } // AddonStatusResponse defines the format of addon status response diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index d12682a11..5127b1502 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -47,8 +47,11 @@ const ( // AddonMetadataFileName is the addon meatadata.yaml file name AddonMetadataFileName string = "metadata.yaml" - // AddonTemplateDirName is the addon template/ dir name - AddonTemplateDirName string = "template" + // AddonTemplateFileName is the addon template.yaml dir name + AddonTemplateFileName string = "template.yaml" + + // AddonResourcesDirName is the addon resources/ dir name + AddonResourcesDirName string = "resources" // AddonDefinitionsDirName is the addon definitions/ dir name AddonDefinitionsDirName string = "definitions" @@ -143,7 +146,8 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, regist } gitAddons, err := getAddonsFromGit(r.Git.URL, r.Git.Path, r.Git.Token, detailed) if err != nil { - return nil, err + log.Logger.Errorf("fail to get addons from registry %s", r.Name) + continue } addons = mergeAddons(addons, gitAddons) } @@ -213,19 +217,25 @@ func renderApplication(addon *apis.DetailAddonResponse, args *apis.EnableAddonRe if args == nil { args = &apis.EnableAddonRequest{Args: map[string]string{}} } - app := &v1beta1.Application{ - TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, - ObjectMeta: metav1.ObjectMeta{ - Name: AddonName2AppName(addon.Name), - Namespace: types.DefaultKubeVelaNS, - Labels: map[string]string{ - oam.LabelAddonName: addon.Name, + app := addon.AppTemplate + if app == nil { + app = &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, + ObjectMeta: metav1.ObjectMeta{ + Name: AddonName2AppName(addon.Name), + Namespace: types.DefaultKubeVelaNS, + Labels: map[string]string{ + oam.LabelAddonName: addon.Name, + }, }, - }, - Spec: v1beta1.ApplicationSpec{ - Components: []common2.ApplicationComponent{}, - }, + Spec: v1beta1.ApplicationSpec{ + Components: []common2.ApplicationComponent{}, + }, + } } + app.Name = AddonName2AppName(addon.Name) + app.Labels = util.MergeMapOverrideWithDst(app.Labels, map[string]string{oam.LabelAddonName: addon.Name}) + for _, tmpl := range addon.YAMLTemplates { comp, err := renderRawComponent(tmpl) if err != nil { @@ -234,40 +244,22 @@ func renderApplication(addon *apis.DetailAddonResponse, args *apis.EnableAddonRe app.Spec.Components = append(app.Spec.Components, *comp) } for _, tmpl := range addon.CUETemplates { - yamlData, err := renderCUETemplate(tmpl.Data, addon.Parameters, args.Args) + comp, err := renderCUETemplate(tmpl, addon.Parameters, args.Args) if err != nil { log.Logger.Errorf("failed to render CUE template: %v", err) return nil, bcode.ErrAddonRenderFail } - comp, err := renderRawComponent(apis.AddonElementFile{Data: yamlData, Name: tmpl.Name, Path: tmpl.Path}) + app.Spec.Components = append(app.Spec.Components, *comp) + } + for _, def := range addon.Definitions { + comp, err := renderRawComponent(def) if err != nil { return nil, err } app.Spec.Components = append(app.Spec.Components, *comp) } - return app, nil -} -func renderCUETemplate(template string, parameters string, args map[string]string) (string, error) { - bt, err := json.Marshal(args) - if err != nil { - return "", err - } - var paramFile = cuemodel.ParameterFieldName + ": {}" - if string(bt) != "null" { - paramFile = fmt.Sprintf("%s: %s", cuemodel.ParameterFieldName, string(bt)) - } - param := fmt.Sprintf("%s\n%s", paramFile, parameters) - v, err := value.NewValue(param, nil, "") - if err != nil { - return "", err - } - out, err := v.LookupByScript(fmt.Sprintf("{%s}", template)) - if err != nil { - return "", err - } - b, err := cueyaml.Encode(out.CueValue()) - return string(b), err + return app, nil } func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error { @@ -320,6 +312,41 @@ func renderRawComponent(elem apis.AddonElementFile) (*common2.ApplicationCompone return &baseRawComponent, nil } +// renderCUETemplate will return a component from cue template +func renderCUETemplate(elem apis.AddonElementFile, parameters string, args map[string]string) (*common2.ApplicationComponent, error) { + bt, err := json.Marshal(args) + if err != nil { + return nil, err + } + var paramFile = cuemodel.ParameterFieldName + ": {}" + if string(bt) != "null" { + paramFile = fmt.Sprintf("%s: %s", cuemodel.ParameterFieldName, string(bt)) + } + param := fmt.Sprintf("%s\n%s", paramFile, parameters) + v, err := value.NewValue(param, nil, "") + if err != nil { + return nil, err + } + out, err := v.LookupByScript(fmt.Sprintf("{%s}", elem.Data)) + if err != nil { + return nil, err + } + compContent, err := out.LookupValue("output") + if err != nil { + return nil, err + } + b, err := cueyaml.Encode(compContent.CueValue()) + + comp := common2.ApplicationComponent{ + Name: strings.Join(append(elem.Path, elem.Name), "-"), + } + err = yaml.Unmarshal(b, &comp) + if err != nil { + return nil, err + } + + return &comp, err +} func addonRegistryModelFromCreateAddonRegistryRequest(req apis.CreateAddonRegistryRequest) *model.AddonRegistry { return &model.AddonRegistry{ Name: req.Name, @@ -390,11 +417,16 @@ func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.Detail break } err = readDefinitions(addonRes, gith, file) - case AddonTemplateDirName: + case AddonResourcesDirName: if !detailed { break } - err = readTemplates(addonRes, gith, file) + err = readResources(addonRes, gith, file) + case AddonTemplateFileName: + if !detailed { + break + } + err = readTemplate(addonRes, gith, file) } if err != nil { @@ -408,15 +440,38 @@ func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.Detail return addons, nil } -func readTemplates(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.RepositoryContent) error { - dirPath := strings.Split(dir.GetPath(), "/") - // remove - for i, d := range dirPath { - if d == AddonTemplateDirName { - dirPath = dirPath[i:] - break +func cutPathUntil(path []string, end string) ([]string, error) { + for i, d := range path { + if d == end { + return path[i:], nil } - dirPath = dirPath[i:] + } + return nil, errors.New("cut path fail, target directory name not found") +} + +func readTemplate(addon *apis.DetailAddonResponse, h *gitHelper, file *github.RepositoryContent) error { + content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) + if err != nil { + return err + } + data, err := content.GetContent() + if err != nil { + return err + } + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + addon.AppTemplate = &v1beta1.Application{} + _, _, err = dec.Decode([]byte(data), nil, addon.AppTemplate) + if err != nil { + return err + } + return nil +} + +func readResources(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.RepositoryContent) error { + dirPath := strings.Split(dir.GetPath(), "/") + dirPath, err := cutPathUntil(dirPath, AddonResourcesDirName) + if err != nil { + return err } _, files, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *dir.Path, nil) @@ -446,7 +501,7 @@ func readTemplates(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.Re addon.YAMLTemplates = append(addon.YAMLTemplates, apis.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) } case "dir": - err = readTemplates(addon, h, file) + err = readResources(addon, h, file) if err != nil { return err } @@ -456,6 +511,12 @@ func readTemplates(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.Re } func readDefinitions(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.RepositoryContent) error { + dirPath := strings.Split(dir.GetPath(), "/") + dirPath, err := cutPathUntil(dirPath, AddonDefinitionsDirName) + if err != nil { + return err + } + _, files, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *dir.Path, nil) if err != nil { return err @@ -471,11 +532,7 @@ func readDefinitions(addon *apis.DetailAddonResponse, h *gitHelper, dir *github. if err != nil { return err } - d, err := getDefinitionMetaFromYAML(b) - if err != nil { - return err - } - addon.Definitions = append(addon.Definitions, d) + addon.Definitions = append(addon.Definitions, apis.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) case "dir": err = readDefinitions(addon, h, file) if err != nil { @@ -486,23 +543,6 @@ func readDefinitions(addon *apis.DetailAddonResponse, h *gitHelper, dir *github. return nil } -func getDefinitionMetaFromYAML(data string) (*apis.Definition, error) { - dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) - obj := &unstructured.Unstructured{} - _, _, err := dec.Decode([]byte(data), nil, obj) - if err != nil { - return nil, bcode.ErrAddonRenderFail - } - d := &apis.Definition{ - Name: obj.GetName(), - Kind: obj.GetKind(), - } - if ann := obj.GetAnnotations(); ann != nil { - d.Description = ann[types.AnnDescription] - } - return d, nil -} - func readMetadata(addon *apis.DetailAddonResponse, h *gitHelper, file *github.RepositoryContent) error { content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) if err != nil { From 976da6f35a8ffee2cfe7a05542b4ceaf557ffe43 Mon Sep 17 00:00:00 2001 From: Hongchao Deng Date: Mon, 8 Nov 2021 20:04:08 +0800 Subject: [PATCH 23/59] Feat: add local cache of addons to avoid Github limit (#2558) * Feat: add local cache of addons to avoid Github limit comment * comment --- pkg/apiserver/rest/usecase/addon.go | 83 ++++++++++++++++++------- pkg/apiserver/rest/utils/bcode/addon.go | 45 ++++++++++---- 2 files changed, 94 insertions(+), 34 deletions(-) diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 5127b1502..2eadc5896 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -30,6 +30,7 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + restutils "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" cuemodel "github.com/oam-dev/kubevela/pkg/cue/model" "github.com/oam-dev/kubevela/pkg/cue/model/value" @@ -77,16 +78,18 @@ func NewAddonUsecase(ds datastore.DataStore) AddonUsecase { panic(err) } return &addonUsecaseImpl{ - ds: ds, - kubeClient: kubecli, - apply: apply.NewAPIApplicator(kubecli), + addonRegistryCache: make(map[string]*restutils.MemoryCache), + addonRegistryDS: ds, + kubeClient: kubecli, + apply: apply.NewAPIApplicator(kubecli), } } type addonUsecaseImpl struct { - ds datastore.DataStore - kubeClient client.Client - apply apply.Applicator + addonRegistryCache map[string]*restutils.MemoryCache + addonRegistryDS datastore.DataStore + kubeClient client.Client + apply apply.Applicator } // GetAddon will get addon information, if detailed is not set, addon's componennt and internal definition won't be returned @@ -117,7 +120,7 @@ func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, EnablingProgress: nil, }, nil } - return nil, bcode.ErrGetApplicationFail + return nil, bcode.ErrGetAddonApplication } switch app.Status.Phase { @@ -140,15 +143,24 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, regist if err != nil { return nil, err } + for _, r := range rs { if registry != "" && r.Name != registry { continue } - gitAddons, err := getAddonsFromGit(r.Git.URL, r.Git.Path, r.Git.Token, detailed) - if err != nil { - log.Logger.Errorf("fail to get addons from registry %s", r.Name) - continue + + var gitAddons []*apis.DetailAddonResponse + if u.isRegistryCacheUpToDate(registry) { + gitAddons = u.getRegistryCache(registry) + } else { + gitAddons, err = getAddonsFromGit(r.Git.URL, r.Git.Path, r.Git.Token, detailed) + if err != nil { + log.Logger.Errorf("fail to get addons from registry %s", r.Name) + continue + } + u.putRegistryCache(registry, gitAddons) } + addons = mergeAddons(addons, gitAddons) } @@ -165,17 +177,18 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, regist sort.Slice(addons, func(i, j int) bool { return addons[i].Name < addons[j].Name }) + return addons, nil } func (u *addonUsecaseImpl) DeleteAddonRegistry(ctx context.Context, name string) error { - return u.ds.Delete(ctx, &model.AddonRegistry{Name: name}) + return u.addonRegistryDS.Delete(ctx, &model.AddonRegistry{Name: name}) } func (u *addonUsecaseImpl) CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) { r := addonRegistryModelFromCreateAddonRegistryRequest(req) - err := u.ds.Add(ctx, r) + err := u.addonRegistryDS.Add(ctx, r) if err != nil { if errors.Is(err, datastore.ErrRecordExist) { return nil, bcode.ErrAddonRegistryExist @@ -193,7 +206,7 @@ func (u *addonUsecaseImpl) GetAddonRegistryModel(ctx context.Context, name strin var r = model.AddonRegistry{ Name: name, } - err := u.ds.Get(ctx, &r) + err := u.addonRegistryDS.Get(ctx, &r) if err != nil { return nil, err } @@ -202,14 +215,18 @@ func (u *addonUsecaseImpl) GetAddonRegistryModel(ctx context.Context, name strin func (u *addonUsecaseImpl) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) { var r = model.AddonRegistry{} - entities, err := u.ds.List(ctx, &r, &datastore.ListOptions{}) + + var list []*apis.AddonRegistryMeta + entities, err := u.addonRegistryDS.List(ctx, &r, &datastore.ListOptions{}) if err != nil { return nil, err } - var list []*apis.AddonRegistryMeta for _, entity := range entities { list = append(list, ConvertAddonRegistryModel2AddonRegistryMeta(entity.(*model.AddonRegistry))) } + sort.Slice(list, func(i, j int) bool { + return list[i].Name < list[j].Name + }) return list, nil } @@ -247,7 +264,7 @@ func renderApplication(addon *apis.DetailAddonResponse, args *apis.EnableAddonRe comp, err := renderCUETemplate(tmpl, addon.Parameters, args.Args) if err != nil { log.Logger.Errorf("failed to render CUE template: %v", err) - return nil, bcode.ErrAddonRenderFail + return nil, bcode.ErrAddonRender } app.Spec.Components = append(app.Spec.Components, *comp) } @@ -274,10 +291,25 @@ func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args ap err = u.kubeClient.Create(ctx, app) if err != nil { log.Logger.Errorf("apply application fail: %s", err.Error()) - return bcode.ErrAddonApplyFail + return bcode.ErrAddonApply } return nil +} +func (u *addonUsecaseImpl) getRegistryCache(name string) []*apis.DetailAddonResponse { + return u.addonRegistryCache[name].GetData().([]*apis.DetailAddonResponse) +} + +func (u *addonUsecaseImpl) putRegistryCache(name string, addons []*apis.DetailAddonResponse) { + u.addonRegistryCache[name] = restutils.NewMemoryCache(addons, time.Minute*3) +} + +func (u *addonUsecaseImpl) isRegistryCacheUpToDate(name string) bool { + d, ok := u.addonRegistryCache[name] + if !ok { + return false + } + return !d.IsExpired() } func (u *addonUsecaseImpl) DisableAddon(ctx context.Context, name string) error { @@ -397,6 +429,9 @@ func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.Detail addonRes := &apis.DetailAddonResponse{} _, files, _, err := gith.Client.Repositories.GetContents(context.Background(), gith.Meta.Owner, gith.Meta.Repo, subItems.GetPath(), nil) if err != nil { + if bcode.IsGithubRateLimit(err) { + return nil, bcode.ErrAddonRegistryRateLimit + } log.Logger.Errorf("failed to read dir %s: %v", subItems.GetPath(), err) continue } @@ -430,6 +465,9 @@ func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.Detail } if err != nil { + if bcode.IsGithubRateLimit(err) { + return nil, bcode.ErrAddonRegistryRateLimit + } log.Logger.Errorf("failed to read file %s: %v", file.GetPath(), err) continue } @@ -576,12 +614,14 @@ func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { baseURL = strings.TrimSuffix(baseURL, ".git") u, err := url.Parse(baseURL) if err != nil { - return nil, err + log.Logger.Errorf("parsing %s failed: %v", baseURL, err) + return nil, bcode.ErrAddonRegistryInvalid } u.Path = path.Join(u.Path, dir) _, gitmeta, err := utils.Parse(u.String()) if err != nil { - return nil, err + log.Logger.Errorf("parsing %s failed: %v", u.String(), err) + return nil, bcode.ErrAddonRegistryInvalid } return &gitHelper{ @@ -593,7 +633,8 @@ func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { func readRepo(h *gitHelper) ([]*github.RepositoryContent, error) { _, dirs, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, h.Meta.Path, nil) if err != nil { - return nil, err + log.Logger.Errorf("readRepo fail: %v", err) + return nil, bcode.WrapGithubRateLimitErr(err) } return dirs, nil } diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go index a1415bf6a..e5633d011 100644 --- a/pkg/apiserver/rest/utils/bcode/addon.go +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -16,25 +16,44 @@ limitations under the License. package bcode +import ( + "github.com/google/go-github/v32/github" +) + var ( - // ErrAddonNotExist addon not exist + // ErrAddonNotExist addon registry not exist ErrAddonNotExist = NewBcode(404, 50001, "addon not exist") - // ErrAddonRegistryExist addon is exist - ErrAddonRegistryExist = NewBcode(400, 50002, "addon name already exists") + // ErrAddonRegistryExist addon registry already exist + ErrAddonRegistryExist = NewBcode(400, 50002, "addon registry already exists") - // ErrAddonRenderFail fail to render addon application - ErrAddonRenderFail = NewBcode(500, 50010, "addon render fail") + // ErrAddonRegistryInvalid addon registry is exist + ErrAddonRegistryInvalid = NewBcode(400, 50003, "addon registry invalid") - // ErrAddonApplyFail fail to apply application to cluster - ErrAddonApplyFail = NewBcode(500, 50011, "fail to apply addon application") + // ErrAddonRegistryRateLimit addon registry is rate limited by Github + ErrAddonRegistryRateLimit = NewBcode(400, 50004, "Exceed Github rate limit") - // ErrGetApplicationFail fail to get addon application - ErrGetApplicationFail = NewBcode(500, 50013, "fail to get addon application") + // ErrAddonRender fail to render addon application + ErrAddonRender = NewBcode(500, 50010, "addon render fail") - // ErrAddonDisableFail fail to disable addon - ErrAddonDisableFail = NewBcode(500, 50016, "fail to disable addon") + // ErrAddonApply fail to apply application to cluster + ErrAddonApply = NewBcode(500, 50011, "fail to apply addon application") - // ErrAddonNotEnabled means addon can't be disable because it's not enabled - ErrAddonNotEnabled = NewBcode(400, 50017, "addon not enabled") + // ErrReadGit fail to get addon application + ErrReadGit = NewBcode(500, 50012, "fail to read git repo") + + // ErrGetAddonApplication fail to get addon application + ErrGetAddonApplication = NewBcode(500, 50013, "fail to get addon application") ) + +func IsGithubRateLimit(err error) bool { + _, ok := err.(*github.RateLimitError) + return ok +} + +func WrapGithubRateLimitErr(err error) error { + if IsGithubRateLimit(err) { + return ErrAddonRegistryRateLimit + } + return err +} From 39e8bc0b98d8705f4548dc6ba4d09e11987d2a32 Mon Sep 17 00:00:00 2001 From: qiaozp <47812250+chivalryq@users.noreply.github.com> Date: Mon, 8 Nov 2021 23:13:04 +0800 Subject: [PATCH 24/59] add addon parameter openAPI schema (#2666) --- pkg/apiserver/rest/apis/v1/types.go | 3 +++ pkg/apiserver/rest/usecase/addon.go | 36 +++++++++++++++++++++++++++++ pkg/controller/utils/capability.go | 6 ++--- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 5bd90ecf5..6a547404d 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -115,6 +115,9 @@ type AddonElementFile struct { type DetailAddonResponse struct { AddonMeta + APISchema *openapi3.Schema `json:"schema"` + UISchema []*utils.UIParameter `json:"uiSchema"` + // More details about the addon, e.g. README Detail string `json:"detail,omitempty"` Definitions []AddonElementFile `json:"definitions"` diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 2eadc5896..6dec1a338 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -2,9 +2,13 @@ package usecase import ( "context" + "cuelang.org/go/cue" "encoding/json" "errors" "fmt" + "github.com/getkin/kin-openapi/openapi3" + utils2 "github.com/oam-dev/kubevela/pkg/controller/utils" + "github.com/oam-dev/kubevela/pkg/utils/common" "net/url" "path" "path/filepath" @@ -473,11 +477,43 @@ func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.Detail } } + if detailed && addonRes.Parameters != "" { + err = genAddonAPISchema(addonRes) + if err != nil { + continue + } + // render default ui schema + addonRes.UISchema = renderDefaultUISchema(addonRes.APISchema) + } addons = append(addons, addonRes) } return addons, nil } +func genAddonAPISchema(addonRes *apis.DetailAddonResponse) error { + param, err := utils2.PrepareParameterCue(addonRes.Name, addonRes.Parameters) + if err != nil { + return err + } + var r cue.Runtime + cueInst, err := r.Compile("-", param) + if err != nil { + return err + } + data, err := common.GenOpenAPI(cueInst) + if err != nil { + log.Logger.Errorf("fail to generate openAPI json schema for addon: %s, err: %s", addonRes.Name, err) + return err + } + schema := &openapi3.Schema{} + if err := schema.UnmarshalJSON(data); err != nil { + log.Logger.Errorf("fail to unmarshal openAPI json schema for addon %s, err: %s", addonRes.Name, err) + return err + } + addonRes.APISchema = schema + return nil +} + func cutPathUntil(path []string, end string) ([]string, error) { for i, d := range path { if d == end { diff --git a/pkg/controller/utils/capability.go b/pkg/controller/utils/capability.go index 7b31e2992..192a066ac 100644 --- a/pkg/controller/utils/capability.go +++ b/pkg/controller/utils/capability.go @@ -420,7 +420,7 @@ func getOpenAPISchema(capability types.Capability, pd *packages.PackageDiscover) // generateOpenAPISchemaFromCapabilityParameter returns the parameter of a definition in cue.Value format func generateOpenAPISchemaFromCapabilityParameter(capability types.Capability, pd *packages.PackageDiscover) ([]byte, error) { - template, err := prepareParameterCue(capability.Name, capability.CueTemplate) + template, err := PrepareParameterCue(capability.Name, capability.CueTemplate) if err != nil { if errors.As(err, &ErrNoSectionParameterInCue{}) { // return OpenAPI with empty object parameter, making it possible to generate ConfigMap @@ -462,8 +462,8 @@ func GenerateOpenAPISchemaFromDefinition(definitionName, cueTemplate string) ([] return generateOpenAPISchemaFromCapabilityParameter(capability, nil) } -// prepareParameterCue cuts `parameter` section form definition .cue file -func prepareParameterCue(capabilityName, capabilityTemplate string) (string, error) { +// PrepareParameterCue cuts `parameter` section form definition .cue file +func PrepareParameterCue(capabilityName, capabilityTemplate string) (string, error) { var template string var withParameterFlag bool r := regexp.MustCompile(`[[:space:]]*parameter:[[:space:]]*`) From 738b416ec2f63f4e1c07b68bf19024adb7b5949f Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Tue, 9 Nov 2021 09:47:41 +0800 Subject: [PATCH 25/59] Fix: fix can not create first app env plan bug (#2665) * Fix: fix can not create first app env plan bug * Fix: fix test bug Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 314 +++++++--- pkg/apiserver/rest/apis/v1/types.go | 62 +- pkg/apiserver/rest/usecase/applicationplan.go | 45 +- .../rest/usecase/applicationplan_test.go | 32 +- .../rest/usecase/testdata/ui-schema.yaml | 549 +++++++++--------- 5 files changed, 594 insertions(+), 408 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index bb9e23b5c..3aa644229 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -1605,9 +1605,9 @@ "parameters": [ { "enum": [ - "component", "trait", - "workflowstep" + "workflowstep", + "component" ], "type": "string", "description": "query the definition type", @@ -2275,10 +2275,10 @@ "common.AppRolloutStatus": { "required": [ "rollingState", - "batchRollingState", "currentBatch", - "upgradedReadyReplicas", "upgradedReplicas", + "upgradedReadyReplicas", + "batchRollingState", "lastTargetAppRevision" ], "properties": { @@ -2745,8 +2745,8 @@ }, "model.ApplicationComponentPlan": { "required": [ - "createTime", "updateTime", + "createTime", "appPrimaryKey", "creator", "name", @@ -3445,10 +3445,6 @@ "v1.ClusterBase": { "required": [ "name", - "alias", - "description", - "icon", - "labels", "providerInfo", "apiServerURL", "dashboardURL", @@ -3658,7 +3654,6 @@ "accessKeySecret", "clusterID", "name", - "alias", "icon" ], "properties": { @@ -3707,9 +3702,7 @@ "v1.CreateApplicationEnvPlanRequest": { "required": [ "name", - "alias", - "clusterSelector", - "componentSelector" + "clusterSelector" ], "properties": { "alias": { @@ -3732,9 +3725,7 @@ "v1.CreateApplicationPlanRequest": { "required": [ "name", - "alias", "namespace", - "description", "icon" ], "properties": { @@ -3845,7 +3836,6 @@ "v1.CreateClusterRequest": { "required": [ "name", - "alias", "icon" ], "properties": { @@ -3881,11 +3871,7 @@ "v1.CreateComponentPlanRequest": { "required": [ "name", - "alias", - "description", - "icon", - "componentType", - "dependsOn" + "componentType" ], "properties": { "alias": { @@ -3966,8 +3952,6 @@ "required": [ "appName", "name", - "alias", - "description", "enable", "default" ], @@ -3998,24 +3982,6 @@ } } }, - "v1.Definition": { - "required": [ - "kind", - "name", - "description" - ], - "properties": { - "description": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, "v1.DefinitionBase": { "required": [ "name", @@ -4036,15 +4002,19 @@ }, "v1.DetailAddonResponse": { "required": [ - "icon", "name", "version", "description", + "icon", "definitions", "parameters", - "cue_templates" + "cue_templates", + "app_template" ], "properties": { + "app_template": { + "$ref": "#/definitions/v1beta1.Application" + }, "cue_templates": { "type": "array", "items": { @@ -4054,7 +4024,7 @@ "definitions": { "type": "array", "items": { - "$ref": "#/definitions/v1.Definition" + "$ref": "#/definitions/v1.AddonElementFile" } }, "dependencies": { @@ -4103,15 +4073,15 @@ }, "v1.DetailApplicationPlanResponse": { "required": [ + "alias", + "namespace", + "gatewayRule", + "name", + "description", + "createTime", "updateTime", "icon", "status", - "name", - "alias", - "description", - "createTime", - "namespace", - "gatewayRule", "policies", "status", "resourceInfo", @@ -4181,20 +4151,20 @@ }, "v1.DetailClusterResponse": { "required": [ + "alias", + "kubeConfigSecret", "createTime", + "description", "icon", "labels", - "reason", - "description", - "status", - "dashboardURL", - "name", - "apiServerURL", - "kubeConfigSecret", - "updateTime", - "alias", "provider", "kubeConfig", + "updateTime", + "name", + "status", + "reason", + "apiServerURL", + "dashboardURL", "resourceInfo" ], "properties": { @@ -4252,11 +4222,11 @@ }, "v1.DetailComponentPlanResponse": { "required": [ - "appPrimaryKey", - "updateTime", "name", - "creator", "type", + "appPrimaryKey", + "creator", + "updateTime", "createTime", "alias" ], @@ -4353,13 +4323,13 @@ }, "v1.DetailPolicyResponse": { "required": [ + "updateTime", "name", "type", "description", "creator", "properties", - "createTime", - "updateTime" + "createTime" ], "properties": { "createTime": { @@ -4389,13 +4359,13 @@ }, "v1.DetailWorkflowPlanResponse": { "required": [ + "updateTime", + "name", + "alias", "description", "enable", "default", "createTime", - "updateTime", - "name", - "alias", "workflowRecord" ], "properties": { @@ -4435,10 +4405,10 @@ }, "v1.DetailWorkflowRecordResponse": { "required": [ - "name", "namespace", "suspend", "terminated", + "name", "deployTime", "deployUser", "commit", @@ -4512,9 +4482,7 @@ "v1.EnvBind": { "required": [ "name", - "alias", - "clusterSelector", - "componentSelector" + "clusterSelector" ], "properties": { "alias": { @@ -4727,6 +4695,39 @@ } } }, + "v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "type": "string" + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply'", + "type": "string" + } + } + }, "v1.NamespaceBase": { "required": [ "name", @@ -4753,10 +4754,10 @@ }, "v1.NamespaceDetailResponse": { "required": [ - "updateTime", "name", "description", - "createTime" + "createTime", + "updateTime" ], "properties": { "createTime": { @@ -4775,6 +4776,92 @@ } } }, + "v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "clusterName": { + "description": "The name of the cluster which the object belongs to. This is used to distinguish resources with same name and namespace in different clusters. This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request.", + "type": "string" + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "type": "string" + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "type": "string" + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string" + } + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will NOT return a 409 - instead, it will either return 201 Created or 500 with Reason ServerTimeout indicating a unique name could not be found in the time allotted, and the client should retry (optionally after the time indicated in the Retry-After header).\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "$ref": "#/definitions/v1.ManagedFieldsEntry" + } + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "$ref": "#/definitions/v1.OwnerReference" + } + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "SelfLink is a URL representing this object. Populated by the system. Read-only.\n\nDEPRECATED Kubernetes will stop propagating this field in 1.20 release and the field is planned to be removed in 1.21 release.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: http://kubernetes.io/docs/user-guide/identifiers#uids", + "type": "string" + } + } + }, "v1.ObjectReference": { "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", "properties": { @@ -4808,6 +4895,41 @@ } } }, + "v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "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: http://kubernetes.io/docs/user-guide/identifiers#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#uids", + "type": "string" + } + } + }, "v1.PolicyBase": { "required": [ "name", @@ -4881,12 +5003,20 @@ } } }, + "v1.TypeMeta": { + "description": "TypeMeta describes an individual object in an API response or request with strings representing the type of the object and its API schema version. Structures that are versioned or persisted should inline TypeMeta.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + } + }, "v1.UpdateApplicationPlanRequest": { - "required": [ - "alias", - "description", - "icon" - ], "properties": { "alias": { "type": "string" @@ -4925,8 +5055,6 @@ }, "v1.UpdateWorkflowPlanRequest": { "required": [ - "alias", - "description", "enable", "default" ], @@ -5025,10 +5153,7 @@ "v1.WorkflowStep": { "required": [ "name", - "alias", - "type", - "description", - "dependsOn" + "type" ], "properties": { "alias": { @@ -5288,6 +5413,27 @@ } } }, + "v1beta1.Application": { + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "$ref": "#/definitions/v1.ObjectMeta" + }, + "spec": { + "$ref": "#/definitions/v1beta1.ApplicationSpec" + }, + "status": { + "$ref": "#/definitions/common.AppStatus" + } + } + }, "v1beta1.ApplicationSpec": { "required": [ "components" diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 6a547404d..a010d0e6d 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -149,7 +149,7 @@ type AccessKeyRequest struct { // CreateClusterRequest request parameters to create a cluster type CreateClusterRequest struct { Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` Description string `json:"description,omitempty"` Icon string `json:"icon"` KubeConfig string `json:"kubeConfig,omitempty" validate:"required_without=KubeConfigSecret"` @@ -164,8 +164,8 @@ type ConnectCloudClusterRequest struct { AccessKeySecret string `json:"accessKeySecret"` ClusterID string `json:"clusterID"` Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias"` - Description string `json:"description,omitempty"` + Alias string `json:"alias" optional:"true" validate:"checkalias"` + Description string `json:"description,omitempty" optional:"true"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` } @@ -227,10 +227,10 @@ type ListCloudClusterCreationResponse struct { // ClusterBase cluster base model type ClusterBase struct { Name string `json:"name"` - Alias string `json:"alias" validate:"checkalias"` - Description string `json:"description"` - Icon string `json:"icon"` - Labels map[string]string `json:"labels"` + Alias string `json:"alias" optional:"true" validate:"checkalias"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon" optional:"true"` + Labels map[string]string `json:"labels" optional:"true"` Provider model.ProviderInfo `json:"providerInfo"` APIServerURL string `json:"apiServerURL"` @@ -302,9 +302,9 @@ type GatewayRule struct { // CreateApplicationPlanRequest create application plan request body type CreateApplicationPlanRequest struct { Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` Namespace string `json:"namespace" validate:"checkname"` - Description string `json:"description"` + Description string `json:"description" optional:"true"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` EnvBind []*EnvBind `json:"envBind,omitempty"` @@ -315,19 +315,19 @@ type CreateApplicationPlanRequest struct { // UpdateApplicationPlanRequest update application plan base config type UpdateApplicationPlanRequest struct { - Alias string `json:"alias" validate:"checkalias"` - Description string `json:"description"` - Icon string `json:"icon"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon" optional:"true"` Labels map[string]string `json:"labels,omitempty"` } // EnvBind application env bind type EnvBind struct { Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias"` - Description string `json:"description,omitempty"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` ClusterSelector ClusterSelector `json:"clusterSelector"` - ComponentSelector *ComponentSelector `json:"componentSelector"` + ComponentSelector *ComponentSelector `json:"componentSelector" optional:"true"` } // ClusterSelector cluster selector @@ -388,14 +388,14 @@ type ComponentPlanListResponse struct { // CreateComponentPlanRequest create component plan request model type CreateComponentPlanRequest struct { Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias"` - Description string `json:"description"` - Icon string `json:"icon"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon" optional:"true"` Labels map[string]string `json:"labels,omitempty"` ComponentType string `json:"componentType" validate:"checkname"` - EnvNames []string `json:"envNames,omitempty"` + EnvNames []string `json:"envNames,omitempty" optional:"true"` Properties string `json:"properties,omitempty"` - DependsOn []string `json:"dependsOn"` + DependsOn []string `json:"dependsOn" optional:"true"` } // DetailComponentPlanResponse detail component plan model @@ -535,8 +535,8 @@ type PolicyDefinition struct { type CreateWorkflowPlanRequest struct { AppName string `json:"appName" validate:"checkname"` Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias"` - Description string `json:"description"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` Steps []WorkflowStep `json:"steps,omitempty"` Enable bool `json:"enable"` Default bool `json:"default"` @@ -544,8 +544,8 @@ type CreateWorkflowPlanRequest struct { // UpdateWorkflowPlanRequest update or create application workflow type UpdateWorkflowPlanRequest struct { - Alias string `json:"alias" validate:"checkalias"` - Description string `json:"description"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` Steps []WorkflowStep `json:"steps,omitempty"` Enable bool `json:"enable"` Default bool `json:"default"` @@ -555,13 +555,13 @@ type UpdateWorkflowPlanRequest struct { type WorkflowStep struct { // Name is the unique name of the workflow step. Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` Type string `json:"type" validate:"checkname"` - Description string `json:"description"` - DependsOn []string `json:"dependsOn"` + Description string `json:"description" optional:"true"` + DependsOn []string `json:"dependsOn" optional:"true"` Properties string `json:"properties,omitempty"` - Inputs common.StepInputs `json:"inputs,omitempty"` - Outputs common.StepOutputs `json:"outputs,omitempty"` + Inputs common.StepInputs `json:"inputs,omitempty" optional:"true"` + Outputs common.StepOutputs `json:"outputs,omitempty" optional:"true"` } // DetailWorkflowPlanResponse detail workflow response @@ -641,8 +641,8 @@ type VelaQLViewResponse map[string]interface{} // PutApplicationPlanEnvRequest set diff request type PutApplicationPlanEnvRequest struct { ComponentSelector *ComponentSelector `json:"componentSelector,omitempty"` - Alias *string `json:"alias,omitempty" validate:"checkalias"` - Description *string `json:"description,omitempty"` + Alias *string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description *string `json:"description,omitempty" optional:"true"` ClusterSelector *ClusterSelector `json:"clusterSelector,omitempty"` } diff --git a/pkg/apiserver/rest/usecase/applicationplan.go b/pkg/apiserver/rest/usecase/applicationplan.go index a22465bc0..cabf58d7a 100644 --- a/pkg/apiserver/rest/usecase/applicationplan.go +++ b/pkg/apiserver/rest/usecase/applicationplan.go @@ -1013,27 +1013,36 @@ func (c *applicationUsecaseImpl) CreateApplicationEnvBindingPlan(ctx context.Con return nil, bcode.ErrApplicationEnvExist } } - app.EnvBinds = append(app.EnvBinds, createModelEnvBind(envReq.EnvBind)) envBinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) if err != nil { - return nil, err + if !errors.Is(err, bcode.ErrApplicationNotEnv) { + return nil, err + } } - envBinding.Envs = append(envBinding.Envs, createEnvBind(envReq.EnvBind)) - properties, err := model.NewJSONStructByStruct(envBinding) - if err != nil { - log.Logger.Errorf("new env binding properties failure,%s", err.Error()) - return nil, bcode.ErrInvalidProperties - } - policy := &model.ApplicationPolicyPlan{ - AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindPolicyDefaultName, - } - if err := c.ds.Get(ctx, policy); err != nil { - return nil, err - } - policy.Properties = properties - if err := c.ds.Put(ctx, policy); err != nil { - return nil, err + if envBinding == nil { + _, err := c.createApplictionPlanEnvBindingPolicy(ctx, app, []*apisv1.EnvBind{&envReq.EnvBind}) + if err != nil { + return nil, err + } + } else { + app.EnvBinds = append(app.EnvBinds, createModelEnvBind(envReq.EnvBind)) + envBinding.Envs = append(envBinding.Envs, createEnvBind(envReq.EnvBind)) + properties, err := model.NewJSONStructByStruct(envBinding) + if err != nil { + log.Logger.Errorf("new env binding properties failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + policy := &model.ApplicationPolicyPlan{ + AppPrimaryKey: app.PrimaryKey(), + Name: EnvBindPolicyDefaultName, + } + if err := c.ds.Get(ctx, policy); err != nil { + return nil, err + } + policy.Properties = properties + if err := c.ds.Put(ctx, policy); err != nil { + return nil, err + } } if err := c.ds.Put(ctx, app); err != nil { return nil, err diff --git a/pkg/apiserver/rest/usecase/applicationplan_test.go b/pkg/apiserver/rest/usecase/applicationplan_test.go index 321335cdb..b57a495db 100644 --- a/pkg/apiserver/rest/usecase/applicationplan_test.go +++ b/pkg/apiserver/rest/usecase/applicationplan_test.go @@ -346,6 +346,36 @@ var _ = Describe("Test application usecase function", func() { }) It("Test CreateApplicationEnvBindingPlan function", func() { + req := v1.CreateApplicationPlanRequest{ + Name: "not-have-env-bind", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + } + _, err := appUsecase.CreateApplicationPlan(context.TODO(), req) + Expect(err).Should(BeNil()) + appModel4, err := appUsecase.GetApplicationPlan(context.TODO(), "not-have-env-bind") + Expect(err).Should(BeNil()) + By("test create first env") + env4, err := appUsecase.CreateApplicationEnvBindingPlan(context.TODO(), appModel4, v1.CreateApplicationEnvPlanRequest{ + EnvBind: v1.EnvBind{ + Name: "prod2", + Alias: "生产环境", + Description: "这是一个用户某客户的生产环境", + ClusterSelector: v1.ClusterSelector{ + Name: "prob", + }, + }, + }) + Expect(err).Should(BeNil()) + Expect(env4).ShouldNot(BeNil()) + + appModelNew, err := appUsecase.GetApplicationPlan(context.TODO(), "not-have-env-bind") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(appModelNew.EnvBinds), 1)).Should(BeEmpty()) + + By("test create not first env") appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -362,7 +392,7 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(env).ShouldNot(BeNil()) - appModelNew, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModelNew, err = appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(len(appModelNew.EnvBinds), 4)).Should(BeEmpty()) diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index d026df893..62f1f59ad 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -1,145 +1,9 @@ -- description: Instructions for assessing whether the container is in a suitable state - to serve traffic. - jsonKey: readinessProbe - label: ReadinessProbe检测 - sort: 1 - subParameters: - - description: Number of seconds after the container is started before the first - probe is initiated. - jsonKey: readinessProbe.initialDelaySeconds - label: InitialDelaySeconds - sort: 100 - uiType: Number - validete: - defaultValue: 0 - required: true - - description: How often, in seconds, to execute the probe. - jsonKey: readinessProbe.periodSeconds - label: PeriodSeconds - sort: 100 - uiType: Number - validete: - defaultValue: 10 - required: true - - description: Minimum consecutive successes for the probe to be considered successful - after having failed. - jsonKey: readinessProbe.successThreshold - label: SuccessThreshold - sort: 100 - uiType: Number - validete: - defaultValue: 1 - required: true - - description: Instructions for assessing container health by probing a TCP socket. - Either this attribute or the exec attribute or the httpGet attribute MUST be - specified. This attribute is mutually exclusive with both the exec attribute - and the httpGet attribute. - jsonKey: readinessProbe.tcpSocket - label: TcpSocket - sort: 100 - subParameters: - - description: The TCP socket within the container that should be probed to assess - container health. - jsonKey: readinessProbe.tcpSocket.port - label: Port - sort: 100 - uiType: Number - validete: - required: true - uiType: KV - validete: {} - - description: Number of seconds after which the probe times out. - jsonKey: readinessProbe.timeoutSeconds - label: TimeoutSeconds - sort: 100 - uiType: Number - validete: - defaultValue: 1 - required: true - - description: Instructions for assessing container health by executing a command. - Either this attribute or the httpGet attribute or the tcpSocket attribute MUST - be specified. This attribute is mutually exclusive with both the httpGet attribute - and the tcpSocket attribute. - jsonKey: readinessProbe.exec - label: Exec - sort: 100 - subParameters: - - description: A command to be executed inside the container to assess its health. - Each space delimited token of the command is a separate array element. Commands - exiting 0 are considered to be successful probes, whilst all other exit codes - are considered failures. - jsonKey: readinessProbe.exec.command - label: Command - sort: 100 - uiType: Strings - validete: - required: true - uiType: KV - validete: {} - - description: Number of consecutive failures required to determine the container - is not alive (liveness probe) or not ready (readiness probe). - jsonKey: readinessProbe.failureThreshold - label: FailureThreshold - sort: 100 - uiType: Number - validete: - defaultValue: 3 - required: true - - description: Instructions for assessing container health by executing an HTTP - GET request. Either this attribute or the exec attribute or the tcpSocket attribute - MUST be specified. This attribute is mutually exclusive with both the exec attribute - and the tcpSocket attribute. - jsonKey: readinessProbe.httpGet - label: HttpGet - sort: 100 - subParameters: - - description: "" - jsonKey: readinessProbe.httpGet.httpHeaders - label: HttpHeaders - sort: 100 - subParameters: - - description: "" - jsonKey: readinessProbe.httpGet.httpHeaders.[].name - label: Name - sort: 100 - uiType: Input - validete: - required: true - - description: "" - jsonKey: readinessProbe.httpGet.httpHeaders.[].value - label: Value - sort: 100 - uiType: Input - validete: - required: true - uiType: Structs - validete: {} - - description: The endpoint, relative to the port, to which the HTTP GET request - should be directed. - jsonKey: readinessProbe.httpGet.path - label: Path - sort: 100 - uiType: Input - validete: - required: true - - description: The TCP socket within the container to which the HTTP GET request - should be directed. - jsonKey: readinessProbe.httpGet.port - label: Port - sort: 100 - uiType: Number - validete: - required: true - uiType: KV - validete: {} - uiType: Group - validete: {} - description: Which image would you like to use for your service jsonKey: image label: Image - sort: 2 + sort: 1 uiType: ImageInput - validete: + validate: required: true - description: Specify image pull policy for your service disable: false @@ -147,7 +11,7 @@ label: 镜像更新策略 sort: 2 uiType: Select - validete: + validate: options: - label: 镜像不存在时更新 value: IfNotPresent @@ -161,45 +25,26 @@ label: Memory sort: 3 uiType: MemoryNumber - validete: {} -- description: Commands to run in the container - jsonKey: cmd - label: Cmd - sort: 100 - uiType: Strings - validete: {} -- description: Which port do you want customer traffic sent to - jsonKey: port - label: Port - sort: 100 - uiType: Number - validete: - defaultValue: 80 - required: true -- description: Specify image pull secrets for your service - jsonKey: imagePullSecrets - label: ImagePullSecrets - sort: 100 - uiType: Strings - validete: {} -- description: Instructions for assessing whether the container is alive. - jsonKey: livenessProbe - label: LivenessProbe - sort: 100 + validate: {} +- description: Instructions for assessing whether the container is in a suitable state + to serve traffic. + jsonKey: readinessProbe + label: ReadinessProbe检测 + sort: 4 subParameters: - description: Number of seconds after which the probe times out. - jsonKey: livenessProbe.timeoutSeconds + jsonKey: timeoutSeconds label: TimeoutSeconds sort: 100 uiType: Number - validete: + validate: defaultValue: 1 required: true - description: Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute. - jsonKey: livenessProbe.exec + jsonKey: exec label: Exec sort: 100 subParameters: @@ -207,141 +52,369 @@ Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures. - jsonKey: livenessProbe.exec.command + jsonKey: command label: Command sort: 100 uiType: Strings - validete: + validate: required: true uiType: KV - validete: {} + validate: {} - description: Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe). - jsonKey: livenessProbe.failureThreshold + jsonKey: failureThreshold label: FailureThreshold sort: 100 uiType: Number - validete: + validate: defaultValue: 3 required: true - description: Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute. - jsonKey: livenessProbe.httpGet + jsonKey: httpGet label: HttpGet sort: 100 subParameters: - description: The TCP socket within the container to which the HTTP GET request should be directed. - jsonKey: livenessProbe.httpGet.port + jsonKey: port label: Port sort: 100 uiType: Number - validete: + validate: required: true - description: "" - jsonKey: livenessProbe.httpGet.httpHeaders + jsonKey: httpHeaders label: HttpHeaders sort: 100 subParameters: - description: "" - jsonKey: livenessProbe.httpGet.httpHeaders.[].name + jsonKey: name label: Name sort: 100 uiType: Input - validete: + validate: required: true - description: "" - jsonKey: livenessProbe.httpGet.httpHeaders.[].value + jsonKey: value label: Value sort: 100 uiType: Input - validete: + validate: required: true uiType: Structs - validete: {} + validate: {} - description: The endpoint, relative to the port, to which the HTTP GET request should be directed. - jsonKey: livenessProbe.httpGet.path + jsonKey: path label: Path sort: 100 uiType: Input - validete: + validate: required: true uiType: KV - validete: {} + validate: {} - description: Number of seconds after the container is started before the first probe is initiated. - jsonKey: livenessProbe.initialDelaySeconds + jsonKey: initialDelaySeconds label: InitialDelaySeconds sort: 100 uiType: Number - validete: + validate: defaultValue: 0 required: true - description: How often, in seconds, to execute the probe. - jsonKey: livenessProbe.periodSeconds + jsonKey: periodSeconds label: PeriodSeconds sort: 100 uiType: Number - validete: + validate: defaultValue: 10 required: true - description: Minimum consecutive successes for the probe to be considered successful after having failed. - jsonKey: livenessProbe.successThreshold + jsonKey: successThreshold label: SuccessThreshold sort: 100 uiType: Number - validete: + validate: defaultValue: 1 required: true - description: Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute. - jsonKey: livenessProbe.tcpSocket + jsonKey: tcpSocket label: TcpSocket sort: 100 subParameters: - description: The TCP socket within the container that should be probed to assess container health. - jsonKey: livenessProbe.tcpSocket.port + jsonKey: port label: Port sort: 100 uiType: Number - validete: + validate: required: true uiType: KV - validete: {} + validate: {} + uiType: Group + validate: {} +- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` + (1 CPU core) + jsonKey: cpu + label: Cpu + sort: 100 + uiType: CPUNumber + validate: {} +- description: Define arguments by using environment variables + disable: false + jsonKey: env + label: Env + sort: 100 + subParameterGroupOption: + - - name + - value + - - name + - valueFrom + subParameters: + - description: Environment variable name + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + - description: The value of the environment variable + jsonKey: value + label: Value + sort: 100 + uiType: Input + validate: {} + - description: Specifies a source the value of this var should come from + disable: false + jsonKey: valueFrom + label: Secret选择器 + sort: 100 + subParameters: + - description: Selects a key of a secret in the pod's namespace + jsonKey: secretKeyRef + label: SecretKeyRef + sort: 100 + subParameters: + - description: The key of the secret to select from. Must be a valid secret + key + jsonKey: key + label: SecretKey选择 + sort: 100 + uiType: SecretKeySelect + validate: + required: true + - description: The name of the secret in the pod's namespace to select from + jsonKey: name + label: Secret选择 + sort: 100 + uiType: SecretSelect + validate: + required: true + uiType: Ignore + validate: + required: true + uiType: InnerGroup + validate: {} + uiType: Structs + validate: {} +- description: If addRevisionLabel is true, the appRevision label will be added to + the underlying pods + jsonKey: addRevisionLabel + label: AddRevisionLabel + sort: 100 + uiType: Switch + validate: + defaultValue: false + required: true +- description: Commands to run in the container + jsonKey: cmd + label: Cmd + sort: 100 + uiType: Strings + validate: {} +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + sort: 100 + uiType: Strings + validate: {} +- description: Instructions for assessing whether the container is alive. + jsonKey: livenessProbe + label: LivenessProbe + sort: 100 + subParameters: + - description: Instructions for assessing container health by executing an HTTP + GET request. Either this attribute or the exec attribute or the tcpSocket attribute + MUST be specified. This attribute is mutually exclusive with both the exec attribute + and the tcpSocket attribute. + jsonKey: httpGet + label: HttpGet + sort: 100 + subParameters: + - description: "" + jsonKey: httpHeaders + label: HttpHeaders + sort: 100 + subParameters: + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: value + label: Value + sort: 100 + uiType: Input + validate: + required: true + uiType: Structs + validate: {} + - description: The endpoint, relative to the port, to which the HTTP GET request + should be directed. + jsonKey: path + label: Path + sort: 100 + uiType: Input + validate: + required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} + - description: Number of seconds after the container is started before the first + probe is initiated. + jsonKey: initialDelaySeconds + label: InitialDelaySeconds + sort: 100 + uiType: Number + validate: + defaultValue: 0 + required: true + - description: How often, in seconds, to execute the probe. + jsonKey: periodSeconds + label: PeriodSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 10 + required: true + - description: Minimum consecutive successes for the probe to be considered successful + after having failed. + jsonKey: successThreshold + label: SuccessThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} + - description: Number of seconds after which the probe times out. + jsonKey: timeoutSeconds + label: TimeoutSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by executing a command. + Either this attribute or the httpGet attribute or the tcpSocket attribute MUST + be specified. This attribute is mutually exclusive with both the httpGet attribute + and the tcpSocket attribute. + jsonKey: exec + label: Exec + sort: 100 + subParameters: + - description: A command to be executed inside the container to assess its health. + Each space delimited token of the command is a separate array element. Commands + exiting 0 are considered to be successful probes, whilst all other exit codes + are considered failures. + jsonKey: command + label: Command + sort: 100 + uiType: Strings + validate: + required: true + uiType: KV + validate: {} + - description: Number of consecutive failures required to determine the container + is not alive (liveness probe) or not ready (readiness probe). + jsonKey: failureThreshold + label: FailureThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 3 + required: true uiType: KV - validete: {} + validate: {} +- description: Which port do you want customer traffic sent to + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + defaultValue: 80 + required: true - description: Declare volumes and volumeMounts jsonKey: volumes label: Volumes sort: 100 subParameters: - description: "" - jsonKey: volumes.[].mountPath + jsonKey: mountPath label: MountPath sort: 100 uiType: Input - validete: + validate: required: true - description: "" - jsonKey: volumes.[].name + jsonKey: name label: Name sort: 100 uiType: Input - validete: + validate: required: true - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' - jsonKey: volumes.[].type + jsonKey: type label: Type sort: 100 uiType: Select - validete: + validate: options: - label: Pvc value: pvc @@ -353,76 +426,4 @@ value: emptyDir required: true uiType: Structs - validete: {} -- description: If addRevisionLabel is true, the appRevision label will be added to - the underlying pods - jsonKey: addRevisionLabel - label: AddRevisionLabel - sort: 100 - uiType: Switch - validete: - defaultValue: false - required: true -- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` - (1 CPU core) - jsonKey: cpu - label: Cpu - sort: 100 - uiType: CPUNumber - validete: {} -- description: Define arguments by using environment variables - disable: false - jsonKey: env - label: Env - sort: 100 - subParameterGroupOption: - - - env.name - - env.value - - - env.name - - env.valueFrom - subParameters: - - description: The value of the environment variable - jsonKey: env.[].value - label: Value - sort: 100 - uiType: Input - validete: {} - - description: Specifies a source the value of this var should come from - jsonKey: env.[].valueFrom - label: ValueFrom - sort: 100 - subParameters: - - description: Selects a key of a secret in the pod's namespace - jsonKey: env.[].valueFrom.secretKeyRef - label: SecretKeyRef - sort: 100 - subParameters: - - description: The key of the secret to select from. Must be a valid secret - key - jsonKey: env.[].valueFrom.secretKeyRef.key - label: Key - sort: 100 - uiType: Input - validete: - required: true - - description: The name of the secret in the pod's namespace to select from - jsonKey: env.[].valueFrom.secretKeyRef.name - label: Name - sort: 100 - uiType: Input - validete: - required: true - uiType: KV - validete: - required: true - uiType: KV - validete: {} - - description: Environment variable name - jsonKey: env.[].name - label: Name - sort: 100 - uiType: Input - validete: - required: true - uiType: Structs - validete: {} + validate: {} From 3505c379d25de5a4fb1be0336ebc94955df10009 Mon Sep 17 00:00:00 2001 From: Hongchao Deng Date: Mon, 8 Nov 2021 23:39:36 -0500 Subject: [PATCH 26/59] Fix: cache IsExpired() (#2669) --- pkg/apiserver/rest/utils/cache.go | 2 +- pkg/apiserver/rest/utils/cache_test.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 pkg/apiserver/rest/utils/cache_test.go diff --git a/pkg/apiserver/rest/utils/cache.go b/pkg/apiserver/rest/utils/cache.go index a3d6da740..616e0db92 100644 --- a/pkg/apiserver/rest/utils/cache.go +++ b/pkg/apiserver/rest/utils/cache.go @@ -32,7 +32,7 @@ func NewMemoryCache(data interface{}, cacheDuration time.Duration) *MemoryCache // IsExpired whether the cache data expires func (m *MemoryCache) IsExpired() bool { - return time.Now().Before(m.startTime.Add(m.cacheDuration)) + return time.Now().After(m.startTime.Add(m.cacheDuration)) } // GetData get cache data diff --git a/pkg/apiserver/rest/utils/cache_test.go b/pkg/apiserver/rest/utils/cache_test.go new file mode 100644 index 000000000..222112360 --- /dev/null +++ b/pkg/apiserver/rest/utils/cache_test.go @@ -0,0 +1,15 @@ +package utils + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test cache utils", func() { + It("should return false for IsExpired()", func() { + c := NewMemoryCache("test", 10*time.Hour) + Expect(c.IsExpired()).Should(BeFalse()) + }) +}) From 7103f8ff52a863c3fb1aa332d6260fa91762fbdb Mon Sep 17 00:00:00 2001 From: Somefive Date: Tue, 9 Nov 2021 13:06:55 +0800 Subject: [PATCH 27/59] Feat: merge master into apiserver (#2660) * Feat(rollout): fill rolloutBatches if empty when scale up/down (#2569) * Feat: fill rolloutBatches if empty * Fix: fix unit-test * Test: add more test Fix: lint Fix: fix lint * Update release.yml (#2537) * Feat: add registry, merge registry and cap center (#2528) * Feat: add registry command * Refactor: comp/trait command combine with registry * Feat: refactor `vela comp/trait` * Fix: import * Fix: fix if type is autodetects.core.oam.dev * Fix: fix list from url * Fix: test * Feat: add test * Fix: remove dup test * Fix: test * Fix: test * Fix: fix label filter * Fix: reviewable * Fix test * fix personal repo in test * Fix test * Fix test * add some boundary check * reviewable * Fix: fix nocalhost trait (#2577) * fix incorrect addon status (#2576) * Fix(cli): client-side throttling in vela CLI (#2581) * fix cli throttling * fix import * set to a lower value * remove addon with no defs (#2574) * Feat: vela logs support multicluster (#2593) * Feat: add basic multiple cluster logs * fix context * Fix select style * Fix select style * remove useless env * fix naming * Feat: vela cluster support use ocm to join/list/detach cluster (#2599) * Feat: add render component and apply component remaining (#2587) * Feat: add render component and apply component remaining * fix ut * fix e2e * allow import package in custom status cue template (#2585) Co-authored-by: chwetion * Fix: abnormal aux name (#2612) * Feat: store workflow step def properties in cm (#2592) * Fix: fix notification def * Feat: store workflow step def properties in cm * fix ci * fix data race * Fix: change Initializer to Application for addon Observability (#2615) In this doc, updated the Observability implementation from initializer to Application. I also store definitions as it's not well stored in vela-templates/addons/observability * Fix: fix backport param (#2611) * Fix: add owner reference in workflow context cm (#2573) * Fix: add owner reference in workflow context cm * fix ci * delete useless test case * Fix: op.delete bugs (#2622) * Fix: op.delete some bugs * Fix: app status update error Fix: make reviewable * Fix: show reconcile error log (#2626) * Feat: add reconcile timeout configuration for vela-core (#2630) * Fix: patch status retry while conflict happens (#2629) * Fix: allow definition schema cm can be same name in different definition type (#2618) * Fix: fix definition schema cm name * fix ut * fix ut * fix show * add switch default case * Feat: remove envbinding policy into workflow (#2556) Fix: add more test * Feat: add vela prob to test cluster (#2635) * Fix: upgrade stern lib to avoid panic for vela logs (#2650) * Fix: filter loggable workload in vela logs (#2651) * Fix: filter loggable workload in vela logs * reviewable * Feat: add vela exec for multi cluster (#2299) fix support vela exec * Fix: health check will check for multiclusters (#2645) * Fix: minor fix for vela cli printing (#2655) * Fix: minor fix for vela cli printing * add dockerfile go mod cache * Feat: support apiserver-related multicluster features (#2625) * Feat: remove envbinding policy into workflow Feat: add support for env change (env gc) Fix: fix rollout timeout setting bug * Feat: support disable trait and env without workflow * Fix: add hint for replaced value Co-authored-by: wyike Co-authored-by: basefas Co-authored-by: qiaozp <47812250+chivalryq@users.noreply.github.com> Co-authored-by: Tianxin Dong Co-authored-by: yangsoon Co-authored-by: Chwetion <137953601@qq.com> Co-authored-by: chwetion Co-authored-by: Jian.Li <74582607+leejanee@users.noreply.github.com> Co-authored-by: Zheng Xi Zhou Co-authored-by: Jianbo Sun --- .github/workflows/issue-commands.yml | 7 +- .github/workflows/release.yml | 183 +-- Dockerfile | 4 + Makefile | 2 + apis/core.oam.dev/common/types.go | 11 + .../common/zz_generated.deepcopy.go | 27 + .../core.oam.dev/v1alpha1/envbinding_types.go | 172 ++- apis/core.oam.dev/v1alpha1/register.go | 11 - .../v1alpha1/zz_generated.deepcopy.go | 208 ++-- .../v1beta1/workflow_step_definition.go | 3 +- apis/types/types.go | 7 + .../core.oam.dev_applicationrevisions.yaml | 38 + .../crds/core.oam.dev_applications.yaml | 34 + .../core.oam.dev_definitionrevisions.yaml | 4 + .../core.oam.dev_workflowstepdefinitions.yaml | 4 + .../addons/terraform-provider-alibaba.yaml | 2 +- .../addons/terraform-provider-aws.yaml | 2 +- .../addons/terraform-provider-azure.yaml | 2 +- .../templates/defwithtemplate/nocalhost.yaml | 56 +- .../templates/defwithtemplate/rollout.yaml | 8 +- .../defwithtemplate/webhook-notification.yaml | 18 +- .../core.oam.dev_applicationrevisions.yaml | 38 + .../crds/core.oam.dev_applications.yaml | 34 + .../core.oam.dev_definitionrevisions.yaml | 4 + .../core.oam.dev_workflowstepdefinitions.yaml | 4 + .../templates/defwithtemplate/nocalhost.yaml | 56 +- .../templates/defwithtemplate/rollout.yaml | 8 +- .../defwithtemplate/webhook-notification.yaml | 18 +- cmd/core/main.go | 5 +- .../api/vela-controller-params-reference.md | 3 +- .../nocalhost/app-bookinfo-authors.yaml | 17 + .../{nocalhost.yaml => app-bookinfo.yaml} | 69 +- .../application-observability.yaml | 130 +++ .../trait-import-grafana-dashboard.yaml | 31 + .../definitions/trait-pure-ingress.yaml | 58 + .../trait-register-grafana-datasource.yaml | 42 + .../initializer-observability.yaml | 159 --- e2e/application/application_test.go | 2 +- e2e/capability/capability_test.go | 153 --- e2e/plugin/plugin_test.go | 56 - .../registry_suite_test.go} | 2 +- e2e/registry/registry_test.go | 149 +++ go.mod | 6 +- go.sum | 45 +- .../core.oam.dev_applicationrevisions.yaml | 38 + .../crds/core.oam.dev_applications.yaml | 34 + .../core.oam.dev_definitionrevisions.yaml | 4 + .../core.oam.dev_workflowstepdefinitions.yaml | 4 + pkg/apiserver/rest/usecase/addon.go | 14 +- pkg/apiserver/rest/usecase/applicationplan.go | 1 + pkg/apiserver/rest/utils/bcode/addon.go | 3 + pkg/appfile/appfile.go | 55 +- pkg/appfile/appfile_test.go | 16 +- pkg/appfile/parser.go | 1 + pkg/clustermanager/cluster_manager.go | 102 ++ pkg/controller/common/context.go | 7 +- .../v1alpha1/envbinding/binding.go | 327 ------ .../envbinding/cluster_gateway_engine.go | 131 --- .../v1alpha1/envbinding/engine.go | 377 ------ .../envbinding/envbinding_controller.go | 321 ------ .../envbinding/envbinding_controller_test.go | 1024 ----------------- .../v1alpha1/envbinding/suit_test.go | 115 -- .../testdata/crds/manifestwork.yaml | 341 ------ .../envbinding/testdata/crds/placement.yaml | 288 ----- .../testdata/crds/placementdecision.yaml | 90 -- .../application/application_controller.go | 78 +- .../v1alpha2/application/apply.go | 31 +- .../v1alpha2/application/apply_test.go | 124 ++ .../v1alpha2/application/generator.go | 102 +- .../v1alpha2/application/generator_test.go | 96 +- .../v1alpha2/application/revision.go | 9 +- .../v1alpha2/applicationrollout/helper.go | 19 +- .../applicationrollout/helper_test.go | 37 +- .../componentdefinition_controller_test.go | 15 +- .../core/scopes/healthscope/healthscope.go | 2 +- .../healthscope/healthscope_controller.go | 90 +- .../traitdefinition_controller_test.go | 4 +- .../workflowstepdefinition_controller.go | 22 + .../workflowstepdefinition_controller_test.go | 197 ++++ pkg/controller/setup.go | 5 - .../v1alpha1/rollout/handler.go | 14 + .../v1alpha1/rollout/handler_suit_test.go | 45 + .../v1alpha1/rollout/rollout_controller.go | 16 + pkg/controller/utils/capability.go | 87 +- .../utils/capability_integrate_test.go | 2 +- pkg/cue/definition/template.go | 15 +- pkg/cue/definition/template_test.go | 29 +- pkg/cue/packages/package.go | 5 + pkg/cue/process/handle.go | 10 +- pkg/cue/process/handle_test.go | 10 + pkg/multicluster/cluster_management.go | 30 +- .../envbinding => multicluster}/gc.go | 70 +- pkg/multicluster/gc_test.go | 61 + pkg/oam/util/helper.go | 2 + pkg/plugin/cli/cli.go | 5 +- pkg/plugin/cli/comp.go | 75 -- pkg/plugin/cli/trait.go | 74 -- pkg/policy/envbinding/patch.go | 187 +++ .../envbinding/patch_test.go} | 177 ++- pkg/policy/envbinding/placement.go | 125 ++ pkg/policy/envbinding/placement_test.go | 133 +++ pkg/policy/envbinding/utils.go | 51 + pkg/stdlib/op.cue | 79 +- pkg/stdlib/packages.go | 5 +- pkg/stdlib/pkgs/kube.cue | 1 + pkg/stdlib/pkgs/multicluster.cue | 125 ++ pkg/stdlib/pkgs/oam.cue | 18 + pkg/stdlib/pkgs/slack.cue | 18 +- pkg/utils/common/args.go | 2 + pkg/utils/common/common.go | 106 +- pkg/velaql/view.go | 6 +- pkg/workflow/context/context.go | 47 +- pkg/workflow/context/context_test.go | 7 +- pkg/workflow/hooks/data_passing_test.go | 2 +- pkg/workflow/providers/kube/handle.go | 19 +- pkg/workflow/providers/kube/handle_test.go | 6 + .../providers/multicluster/multicluster.go | 157 +++ .../multicluster/multicluster_test.go | 479 ++++++++ pkg/workflow/providers/oam/apply.go | 103 +- pkg/workflow/providers/oam/apply_test.go | 91 +- pkg/workflow/step/generator.go | 104 ++ pkg/workflow/step/generator_test.go | 129 +++ pkg/workflow/workflow.go | 2 +- pkg/workflow/workflow_test.go | 1 + references/apis/types.go | 9 +- references/appfile/app.go | 2 +- references/cli/capability.go | 292 ----- references/cli/cli.go | 5 +- references/cli/cluster.go | 309 ++++- references/cli/components.go | 213 ++-- references/cli/exec.go | 117 +- references/cli/help.go | 5 - references/cli/logs.go | 86 +- references/cli/registry.go | 773 +++++++++++++ references/{plugins => cli}/registry_test.go | 49 +- references/cli/traits.go | 215 ++-- references/cli/traits_test.go | 2 +- references/common/application.go | 190 --- references/common/capability.go | 520 --------- references/common/component.go | 43 - references/common/registry.go | 159 +++ .../{capability_test.go => registry_test.go} | 0 references/common/trait.go | 96 -- references/plugins/local.go | 180 --- references/plugins/local_test.go | 107 -- references/plugins/references.go | 7 + test/e2e-apiserver-test/addon_test.go | 5 +- .../multicluster_rollout_test.go | 19 +- .../multicluster_test.go | 114 +- test/e2e-multicluster-test/suite_test.go | 17 +- .../example-envbinding-app-wo-workflow.yaml | 38 + .../app/example-lite-envbinding-app.yaml | 32 + test/e2e-test/helm_app_test.go | 2 +- test/e2e-test/kube_app_test.go | 2 +- test/e2e-test/rollout_trait_test.go | 109 +- .../rollout/deployment/application.yaml | 4 +- vela-templates/addons/auto-gen/keda.yaml | 25 - .../addons/auto-gen/prometheus.yaml | 26 - .../auto-gen/terraform-provider-alibaba.yaml | 2 +- .../auto-gen/terraform-provider-aws.yaml | 2 +- .../auto-gen/terraform-provider-azure.yaml | 2 +- vela-templates/addons/keda/readme.md | 3 - vela-templates/addons/keda/template.yaml | 34 - vela-templates/addons/prometheus/readme.md | 3 - .../addons/prometheus/template.yaml | 29 - .../terraform-provider-alibaba/template.yaml | 2 +- .../terraform-provider-aws/template.yaml | 2 +- .../terraform-provider-azure/template.yaml | 2 +- .../definitions/internal/nocalhost.cue | 59 +- .../definitions/internal/rollout.cue | 8 +- .../internal/webhook-notification.cue | 18 +- 171 files changed, 6332 insertions(+), 6352 deletions(-) create mode 100644 docs/examples/nocalhost/app-bookinfo-authors.yaml rename docs/examples/nocalhost/{nocalhost.yaml => app-bookinfo.yaml} (65%) create mode 100644 docs/examples/obervability/application-observability.yaml create mode 100644 docs/examples/obervability/definitions/trait-import-grafana-dashboard.yaml create mode 100644 docs/examples/obervability/definitions/trait-pure-ingress.yaml create mode 100644 docs/examples/obervability/definitions/trait-register-grafana-datasource.yaml delete mode 100644 docs/examples/obervability/initializer-observability.yaml delete mode 100644 e2e/capability/capability_test.go rename e2e/{capability/capability_suite_test.go => registry/registry_suite_test.go} (94%) create mode 100644 e2e/registry/registry_test.go delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/binding.go delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/cluster_gateway_engine.go delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/engine.go delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/envbinding_controller.go delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/envbinding_controller_test.go delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/suit_test.go delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/manifestwork.yaml delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/placement.yaml delete mode 100644 pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/placementdecision.yaml create mode 100644 pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller_test.go rename pkg/{controller/core.oam.dev/v1alpha1/envbinding => multicluster}/gc.go (55%) create mode 100644 pkg/multicluster/gc_test.go delete mode 100644 pkg/plugin/cli/comp.go delete mode 100644 pkg/plugin/cli/trait.go create mode 100644 pkg/policy/envbinding/patch.go rename pkg/{controller/core.oam.dev/v1alpha1/envbinding/binding_test.go => policy/envbinding/patch_test.go} (56%) create mode 100644 pkg/policy/envbinding/placement.go create mode 100644 pkg/policy/envbinding/placement_test.go create mode 100644 pkg/policy/envbinding/utils.go create mode 100644 pkg/stdlib/pkgs/multicluster.cue create mode 100644 pkg/workflow/providers/multicluster/multicluster.go create mode 100644 pkg/workflow/providers/multicluster/multicluster_test.go create mode 100644 pkg/workflow/step/generator.go create mode 100644 pkg/workflow/step/generator_test.go delete mode 100644 references/cli/capability.go create mode 100644 references/cli/registry.go rename references/{plugins => cli}/registry_test.go (56%) delete mode 100644 references/common/capability.go delete mode 100644 references/common/component.go create mode 100644 references/common/registry.go rename references/common/{capability_test.go => registry_test.go} (100%) create mode 100644 test/e2e-multicluster-test/testdata/app/example-envbinding-app-wo-workflow.yaml create mode 100644 test/e2e-multicluster-test/testdata/app/example-lite-envbinding-app.yaml delete mode 100644 vela-templates/addons/auto-gen/keda.yaml delete mode 100644 vela-templates/addons/auto-gen/prometheus.yaml delete mode 100644 vela-templates/addons/keda/readme.md delete mode 100644 vela-templates/addons/keda/template.yaml delete mode 100644 vela-templates/addons/prometheus/readme.md delete mode 100644 vela-templates/addons/prometheus/template.yaml diff --git a/.github/workflows/issue-commands.yml b/.github/workflows/issue-commands.yml index 8bd0e400f..95d491629 100644 --- a/.github/workflows/issue-commands.yml +++ b/.github/workflows/issue-commands.yml @@ -45,7 +45,12 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const version = process.env.VERSION - const label = "backport release-" + version + var label string + if (version.includes("release")) { + label = "backport version" + } else { + label = "backport release-" + version + } // Add our backport label. github.issues.addLabels({ // Every pull request is an issue, but not every issue is a pull request. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ecd3ed045..07cb202c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,133 +4,136 @@ on: push: tags: - "v*" - workflow_dispatch: {} + workflow_dispatch: { } + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: - publish-cli: + build: runs-on: ubuntu-latest + name: build + strategy: + matrix: + TARGETS: [ linux/amd64, darwin/amd64, windows/amd64, linux/arm64, darwin/arm64 ] env: - VELA_VERSION: ${{ github.ref }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VELA_VERSION_KEY: github.com/oam-dev/kubevela/version.VelaVersion + VELA_GITVERSION_KEY: github.com/oam-dev/kubevela/version.GitRevision + GO_BUILD_ENV: GO111MODULE=on CGO_ENABLED=0 + DIST_DIRS: find * -type d -exec steps: + - name: Checkout + uses: actions/checkout@v2 - name: Set up Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v2 with: go-version: 1.16 - id: go - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} - - name: Tag helm chart image - run: | - sed -i 's/latest/${{ steps.get_version.outputs.VERSION }}/g' charts/vela-core/values.yaml - sed -i 's/0.1.0/${{ steps.get_version.outputs.VERSION }}/g' charts/vela-core/Chart.yaml - - name: Run cross-build - run: make cross-build - - name: Run compress binary - run: make compress - name: Get release id: get_release uses: bruceadams/get-release@v1.2.2 - - name: Upload Vela Linux amd64 tar.gz + - name: Get version + run: echo "VELA_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + - name: Get matrix + id: get_matrix + run: | + TARGETS=${{matrix.TARGETS}} + echo ::set-output name=OS::${TARGETS%/*} + echo ::set-output name=ARCH::${TARGETS#*/} + - name: Get ldflags + id: get_ldflags + run: | + LDFLAGS="-s -w -X ${{ env.VELA_VERSION_KEY }}=${{ env.VELA_VERSION }} -X ${{ env.VELA_GITVERSION_KEY }}=git-$(git rev-parse --short HEAD)" + echo "LDFLAGS=${LDFLAGS}" >> $GITHUB_ENV + - name: Build + run: | + ${{ env.GO_BUILD_ENV }} GOOS=${{ steps.get_matrix.outputs.OS }} GOARCH=${{ steps.get_matrix.outputs.ARCH }} \ + go build -ldflags "${{ env.LDFLAGS }}" \ + -o _bin/vela/${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}/vela -v \ + ./references/cmd/cli/main.go + ${{ env.GO_BUILD_ENV }} GOOS=${{ steps.get_matrix.outputs.OS }} GOARCH=${{ steps.get_matrix.outputs.ARCH }} \ + go build -ldflags "${{ env.LDFLAGS }}" \ + -o _bin/kubectl-vela/${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}/kubectl-vela -v \ + ./cmd/plugin/main.go + - name: Compress + run: | + echo "\n## Release Info\nVERSION: ${{ env.VELA_VERSION }}" >> README.md && \ + echo "GIT_COMMIT: ${GITHUB_SHA}\n" >> README.md && \ + cd _bin/vela && \ + ${{ env.DIST_DIRS }} cp ../../LICENSE {} \; && \ + ${{ env.DIST_DIRS }} cp ../../README.md {} \; && \ + ${{ env.DIST_DIRS }} tar -zcf vela-{}.tar.gz {} \; && \ + ${{ env.DIST_DIRS }} zip -r vela-{}.zip {} \; && \ + cd ../kubectl-vela && \ + ${{ env.DIST_DIRS }} cp ../../LICENSE {} \; && \ + ${{ env.DIST_DIRS }} cp ../../README.md {} \; && \ + ${{ env.DIST_DIRS }} tar -zcf kubectl-vela-{}.tar.gz {} \; && \ + ${{ env.DIST_DIRS }} zip -r kubectl-vela-{}.zip {} \; && \ + cd .. && \ + sha256sum vela/vela-* kubectl-vela/kubectl-vela-* >> sha256-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.txt \ + - name: Upload Vela tar.gz uses: actions/upload-release-asset@v1.0.2 with: upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/vela/vela-linux-amd64.tar.gz - asset_name: vela-${{ steps.get_version.outputs.VERSION }}-linux-amd64.tar.gz + asset_path: ./_bin/vela/vela-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.tar.gz + asset_name: vela-${{ env.VELA_VERSION }}-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.tar.gz asset_content_type: binary/octet-stream - - name: Upload Vela Linux amd64 zip + - name: Upload Vela zip uses: actions/upload-release-asset@v1.0.2 with: upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/vela/vela-linux-amd64.zip - asset_name: vela-${{ steps.get_version.outputs.VERSION }}-linux-amd64.zip + asset_path: ./_bin/vela/vela-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.zip + asset_name: vela-${{ env.VELA_VERSION }}-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.zip asset_content_type: binary/octet-stream - - name: Upload Vela MacOS tar.gz + - name: Upload Kubectl-Vela tar.gz uses: actions/upload-release-asset@v1.0.2 with: upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/vela/vela-darwin-amd64.tar.gz - asset_name: vela-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.tar.gz + asset_path: ./_bin/kubectl-vela/kubectl-vela-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.tar.gz + asset_name: kubectl-vela-${{ env.VELA_VERSION }}-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.tar.gz asset_content_type: binary/octet-stream - - name: Upload Vela MacOS zip + - name: Upload Kubectl-Vela zip uses: actions/upload-release-asset@v1.0.2 with: upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/vela/vela-darwin-amd64.zip - asset_name: vela-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.zip + asset_path: ./_bin/kubectl-vela/kubectl-vela-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.zip + asset_name: kubectl-vela-${{ env.VELA_VERSION }}-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.zip asset_content_type: binary/octet-stream - - name: Upload Vela Windows tar.gz - uses: actions/upload-release-asset@v1.0.2 + - name: Post sha256 + uses: actions/upload-artifact@v2 with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/vela/vela-windows-amd64.tar.gz - asset_name: vela-${{ steps.get_version.outputs.VERSION }}-windows-amd64.tar.gz - asset_content_type: binary/octet-stream - - name: Upload Vela Windows zip - uses: actions/upload-release-asset@v1.0.2 + name: sha256sums + path: ./_bin/sha256-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.txt + retention-days: 1 + + upload-sha256sums-plugin-homebrew: + needs: build + runs-on: ubuntu-latest + name: upload-sha256sums + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Get release + id: get_release + uses: bruceadams/get-release@v1.2.2 + - name: Download sha256sums + uses: actions/download-artifact@v2 with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/vela/vela-windows-amd64.zip - asset_name: vela-${{ steps.get_version.outputs.VERSION }}-windows-amd64.zip - asset_content_type: binary/octet-stream - - name: Upload Kubectl-Vela Linux amd64 tar.gz - uses: actions/upload-release-asset@v1.0.2 - with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/kubectl-vela/kubectl-vela-linux-amd64.tar.gz - asset_name: kubectl-vela-${{ steps.get_version.outputs.VERSION }}-linux-amd64.tar.gz - asset_content_type: binary/octet-stream - - name: Upload Kubectl-Vela Linux amd64 zip - uses: actions/upload-release-asset@v1.0.2 - with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/kubectl-vela/kubectl-vela-linux-amd64.zip - asset_name: kubectl-vela-${{ steps.get_version.outputs.VERSION }}-linux-amd64.zip - asset_content_type: binary/octet-stream - - name: Upload Kubectl-Vela MacOS tar.gz - uses: actions/upload-release-asset@v1.0.2 - with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/kubectl-vela/kubectl-vela-darwin-amd64.tar.gz - asset_name: kubectl-vela-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.tar.gz - asset_content_type: binary/octet-stream - - name: Upload Kubectl-Vela MacOS zip - uses: actions/upload-release-asset@v1.0.2 - with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/kubectl-vela/kubectl-vela-darwin-amd64.zip - asset_name: kubectl-vela-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.zip - asset_content_type: binary/octet-stream - - name: Upload Kubectl-Vela Windows tar.gz - uses: actions/upload-release-asset@v1.0.2 - with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/kubectl-vela/kubectl-vela-windows-amd64.tar.gz - asset_name: kubectl-vela-${{ steps.get_version.outputs.VERSION }}-windows-amd64.tar.gz - asset_content_type: binary/octet-stream - - name: Upload Kubectl-Vela Windows zip - uses: actions/upload-release-asset@v1.0.2 - with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/kubectl-vela/kubectl-vela-windows-amd64.zip - asset_name: kubectl-vela-${{ steps.get_version.outputs.VERSION }}-windows-amd64.zip - asset_content_type: binary/octet-stream + name: sha256sums + - shell: bash + run: | + for file in * + do + cat ${file} >> sha256sums.txt + done - name: Upload Checksums uses: actions/upload-release-asset@v1.0.2 with: upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./_bin/sha256sums.txt + asset_path: sha256sums.txt asset_name: sha256sums.txt asset_content_type: text/plain - name: Update kubectl plugin version in krew-index uses: rajatjindal/krew-release-bot@v0.0.38 - - homebrew: - runs-on: macos-latest - steps: - name: Update Homebrew formula uses: dawidd6/action-homebrew-bump-formula@v3 with: diff --git a/Dockerfile b/Dockerfile index 8a5318e68..57d77e277 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,10 @@ WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum + +# It's a proxy for CN developer, please unblock it if you have network issue +# RUN go env -w GOPROXY=https://goproxy.cn,direct + # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download diff --git a/Makefile b/Makefile index 55f28d3d7..5d324e79e 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +SHELL := /bin/bash + # Vela version VELA_VERSION ?= master # Repo info diff --git a/apis/core.oam.dev/common/types.go b/apis/core.oam.dev/common/types.go index 962a87d6f..dff120af9 100644 --- a/apis/core.oam.dev/common/types.go +++ b/apis/core.oam.dev/common/types.go @@ -304,6 +304,17 @@ type AppStatus struct { // AppliedResources record the resources that the workflow step apply. AppliedResources []ClusterObjectReference `json:"appliedResources,omitempty"` + + // PolicyStatus records the status of policy + PolicyStatus []PolicyStatus `json:"policy,omitempty"` +} + +// PolicyStatus records the status of policy +type PolicyStatus struct { + Name string `json:"name"` + Type string `json:"type"` + // +kubebuilder:pruning:PreserveUnknownFields + Status *runtime.RawExtension `json:"status,omitempty"` } // WorkflowStatus record the status of workflow diff --git a/apis/core.oam.dev/common/zz_generated.deepcopy.go b/apis/core.oam.dev/common/zz_generated.deepcopy.go index 766c22b1e..370bb17bf 100644 --- a/apis/core.oam.dev/common/zz_generated.deepcopy.go +++ b/apis/core.oam.dev/common/zz_generated.deepcopy.go @@ -83,6 +83,13 @@ func (in *AppStatus) DeepCopyInto(out *AppStatus) { *out = make([]ClusterObjectReference, len(*in)) copy(*out, *in) } + if in.PolicyStatus != nil { + in, out := &in.PolicyStatus, &out.PolicyStatus + *out = make([]PolicyStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppStatus. @@ -401,6 +408,26 @@ func (in *KubeParameter) DeepCopy() *KubeParameter { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicyStatus) DeepCopyInto(out *PolicyStatus) { + *out = *in + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyStatus. +func (in *PolicyStatus) DeepCopy() *PolicyStatus { + if in == nil { + return nil + } + out := new(PolicyStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RawComponent) DeepCopyInto(out *RawComponent) { *out = *in diff --git a/apis/core.oam.dev/v1alpha1/envbinding_types.go b/apis/core.oam.dev/v1alpha1/envbinding_types.go index 5e7b1b97f..25a414c42 100644 --- a/apis/core.oam.dev/v1alpha1/envbinding_types.go +++ b/apis/core.oam.dev/v1alpha1/envbinding_types.go @@ -17,48 +17,62 @@ package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - "github.com/oam-dev/kubevela/apis/core.oam.dev/condition" ) -// ClusterManagementEngine represents a multi-cluster management solution -type ClusterManagementEngine string - const ( - // OCMEngine represents Open-Cluster-Management multi-cluster management solution - OCMEngine ClusterManagementEngine = "ocm" - - // SingleClusterEngine represents single cluster ClusterManagerEngine - SingleClusterEngine ClusterManagementEngine = "single-cluster" - - // ClusterGatewayEngine represents multi-cluster management solution with cluster-gateway - ClusterGatewayEngine ClusterManagementEngine = "cluster-gateway" + // EnvBindingPolicyType refers to the type of EnvBinding + EnvBindingPolicyType = "env-binding" ) -// EnvBindingPhase is a label for the condition of a EnvBinding at the current time -type EnvBindingPhase string +// EnvTraitPatch is the patch to trait +type EnvTraitPatch struct { + Type string `json:"type"` + Properties *runtime.RawExtension `json:"properties,omitempty"` + Disable bool `json:"disable,omitempty"` +} -const ( - // EnvBindingPrepare means EnvBinding is preparing the pre-work for cluster scheduling - EnvBindingPrepare EnvBindingPhase = "preparing" +// ToApplicationTrait convert EnvTraitPatch into ApplicationTrait +func (in *EnvTraitPatch) ToApplicationTrait() *common.ApplicationTrait { + out := &common.ApplicationTrait{Type: in.Type} + if in.Properties != nil { + out.Properties = in.Properties.DeepCopy() + } + return out +} - // EnvBindingRendering means EnvBinding is rendering the apps in different envs - EnvBindingRendering EnvBindingPhase = "rendering" +// EnvComponentPatch is the patch to component +type EnvComponentPatch struct { + Name string `json:"name"` + Type string `json:"type"` + Properties *runtime.RawExtension `json:"properties,omitempty"` + Traits []EnvTraitPatch `json:"traits,omitempty"` +} - // EnvBindingScheduling means EnvBinding is deciding which cluster the apps is scheduled to. - EnvBindingScheduling EnvBindingPhase = "scheduling" - - // EnvBindingFinished means EnvBinding finished env binding - EnvBindingFinished EnvBindingPhase = "finished" -) +// ToApplicationComponent convert EnvComponentPatch into ApplicationComponent +func (in *EnvComponentPatch) ToApplicationComponent() *common.ApplicationComponent { + out := &common.ApplicationComponent{ + Name: in.Name, + Type: in.Type, + } + if in.Properties != nil { + out.Properties = in.Properties.DeepCopy() + } + if in.Traits != nil { + for _, trait := range in.Traits { + if !trait.Disable { + out.Traits = append(out.Traits, *trait.ToApplicationTrait()) + } + } + } + return out +} // EnvPatch specify the parameter configuration for different environments type EnvPatch struct { - Components []common.ApplicationComponent `json:"components"` + Components []EnvComponentPatch `json:"components,omitempty"` } // NamespaceSelector defines the rules to select a Namespace resource. @@ -86,90 +100,34 @@ type EnvConfig struct { Name string `json:"name"` Placement EnvPlacement `json:"placement,omitempty"` Selector *EnvSelector `json:"selector,omitempty"` - Patch EnvPatch `json:"patch"` + Patch EnvPatch `json:"patch,omitempty"` } -// AppTemplate represents a application to be configured. -type AppTemplate struct { - // +kubebuilder:validation:EmbeddedResource - // +kubebuilder:pruning:PreserveUnknownFields - runtime.RawExtension `json:",inline"` -} - -// ClusterDecision recorded the mapping of environment and cluster -type ClusterDecision struct { - Env string `json:"env"` - Cluster string `json:"cluster,omitempty"` - Namespace string `json:"namespace,omitempty"` -} - -// A ConfigMapReference is a reference to a configMap in an arbitrary namespace. -type ConfigMapReference struct { - // Name of the secret. - Name string `json:"name"` - - // Namespace of the secret. - Namespace string `json:"namespace,omitempty"` -} - -// A EnvBindingSpec defines the desired state of a EnvBinding. +// EnvBindingSpec defines a list of envs type EnvBindingSpec struct { - Engine ClusterManagementEngine `json:"engine,omitempty"` - - // AppTemplate indicates the application template. - AppTemplate AppTemplate `json:"appTemplate"` - Envs []EnvConfig `json:"envs"` - - // OutputResourcesTo specifies the namespace and name of a ConfigMap - // which store the resources rendered after differentiated configuration - // +optional - OutputResourcesTo *ConfigMapReference `json:"outputResourcesTo,omitempty"` } -// A EnvBindingStatus is the status of EnvBinding +// PlacementDecision describes the placement of one application instance +type PlacementDecision struct { + Cluster string `json:"cluster"` + Namespace string `json:"namespace"` +} + +// EnvStatus records the status of one env +type EnvStatus struct { + Env string `json:"env"` + Placements []PlacementDecision `json:"placements"` +} + +// ClusterConnection records the connection with clusters and the last active app revision when they are active (still be used) +type ClusterConnection struct { + ClusterName string `json:"clusterName"` + LastActiveRevision string `json:"lastActiveRevision"` +} + +// EnvBindingStatus records the status of all env type EnvBindingStatus struct { - // ConditionedStatus reflects the observed status of a resource - condition.ConditionedStatus `json:",inline"` - - Phase EnvBindingPhase `json:"phase,omitempty"` - - ClusterDecisions []ClusterDecision `json:"clusterDecisions,omitempty"` - - // ResourceTracker record the status of the ResourceTracker - ResourceTracker *corev1.ObjectReference `json:"resourceTracker,omitempty"` -} - -// EnvBinding is the Schema for the EnvBinding API -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:resource:scope=Namespaced,categories={oam},shortName=envbind -// +kubebuilder:printcolumn:name="ENGINE",type=string,JSONPath=`.spec.engine` -// +kubebuilder:printcolumn:name="PHASE",type=string,JSONPath=`.status.phase` -// +kubebuilder:printcolumn:name="AGE",type=date,JSONPath=".metadata.creationTimestamp" -type EnvBinding struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec EnvBindingSpec `json:"spec,omitempty"` - Status EnvBindingStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true - -// EnvBindingList contains a list of EnvBinding. -type EnvBindingList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []EnvBinding `json:"items"` -} - -// SetConditions set condition for EnvBinding -func (e *EnvBinding) SetConditions(c ...condition.Condition) { - e.Status.SetConditions(c...) -} - -// GetCondition gets condition from EnvBinding -func (e *EnvBinding) GetCondition(conditionType condition.ConditionType) condition.Condition { - return e.Status.GetCondition(conditionType) + Envs []EnvStatus `json:"envs"` + ClusterConnections []ClusterConnection `json:"clusterConnections"` } diff --git a/apis/core.oam.dev/v1alpha1/register.go b/apis/core.oam.dev/v1alpha1/register.go index 242997da8..26068c738 100644 --- a/apis/core.oam.dev/v1alpha1/register.go +++ b/apis/core.oam.dev/v1alpha1/register.go @@ -17,8 +17,6 @@ package v1alpha1 import ( - "reflect" - "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) @@ -37,14 +35,5 @@ var ( SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} ) -// EnvBinding type metadata. -var ( - EnvBindingKind = reflect.TypeOf(EnvBinding{}).Name() - EnvBindingGroupKind = schema.GroupKind{Group: Group, Kind: EnvBindingKind}.String() - EnvBindingKindAPIVersion = EnvBindingKind + "." + SchemeGroupVersion.String() - EnvBindingKindVersionKind = SchemeGroupVersion.WithKind(EnvBindingKind) -) - func init() { - SchemeBuilder.Register(&EnvBinding{}, &EnvBindingList{}) } diff --git a/apis/core.oam.dev/v1alpha1/zz_generated.deepcopy.go b/apis/core.oam.dev/v1alpha1/zz_generated.deepcopy.go index 8919c4b80..6fa9501f6 100644 --- a/apis/core.oam.dev/v1alpha1/zz_generated.deepcopy.go +++ b/apis/core.oam.dev/v1alpha1/zz_generated.deepcopy.go @@ -21,121 +21,29 @@ limitations under the License. package v1alpha1 import ( - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AppTemplate) DeepCopyInto(out *AppTemplate) { - *out = *in - in.RawExtension.DeepCopyInto(&out.RawExtension) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppTemplate. -func (in *AppTemplate) DeepCopy() *AppTemplate { - if in == nil { - return nil - } - out := new(AppTemplate) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterDecision) DeepCopyInto(out *ClusterDecision) { +func (in *ClusterConnection) DeepCopyInto(out *ClusterConnection) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterDecision. -func (in *ClusterDecision) DeepCopy() *ClusterDecision { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterConnection. +func (in *ClusterConnection) DeepCopy() *ClusterConnection { if in == nil { return nil } - out := new(ClusterDecision) + out := new(ClusterConnection) in.DeepCopyInto(out) return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConfigMapReference) DeepCopyInto(out *ConfigMapReference) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapReference. -func (in *ConfigMapReference) DeepCopy() *ConfigMapReference { - if in == nil { - return nil - } - out := new(ConfigMapReference) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *EnvBinding) DeepCopyInto(out *EnvBinding) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvBinding. -func (in *EnvBinding) DeepCopy() *EnvBinding { - if in == nil { - return nil - } - out := new(EnvBinding) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *EnvBinding) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *EnvBindingList) DeepCopyInto(out *EnvBindingList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]EnvBinding, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvBindingList. -func (in *EnvBindingList) DeepCopy() *EnvBindingList { - if in == nil { - return nil - } - out := new(EnvBindingList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *EnvBindingList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvBindingSpec) DeepCopyInto(out *EnvBindingSpec) { *out = *in - in.AppTemplate.DeepCopyInto(&out.AppTemplate) if in.Envs != nil { in, out := &in.Envs, &out.Envs *out = make([]EnvConfig, len(*in)) @@ -143,11 +51,6 @@ func (in *EnvBindingSpec) DeepCopyInto(out *EnvBindingSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.OutputResourcesTo != nil { - in, out := &in.OutputResourcesTo, &out.OutputResourcesTo - *out = new(ConfigMapReference) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvBindingSpec. @@ -163,16 +66,17 @@ func (in *EnvBindingSpec) DeepCopy() *EnvBindingSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvBindingStatus) DeepCopyInto(out *EnvBindingStatus) { *out = *in - in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus) - if in.ClusterDecisions != nil { - in, out := &in.ClusterDecisions, &out.ClusterDecisions - *out = make([]ClusterDecision, len(*in)) - copy(*out, *in) + if in.Envs != nil { + in, out := &in.Envs, &out.Envs + *out = make([]EnvStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - if in.ResourceTracker != nil { - in, out := &in.ResourceTracker, &out.ResourceTracker - *out = new(v1.ObjectReference) - **out = **in + if in.ClusterConnections != nil { + in, out := &in.ClusterConnections, &out.ClusterConnections + *out = make([]ClusterConnection, len(*in)) + copy(*out, *in) } } @@ -186,6 +90,33 @@ func (in *EnvBindingStatus) DeepCopy() *EnvBindingStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvComponentPatch) DeepCopyInto(out *EnvComponentPatch) { + *out = *in + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Traits != nil { + in, out := &in.Traits, &out.Traits + *out = make([]EnvTraitPatch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvComponentPatch. +func (in *EnvComponentPatch) DeepCopy() *EnvComponentPatch { + if in == nil { + return nil + } + out := new(EnvComponentPatch) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvConfig) DeepCopyInto(out *EnvConfig) { *out = *in @@ -213,7 +144,7 @@ func (in *EnvPatch) DeepCopyInto(out *EnvPatch) { *out = *in if in.Components != nil { in, out := &in.Components, &out.Components - *out = make([]common.ApplicationComponent, len(*in)) + *out = make([]EnvComponentPatch, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -275,6 +206,46 @@ func (in *EnvSelector) DeepCopy() *EnvSelector { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvStatus) DeepCopyInto(out *EnvStatus) { + *out = *in + if in.Placements != nil { + in, out := &in.Placements, &out.Placements + *out = make([]PlacementDecision, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvStatus. +func (in *EnvStatus) DeepCopy() *EnvStatus { + if in == nil { + return nil + } + out := new(EnvStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvTraitPatch) DeepCopyInto(out *EnvTraitPatch) { + *out = *in + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvTraitPatch. +func (in *EnvTraitPatch) DeepCopy() *EnvTraitPatch { + if in == nil { + return nil + } + out := new(EnvTraitPatch) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespaceSelector) DeepCopyInto(out *NamespaceSelector) { *out = *in @@ -296,3 +267,18 @@ func (in *NamespaceSelector) DeepCopy() *NamespaceSelector { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementDecision) DeepCopyInto(out *PlacementDecision) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementDecision. +func (in *PlacementDecision) DeepCopy() *PlacementDecision { + if in == nil { + return nil + } + out := new(PlacementDecision) + in.DeepCopyInto(out) + return out +} diff --git a/apis/core.oam.dev/v1beta1/workflow_step_definition.go b/apis/core.oam.dev/v1beta1/workflow_step_definition.go index a2518fd72..10aac8254 100644 --- a/apis/core.oam.dev/v1beta1/workflow_step_definition.go +++ b/apis/core.oam.dev/v1beta1/workflow_step_definition.go @@ -38,7 +38,8 @@ type WorkflowStepDefinitionSpec struct { type WorkflowStepDefinitionStatus struct { // ConditionedStatus reflects the observed status of a resource condition.ConditionedStatus `json:",inline"` - + // ConfigMapRef refer to a ConfigMap which contains OpenAPI V3 JSON schema of Component parameters. + ConfigMapRef string `json:"configMapRef,omitempty"` // LatestRevision of the component definition // +optional LatestRevision *common.Revision `json:"latestRevision,omitempty"` diff --git a/apis/types/types.go b/apis/types/types.go index 4179dcfe7..fb845062a 100644 --- a/apis/types/types.go +++ b/apis/types/types.go @@ -87,3 +87,10 @@ var DefaultFilterAnnots = []string{ oam.AnnotationFilterAnnotationKeys, oam.AnnotationLastAppliedConfiguration, } + +// Cluster contains base info of cluster +type Cluster struct { + Name string + Type string + EndPoint string +} diff --git a/charts/vela-core/crds/core.oam.dev_applicationrevisions.yaml b/charts/vela-core/crds/core.oam.dev_applicationrevisions.yaml index 66f8af181..7d38bf9f4 100644 --- a/charts/vela-core/crds/core.oam.dev_applicationrevisions.yaml +++ b/charts/vela-core/crds/core.oam.dev_applicationrevisions.yaml @@ -622,6 +622,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -2778,6 +2795,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -4431,6 +4465,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains + OpenAPI V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/charts/vela-core/crds/core.oam.dev_applications.yaml b/charts/vela-core/crds/core.oam.dev_applications.yaml index 18d952e62..a18b2dba4 100644 --- a/charts/vela-core/crds/core.oam.dev_applications.yaml +++ b/charts/vela-core/crds/core.oam.dev_applications.yaml @@ -443,6 +443,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -1250,6 +1267,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: diff --git a/charts/vela-core/crds/core.oam.dev_definitionrevisions.yaml b/charts/vela-core/crds/core.oam.dev_definitionrevisions.yaml index d09f351d3..ce9d540dd 100644 --- a/charts/vela-core/crds/core.oam.dev_definitionrevisions.yaml +++ b/charts/vela-core/crds/core.oam.dev_definitionrevisions.yaml @@ -1123,6 +1123,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains + OpenAPI V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/charts/vela-core/crds/core.oam.dev_workflowstepdefinitions.yaml b/charts/vela-core/crds/core.oam.dev_workflowstepdefinitions.yaml index 880959871..c6a0d4353 100644 --- a/charts/vela-core/crds/core.oam.dev_workflowstepdefinitions.yaml +++ b/charts/vela-core/crds/core.oam.dev_workflowstepdefinitions.yaml @@ -216,6 +216,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains OpenAPI + V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml b/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml index 7bef5dae5..33520c91b 100644 --- a/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml @@ -8,7 +8,7 @@ data: addons.oam.dev/description: Kubernetes Terraform Controller for Alibaba Cloud addons.oam.dev/name: terraform/provider-alibaba name: terraform-provider-alibaba - namespace: default + namespace: vela-system spec: components: - name: alibaba-account-creds diff --git a/charts/vela-core/templates/addons/terraform-provider-aws.yaml b/charts/vela-core/templates/addons/terraform-provider-aws.yaml index d47bc477d..4ae77de1e 100644 --- a/charts/vela-core/templates/addons/terraform-provider-aws.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-aws.yaml @@ -8,7 +8,7 @@ data: addons.oam.dev/description: Kubernetes Terraform Controller for AWS addons.oam.dev/name: terraform/provider-aws name: terraform-provider-aws - namespace: default + namespace: vela-system spec: components: - name: aws-account-creds diff --git a/charts/vela-core/templates/addons/terraform-provider-azure.yaml b/charts/vela-core/templates/addons/terraform-provider-azure.yaml index c74593283..1dc89d886 100644 --- a/charts/vela-core/templates/addons/terraform-provider-azure.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-azure.yaml @@ -8,7 +8,7 @@ data: addons.oam.dev/description: Kubernetes Terraform Controller for Azure addons.oam.dev/name: terraform/provider-azure name: terraform-provider-azure - namespace: default + namespace: vela-system spec: components: - name: azure-account-creds diff --git a/charts/vela-core/templates/defwithtemplate/nocalhost.yaml b/charts/vela-core/templates/defwithtemplate/nocalhost.yaml index fc4d1f655..5a1831cce 100644 --- a/charts/vela-core/templates/defwithtemplate/nocalhost.yaml +++ b/charts/vela-core/templates/defwithtemplate/nocalhost.yaml @@ -18,10 +18,27 @@ spec: "encoding/json" ) + outputs: nocalhostService: { + apiVersion: "v1" + kind: "Service" + metadata: name: context.name + spec: { + selector: "app.oam.dev/component": context.name + ports: [ + { + port: parameter.port + targetPort: parameter.port + }, + ] + type: "ClusterIP" + } + } patch: metadata: annotations: { "dev.nocalhost/application-name": context.appName "dev.nocalhost/application-namespace": context.namespace "dev.nocalhost": json.Marshal({ + name: context.name + serviceType: parameter.serviceType containers: [ { name: context.name @@ -29,7 +46,24 @@ spec: if parameter.gitUrl != _|_ { gitUrl: parameter.gitUrl } - image: parameter.image + if parameter.image == "go" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/golang:latest" + } + if parameter.image == "java" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/java:latest" + } + if parameter.image == "python" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/python:latest" + } + if parameter.image == "node" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/node:latest" + } + if parameter.image == "ruby" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/ruby:latest" + } + if parameter.image != "go" && parameter.image != "java" && parameter.image != "python" && parameter.image != "node" && parameter.image != "ruby" { + image: parameter.image + } shell: parameter.shell workDir: parameter.workDir if parameter.storageClass != _|_ { @@ -68,27 +102,33 @@ spec: if parameter.portForward != _|_ { portForward: parameter.portForward } + if parameter.portForward == _|_ { + portForward: ["\(parameter.port)" + ":" + "\(parameter.port)"] + } } }, ] }) } + language: "go" | "java" | "python" | "node" | "ruby" parameter: { + port: int + serviceType: *"deployment" | string gitUrl?: string - image: string + image: language | string shell: *"bash" | string workDir: *"/home/nocalhost-dev" | string storageClass?: string - command?: { - run?: [...string] - debug?: [...string] + command: { + run: *["sh", "run.sh"] | [...string] + debug: *["sh", "debug.sh"] | [...string] } debug?: remoteDebugPort?: int hotReload: *true | bool sync: { - type: *"send" | string - filePattern?: [...string] - ignoreFilePattern?: [...string] + type: *"send" | string + filePattern: *["./"] | [...string] + ignoreFilePattern: *[".git", ".vscode", ".idea", ".gradle", "build"] | [...string] } env?: [...{ name: string diff --git a/charts/vela-core/templates/defwithtemplate/rollout.yaml b/charts/vela-core/templates/defwithtemplate/rollout.yaml index 0c0c9ecca..e5c360d73 100644 --- a/charts/vela-core/templates/defwithtemplate/rollout.yaml +++ b/charts/vela-core/templates/defwithtemplate/rollout.yaml @@ -24,8 +24,10 @@ spec: componentName: context.name rolloutPlan: { rolloutStrategy: "IncreaseFirst" - rolloutBatches: parameter.rolloutBatches - targetSize: parameter.targetSize + if parameter.rolloutBatches != _|_ { + rolloutBatches: parameter.rolloutBatches + } + targetSize: parameter.targetSize if parameter["batchPartition"] != _|_ { batchPartition: parameter.batchPartition } @@ -35,7 +37,7 @@ spec: parameter: { targetRevision: *context.revision | string targetSize: int - rolloutBatches: [...rolloutBatch] + rolloutBatches?: [...rolloutBatch] batchPartition?: int } rolloutBatch: replicas: int diff --git a/charts/vela-core/templates/defwithtemplate/webhook-notification.yaml b/charts/vela-core/templates/defwithtemplate/webhook-notification.yaml index 260ec4abf..3134fed8b 100644 --- a/charts/vela-core/templates/defwithtemplate/webhook-notification.yaml +++ b/charts/vela-core/templates/defwithtemplate/webhook-notification.yaml @@ -100,17 +100,17 @@ spec: url?: string value?: string style?: string - text?: text + text?: textType confirm?: { - title: text - text: text - confirm: text - deny: text + title: textType + text: textType + confirm: textType + deny: textType style?: string } options?: [...option] initial_options?: [...option] - placeholder?: text + placeholder?: textType initial_date?: string image_url?: string alt_text?: string @@ -124,16 +124,16 @@ spec: initial_time?: string }] } - text: { + textType: { type: string text: string emoji?: bool verbatim?: bool } option: { - text: text + text: textType value: string - description?: text + description?: textType url?: string } secretRef: { diff --git a/charts/vela-minimal/crds/core.oam.dev_applicationrevisions.yaml b/charts/vela-minimal/crds/core.oam.dev_applicationrevisions.yaml index 66f8af181..7d38bf9f4 100644 --- a/charts/vela-minimal/crds/core.oam.dev_applicationrevisions.yaml +++ b/charts/vela-minimal/crds/core.oam.dev_applicationrevisions.yaml @@ -622,6 +622,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -2778,6 +2795,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -4431,6 +4465,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains + OpenAPI V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/charts/vela-minimal/crds/core.oam.dev_applications.yaml b/charts/vela-minimal/crds/core.oam.dev_applications.yaml index 18d952e62..a18b2dba4 100644 --- a/charts/vela-minimal/crds/core.oam.dev_applications.yaml +++ b/charts/vela-minimal/crds/core.oam.dev_applications.yaml @@ -443,6 +443,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -1250,6 +1267,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: diff --git a/charts/vela-minimal/crds/core.oam.dev_definitionrevisions.yaml b/charts/vela-minimal/crds/core.oam.dev_definitionrevisions.yaml index d09f351d3..ce9d540dd 100644 --- a/charts/vela-minimal/crds/core.oam.dev_definitionrevisions.yaml +++ b/charts/vela-minimal/crds/core.oam.dev_definitionrevisions.yaml @@ -1123,6 +1123,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains + OpenAPI V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/charts/vela-minimal/crds/core.oam.dev_workflowstepdefinitions.yaml b/charts/vela-minimal/crds/core.oam.dev_workflowstepdefinitions.yaml index 880959871..c6a0d4353 100644 --- a/charts/vela-minimal/crds/core.oam.dev_workflowstepdefinitions.yaml +++ b/charts/vela-minimal/crds/core.oam.dev_workflowstepdefinitions.yaml @@ -216,6 +216,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains OpenAPI + V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/charts/vela-minimal/templates/defwithtemplate/nocalhost.yaml b/charts/vela-minimal/templates/defwithtemplate/nocalhost.yaml index fc4d1f655..5a1831cce 100644 --- a/charts/vela-minimal/templates/defwithtemplate/nocalhost.yaml +++ b/charts/vela-minimal/templates/defwithtemplate/nocalhost.yaml @@ -18,10 +18,27 @@ spec: "encoding/json" ) + outputs: nocalhostService: { + apiVersion: "v1" + kind: "Service" + metadata: name: context.name + spec: { + selector: "app.oam.dev/component": context.name + ports: [ + { + port: parameter.port + targetPort: parameter.port + }, + ] + type: "ClusterIP" + } + } patch: metadata: annotations: { "dev.nocalhost/application-name": context.appName "dev.nocalhost/application-namespace": context.namespace "dev.nocalhost": json.Marshal({ + name: context.name + serviceType: parameter.serviceType containers: [ { name: context.name @@ -29,7 +46,24 @@ spec: if parameter.gitUrl != _|_ { gitUrl: parameter.gitUrl } - image: parameter.image + if parameter.image == "go" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/golang:latest" + } + if parameter.image == "java" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/java:latest" + } + if parameter.image == "python" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/python:latest" + } + if parameter.image == "node" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/node:latest" + } + if parameter.image == "ruby" { + image: "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/ruby:latest" + } + if parameter.image != "go" && parameter.image != "java" && parameter.image != "python" && parameter.image != "node" && parameter.image != "ruby" { + image: parameter.image + } shell: parameter.shell workDir: parameter.workDir if parameter.storageClass != _|_ { @@ -68,27 +102,33 @@ spec: if parameter.portForward != _|_ { portForward: parameter.portForward } + if parameter.portForward == _|_ { + portForward: ["\(parameter.port)" + ":" + "\(parameter.port)"] + } } }, ] }) } + language: "go" | "java" | "python" | "node" | "ruby" parameter: { + port: int + serviceType: *"deployment" | string gitUrl?: string - image: string + image: language | string shell: *"bash" | string workDir: *"/home/nocalhost-dev" | string storageClass?: string - command?: { - run?: [...string] - debug?: [...string] + command: { + run: *["sh", "run.sh"] | [...string] + debug: *["sh", "debug.sh"] | [...string] } debug?: remoteDebugPort?: int hotReload: *true | bool sync: { - type: *"send" | string - filePattern?: [...string] - ignoreFilePattern?: [...string] + type: *"send" | string + filePattern: *["./"] | [...string] + ignoreFilePattern: *[".git", ".vscode", ".idea", ".gradle", "build"] | [...string] } env?: [...{ name: string diff --git a/charts/vela-minimal/templates/defwithtemplate/rollout.yaml b/charts/vela-minimal/templates/defwithtemplate/rollout.yaml index 0c0c9ecca..e5c360d73 100644 --- a/charts/vela-minimal/templates/defwithtemplate/rollout.yaml +++ b/charts/vela-minimal/templates/defwithtemplate/rollout.yaml @@ -24,8 +24,10 @@ spec: componentName: context.name rolloutPlan: { rolloutStrategy: "IncreaseFirst" - rolloutBatches: parameter.rolloutBatches - targetSize: parameter.targetSize + if parameter.rolloutBatches != _|_ { + rolloutBatches: parameter.rolloutBatches + } + targetSize: parameter.targetSize if parameter["batchPartition"] != _|_ { batchPartition: parameter.batchPartition } @@ -35,7 +37,7 @@ spec: parameter: { targetRevision: *context.revision | string targetSize: int - rolloutBatches: [...rolloutBatch] + rolloutBatches?: [...rolloutBatch] batchPartition?: int } rolloutBatch: replicas: int diff --git a/charts/vela-minimal/templates/defwithtemplate/webhook-notification.yaml b/charts/vela-minimal/templates/defwithtemplate/webhook-notification.yaml index 260ec4abf..3134fed8b 100644 --- a/charts/vela-minimal/templates/defwithtemplate/webhook-notification.yaml +++ b/charts/vela-minimal/templates/defwithtemplate/webhook-notification.yaml @@ -100,17 +100,17 @@ spec: url?: string value?: string style?: string - text?: text + text?: textType confirm?: { - title: text - text: text - confirm: text - deny: text + title: textType + text: textType + confirm: textType + deny: textType style?: string } options?: [...option] initial_options?: [...option] - placeholder?: text + placeholder?: textType initial_date?: string image_url?: string alt_text?: string @@ -124,16 +124,16 @@ spec: initial_time?: string }] } - text: { + textType: { type: string text: string emoji?: bool verbatim?: bool } option: { - text: text + text: textType value: string - description?: text + description?: textType url?: string } secretRef: { diff --git a/cmd/core/main.go b/cmd/core/main.go index a56da7fb1..7b8a69c58 100644 --- a/cmd/core/main.go +++ b/cmd/core/main.go @@ -32,6 +32,7 @@ import ( appsv1 "k8s.io/api/apps/v1" "k8s.io/klog/v2" + "k8s.io/klog/v2/klogr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -111,6 +112,8 @@ func main() { flag.StringVar(&storageDriver, "storage-driver", "Local", "Application file save to the storage driver") flag.DurationVar(&syncPeriod, "informer-re-sync-interval", 60*time.Minute, "controller shared informer lister full re-sync period") + flag.DurationVar(&commonconfig.ReconcileTimeout, "reconcile-timeout", time.Minute*3, + "the timeout for controller reconcile") flag.StringVar(&oam.SystemDefinitonNamespace, "system-definition-namespace", "vela-system", "define the namespace of the system-level definition") flag.IntVar(&controllerArgs.ConcurrentReconciles, "concurrent-reconciles", 4, "concurrent-reconciles is the concurrent reconcile number of the controller. The default value is 4") flag.Float64Var(&qps, "kube-api-qps", 50, "the qps for reconcile clients. Low qps may lead to low throughput. High qps may give stress to api-server. Raise this value if concurrent-reconciles is set to be high.") @@ -192,7 +195,7 @@ func main() { os.Exit(1) } } - + ctrl.SetLogger(klogr.New()) mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, diff --git a/design/api/vela-controller-params-reference.md b/design/api/vela-controller-params-reference.md index 5e3c95572..96b743499 100644 --- a/design/api/vela-controller-params-reference.md +++ b/design/api/vela-controller-params-reference.md @@ -22,6 +22,7 @@ | disable-caps | string | "" | To be disabled builtin capability list. | | storage-driver | string | Local | Application file save to the storage driver | | informer-re-sync-interval | time | 1h | Controller shared informer lister full re-sync period, the interval between two routinely reconciles for one CR (like Application) if no changes made to it. | +| reconcile-timeout | time | 3m | The timeout for controller reconcile. | | system-definition-namespace | string | vela-system | define the namespace of the system-level definition | | concurrent-reconciles | int | 4 | The concurrent reconcile number of the controller. You can increase the degree of concurrency if a large number of CPU cores are provided to the controller. | | kube-api-qps | int | 50 | The qps for reconcile k8s clients. Increase it if you have high concurrency. A small number might restrict the requests to the api-server which may cause a long waiting queue when there are a large number of inflight requests. Try to avoid setting it too high since it will cause large burden on apiserver. | @@ -39,4 +40,4 @@ | Medium | < 500 | < 5,000 | < 30,000 | 4 | 500 | 800 | 1 | 2Gi | | Large | < 1,000 | < 12,000 | < 72,000 | 4 | 800 | 1,000 | 2 | 4Gi | -> For details, read KubeVela Performance Test Report \ No newline at end of file +> For details, read KubeVela Performance Test Report diff --git a/docs/examples/nocalhost/app-bookinfo-authors.yaml b/docs/examples/nocalhost/app-bookinfo-authors.yaml new file mode 100644 index 000000000..925fca97c --- /dev/null +++ b/docs/examples/nocalhost/app-bookinfo-authors.yaml @@ -0,0 +1,17 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: bookinfo-authors +spec: + components: + - name: authors + type: webservice + properties: + image: nocalhost-docker.pkg.coding.net/nocalhost/bookinfo/authors:latest + port: 9080 + traits: + - type: nocalhost + properties: + port: 9080 + gitUrl: https://github.com/nocalhost/bookinfo-authors.git + image: go diff --git a/docs/examples/nocalhost/nocalhost.yaml b/docs/examples/nocalhost/app-bookinfo.yaml similarity index 65% rename from docs/examples/nocalhost/nocalhost.yaml rename to docs/examples/nocalhost/app-bookinfo.yaml index 88fb343e1..8628f1c92 100644 --- a/docs/examples/nocalhost/nocalhost.yaml +++ b/docs/examples/nocalhost/app-bookinfo.yaml @@ -10,12 +10,9 @@ spec: image: nocalhost-docker.pkg.coding.net/nocalhost/bookinfo/productpage:latest port: 9080 traits: - - type: expose - properties: - port: - - 9080 - type: nocalhost properties: + port: 9080 gitUrl: https://github.com/nocalhost/bookinfo-productpage.git image: nocalhost-docker.pkg.coding.net/nocalhost/dev-images/python:3.7.7-slim-productpage-with-pydevd shell: "bash" @@ -56,21 +53,11 @@ spec: image: nocalhost-docker.pkg.coding.net/nocalhost/bookinfo/authors:latest port: 9080 traits: - - type: expose - properties: - port: - - 9080 - type: nocalhost properties: + port: 9080 gitUrl: https://github.com/nocalhost/bookinfo-authors.git - image: nocalhost-docker.pkg.coding.net/nocalhost/dev-images/golang:latest - command: - run: - - sh - - run.sh - debug: - - sh - - debug.sh + image: go debug: remoteDebugPort: 9009 @@ -80,27 +67,11 @@ spec: image: nocalhost-docker.pkg.coding.net/nocalhost/bookinfo/details:latest port: 9080 traits: - - type: expose - properties: - port: - - 9080 - type: nocalhost properties: + port: 9080 gitUrl: https://github.com/nocalhost/bookinfo-details.git image: nocalhost-docker.pkg.coding.net/nocalhost/dev-images/ruby:2.7.1-slim - command: - run: - - sh - - run.sh - debug: - - sh - - debug.sh - sync: - filePattern: - - ./ - ignoreFilePattern: - - .git - - .idea - name: ratings type: webservice @@ -108,21 +79,11 @@ spec: image: nocalhost-docker.pkg.coding.net/nocalhost/bookinfo/ratings:latest port: 9080 traits: - - type: expose - properties: - port: - - 9080 - type: nocalhost properties: + port: 9080 gitUrl: https://github.com/nocalhost/bookinfo-ratings.git image: nocalhost-docker.pkg.coding.net/nocalhost/dev-images/node:12.18.1-slim - command: - run: - - sh - - run.sh - debug: - - sh - - debug.sh - name: reviews type: webservice @@ -130,28 +91,10 @@ spec: image: nocalhost-docker.pkg.coding.net/nocalhost/bookinfo/reviews:latest port: 9080 traits: - - type: expose - properties: - port: - - 9080 - type: nocalhost properties: + port: 9080 gitUrl: https://github.com/nocalhost/bookinfo-reviews.git image: nocalhost-docker.pkg.coding.net/nocalhost/dev-images/java:latest - command: - run: - - sh - - run.sh - debug: - - sh - - debug.sh debug: remoteDebugPort: 5005 - sync: - filePattern: - - ./ - ignoreFilePattern: - - .git - - .idea - - .gradle - - build diff --git a/docs/examples/obervability/application-observability.yaml b/docs/examples/obervability/application-observability.yaml new file mode 100644 index 000000000..4fc544e09 --- /dev/null +++ b/docs/examples/obervability/application-observability.yaml @@ -0,0 +1,130 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: observability +spec: { } + +--- +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + annotations: + addons.oam.dev/description: "An out of the box solution for KubeVela observability" + name: grafana + namespace: observability +spec: + components: + # install grafana datasource registration chart + - name: grafana-registration-release + type: helm + properties: + repoType: git + url: https://github.com/oam-dev/grafana-registration + git: + branch: master + chart: ./chart + targetNamespace: observability + values: + replicaCount: 1 + + # install Grafana + - name: grafana + properties: + chart: grafana + version: 6.14.1 + repoType: helm + # original url: https://grafana.github.io/helm-charts + url: https://charts.kubevela.net/addons + targetNamespace: observability + releaseName: grafana + type: helm + traits: + - type: pure-ingress + properties: + domain: grafana.c58136db32cbc44cca364bf1cf7f90519.cn-hongkong.alicontainer.com + http: + "/": 80 + - type: import-grafana-dashboard + properties: + grafanaServiceName: grafana + grafanaServiceNamespace: observability + credentialSecret: grafana + credentialSecretNamespace: observability + urls: + - "https://charts.kubevela.net/addons/dashboards/kubevela_core_logging.json" + - "https://charts.kubevela.net/addons/dashboards/kubevela_core_monitoring.json" + - "https://charts.kubevela.net/addons/dashboards/flux2/cluster.json" + - "https://charts.kubevela.net/addons/dashboards/kubevela_application_logging.json" + + # install loki + - name: loki + type: helm + properties: + chart: loki-stack + version: 2.4.1 + repoType: helm + # original url: https://grafana.github.io/helm-charts + url: https://charts.kubevela.net/addons + targetNamespace: observability + releaseName: loki + traits: + - type: register-grafana-datasource # register loki datasource to Grafana + properties: + grafanaServiceName: grafana + grafanaServiceNamespace: observability + credentialSecret: grafana + credentialSecretNamespace: observability + name: loki + service: loki + namespace: observability + type: loki + access: proxy + + # install Prometheus + - name: prometheus-server + type: helm + properties: + chart: prometheus + version: 14.4.1 + repoType: helm + # original url: https://prometheus-community.github.io/helm-charts + url: https://charts.kubevela.net/addons + targetNamespace: observability + releaseName: prometheus + values: + alertmanager: + persistentVolume: + storageClass: "alicloud-disk-available" + size: "20Gi" + server: + persistentVolume: + storageClass: "alicloud-disk-available" + size: "20Gi" + + traits: + - type: register-grafana-datasource # register Prometheus datasource to Grafana + properties: + grafanaServiceName: grafana + grafanaServiceNamespace: observability + credentialSecret: grafana + credentialSecretNamespace: observability + name: prometheus + service: prometheus-server + namespace: observability + type: prometheus + access: proxy + + # install kube-state-metrics + - name: kube-state-metrics + type: helm + properties: + chart: kube-state-metrics + version: 3.4.1 + repoType: helm + # original url: https://prometheus-community.github.io/helm-charts + url: https://charts.kubevela.net/addons + targetNamespace: observability + values: + image: + repository: oamdev/kube-state-metrics + tag: v2.1.0 diff --git a/docs/examples/obervability/definitions/trait-import-grafana-dashboard.yaml b/docs/examples/obervability/definitions/trait-import-grafana-dashboard.yaml new file mode 100644 index 000000000..8a79a7219 --- /dev/null +++ b/docs/examples/obervability/definitions/trait-import-grafana-dashboard.yaml @@ -0,0 +1,31 @@ +apiVersion: core.oam.dev/v1beta1 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "Import dashboards to Grafana" + name: import-grafana-dashboard + namespace: vela-system +spec: + schematic: + cue: + template: | + outputs: registerdatasource: { + apiVersion: "grafana.extension.oam.dev/v1alpha1" + kind: "ImportDashboard" + spec: { + grafana: { + service: parameter.grafanaServiceName + namespace: parameter.grafanaServiceNamespace + credentialSecret: parameter.credentialSecret + credentialSecretNamespace: parameter.credentialSecretNamespace + } + urls: parameter.urls + } + } + parameter: { + grafanaServiceName: string + grafanaServiceNamespace: *"default" | string + credentialSecret: string + credentialSecretNamespace: *"default" | string + urls: [...string] + } \ No newline at end of file diff --git a/docs/examples/obervability/definitions/trait-pure-ingress.yaml b/docs/examples/obervability/definitions/trait-pure-ingress.yaml new file mode 100644 index 000000000..9fa412b91 --- /dev/null +++ b/docs/examples/obervability/definitions/trait-pure-ingress.yaml @@ -0,0 +1,58 @@ +apiVersion: core.oam.dev/v1beta1 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "Enable public web traffic for the component without creating a Service." + name: pure-ingress + namespace: vela-system +spec: + status: + customStatus: |- + let igs = context.outputs.ingress.status.loadBalancer.ingress + if igs == _|_ { + message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n" + } + if len(igs) > 0 { + if igs[0].ip != _|_ { + message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + igs[0].ip + } + if igs[0].ip == _|_ { + message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + } + } + healthPolicy: | + isHealth: len(context.outputs.ingress.status.loadBalancer.ingress) > 0 + schematic: + cue: + template: | + + outputs: ingress: { + apiVersion: "networking.k8s.io/v1beta1" + kind: "Ingress" + metadata: + name: context.name + spec: { + rules: [{ + host: parameter.domain + http: { + paths: [ + for k, v in parameter.http { + path: k + backend: { + serviceName: context.name + servicePort: v + } + }, + ] + } + }] + } + } + + parameter: { + // +usage=Specify the domain you want to expose + domain: string + + // +usage=Specify the mapping relationship between the http path and the workload port + http: [string]: int + } diff --git a/docs/examples/obervability/definitions/trait-register-grafana-datasource.yaml b/docs/examples/obervability/definitions/trait-register-grafana-datasource.yaml new file mode 100644 index 000000000..486ed431d --- /dev/null +++ b/docs/examples/obervability/definitions/trait-register-grafana-datasource.yaml @@ -0,0 +1,42 @@ +apiVersion: core.oam.dev/v1beta1 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "Add a datasource to Grafana" + name: register-grafana-datasource + namespace: vela-system +spec: + schematic: + cue: + template: | + outputs: registerdatasource: { + apiVersion: "grafana.extension.oam.dev/v1alpha1" + kind: "DatasourceRegistration" + spec: { + grafana: { + service: parameter.grafanaServiceName + namespace: parameter.grafanaServiceNamespace + credentialSecret: parameter.credentialSecret + credentialSecretNamespace: parameter.credentialSecretNamespace + } + datasource: { + name: parameter.name + type: parameter.type + access: parameter.access + service: parameter.service + namespace: parameter.namespace + } + } + } + + parameter: { + grafanaServiceName: string + grafanaServiceNamespace: *"default" | string + credentialSecret: string + credentialSecretNamespace: string + name: string + type: string + access: *"proxy" | string + service: string + namespace: *"default" | string + } diff --git a/docs/examples/obervability/initializer-observability.yaml b/docs/examples/obervability/initializer-observability.yaml deleted file mode 100644 index abd18a849..000000000 --- a/docs/examples/obervability/initializer-observability.yaml +++ /dev/null @@ -1,159 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: observability -spec: { } - ---- -apiVersion: core.oam.dev/v1beta1 -kind: Initializer -metadata: - name: grafana - namespace: observability -spec: - appTemplate: - spec: - components: - # install grafana datasource registration chart - - name: grafana-registration-release - properties: - apiVersion: helm.toolkit.fluxcd.io/v2beta1 - kind: HelmRelease - metadata: - name: grafana-registration-release - namespace: observability - spec: - chart: - spec: - chart: ./chart - interval: 1m - sourceRef: - kind: GitRepository - name: grafana-registration-repo - namespace: observability - interval: 5m - values: - replicaCount: 1 - type: raw - - name: grafana-registration-repo - properties: - apiVersion: source.toolkit.fluxcd.io/v1beta1 - kind: GitRepository - metadata: - name: grafana-registration-repo - namespace: observability - spec: - interval: 5m - ref: - branch: master - url: https://github.com/oam-dev/grafana-registration - type: raw - - # install Grafana - - name: grafana - properties: - chart: grafana - version: 6.14.1 - repoType: helm - # original url: https://grafana.github.io/helm-charts - url: https://charts.kubevela.net/addons - targetNamespace: observability - releaseName: grafana - type: helm - traits: - - type: pure-ingress - properties: - domain: grafana.cf7223b8abedc4691b7eccfe3c675850a.cn-hongkong.alicontainer.com - http: - "/": 80 - - type: import-grafana-dashboard - properties: - grafanaServiceName: grafana - grafanaServiceNamespace: observability - credentialSecret: grafana - credentialSecretNamespace: observability - urls: - - "https://charts.kubevela.net/addons/dashboards/kubevela_core_logging.json" - - "https://charts.kubevela.net/addons/dashboards/kubevela_core_monitoring.json" - - "https://charts.kubevela.net/addons/dashboards/flux2/cluster.json" - - "https://charts.kubevela.net/addons/dashboards/kubevela_application_logging.json" - - # install loki - - name: loki - type: helm - properties: - chart: loki-stack - version: 2.4.1 - repoType: helm - # original url: https://grafana.github.io/helm-charts - url: https://charts.kubevela.net/addons - targetNamespace: observability - releaseName: loki - traits: - - type: register-grafana-datasource # register loki datasource to Grafana - properties: - grafanaServiceName: grafana - grafanaServiceNamespace: observability - credentialSecret: grafana - credentialSecretNamespace: observability - name: loki - service: loki - namespace: observability - type: loki - access: proxy - - # install Prometheus - - name: prometheus-server - type: helm - properties: - chart: prometheus - version: 14.4.1 - repoType: helm - # original url: https://prometheus-community.github.io/helm-charts - url: https://charts.kubevela.net/addons - targetNamespace: observability - releaseName: prometheus - values: - alertmanager: - persistentVolume: - storageClass: "alicloud-disk-available" - size: "20Gi" - server: - persistentVolume: - storageClass: "alicloud-disk-available" - size: "20Gi" - - traits: - - type: register-grafana-datasource # register Prometheus datasource to Grafana - properties: - grafanaServiceName: grafana - grafanaServiceNamespace: observability - credentialSecret: grafana - credentialSecretNamespace: observability - name: prometheus - service: prometheus-server - namespace: observability - type: prometheus - access: proxy - - # install kube-state-metrics - - name: kube-state-metrics - type: helm - properties: - chart: kube-state-metrics - version: 3.4.1 - repoType: helm - # original url: https://prometheus-community.github.io/helm-charts - url: https://charts.kubevela.net/addons - targetNamespace: observability - values: - image: - repository: oamdev/kube-state-metrics - tag: v2.1.0 - - dependsOn: - - ref: - apiVersion: core.oam.dev/v1beta1 - kind: Initializer - name: fluxcd - namespace: vela-system diff --git a/e2e/application/application_test.go b/e2e/application/application_test.go index feeed3dfa..d5199f4c1 100644 --- a/e2e/application/application_test.go +++ b/e2e/application/application_test.go @@ -49,11 +49,11 @@ var _ = ginkgo.Describe("Test Vela Application", func() { e2e.EnvInitContext("env init", envName) e2e.EnvSetContext("env set", envName) e2e.JsonAppFileContext("deploy app-basic", appbasicJsonAppFile) + ApplicationExecContext("exec -- COMMAND", applicationName) e2e.JsonAppFileContext("update app-basic, add scaler trait with replicas 2", appbasicAddTraitJsonAppFile) e2e.ComponentListContext("ls", applicationName, workloadType, traitAlias) ApplicationStatusContext("status", applicationName, workloadType) ApplicationStatusDeeplyContext("status", applicationName, workloadType, envName) - ApplicationExecContext("exec -- COMMAND", applicationName) // ApplicationPortForwardContext("port-forward", applicationName) e2e.WorkloadDeleteContext("delete", applicationName) diff --git a/e2e/capability/capability_test.go b/e2e/capability/capability_test.go deleted file mode 100644 index 84d8e33a9..000000000 --- a/e2e/capability/capability_test.go +++ /dev/null @@ -1,153 +0,0 @@ -/* -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 - -import ( - "fmt" - - "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/e2e" - "github.com/oam-dev/kubevela/references/apis" - - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" -) - -var ( - capabilityCenterBasic = apis.CapabilityCenterMeta{ - Name: "capability-center-e2e-basic", - URL: "https://github.com/oam-dev/kubevela/tree/master/pkg/plugins/testdata", - } - - websvcCapability = types.Capability{ - Name: "webservice.testapps", - Type: types.TypeWorkload, - } - - scaleCapability = types.Capability{ - Name: "scaler", - Type: types.TypeTrait, - } - - routeCapability = types.Capability{ - Name: "routes.test", - Type: types.TypeTrait, - } - - ingressCapability = types.Capability{ - Name: "ingress.test", - Type: types.TypeTrait, - } -) - -// TODO: change this into a mock UT to avoid remote call. - -var _ = ginkgo.Describe("Capability", func() { - ginkgo.Context("capability center", func() { - ginkgo.It("add a capability center", func() { - cli := fmt.Sprintf("vela cap center config %s %s", capabilityCenterBasic.Name, capabilityCenterBasic.URL) - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - expectedOutput1 := fmt.Sprintf("Successfully configured capability center %s and sync from remote", capabilityCenterBasic.Name) - gomega.Expect(output).To(gomega.ContainSubstring(expectedOutput1)) - }) - - ginkgo.It("list capability centers", func() { - cli := "vela cap center ls" - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(output).To(gomega.ContainSubstring("NAME")) - gomega.Expect(output).To(gomega.ContainSubstring("ADDRESS")) - gomega.Expect(output).To(gomega.ContainSubstring(capabilityCenterBasic.Name)) - gomega.Expect(output).To(gomega.ContainSubstring(capabilityCenterBasic.URL)) - }) - }) - - ginkgo.Context("capability", func() { - ginkgo.It("install a workload capability to cluster", func() { - cli := fmt.Sprintf("vela cap install %s/%s", capabilityCenterBasic.Name, websvcCapability.Name) - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - expectedSubStr1 := fmt.Sprintf("Installing %s capability", websvcCapability.Type) - expectedSubStr2 := fmt.Sprintf("Successfully installed capability %s from %s", websvcCapability.Name, capabilityCenterBasic.Name) - gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr1)) - gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr2)) - }) - - ginkgo.It("install a trait capability to cluster", func() { - cli := fmt.Sprintf("vela cap install %s/%s", capabilityCenterBasic.Name, scaleCapability.Name) - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - expectedSubStr1 := fmt.Sprintf("Installing %s capability", scaleCapability.Type) - expectedSubStr2 := fmt.Sprintf("Successfully installed capability %s from %s", scaleCapability.Name, capabilityCenterBasic.Name) - gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr1)) - gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr2)) - }) - - ginkgo.It("install a trait capability without definition reference to cluster", func() { - cli := fmt.Sprintf("vela cap install %s/%s", capabilityCenterBasic.Name, ingressCapability.Name) - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - expectedSubStr1 := fmt.Sprintf("Installing %s capability", ingressCapability.Type) - expectedSubStr2 := fmt.Sprintf("Successfully installed capability %s from %s", ingressCapability.Name, capabilityCenterBasic.Name) - gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr1)) - gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr2)) - }) - - ginkgo.It("list all capabilities", func() { - cli := fmt.Sprintf("vela cap ls %s", capabilityCenterBasic.Name) - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(output).To(gomega.ContainSubstring("NAME")) - gomega.Expect(output).To(gomega.ContainSubstring("CENTER")) - gomega.Expect(output).To(gomega.ContainSubstring(websvcCapability.Name)) - gomega.Expect(output).To(gomega.ContainSubstring(ingressCapability.Name)) - gomega.Expect(output).To(gomega.ContainSubstring(scaleCapability.Name)) - gomega.Expect(output).To(gomega.ContainSubstring(routeCapability.Name)) - gomega.Expect(output).To(gomega.ContainSubstring("installed")) - }) - - ginkgo.It("uninstall a workload capability from cluster", func() { - cli := fmt.Sprintf("vela cap uninstall %s", websvcCapability.Name) - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - expectedSubStr := fmt.Sprintf("Successfully uninstalled capability %s", websvcCapability.Name) - gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr)) - }) - - ginkgo.It("uninstall a trait capability from cluster", func() { - cli := fmt.Sprintf("vela cap uninstall %s", ingressCapability.Name) - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - expectedSubStr := fmt.Sprintf("Successfully uninstalled capability %s", ingressCapability.Name) - gomega.Expect(output).To(gomega.ContainSubstring(expectedSubStr)) - - // unstall other installed test capability - cli = fmt.Sprintf("vela cap uninstall %s", scaleCapability.Name) - _, err = e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - }) - - ginkgo.It("delete a capability center", func() { - cli := fmt.Sprintf("vela cap center remove %s", capabilityCenterBasic.Name) - output, err := e2e.Exec(cli) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - expectedOutput := fmt.Sprintf("%s capability center removed successfully", capabilityCenterBasic.Name) - gomega.Expect(output).To(gomega.ContainSubstring(expectedOutput)) - }) - }) -}) diff --git a/e2e/plugin/plugin_test.go b/e2e/plugin/plugin_test.go index 4b8560508..9801d2c97 100644 --- a/e2e/plugin/plugin_test.go +++ b/e2e/plugin/plugin_test.go @@ -19,7 +19,6 @@ package plugin import ( "fmt" "os" - "os/exec" "time" . "github.com/onsi/ginkgo" @@ -172,61 +171,6 @@ var _ = Describe("Test Kubectl Plugin", func() { Expect(output).ShouldNot(ContainSubstring("mySecretKey")) }) }) - - Context("Test kubectl vela comp discover", func() { - It("Test list components in local registry", func() { - output, err := e2e.Exec("kubectl-vela comp --discover --url=" + testRegistryPath) - Expect(err).NotTo(HaveOccurred()) - Expect(output).Should(ContainSubstring("Showing components from registry")) - Expect(output).Should(ContainSubstring("fake-workload")) - }) - }) - Context("Test kubectl vela trait discover", func() { - It("Test list traits in local registry", func() { - output, err := e2e.Exec("kubectl-vela trait --discover --url=" + testRegistryPath) - Expect(err).NotTo(HaveOccurred()) - Expect(output).Should(ContainSubstring("Showing traits from registry")) - Expect(output).Should(ContainSubstring("dynamic-sa")) - }) - }) - Context("Test kubectl vela comp and trait install", func() { - It("Test install a sample component", func() { - output, err := e2e.Exec("kubectl-vela comp get cloneset --url=" + testRegistryPath) - Expect(err).NotTo(HaveOccurred()) - Expect(output).Should(ContainSubstring("Successfully install component: cloneset")) - }) - It("Test install a sample trait", func() { - output, err := e2e.Exec("kubectl-vela trait get init-container --url=" + testRegistryPath) - Expect(err).NotTo(HaveOccurred()) - Expect(output).Should(ContainSubstring("Successfully install trait: init-container")) - }) - }) - Context("Test kubectl vela list installed comp and trait", func() { - It("Test list installed component", func() { - output, err := e2e.Exec("kubectl-vela comp --url=" + testRegistryPath) - Expect(err).NotTo(HaveOccurred()) - Expect(output).Should(ContainSubstring("cloneset")) - }) - It("Test list installed trait", func() { - output, err := e2e.Exec("kubectl-vela trait --url=" + testRegistryPath) - Expect(err).NotTo(HaveOccurred()) - Expect(output).Should(ContainSubstring("init-container")) - }) - }) - Context("Test uninstall vela trait", func() { - It("Clean the sample component", func() { - cmd := exec.Command("kubectl", "delete", "componentDefinition", "cloneset", "-n", "vela-system") - output, err := cmd.Output() - Expect(err).NotTo(HaveOccurred()) - Expect(output).Should(ContainSubstring("componentdefinition.core.oam.dev \"cloneset\" deleted")) - }) - It("Clean the sample trait", func() { - cmd := exec.Command("kubectl", "delete", "traitDefinition", "init-container", "-n", "vela-system") - output, err := cmd.Output() - Expect(err).NotTo(HaveOccurred()) - Expect(output).Should(ContainSubstring("traitdefinition.core.oam.dev \"init-container\" deleted")) - }) - }) }) var application = ` diff --git a/e2e/capability/capability_suite_test.go b/e2e/registry/registry_suite_test.go similarity index 94% rename from e2e/capability/capability_suite_test.go rename to e2e/registry/registry_suite_test.go index 6c5500b54..befbf2235 100644 --- a/e2e/capability/capability_suite_test.go +++ b/e2e/registry/registry_suite_test.go @@ -25,5 +25,5 @@ import ( func TestEnv(t *testing.T) { gomega.RegisterFailHandler(ginkgo.Fail) - ginkgo.RunSpecs(t, "Capability Suite") + ginkgo.RunSpecs(t, "Registry Suite") } diff --git a/e2e/registry/registry_test.go b/e2e/registry/registry_test.go new file mode 100644 index 000000000..e11df76fe --- /dev/null +++ b/e2e/registry/registry_test.go @@ -0,0 +1,149 @@ +/* +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 + +import ( + "fmt" + "os/exec" + + "github.com/oam-dev/kubevela/e2e" + "github.com/oam-dev/kubevela/references/apis" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var ( + registryConfigs = []apis.RegistryConfig{ + { + Name: "e2e-oss-registry", + URL: "oss://registry.e2e.net", + Token: "", + }, + { + Name: "e2e-github-registry", + URL: "https://github.com/oam-dev/catalog/tree/master/traits", + Token: "", + }, + } +) + +var testTrait = "crd-manual-scaler" + +// TODO: change this into a mock UT to avoid remote call. + +var _ = Describe("test registry and trait/comp command", func() { + Context("registry", func() { + It("add and remove registry config", func() { + for _, config := range registryConfigs { + cli := fmt.Sprintf("vela registry config %s %s", config.Name, config.URL) + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Successfully configured registry %s", config.Name))) + } + }) + + It("list registry config", func() { + cli := "vela registry ls" + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("NAME")) + Expect(output).To(ContainSubstring("URL")) + for _, config := range registryConfigs { + Expect(output).To(ContainSubstring(config.Name)) + Expect(output).To(ContainSubstring(config.URL)) + } + }) + + It("remove registry config", func() { + for _, config := range registryConfigs { + cli := fmt.Sprintf("vela registry remove %s", config.Name) + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Successfully remove registry %s", config.Name))) + } + + }) + }) + + Context("list and install trait from registry", func() { + It("list trait from cluster", func() { + cli := "vela trait" + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("NAME")) + Expect(output).To(ContainSubstring("APPLIES-TO")) + Expect(output).To(ContainSubstring("pvc")) + Expect(output).To(ContainSubstring("[deployments.apps]")) + }) + It("list trait from default registry", func() { + cli := "vela trait --discover" + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("Showing trait definition from registry: default")) + Expect(output).To(ContainSubstring("NAME")) + Expect(output).To(ContainSubstring("APPLIES-TO")) + Expect(output).To(ContainSubstring("STATUS")) + Expect(output).To(ContainSubstring("autoscale")) + Expect(output).To(ContainSubstring("[deployments.apps]")) + }) + + It("install traits to cluster", func() { + cli := fmt.Sprintf("vela trait get %s", testTrait) + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + expectedSubStr1 := fmt.Sprintf("Installing trait %s", testTrait) + expectedSubStr2 := fmt.Sprintf("Successfully install trait: %s", testTrait) + Expect(output).To(ContainSubstring(expectedSubStr1)) + Expect(output).To(ContainSubstring(expectedSubStr2)) + }) + + It("Clean the test trait", func() { + cmd := exec.Command("kubectl", "delete", "traitDefinition", testTrait, "-n", "vela-system") + output, err := cmd.Output() + Expect(err).NotTo(HaveOccurred()) + Expect(output).Should(ContainSubstring("traitdefinition.core.oam.dev \"" + testTrait + "\" deleted")) + }) + + It("test list trait in raw url", func() { + cli := "vela trait --discover --url=oss://registry.kubevela.net" + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("Showing trait definition from url"), ContainSubstring("oss://registry.kubevela.net")) + }) + + }) + + Context("test list component definition", func() { + It("test list installed component definition", func() { + cli := "vela comp" + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + Expect(output).To(ContainSubstring("NAME")) + Expect(output).To(ContainSubstring("DEFINITION")) + Expect(output).To(ContainSubstring("raw")) + Expect(output).To(ContainSubstring("deployments.apps")) + }) + It("test list with label", func() { + cli := "vela comp --label type=terraform" + output, err := e2e.Exec(cli) + Expect(err).NotTo(HaveOccurred()) + Expect(output).NotTo(ContainSubstring("raw")) + Expect(output).To(ContainSubstring("alibaba-ack")) + }) + }) +}) diff --git a/go.mod b/go.mod index c765a3a31..61d4e26d7 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,8 @@ require ( github.com/imdario/mergo v0.3.12 github.com/kyokomi/emoji v2.2.4+incompatible github.com/mitchellh/hashstructure/v2 v2.0.1 - github.com/oam-dev/cluster-gateway v1.1.2 + github.com/oam-dev/cluster-gateway v1.1.6 + github.com/oam-dev/cluster-register v1.0.1 github.com/oam-dev/terraform-config-inspect v0.0.0-20210418082552-fc72d929aa28 github.com/oam-dev/terraform-controller v0.2.5 github.com/olekukonko/tablewriter v0.0.5 @@ -86,5 +87,6 @@ require ( replace ( github.com/docker/docker => github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible - github.com/wercker/stern => github.com/oam-dev/stern v1.13.0-alpha + github.com/wercker/stern => github.com/oam-dev/stern v1.13.1 + sigs.k8s.io/apiserver-network-proxy/konnectivity-client => sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.24 ) diff --git a/go.sum b/go.sum index 120d949e7..bfb98c7b3 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,7 @@ github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+B github.com/Azure/go-autorest/autorest v0.9.3-0.20191028180845-3492b2aff503/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= github.com/Azure/go-autorest/autorest v0.10.2/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= @@ -84,6 +85,7 @@ github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEg github.com/Azure/go-autorest/autorest/adal v0.8.1-0.20191028180845-3492b2aff503/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= @@ -94,6 +96,7 @@ github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSY github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= @@ -932,8 +935,9 @@ github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= -github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -1149,8 +1153,9 @@ github.com/minio/minio-go/v6 v6.0.49/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tB github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4= github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -1171,13 +1176,15 @@ github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:NT0cwArZg/wGdvY8pzej4tPr+9WGmDdkF8Suj+mkz2g= github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= @@ -1210,6 +1217,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= @@ -1228,10 +1236,12 @@ github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oam-dev/cluster-gateway v1.1.2 h1:sxC8Uyx/d3Yu8nIFSz31i+4JKhJfDAS9XVIPEWa1y+Q= -github.com/oam-dev/cluster-gateway v1.1.2/go.mod h1:EjPUZwTYBe+gFtPV/yGohLE19fDr3CUg4tfSRY72fkM= -github.com/oam-dev/stern v1.13.0-alpha h1:EVjM8Qvh6LssB6t4RZrjf9DtCq1cz+/cy6OF7fpy9wk= -github.com/oam-dev/stern v1.13.0-alpha/go.mod h1:AOkvfFUv0Arz7GBi0jz7S0Jsu4K/kdvSjNsnRt1+BIg= +github.com/oam-dev/cluster-gateway v1.1.6 h1:CY6m2Qcs6XJ/l/NY48CdHD7GAel9zZ/erUOz2zYzxkI= +github.com/oam-dev/cluster-gateway v1.1.6/go.mod h1:SF7S4Ss+VUs2OVxmvSrrFGcaNFoXy6JWxHAnUxC1QcY= +github.com/oam-dev/cluster-register v1.0.1 h1:mUT+vZeM5IZcwgLI003736xUyu/4FNqnsp3OnePUXcI= +github.com/oam-dev/cluster-register v1.0.1/go.mod h1:AoqoF9HgmluxtRBYyvKDbLNdlPY6Xvm+/6uo6LjLaBw= +github.com/oam-dev/stern v1.13.1 h1:Gt7xMBmQjRueHVFjRo5CHDTVhiYrssjlmvPwRiZtq7c= +github.com/oam-dev/stern v1.13.1/go.mod h1:0pLjZt0amXE/ErF16Rdrgd98H2owN8Hmn3/7CX5+AeA= github.com/oam-dev/terraform-config-inspect v0.0.0-20210418082552-fc72d929aa28 h1:tD8HiFKnt0jnwdTWjeqUnfnUYLD/+Nsmj8ZGIxqDWiU= github.com/oam-dev/terraform-config-inspect v0.0.0-20210418082552-fc72d929aa28/go.mod h1:Mu8i0/DdplvnjwRbAYPsc8+LRR27n/mp8VWdkN10GzE= github.com/oam-dev/terraform-controller v0.2.5 h1:ntSyLUZkjvyHPJEuSYGvsa5+Sb3fDXkqoS+iD3avHQo= @@ -1324,7 +1334,6 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.0.0-20180311214515-816c9085562c/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -1768,6 +1777,7 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1994,6 +2004,7 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2023,8 +2034,9 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYe 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= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2043,6 +2055,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= @@ -2416,6 +2429,7 @@ k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= k8s.io/api v0.18.3/go.mod h1:UOaMwERbqJMfeeeHc8XJKawj4P9TgDRnViIqqBeH2QA= k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI= k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY= +k8s.io/api v0.20.10/go.mod h1:0kei3F6biGjtRQBo5dUeujq6Ji3UCh9aOSfp/THYd7I= k8s.io/api v0.21.0/go.mod h1:+YbrhBBGgsxbF6o6Kj4KJPJnBmAKuXDeS3E18bgHNVU= k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= k8s.io/api v0.21.2/go.mod h1:Lv6UGJZ1rlMI1qusN8ruAp9PUBFyBwpEHAdG24vIsiU= @@ -2443,6 +2457,7 @@ k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftc k8s.io/apimachinery v0.18.3/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig= +k8s.io/apimachinery v0.20.10/go.mod h1:kQa//VOAwyVwJ2+L9kOREbsnryfsGSkSM1przND4+mw= k8s.io/apimachinery v0.21.0/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= @@ -2472,6 +2487,7 @@ k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= k8s.io/client-go v0.18.3/go.mod h1:4a/dpQEvzAhT1BbuWW09qvIaGw6Gbu1gZYiQZIi1DMw= k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q= k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU= +k8s.io/client-go v0.20.10/go.mod h1:fFg+aLoasv/R+xiVaWjxeqGFYltzgQcOQzkFaSRfnJ0= k8s.io/client-go v0.21.0/go.mod h1:nNBytTF9qPFDEhoqgEPaarobC8QPae13bElIVHzIglA= k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= k8s.io/client-go v0.21.2/go.mod h1:HdJ9iknWpbl3vMGtib6T2PyI/VYxiZfq936WNVHBRrA= @@ -2493,6 +2509,7 @@ k8s.io/component-base v0.0.0-20191122220729-2684fb322cb9/go.mod h1:NFuUusy/X4Tk2 k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc= k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmDfUM= k8s.io/component-base v0.18.6/go.mod h1:knSVsibPR5K6EW2XOjEHik6sdU5nCvKMrzMt2D4In14= +k8s.io/component-base v0.20.10/go.mod h1:ZKOEin1xu68aJzxgzl5DZSp5J1IrjAOPlPN90/t6OI8= k8s.io/component-base v0.21.0/go.mod h1:qvtjz6X0USWXbgmbfXR+Agik4RZ3jv2Bgr5QnZzdPYw= k8s.io/component-base v0.21.2/go.mod h1:9lvmIThzdlrJj5Hp8Z/TOgIkdfsNARQ1pT+3PByuiuc= k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ= @@ -2558,6 +2575,7 @@ mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE= +open-cluster-management.io/api v0.0.0-20210610125115-f57c747b84aa/go.mod h1:9qiA5h/8kvPQnJEOlAPHVjRO9a1jCmDhGzvgMBvXEaE= open-cluster-management.io/api v0.0.0-20210804091127-340467ff6239 h1:ToDTkftv88UVZSCqTCzYZTkYoba28z+An08Yrm9aOAA= open-cluster-management.io/api v0.0.0-20210804091127-340467ff6239/go.mod h1:9qiA5h/8kvPQnJEOlAPHVjRO9a1jCmDhGzvgMBvXEaE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -2566,11 +2584,10 @@ rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22 h1:fmRfl9WJ4ApJn7LxNuED4m0t18qivVQOxP6aAYG9J6c= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy v0.0.24 h1:yaswrAqidc2XdLK2GRacVEBb55g4dg91f/B7b0SYliY= +sigs.k8s.io/apiserver-network-proxy v0.0.24/go.mod h1:z/U9KltvRVSMttVl3cdQo8cPuXEjr+Qn3A5sUJR55XI= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.24 h1:bCO6TN9VG1bK3nCG5ghQ5httx1HpsG5MD8XtRDySHDM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.24/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-runtime v1.0.3-0.20210913073608-0663f60bfee2 h1:c6RYHA1wUg9IEsfjnxg0WsPwvDC2Qw2eryXKXgSEF1c= sigs.k8s.io/apiserver-runtime v1.0.3-0.20210913073608-0663f60bfee2/go.mod h1:gvPfh5FX3Wi3kIRpkh7qvY0i/DQl3SDpRtvqMGZE3Vo= sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo= diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationrevisions.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationrevisions.yaml index 185c3ac2f..0f040b998 100644 --- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationrevisions.yaml +++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applicationrevisions.yaml @@ -622,6 +622,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -2778,6 +2795,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -4431,6 +4465,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains + OpenAPI V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applications.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applications.yaml index 64038d672..549e5948a 100644 --- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_applications.yaml +++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_applications.yaml @@ -564,6 +564,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: @@ -1638,6 +1655,23 @@ spec: description: The generation observed by the application controller. format: int64 type: integer + policy: + description: PolicyStatus records the status of policy + items: + description: PolicyStatus records the status of policy + properties: + name: + type: string + status: + type: object + + type: + type: string + required: + - name + - type + type: object + type: array resourceTracker: description: ResourceTracker record the status of the ResourceTracker properties: diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_definitionrevisions.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_definitionrevisions.yaml index 8ffc3eaac..f723729fd 100644 --- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_definitionrevisions.yaml +++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_definitionrevisions.yaml @@ -1123,6 +1123,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains + OpenAPI V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_workflowstepdefinitions.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_workflowstepdefinitions.yaml index 3d2de420f..9303ba2d9 100644 --- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_workflowstepdefinitions.yaml +++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_workflowstepdefinitions.yaml @@ -216,6 +216,10 @@ spec: - type type: object type: array + configMapRef: + description: ConfigMapRef refer to a ConfigMap which contains OpenAPI + V3 JSON schema of Component parameters. + type: string latestRevision: description: LatestRevision of the component definition properties: diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 6dec1a338..b4d6706b5 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -2,13 +2,9 @@ package usecase import ( "context" - "cuelang.org/go/cue" "encoding/json" "errors" "fmt" - "github.com/getkin/kin-openapi/openapi3" - utils2 "github.com/oam-dev/kubevela/pkg/controller/utils" - "github.com/oam-dev/kubevela/pkg/utils/common" "net/url" "path" "path/filepath" @@ -16,6 +12,12 @@ import ( "strings" "time" + "cuelang.org/go/cue" + "github.com/getkin/kin-openapi/openapi3" + + utils2 "github.com/oam-dev/kubevela/pkg/controller/utils" + "github.com/oam-dev/kubevela/pkg/utils/common" + cueyaml "cuelang.org/go/encoding/yaml" "github.com/google/go-github/v32/github" "golang.org/x/oauth2" @@ -372,7 +374,9 @@ func renderCUETemplate(elem apis.AddonElementFile, parameters string, args map[s return nil, err } b, err := cueyaml.Encode(compContent.CueValue()) - + if err != nil { + return nil, err + } comp := common2.ApplicationComponent{ Name: strings.Join(append(elem.Path, elem.Name), "-"), } diff --git a/pkg/apiserver/rest/usecase/applicationplan.go b/pkg/apiserver/rest/usecase/applicationplan.go index cabf58d7a..6086901d7 100644 --- a/pkg/apiserver/rest/usecase/applicationplan.go +++ b/pkg/apiserver/rest/usecase/applicationplan.go @@ -510,6 +510,7 @@ func (c *applicationUsecaseImpl) GetApplicationPlanEnvBindingPolicy(ctx context. return &envBindingSpec, nil } +// nolint func (c *applicationUsecaseImpl) createApplictionPlanEnvBindingPolicy(ctx context.Context, app *model.ApplicationPlan, envbinds apisv1.EnvBindList) (*model.ApplicationPolicyPlan, error) { policy := &model.ApplicationPolicyPlan{ AppPrimaryKey: app.PrimaryKey(), diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go index e5633d011..699f5bc51 100644 --- a/pkg/apiserver/rest/utils/bcode/addon.go +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -46,11 +46,14 @@ var ( ErrGetAddonApplication = NewBcode(500, 50013, "fail to get addon application") ) +// IsGithubRateLimit check if error is github rate limit func IsGithubRateLimit(err error) bool { + // nolint _, ok := err.(*github.RateLimitError) return ok } +// WrapGithubRateLimitErr wraps error if it is github rate limit func WrapGithubRateLimitErr(err error) error { if IsGithubRateLimit(err) { return ErrAddonRegistryRateLimit diff --git a/pkg/appfile/appfile.go b/pkg/appfile/appfile.go index 32b97801c..2572254c4 100644 --- a/pkg/appfile/appfile.go +++ b/pkg/appfile/appfile.go @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/appfile/helm" @@ -46,6 +47,7 @@ import ( "github.com/oam-dev/kubevela/pkg/cue/process" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/workflow/step" ) // constant error information @@ -158,6 +160,7 @@ type Appfile struct { WorkflowMode common.WorkflowMode parser *Parser + app *v1beta1.Application } // Handler handles reconcile @@ -173,13 +176,20 @@ func (af *Appfile) PrepareWorkflowAndPolicy() (policies []*unstructured.Unstruct return } - af.generateSteps() - return + af.WorkflowSteps, err = step.NewChainWorkflowStepGenerator( + &step.Deploy2EnvWorkflowStepGenerator{}, + &step.ApplyComponentWorkflowStepGenerator{}, + ).Generate(af.app, af.WorkflowSteps) + + return policies, err } func (af *Appfile) generateUnstructureds(workloads []*Workload) ([]*unstructured.Unstructured, error) { var uns []*unstructured.Unstructured for _, wl := range workloads { + if wl.Type == v1alpha1.EnvBindingPolicyType { + continue + } un, err := generateUnstructuredFromCUEModule(wl, af.Name, af.AppRevisionName, af.Namespace, af.Components, af.Artifacts) if err != nil { return nil, err @@ -193,20 +203,6 @@ func (af *Appfile) generateUnstructureds(workloads []*Workload) ([]*unstructured return uns, nil } -func (af *Appfile) generateSteps() { - if len(af.WorkflowSteps) == 0 { - for _, comp := range af.Components { - af.WorkflowSteps = append(af.WorkflowSteps, v1beta1.WorkflowStep{ - Name: comp.Name, - Type: "apply-component", - Properties: util.Object2RawExtension(map[string]string{ - "component": comp.Name, - }), - }) - } - } -} - func generateUnstructuredFromCUEModule(wl *Workload, appName, revision, ns string, components []common.ApplicationComponent, artifacts []*types.ComponentManifest) (*unstructured.Unstructured, error) { pCtx := process.NewPolicyContext(ns, wl.Name, appName, revision, components) pCtx.PushData(model.ContextDataArtifacts, prepareArtifactsData(artifacts)) @@ -264,13 +260,15 @@ func (af *Appfile) GenerateComponentManifest(wl *Workload) (*types.ComponentMani if af.Namespace == "" { af.Namespace = corev1.NamespaceDefault } + // generate context here to avoid nil pointer panic + wl.Ctx = NewBasicContext(af.Name, wl.Name, af.AppRevisionName, af.Namespace, wl.Params) switch wl.CapabilityCategory { case types.HelmCategory: return generateComponentFromHelmModule(wl, af.Name, af.AppRevisionName, af.Namespace) case types.KubeCategory: return generateComponentFromKubeModule(wl, af.Name, af.AppRevisionName, af.Namespace) case types.TerraformCategory: - return generateComponentFromTerraformModule(wl, af.Name, af.AppRevisionName, af.Namespace) + return generateComponentFromTerraformModule(wl, af.Name, af.Namespace) default: return generateComponentFromCUEModule(wl, af.Name, af.AppRevisionName, af.Namespace) } @@ -441,18 +439,20 @@ func (af *Appfile) setWorkloadRefToTrait(wlRef corev1.ObjectReference, trait *un // PrepareProcessContext prepares a DSL process Context func PrepareProcessContext(wl *Workload, applicationName, revision, namespace string) (process.Context, error) { - pCtx := NewBasicContext(wl, applicationName, revision, namespace) - if err := wl.EvalContext(pCtx); err != nil { + if wl.Ctx == nil { + wl.Ctx = NewBasicContext(applicationName, wl.Name, revision, namespace, wl.Params) + } + if err := wl.EvalContext(wl.Ctx); err != nil { return nil, errors.Wrapf(err, "evaluate base template app=%s in namespace=%s", applicationName, namespace) } - return pCtx, nil + return wl.Ctx, nil } // NewBasicContext prepares a basic DSL process Context -func NewBasicContext(wl *Workload, applicationName, revision, namespace string) process.Context { - pCtx := process.NewContext(namespace, wl.Name, applicationName, revision) - if wl.Params != nil { - pCtx.SetParameters(wl.Params) +func NewBasicContext(applicationName, workloadName, revision, namespace string, params map[string]interface{}) process.Context { + pCtx := process.NewContext(namespace, workloadName, applicationName, revision) + if params != nil { + pCtx.SetParameters(params) } return pCtx } @@ -462,18 +462,15 @@ func generateComponentFromCUEModule(wl *Workload, appName, revision, ns string) if err != nil { return nil, err } - wl.Ctx = pCtx return baseGenerateComponent(pCtx, wl, appName, ns) } -func generateComponentFromTerraformModule(wl *Workload, appName, revision, ns string) (*types.ComponentManifest, error) { - pCtx := NewBasicContext(wl, appName, revision, ns) - return baseGenerateComponent(pCtx, wl, appName, ns) +func generateComponentFromTerraformModule(wl *Workload, appName, ns string) (*types.ComponentManifest, error) { + return baseGenerateComponent(wl.Ctx, wl, appName, ns) } func baseGenerateComponent(pCtx process.Context, wl *Workload, appName, ns string) (*types.ComponentManifest, error) { var err error - for _, tr := range wl.Traits { if err := tr.EvalContext(pCtx); err != nil { return nil, errors.Wrapf(err, "evaluate template trait=%s app=%s", tr.Name, wl.Name) diff --git a/pkg/appfile/appfile_test.go b/pkg/appfile/appfile_test.go index 8f328feb0..0ba07fc56 100644 --- a/pkg/appfile/appfile_test.go +++ b/pkg/appfile/appfile_test.go @@ -390,19 +390,6 @@ wait: op.#ConditionalWait & { notCueStepDef.Namespace = "default" err = k8sClient.Create(context.Background(), ¬CueStepDef) Expect(err).To(BeNil()) - - appfile := &Appfile{ - Components: []common.ApplicationComponent{ - { - Name: "test1", - }, - { - Name: "test2", - }, - }, - } - appfile.generateSteps() - Expect(len(appfile.WorkflowSteps)).Should(Equal(2)) }) }) @@ -469,6 +456,7 @@ spec: engine: definition.NewWorkloadAbstractEngine("test-policy", pd), }, }, + app: &v1beta1.Application{}, } _, err := testAppfile.GenerateComponentManifests() Expect(err).Should(BeNil()) @@ -884,7 +872,7 @@ variable "password" { revision: "v1", } - pCtx := NewBasicContext(args.wl, args.appName, args.revision, ns) + pCtx := NewBasicContext(args.appName, args.wl.Name, args.revision, ns, args.wl.Params) comp, err := evalWorkloadWithContext(pCtx, args.wl, ns, args.appName, compName) Expect(comp.StandardWorkload).ShouldNot(BeNil()) Expect(comp.Name).Should(Equal("")) diff --git a/pkg/appfile/parser.go b/pkg/appfile/parser.go index 79ab91b84..63e74f598 100644 --- a/pkg/appfile/parser.go +++ b/pkg/appfile/parser.go @@ -146,6 +146,7 @@ func (p *Parser) newAppfile(appName, ns string, app *v1beta1.Application) *Appfi RelatedScopeDefinitions: make(map[string]*v1beta1.ScopeDefinition), parser: p, + app: app, } for k, v := range app.Annotations { file.AppAnnotations[k] = v diff --git a/pkg/clustermanager/cluster_manager.go b/pkg/clustermanager/cluster_manager.go index 7c600ca69..6e1c2e1ee 100644 --- a/pkg/clustermanager/cluster_manager.go +++ b/pkg/clustermanager/cluster_manager.go @@ -17,9 +17,21 @@ limitations under the License. package clustermanager import ( + "context" + "fmt" + + "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + v13 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + errors2 "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/clientcmd" + v12 "open-cluster-management.io/api/cluster/v1" "sigs.k8s.io/controller-runtime/pkg/client" + types2 "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/utils/common" ) @@ -35,3 +47,93 @@ func GetClient(kubeConfigData []byte) (client.Client, error) { } return client.New(restConfig, client.Options{Scheme: common.Scheme}) } + +// GetRegisteredClusters will get all registered clusters in control plane +func GetRegisteredClusters(c client.Client) ([]types2.Cluster, error) { + var clusters []types2.Cluster + secrets := v1.SecretList{} + if err := c.List(context.Background(), &secrets, client.HasLabels{v1alpha1.LabelKeyClusterCredentialType}, client.InNamespace(multicluster.ClusterGatewaySecretNamespace)); err != nil { + return nil, errors.Wrapf(err, "failed to get clusterSecret secrets") + } + for _, clusterSecret := range secrets.Items { + clusters = append(clusters, types2.Cluster{ + Name: clusterSecret.Name, + Type: clusterSecret.GetLabels()[v1alpha1.LabelKeyClusterCredentialType], + EndPoint: string(clusterSecret.Data["endpoint"]), + }) + } + + crdName := types.NamespacedName{Name: "managedclusters." + v12.GroupName} + if err := c.Get(context.Background(), crdName, &v13.CustomResourceDefinition{}); err != nil { + if errors2.IsNotFound(err) { + return clusters, nil + } + return nil, err + } + + managedClusters := v12.ManagedClusterList{} + if err := c.List(context.Background(), &managedClusters); err != nil { + return nil, errors.Wrapf(err, "failed to get managed clusters") + } + for _, cluster := range managedClusters.Items { + if len(cluster.Spec.ManagedClusterClientConfigs) != 0 { + clusters = append(clusters, types2.Cluster{ + Name: cluster.Name, + Type: "ManagedCluster", + EndPoint: cluster.Spec.ManagedClusterClientConfigs[0].URL, + }) + } + } + return clusters, nil +} + +// EnsureClusterNotExists will check the cluster is not existed in control plane +func EnsureClusterNotExists(c client.Client, clusterName string) error { + exist, err := clusterExists(c, clusterName) + if err != nil { + return err + } + if exist { + return fmt.Errorf("cluster %s already exists", clusterName) + } + return nil +} + +// EnsureClusterExists will check the cluster is existed in control plane +func EnsureClusterExists(c client.Client, clusterName string) error { + exist, err := clusterExists(c, clusterName) + if err != nil { + return err + } + if !exist { + return fmt.Errorf("cluster %s not exists", clusterName) + } + return nil +} + +// clusterExists will check whether the cluster exist or not +func clusterExists(c client.Client, clusterName string) (bool, error) { + err := c.Get(context.Background(), types.NamespacedName{Name: clusterName, Namespace: multicluster.ClusterGatewaySecretNamespace}, &v1.Secret{}) + if err == nil { + return true, nil + } + if !errors2.IsNotFound(err) { + return false, errors.Wrapf(err, "failed to check duplicate cluster") + } + + crdName := types.NamespacedName{Name: "managedclusters." + v12.GroupName} + if err = c.Get(context.Background(), crdName, &v13.CustomResourceDefinition{}); err != nil { + if errors2.IsNotFound(err) { + return false, nil + } + return false, errors.Wrapf(err, "failed to get managedcluster CRD to check duplicate cluster") + } + err = c.Get(context.Background(), types.NamespacedName{Name: clusterName, Namespace: multicluster.ClusterGatewaySecretNamespace}, &v12.ManagedCluster{}) + if err == nil { + return true, nil + } + if !errors2.IsNotFound(err) { + return false, errors.Wrapf(err, "failed to check duplicate cluster") + } + return false, nil +} diff --git a/pkg/controller/common/context.go b/pkg/controller/common/context.go index ace5b4b05..fab0daa4b 100644 --- a/pkg/controller/common/context.go +++ b/pkg/controller/common/context.go @@ -26,11 +26,12 @@ var ( PerfEnabled = false ) -const ( - reconcileTimeout = time.Minute +var ( + // ReconcileTimeout timeout for controller to reconcile + ReconcileTimeout = time.Minute * 3 ) // NewReconcileContext create context with default timeout (60s) func NewReconcileContext(ctx context.Context) (context.Context, context.CancelFunc) { - return context.WithTimeout(ctx, reconcileTimeout) + return context.WithTimeout(ctx, ReconcileTimeout) } diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/binding.go b/pkg/controller/core.oam.dev/v1alpha1/envbinding/binding.go deleted file mode 100644 index f61ad3a89..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/binding.go +++ /dev/null @@ -1,327 +0,0 @@ -/* - 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 envbinding - -import ( - "context" - "encoding/json" - - "github.com/imdario/mergo" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - kerrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/utils/pointer" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/appfile" - "github.com/oam-dev/kubevela/pkg/oam" - "github.com/oam-dev/kubevela/pkg/oam/util" -) - -// EnvBindApp describes the app bound to the environment -type EnvBindApp struct { - baseApp *v1beta1.Application - PatchedApp *v1beta1.Application - envConfig *v1alpha1.EnvConfig - - componentManifests []*types.ComponentManifest - assembledManifests map[string][]*unstructured.Unstructured - - ScheduledManifests map[string]*unstructured.Unstructured -} - -// NewEnvBindApp create EnvBindApp -func NewEnvBindApp(base *v1beta1.Application, envConfig *v1alpha1.EnvConfig) *EnvBindApp { - return &EnvBindApp{ - baseApp: base, - envConfig: envConfig, - } -} - -// GenerateConfiguredApplication patch component parameters to base Application -func (e *EnvBindApp) GenerateConfiguredApplication() error { - newApp := e.baseApp.DeepCopy() - - var baseComponent *common.ApplicationComponent - var misMatchedIdxs []int - for patchIdx := range e.envConfig.Patch.Components { - var matchedIdx int - isMatched := false - patchComponent := e.envConfig.Patch.Components[patchIdx] - - for baseIdx := range e.baseApp.Spec.Components { - component := e.baseApp.Spec.Components[baseIdx] - if patchComponent.Name == component.Name && patchComponent.Type == component.Type { - matchedIdx, baseComponent = baseIdx, &component - isMatched = true - break - } - } - if !isMatched || baseComponent == nil { - misMatchedIdxs = append(misMatchedIdxs, patchIdx) - continue - } - targetComponent, err := PatchComponent(baseComponent, &patchComponent) - if err != nil { - return err - } - newApp.Spec.Components[matchedIdx] = *targetComponent - } - for _, idx := range misMatchedIdxs { - newApp.Spec.Components = append(newApp.Spec.Components, e.envConfig.Patch.Components[idx]) - } - // select which components to use - if e.envConfig.Selector != nil { - compMap := make(map[string]bool) - if len(e.envConfig.Selector.Components) > 0 { - for _, comp := range e.envConfig.Selector.Components { - compMap[comp] = true - } - } - comps := make([]common.ApplicationComponent, 0) - for _, comp := range newApp.Spec.Components { - if _, ok := compMap[comp.Name]; ok { - comps = append(comps, comp) - } - } - newApp.Spec.Components = comps - } - e.PatchedApp = newApp - return nil -} - -func (e *EnvBindApp) render(ctx context.Context, appParser *appfile.Parser) error { - if e.PatchedApp == nil { - return errors.New("EnvBindApp must has been generated a configured Application") - } - ctx = util.SetNamespaceInCtx(ctx, e.PatchedApp.Namespace) - appFile, err := appParser.GenerateAppFile(ctx, e.PatchedApp) - if err != nil { - return err - } - comps, err := appFile.GenerateComponentManifests() - if err != nil { - return err - } - e.componentManifests = comps - return nil -} - -func (e *EnvBindApp) assemble() error { - if e.componentManifests == nil { - return errors.New("EnvBindApp must has been rendered") - } - - assembledManifests := make(map[string][]*unstructured.Unstructured, len(e.componentManifests)) - for _, comp := range e.componentManifests { - resources := make([]*unstructured.Unstructured, len(comp.Traits)+1) - workload := comp.StandardWorkload - workload.SetName(comp.Name) - e.SetNamespace(workload) - util.AddLabels(workload, map[string]string{oam.LabelOAMResourceType: oam.ResourceTypeWorkload}) - resources[0] = workload - - for i := 0; i < len(comp.Traits); i++ { - trait := comp.Traits[i] - util.AddLabels(trait, map[string]string{oam.LabelOAMResourceType: oam.ResourceTypeTrait}) - e.SetTraitName(comp.Name, trait) - e.SetNamespace(trait) - resources[i+1] = trait - } - assembledManifests[comp.Name] = resources - - if len(comp.PackagedWorkloadResources) != 0 { - assembledManifests[comp.Name] = append(assembledManifests[comp.Name], comp.PackagedWorkloadResources...) - } - } - e.assembledManifests = assembledManifests - return nil -} - -// SetTraitName set name for Trait -func (e *EnvBindApp) SetTraitName(compName string, trait *unstructured.Unstructured) { - if len(trait.GetName()) == 0 { - traitType := trait.GetLabels()[oam.TraitTypeLabel] - traitName := util.GenTraitNameCompatible(compName, trait, traitType) - trait.SetName(traitName) - } -} - -// SetNamespace set namespace for *unstructured.Unstructured -func (e *EnvBindApp) SetNamespace(resource *unstructured.Unstructured) { - if len(resource.GetNamespace()) != 0 { - return - } - appNs := e.PatchedApp.Namespace - if len(appNs) == 0 { - appNs = "default" - } - resource.SetNamespace(appNs) -} - -// CreateEnvBindApps create EnvBindApps from different envs -func CreateEnvBindApps(envBinding *v1alpha1.EnvBinding, baseApp *v1beta1.Application) ([]*EnvBindApp, error) { - envBindApps := make([]*EnvBindApp, len(envBinding.Spec.Envs)) - for i := range envBinding.Spec.Envs { - env := envBinding.Spec.Envs[i] - envBindApp := NewEnvBindApp(baseApp, &env) - err := envBindApp.GenerateConfiguredApplication() - if err != nil { - return nil, errors.WithMessagef(err, "failed to patch parameter for env %s", env.Name) - } - envBindApps[i] = envBindApp - } - return envBindApps, nil -} - -// RenderEnvBindApps render EnvBindApps -func RenderEnvBindApps(ctx context.Context, envBindApps []*EnvBindApp, appParser *appfile.Parser) error { - for _, envBindApp := range envBindApps { - err := envBindApp.render(ctx, appParser) - if err != nil { - return errors.WithMessagef(err, "fail to render application for env %s", envBindApp.envConfig.Name) - } - } - return nil -} - -// AssembleEnvBindApps assemble resource for EnvBindApp -func AssembleEnvBindApps(envBindApps []*EnvBindApp) error { - for _, envBindApp := range envBindApps { - err := envBindApp.assemble() - if err != nil { - return errors.WithMessagef(err, "fail to assemble resource for application in env %s", envBindApp.envConfig.Name) - } - } - return nil -} - -// PatchComponent patch component parameter to target component parameter -func PatchComponent(baseComponent *common.ApplicationComponent, patchComponent *common.ApplicationComponent) (*common.ApplicationComponent, error) { - targetComponent := baseComponent.DeepCopy() - - mergedProperties, err := PatchProperties(baseComponent.Properties, patchComponent.Properties) - if err != nil { - return nil, errors.WithMessagef(err, "fail to patch properties for component %s", baseComponent.Name) - } - targetComponent.Properties = util.Object2RawExtension(mergedProperties) - - var baseTrait *common.ApplicationTrait - var misMatchedIdxs []int - for patchIdx := range patchComponent.Traits { - var matchedIdx int - isMatched := false - patchTrait := patchComponent.Traits[patchIdx] - - for index := range targetComponent.Traits { - trait := targetComponent.Traits[index] - if patchTrait.Type == trait.Type { - matchedIdx, baseTrait = index, &trait - isMatched = true - break - } - } - if !isMatched || baseTrait == nil { - misMatchedIdxs = append(misMatchedIdxs, patchIdx) - continue - } - mergedProperties, err = PatchProperties(baseTrait.Properties, patchTrait.Properties) - if err != nil { - return nil, err - } - targetComponent.Traits[matchedIdx].Properties = util.Object2RawExtension(mergedProperties) - } - - for _, idx := range misMatchedIdxs { - targetComponent.Traits = append(targetComponent.Traits, patchComponent.Traits[idx]) - } - return targetComponent, nil -} - -// PatchProperties merge patch parameter for dst parameter -func PatchProperties(dst *runtime.RawExtension, patch *runtime.RawExtension) (map[string]interface{}, error) { - patchParameter, err := util.RawExtension2Map(patch) - if err != nil { - return nil, err - } - baseParameter, err := util.RawExtension2Map(dst) - if err != nil { - return nil, err - } - if baseParameter == nil { - baseParameter = make(map[string]interface{}) - } - opts := []func(*mergo.Config){ - // WithOverride will make merge override non-empty dst attributes with non-empty src attributes values. - mergo.WithOverride, - } - err = mergo.Merge(&baseParameter, patchParameter, opts...) - if err != nil { - return nil, err - } - return baseParameter, nil -} - -// StoreManifest2ConfigMap store manifest to configmap -func StoreManifest2ConfigMap(ctx context.Context, cli client.Client, envBinding *v1alpha1.EnvBinding, apps []*EnvBindApp) error { - cm := new(corev1.ConfigMap) - data := make(map[string]string) - for _, app := range apps { - m := make(map[string]map[string]interface{}) - for name, manifest := range app.ScheduledManifests { - m[name] = manifest.UnstructuredContent() - } - d, err := json.Marshal(m) - if err != nil { - return errors.Wrapf(err, "fail to marshal patched application for env %s", app.envConfig.Name) - } - data[app.envConfig.Name] = string(d) - } - cm.Data = data - cm.SetName(envBinding.Spec.OutputResourcesTo.Name) - if len(envBinding.Spec.OutputResourcesTo.Namespace) == 0 { - cm.SetNamespace("default") - } else { - cm.SetNamespace(envBinding.Spec.OutputResourcesTo.Namespace) - } - - ownerReference := []metav1.OwnerReference{{ - APIVersion: envBinding.APIVersion, - Kind: envBinding.Kind, - Name: envBinding.Name, - UID: envBinding.GetUID(), - Controller: pointer.BoolPtr(true), - BlockOwnerDeletion: pointer.BoolPtr(true), - }} - cm.SetOwnerReferences(ownerReference) - - cmCopy := cm.DeepCopy() - if err := cli.Get(ctx, client.ObjectKey{Namespace: cmCopy.Namespace, Name: cmCopy.Name}, cmCopy); err != nil { - if kerrors.IsNotFound(err) { - return cli.Create(ctx, cm) - } - return err - } - return cli.Update(ctx, cm) -} diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/cluster_gateway_engine.go b/pkg/controller/core.oam.dev/v1alpha1/envbinding/cluster_gateway_engine.go deleted file mode 100644 index 1405897bf..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/cluster_gateway_engine.go +++ /dev/null @@ -1,131 +0,0 @@ -/* - 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 envbinding - -import ( - "context" - - "github.com/pkg/errors" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/pkg/appfile" - "github.com/oam-dev/kubevela/pkg/multicluster" -) - -const ( - // OverrideNamespaceLabelKey identifies the override namespace for patched Application - OverrideNamespaceLabelKey = "envbinding.oam.dev/override-namespace" -) - -// ClusterGatewayEngine construct the multicluster engine of using cluster-gateway -type ClusterGatewayEngine struct { - client.Client - envBindingName string - clusterDecisions map[string]v1alpha1.ClusterDecision -} - -// NewClusterGatewayEngine create multicluster engine to use cluster-gateway -func NewClusterGatewayEngine(cli client.Client, envBindingName string) ClusterManagerEngine { - return &ClusterGatewayEngine{ - Client: cli, - envBindingName: envBindingName, - } -} - -// TODO only support single cluster name and namespace name now, should support label selector -func (engine *ClusterGatewayEngine) prepare(ctx context.Context, configs []v1alpha1.EnvConfig) error { - engine.clusterDecisions = make(map[string]v1alpha1.ClusterDecision) - locationToConfig := make(map[string]string) - for _, config := range configs { - var namespace, clusterName string - // check if namespace selector is valid - if config.Placement.NamespaceSelector != nil { - if len(config.Placement.NamespaceSelector.Labels) != 0 { - return errors.Errorf("invalid env %s: namespace selector in cluster-gateway does not support label selector for now", config.Name) - } - namespace = config.Placement.NamespaceSelector.Name - } - // check if cluster selector is valid - if config.Placement.ClusterSelector != nil { - if len(config.Placement.ClusterSelector.Labels) != 0 { - return errors.Errorf("invalid env %s: cluster selector does not support label selector for now", config.Name) - } - clusterName = config.Placement.ClusterSelector.Name - } - // set fallback cluster - if clusterName == "" { - clusterName = multicluster.ClusterLocalName - } - // check if current environment uses the same cluster and namespace as resource destination with other environment, if yes, a conflict occurs - location := clusterName + "/" + namespace - if dupConfigName, ok := locationToConfig[location]; ok { - return errors.Errorf("invalid env %s: location %s conflict with env %s", config.Name, location, dupConfigName) - } - locationToConfig[clusterName] = config.Name - // check if target cluster exists - if clusterName != multicluster.ClusterLocalName { - if err := engine.Get(ctx, types.NamespacedName{Namespace: multicluster.ClusterGatewaySecretNamespace, Name: clusterName}, &v1.Secret{}); err != nil { - return errors.Wrapf(err, "failed to get cluster %s for env %s", clusterName, config.Name) - } - } - engine.clusterDecisions[config.Name] = v1alpha1.ClusterDecision{Env: config.Name, Cluster: clusterName, Namespace: namespace} - } - return nil -} - -func (engine *ClusterGatewayEngine) initEnvBindApps(ctx context.Context, envBinding *v1alpha1.EnvBinding, baseApp *v1beta1.Application, appParser *appfile.Parser) ([]*EnvBindApp, error) { - return CreateEnvBindApps(envBinding, baseApp) -} - -func (engine *ClusterGatewayEngine) schedule(ctx context.Context, apps []*EnvBindApp) ([]v1alpha1.ClusterDecision, error) { - for _, app := range apps { - app.ScheduledManifests = make(map[string]*unstructured.Unstructured) - clusterName := engine.clusterDecisions[app.envConfig.Name].Cluster - namespace := engine.clusterDecisions[app.envConfig.Name].Namespace - raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(app.PatchedApp) - if err != nil { - return nil, errors.Wrapf(err, "failed to convert app [Env: %s](%s/%s) into unstructured", app.envConfig.Name, app.PatchedApp.Namespace, app.PatchedApp.Name) - } - patchedApp := &unstructured.Unstructured{Object: raw} - multicluster.SetClusterName(patchedApp, clusterName) - SetOverrideNamespace(patchedApp, namespace) - app.ScheduledManifests[patchedApp.GetName()] = patchedApp - } - var decisions []v1alpha1.ClusterDecision - for _, decision := range engine.clusterDecisions { - decisions = append(decisions, decision) - } - return decisions, nil -} - -// SetOverrideNamespace set the override namespace for object in its label -func SetOverrideNamespace(obj *unstructured.Unstructured, overrideNamespace string) { - if overrideNamespace != "" { - labels := obj.GetLabels() - if labels == nil { - labels = map[string]string{} - } - labels[OverrideNamespaceLabelKey] = overrideNamespace - obj.SetLabels(labels) - } -} diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/engine.go b/pkg/controller/core.oam.dev/v1alpha1/envbinding/engine.go deleted file mode 100644 index ef7a34645..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/engine.go +++ /dev/null @@ -1,377 +0,0 @@ -/* - 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 envbinding - -import ( - "context" - "fmt" - "reflect" - - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - kerrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/klog/v2" - ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1" - ocmworkv1 "open-cluster-management.io/api/work/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/pkg/appfile" - "github.com/oam-dev/kubevela/pkg/oam/util" -) - -// ClusterManagerEngine defines Cluster Manage interface -type ClusterManagerEngine interface { - prepare(ctx context.Context, configs []v1alpha1.EnvConfig) error - initEnvBindApps(ctx context.Context, envBinding *v1alpha1.EnvBinding, baseApp *v1beta1.Application, appParser *appfile.Parser) ([]*EnvBindApp, error) - schedule(ctx context.Context, apps []*EnvBindApp) ([]v1alpha1.ClusterDecision, error) -} - -// OCMEngine represents Open-Cluster-Management multi-cluster management solution -type OCMEngine struct { - cli client.Client - clusterDecisions map[string]string - appNs string - envBindingName string - appName string -} - -// NewOCMEngine create Open-Cluster-Management ClusterManagerEngine -func NewOCMEngine(cli client.Client, appName, appNs, envBindingName string) ClusterManagerEngine { - return &OCMEngine{ - cli: cli, - appNs: appNs, - appName: appName, - envBindingName: envBindingName, - } -} - -// prepare complete the pre-work of cluster scheduling and select the target cluster -// 1) if user directly specify the cluster name, Prepare will do nothing -// 2) if user use Labels to select the target cluster, Prepare will create the Placement to select cluster -func (o *OCMEngine) prepare(ctx context.Context, configs []v1alpha1.EnvConfig) error { - var err error - for _, config := range configs { - if len(config.Placement.ClusterSelector.Name) != 0 { - continue - } - err = o.dispatchPlacement(ctx, config) - if err != nil { - return err - } - } - - clusterDecisions := make(map[string]string) - for _, config := range configs { - if len(config.Placement.ClusterSelector.Name) != 0 { - clusterDecisions[config.Name] = config.Placement.ClusterSelector.Name - continue - } - placementName := generatePlacementName(o.appName, config.Name) - clusterDecisions[config.Name], err = o.getSelectedCluster(ctx, placementName, o.appNs) - if err != nil { - return err - } - } - o.clusterDecisions = clusterDecisions - return nil -} - -func (o *OCMEngine) initEnvBindApps(ctx context.Context, envBinding *v1alpha1.EnvBinding, baseApp *v1beta1.Application, appParser *appfile.Parser) ([]*EnvBindApp, error) { - envBindApps, err := CreateEnvBindApps(envBinding, baseApp) - if err != nil { - return nil, err - } - if err = RenderEnvBindApps(ctx, envBindApps, appParser); err != nil { - return nil, err - } - if err = AssembleEnvBindApps(envBindApps); err != nil { - return nil, err - } - return envBindApps, nil -} - -// Schedule decides which cluster the apps is scheduled to -func (o *OCMEngine) schedule(ctx context.Context, apps []*EnvBindApp) ([]v1alpha1.ClusterDecision, error) { - var clusterDecisions []v1alpha1.ClusterDecision - - for i := range apps { - app := apps[i] - app.ScheduledManifests = make(map[string]*unstructured.Unstructured, 1) - clusterName := o.clusterDecisions[app.envConfig.Name] - manifestWork := new(ocmworkv1.ManifestWork) - workloads := make([]ocmworkv1.Manifest, 0, len(app.assembledManifests)) - for _, component := range app.PatchedApp.Spec.Components { - manifest := app.assembledManifests[component.Name] - for j := range manifest { - workloads = append(workloads, ocmworkv1.Manifest{ - RawExtension: *util.Object2RawExtension(manifest[j]), - }) - } - } - manifestWork.Spec.Workload.Manifests = workloads - obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(manifestWork) - if err != nil { - return nil, err - } - unstructuredManifestWork := &unstructured.Unstructured{ - Object: obj, - } - unstructuredManifestWork.SetGroupVersionKind(ocmworkv1.GroupVersion.WithKind(reflect.TypeOf(ocmworkv1.ManifestWork{}).Name())) - envBindAppName := constructEnvBindAppName(o.envBindingName, app.envConfig.Name, o.appName) - unstructuredManifestWork.SetName(o.appName) - unstructuredManifestWork.SetNamespace(clusterName) - app.ScheduledManifests[envBindAppName] = unstructuredManifestWork - } - - for env, cluster := range o.clusterDecisions { - clusterDecisions = append(clusterDecisions, v1alpha1.ClusterDecision{ - Env: env, - Cluster: cluster, - }) - } - return clusterDecisions, nil -} - -// dispatchPlacement dispatch Placement Object of OCM for cluster selected -func (o *OCMEngine) dispatchPlacement(ctx context.Context, config v1alpha1.EnvConfig) error { - placement := new(ocmclusterv1alpha1.Placement) - placementName := generatePlacementName(o.appName, config.Name) - placement.SetName(placementName) - placement.SetNamespace(o.appNs) - - clusterNum := int32(1) - placement.Spec.NumberOfClusters = &clusterNum - placement.Spec.Predicates = []ocmclusterv1alpha1.ClusterPredicate{{ - RequiredClusterSelector: ocmclusterv1alpha1.ClusterSelector{ - LabelSelector: metav1.LabelSelector{ - MatchLabels: config.Placement.ClusterSelector.Labels, - }, - }, - }} - - oldPd := new(ocmclusterv1alpha1.Placement) - if err := o.cli.Get(ctx, client.ObjectKey{Namespace: placement.Namespace, Name: placement.Name}, oldPd); err != nil { - if kerrors.IsNotFound(err) { - return o.cli.Create(ctx, placement) - } - return err - } - return o.cli.Patch(ctx, placement, client.Merge) -} - -// getSelectedCluster get selected cluster from PlacementDecision -func (o *OCMEngine) getSelectedCluster(ctx context.Context, name, namespace string) (string, error) { - var clusterName string - listOpts := []client.ListOption{ - client.MatchingLabels{ - "cluster.open-cluster-management.io/placement": name, - }, - client.InNamespace(namespace), - } - - pdList := new(ocmclusterv1alpha1.PlacementDecisionList) - err := o.cli.List(ctx, pdList, listOpts...) - if err != nil { - return "", err - } - if len(pdList.Items) < 1 { - return "", errors.New("fail to get PlacementDecision") - } - - if len(pdList.Items[0].Status.Decisions) < 1 { - return "", errors.New("no matched cluster") - } - clusterName = pdList.Items[0].Status.Decisions[0].ClusterName - return clusterName, nil -} - -// generatePlacementName generate placementName from app Name and env Name -func generatePlacementName(appName, envName string) string { - return fmt.Sprintf("%s-%s", appName, envName) -} - -// SingleClusterEngine represents deploy resources to the local cluster -type SingleClusterEngine struct { - cli client.Client - appNs string - appName string - envBindingName string - clusterDecisions map[string]string - namespaceDecisions map[string]string -} - -// NewSingleClusterEngine create a single cluster ClusterManagerEngine -func NewSingleClusterEngine(cli client.Client, appName, appNs, envBindingName string) ClusterManagerEngine { - return &SingleClusterEngine{ - cli: cli, - appNs: appNs, - appName: appName, - envBindingName: envBindingName, - } -} - -func (s *SingleClusterEngine) prepare(ctx context.Context, configs []v1alpha1.EnvConfig) error { - clusterDecisions := make(map[string]string) - for _, config := range configs { - clusterDecisions[config.Name] = string(v1alpha1.SingleClusterEngine) - } - s.clusterDecisions = clusterDecisions - return nil -} - -func (s *SingleClusterEngine) initEnvBindApps(ctx context.Context, envBinding *v1alpha1.EnvBinding, baseApp *v1beta1.Application, appParser *appfile.Parser) ([]*EnvBindApp, error) { - return CreateEnvBindApps(envBinding, baseApp) -} - -func (s *SingleClusterEngine) schedule(ctx context.Context, apps []*EnvBindApp) ([]v1alpha1.ClusterDecision, error) { - var clusterDecisions []v1alpha1.ClusterDecision - namespaceDecisions := make(map[string]string) - for i := range apps { - app := apps[i] - - selectedNamespace, err := s.getSelectedNamespace(ctx, app) - namespaceDecisions[app.envConfig.Name] = selectedNamespace - if err != nil { - return nil, err - } - - app.ScheduledManifests = make(map[string]*unstructured.Unstructured, 1) - unstructuredApp, err := util.Object2Unstructured(app.PatchedApp) - if err != nil { - return nil, err - } - envBindAppName := constructEnvBindAppName(s.envBindingName, app.envConfig.Name, s.appName) - unstructuredApp.SetNamespace(selectedNamespace) - app.ScheduledManifests[envBindAppName] = unstructuredApp - } - - s.namespaceDecisions = namespaceDecisions - for env, cluster := range s.clusterDecisions { - clusterDecisions = append(clusterDecisions, v1alpha1.ClusterDecision{ - Env: env, - Cluster: cluster, - Namespace: s.namespaceDecisions[env], - }) - } - return clusterDecisions, nil -} - -func (s *SingleClusterEngine) getSelectedNamespace(ctx context.Context, envBindApp *EnvBindApp) (string, error) { - if envBindApp.envConfig.Placement.NamespaceSelector != nil { - selector := envBindApp.envConfig.Placement.NamespaceSelector - if len(selector.Name) != 0 { - return selector.Name, nil - } - if len(selector.Labels) != 0 { - namespaceList := new(corev1.NamespaceList) - listOpts := []client.ListOption{ - client.MatchingLabels(selector.Labels), - } - err := s.cli.List(ctx, namespaceList, listOpts...) - if err != nil || len(namespaceList.Items) == 0 { - return "", errors.Wrapf(err, "fail to list selected namespace for env %s", envBindApp.envConfig.Name) - } - return namespaceList.Items[0].Name, nil - } - } - return envBindApp.PatchedApp.Namespace, nil -} - -func validatePlacement(envBinding *v1alpha1.EnvBinding) error { - if envBinding.Spec.Engine == v1alpha1.OCMEngine || len(envBinding.Spec.Engine) == 0 { - for _, config := range envBinding.Spec.Envs { - if config.Placement.ClusterSelector == nil { - return errors.New("the cluster selector of placement shouldn't be empty") - } - } - } - return nil -} - -func constructEnvBindAppName(envBindingName, envName, appName string) string { - return fmt.Sprintf("%s-%s-%s", envBindingName, envName, appName) -} - -func constructResourceTrackerName(envBindingName, namespace string) string { - return fmt.Sprintf("%s-%s-%s", "envbinding", envBindingName, namespace) -} - -func garbageCollect(ctx context.Context, k8sClient client.Client, envBinding *v1alpha1.EnvBinding, apps []*EnvBindApp) error { - rtRef := envBinding.Status.ResourceTracker - if rtRef == nil { - return nil - } - - rt := new(v1beta1.ResourceTracker) - if envBinding.Spec.OutputResourcesTo != nil && len(envBinding.Spec.OutputResourcesTo.Name) != 0 { - rt.SetName(rtRef.Name) - err := k8sClient.Delete(ctx, rt) - return client.IgnoreNotFound(err) - } - - rtKey := client.ObjectKey{Namespace: rtRef.Namespace, Name: rtRef.Name} - if err := k8sClient.Get(ctx, rtKey, rt); err != nil { - return err - } - var manifests []*unstructured.Unstructured - for _, app := range apps { - for _, obj := range app.ScheduledManifests { - manifests = append(manifests, obj) - } - } - for _, oldRsc := range rt.Status.TrackedResources { - isRemoved := true - for _, newRsc := range manifests { - if equalMateData(oldRsc, newRsc) { - isRemoved = false - break - } - } - if isRemoved { - if err := deleteOldResource(ctx, k8sClient, oldRsc); err != nil { - return err - } - klog.InfoS("Successfully GC a resource", "name", oldRsc.Name, "apiVersion", oldRsc.APIVersion, "kind", oldRsc.Kind) - } - } - return nil -} - -func equalMateData(rscRef corev1.ObjectReference, newRsc *unstructured.Unstructured) bool { - if rscRef.APIVersion == newRsc.GetAPIVersion() && rscRef.Kind == newRsc.GetKind() && - rscRef.Namespace == newRsc.GetNamespace() && rscRef.Name == newRsc.GetName() { - return true - } - return false -} - -func deleteOldResource(ctx context.Context, k8sClient client.Client, ref corev1.ObjectReference) error { - obj := new(unstructured.Unstructured) - obj.SetAPIVersion(ref.APIVersion) - obj.SetKind(ref.Kind) - obj.SetNamespace(ref.Namespace) - obj.SetName(ref.Name) - if err := k8sClient.Delete(ctx, obj); err != nil && !kerrors.IsNotFound(err) { - return errors.Wrapf(err, "cannot delete resource %v", ref) - } - return nil -} diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/envbinding_controller.go b/pkg/controller/core.oam.dev/v1alpha1/envbinding/envbinding_controller.go deleted file mode 100644 index 790177785..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/envbinding_controller.go +++ /dev/null @@ -1,321 +0,0 @@ -/* - 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 envbinding - -import ( - "context" - - "github.com/crossplane/crossplane-runtime/pkg/event" - "github.com/crossplane/crossplane-runtime/pkg/meta" - "github.com/pkg/errors" - v1 "k8s.io/api/core/v1" - kerrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/util/retry" - "k8s.io/klog/v2" - "k8s.io/utils/pointer" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - - "github.com/oam-dev/kubevela/apis/core.oam.dev/condition" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/pkg/appfile" - common2 "github.com/oam-dev/kubevela/pkg/controller/common" - oamctrl "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" - "github.com/oam-dev/kubevela/pkg/cue/packages" - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" - "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/pkg/utils/apply" -) - -const ( - resourceTrackerFinalizer = "envbinding.oam.dev/resource-tracker-finalizer" -) - -// Reconciler reconciles a EnvBinding object -type Reconciler struct { - client.Client - dm discoverymapper.DiscoveryMapper - pd *packages.PackageDiscover - Scheme *runtime.Scheme - record event.Recorder - concurrentReconciles int -} - -// Reconcile is the main logic for EnvBinding controller -func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - ctx, cancel := common2.NewReconcileContext(ctx) - defer cancel() - klog.InfoS("Reconcile EnvBinding", "envbinding", klog.KRef(req.Namespace, req.Name)) - - envBinding := new(v1alpha1.EnvBinding) - if err := r.Client.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, envBinding); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - endReconcile, err := r.handleFinalizers(ctx, envBinding) - if err != nil { - return r.endWithNegativeCondition(ctx, envBinding, condition.ReconcileError(err)) - } - if endReconcile { - return ctrl.Result{}, nil - } - - if err := validatePlacement(envBinding); err != nil { - klog.ErrorS(err, "The placement is not compliant") - r.record.Event(envBinding, event.Warning("The placement is not compliant", err)) - return r.endWithNegativeCondition(ctx, envBinding, condition.ReconcileError(err)) - } - - baseApp, err := util.RawExtension2Application(envBinding.Spec.AppTemplate.RawExtension) - if err != nil { - klog.ErrorS(err, "Failed to parse AppTemplate of EnvBinding") - r.record.Event(envBinding, event.Warning("Failed to parse AppTemplate of EnvBinding", err)) - return r.endWithNegativeCondition(ctx, envBinding, condition.ReconcileError(err)) - } - - var engine ClusterManagerEngine - switch envBinding.Spec.Engine { - case v1alpha1.OCMEngine: - engine = NewOCMEngine(r.Client, baseApp.Name, baseApp.Namespace, envBinding.Name) - case v1alpha1.SingleClusterEngine: - engine = NewSingleClusterEngine(r.Client, baseApp.Name, baseApp.Namespace, envBinding.Name) - case v1alpha1.ClusterGatewayEngine: - engine = NewClusterGatewayEngine(r.Client, envBinding.Name) - default: - engine = NewClusterGatewayEngine(r.Client, envBinding.Name) - } - - // prepare the pre-work for cluster scheduling - envBinding.Status.Phase = v1alpha1.EnvBindingPrepare - if err = engine.prepare(ctx, envBinding.Spec.Envs); err != nil { - klog.ErrorS(err, "Failed to prepare the pre-work for cluster scheduling") - r.record.Event(envBinding, event.Warning("Failed to prepare the pre-work for cluster scheduling", err)) - return r.endWithNegativeCondition(ctx, envBinding, condition.ReconcileError(err)) - } - - // patch the component parameters for application in different envs - envBinding.Status.Phase = v1alpha1.EnvBindingRendering - appParser := appfile.NewApplicationParser(r.Client, r.dm, r.pd) - envBindApps, err := engine.initEnvBindApps(ctx, envBinding, baseApp, appParser) - if err != nil { - klog.ErrorS(err, "Failed to patch the parameters for application in different envs") - r.record.Event(envBinding, event.Warning("Failed to patch the parameters for application in different envs", err)) - return r.endWithNegativeCondition(ctx, envBinding, condition.ReconcileError(err)) - } - - // schedule resource of applications in different envs - envBinding.Status.Phase = v1alpha1.EnvBindingScheduling - clusterDecisions, err := engine.schedule(ctx, envBindApps) - if err != nil { - klog.ErrorS(err, "Failed to schedule resource of applications in different envs") - r.record.Event(envBinding, event.Warning("Failed to schedule resource of applications in different envs", err)) - return r.endWithNegativeCondition(ctx, envBinding, condition.ReconcileError(err)) - } - - if err = garbageCollect(ctx, r.Client, envBinding, envBindApps); err != nil { - klog.ErrorS(err, "Failed to garbage collect old resource for envBinding") - r.record.Event(envBinding, event.Warning("Failed to garbage collect old resource for envBinding", err)) - return r.endWithNegativeCondition(ctx, envBinding, condition.ReconcileError(err)) - } - - if err = r.applyOrRecordManifests(ctx, envBinding, envBindApps); err != nil { - klog.ErrorS(err, "Failed to apply or record manifests") - r.record.Event(envBinding, event.Warning("Failed to apply or record manifests", err)) - return r.endWithNegativeCondition(ctx, envBinding, condition.ReconcileError(err)) - } - - envBinding.Status.Phase = v1alpha1.EnvBindingFinished - envBinding.Status.ClusterDecisions = clusterDecisions - if err = r.Client.Status().Patch(ctx, envBinding, client.Merge); err != nil { - klog.ErrorS(err, "Failed to update status") - r.record.Event(envBinding, event.Warning("Failed to update status", err)) - return ctrl.Result{}, util.EndReconcileWithNegativeCondition(ctx, r, envBinding, condition.ReconcileError(err)) - } - return ctrl.Result{}, nil -} - -func (r *Reconciler) applyOrRecordManifests(ctx context.Context, envBinding *v1alpha1.EnvBinding, envBindApps []*EnvBindApp) error { - if envBinding.Spec.OutputResourcesTo != nil && len(envBinding.Spec.OutputResourcesTo.Name) != 0 { - if err := StoreManifest2ConfigMap(ctx, r.Client, envBinding, envBindApps); err != nil { - klog.ErrorS(err, "Failed to store manifest of different envs to configmap") - r.record.Event(envBinding, event.Warning("Failed to store manifest of different envs to configmap", err)) - } - envBinding.Status.ResourceTracker = nil - return nil - } - - rt, err := r.createOrGetResourceTracker(ctx, envBinding) - if err != nil { - return err - } - if err = r.dispatchManifests(ctx, rt, envBindApps); err != nil { - klog.ErrorS(err, "Failed to dispatch resources of different envs to cluster") - r.record.Event(envBinding, event.Warning("Failed to dispatch resources of different envs to cluster", err)) - return err - } - - if err = r.updateResourceTrackerStatus(ctx, rt.Name, envBindApps); err != nil { - return err - } - envBinding.Status.ResourceTracker = &v1.ObjectReference{ - Kind: rt.Kind, - APIVersion: rt.APIVersion, - Name: rt.Name, - } - return nil -} - -func (r *Reconciler) dispatchManifests(ctx context.Context, resourceTracker *v1beta1.ResourceTracker, envBindApps []*EnvBindApp) error { - ownerReference := []metav1.OwnerReference{{ - APIVersion: resourceTracker.APIVersion, - Kind: resourceTracker.Kind, - Name: resourceTracker.Name, - UID: resourceTracker.GetUID(), - Controller: pointer.BoolPtr(true), - BlockOwnerDeletion: pointer.BoolPtr(true), - }} - - applicator := apply.NewAPIApplicator(r.Client) - for _, app := range envBindApps { - for _, obj := range app.ScheduledManifests { - obj.SetOwnerReferences(ownerReference) - if err := applicator.Apply(ctx, obj); err != nil { - return err - } - } - } - return nil -} - -func (r *Reconciler) createOrGetResourceTracker(ctx context.Context, envBinding *v1alpha1.EnvBinding) (*v1beta1.ResourceTracker, error) { - rt := new(v1beta1.ResourceTracker) - rtName := constructResourceTrackerName(envBinding.Name, envBinding.Namespace) - err := r.Client.Get(ctx, client.ObjectKey{Name: rtName}, rt) - if err == nil { - return rt, nil - } - if !kerrors.IsNotFound(err) { - return nil, errors.Wrap(err, "cannot get resource tracker") - } - klog.InfoS("Going to create a resource tracker", "resourceTracker", rtName) - rt.SetName(rtName) - if err = r.Client.Create(ctx, rt); err != nil { - return nil, err - } - return rt, nil -} - -func (r *Reconciler) updateResourceTrackerStatus(ctx context.Context, rtName string, envBindApps []*EnvBindApp) error { - var refs []v1.ObjectReference - for _, app := range envBindApps { - for _, obj := range app.ScheduledManifests { - refs = append(refs, v1.ObjectReference{ - APIVersion: obj.GetAPIVersion(), - Kind: obj.GetKind(), - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - }) - } - } - - resourceTracker := new(v1beta1.ResourceTracker) - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { - if err = r.Client.Get(ctx, client.ObjectKey{Name: rtName}, resourceTracker); err != nil { - return - } - resourceTracker.Status.TrackedResources = refs - return r.Client.Status().Update(ctx, resourceTracker) - }); err != nil { - return err - } - klog.InfoS("Successfully update resource tracker status", "resourceTracker", rtName) - return nil -} - -func (r *Reconciler) handleFinalizers(ctx context.Context, envBinding *v1alpha1.EnvBinding) (bool, error) { - if envBinding.ObjectMeta.DeletionTimestamp.IsZero() { - if !meta.FinalizerExists(envBinding, resourceTrackerFinalizer) { - meta.AddFinalizer(envBinding, resourceTrackerFinalizer) - klog.InfoS("Register new finalizer for envBinding", "envBinding", klog.KObj(envBinding), "finalizer", resourceTrackerFinalizer) - return true, errors.Wrap(r.Client.Update(ctx, envBinding), "cannot update envBinding finalizer") - } - } else { - if meta.FinalizerExists(envBinding, resourceTrackerFinalizer) { - rt := new(v1beta1.ResourceTracker) - rt.SetName(constructResourceTrackerName(envBinding.Name, envBinding.Namespace)) - if err := r.Client.Get(ctx, client.ObjectKey{Name: rt.Name}, rt); err != nil && !kerrors.IsNotFound(err) { - klog.ErrorS(err, "Failed to get resource tracker of envBinding", "envBinding", klog.KObj(envBinding)) - return true, errors.WithMessage(err, "cannot remove finalizer") - } - - if err := r.Client.Delete(ctx, rt); err != nil && !kerrors.IsNotFound(err) { - klog.ErrorS(err, "Failed to delete resource tracker of envBinding", "envBinding", klog.KObj(envBinding)) - return true, errors.WithMessage(err, "cannot remove finalizer") - } - - if err := GarbageCollectionForAllResourceTrackersInSubCluster(ctx, r.Client, envBinding); err != nil { - return true, err - } - meta.RemoveFinalizer(envBinding, resourceTrackerFinalizer) - return true, errors.Wrap(r.Client.Update(ctx, envBinding), "cannot update envBinding finalizer") - } - } - return false, nil -} - -func (r *Reconciler) endWithNegativeCondition(ctx context.Context, envBinding *v1alpha1.EnvBinding, cond condition.Condition) (ctrl.Result, error) { - envBinding.SetConditions(cond) - if err := r.Client.Status().Patch(ctx, envBinding, client.Merge); err != nil { - return ctrl.Result{}, errors.WithMessage(err, "cannot update envbinding status") - } - // if any condition is changed, patching status can trigger requeue the resource and we should return nil to - // avoid requeue it again - if util.IsConditionChanged([]condition.Condition{cond}, envBinding) { - return ctrl.Result{}, nil - } - // if no condition is changed, patching status can not trigger requeue, so we must return an error to - // requeue the resource - return ctrl.Result{}, errors.Errorf("object level reconcile error, type: %q, msg: %q", string(cond.Type), cond.Message) -} - -// SetupWithManager will setup with event recorder -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { - r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor("EnvBinding")). - WithAnnotations("controller", "EnvBinding") - return ctrl.NewControllerManagedBy(mgr). - WithOptions(controller.Options{ - MaxConcurrentReconciles: r.concurrentReconciles, - }). - For(&v1alpha1.EnvBinding{}). - Complete(r) -} - -// Setup adds a controller that reconciles EnvBinding. -func Setup(mgr ctrl.Manager, args oamctrl.Args) error { - r := Reconciler{ - Client: mgr.GetClient(), - dm: args.DiscoveryMapper, - pd: args.PackageDiscover, - Scheme: mgr.GetScheme(), - concurrentReconciles: args.ConcurrentReconciles, - } - return r.SetupWithManager(mgr) -} diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/envbinding_controller_test.go b/pkg/controller/core.oam.dev/v1alpha1/envbinding/envbinding_controller_test.go deleted file mode 100644 index 2349f3f1f..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/envbinding_controller_test.go +++ /dev/null @@ -1,1024 +0,0 @@ -/* - 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 envbinding - -import ( - "context" - "encoding/json" - "fmt" - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/pkg/errors" - v1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1" - ocmworkv1 "open-cluster-management.io/api/work/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/yaml" - - commontype "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/pkg/oam/testutil" - "github.com/oam-dev/kubevela/pkg/oam/util" -) - -var _ = Describe("EnvBinding Normal tests", func() { - ctx := context.Background() - var namespace string - var ns corev1.Namespace - var spokeNs corev1.Namespace - var spokeClusterName string - var AppTemplate v1beta1.Application - var BaseEnvBinding v1alpha1.EnvBinding - - AppTemplate = v1beta1.Application{ - TypeMeta: metav1.TypeMeta{ - Kind: "Application", - APIVersion: "core.oam.dev/v1beta1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "template-app", - }, - Spec: v1beta1.ApplicationSpec{ - Components: []commontype.ApplicationComponent{ - { - Name: "web", - Type: "webservice", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "nginx", - }), - Traits: []commontype.ApplicationTrait{ - { - Type: "labels", - Properties: util.Object2RawExtension(map[string]interface{}{ - "hello": "world", - }), - }, - }, - }, - { - Name: "server", - Type: "webservice", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "nginx", - "port": 80, - }), - }, - }, - }, - } - - BaseEnvBinding = v1alpha1.EnvBinding{ - TypeMeta: metav1.TypeMeta{ - Kind: "EnvBinding", - APIVersion: "core.oam.dev/v1beta1", - }, - Spec: v1alpha1.EnvBindingSpec{ - Engine: v1alpha1.OCMEngine, - Envs: []v1alpha1.EnvConfig{{ - Name: "prod", - Patch: v1alpha1.EnvPatch{ - Components: []commontype.ApplicationComponent{{ - Name: "web", - Type: "webservice", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "busybox", - }), - Traits: []commontype.ApplicationTrait{ - { - Type: "labels", - Properties: util.Object2RawExtension(map[string]interface{}{ - "hello": "patch", - }), - }, - }, - }, { - Name: "server", - Type: "webservice", - Properties: util.Object2RawExtension(map[string]interface{}{ - "port": 8080, - }), - }}, - }, - Placement: v1alpha1.EnvPlacement{ - ClusterSelector: &commontype.ClusterSelector{}, - }, - }}, - }, - } - - BeforeEach(func() { - spokeClusterName = "cluster1" - namespace = randomNamespaceName("envbinding-unit-test") - ns = corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} - spokeNs = corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: spokeClusterName, Labels: map[string]string{"purpose": "test"}}} - - Eventually(func() error { - return k8sClient.Create(ctx, &ns) - }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - - Eventually(func() error { - return k8sClient.Create(ctx, &spokeNs) - }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - - webServiceDef := webService.DeepCopy() - webServiceDef.SetNamespace(namespace) - Eventually(func() error { - return k8sClient.Create(ctx, webServiceDef) - }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - - labelsDef := labels.DeepCopy() - labelsDef.SetNamespace(namespace) - Eventually(func() error { - return k8sClient.Create(ctx, labelsDef) - }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - - podInfoDef := podInfo.DeepCopy() - podInfoDef.SetNamespace(namespace) - Eventually(func() error { - return k8sClient.Create(ctx, podInfoDef) - }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - }) - - AfterEach(func() { - By("Clean up resources after a test") - k8sClient.DeleteAllOf(ctx, &v1alpha1.EnvBinding{}, client.InNamespace(namespace)) - k8sClient.DeleteAllOf(ctx, &v1beta1.ComponentDefinition{}, client.InNamespace(namespace)) - k8sClient.DeleteAllOf(ctx, &v1beta1.TraitDefinition{}, client.InNamespace(namespace)) - k8sClient.DeleteAllOf(ctx, &ocmclusterv1alpha1.Placement{}, client.InNamespace(namespace)) - k8sClient.DeleteAllOf(ctx, &ocmclusterv1alpha1.PlacementDecision{}, client.InNamespace(namespace)) - k8sClient.DeleteAllOf(ctx, &ocmworkv1.ManifestWork{}, client.InNamespace(namespace)) - - By(fmt.Sprintf("Delete the entire namespaceName %s", ns.Name)) - Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed()) - }) - - Context("Test EnvBinding with OCM engine", func() { - It("Test EnvBinding select cluster by name", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("app-with-two-components") - appTemplate.SetNamespace(namespace) - - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-select-cluster-by-name") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Envs[0].Placement.ClusterSelector.Name = spokeClusterName - envBinding.Spec.OutputResourcesTo = &v1alpha1.ConfigMapReference{ - Namespace: envBinding.Namespace, - Name: envBinding.Name, - } - envBinding.Spec.Engine = v1alpha1.OCMEngine - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check whether create configmap") - cm := new(corev1.ConfigMap) - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: envBinding.Name, Namespace: namespace}, cm) - }, 30*time.Second, 1*time.Second).Should(BeNil()) - - By("Check whether the parameter is patched") - m := map[string]*ocmworkv1.ManifestWork{} - mw1Yaml := cm.Data[envBinding.Spec.Envs[0].Name] - Expect(yaml.Unmarshal([]byte(mw1Yaml), &m)).Should(BeNil()) - mw := m[envBinding.Name+"-"+envBinding.Spec.Envs[0].Name+"-"+appTemplate.Name] - workload1 := new(v1.Deployment) - Expect(yaml.Unmarshal(mw.Spec.Workload.Manifests[0].Raw, workload1)).Should(BeNil()) - Expect(workload1.Spec.Template.GetLabels()["hello"]).Should(Equal("patch")) - Expect(workload1.Spec.Template.Spec.Containers[0].Image).Should(Equal("busybox")) - - workload2 := new(v1.Deployment) - Expect(yaml.Unmarshal(mw.Spec.Workload.Manifests[1].Raw, workload2)).Should(BeNil()) - Expect(workload2.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort).Should(Equal(int32(8080))) - - By("Check whether the cluster is selected correctly") - Expect(mw.GetNamespace()).Should(Equal(spokeClusterName)) - }) - - It("Test EnvBinding select cluster by label", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetNamespace(namespace) - appTemplate.SetName("app-with-two-components") - - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-select-cluster-by-label") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Envs[0].Placement.ClusterSelector.Labels = map[string]string{ - "purpose": "test", - } - envBinding.Spec.OutputResourcesTo = &v1alpha1.ConfigMapReference{ - Namespace: envBinding.Namespace, - Name: envBinding.Name, - } - envBinding.Spec.Engine = v1alpha1.OCMEngine - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - plName := fmt.Sprintf("%s-%s", appTemplate.Name, envBinding.Spec.Envs[0].Name) - Expect(fakePlacementDecision(ctx, plName, appTemplate.Namespace, spokeClusterName)).Should(BeNil()) - - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check whether create configmap") - cm := new(corev1.ConfigMap) - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: envBinding.Name, Namespace: namespace}, cm) - }, 30*time.Second, 1*time.Second).Should(BeNil()) - - By("Check whether the parameter is patched") - m := map[string]*ocmworkv1.ManifestWork{} - mw1Yaml := cm.Data[envBinding.Spec.Envs[0].Name] - Expect(yaml.Unmarshal([]byte(mw1Yaml), &m)).Should(BeNil()) - mw := m[envBinding.Name+"-"+envBinding.Spec.Envs[0].Name+"-"+appTemplate.Name] - workload := new(v1.Deployment) - Expect(yaml.Unmarshal(mw.Spec.Workload.Manifests[0].Raw, workload)).Should(BeNil()) - Expect(workload.Spec.Template.GetLabels()["hello"]).Should(Equal("patch")) - Expect(workload.Spec.Template.Spec.Containers[0].Image).Should(Equal("busybox")) - - By("Check whether the cluster is selected correctly") - Expect(mw.GetNamespace()).Should(Equal(spokeClusterName)) - }) - - It("Test EnvBinding contains two envs config", func() { - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("app-with-two-component") - appTemplate.SetNamespace(namespace) - - envBinding := BaseEnvBinding.DeepCopy() - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-with-two-env-config") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Envs[0].Placement.ClusterSelector.Name = spokeClusterName - - envBinding.Spec.Envs = append(envBinding.Spec.Envs, v1alpha1.EnvConfig{ - Name: "test", - Patch: v1alpha1.EnvPatch{ - Components: []commontype.ApplicationComponent{{ - Name: "web", - Type: "webservice", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "nginx:1.20", - }), - Traits: []commontype.ApplicationTrait{ - { - Type: "labels", - Properties: util.Object2RawExtension(map[string]interface{}{ - "hello": "patch-test", - }), - }, - }, - }}, - }, - Placement: v1alpha1.EnvPlacement{ - ClusterSelector: &commontype.ClusterSelector{ - Name: spokeClusterName, - }, - }, - }) - envBinding.Spec.OutputResourcesTo = &v1alpha1.ConfigMapReference{ - Namespace: envBinding.Namespace, - Name: envBinding.Name, - } - envBinding.Spec.Engine = v1alpha1.OCMEngine - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check whether create configmap") - cm := new(corev1.ConfigMap) - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: envBinding.Name, Namespace: namespace}, cm) - }, 30*time.Second, 1*time.Second).Should(BeNil()) - - By("Check whether the parameter is patched") - m := map[string]*ocmworkv1.ManifestWork{} - mw1Yaml := cm.Data[envBinding.Spec.Envs[0].Name] - Expect(yaml.Unmarshal([]byte(mw1Yaml), &m)).Should(BeNil()) - mw1 := m[envBinding.Name+"-"+envBinding.Spec.Envs[0].Name+"-"+appTemplate.Name] - workload1 := new(v1.Deployment) - Expect(yaml.Unmarshal(mw1.Spec.Workload.Manifests[0].Raw, workload1)).Should(BeNil()) - Expect(workload1.Spec.Template.GetLabels()["hello"]).Should(Equal("patch")) - Expect(workload1.Spec.Template.Spec.Containers[0].Image).Should(Equal("busybox")) - - m2 := map[string]*ocmworkv1.ManifestWork{} - mw2Yaml := cm.Data[envBinding.Spec.Envs[1].Name] - Expect(yaml.Unmarshal([]byte(mw2Yaml), &m2)).Should(BeNil()) - mw2 := m2[envBinding.Name+"-"+envBinding.Spec.Envs[1].Name+"-"+appTemplate.Name] - workload2 := new(v1.Deployment) - Expect(yaml.Unmarshal(mw2.Spec.Workload.Manifests[0].Raw, workload2)).Should(BeNil()) - Expect(workload2.Spec.Template.GetLabels()["hello"]).Should(Equal("patch-test")) - Expect(workload2.Spec.Template.Spec.Containers[0].Image).Should(Equal("nginx:1.20")) - }) - - It("Test Application in EnvBinding contains helm type component", func() { - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("app-with-helm") - appTemplate.SetNamespace(namespace) - appTemplate.Spec.Components = []commontype.ApplicationComponent{{ - Name: "demo-podinfo", - Type: "pod-info", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": map[string]string{ - "tag": "5.1.2", - }, - }), - }} - - envBinding := BaseEnvBinding.DeepCopy() - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-with-app-has-helm") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - - envBinding.Spec.Envs = []v1alpha1.EnvConfig{{ - Name: "prod", - Patch: v1alpha1.EnvPatch{ - Components: []commontype.ApplicationComponent{{ - Name: "demo-podinfo", - Type: "pod-info", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": map[string]string{ - "tag": "5.1.2", - }, - }), - }}, - }, - Placement: v1alpha1.EnvPlacement{ - ClusterSelector: &commontype.ClusterSelector{ - Name: spokeClusterName, - }, - }, - }} - envBinding.Spec.OutputResourcesTo = &v1alpha1.ConfigMapReference{ - Namespace: envBinding.Namespace, - Name: envBinding.Name, - } - envBinding.Spec.Engine = v1alpha1.OCMEngine - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check whether create configmap") - cm := new(corev1.ConfigMap) - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: envBinding.Name, Namespace: namespace}, cm) - }, 30*time.Second, 1*time.Second).Should(BeNil()) - - m := make(map[string]*ocmworkv1.ManifestWork) - mw1Yaml := cm.Data[envBinding.Spec.Envs[0].Name] - Expect(yaml.Unmarshal([]byte(mw1Yaml), &m)).Should(BeNil()) - mw := m[envBinding.Name+"-"+envBinding.Spec.Envs[0].Name+"-"+appTemplate.Name] - Expect(len(mw.Spec.Workload.Manifests)).Should(Equal(3)) - }) - - It("Test EnvBinding apply resources to cluster", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("app-with-ocm") - appTemplate.SetNamespace(namespace) - - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-apply-resources-with-ocm") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Envs[0].Placement.ClusterSelector.Name = spokeClusterName - envBinding.Spec.Engine = v1alpha1.OCMEngine - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check whether create manifestWork") - mw := new(ocmworkv1.ManifestWork) - mwName := appTemplate.Name - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: mwName, Namespace: spokeClusterName}, mw) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - By("Check whether the parameter is patched") - workload1 := new(v1.Deployment) - Expect(yaml.Unmarshal(mw.Spec.Workload.Manifests[0].Raw, workload1)).Should(BeNil()) - Expect(workload1.Spec.Template.GetLabels()["hello"]).Should(Equal("patch")) - Expect(workload1.Spec.Template.Spec.Containers[0].Image).Should(Equal("busybox")) - - workload2 := new(v1.Deployment) - Expect(yaml.Unmarshal(mw.Spec.Workload.Manifests[1].Raw, workload2)).Should(BeNil()) - Expect(workload2.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort).Should(Equal(int32(8080))) - - By("Check whether the cluster is selected correctly") - Expect(mw.GetNamespace()).Should(Equal(spokeClusterName)) - }) - }) - - Context("Test EnvBinding with SingleCluster Engine", func() { - It("Test EnvBinding which will apply resources to cluster", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("test-app-apply2cluster") - appTemplate.SetNamespace(namespace) - - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-apply-resources") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Engine = v1alpha1.SingleClusterEngine - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check the Application created by EnvBinding Controller") - appName := appTemplate.Name - appReq := client.ObjectKey{Name: appName, Namespace: namespace} - envBindApp := new(v1beta1.Application) - Eventually(func() error { - return k8sClient.Get(ctx, appReq, envBindApp) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - By("Check whether the parameter is patched") - componentParameter := make(map[string]string) - Expect(json.Unmarshal(envBindApp.Spec.Components[0].Properties.Raw, &componentParameter)).Should(BeNil()) - Expect(componentParameter["image"]).Should(Equal("busybox")) - - traitParameter := make(map[string]string) - Expect(json.Unmarshal(envBindApp.Spec.Components[0].Traits[0].Properties.Raw, &traitParameter)).Should(BeNil()) - Expect(traitParameter["hello"]).Should(Equal("patch")) - }) - - It("Test EnvBinding which will store resources to configMap", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("test-app-store2configmap") - appTemplate.SetNamespace(namespace) - - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-store2configmap") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Engine = v1alpha1.SingleClusterEngine - envBinding.Spec.OutputResourcesTo = &v1alpha1.ConfigMapReference{ - Namespace: namespace, - Name: envBinding.Name, - } - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check whether create configmap") - cmKey := client.ObjectKey{Name: envBinding.Spec.OutputResourcesTo.Name, Namespace: envBinding.Spec.OutputResourcesTo.Namespace} - cm := new(corev1.ConfigMap) - Eventually(func() error { - return k8sClient.Get(ctx, cmKey, cm) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - By("Check whether the parameter is patched") - m := make(map[string]*v1beta1.Application) - Expect(yaml.Unmarshal([]byte(cm.Data["prod"]), &m)).Should(BeNil()) - appName := fmt.Sprintf("%s-%s-%s", envBinding.Name, "prod", appTemplate.Name) - app := m[appName] - - componentParameter := make(map[string]string) - Expect(json.Unmarshal(app.Spec.Components[0].Properties.Raw, &componentParameter)).Should(BeNil()) - Expect(componentParameter["image"]).Should(Equal("busybox")) - - traitParameter := make(map[string]string) - Expect(json.Unmarshal(app.Spec.Components[0].Traits[0].Properties.Raw, &traitParameter)).Should(BeNil()) - Expect(traitParameter["hello"]).Should(Equal("patch")) - }) - - It("Test EnvBinding select namespace by name", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("test-app-specify-ns") - appTemplate.SetNamespace(namespace) - - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-specify-ns") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Engine = v1alpha1.SingleClusterEngine - envBinding.Spec.Envs[0].Placement = v1alpha1.EnvPlacement{ - NamespaceSelector: &v1alpha1.NamespaceSelector{ - Name: spokeNs.Name, - }, - } - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check the Application created by EnvBinding Controller") - appName := appTemplate.Name - appReq := client.ObjectKey{Name: appName, Namespace: spokeNs.Name} - envBindApp := new(v1beta1.Application) - Eventually(func() error { - return k8sClient.Get(ctx, appReq, envBindApp) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - By("Check whether the parameter is patched") - componentParameter := make(map[string]string) - Expect(json.Unmarshal(envBindApp.Spec.Components[0].Properties.Raw, &componentParameter)).Should(BeNil()) - Expect(componentParameter["image"]).Should(Equal("busybox")) - - traitParameter := make(map[string]string) - Expect(json.Unmarshal(envBindApp.Spec.Components[0].Traits[0].Properties.Raw, &traitParameter)).Should(BeNil()) - Expect(traitParameter["hello"]).Should(Equal("patch")) - }) - - It("Test EnvBinding select namespace by label", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("test-app-select-ns-label") - appTemplate.SetNamespace(namespace) - - envBinding.SetNamespace(namespace) - envBinding.SetName("envbinding-select-ns-label") - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Engine = v1alpha1.SingleClusterEngine - envBinding.Spec.Envs[0].Placement = v1alpha1.EnvPlacement{ - NamespaceSelector: &v1alpha1.NamespaceSelector{ - Labels: map[string]string{ - "purpose": "test", - }, - }, - } - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check the Application created by EnvBinding Controller") - appName := appTemplate.Name - appReq := client.ObjectKey{Name: appName, Namespace: spokeNs.Name} - envBindApp := new(v1beta1.Application) - Eventually(func() error { - return k8sClient.Get(ctx, appReq, envBindApp) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - By("Check whether the parameter is patched") - componentParameter := make(map[string]string) - Expect(json.Unmarshal(envBindApp.Spec.Components[0].Properties.Raw, &componentParameter)).Should(BeNil()) - Expect(componentParameter["image"]).Should(Equal("busybox")) - - traitParameter := make(map[string]string) - Expect(json.Unmarshal(envBindApp.Spec.Components[0].Traits[0].Properties.Raw, &traitParameter)).Should(BeNil()) - Expect(traitParameter["hello"]).Should(Equal("patch")) - }) - }) - - Context("Test GC mechanism for EnvBinding", func() { - It("Test EnvBinding apply resource to single cluster", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("test-app-apply2cluster") - appTemplate.SetNamespace(namespace) - - envBinding.SetName("test-envbinding-gc-single-cluster") - envBinding.SetNamespace(namespace) - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Engine = v1alpha1.SingleClusterEngine - envBinding.Spec.Envs[0].Placement = v1alpha1.EnvPlacement{ - NamespaceSelector: &v1alpha1.NamespaceSelector{ - Name: spokeNs.Name, - }, - } - - envBinding.Spec.Envs = append(envBinding.Spec.Envs, v1alpha1.EnvConfig{ - Name: "test", - Patch: v1alpha1.EnvPatch{ - Components: []commontype.ApplicationComponent{{ - Name: "web", - Type: "webservice", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "nginx:1.20", - }), - Traits: []commontype.ApplicationTrait{ - { - Type: "labels", - Properties: util.Object2RawExtension(map[string]interface{}{ - "hello": "patch-test", - }), - }, - }, - }}, - }, - Placement: v1alpha1.EnvPlacement{ - NamespaceSelector: &v1alpha1.NamespaceSelector{ - Name: spokeNs.Name, - }, - }, - }) - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - - testutil.ReconcileRetry(&r, req) - - By("Check the Application created by EnvBinding Controller") - app1Name := appTemplate.Name - app1Key := client.ObjectKey{Name: app1Name, Namespace: spokeNs.Name} - envBindApp1 := new(v1beta1.Application) - Eventually(func() error { - return k8sClient.Get(ctx, app1Key, envBindApp1) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - app2Name := appTemplate.Name - app2Key := client.ObjectKey{Name: app2Name, Namespace: spokeNs.Name} - envBindApp2 := new(v1beta1.Application) - Eventually(func() error { - return k8sClient.Get(ctx, app2Key, envBindApp2) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - By("Check the ResourceTracker created by EnvBinding Controller") - rtName := constructResourceTrackerName(envBinding.Name, envBinding.Namespace) - rtKey := client.ObjectKey{Name: rtName} - rt := new(v1beta1.ResourceTracker) - Eventually(func() error { - return k8sClient.Get(ctx, rtKey, rt) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - Expect(len(rt.Status.TrackedResources)).Should(Equal(len(envBinding.Spec.Envs))) - Expect(rt.Status.TrackedResources[0].Name).Should(Equal(app1Name)) - Expect(rt.Status.TrackedResources[1].Name).Should(Equal(app2Name)) - - By("Modify the Spec of EnvBinding") - Eventually(func() error { - newEnvBinding := new(v1alpha1.EnvBinding) - err := k8sClient.Get(ctx, req.NamespacedName, newEnvBinding) - if err != nil { - return err - } - newEnvBinding.Spec.Envs = envBinding.Spec.Envs[1:] - return k8sClient.Update(ctx, newEnvBinding) - }, 5*time.Second, 1*time.Second).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check the Application is deleted") - Eventually(func() error { - return client.IgnoreNotFound(k8sClient.Get(ctx, app1Key, envBindApp1)) - }) - Eventually(func() error { - err := k8sClient.Get(ctx, rtKey, rt) - if err != nil { - return err - } - if len(rt.Status.TrackedResources) != 1 { - return errors.New("failed to update resourceTracker") - } - return nil - }, 3*time.Second, 1*time.Second).Should(BeNil()) - Expect(rt.Status.TrackedResources[0].Name).Should(Equal(app2Name)) - - By("Delete EnvBinding") - Expect(k8sClient.Delete(ctx, envBinding)) - testutil.ReconcileRetry(&r, req) - - By("Check the ResourceTracker and Application is deleted") - Eventually(func() error { - return client.IgnoreNotFound(k8sClient.Get(ctx, app2Key, envBindApp2)) - }) - Eventually(func() error { - return client.IgnoreNotFound(k8sClient.Get(ctx, rtKey, rt)) - }) - }) - - It("Test EnvBinding apply resource to multi cluster", func() { - envBinding := BaseEnvBinding.DeepCopy() - appTemplate := AppTemplate.DeepCopy() - appTemplate.SetName("test-app-apply2cluster") - appTemplate.SetNamespace(namespace) - - envBinding.SetName("test-envbinding-gc-multi-cluster") - envBinding.SetNamespace(namespace) - envBinding.Spec.AppTemplate = v1alpha1.AppTemplate{ - RawExtension: *util.Object2RawExtension(appTemplate), - } - envBinding.Spec.Envs[0].Placement.ClusterSelector.Name = spokeClusterName - - envBinding.Spec.Envs = append(envBinding.Spec.Envs, v1alpha1.EnvConfig{ - Name: "test", - Patch: v1alpha1.EnvPatch{ - Components: []commontype.ApplicationComponent{{ - Name: "web", - Type: "webservice", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "nginx:1.20", - }), - Traits: []commontype.ApplicationTrait{ - { - Type: "labels", - Properties: util.Object2RawExtension(map[string]interface{}{ - "hello": "patch-test", - }), - }, - }, - }}, - }, - Placement: v1alpha1.EnvPlacement{ - ClusterSelector: &commontype.ClusterSelector{ - Name: spokeNs.Name, - }, - }, - }) - - req := reconcile.Request{NamespacedName: client.ObjectKey{Namespace: namespace, Name: envBinding.Name}} - By("Create envBinding") - Expect(k8sClient.Create(ctx, envBinding)).Should(BeNil()) - - testutil.ReconcileOnce(&r, req) - testutil.ReconcileRetry(&r, req) - - By("Check the ManifestWork created by EnvBinding Controller") - mw1Name := appTemplate.Name - mw1Key := client.ObjectKey{Name: mw1Name, Namespace: spokeNs.Name} - mw1 := new(ocmworkv1.ManifestWork) - Eventually(func() error { - return k8sClient.Get(ctx, mw1Key, mw1) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - mw2Name := appTemplate.Name - mw2Key := client.ObjectKey{Name: mw2Name, Namespace: spokeNs.Name} - mw2 := new(ocmworkv1.ManifestWork) - Eventually(func() error { - return k8sClient.Get(ctx, mw2Key, mw2) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - By("Check the ResourceTracker created by EnvBinding Controller") - rtName := constructResourceTrackerName(envBinding.Name, envBinding.Namespace) - rtKey := client.ObjectKey{Name: rtName} - rt := new(v1beta1.ResourceTracker) - Eventually(func() error { - return k8sClient.Get(ctx, rtKey, rt) - }, 3*time.Second, 1*time.Second).Should(BeNil()) - - Expect(len(rt.Status.TrackedResources)).Should(Equal(len(envBinding.Spec.Envs))) - Expect(rt.Status.TrackedResources[0].Name).Should(Equal(mw1Name)) - Expect(rt.Status.TrackedResources[1].Name).Should(Equal(mw2Name)) - - By("Modify the Spec of EnvBinding") - Eventually(func() error { - newEnvBinding := new(v1alpha1.EnvBinding) - err := k8sClient.Get(ctx, req.NamespacedName, newEnvBinding) - if err != nil { - return err - } - newEnvBinding.Spec.Envs = envBinding.Spec.Envs[1:] - return k8sClient.Update(ctx, newEnvBinding) - }, 5*time.Second, 1*time.Second).Should(BeNil()) - testutil.ReconcileRetry(&r, req) - - By("Check the ManifestWork is deleted") - Eventually(func() error { - return client.IgnoreNotFound(k8sClient.Get(ctx, mw1Key, mw1)) - }) - Eventually(func() error { - err := k8sClient.Get(ctx, rtKey, rt) - if err != nil { - return err - } - if len(rt.Status.TrackedResources) != 1 { - return errors.New("failed to update resourceTracker") - } - return nil - }, 3*time.Second, 1*time.Second).Should(BeNil()) - Expect(rt.Status.TrackedResources[0].Name).Should(Equal(mw2Name)) - - By("Delete EnvBinding") - Expect(k8sClient.Delete(ctx, envBinding)) - testutil.ReconcileRetry(&r, req) - - By("Check the ResourceTracker and Application is deleted") - Eventually(func() error { - return client.IgnoreNotFound(k8sClient.Get(ctx, mw2Key, mw2)) - }) - Eventually(func() error { - return client.IgnoreNotFound(k8sClient.Get(ctx, rtKey, rt)) - }) - }) - }) -}) - -func fakePlacementDecision(ctx context.Context, plName, namespace, clusterName string) error { - pd := &ocmclusterv1alpha1.PlacementDecision{} - pdName := plName + "-placement-decision" - pd.SetName(pdName) - pd.SetNamespace(namespace) - pd.Status.Decisions = []ocmclusterv1alpha1.ClusterDecision{{ - ClusterName: clusterName, - }} - pd.SetLabels(map[string]string{ - "cluster.open-cluster-management.io/placement": plName, - }) - - bts, err := json.Marshal(pd.Status) - if err != nil { - return err - } - data := make(map[string]interface{}) - if err = json.Unmarshal(bts, &data); err != nil { - return err - } - if err = k8sClient.Create(ctx, pd); err != nil { - return err - } - if err = k8sClient.Get(ctx, client.ObjectKey{Name: pdName, Namespace: namespace}, pd); err != nil { - return err - } - - return k8sClient.Status().Update(ctx, pd) -} - -var webService = &v1beta1.ComponentDefinition{ - TypeMeta: metav1.TypeMeta{ - Kind: "ComponentDefinition", - APIVersion: "core.oam.dev/v1beta1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "webservice", - }, - Spec: v1beta1.ComponentDefinitionSpec{ - Workload: commontype.WorkloadTypeDescriptor{ - Definition: commontype.WorkloadGVK{ - APIVersion: "apps/v1", - Kind: "Deployment", - }, - }, - Schematic: &commontype.Schematic{ - CUE: &commontype.CUE{ - Template: webServiceTemplate, - }, - }, - }, -} - -var webServiceTemplate = `output: { - apiVersion: "apps/v1" - kind: "Deployment" - metadata: labels: { - "componentdefinition.oam.dev/version": "v1" - } - spec: { - selector: matchLabels: { - "app.oam.dev/component": context.name - } - template: { - metadata: labels: { - "app.oam.dev/component": context.name - } - spec: { - containers: [{ - name: context.name - image: parameter.image - if parameter["cmd"] != _|_ { - command: parameter.cmd - } - if parameter["env"] != _|_ { - env: parameter.env - } - if context["config"] != _|_ { - env: context.config - } - ports: [{ - containerPort: parameter.port - }] - if parameter["cpu"] != _|_ { - resources: { - limits: - cpu: parameter.cpu - requests: - cpu: parameter.cpu - } - } - }] - } - } - } -} -parameter: { - image: string - cmd?: [...string] - port: *80 | int - env?: [...{ - name: string - value?: string - valueFrom?: { - secretKeyRef: { - name: string - key: string - } - } - }] - cpu?: string -} -` - -var labels = &v1beta1.TraitDefinition{ - TypeMeta: metav1.TypeMeta{ - Kind: "TraitDefinition", - APIVersion: "core.oam.dev/v1beta1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "labels", - }, - Spec: v1beta1.TraitDefinitionSpec{ - Schematic: &commontype.Schematic{ - CUE: &commontype.CUE{ - Template: labelsTemplate, - }, - }, - }, -} - -var labelsTemplate = `patch: { - spec: template: metadata: labels: { - for k, v in parameter { - "\(k)": v - } - } -} -parameter: [string]: string -` - -var podInfo = &v1beta1.ComponentDefinition{ - TypeMeta: metav1.TypeMeta{ - Kind: "ComponentDefinition", - APIVersion: "core.oam.dev/v1beta1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pod-info", - }, - Spec: v1beta1.ComponentDefinitionSpec{ - Workload: commontype.WorkloadTypeDescriptor{ - Definition: commontype.WorkloadGVK{ - APIVersion: "apps/v1", - Kind: "Deployment", - }, - }, - Schematic: &commontype.Schematic{ - HELM: &commontype.Helm{ - Release: *util.Object2RawExtension(map[string]interface{}{ - "chart": map[string]interface{}{ - "spec": map[string]interface{}{ - "chart": "podinfo", - "version": "5.1.4", - }, - }, - }), - Repository: *util.Object2RawExtension(map[string]interface{}{ - "url": "http://oam.dev/catalog/", - }), - }, - }, - }, -} diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/suit_test.go b/pkg/controller/core.oam.dev/v1alpha1/envbinding/suit_test.go deleted file mode 100644 index 2dc0ba6c9..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/suit_test.go +++ /dev/null @@ -1,115 +0,0 @@ -/* - 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 envbinding - -import ( - "context" - "fmt" - "math/rand" - "path/filepath" - "strconv" - "testing" - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - - "github.com/oam-dev/kubevela/pkg/cue/packages" - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" - "github.com/oam-dev/kubevela/pkg/utils/common" -) - -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment -var controllerDone context.CancelFunc -var r Reconciler - -func TestEnvBinding(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "EnvBinding Suite") -} - -var _ = BeforeSuite(func(done Done) { - By("Bootstrapping test environment") - useExistCluster := false - testEnv = &envtest.Environment{ - ControlPlaneStartTimeout: time.Minute, - ControlPlaneStopTimeout: time.Minute, - CRDDirectoryPaths: []string{ - filepath.Join("../../../../..", "charts/vela-core/crds"), // this has all the required CRDs, - "./testdata/crds", - }, - UseExistingCluster: &useExistCluster, - } - var err error - cfg, err = testEnv.Start() - Expect(err).ToNot(HaveOccurred()) - Expect(cfg).ToNot(BeNil()) - - By("Create the k8s client") - k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) - Expect(err).ToNot(HaveOccurred()) - Expect(k8sClient).ToNot(BeNil()) - - By("Starting the controller in the background") - mgr, err := ctrl.NewManager(cfg, ctrl.Options{ - Scheme: common.Scheme, - MetricsBindAddress: "0", - Port: 48081, - }) - Expect(err).ToNot(HaveOccurred()) - dm, err := discoverymapper.New(mgr.GetConfig()) - Expect(err).ToNot(HaveOccurred()) - _, err = dm.Refresh() - Expect(err).ToNot(HaveOccurred()) - pd, err := packages.NewPackageDiscover(cfg) - Expect(err).ToNot(HaveOccurred()) - - r = Reconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - dm: dm, - pd: pd, - } - Expect(r.SetupWithManager(mgr)).ToNot(HaveOccurred()) - var ctx context.Context - ctx, controllerDone = context.WithCancel(context.Background()) - go func() { - defer GinkgoRecover() - Expect(mgr.Start(ctx)).ToNot(HaveOccurred()) - }() - - close(done) -}, 120) - -var _ = AfterSuite(func() { - By("Stop the controller") - controllerDone() - - By("Tearing down the test environment") - err := testEnv.Stop() - Expect(err).ToNot(HaveOccurred()) -}) - -func randomNamespaceName(basic string) string { - return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16)) -} diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/manifestwork.yaml b/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/manifestwork.yaml deleted file mode 100644 index 962aadfaa..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/manifestwork.yaml +++ /dev/null @@ -1,341 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: manifestworks.work.open-cluster-management.io -spec: - conversion: - strategy: None - group: work.open-cluster-management.io - names: - kind: ManifestWork - listKind: ManifestWorkList - plural: manifestworks - singular: manifestwork - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: ManifestWork represents a manifests workload that hub wants to - deploy on the managed cluster. A manifest workload is defined as a set of - Kubernetes resources. ManifestWork must be created in the cluster namespace - on the hub, so that agent on the corresponding managed cluster can access - this resource and deploy on the managed cluster. - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Spec represents a desired configuration of work to be deployed - on the managed cluster. - properties: - deleteOption: - description: DeleteOption represents deletion strategy when the manifestwork - is deleted. Foreground deletion strategy is applied to all the resource - in this manifestwork if it is not set. - properties: - propagationPolicy: - default: ForeGround - description: propagationPolicy can be Foreground, Orphan or SelectivelyOrphan - SelectivelyOrphan should be rarely used. It is provided for - cases where particular resources is transfering ownership from - one ManifestWork to another or another management unit. Setting - this value will allow a flow like 1. create manifestwork/2 to - manage foo 2. update manifestwork/1 to selectively orphan foo - 3. remove foo from manifestwork/1 without impacting continuity - because manifestwork/2 adopts it. - type: string - selectivelyOrphans: - description: selectivelyOrphan represents a list of resources - following orphan deletion stratecy - properties: - orphaningRules: - description: orphaningRules defines a slice of orphaningrule. - Each orphaningrule identifies a single resource included - in this manifestwork - items: - description: OrphaningRule identifies a single resource - included in this manifestwork - properties: - Name: - description: Name is the names of the resources in the - workload that the strategy is applied - type: string - Namespace: - description: Namespace is the namespaces of the resources - in the workload that the strategy is applied - type: string - group: - description: Group is the api group of the resources - in the workload that the strategy is applied - type: string - resource: - description: Resource is the resources in the workload - that the strategy is applied - type: string - type: object - type: array - type: object - type: object - workload: - description: Workload represents the manifest workload to be deployed - on a managed cluster. - properties: - manifests: - description: Manifests represents a list of kuberenetes resources - to be deployed on a managed cluster. - items: - description: Manifest represents a resource to be deployed on - managed cluster. - type: object - x-kubernetes-embedded-resource: true - x-kubernetes-preserve-unknown-fields: true - type: array - type: object - type: object - status: - description: Status represents the current status of work. - properties: - conditions: - description: 'Conditions contains the different condition statuses - for this work. Valid condition types are: 1. Applied represents - workload in ManifestWork is applied successfully on managed cluster. - 2. Progressing represents workload in ManifestWork is being applied - on managed cluster. 3. Available represents workload in ManifestWork - exists on the managed cluster. 4. Degraded represents the current - state of workload does not match the desired state for a certain - period.' - items: - description: "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: - \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type - \ // +patchStrategy=merge // +listType=map // +listMapKey=type - \ Conditions []metav1.Condition `json:\"conditions,omitempty\" - patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` - \n // other fields }" - properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: message is a human readable message indicating - details about the transition. This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - resourceStatus: - description: ResourceStatus represents the status of each resource - in manifestwork deployed on a managed cluster. The Klusterlet agent - on managed cluster syncs the condition from the managed cluster - to the hub. - properties: - manifests: - description: 'Manifests represents the condition of manifests - deployed on managed cluster. Valid condition types are: 1. Progressing - represents the resource is being applied on managed cluster. - 2. Applied represents the resource is applied successfully on - managed cluster. 3. Available represents the resource exists - on the managed cluster. 4. Degraded represents the current state - of resource does not match the desired state for a certain period.' - items: - description: ManifestCondition represents the conditions of - the resources deployed on a managed cluster. - properties: - conditions: - description: Conditions represents the conditions of this - resource on a managed cluster. - items: - description: "Condition contains details for one aspect - of the current state of this API Resource. --- This - struct is intended for direct use as an array at the - field path .status.conditions. For example, type FooStatus - struct{ // Represents the observations of a foo's - current state. // Known .status.conditions.type - are: \"Available\", \"Progressing\", and \"Degraded\" - \ // +patchMergeKey=type // +patchStrategy=merge - \ // +listType=map // +listMapKey=type Conditions - []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" - patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` - \n // other fields }" - properties: - lastTransitionTime: - description: lastTransitionTime is the last time the - condition transitioned from one status to another. - This should be when the underlying condition changed. If - that is not known, then using the time when the - API field changed is acceptable. - format: date-time - type: string - message: - description: message is a human readable message indicating - details about the transition. This may be an empty - string. - maxLength: 32768 - type: string - observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, - if .metadata.generation is currently 12, but the - .status.conditions[x].observedGeneration is 9, the - condition is out of date with respect to the current - state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: reason contains a programmatic identifier - indicating the reason for the condition's last transition. - Producers of specific condition types may define - expected values and meanings for this field, and - whether the values are considered a guaranteed API. - The value should be a CamelCase string. This field - may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, - False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in - foo.example.com/CamelCase. --- Many .condition.type - values are consistent across resources like Available, - but because arbitrary conditions can be useful (see - .node.status.conditions), the ability to deconflict - is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - resourceMeta: - description: ResourceMeta represents the group, version, - kind, name and namespace of a resoure. - properties: - group: - description: Group is the API Group of the Kubernetes - resource. - type: string - kind: - description: Kind is the kind of the Kubernetes resource. - type: string - name: - description: Name is the name of the Kubernetes resource. - type: string - namespace: - description: Name is the namespace of the Kubernetes - resource. - type: string - ordinal: - description: Ordinal represents the index of the manifest - on spec. - format: int32 - type: integer - resource: - description: Resource is the resource name of the Kubernetes - resource. - type: string - version: - description: Version is the version of the Kubernetes - resource. - type: string - type: object - type: object - type: array - type: object - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: ManifestWork - listKind: ManifestWorkList - plural: manifestworks - singular: manifestwork - conditions: - - lastTransitionTime: "2021-08-12T08:23:40Z" - message: no conflicts found - reason: NoConflicts - status: "True" - type: NamesAccepted - - lastTransitionTime: "2021-08-12T08:23:40Z" - message: the initial names have been accepted - reason: InitialNamesAccepted - status: "True" - type: Established - storedVersions: - - v1 \ No newline at end of file diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/placement.yaml b/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/placement.yaml deleted file mode 100644 index 450f534cd..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/placement.yaml +++ /dev/null @@ -1,288 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: placements.cluster.open-cluster-management.io -spec: - conversion: - strategy: None - group: cluster.open-cluster-management.io - names: - kind: Placement - listKind: PlacementList - plural: placements - singular: placement - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: "Placement defines a rule to select a set of ManagedClusters - from the ManagedClusterSets bound to the placement namespace. \n Here is - how the placement policy combines with other selection methods to determine - a matching list of ManagedClusters: 1) Kubernetes clusters are registered - with hub as cluster-scoped ManagedClusters; 2) ManagedClusters are organized - into cluster-scoped ManagedClusterSets; 3) ManagedClusterSets are bound - to workload namespaces; 4) Namespace-scoped Placements specify a slice of - ManagedClusterSets which select a working set of potential ManagedClusters; - 5) Then Placements subselect from that working set using label/claim selection. - \n No ManagedCluster will be selected if no ManagedClusterSet is bound to - the placement namespace. User is able to bind a ManagedClusterSet to a namespace - by creating a ManagedClusterSetBinding in that namespace if they have a - RBAC rule to CREATE on the virtual subresource of `managedclustersets/bind`. - \n A slice of PlacementDecisions with label cluster.open-cluster-management.io/placement={placement - name} will be created to represent the ManagedClusters selected by this - placement. \n If a ManagedCluster is selected and added into the PlacementDecisions, - other components may apply workload on it; once it is removed from the PlacementDecisions, - the workload applied on this ManagedCluster should be evicted accordingly." - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: Spec defines the attributes of Placement. - properties: - clusterSets: - description: ClusterSets represent the ManagedClusterSets from which - the ManagedClusters are selected. If the slice is empty, ManagedClusters - will be selected from the ManagedClusterSets bound to the placement - namespace, otherwise ManagedClusters will be selected from the intersection - of this slice and the ManagedClusterSets bound to the placement - namespace. - items: - type: string - type: array - numberOfClusters: - description: NumberOfClusters represents the desired number of ManagedClusters - to be selected which meet the placement requirements. 1) If not - specified, all ManagedClusters which meet the placement requirements - (including ClusterSets, and Predicates) will be selected; 2) - Otherwise if the nubmer of ManagedClusters meet the placement requirements - is larger than NumberOfClusters, a random subset with desired - number of ManagedClusters will be selected; 3) If the nubmer of - ManagedClusters meet the placement requirements is equal to NumberOfClusters, all - of them will be selected; 4) If the nubmer of ManagedClusters meet - the placement requirements is less than NumberOfClusters, all - of them will be selected, and the status of condition `PlacementConditionSatisfied` - will be set to false; - format: int32 - type: integer - predicates: - description: Predicates represent a slice of predicates to select - ManagedClusters. The predicates are ORed. - items: - description: ClusterPredicate represents a predicate to select ManagedClusters. - properties: - requiredClusterSelector: - description: RequiredClusterSelector represents a selector of - ManagedClusters by label and claim. If specified, 1) Any ManagedCluster, - which does not match the selector, should not be selected - by this ClusterPredicate; 2) If a selected ManagedCluster - (of this ClusterPredicate) ceases to match the selector (e.g. - due to an update) of any ClusterPredicate, it will be eventually - removed from the placement decisions; 3) If a ManagedCluster - (not selected previously) starts to match the selector, it - will either be selected or at least has a chance to be - selected (when NumberOfClusters is specified); - properties: - claimSelector: - description: ClaimSelector represents a selector of ManagedClusters - by clusterClaims in status - properties: - matchExpressions: - description: matchExpressions is a list of cluster claim - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector - that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector - applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are In, - NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. - If the operator is In or NotIn, the values array - must be non-empty. If the operator is Exists - or DoesNotExist, the values array must be empty. - This array is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - type: object - labelSelector: - description: LabelSelector represents a selector of ManagedClusters - by label - properties: - matchExpressions: - description: matchExpressions is a list of label selector - requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector - that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector - applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are In, - NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. - If the operator is In or NotIn, the values array - must be non-empty. If the operator is Exists - or DoesNotExist, the values array must be empty. - This array is replaced during a strategic merge - patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. - A single {key,value} in the matchLabels map is equivalent - to an element of matchExpressions, whose key field - is "key", the operator is "In", and the values array - contains only "value". The requirements are ANDed. - type: object - type: object - type: object - type: object - type: array - type: object - status: - description: Status represents the current status of the Placement - properties: - conditions: - description: Conditions contains the different condition statuses - for this Placement. - items: - description: "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: - \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type - \ // +patchStrategy=merge // +listType=map // +listMapKey=type - \ Conditions []metav1.Condition `json:\"conditions,omitempty\" - patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` - \n // other fields }" - properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: message is a human readable message indicating - details about the transition. This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - numberOfSelectedClusters: - description: NumberOfSelectedClusters represents the number of selected - ManagedClusters - format: int32 - type: integer - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: Placement - listKind: PlacementList - plural: placements - singular: placement - conditions: - - lastTransitionTime: "2021-08-04T08:37:09Z" - message: no conflicts found - reason: NoConflicts - status: "True" - type: NamesAccepted - - lastTransitionTime: "2021-08-04T08:37:09Z" - message: the initial names have been accepted - reason: InitialNamesAccepted - status: "True" - type: Established - storedVersions: - - v1alpha1 \ No newline at end of file diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/placementdecision.yaml b/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/placementdecision.yaml deleted file mode 100644 index a9c7727ea..000000000 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/testdata/crds/placementdecision.yaml +++ /dev/null @@ -1,90 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: placementdecisions.cluster.open-cluster-management.io -spec: - conversion: - strategy: None - group: cluster.open-cluster-management.io - names: - kind: PlacementDecision - listKind: PlacementDecisionList - plural: placementdecisions - singular: placementdecision - scope: Namespaced - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: "PlacementDecision indicates a decision from a placement PlacementDecision - should has a label cluster.open-cluster-management.io/placement={placement - name} to reference a certain placement. \n If a placement has spec.numberOfClusters - specified, the total number of decisions contained in status.decisions of - PlacementDecisions should always be NumberOfClusters; otherwise, the total - number of decisions should be the number of ManagedClusters which match - the placement requirements. \n Some of the decisions might be empty when - there are no enough ManagedClusters meet the placement requirements." - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - status: - description: Status represents the current status of the PlacementDecision - properties: - decisions: - description: Decisions is a slice of decisions according to a placement - The number of decisions should not be larger than 100 - items: - description: ClusterDecision represents a decision from a placement - An empty ClusterDecision indicates it is not scheduled yet. - properties: - clusterName: - description: ClusterName is the name of the ManagedCluster. - If it is not empty, its value should be unique cross all placement - decisions for the Placement. - type: string - reason: - description: Reason represents the reason why the ManagedCluster - is selected. - type: string - required: - - clusterName - - reason - type: object - type: array - required: - - decisions - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: PlacementDecision - listKind: PlacementDecisionList - plural: placementdecisions - singular: placementdecision - conditions: - - lastTransitionTime: "2021-08-04T08:37:09Z" - message: no conflicts found - reason: NoConflicts - status: "True" - type: NamesAccepted - - lastTransitionTime: "2021-08-04T08:37:09Z" - message: the initial names have been accepted - reason: InitialNamesAccepted - status: "True" - type: Established - storedVersions: - - v1alpha1 \ No newline at end of file diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go index 8c65476bf..00e6893cd 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go @@ -22,19 +22,20 @@ import ( "time" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/crossplane/crossplane-runtime/pkg/event" - "github.com/crossplane/crossplane-runtime/pkg/meta" - "github.com/pkg/errors" kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/retry" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/meta" + "github.com/pkg/errors" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/condition" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" @@ -42,9 +43,9 @@ import ( "github.com/oam-dev/kubevela/pkg/appfile" common2 "github.com/oam-dev/kubevela/pkg/controller/common" core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" - "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha1/envbinding" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/assemble" "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" oamutil "github.com/oam-dev/kubevela/pkg/oam/util" @@ -169,7 +170,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu r.Recorder.Event(app, event.Normal(velatypes.ReasonRendered, velatypes.MessageRendered)) if !appWillRollout(app) { - steps, err := handler.GenerateApplicationSteps(ctx, app, appParser, appFile, handler.currentAppRev, r.Client, r.dm, r.pd) + handler.addAppliedResource(app.Status.AppliedResources...) + steps, err := handler.GenerateApplicationSteps(ctx, app, appParser, appFile, handler.currentAppRev) if err != nil { klog.Error(err, "[handle workflow]") r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedWorkflow, err)) @@ -185,25 +187,24 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } handler.addServiceStatus(false, app.Status.Services...) - handler.addAppliedResource(app.Status.AppliedResources...) app.Status.AppliedResources = handler.appliedResources app.Status.Services = handler.services switch workflowState { case common.WorkflowStateSuspended: - return ctrl.Result{}, r.patchStatus(ctx, app, common.ApplicationWorkflowSuspending) + return ctrl.Result{}, r.patchStatusWithRetryOnConflict(ctx, app, common.ApplicationWorkflowSuspending) case common.WorkflowStateTerminated: if err := r.doWorkflowFinish(app, wf); err != nil { - return r.endWithNegativeCondition(ctx, app, condition.ErrorCondition("DoWorkflowFinish", err), common.ApplicationRunningWorkflow) + return r.endWithNegativeConditionWithRetry(ctx, app, condition.ErrorCondition("DoWorkflowFinish", err), common.ApplicationRunningWorkflow) } - return ctrl.Result{}, r.patchStatus(ctx, app, common.ApplicationWorkflowTerminated) + return ctrl.Result{}, r.patchStatusWithRetryOnConflict(ctx, app, common.ApplicationWorkflowTerminated) case common.WorkflowStateExecuting: - return reconcile.Result{RequeueAfter: baseWorkflowBackoffWaitTime}, r.patchStatus(ctx, app, common.ApplicationRunningWorkflow) + return reconcile.Result{RequeueAfter: baseWorkflowBackoffWaitTime}, r.patchStatusWithRetryOnConflict(ctx, app, common.ApplicationRunningWorkflow) case common.WorkflowStateSucceeded: wfStatus := app.Status.Workflow if wfStatus != nil { ref, err := handler.DispatchAndGC(ctx) if err == nil { - err = envbinding.GarbageCollectionForOutdatedResourcesInSubClusters(ctx, r.Client, policies, func(c context.Context) error { + err = multicluster.GarbageCollectionForOutdatedResourcesInSubClusters(ctx, app, func(c context.Context) error { _, e := handler.DispatchAndGC(c) return e }) @@ -212,17 +213,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu klog.ErrorS(err, "Failed to gc after workflow", "application", klog.KObj(app)) r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedGC, err)) - return r.endWithNegativeCondition(ctx, app, condition.ErrorCondition("GCAfterWorkflow", err), common.ApplicationRunningWorkflow) + return r.endWithNegativeConditionWithRetry(ctx, app, condition.ErrorCondition("GCAfterWorkflow", err), common.ApplicationRunningWorkflow) } app.Status.ResourceTracker = ref } if err := r.doWorkflowFinish(app, wf); err != nil { - return r.endWithNegativeCondition(ctx, app, condition.ErrorCondition("DoWorkflowFinish", err), common.ApplicationRunningWorkflow) + return r.endWithNegativeConditionWithRetry(ctx, app, condition.ErrorCondition("DoWorkflowFinish", err), common.ApplicationRunningWorkflow) } app.Status.SetConditions(condition.ReadyCondition("WorkflowFinished")) r.Recorder.Event(app, event.Normal(velatypes.ReasonApplied, velatypes.MessageWorkflowFinished)) klog.Info("Application manifests has applied by workflow successfully", "application", klog.KObj(app)) - return ctrl.Result{}, r.patchStatus(ctx, app, common.ApplicationWorkflowFinished) + return ctrl.Result{}, r.patchStatusWithRetryOnConflict(ctx, app, common.ApplicationWorkflowFinished) case common.WorkflowStateFinished: if status := app.Status.Workflow; status != nil && status.Terminated { return ctrl.Result{}, nil @@ -234,7 +235,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err != nil { klog.ErrorS(err, "Failed to render components", "application", klog.KObj(app)) r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRender, err)) - return r.endWithNegativeCondition(ctx, app, condition.ErrorCondition("Render", err), common.ApplicationRendering) + return r.endWithNegativeConditionWithRetry(ctx, app, condition.ErrorCondition("Render", err), common.ApplicationRendering) } assemble.HandleCheckManageWorkloadTrait(*handler.currentAppRev, comps) @@ -242,7 +243,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err := handler.HandleComponentsRevision(ctx, comps); err != nil { klog.ErrorS(err, "Failed to handle compoents revision", "application", klog.KObj(app)) r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err)) - return r.endWithNegativeCondition(ctx, app, condition.ErrorCondition("Render", err), common.ApplicationRendering) + return r.endWithNegativeConditionWithRetry(ctx, app, condition.ErrorCondition("Render", err), common.ApplicationRendering) } klog.Info("Application manifests has prepared and ready for appRollout to handle", "application", klog.KObj(app)) } @@ -332,6 +333,9 @@ func (r *Reconciler) handleFinalizers(ctx context.Context, app *v1beta1.Applicat return true, errors.WithMessage(err, "cannot remove finalizer") } } + if err := multicluster.GarbageCollectionForAllResourceTrackersInSubCluster(ctx, r.Client, app); err != nil { + return true, err + } meta.RemoveFinalizer(app, resourceTrackerFinalizer) // legacyOnlyRevisionFinalizer will be deprecated in the future // this is for backward compatibility @@ -342,18 +346,50 @@ func (r *Reconciler) handleFinalizers(ctx context.Context, app *v1beta1.Applicat return false, nil } -func (r *Reconciler) endWithNegativeCondition(ctx context.Context, app *v1beta1.Application, condition condition.Condition, phase common.ApplicationPhase) (ctrl.Result, error) { +func (r *Reconciler) _endWithNegativeCondition(ctx context.Context, app *v1beta1.Application, condition condition.Condition, phase common.ApplicationPhase, retry bool) (ctrl.Result, error) { app.SetConditions(condition) - if err := r.patchStatus(ctx, app, phase); err != nil { + handler := r.patchStatus + if retry { + handler = r.patchStatusWithRetryOnConflict + } + if err := handler(ctx, app, phase); err != nil { return ctrl.Result{}, errors.WithMessage(err, "cannot update application status") } return ctrl.Result{}, fmt.Errorf("object level reconcile error, type: %q, msg: %q", string(condition.Type), condition.Message) } +func (r *Reconciler) endWithNegativeCondition(ctx context.Context, app *v1beta1.Application, condition condition.Condition, phase common.ApplicationPhase) (ctrl.Result, error) { + return r._endWithNegativeCondition(ctx, app, condition, phase, false) +} + +// Note: Only operations that must override the status should use this function, it should only focus on workflow operations by now. +func (r *Reconciler) endWithNegativeConditionWithRetry(ctx context.Context, app *v1beta1.Application, condition condition.Condition, phase common.ApplicationPhase) (ctrl.Result, error) { + return r._endWithNegativeCondition(ctx, app, condition, phase, true) +} + func (r *Reconciler) patchStatus(ctx context.Context, app *v1beta1.Application, phase common.ApplicationPhase) error { app.Status.Phase = phase updateObservedGeneration(app) - return r.Client.Status().Patch(ctx, app, client.Merge) + return r.Status().Patch(ctx, app, client.Merge) +} + +// Note: Only operations that must override the status should use this function, it should only focus on workflow operations by now. +func (r *Reconciler) patchStatusWithRetryOnConflict(ctx context.Context, app *v1beta1.Application, phase common.ApplicationPhase) error { + app.Status.Phase = phase + updateObservedGeneration(app) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + status := app.Status.DeepCopy() + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(app), app); err != nil { + klog.ErrorS(err, "failed to get application while patching status", "application", klog.KObj(app)) + return err + } + app.Status = *status + err := r.Status().Patch(ctx, app, client.Merge) + if err != nil { + klog.ErrorS(err, "failed to re-patch status", "application", klog.KObj(app)) + } + return err + }) } func (r *Reconciler) doWorkflowFinish(app *v1beta1.Application, wf workflow.Workflow) error { diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go index 1d1f7ad49..5755e4bd3 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go @@ -81,6 +81,25 @@ func (h *AppHandler) Dispatch(ctx context.Context, cluster string, owner common. return err } +// Delete delete manifests from k8s. +func (h *AppHandler) Delete(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifest *unstructured.Unstructured) error { + if err := h.r.Delete(ctx, manifest); err != nil { + return err + } + ref := common.ClusterObjectReference{ + Cluster: cluster, + Creator: owner, + ObjectReference: corev1.ObjectReference{ + Name: manifest.GetName(), + Namespace: manifest.GetNamespace(), + Kind: manifest.GetKind(), + APIVersion: manifest.GetAPIVersion(), + }, + } + h.deleteAppliedResource(ref) + return nil +} + // addAppliedResource recorde applied resource. // reconcile run at single threaded. So there is no need to consider to use locker. func (h *AppHandler) addAppliedResource(refs ...common.ClusterObjectReference) { @@ -98,6 +117,16 @@ func (h *AppHandler) addAppliedResource(refs ...common.ClusterObjectReference) { } } +func (h *AppHandler) deleteAppliedResource(ref common.ClusterObjectReference) { + resouces := []common.ClusterObjectReference{} + for _, current := range h.appliedResources { + if !isSameObjReference(current, ref) { + resouces = append(resouces, current) + } + } + h.appliedResources = resouces +} + func isSameObjReference(ref1, ref2 common.ClusterObjectReference) bool { return ref1.Cluster == ref2.Cluster && ref1.Namespace == ref2.Namespace && @@ -228,7 +257,7 @@ func (h *AppHandler) aggregateHealthStatus(appFile *appfile.Appfile) ([]common.A switch wl.CapabilityCategory { case types.TerraformCategory: - pCtx = appfile.NewBasicContext(wl, appFile.Name, appFile.AppRevisionName, appFile.Namespace) + pCtx = appfile.NewBasicContext(appFile.Name, wl.Name, appFile.AppRevisionName, appFile.Namespace, wl.Params) ctx := context.Background() var configuration terraformapi.Configuration if err := h.r.Client.Get(ctx, client.ObjectKey{Name: wl.Name, Namespace: h.app.Namespace}, &configuration); err != nil { diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply_test.go index c572c99ed..bc543d61c 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/apply_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply_test.go @@ -20,6 +20,7 @@ import ( "context" "strconv" "strings" + "testing" "time" "github.com/oam-dev/kubevela/pkg/oam/testutil" @@ -28,10 +29,13 @@ import ( terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/yaml" @@ -40,6 +44,7 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" velatypes "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/appfile" + "github.com/oam-dev/kubevela/pkg/oam/util" ) const workloadDefinition = ` @@ -215,3 +220,122 @@ var _ = Describe("Test statusAggregate", func() { Expect(err).Should(BeNil()) }) }) + +var _ = Describe("Test deleter resource", func() { + It("Test delete resource will remove ref from reference", func() { + deployName := "test-del-resource-workload" + namespace := "test-del-resource-namespace" + ctx := context.Background() + Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})).Should(BeNil()) + deploy := appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: deployName, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: pointer.Int32Ptr(3), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "test", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "test-image", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, &deploy)).Should(BeNil()) + u := unstructured.Unstructured{} + u.SetAPIVersion("apps/v1") + u.SetKind("Deployment") + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: deployName, Namespace: namespace}, &u)).Should(BeNil()) + appliedRsc := []common.ClusterObjectReference{ + { + Creator: common.WorkflowResourceCreator, + ObjectReference: corev1.ObjectReference{ + Kind: u.GetKind(), + APIVersion: u.GetAPIVersion(), + Namespace: u.GetNamespace(), + Name: deployName, + }, + }, + { + Creator: common.WorkflowResourceCreator, + ObjectReference: corev1.ObjectReference{ + Kind: "StatefulSet", + APIVersion: "apps/v1", + Namespace: "test-namespace", + Name: "test-sts", + }, + }, + } + h := AppHandler{r: reconciler, appliedResources: appliedRsc} + Expect(h.Delete(ctx, "", common.WorkflowResourceCreator, &u)) + checkDeploy := unstructured.Unstructured{} + checkDeploy.SetAPIVersion("apps/v1") + checkDeploy.SetKind("Deployment") + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: deployName, Namespace: namespace}, &u)).Should(SatisfyAny(util.NotFoundMatcher{})) + Expect(len(h.appliedResources)).Should(BeEquivalentTo(1)) + Expect(h.appliedResources[0].Kind).Should(BeEquivalentTo("StatefulSet")) + Expect(h.appliedResources[0].Name).Should(BeEquivalentTo("test-sts")) + }) +}) + +func TestDeleteAppliedResourceFunc(t *testing.T) { + h := AppHandler{appliedResources: []common.ClusterObjectReference{ + { + ObjectReference: corev1.ObjectReference{ + Name: "wl-1", + Kind: "Deployment", + }, + }, + { + ObjectReference: corev1.ObjectReference{ + Name: "wl-2", + Kind: "Deployment", + }, + }, + { + ObjectReference: corev1.ObjectReference{ + Name: "wl-1", + Kind: "StatefulSet", + }, + }, + { + Cluster: "runtime-cluster", + ObjectReference: corev1.ObjectReference{ + Name: "wl-1", + Kind: "StatefulSet", + }, + }, + }} + deleteResc_1 := common.ClusterObjectReference{ObjectReference: corev1.ObjectReference{Name: "wl-1", Kind: "StatefulSet"}, Cluster: "runtime-cluster"} + deleteResc_2 := common.ClusterObjectReference{ObjectReference: corev1.ObjectReference{Name: "wl-2", Kind: "Deployment"}} + h.deleteAppliedResource(deleteResc_1) + h.deleteAppliedResource(deleteResc_2) + if len(h.appliedResources) != 2 { + t.Errorf("applied length error acctually %d", len(h.appliedResources)) + } + if h.appliedResources[0].Name != "wl-1" || h.appliedResources[0].Kind != "Deployment" { + t.Errorf("resource missmatch") + } + if h.appliedResources[1].Name != "wl-1" || h.appliedResources[1].Kind != "StatefulSet" { + t.Errorf("resource missmatch") + } +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/generator.go b/pkg/controller/core.oam.dev/v1alpha2/application/generator.go index 622216def..cfa873f5c 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/generator.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/generator.go @@ -30,13 +30,12 @@ import ( "github.com/oam-dev/kubevela/pkg/appfile" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/assemble" "github.com/oam-dev/kubevela/pkg/cue/model/value" - "github.com/oam-dev/kubevela/pkg/cue/packages" "github.com/oam-dev/kubevela/pkg/multicluster" - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils" "github.com/oam-dev/kubevela/pkg/workflow/providers" "github.com/oam-dev/kubevela/pkg/workflow/providers/kube" + multiclusterProvider "github.com/oam-dev/kubevela/pkg/workflow/providers/multicluster" oamProvider "github.com/oam-dev/kubevela/pkg/workflow/providers/oam" "github.com/oam-dev/kubevela/pkg/workflow/tasks" wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" @@ -48,15 +47,13 @@ func (h *AppHandler) GenerateApplicationSteps(ctx context.Context, app *v1beta1.Application, appParser *appfile.Parser, af *appfile.Appfile, - appRev *v1beta1.ApplicationRevision, - cli client.Client, - dm discoverymapper.DiscoveryMapper, - pd *packages.PackageDiscover) ([]wfTypes.TaskRunner, error) { + appRev *v1beta1.ApplicationRevision) ([]wfTypes.TaskRunner, error) { handlerProviders := providers.NewProviders() - kube.Install(handlerProviders, cli, h.Dispatch) + kube.Install(handlerProviders, h.r.Client, h.Dispatch, h.Delete) oamProvider.Install(handlerProviders, app, h.applyComponentFunc( - appParser, appRev, af, cli)) - taskDiscover := tasks.NewTaskDiscover(handlerProviders, pd, cli, dm) + appParser, appRev, af), h.renderComponentFunc(appParser, appRev, af)) + taskDiscover := tasks.NewTaskDiscover(handlerProviders, h.r.pd, h.r.Client, h.r.dm) + multiclusterProvider.Install(handlerProviders, h.r.Client, app) var tasks []wfTypes.TaskRunner for _, step := range af.WorkflowSteps { options := &wfTypes.GeneratorOptions{ @@ -123,39 +120,43 @@ func convertStepProperties(step *v1beta1.WorkflowStep, app *v1beta1.Application) return errors.Errorf("component %s not found", o.Component) } -func (h *AppHandler) applyComponentFunc(appParser *appfile.Parser, appRev *v1beta1.ApplicationRevision, af *appfile.Appfile, cli client.Client) oamProvider.ComponentApply { - return func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) { +func (h *AppHandler) renderComponentFunc(appParser *appfile.Parser, appRev *v1beta1.ApplicationRevision, af *appfile.Appfile) oamProvider.ComponentRender { + return func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) { ctx := multicluster.ContextWithClusterName(context.Background(), clusterName) - wl, err := appParser.ParseWorkloadFromRevision(comp, appRev) + _, manifest, err := h.prepareWorkloadAndManifests(ctx, appParser, comp, appRev, patcher, af) if err != nil { - return nil, nil, false, errors.WithMessage(err, "ParseWorkload") + return nil, nil, err } - wl.Patch = patcher - manifest, err := af.GenerateComponentManifest(wl) + return renderComponentsAndTraits(h.r.Client, manifest, appRev, overrideNamespace) + } +} + +func (h *AppHandler) applyComponentFunc(appParser *appfile.Parser, appRev *v1beta1.ApplicationRevision, af *appfile.Appfile) oamProvider.ComponentApply { + return func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) { + ctx := multicluster.ContextWithClusterName(context.Background(), clusterName) + if overrideNamespace != "" { + oldNamespace := h.app.Namespace + h.app.Namespace = overrideNamespace + defer func() { + h.app.Namespace = oldNamespace + }() + } + + wl, manifest, err := h.prepareWorkloadAndManifests(ctx, appParser, comp, appRev, patcher, af) if err != nil { - return nil, nil, false, errors.WithMessage(err, "GenerateComponentManifest") - } - if err := af.SetOAMContract(manifest); err != nil { - return nil, nil, false, errors.WithMessage(err, "SetOAMContract") - } - if err := h.HandleComponentsRevision(ctx, []*types.ComponentManifest{manifest}); err != nil { - return nil, nil, false, errors.WithMessage(err, "HandleComponentsRevision") + return nil, nil, false, err } if len(manifest.PackagedWorkloadResources) != 0 { if err := h.Dispatch(ctx, clusterName, common.WorkflowResourceCreator, manifest.PackagedWorkloadResources...); err != nil { return nil, nil, false, errors.WithMessage(err, "cannot dispatch packaged workload resources") } } - readyWorkload, readyTraits, err := assemble.PrepareBeforeApply(manifest, appRev, []assemble.WorkloadOption{assemble.DiscoveryHelmBasedWorkload(context.TODO(), h.r.Client)}) + wl.Ctx.SetCtx(ctx) + + readyWorkload, readyTraits, err := renderComponentsAndTraits(h.r.Client, manifest, appRev, overrideNamespace) if err != nil { - return nil, nil, false, errors.WithMessage(err, "assemble resources before apply fail") - } - if overrideNamespace != "" { - readyWorkload.SetNamespace(overrideNamespace) - for _, readyTrait := range readyTraits { - readyTrait.SetNamespace(overrideNamespace) - } + return nil, nil, false, err } skipStandardWorkload := skipApplyWorkload(wl) if !skipStandardWorkload { @@ -176,11 +177,50 @@ func (h *AppHandler) applyComponentFunc(appParser *appfile.Parser, appRev *v1bet if !isHealth { return nil, nil, false, nil } - workload, traits, err := getComponentResources(ctx, manifest, skipStandardWorkload, cli) + workload, traits, err := getComponentResources(ctx, manifest, skipStandardWorkload, h.r.Client) return workload, traits, true, err } } +func (h *AppHandler) prepareWorkloadAndManifests(ctx context.Context, + appParser *appfile.Parser, + comp common.ApplicationComponent, + appRev *v1beta1.ApplicationRevision, + patcher *value.Value, + af *appfile.Appfile) (*appfile.Workload, *types.ComponentManifest, error) { + wl, err := appParser.ParseWorkloadFromRevision(comp, appRev) + if err != nil { + return nil, nil, errors.WithMessage(err, "ParseWorkload") + } + wl.Patch = patcher + manifest, err := af.GenerateComponentManifest(wl) + if err != nil { + return nil, nil, errors.WithMessage(err, "GenerateComponentManifest") + } + if err := af.SetOAMContract(manifest); err != nil { + return nil, nil, errors.WithMessage(err, "SetOAMContract") + } + if err := h.HandleComponentsRevision(ctx, []*types.ComponentManifest{manifest}); err != nil { + return nil, nil, errors.WithMessage(err, "HandleComponentsRevision") + } + + return wl, manifest, nil +} + +func renderComponentsAndTraits(client client.Client, manifest *types.ComponentManifest, appRev *v1beta1.ApplicationRevision, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) { + readyWorkload, readyTraits, err := assemble.PrepareBeforeApply(manifest, appRev, []assemble.WorkloadOption{assemble.DiscoveryHelmBasedWorkload(context.TODO(), client)}) + if err != nil { + return nil, nil, errors.WithMessage(err, "assemble resources before apply fail") + } + if overrideNamespace != "" { + readyWorkload.SetNamespace(overrideNamespace) + for _, readyTrait := range readyTraits { + readyTrait.SetNamespace(overrideNamespace) + } + } + return readyWorkload, readyTraits, nil +} + func skipApplyWorkload(wl *appfile.Workload) bool { for _, trait := range wl.Traits { if trait.FullTemplate.TraitDefinition.Spec.ManageWorkload { diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/generator_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/generator_test.go index d26d6b223..c34b81078 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/generator_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/generator_test.go @@ -30,8 +30,6 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/common" oamcore "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/pkg/cue/packages" - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" "github.com/oam-dev/kubevela/pkg/oam/util" ) @@ -112,10 +110,6 @@ var _ = Describe("Test Application workflow generator", func() { _, err = af.PrepareWorkflowAndPolicy() Expect(err).Should(BeNil()) appRev := &oamcore.ApplicationRevision{} - dm, err := discoverymapper.New(cfg) - Expect(err).To(BeNil()) - pd, err := packages.NewPackageDiscover(cfg) - Expect(err).To(BeNil()) handler := &AppHandler{ r: reconciler, @@ -123,7 +117,7 @@ var _ = Describe("Test Application workflow generator", func() { parser: appParser, } - taskRunner, err := handler.GenerateApplicationSteps(ctx, app, appParser, af, appRev, k8sClient, dm, pd) + taskRunner, err := handler.GenerateApplicationSteps(ctx, app, appParser, af, appRev) Expect(err).To(BeNil()) Expect(len(taskRunner)).Should(BeEquivalentTo(2)) Expect(taskRunner[0].Name()).Should(BeEquivalentTo("myweb1")) @@ -160,10 +154,6 @@ var _ = Describe("Test Application workflow generator", func() { _, err = af.PrepareWorkflowAndPolicy() Expect(err).Should(BeNil()) appRev := &oamcore.ApplicationRevision{} - dm, err := discoverymapper.New(cfg) - Expect(err).To(BeNil()) - pd, err := packages.NewPackageDiscover(cfg) - Expect(err).To(BeNil()) handler := &AppHandler{ r: reconciler, @@ -171,10 +161,92 @@ var _ = Describe("Test Application workflow generator", func() { parser: appParser, } - taskRunner, err := handler.GenerateApplicationSteps(ctx, app, appParser, af, appRev, k8sClient, dm, pd) + taskRunner, err := handler.GenerateApplicationSteps(ctx, app, appParser, af, appRev) Expect(err).To(BeNil()) Expect(len(taskRunner)).Should(BeEquivalentTo(2)) Expect(taskRunner[0].Name()).Should(BeEquivalentTo("myweb1")) Expect(taskRunner[1].Name()).Should(BeEquivalentTo("myweb2")) }) + + It("Test render component", func() { + cd := &oamcore.ComponentDefinition{} + td := &oamcore.TraitDefinition{} + + defJson, err := yaml.YAMLToJSON([]byte(componentDefYaml)) + Expect(err).Should(BeNil()) + Expect(json.Unmarshal(defJson, cd)).Should(BeNil()) + cd.SetNamespace("vela-system") + Expect(k8sClient.Create(ctx, cd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + rolloutTdDef, err := yaml.YAMLToJSON([]byte(rolloutTraitDefinition)) + Expect(err).Should(BeNil()) + Expect(json.Unmarshal(rolloutTdDef, td)).Should(BeNil()) + td.SetNamespace("vela-system") + Expect(k8sClient.Create(ctx, td)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + app := &oamcore.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "app-test", + Namespace: namespaceName, + }, + Spec: oamcore.ApplicationSpec{ + Components: []common.ApplicationComponent{ + { + Name: "myweb1", + Type: "worker", + Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)}, + Traits: []common.ApplicationTrait{ + { + Type: "rollout", + }, + }, + }, + }, + }, + } + af, err := appParser.GenerateAppFile(ctx, app) + Expect(err).Should(BeNil()) + _, err = af.PrepareWorkflowAndPolicy() + Expect(err).Should(BeNil()) + apprev := &oamcore.ApplicationRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-test-v1", + Namespace: namespaceName, + }, + Spec: oamcore.ApplicationRevisionSpec{ + Application: *app.DeepCopy(), + ComponentDefinitions: make(map[string]oamcore.ComponentDefinition), + WorkloadDefinitions: make(map[string]oamcore.WorkloadDefinition), + TraitDefinitions: make(map[string]oamcore.TraitDefinition), + ScopeDefinitions: make(map[string]oamcore.ScopeDefinition), + }, + } + apprev.Spec.ComponentDefinitions["worker"] = *cd + apprev.Spec.TraitDefinitions["rollout"] = *td + Expect(k8sClient.Create(ctx, apprev)).Should(BeNil()) + + handler := &AppHandler{ + r: reconciler, + app: app, + parser: appParser, + } + + renderFunc := handler.renderComponentFunc(appParser, apprev, af) + comp := common.ApplicationComponent{ + Name: "myweb1", + Type: "worker", + Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)}, + Traits: []common.ApplicationTrait{ + { + Type: "rollout", + }, + }, + } + _, _, err = renderFunc(comp, nil, "", "") + Expect(err).Should(BeNil()) + }) }) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go index ab2a5854c..281f28231 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go @@ -24,6 +24,7 @@ import ( "strings" "github.com/oam-dev/kubevela/pkg/cue/model" + "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" @@ -859,7 +860,8 @@ func cleanUpWorkflowComponentRevision(ctx context.Context, h *AppHandler) error ns := resource.Namespace r := &unstructured.Unstructured{} r.GetObjectKind().SetGroupVersionKind(resource.GroupVersionKind()) - err := h.r.Get(ctx, ktypes.NamespacedName{Name: compName, Namespace: ns}, r) + _ctx := multicluster.ContextWithClusterName(ctx, resource.Cluster) + err := h.r.Get(_ctx, ktypes.NamespacedName{Name: compName, Namespace: ns}, r) if err != nil { return err } @@ -877,7 +879,8 @@ func cleanUpWorkflowComponentRevision(ctx context.Context, h *AppHandler) error listOpts := []client.ListOption{client.MatchingLabels{ oam.LabelControllerRevisionComponent: curComp.Name, }, client.InNamespace(h.app.Namespace)} - if err := h.r.List(ctx, crList, listOpts...); err != nil { + _ctx := multicluster.ContextWithClusterName(ctx, curComp.Cluster) + if err := h.r.List(_ctx, crList, listOpts...); err != nil { return err } needKill := len(crList.Items) - h.r.appRevisionLimit - len(compRevisionInUse[curComp.Name]) @@ -893,7 +896,7 @@ func cleanUpWorkflowComponentRevision(ctx context.Context, h *AppHandler) error if _, inUse := compRevisionInUse[curComp.Name][rev.Name]; inUse { continue } - if err := h.r.Delete(ctx, rev.DeepCopy()); err != nil && !apierrors.IsNotFound(err) { + if err := h.r.Delete(_ctx, rev.DeepCopy()); err != nil && !apierrors.IsNotFound(err) { return err } needKill-- diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationrollout/helper.go b/pkg/controller/core.oam.dev/v1alpha2/applicationrollout/helper.go index 800158c2d..51ff49a75 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/applicationrollout/helper.go +++ b/pkg/controller/core.oam.dev/v1alpha2/applicationrollout/helper.go @@ -89,13 +89,10 @@ func HandleReplicas(ctx context.Context, rolloutComp string, c client.Client) as pv := fieldpath.Pave(u.UnstructuredContent()) // we hard code here, but we can easily support more types of workload by add more cases logic in switch - var replicasFieldPath string - switch u.GetKind() { - case reflect.TypeOf(v1alpha1.CloneSet{}).Name(), reflect.TypeOf(appsv1.Deployment{}).Name(), reflect.TypeOf(appsv1.StatefulSet{}).Name(): - replicasFieldPath = "spec.replicas" - default: + replicasFieldPath, err := GetWorkloadReplicasPath(*u) + if err != nil { klog.Errorf("rollout meet a workload we cannot support yet", "Kind", u.GetKind(), "name", u.GetName()) - return fmt.Errorf("rollout meet a workload we cannot support yet Kind %s name %s", u.GetKind(), u.GetName()) + return err } workload := u.DeepCopy() @@ -127,6 +124,16 @@ func HandleReplicas(ctx context.Context, rolloutComp string, c client.Client) as }) } +// GetWorkloadReplicasPath get replicas path of workload +func GetWorkloadReplicasPath(u unstructured.Unstructured) (string, error) { + switch u.GetKind() { + case reflect.TypeOf(v1alpha1.CloneSet{}).Name(), reflect.TypeOf(appsv1.Deployment{}).Name(), reflect.TypeOf(appsv1.StatefulSet{}).Name(): + return "spec.replicas", nil + default: + return "", fmt.Errorf("rollout meet a workload we cannot support yet Kind %s name %s", u.GetKind(), u.GetName()) + } +} + // appRollout should take over updating workload, so disable previous controller owner(resourceTracker) func disableControllerOwner(workload *unstructured.Unstructured) { if workload == nil { diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationrollout/helper_test.go b/pkg/controller/core.oam.dev/v1alpha2/applicationrollout/helper_test.go index 6d3cc8623..bd65ce775 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/applicationrollout/helper_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/applicationrollout/helper_test.go @@ -20,12 +20,13 @@ import ( "testing" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" oamstandard "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/pkg/oam/util" "gotest.tools/assert" + appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/utils/pointer" @@ -192,3 +193,37 @@ func TestHandleTerminated(t *testing.T) { } } } + +func TestGetWorkloadReplicasPath(t *testing.T) { + deploy := appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "appsv1", + Kind: "Deployment", + }, + } + u, err := util.Object2Unstructured(deploy) + if err != nil { + t.Errorf("deployment shounld't meet an error %w", err) + } + pathStr, err := GetWorkloadReplicasPath(*u) + if err != nil { + t.Errorf("deployment should handle deployment") + } + if pathStr != "spec.replicas" { + t.Errorf("deployPath error got %s", pathStr) + } + ds := appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "appsv1", + Kind: "DaemonSet", + }, + } + u, err = util.Object2Unstructured(ds) + if err != nil { + t.Errorf("ds shounld't meet an error %w", err) + } + _, err = GetWorkloadReplicasPath(*u) + if err == nil { + t.Errorf("daemonset shouldn't support") + } +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_controller_test.go index 70378c3d6..10a48fe25 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_controller_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_controller_test.go @@ -173,7 +173,7 @@ spec: By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) + name := fmt.Sprintf("component-%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) Eventually(func() bool { err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) return err == nil @@ -264,7 +264,7 @@ spec: By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) + name := fmt.Sprintf("component-%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) Eventually(func() bool { err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) return err == nil @@ -306,8 +306,9 @@ spec: cd.SetNamespace(namespace) Expect(k8sClient.Create(ctx, &cd)).Should(Succeed()) req := reconcile.Request{NamespacedName: client.ObjectKey{Name: cd.Name, Namespace: cd.Namespace}} + By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, cd.Name) + name := fmt.Sprintf("component-%s%s", types.CapabilityConfigMapNamePrefix, cd.Name) Eventually(func() bool { testutil.ReconcileRetry(&r, req) err := k8sClient.Get(ctx, client.ObjectKey{Namespace: cd.Namespace, Name: name}, &cm) @@ -347,7 +348,7 @@ spec: By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) + name := fmt.Sprintf("component-%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) Eventually(func() bool { err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) return err == nil @@ -392,7 +393,7 @@ spec: By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) + name := fmt.Sprintf("component-%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) Eventually(func() bool { err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) return err == nil @@ -501,7 +502,7 @@ spec: By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) + name := fmt.Sprintf("component-%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) Eventually(func() bool { err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) return err == nil @@ -725,7 +726,7 @@ spec: By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) + name := fmt.Sprintf("component-%s%s", types.CapabilityConfigMapNamePrefix, componentDefinitionName) Eventually(func() bool { testutil.ReconcileRetry(&r, req) err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go index 5e83ca535..5660b69c5 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go @@ -439,7 +439,7 @@ func CUEBasedHealthCheck(ctx context.Context, c client.Client, wlRef WorkloadRef switch wl.CapabilityCategory { case oamtypes.TerraformCategory: - pCtx = af.NewBasicContext(wl, appfile.Name, appfile.AppRevisionName, appfile.Namespace) + pCtx = af.NewBasicContext(appfile.Name, wl.Name, appfile.AppRevisionName, appfile.Namespace, wl.Params) ctx := context.Background() var configuration terraformapi.Configuration if err := c.Get(ctx, client.ObjectKey{Name: wl.Name, Namespace: ns}, &configuration); err != nil { diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope_controller.go b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope_controller.go index cdc97125b..f01b42f62 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope_controller.go @@ -39,17 +39,16 @@ import ( commonapis "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/condition" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" af "github.com/oam-dev/kubevela/pkg/appfile" "github.com/oam-dev/kubevela/pkg/controller/common" controller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" - "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha1/envbinding" "github.com/oam-dev/kubevela/pkg/cue/packages" "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/policy/envbinding" ) const ( @@ -462,60 +461,18 @@ func (r *Reconciler) patchHealthStatusToApplications(ctx context.Context, appHea return nil } -func (r *Reconciler) getEnvBinding(ctx context.Context, appName string, ns string) (*v1alpha1.EnvBinding, *v1beta1.Application, error) { - app := new(v1beta1.Application) - appKey := client.ObjectKey{Name: appName, Namespace: ns} - if err := r.client.Get(ctx, appKey, app); err != nil { - return nil, nil, err - } - - var envBindingName string - for _, policy := range app.Spec.Policies { - if policy.Type == "env-binding" { - envBindingName = policy.Name - break - } - } - if len(envBindingName) == 0 { - return nil, app, nil - } - - envBinding := new(v1alpha1.EnvBinding) - envBindingKey := client.ObjectKey{Name: envBindingName, Namespace: ns} - if err := r.client.Get(ctx, envBindingKey, envBinding); err != nil { - return nil, nil, err - } - - if envBinding.Status.Phase != v1alpha1.EnvBindingFinished { - return nil, nil, errors.Errorf("policy env-binding was not ready") - } - return envBinding, app, nil -} - func (r *Reconciler) createAppfile(ctx context.Context, appName, ns, envName string) (*af.Appfile, error) { appParser := af.NewApplicationParser(r.client, r.dm, r.pd) if len(envName) != 0 { - envBinding, baseApp, err := r.getEnvBinding(ctx, appName, ns) + app := &v1beta1.Application{} + if err := r.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: appName}, app); err != nil { + return nil, err + } + patchedApp, err := envbinding.PatchApplicationByEnvBindingEnv(app, "", envName) if err != nil { return nil, err } - var targetEnvConfig *v1alpha1.EnvConfig - for i := range envBinding.Spec.Envs { - envConfig := envBinding.Spec.Envs[i] - if envConfig.Name == envName { - targetEnvConfig = &envConfig - break - } - } - if targetEnvConfig == nil { - return nil, errors.Errorf("policy env-binding doesn't contains env %s", envName) - } - - envBindApp := envbinding.NewEnvBindApp(baseApp, targetEnvConfig) - if err = envBindApp.GenerateConfiguredApplication(); err != nil { - return nil, err - } - return appParser.GenerateAppFile(ctx, envBindApp.PatchedApp) + return appParser.GenerateAppFile(ctx, patchedApp) } app := &v1beta1.Application{} @@ -571,20 +528,31 @@ func constructAppCompStatus(appC *AppHealthCondition, hsRef corev1.ObjectReferen func (r *Reconciler) createWorkloadRefs(ctx context.Context, appRef v1alpha2.AppReference, ns string) []WorkloadReference { wlRefs := make([]WorkloadReference, 0) - envBinding, application, err := r.getEnvBinding(ctx, appRef.AppName, ns) - if err != nil { - klog.ErrorS(err, "Failed to get envBinding") + application := &v1beta1.Application{} + if err := r.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: appRef.AppName}, application); err != nil { + klog.ErrorS(err, "Failed to get application") return wlRefs } - var decisions []v1alpha1.ClusterDecision - decisionsMap := make(map[string]string) - if envBinding == nil { - decisions = make([]v1alpha1.ClusterDecision, 1) - } else { - decisions = envBinding.Status.ClusterDecisions - for _, decision := range decisions { - decisionsMap[decision.Cluster] = decision.Env + // ugly implementation, should be reworked in future + decisionsMap := map[string]string{} + var decisions []struct { + Cluster string + Env string + } + policyStatus, err := envbinding.GetEnvBindingPolicyStatus(application, "") + if err == nil && policyStatus != nil { + for _, env := range policyStatus.Envs { + for _, placement := range env.Placements { + decisionsMap[placement.Cluster] = env.Env + decisions = append(decisions, struct { + Cluster string + Env string + }{ + Cluster: placement.Cluster, + Env: env.Env, + }) + } } } diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_controller_test.go index f64efb962..e621e3621 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_controller_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_controller_test.go @@ -137,7 +137,7 @@ spec: By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, traitDefinitionName) + name := fmt.Sprintf("trait-%s%s", types.CapabilityConfigMapNamePrefix, traitDefinitionName) Eventually(func() bool { err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) return err == nil @@ -288,7 +288,7 @@ spec: By("Check whether ConfigMap is created") var cm corev1.ConfigMap - name := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, traitDefinitionName) + name := fmt.Sprintf("trait-%s%s", types.CapabilityConfigMapNamePrefix, traitDefinitionName) Eventually(func() bool { testutil.ReconcileRetry(&r, req) err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller.go b/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller.go index 66068e266..165725d42 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller.go @@ -122,6 +122,28 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu r.record.Event(&wfstepdefinition, event.Warning("failed to garbage collect DefinitionRevision of type WorkflowStepDefinition", err)) } + def := utils.NewCapabilityStepDef(&wfstepdefinition) + def.Name = req.NamespacedName.Name + // Store the parameter of stepDefinition to configMap + cmName, err := def.StoreOpenAPISchema(ctx, r.Client, r.pd, req.Namespace, req.Name, defRev.Name) + if err != nil { + klog.InfoS("Could not store capability in ConfigMap", "err", err) + r.record.Event(&(wfstepdefinition), event.Warning("Could not store capability in ConfigMap", err)) + return ctrl.Result{}, util.PatchCondition(ctx, r, &wfstepdefinition, + condition.ReconcileError(fmt.Errorf(util.ErrStoreCapabilityInConfigMap, wfstepdefinition.Name, err))) + } + + if wfstepdefinition.Status.ConfigMapRef != cmName { + wfstepdefinition.Status.ConfigMapRef = cmName + if err := r.UpdateStatus(ctx, &wfstepdefinition); err != nil { + klog.ErrorS(err, "Could not update WorkflowStepDefinition Status", "workflowStepDefinition", klog.KRef(req.Namespace, req.Name)) + r.record.Event(&wfstepdefinition, event.Warning("Could not update WorkflowStepDefinition Status", err)) + return ctrl.Result{}, util.PatchCondition(ctx, r, &wfstepdefinition, + condition.ReconcileError(fmt.Errorf(util.ErrUpdateWorkflowStepDefinition, wfstepdefinition.Name, err))) + } + klog.InfoS("Successfully updated the status.configMapRef of the WorkflowStepDefinition", "workflowStepDefinition", + klog.KRef(req.Namespace, req.Name), "status.configMapRef", cmName) + } return ctrl.Result{}, nil } diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller_test.go new file mode 100644 index 000000000..d53216868 --- /dev/null +++ b/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller_test.go @@ -0,0 +1,197 @@ +/* + + 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 workflowstepdefinition + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/oam/testutil" + "github.com/oam-dev/kubevela/pkg/oam/util" +) + +var _ = Describe("Apply WorkflowStepDefinition to store its schema to ConfigMap Test", func() { + ctx := context.Background() + var ns corev1.Namespace + + Context("When the WorkflowStepDefinition is valid, but the namespace doesn't exist, should occur errors", func() { + It("Apply WorkflowStepDefinition", func() { + By("Apply WorkflowStepDefinition") + var validWorkflowStepDefinition = ` +apiVersion: core.oam.dev/v1beta1 +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Apply raw kubernetes objects for your workflow steps + name: apply-object + namespace: not-exist +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + apply: op.#Apply & { + value: parameter.value + cluster: parameter.cluster + } + parameter: { + // +usage=Specify the value of the object + value: {...} + // +usage=Specify the cluster of the object + cluster: *"" | string + } +` + + var def v1beta1.WorkflowStepDefinition + Expect(yaml.Unmarshal([]byte(validWorkflowStepDefinition), &def)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &def)).Should(Not(Succeed())) + }) + }) + + Context("When the WorkflowStepDefinition is valid, should create a ConfigMap", func() { + var WorkflowStepDefinitionName = "apply-object" + var namespace = "ns-wfs-def-1" + req := reconcile.Request{NamespacedName: client.ObjectKey{Name: WorkflowStepDefinitionName, Namespace: namespace}} + + It("Apply WorkflowStepDefinition", func() { + ns = corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + By("Create a namespace") + Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{})) + + By("Apply WorkflowStepDefinition") + var validWorkflowStepDefinition = ` +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Apply raw kubernetes objects for your workflow steps + name: apply-object + namespace: ns-wfs-def-1 +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + apply: op.#Apply & { + value: parameter.value + cluster: parameter.cluster + } + parameter: { + // +usage=Specify the value of the object + value: {...} + // +usage=Specify the cluster of the object + cluster: *"" | string + } +` + + var def v1beta1.WorkflowStepDefinition + Expect(yaml.Unmarshal([]byte(validWorkflowStepDefinition), &def)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &def)).Should(Succeed()) + testutil.ReconcileRetry(&r, req) + + By("Check whether ConfigMap is created") + var cm corev1.ConfigMap + name := fmt.Sprintf("workflowstep-%s%s", types.CapabilityConfigMapNamePrefix, WorkflowStepDefinitionName) + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &cm) + return err == nil + }, 30*time.Second, time.Second).Should(BeTrue()) + Expect(cm.Data[types.OpenapiV3JSONSchema]).Should(Not(Equal(""))) + Expect(cm.Labels["definition.oam.dev/name"]).Should(Equal(WorkflowStepDefinitionName)) + + By("Check whether ConfigMapRef refer to right") + Eventually(func() string { + _ = k8sClient.Get(ctx, client.ObjectKey{Namespace: def.Namespace, Name: def.Name}, &def) + return def.Status.ConfigMapRef + }, 30*time.Second, time.Second).Should(Equal(name)) + + By("Delete the workflowstep") + Expect(k8sClient.Delete(ctx, &def)).Should(Succeed()) + testutil.ReconcileRetry(&r, req) + }) + }) + + Context("When the WorkflowStepDefinition is invalid, should report issues", func() { + var invalidWorkflowStepDefinitionName = "invalid-wf1" + var namespace = "ns-wfs-def2" + BeforeEach(func() { + ns = corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + By("Create a namespace") + Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{})) + }) + + It("Applying invalid WorkflowStepDefinition", func() { + By("Apply the WorkflowStepDefinition") + var invalidWorkflowStepDefinition = ` +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Apply raw kubernetes objects for your workflow steps + name: invalid-wf1 + namespace: ns-wfs-def2 +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + apply: op.#Apply & { + value: parameter.value + cluster: parameter.cluster + } +` + + var invalidDef v1beta1.WorkflowStepDefinition + Expect(yaml.Unmarshal([]byte(invalidWorkflowStepDefinition), &invalidDef)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &invalidDef)).Should(Succeed()) + gotWorkflowStepDefinition := &v1beta1.WorkflowStepDefinition{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: invalidWorkflowStepDefinitionName, Namespace: namespace}, gotWorkflowStepDefinition)).Should(BeNil()) + }) + }) +}) diff --git a/pkg/controller/setup.go b/pkg/controller/setup.go index c0b3982c1..044120e17 100644 --- a/pkg/controller/setup.go +++ b/pkg/controller/setup.go @@ -21,7 +21,6 @@ import ( "github.com/oam-dev/kubevela/pkg/controller/common" controller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" - "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha1/envbinding" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/core/traits/manualscalertrait" "github.com/oam-dev/kubevela/pkg/controller/standard.oam.dev/v1alpha1/rollout" @@ -37,7 +36,6 @@ func Setup(mgr ctrl.Manager, disableCaps string, args controller.Args) error { manualscalertrait.Setup, healthscope.Setup, rollout.Setup, - envbinding.Setup, } case common.DisableAllCaps: default: @@ -52,9 +50,6 @@ func Setup(mgr ctrl.Manager, disableCaps string, args controller.Args) error { if !disableCapsSet.Contains(common.RolloutControllerName) { functions = append(functions, rollout.Setup) } - if !disableCapsSet.Contains(common.EnvBindingControllerName) { - functions = append(functions, envbinding.Setup) - } } for _, setup := range functions { diff --git a/pkg/controller/standard.oam.dev/v1alpha1/rollout/handler.go b/pkg/controller/standard.oam.dev/v1alpha1/rollout/handler.go index f864805db..b6bbbc2c0 100644 --- a/pkg/controller/standard.oam.dev/v1alpha1/rollout/handler.go +++ b/pkg/controller/standard.oam.dev/v1alpha1/rollout/handler.go @@ -34,6 +34,7 @@ import ( "k8s.io/utils/pointer" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1" @@ -240,6 +241,19 @@ func (h *handler) checkWorkloadNotExist(ctx context.Context) (bool, error) { return false, nil } +func getWorkloadReplicasNum(u unstructured.Unstructured) (int32, error) { + replicaPath, err := applicationrollout.GetWorkloadReplicasPath(u) + if err != nil { + return 0, fmt.Errorf("get workload replicas path err %w", err) + } + wlpv := fieldpath.Pave(u.UnstructuredContent()) + replicas, err := wlpv.GetInteger(replicaPath) + if err != nil { + return 0, fmt.Errorf("get workload replicas err %w", err) + } + return int32(replicas), nil +} + // checkRollingTerminated check the rollout if have finished func checkRollingTerminated(rollout v1alpha1.Rollout) bool { // handle rollout completed diff --git a/pkg/controller/standard.oam.dev/v1alpha1/rollout/handler_suit_test.go b/pkg/controller/standard.oam.dev/v1alpha1/rollout/handler_suit_test.go index c0e35d658..f9d1a2615 100644 --- a/pkg/controller/standard.oam.dev/v1alpha1/rollout/handler_suit_test.go +++ b/pkg/controller/standard.oam.dev/v1alpha1/rollout/handler_suit_test.go @@ -40,6 +40,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" ) var _ = Describe("Test rollout related handler func", func() { @@ -513,6 +514,50 @@ var _ = Describe("Test rollout related handler func", func() { Expect(checkRt.Status.TrackedResources[0].Name).Should(BeEquivalentTo(u.GetName())) Expect(checkRt.Status.TrackedResources[0].UID).Should(BeEquivalentTo(u.GetUID())) }) + + It("TestGetWorkloadReplicasNum", func() { + deployName := "test-workload-get" + deploy := appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: deployName, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: pointer.Int32Ptr(3), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "test", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "test-container", + Image: "test-image", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, &deploy)).Should(BeNil()) + u := unstructured.Unstructured{} + u.SetAPIVersion("apps/v1") + u.SetKind("Deployment") + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: deployName, Namespace: namespace}, &u)).Should(BeNil()) + rep, err := getWorkloadReplicasNum(u) + Expect(err).Should(BeNil()) + Expect(rep).Should(BeEquivalentTo(3)) + }) }) }) diff --git a/pkg/controller/standard.oam.dev/v1alpha1/rollout/rollout_controller.go b/pkg/controller/standard.oam.dev/v1alpha1/rollout/rollout_controller.go index 2eae7a01d..2b107a37b 100644 --- a/pkg/controller/standard.oam.dev/v1alpha1/rollout/rollout_controller.go +++ b/pkg/controller/standard.oam.dev/v1alpha1/rollout/rollout_controller.go @@ -19,6 +19,9 @@ package rollout import ( "context" "encoding/json" + "math" + + "k8s.io/apimachinery/pkg/util/intstr" "github.com/pkg/errors" @@ -115,6 +118,7 @@ func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if rollout.Status.RollingState == v1alpha1.LocatingTargetAppState { if rollout.GetAnnotations() == nil || rollout.GetAnnotations()[oam.AnnotationWorkloadName] != h.targetWorkload.GetName() { + // this is a update operation, the target workload will change so modify annotation gvk := map[string]string{"apiVersion": h.targetWorkload.GetAPIVersion(), "kind": h.targetWorkload.GetKind()} gvkValue, _ := json.Marshal(gvk) rollout.SetAnnotations(oamutil.MergeMapOverrideWithDst(rollout.GetAnnotations(), @@ -125,6 +129,18 @@ func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu // next round reconcile will create workload and pass `LocatingTargetAppState` phase return ctrl.Result{}, h.Update(ctx, rollout) } + + // this is a scale operation, if user don't fill rolloutBatches, fill it with default value + if len(h.sourceRevName) == 0 && len(rollout.Spec.RolloutPlan.RolloutBatches) == 0 { + // logic reach here means cannot get an error, so ignore it + replicas, _ := getWorkloadReplicasNum(*h.targetWorkload) + rollout.Spec.RolloutPlan.RolloutBatches = []v1alpha1.RolloutBatch{{ + Replicas: intstr.FromInt(int(math.Abs(float64(*rollout.Spec.RolloutPlan.TargetSize - replicas))))}, + } + klog.InfoS("rollout controller set default rollout batches ", h.rollout.GetName(), + " namespace: ", rollout.Namespace, "targetSize", rollout.Spec.RolloutPlan.TargetSize) + return ctrl.Result{}, h.Update(ctx, rollout) + } } switch rollout.Status.RollingState { diff --git a/pkg/controller/utils/capability.go b/pkg/controller/utils/capability.go index 192a066ac..11b89d208 100644 --- a/pkg/controller/utils/capability.go +++ b/pkg/controller/utils/capability.go @@ -61,6 +61,10 @@ const ( TerraformTupleTypePrefix string = "tuple(" TerraformMapTypePrefix string = "map(" TerraformObjectTypePrefix string = "object(" + + typeTraitDefinition = "trait" + typeComponentDefinition = "component" + typeWorkflowStepDefinition = "workflowstep" ) // ErrNoSectionParameterInCue means there is not parameter section in Cue template of a workload @@ -245,7 +249,7 @@ func (def *CapabilityComponentDefinition) StoreOpenAPISchema(ctx context.Context Controller: pointer.BoolPtr(true), BlockOwnerDeletion: pointer.BoolPtr(true), }} - cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, componentDefinition.Name, jsonSchema, ownerReference) + cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, componentDefinition.Name, typeComponentDefinition, jsonSchema, ownerReference) if err != nil { return cmName, err } @@ -263,7 +267,7 @@ func (def *CapabilityComponentDefinition) StoreOpenAPISchema(ctx context.Context Controller: pointer.BoolPtr(true), BlockOwnerDeletion: pointer.BoolPtr(true), }} - _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, jsonSchema, ownerReference) + _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, typeComponentDefinition, jsonSchema, ownerReference) if err != nil { return cmName, err } @@ -326,7 +330,7 @@ func (def *CapabilityTraitDefinition) StoreOpenAPISchema(ctx context.Context, k8 Controller: pointer.BoolPtr(true), BlockOwnerDeletion: pointer.BoolPtr(true), }} - cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, traitDefinition.Name, jsonSchema, ownerReference) + cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, traitDefinition.Name, typeTraitDefinition, jsonSchema, ownerReference) if err != nil { return cmName, err } @@ -344,7 +348,76 @@ func (def *CapabilityTraitDefinition) StoreOpenAPISchema(ctx context.Context, k8 Controller: pointer.BoolPtr(true), BlockOwnerDeletion: pointer.BoolPtr(true), }} - _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, jsonSchema, ownerReference) + _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, typeTraitDefinition, jsonSchema, ownerReference) + if err != nil { + return cmName, err + } + return cmName, nil +} + +// CapabilityStepDefinition is the Capability struct for WorkflowStepDefinition +type CapabilityStepDefinition struct { + Name string `json:"name"` + StepDefinition v1beta1.WorkflowStepDefinition `json:"stepDefinition"` + + CapabilityBaseDefinition +} + +// NewCapabilityStepDef will create a CapabilityStepDefinition +func NewCapabilityStepDef(stepdefinition *v1beta1.WorkflowStepDefinition) CapabilityStepDefinition { + var def CapabilityStepDefinition + def.Name = stepdefinition.Name + def.StepDefinition = *stepdefinition.DeepCopy() + return def +} + +// GetOpenAPISchema gets OpenAPI v3 schema by StepDefinition name +func (def *CapabilityStepDefinition) GetOpenAPISchema(pd *packages.PackageDiscover, name string) ([]byte, error) { + capability, err := appfile.ConvertTemplateJSON2Object(name, nil, def.StepDefinition.Spec.Schematic) + if err != nil { + return nil, fmt.Errorf("failed to convert WorkflowStepDefinition to Capability Object") + } + return getOpenAPISchema(capability, pd) +} + +// StoreOpenAPISchema stores OpenAPI v3 schema from StepDefinition in ConfigMap +func (def *CapabilityStepDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, pd *packages.PackageDiscover, namespace, name string, revName string) (string, error) { + var jsonSchema []byte + var err error + + jsonSchema, err = def.GetOpenAPISchema(pd, name) + if err != nil { + return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err) + } + + stepDefinition := def.StepDefinition + ownerReference := []metav1.OwnerReference{{ + APIVersion: stepDefinition.APIVersion, + Kind: stepDefinition.Kind, + Name: stepDefinition.Name, + UID: stepDefinition.GetUID(), + Controller: pointer.BoolPtr(true), + BlockOwnerDeletion: pointer.BoolPtr(true), + }} + cmName, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, stepDefinition.Name, typeWorkflowStepDefinition, jsonSchema, ownerReference) + if err != nil { + return cmName, err + } + + // Create a configmap to store parameter for each definitionRevision + defRev := new(v1beta1.DefinitionRevision) + if err = k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: revName}, defRev); err != nil { + return "", err + } + ownerReference = []metav1.OwnerReference{{ + APIVersion: defRev.APIVersion, + Kind: defRev.Kind, + Name: defRev.Name, + UID: defRev.GetUID(), + Controller: pointer.BoolPtr(true), + BlockOwnerDeletion: pointer.BoolPtr(true), + }} + _, err = def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, revName, typeWorkflowStepDefinition, jsonSchema, ownerReference) if err != nil { return cmName, err } @@ -357,8 +430,8 @@ type CapabilityBaseDefinition struct { // CreateOrUpdateConfigMap creates ConfigMap to store OpenAPI v3 schema or or updates data in ConfigMap func (def *CapabilityBaseDefinition) CreateOrUpdateConfigMap(ctx context.Context, k8sClient client.Client, namespace, - definitionName string, jsonSchema []byte, ownerReferences []metav1.OwnerReference) (string, error) { - cmName := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, definitionName) + definitionName, definitionType string, jsonSchema []byte, ownerReferences []metav1.OwnerReference) (string, error) { + cmName := fmt.Sprintf("%s-%s%s", definitionType, types.CapabilityConfigMapNamePrefix, definitionName) var cm v1.ConfigMap var data = map[string]string{ types.OpenapiV3JSONSchema: string(jsonSchema), @@ -399,7 +472,7 @@ func (def *CapabilityBaseDefinition) CreateOrUpdateConfigMap(ctx context.Context return cmName, nil } -// getDefinition is the main function for GetDefinition API +// getOpenAPISchema is the main function for GetDefinition API func getOpenAPISchema(capability types.Capability, pd *packages.PackageDiscover) ([]byte, error) { openAPISchema, err := generateOpenAPISchemaFromCapabilityParameter(capability, pd) if err != nil { diff --git a/pkg/controller/utils/capability_integrate_test.go b/pkg/controller/utils/capability_integrate_test.go index 65ee92dc4..890480f11 100644 --- a/pkg/controller/utils/capability_integrate_test.go +++ b/pkg/controller/utils/capability_integrate_test.go @@ -181,7 +181,7 @@ spec: Controller: pointer.BoolPtr(true), BlockOwnerDeletion: pointer.BoolPtr(true), }} - _, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, definitionName, []byte(""), ownerReference) + _, err := def.CreateOrUpdateConfigMap(ctx, k8sClient, namespace, definitionName, typeTraitDefinition, []byte(""), ownerReference) Expect(err).Should(BeNil()) }) }) diff --git a/pkg/cue/definition/template.go b/pkg/cue/definition/template.go index a18544783..a7b3e3a5a 100644 --- a/pkg/cue/definition/template.go +++ b/pkg/cue/definition/template.go @@ -231,10 +231,11 @@ func (wd *workloadDef) Status(ctx process.Context, cli client.Client, ns string, if err != nil { return "", errors.WithMessage(err, "get template context") } - return getStatusMessage(templateContext, customStatusTemplate, parameter) + return getStatusMessage(wd.pd, templateContext, customStatusTemplate, parameter) } -func getStatusMessage(templateContext map[string]interface{}, customStatusTemplate string, parameter interface{}) (string, error) { +func getStatusMessage(pd *packages.PackageDiscover, templateContext map[string]interface{}, customStatusTemplate string, parameter interface{}) (string, error) { + bi := build.NewContext().NewInstance("", nil) var ctxBuff string var paramBuff = "parameter: {}\n" @@ -251,10 +252,12 @@ func getStatusMessage(templateContext map[string]interface{}, customStatusTempla if string(bt) != "null" { paramBuff = "parameter: " + string(bt) + "\n" } - var buff = ctxBuff + paramBuff + customStatusTemplate + var buff = customStatusTemplate + "\n" + ctxBuff + paramBuff + if err := bi.AddFile("-", buff); err != nil { + return "", errors.WithMessagef(err, "invalid cue template of customStatus") + } - var r cue.Runtime - inst, err := r.Compile("-", buff) + inst, err := pd.ImportPackagesAndBuildInstance(bi) if err != nil { return "", errors.WithMessage(err, "compile customStatus template") } @@ -426,7 +429,7 @@ func (td *traitDef) Status(ctx process.Context, cli client.Client, ns string, cu if err != nil { return "", errors.WithMessage(err, "get template context") } - return getStatusMessage(templateContext, customStatusTemplate, parameter) + return getStatusMessage(td.pd, templateContext, customStatusTemplate, parameter) } // HealthCheck address health check for trait diff --git a/pkg/cue/definition/template_test.go b/pkg/cue/definition/template_test.go index ac640e5fd..f1387599c 100644 --- a/pkg/cue/definition/template_test.go +++ b/pkg/cue/definition/template_test.go @@ -1245,9 +1245,36 @@ if len(context.outputs.ingress.status.loadBalancer.ingress) == 0 { statusTemp: `message: parameter.configInfo.name + ".type: " + context.outputs["\(parameter.configInfo.name)"].spec.type`, expMessage: "test-name.type: NodePort", }, + "import package in template": { + tpContext: map[string]interface{}{ + "outputs": map[string]interface{}{ + "service": map[string]interface{}{ + "spec": map[string]interface{}{ + "type": "NodePort", + "clusterIP": "10.0.0.1", + "ports": []interface{}{ + map[string]interface{}{ + "port": 80, + }, + }, + }, + }, + "ingress": map[string]interface{}{ + "rules": []interface{}{ + map[string]interface{}{ + "host": "example.com", + }, + }, + }, + }, + }, + statusTemp: `import "strconv" + message: "ports: " + strconv.FormatInt(context.outputs.service.spec.ports[0].port,10)`, + expMessage: "ports: 80", + }, } for message, ca := range cases { - gotMessage, err := getStatusMessage(ca.tpContext, ca.statusTemp, ca.parameter) + gotMessage, err := getStatusMessage(&packages.PackageDiscover{}, ca.tpContext, ca.statusTemp, ca.parameter) assert.NoError(t, err, message) assert.Equal(t, ca.expMessage, gotMessage, message) } diff --git a/pkg/cue/packages/package.go b/pkg/cue/packages/package.go index 93b281617..bddc1dd04 100644 --- a/pkg/cue/packages/package.go +++ b/pkg/cue/packages/package.go @@ -35,6 +35,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + + "github.com/oam-dev/kubevela/pkg/stdlib" ) const ( @@ -107,6 +109,9 @@ func (pd *PackageDiscover) ImportBuiltinPackagesFor(bi *build.Instance) { // ImportPackagesAndBuildInstance Combine import built-in packages and build cue template together to avoid data race func (pd *PackageDiscover) ImportPackagesAndBuildInstance(bi *build.Instance) (inst *cue.Instance, err error) { pd.ImportBuiltinPackagesFor(bi) + if err := stdlib.AddImportsFor(bi, ""); err != nil { + return nil, err + } var r cue.Runtime pd.mutex.Lock() defer pd.mutex.Unlock() diff --git a/pkg/cue/process/handle.go b/pkg/cue/process/handle.go index d80846bce..e1305f77e 100644 --- a/pkg/cue/process/handle.go +++ b/pkg/cue/process/handle.go @@ -41,6 +41,7 @@ type Context interface { SetParameters(params map[string]interface{}) PushData(key string, data interface{}) GetCtx() context.Context + SetCtx(context.Context) } // Auxiliary are objects rendered by definition template. @@ -197,7 +198,7 @@ func (ctx *templateContext) BaseContextFile() string { if len(ctx.auxiliaries) > 0 { var auxLines []string for _, auxiliary := range ctx.auxiliaries { - auxLines = append(auxLines, fmt.Sprintf("%s: %s", auxiliary.Name, structMarshal(auxiliary.Ins.String()))) + auxLines = append(auxLines, fmt.Sprintf("\"%s\": %s", auxiliary.Name, structMarshal(auxiliary.Ins.String()))) } if len(auxLines) > 0 { buff += fmt.Sprintf(model.OutputsFieldName+": {%s}\n", strings.Join(auxLines, "\n")) @@ -292,6 +293,13 @@ func (ctx *templateContext) GetCtx() context.Context { return context.TODO() } +func (ctx *templateContext) SetCtx(newContext context.Context) { + if ctx.ctx != nil { + return + } + ctx.ctx = newContext +} + func structMarshal(v string) string { skip := false v = strings.TrimFunc(v, func(r rune) bool { diff --git a/pkg/cue/process/handle_test.go b/pkg/cue/process/handle_test.go index bd57ee909..63bf8e797 100644 --- a/pkg/cue/process/handle_test.go +++ b/pkg/cue/process/handle_test.go @@ -64,6 +64,11 @@ image: "myserver" Name: "service", } + svcAuxWithAbnormalName := Auxiliary{ + Ins: svcIns, + Name: "service-1", + } + targetParams := map[string]interface{}{ "parameter1": "string", "parameter2": map[string]string{ @@ -98,6 +103,7 @@ image: "myserver" ctx := NewContext("myns", "mycomp", "myapp", "myapp-v1") ctx.SetBase(base) ctx.AppendAuxiliaries(svcAux) + ctx.AppendAuxiliaries(svcAuxWithAbnormalName) ctx.SetParameters(targetParams) ctx.PushData(model.ContextDataArtifacts, targetData) ctx.PushData("arbitraryData", targetArbitraryData) @@ -132,6 +138,10 @@ image: "myserver" assert.Equal(t, nil, err) assert.Equal(t, "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\"}", string(outputsJs)) + outputsJs, err = ctxInst.Lookup("context", model.OutputsFieldName, "service-1").MarshalJSON() + assert.Equal(t, nil, err) + assert.Equal(t, "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\"}", string(outputsJs)) + ns, err := ctxInst.Lookup("context", model.ContextNamespace).String() assert.Equal(t, nil, err) assert.Equal(t, "myns", ns) diff --git a/pkg/multicluster/cluster_management.go b/pkg/multicluster/cluster_management.go index 8a45de977..69d4c91f6 100644 --- a/pkg/multicluster/cluster_management.go +++ b/pkg/multicluster/cluster_management.go @@ -33,9 +33,8 @@ import ( "k8s.io/client-go/tools/clientcmd/api" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/policy/envbinding" errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" ) @@ -76,8 +75,8 @@ func ensureClusterNotExists(ctx context.Context, c client.Client, clusterName st return nil } -// getMutableClusterSecret retrieves the cluster secret and check if any application is using the cluster -func getMutableClusterSecret(ctx context.Context, c client.Client, clusterName string) (*v1.Secret, error) { +// GetMutableClusterSecret retrieves the cluster secret and check if any application is using the cluster +func GetMutableClusterSecret(ctx context.Context, c client.Client, clusterName string) (*v1.Secret, error) { clusterSecret := &v1.Secret{} if err := c.Get(ctx, types2.NamespacedName{Namespace: ClusterGatewaySecretNamespace, Name: clusterName}, clusterSecret); err != nil { return nil, errors.Wrapf(err, "failed to find target cluster secret %s", clusterName) @@ -86,15 +85,20 @@ func getMutableClusterSecret(ctx context.Context, c client.Client, clusterName s if labels == nil || labels[v1alpha12.LabelKeyClusterCredentialType] == "" { return nil, fmt.Errorf("invalid cluster secret %s: cluster credential type label %s is not set", clusterName, v1alpha12.LabelKeyClusterCredentialType) } - ebs := &v1alpha1.EnvBindingList{} - if err := c.List(ctx, ebs); err != nil { - return nil, errors.Wrap(err, "failed to find EnvBindings to check clusters") + apps := &v1beta1.ApplicationList{} + if err := c.List(ctx, apps); err != nil { + return nil, errors.Wrap(err, "failed to find applications to check clusters") } errs := errors3.ErrorList{} - for _, eb := range ebs.Items { - for _, decision := range eb.Status.ClusterDecisions { - if decision.Cluster == clusterName { - errs.Append(fmt.Errorf("application %s/%s (env: %s, envBinding: %s) is currently using cluster %s", eb.Namespace, eb.Labels[oam.LabelAppName], decision.Env, eb.Name, clusterName)) + for _, app := range apps.Items { + status, err := envbinding.GetEnvBindingPolicyStatus(app.DeepCopy(), "") + if err == nil && status != nil { + for _, env := range status.Envs { + for _, placement := range env.Placements { + if placement.Cluster == clusterName { + errs.Append(fmt.Errorf("application %s/%s (env: %s) is currently using cluster %s", app.Namespace, app.Name, env.Env, clusterName)) + } + } } } } @@ -177,7 +181,7 @@ func DetachCluster(ctx context.Context, k8sClient client.Client, clusterName str if clusterName == ClusterLocalName { return ErrReservedLocalClusterName } - clusterSecret, err := getMutableClusterSecret(ctx, k8sClient, clusterName) + clusterSecret, err := GetMutableClusterSecret(ctx, k8sClient, clusterName) if err != nil { return errors.Wrapf(err, "cluster %s is not mutable now", clusterName) } @@ -189,7 +193,7 @@ func RenameCluster(ctx context.Context, k8sClient client.Client, oldClusterName if newClusterName == ClusterLocalName { return ErrReservedLocalClusterName } - clusterSecret, err := getMutableClusterSecret(ctx, k8sClient, oldClusterName) + clusterSecret, err := GetMutableClusterSecret(ctx, k8sClient, oldClusterName) if err != nil { return errors.Wrapf(err, "cluster %s is not mutable now", oldClusterName) } diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/gc.go b/pkg/multicluster/gc.go similarity index 55% rename from pkg/controller/core.oam.dev/v1alpha1/envbinding/gc.go rename to pkg/multicluster/gc.go index 8194b3f25..5853ca15d 100644 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/gc.go +++ b/pkg/multicluster/gc.go @@ -14,52 +14,49 @@ limitations under the License. */ -package envbinding +package multicluster import ( "context" "github.com/pkg/errors" kerrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" - "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/policy/envbinding" errors2 "github.com/oam-dev/kubevela/pkg/utils/errors" ) -func isEnvBindingPolicy(policy *unstructured.Unstructured) bool { - policyKindAPIVersion := policy.GetKind() + "." + policy.GetAPIVersion() - return policyKindAPIVersion == v1alpha1.EnvBindingKindAPIVersion +func getAppliedClusters(app *v1beta1.Application) []string { + status, err := envbinding.GetEnvBindingPolicyStatus(app, "") + appliedClusters := map[string]bool{} + if err != nil { + klog.InfoS("failed to get envbinding policy status during gc", "err", err.Error()) + // fallback + for _, v := range app.Status.AppliedResources { + appliedClusters[v.Cluster] = true + } + } + if status != nil { + for _, conn := range status.ClusterConnections { + appliedClusters[conn.ClusterName] = true + } + } + var clusters []string + for cluster := range appliedClusters { + clusters = append(clusters, cluster) + } + return clusters } // GarbageCollectionForOutdatedResourcesInSubClusters run garbage collection in sub clusters and remove outdated ResourceTrackers with their associated resources -func GarbageCollectionForOutdatedResourcesInSubClusters(ctx context.Context, c client.Client, policies []*unstructured.Unstructured, gcHandler func(context.Context) error) error { - subClusters := make(map[string]bool) - for _, raw := range policies { - if !isEnvBindingPolicy(raw) { - continue - } - policy := &v1alpha1.EnvBinding{} - if err := c.Get(ctx, types.NamespacedName{Namespace: raw.GetNamespace(), Name: raw.GetName()}, policy); err != nil { - klog.Infof("failed to run gc for envBinding subClusters: %v", err) - } - if policy.Status.ClusterDecisions == nil { - continue - } - for _, decision := range policy.Status.ClusterDecisions { - subClusters[decision.Cluster] = true - } - } +func GarbageCollectionForOutdatedResourcesInSubClusters(ctx context.Context, app *v1beta1.Application, gcHandler func(context.Context) error) error { var errs errors2.ErrorList - for clusterName := range subClusters { - if err := gcHandler(multicluster.ContextWithClusterName(ctx, clusterName)); err != nil { + for _, clusterName := range getAppliedClusters(app) { + if err := gcHandler(ContextWithClusterName(ctx, clusterName)); err != nil { if !errors.As(err, &errors2.ResourceTrackerNotExistError{}) { errs.Append(errors.Wrapf(err, "failed to run gc in subCluster %s", clusterName)) } @@ -72,23 +69,18 @@ func GarbageCollectionForOutdatedResourcesInSubClusters(ctx context.Context, c c } // GarbageCollectionForAllResourceTrackersInSubCluster run garbage collection in sub clusters and remove all ResourceTrackers for the EnvBinding -func GarbageCollectionForAllResourceTrackersInSubCluster(ctx context.Context, c client.Client, envBinding *v1alpha1.EnvBinding) error { - baseApp, err := util.RawExtension2Application(envBinding.Spec.AppTemplate.RawExtension) - if err != nil { - klog.ErrorS(err, "failed to parse AppTemplate of EnvBinding") - return errors.WithMessage(err, "cannot remove finalizer") - } +func GarbageCollectionForAllResourceTrackersInSubCluster(ctx context.Context, c client.Client, app *v1beta1.Application) error { // delete subCluster resourceTracker - for _, decision := range envBinding.Status.ClusterDecisions { - subCtx := multicluster.ContextWithClusterName(ctx, decision.Cluster) + for _, cluster := range getAppliedClusters(app) { + subCtx := ContextWithClusterName(ctx, cluster) listOpts := []client.ListOption{ client.MatchingLabels{ - oam.LabelAppName: baseApp.Name, - oam.LabelAppNamespace: baseApp.Namespace, + oam.LabelAppName: app.Name, + oam.LabelAppNamespace: app.Namespace, }} rtList := &v1beta1.ResourceTrackerList{} if err := c.List(subCtx, rtList, listOpts...); err != nil { - klog.ErrorS(err, "failed to list resource tracker of app", "name", baseApp.Name, "env", decision.Env) + klog.ErrorS(err, "failed to list resource tracker of app", "name", app.Name, "cluster", cluster) return errors.WithMessage(err, "cannot remove finalizer") } for _, rt := range rtList.Items { diff --git a/pkg/multicluster/gc_test.go b/pkg/multicluster/gc_test.go new file mode 100644 index 000000000..f47097c00 --- /dev/null +++ b/pkg/multicluster/gc_test.go @@ -0,0 +1,61 @@ +/* +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 multicluster + +import ( + "encoding/json" + "sort" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" +) + +func TestGetAppliedCluster(t *testing.T) { + r := require.New(t) + app := &v1beta1.Application{} + app.Status.AppliedResources = []common.ClusterObjectReference{{ + Cluster: "cluster-0", + }} + app.Status.PolicyStatus = []common.PolicyStatus{{ + Type: v1alpha1.EnvBindingPolicyType, + Status: &runtime.RawExtension{Raw: []byte(`bad value`)}, + }} + clusters := getAppliedClusters(app) + r.Equal(1, len(clusters)) + r.Equal("cluster-0", clusters[0]) + envBindingStatus := &v1alpha1.EnvBindingStatus{ClusterConnections: []v1alpha1.ClusterConnection{{ + ClusterName: "cluster-1", + }, { + ClusterName: "cluster-2", + }}} + bs, err := json.Marshal(envBindingStatus) + r.NoError(err) + app.Status.PolicyStatus = []common.PolicyStatus{{ + Type: v1alpha1.EnvBindingPolicyType, + Status: &runtime.RawExtension{Raw: bs}, + }} + clusters = getAppliedClusters(app) + r.Equal(2, len(clusters)) + sort.Strings(clusters) + r.Equal("cluster-1", clusters[0]) + r.Equal("cluster-2", clusters[1]) +} diff --git a/pkg/oam/util/helper.go b/pkg/oam/util/helper.go index 226578ec0..8dea9e04d 100644 --- a/pkg/oam/util/helper.go +++ b/pkg/oam/util/helper.go @@ -100,6 +100,8 @@ const ( ErrUpdateComponentDefinition = "cannot update ComponentDefinition %s: %v" // ErrUpdateTraitDefinition is the error while update TraitDefinition ErrUpdateTraitDefinition = "cannot update TraitDefinition %s: %v" + // ErrUpdateStepDefinition is the error while update WorkflowStepDefinition + ErrUpdateStepDefinition = "cannot update WorkflowStepDefinition %s: %v" // ErrUpdatePolicyDefinition is the error while update PolicyDefinition ErrUpdatePolicyDefinition = "cannot update PolicyDefinition %s: %v" // ErrUpdateWorkflowStepDefinition is the error while update WorkflowStepDefinition diff --git a/pkg/plugin/cli/cli.go b/pkg/plugin/cli/cli.go index 08d139d18..b8cb69285 100644 --- a/pkg/plugin/cli/cli.go +++ b/pkg/plugin/cli/cli.go @@ -60,8 +60,9 @@ func NewCommand() *cobra.Command { NewDryRunCommand(commandArgs, ioStream), NewLiveDiffCommand(commandArgs, ioStream), NewCapabilityShowCommand(commandArgs, ioStream), - NewCompCommand(commandArgs, ioStream), - NewTraitCommand(commandArgs, ioStream), + cli.NewComponentsCommand(commandArgs, ioStream), + cli.NewTraitCommand(commandArgs, ioStream), + cli.NewRegistryCommand(ioStream), NewVersionCommand(), NewHelpCommand(), ) diff --git a/pkg/plugin/cli/comp.go b/pkg/plugin/cli/comp.go deleted file mode 100644 index 6e684e125..000000000 --- a/pkg/plugin/cli/comp.go +++ /dev/null @@ -1,75 +0,0 @@ -/* - 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 cli - -import ( - "github.com/spf13/cobra" - - "github.com/oam-dev/kubevela/apis/types" - common2 "github.com/oam-dev/kubevela/pkg/utils/common" - cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" - "github.com/oam-dev/kubevela/references/cli" -) - -// NewCompCommand creates `comp` command -func NewCompCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "comp", - DisableFlagsInUseLine: true, - Short: "Show components in capability registry", - Long: "Show components in capability registry", - Example: "kubectl vela comp", - RunE: func(cmd *cobra.Command, args []string) error { - isDiscover, _ := cmd.Flags().GetBool("discover") - url, _ := cmd.PersistentFlags().GetString("url") - err := cli.PrintComponentListFromRegistry(isDiscover, url, ioStreams) - return err - }, - Annotations: map[string]string{ - types.TagCommandType: types.TypePlugin, - }, - } - cmd.SetOut(ioStreams.Out) - cmd.AddCommand( - NewCompGetCommand(c, ioStreams), - ) - cmd.Flags().Bool("discover", false, "discover traits in registries") - cmd.PersistentFlags().String("url", cli.DefaultRegistry, "specify the registry URL") - return cmd -} - -// NewCompGetCommand creates `comp get` command -func NewCompGetCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "get ", - Short: "get component from registry", - Long: "get component from registry", - Example: "kubectl vela comp get ", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - ioStreams.Error("you must specify a" + - " component name") - return nil - } - name := args[0] - url, _ := cmd.Flags().GetString("url") - - return cli.InstallCompByName(c, ioStreams, name, url) - }, - } - return cmd -} diff --git a/pkg/plugin/cli/trait.go b/pkg/plugin/cli/trait.go deleted file mode 100644 index 995233d16..000000000 --- a/pkg/plugin/cli/trait.go +++ /dev/null @@ -1,74 +0,0 @@ -/* - 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 cli - -import ( - "github.com/spf13/cobra" - - "github.com/oam-dev/kubevela/apis/types" - common2 "github.com/oam-dev/kubevela/pkg/utils/common" - cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" - "github.com/oam-dev/kubevela/references/cli" -) - -// NewTraitCommand creates `trait` command -func NewTraitCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "trait", - DisableFlagsInUseLine: true, - Short: "Show traits in capability registry", - Long: "Show traits in capability registry", - Example: "kubectl vela trait", - RunE: func(cmd *cobra.Command, args []string) error { - isDiscover, _ := cmd.Flags().GetBool("discover") - url, _ := cmd.PersistentFlags().GetString("url") - err := cli.PrintTraitListFromRegistry(isDiscover, url, ioStreams) - return err - }, - Annotations: map[string]string{ - types.TagCommandType: types.TypePlugin, - }, - } - cmd.SetOut(ioStreams.Out) - cmd.AddCommand( - NewTraitGetCommand(c, ioStreams), - ) - cmd.Flags().Bool("discover", false, "discover traits in registries") - cmd.PersistentFlags().String("url", cli.DefaultRegistry, "specify the registry URL") - return cmd -} - -// NewTraitGetCommand creates `trait get` command -func NewTraitGetCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "get ", - Short: "get trait from registry", - Long: "get trait from registry", - Example: "kubectl vela trait get ", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - ioStreams.Error("you must specify the trait name") - return nil - } - name := args[0] - url, _ := cmd.Flags().GetString("url") - - return cli.InstallTraitByName(c, ioStreams, name, url) - }, - } - return cmd -} diff --git a/pkg/policy/envbinding/patch.go b/pkg/policy/envbinding/patch.go new file mode 100644 index 000000000..b5e495799 --- /dev/null +++ b/pkg/policy/envbinding/patch.go @@ -0,0 +1,187 @@ +/* +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 envbinding + +import ( + "encoding/json" + "fmt" + + "github.com/imdario/mergo" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/oam/util" + errors2 "github.com/oam-dev/kubevela/pkg/utils/errors" +) + +// MergeRawExtension merge two raw extension +func MergeRawExtension(base *runtime.RawExtension, patch *runtime.RawExtension) (*runtime.RawExtension, error) { + patchParameter, err := util.RawExtension2Map(patch) + if err != nil { + return nil, errors.Wrapf(err, "failed to convert patch parameters to map") + } + baseParameter, err := util.RawExtension2Map(base) + if err != nil { + return nil, errors.Wrapf(err, "failed to convert base parameters to map") + } + if baseParameter == nil { + baseParameter = make(map[string]interface{}) + } + err = mergo.Merge(&baseParameter, patchParameter, mergo.WithOverride) + if err != nil { + return nil, errors.Wrapf(err, "failed to do merge with override") + } + bs, err := json.Marshal(baseParameter) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal merged properties") + } + return &runtime.RawExtension{Raw: bs}, nil +} + +// MergeComponent merge two component, it will first merge their properties and then merge their traits +func MergeComponent(base *common.ApplicationComponent, patch *v1alpha1.EnvComponentPatch) (*common.ApplicationComponent, error) { + newComponent := base.DeepCopy() + var err error + + // merge component properties + newComponent.Properties, err = MergeRawExtension(base.Properties, patch.Properties) + if err != nil { + return nil, errors.Wrapf(err, "failed to merge component properties") + } + + // prepare traits + traitMaps := map[string]*common.ApplicationTrait{} + var traitOrders []string + for _, trait := range base.Traits { + traitMaps[trait.Type] = trait.DeepCopy() + traitOrders = append(traitOrders, trait.Type) + } + + // patch traits + var errs errors2.ErrorList + for _, trait := range patch.Traits { + if baseTrait, exists := traitMaps[trait.Type]; exists { + if trait.Disable { + delete(traitMaps, trait.Type) + continue + } + baseTrait.Properties, err = MergeRawExtension(baseTrait.Properties, trait.Properties) + if err != nil { + errs.Append(errors.Wrapf(err, "failed to merge trait %s", trait.Type)) + } + } else { + if trait.Disable { + continue + } + traitMaps[trait.Type] = trait.ToApplicationTrait() + traitOrders = append(traitOrders, trait.Type) + } + } + if errs.HasError() { + return nil, errors.Wrapf(err, "failed to merge component traits") + } + + // fill in traits + newComponent.Traits = []common.ApplicationTrait{} + for _, traitType := range traitOrders { + if _, exists := traitMaps[traitType]; exists { + newComponent.Traits = append(newComponent.Traits, *traitMaps[traitType]) + } + } + return newComponent, nil +} + +func filterComponents(components []string, selector *v1alpha1.EnvSelector) []string { + if selector != nil { + filter := map[string]bool{} + for _, compName := range selector.Components { + filter[compName] = true + } + var _comps []string + for _, compName := range components { + if _, ok := filter[compName]; ok { + _comps = append(_comps, compName) + } + } + return _comps + } + return components +} + +// PatchApplication patch base application with patch and selector +func PatchApplication(base *v1beta1.Application, patch *v1alpha1.EnvPatch, selector *v1alpha1.EnvSelector) (*v1beta1.Application, error) { + newApp := base.DeepCopy() + + // init components + compMaps := map[string]*common.ApplicationComponent{} + var compOrders []string + for _, comp := range base.Spec.Components { + compMaps[comp.Name] = comp.DeepCopy() + compOrders = append(compOrders, comp.Name) + } + + // patch components + var errs errors2.ErrorList + var err error + for _, comp := range patch.Components { + if baseComp, exists := compMaps[comp.Name]; exists { + if baseComp.Type != comp.Type { + compMaps[comp.Name] = comp.ToApplicationComponent() + } else { + compMaps[comp.Name], err = MergeComponent(baseComp, comp.DeepCopy()) + if err != nil { + errs.Append(errors.Wrapf(err, "failed to merge component %s", comp.Name)) + } + } + } else { + compMaps[comp.Name] = comp.ToApplicationComponent() + compOrders = append(compOrders, comp.Name) + } + } + if errs.HasError() { + return nil, errors.Wrapf(err, "failed to merge application components") + } + newApp.Spec.Components = []common.ApplicationComponent{} + + // if selector is enabled, filter + compOrders = filterComponents(compOrders, selector) + + // fill in new application + for _, compName := range compOrders { + newApp.Spec.Components = append(newApp.Spec.Components, *compMaps[compName]) + } + return newApp, nil +} + +// PatchApplicationByEnvBindingEnv get patched application directly through policyName and envName +func PatchApplicationByEnvBindingEnv(app *v1beta1.Application, policyName string, envName string) (*v1beta1.Application, error) { + policy, err := GetEnvBindingPolicy(app, policyName) + if err != nil { + return nil, err + } + if policy != nil { + for _, env := range policy.Envs { + if env.Name == envName { + return PatchApplication(app, &env.Patch, env.Selector) + } + } + } + return nil, fmt.Errorf("target env %s in policy %s not found", envName, policyName) +} diff --git a/pkg/controller/core.oam.dev/v1alpha1/envbinding/binding_test.go b/pkg/policy/envbinding/patch_test.go similarity index 56% rename from pkg/controller/core.oam.dev/v1alpha1/envbinding/binding_test.go rename to pkg/policy/envbinding/patch_test.go index 5bbde8740..a34979d27 100644 --- a/pkg/controller/core.oam.dev/v1alpha1/envbinding/binding_test.go +++ b/pkg/policy/envbinding/patch_test.go @@ -31,27 +31,27 @@ import ( func Test_EnvBindApp_GenerateConfiguredApplication(t *testing.T) { testcases := []struct { baseApp *v1beta1.Application - envConfig *v1alpha1.EnvConfig + envName string + envPatch v1alpha1.EnvPatch expectedApp *v1beta1.Application + selector *v1alpha1.EnvSelector }{{ baseApp: baseApp, - envConfig: &v1alpha1.EnvConfig{ - Name: "prod", - Patch: v1alpha1.EnvPatch{ - Components: []common.ApplicationComponent{{ - Name: "express-server", - Type: "webservice", + envName: "prod", + envPatch: v1alpha1.EnvPatch{ + Components: []v1alpha1.EnvComponentPatch{{ + Name: "express-server", + Type: "webservice", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + }), + Traits: []v1alpha1.EnvTraitPatch{{ + Type: "ingress-1-20", Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "busybox", + "domain": "newTestsvc.example.com", }), - Traits: []common.ApplicationTrait{{ - Type: "ingress-1-20", - Properties: util.Object2RawExtension(map[string]interface{}{ - "domain": "newTestsvc.example.com", - }), - }}, }}, - }, + }}, }, expectedApp: &v1beta1.Application{ TypeMeta: metav1.TypeMeta{ @@ -83,33 +83,31 @@ func Test_EnvBindApp_GenerateConfiguredApplication(t *testing.T) { }, }, { baseApp: baseApp, - envConfig: &v1alpha1.EnvConfig{ - Name: "prod", - Patch: v1alpha1.EnvPatch{ - Components: []common.ApplicationComponent{{ - Name: "express-server", - Type: "webservice", - Traits: []common.ApplicationTrait{{ - Type: "labels", - Properties: util.Object2RawExtension(map[string]interface{}{ - "test": "label", - }), - }}, - }, { - Name: "new-server", - Type: "worker", + envName: "prod", + envPatch: v1alpha1.EnvPatch{ + Components: []v1alpha1.EnvComponentPatch{{ + Name: "express-server", + Type: "webservice", + Traits: []v1alpha1.EnvTraitPatch{{ + Type: "labels", Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "busybox", - "cmd": []string{"sleep", "1000"}, + "test": "label", }), - Traits: []common.ApplicationTrait{{ - Type: "labels", - Properties: util.Object2RawExtension(map[string]interface{}{ - "test": "label", - }), - }}, }}, - }, + }, { + Name: "new-server", + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "1000"}, + }), + Traits: []v1alpha1.EnvTraitPatch{{ + Type: "labels", + Properties: util.Object2RawExtension(map[string]interface{}{ + "test": "label", + }), + }}, + }}, }, expectedApp: &v1beta1.Application{ TypeMeta: metav1.TypeMeta{ @@ -157,13 +155,108 @@ func Test_EnvBindApp_GenerateConfiguredApplication(t *testing.T) { }}, }, }, + }, { + // Test Disable Trait + baseApp: baseApp, + envName: "prod", + envPatch: v1alpha1.EnvPatch{ + Components: []v1alpha1.EnvComponentPatch{{ + Name: "express-server", + Type: "webservice", + Traits: []v1alpha1.EnvTraitPatch{{ + Type: "ingress-1-20", + Disable: true, + }}, + }}, + }, + expectedApp: &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1beta1", + Kind: "Application", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "express-server", + Type: "webservice", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "crccheck/hello-world", + "port": 8000, + }), + Traits: []common.ApplicationTrait{}, + }}, + }, + }, + }, { + // Test component selector + baseApp: baseApp, + envName: "prod", + envPatch: v1alpha1.EnvPatch{ + Components: []v1alpha1.EnvComponentPatch{{ + Name: "new-server", + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + }), + }}, + }, + selector: &v1alpha1.EnvSelector{ + Components: []string{"new-server"}, + }, + expectedApp: &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1beta1", + Kind: "Application", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "new-server", + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + }), + }}, + }, + }, + }, { + // Test empty component selector + baseApp: baseApp, + envName: "prod", + envPatch: v1alpha1.EnvPatch{ + Components: []v1alpha1.EnvComponentPatch{{ + Name: "new-server", + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + }), + }}, + }, + selector: &v1alpha1.EnvSelector{ + Components: []string{}, + }, + expectedApp: &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1beta1", + Kind: "Application", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{}, + }, + }, }} for _, testcase := range testcases { - envBindApp := NewEnvBindApp(testcase.baseApp, testcase.envConfig) - err := envBindApp.GenerateConfiguredApplication() + app, err := PatchApplication(testcase.baseApp, &testcase.envPatch, testcase.selector) assert.NoError(t, err) - assert.Equal(t, envBindApp.PatchedApp, testcase.expectedApp) + assert.Equal(t, testcase.expectedApp, app) } } diff --git a/pkg/policy/envbinding/placement.go b/pkg/policy/envbinding/placement.go new file mode 100644 index 000000000..bd09c4e2c --- /dev/null +++ b/pkg/policy/envbinding/placement.go @@ -0,0 +1,125 @@ +/* +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 envbinding + +import ( + "encoding/json" + + "k8s.io/apimachinery/pkg/runtime" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" +) + +// ReadPlacementDecisions read placement decisions from application status, return (decisions, if decision is made, error) +func ReadPlacementDecisions(app *v1beta1.Application, policyName string, envName string) ([]v1alpha1.PlacementDecision, bool, error) { + envBindingStatus, err := GetEnvBindingPolicyStatus(app, policyName) + if err != nil || envBindingStatus == nil { + return nil, false, err + } + for _, envStatus := range envBindingStatus.Envs { + if envStatus.Env == envName { + return envStatus.Placements, true, nil + } + } + return nil, false, nil +} + +// updateClusterConnections update cluster connection in envbinding status with decisions +func updateClusterConnections(status *v1alpha1.EnvBindingStatus, decisions []v1alpha1.PlacementDecision, app *v1beta1.Application) { + var currentRev string + if app.Status.LatestRevision != nil { + currentRev = app.Status.LatestRevision.Name + } + clusterMap := map[string]bool{} + for _, decision := range decisions { + clusterMap[decision.Cluster] = true + } + for clusterName := range clusterMap { + exists := false + for idx, conn := range status.ClusterConnections { + if conn.ClusterName == clusterName { + exists = true + status.ClusterConnections[idx].LastActiveRevision = currentRev + break + } + } + if !exists { + status.ClusterConnections = append(status.ClusterConnections, v1alpha1.ClusterConnection{ + ClusterName: clusterName, + LastActiveRevision: currentRev, + }) + } + } +} + +// WritePlacementDecisions write placement decisions into application status +func WritePlacementDecisions(app *v1beta1.Application, policyName string, envName string, decisions []v1alpha1.PlacementDecision) error { + statusExists := false + for idx, policyStatus := range app.Status.PolicyStatus { + if policyStatus.Name == policyName && policyStatus.Type == v1alpha1.EnvBindingPolicyType { + envBindingStatus := &v1alpha1.EnvBindingStatus{} + err := json.Unmarshal(policyStatus.Status.Raw, envBindingStatus) + if err != nil { + return err + } + insert := true + for _idx, envStatus := range envBindingStatus.Envs { + if envStatus.Env == envName { + // TODO gc + envBindingStatus.Envs[_idx].Placements = decisions + insert = false + break + } + } + if insert { + envBindingStatus.Envs = append(envBindingStatus.Envs, v1alpha1.EnvStatus{ + Env: envName, + Placements: decisions, + }) + } + updateClusterConnections(envBindingStatus, decisions, app) + bs, err := json.Marshal(envBindingStatus) + if err != nil { + return err + } + app.Status.PolicyStatus[idx].Status = &runtime.RawExtension{Raw: bs} + statusExists = true + break + } + } + if !statusExists { + envBindingStatus := &v1alpha1.EnvBindingStatus{ + Envs: []v1alpha1.EnvStatus{{ + Env: envName, + Placements: decisions, + }}, + } + updateClusterConnections(envBindingStatus, decisions, app) + bs, err := json.Marshal(envBindingStatus) + if err != nil { + return err + } + app.Status.PolicyStatus = append(app.Status.PolicyStatus, common.PolicyStatus{ + Name: policyName, + Type: v1alpha1.EnvBindingPolicyType, + Status: &runtime.RawExtension{Raw: bs}, + }) + } + return nil +} diff --git a/pkg/policy/envbinding/placement_test.go b/pkg/policy/envbinding/placement_test.go new file mode 100644 index 000000000..dd13d2f3b --- /dev/null +++ b/pkg/policy/envbinding/placement_test.go @@ -0,0 +1,133 @@ +/* +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 envbinding + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" +) + +func TestReadPlacementDecisions(t *testing.T) { + pld := []v1alpha1.PlacementDecision{{ + Cluster: "example-cluster", + Namespace: "example-namespace", + }} + testCases := []struct { + Status *v1alpha1.EnvBindingStatus + StatusRaw []byte + ExpectedExists bool + ExpectedHasError bool + }{{ + Status: nil, + StatusRaw: []byte(`bad value`), + ExpectedExists: false, + ExpectedHasError: true, + }, { + Status: &v1alpha1.EnvBindingStatus{ + Envs: []v1alpha1.EnvStatus{{ + Env: "example-env", + Placements: pld, + }}, + }, + ExpectedExists: true, + ExpectedHasError: false, + }, { + Status: &v1alpha1.EnvBindingStatus{ + Envs: []v1alpha1.EnvStatus{{ + Env: "bad-env", + Placements: pld, + }}, + }, + ExpectedExists: false, + ExpectedHasError: false, + }} + r := require.New(t) + for _, testCase := range testCases { + app := &v1beta1.Application{} + _status := common.PolicyStatus{ + Name: "example-policy", + Type: v1alpha1.EnvBindingPolicyType, + } + if testCase.Status == nil { + _status.Status = &runtime.RawExtension{Raw: testCase.StatusRaw} + } else { + bs, err := json.Marshal(testCase.Status) + r.NoError(err) + _status.Status = &runtime.RawExtension{Raw: bs} + } + app.Status.PolicyStatus = []common.PolicyStatus{_status} + pds, exists, err := ReadPlacementDecisions(app, "", "example-env") + r.Equal(testCase.ExpectedExists, exists) + if testCase.ExpectedHasError { + r.Error(err) + continue + } + r.NoError(err) + if exists { + r.Equal(len(pld), len(pds)) + for idx := range pld { + r.Equal(pld[idx].Cluster, pds[idx].Cluster) + r.Equal(pld[idx].Namespace, pds[idx].Namespace) + } + } + } +} + +func TestUpdateClusterConnections(t *testing.T) { + app := &v1beta1.Application{} + app.Status.LatestRevision = &common.Revision{Name: "v1"} + status := &v1alpha1.EnvBindingStatus{ + ClusterConnections: []v1alpha1.ClusterConnection{{ + ClusterName: "cluster-1", + LastActiveRevision: "v0", + }, { + ClusterName: "cluster-2", + LastActiveRevision: "v0", + }}, + } + decisions := []v1alpha1.PlacementDecision{{ + Cluster: "cluster-1", + }, { + Cluster: "cluster-3", + }} + updateClusterConnections(status, decisions, app) + + r := require.New(t) + expectedConnections := []v1alpha1.ClusterConnection{{ + ClusterName: "cluster-1", + LastActiveRevision: "v1", + }, { + ClusterName: "cluster-2", + LastActiveRevision: "v0", + }, { + ClusterName: "cluster-3", + LastActiveRevision: "v1", + }} + r.Equal(len(expectedConnections), len(status.ClusterConnections)) + for idx, conn := range expectedConnections { + _conn := status.ClusterConnections[idx] + r.Equal(conn.ClusterName, _conn.ClusterName) + r.Equal(conn.LastActiveRevision, _conn.LastActiveRevision) + } +} diff --git a/pkg/policy/envbinding/utils.go b/pkg/policy/envbinding/utils.go new file mode 100644 index 000000000..569b829e1 --- /dev/null +++ b/pkg/policy/envbinding/utils.go @@ -0,0 +1,51 @@ +/* +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 envbinding + +import ( + "encoding/json" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" +) + +// GetEnvBindingPolicy extract env-binding policy with given policy name, if policy name is empty, the first env-binding policy will be used +func GetEnvBindingPolicy(app *v1beta1.Application, policyName string) (*v1alpha1.EnvBindingSpec, error) { + for _, policy := range app.Spec.Policies { + if (policy.Name == policyName || policyName == "") && policy.Type == v1alpha1.EnvBindingPolicyType { + envBindingSpec := &v1alpha1.EnvBindingSpec{} + err := json.Unmarshal(policy.Properties.Raw, envBindingSpec) + return envBindingSpec, err + } + } + return nil, nil +} + +// GetEnvBindingPolicyStatus extract env-binding policy status with given policy name, if policy name is empty, the first env-binding policy will be used +func GetEnvBindingPolicyStatus(app *v1beta1.Application, policyName string) (*v1alpha1.EnvBindingStatus, error) { + for _, policyStatus := range app.Status.PolicyStatus { + if (policyStatus.Name == policyName || policyName == "") && policyStatus.Type == v1alpha1.EnvBindingPolicyType { + envBindingStatus := &v1alpha1.EnvBindingStatus{} + if policyStatus.Status != nil { + err := json.Unmarshal(policyStatus.Status.Raw, envBindingStatus) + return envBindingStatus, err + } + return nil, nil + } + } + return nil, nil +} diff --git a/pkg/stdlib/op.cue b/pkg/stdlib/op.cue index 8c4804347..8071f8ab6 100644 --- a/pkg/stdlib/op.cue +++ b/pkg/stdlib/op.cue @@ -1,5 +1,4 @@ import ( - "encoding/yaml" "encoding/json" "encoding/base64" "strings" @@ -36,6 +35,32 @@ import ( #ApplyComponent: oam.#ApplyComponent +#RenderComponent: oam.#RenderComponent + +#ApplyComponentRemaining: #Steps & { + // exceptions specify the resources not to apply. + exceptions: [...string] + _exceptions: {for c in exceptions {"\(c)": true}} + component: string + + load: oam.#LoadComponets @step(1) + render: #Steps & { + rendered: oam.#RenderComponent & { + value: load.value[component] + } + comp: kube.#Apply & { + value: rendered.output + } + for name, c in rendered.outputs { + if _exceptions[name] == _|_ { + "\(name)": kube.#Apply & { + value: c + } + } + } + } @step(2) +} + #ApplyRemaining: #Steps & { // exceptions specify the resources not to apply. exceptions: [...string] @@ -80,57 +105,7 @@ import ( } } -#ApplyEnvBindApp: #Steps & { - env: string - policy: string - app: string - namespace: string - _namespace: namespace - - envBinding: kube.#Read & { - value: { - apiVersion: "core.oam.dev/v1alpha1" - kind: "EnvBinding" - metadata: { - name: policy - namespace: _namespace - } - } - } @step(1) - - // wait until envBinding.value.status equal "finished" - wait: #ConditionalWait & { - continue: envBinding.value.status.phase == "finished" - } @step(2) - - configMap: kube.#Read & { - value: { - apiVersion: "v1" - kind: "ConfigMap" - metadata: { - name: policy - namespace: _namespace - } - data?: _ - } - } @step(3) - - patchedApp: yaml.Unmarshal(configMap.value.data["\(env)"])[context.name] - components: patchedApp.spec.components - apply: #Steps & { - for key, comp in components { - "\(key)": #ApplyComponent & { - value: comp - if patchedApp.metadata.labels != _|_ && patchedApp.metadata.labels["cluster.oam.dev/clusterName"] != _|_ { - cluster: patchedApp.metadata.labels["cluster.oam.dev/clusterName"] - } - if patchedApp.metadata.labels != _|_ && patchedApp.metadata.labels["envbinding.oam.dev/override-namespace"] != _|_ { - namespace: patchedApp.metadata.labels["envbinding.oam.dev/override-namespace"] - } - } @step(4) - } - } -} +#ApplyEnvBindApp: multicluster.#ApplyEnvBindApp #HTTPGet: http.#Do & {method: "GET"} diff --git a/pkg/stdlib/packages.go b/pkg/stdlib/packages.go index 1a8e5ddd8..9a2ed616f 100644 --- a/pkg/stdlib/packages.go +++ b/pkg/stdlib/packages.go @@ -27,8 +27,7 @@ import ( var ( //go:embed pkgs op.cue - fs embed.FS - pkgContent string + fs embed.FS ) // GetPackages Get Stdlib packages @@ -44,7 +43,7 @@ func GetPackages(tagTempl string) (map[string]string, error) { return nil, err } - pkgContent = string(opBytes) + "\n" + pkgContent := string(opBytes) + "\n" for _, file := range files { body, err := fs.ReadFile("pkgs/" + file.Name()) if err != nil { diff --git a/pkg/stdlib/pkgs/kube.cue b/pkg/stdlib/pkgs/kube.cue index af381fcec..a825ef1e3 100644 --- a/pkg/stdlib/pkgs/kube.cue +++ b/pkg/stdlib/pkgs/kube.cue @@ -41,4 +41,5 @@ namespace: *"default" | string } } + ... } diff --git a/pkg/stdlib/pkgs/multicluster.cue b/pkg/stdlib/pkgs/multicluster.cue new file mode 100644 index 000000000..7b65c6cef --- /dev/null +++ b/pkg/stdlib/pkgs/multicluster.cue @@ -0,0 +1,125 @@ +#Placement: { + clusterSelector?: { + labels?: [string]: string + name?: string + } + namespaceSelector?: { + labels?: [string]: string + name?: string + } +} + +#PlacementDecision: { + namespace?: string + cluster?: string +} + +#Component: { + name: string + type: string + properties?: {...} + traits?: [...{ + type: string + disable?: bool + properties: {...} + }] +} + +#ReadPlacementDecisions: { + #provider: "multicluster" + #do: "read-placement-decisions" + + inputs: { + policy: string + envName: string + } + + outputs: { + decisions?: [...#PlacementDecision] + } +} + +#MakePlacementDecisions: { + #provider: "multicluster" + #do: "make-placement-decisions" + + inputs: { + policyName: string + envName: string + placement: #Placement + } + + outputs: { + decisions: [...#PlacementDecision] + } +} + +#PatchApplication: { + #provider: "multicluster" + #do: "patch-application" + + inputs: { + envName: string + patch?: components: [...#Component] + selector?: components: [...string] + } + + outputs: {...} + ... +} + +#ApplyEnvBindApp: { + #do: "steps" + + env: string + policy: string + app: string + namespace: string + + loadPolicies: oam.#LoadPolicies @step(1) + loadPolicy: loadPolicies.value["\(policy)"] + envMap: { + for ev in loadPolicy.properties.envs { + "\(ev.name)": ev + } + ... + } + envConfig: envMap["\(env)"] + + placementDecisions: multicluster.#MakePlacementDecisions & { + inputs: { + policyName: policy + envName: env + placement: envConfig.placement + } + } @step(2) + + patchedApp: multicluster.#PatchApplication & { + inputs: { + envName: env + if envConfig.selector != _|_ { + selector: envConfig.selector + } + if envConfig.patch != _|_ { + patch: envConfig.patch + } + } + } @step(3) + + components: patchedApp.outputs.spec.components + apply: #Steps & { + for decision in placementDecisions.outputs.decisions { + for key, comp in components { + "\(decision.cluster)-\(decision.namespace)-\(key)": #ApplyComponent & { + value: comp + if decision.cluster != _|_ { + cluster: decision.cluster + } + if decision.namespace != _|_ { + namespace: decision.namespace + } + } @step(4) + } + } + } +} diff --git a/pkg/stdlib/pkgs/oam.cue b/pkg/stdlib/pkgs/oam.cue index b30d87ecc..51e9d15f2 100644 --- a/pkg/stdlib/pkgs/oam.cue +++ b/pkg/stdlib/pkgs/oam.cue @@ -7,8 +7,26 @@ ... } +#RenderComponent: { + #provider: "oam" + #do: "component-render" + cluster: *"" | string + value: {...} + patch?: {...} + output?: {...} + outputs?: {...} + ... +} + #LoadComponets: { #provider: "oam" #do: "load" ... } + +#LoadPolicies: { + #provider: "oam" + #do: "load-policies" + value?: {...} + ... +} diff --git a/pkg/stdlib/pkgs/slack.cue b/pkg/stdlib/pkgs/slack.cue index a903ed612..ee255772b 100644 --- a/pkg/stdlib/pkgs/slack.cue +++ b/pkg/stdlib/pkgs/slack.cue @@ -18,17 +18,17 @@ url?: string value?: string style?: string - text?: #text + text?: #textType confirm?: { - title: #text - text: #text - confirm: #text - deny: #text + title: #textType + text: #textType + confirm: #textType + deny: #textType style?: string } options?: [...#option] initial_options?: [...#option] - placeholder?: #text + placeholder?: #textType initial_date?: string image_url?: string alt_text?: string @@ -45,7 +45,7 @@ }] } -#text: { +#textType: { type: string text: string emoji?: bool @@ -53,8 +53,8 @@ } #option: { - text: text + text: #textType value: string - description?: text + description?: #textType url?: string } diff --git a/pkg/utils/common/args.go b/pkg/utils/common/args.go index 824065987..76482358a 100644 --- a/pkg/utils/common/args.go +++ b/pkg/utils/common/args.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" + "k8s.io/client-go/util/flowcontrol" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" @@ -43,6 +44,7 @@ func (a *Args) SetConfig() error { if err != nil { return err } + restConf.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(100, 200) a.Config = restConf return nil } diff --git a/pkg/utils/common/common.go b/pkg/utils/common/common.go index 36c35d1a4..7b2e07d79 100644 --- a/pkg/utils/common/common.go +++ b/pkg/utils/common/common.go @@ -34,6 +34,7 @@ import ( "cuelang.org/go/encoding/openapi" "github.com/AlecAivazis/survey/v2" "github.com/hashicorp/hcl/v2/hclparse" + clustergatewayapi "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" "github.com/oam-dev/terraform-config-inspect/tfconfig" terraformv1beta1 "github.com/oam-dev/terraform-controller/api/v1beta1" kruise "github.com/openkruise/kruise-api/apps/v1alpha1" @@ -44,7 +45,9 @@ import ( k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/util/flowcontrol" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" + ocmclusterv1 "open-cluster-management.io/api/cluster/v1" ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1" ocmworkv1 "open-cluster-management.io/api/work/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -52,6 +55,8 @@ import ( "sigs.k8s.io/yaml" oamcore "github.com/oam-dev/kubevela/apis/core.oam.dev" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" oamstandard "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1" velacue "github.com/oam-dev/kubevela/pkg/cue" "github.com/oam-dev/kubevela/pkg/cue/model" @@ -73,7 +78,9 @@ func init() { _ = kruise.AddToScheme(Scheme) _ = terraformv1beta1.AddToScheme(Scheme) _ = ocmclusterv1alpha1.Install(Scheme) + _ = ocmclusterv1.Install(Scheme) _ = ocmworkv1.Install(Scheme) + _ = clustergatewayapi.AddToScheme(Scheme) // +kubebuilder:scaffold:scheme } @@ -83,8 +90,10 @@ func InitBaseRestConfig() (Args, error) { if err != nil && os.Getenv("IGNORE_KUBE_CONFIG") != "true" { fmt.Println("get kubeConfig err", err) os.Exit(1) + } else if err != nil { + return Args{}, err } - + restConf.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(100, 200) return Args{ Config: restConf, Schema: Scheme, @@ -236,7 +245,80 @@ func RealtimePrintCommandOutput(cmd *exec.Cmd, logFile string) error { return nil } -// AskToChooseOneService will ask users to select one service of the application if more than one exidi +// ClusterObject2Map convert ClusterObjectReference to a readable map +func ClusterObject2Map(refs []common.ClusterObjectReference) map[string]string { + clusterResourceRefTmpl := "Cluster: %s | Namespace: %s | Component: %s | Kind: %s" + objs := make(map[string]string, len(refs)) + for _, r := range refs { + if r.Cluster == "" { + r.Cluster = "local" + } + objs[r.Cluster+"/"+r.Namespace+"/"+r.Name] = fmt.Sprintf(clusterResourceRefTmpl, r.Cluster, r.Namespace, r.Name, r.Kind) + } + return objs +} + +// ResourceLocation indicates the resource location +type ResourceLocation struct { + Cluster string + Namespace string +} + +func filterWorkload(resources []common.ClusterObjectReference) []common.ClusterObjectReference { + var filteredOR []common.ClusterObjectReference + loggableWorkload := map[string]bool{ + "Deployment": true, + "StatefulSet": true, + "CloneSet": true, + "Job": true, + } + for _, r := range resources { + if _, ok := loggableWorkload[r.Kind]; ok { + filteredOR = append(filteredOR, r) + } + } + return filteredOR +} + +// AskToChooseOneEnvResource will ask users to select one applied resource of the application if more than one +// resources is a map for component to applied resources +// return the selected ClusterObjectReference +func AskToChooseOneEnvResource(app *v1beta1.Application) (*common.ClusterObjectReference, error) { + resources := app.Status.AppliedResources + if len(resources) == 0 { + return nil, fmt.Errorf("no resources in the application deployed yet") + } + resources = filterWorkload(resources) + // filter locations + if len(resources) == 0 { + return nil, fmt.Errorf("no supported workload resources detected in deployed resources") + } + if len(resources) == 1 { + return &resources[0], nil + } + opMap := ClusterObject2Map(resources) + var ops []string + for _, r := range opMap { + ops = append(ops, r) + } + prompt := &survey.Select{ + Message: fmt.Sprintf("You have %d deployed resources in your app. Please choose one:", len(ops)), + Options: ops, + } + var selectedRsc string + err := survey.AskOne(prompt, &selectedRsc) + if err != nil { + return nil, fmt.Errorf("choosing resource err %w", err) + } + for k, resource := range ops { + if selectedRsc == resource { + return &resources[k], nil + } + } + return nil, fmt.Errorf("choosing resource err %w", err) +} + +// AskToChooseOneService will ask users to select one service of the application if more than one func AskToChooseOneService(svcNames []string) (string, error) { if len(svcNames) == 0 { return "", fmt.Errorf("no service exist in the application") @@ -256,6 +338,26 @@ func AskToChooseOneService(svcNames []string) (string, error) { return svcName, nil } +// AskToChooseOnePods will ask users to select one pods of the resource if more than one +func AskToChooseOnePods(podNames []string) (string, error) { + if len(podNames) == 0 { + return "", fmt.Errorf("no service exist in the application") + } + if len(podNames) == 1 { + return podNames[0], nil + } + prompt := &survey.Select{ + Message: "You have multiple pods in the specified resource. Please choose one: ", + Options: podNames, + } + var svcName string + err := survey.AskOne(prompt, &svcName) + if err != nil { + return "", fmt.Errorf("choosing pod err %w", err) + } + return svcName, nil +} + // ReadYamlToObject will read a yaml K8s object to runtime.Object func ReadYamlToObject(path string, object k8sruntime.Object) error { data, err := os.ReadFile(filepath.Clean(path)) diff --git a/pkg/velaql/view.go b/pkg/velaql/view.go index bb3597877..d242fce90 100644 --- a/pkg/velaql/view.go +++ b/pkg/velaql/view.go @@ -77,7 +77,7 @@ func (v *ViewHandler) QueryView(ctx context.Context, query Query) (*value.Value, ctx = oamutil.SetNamespaceInCtx(ctx, v.namespace) handlerProviders := providers.NewProviders() - kube.Install(handlerProviders, v.cli, v.dispatch) + kube.Install(handlerProviders, v.cli, v.dispatch, v.delete) taskDiscover := tasks.NewTaskDiscover(handlerProviders, v.pd, v.cli, v.dm) genTask, err := taskDiscover.GetTaskGenerator(ctx, v.workflowStep.Type) if err != nil { @@ -115,6 +115,10 @@ func (v *ViewHandler) dispatch(ctx context.Context, cluster string, owner common return nil } +func (v *ViewHandler) delete(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifest *unstructured.Unstructured) error { + return v.cli.Delete(ctx, manifest) +} + // QueryParameterKey query parameter key type QueryParameterKey struct { Outputs common.StepOutputs `json:"outputs"` diff --git a/pkg/workflow/context/context.go b/pkg/workflow/context/context.go index 107377f02..a63d5138e 100644 --- a/pkg/workflow/context/context.go +++ b/pkg/workflow/context/context.go @@ -26,8 +26,12 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/cue/model" "github.com/oam-dev/kubevela/pkg/cue/model/value" "github.com/oam-dev/kubevela/pkg/oam/util" @@ -256,35 +260,9 @@ func (comp *ComponentManifest) unmarshal(v string) error { return nil } -// NewContext new workflow context. -func NewContext(cli client.Client, ns, rev string) (Context, error) { - - var ( - ctx = context.Background() - manifestCm corev1.ConfigMap - ) - - if err := cli.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: rev, - }, &manifestCm); err != nil { - return nil, errors.WithMessagef(err, "Get manifest ConfigMap %s/%s ", ns, rev) - } - - wfCtx, err := newContext(cli, ns, rev) - if err != nil { - return nil, err - } - if err := wfCtx.LoadFromConfigMap(manifestCm); err != nil { - return nil, errors.WithMessagef(err, "load from ConfigMap %s/%s", ns, rev) - } - - return wfCtx, wfCtx.Commit() -} - -// NewEmptyContext new workflow context without initialize data. -func NewEmptyContext(cli client.Client, ns, app string) (Context, error) { - wfCtx, err := newContext(cli, ns, app) +// NewContext new workflow context without initialize data. +func NewContext(cli client.Client, ns, app string, appUID types.UID) (Context, error) { + wfCtx, err := newContext(cli, ns, app, appUID) if err != nil { return nil, err } @@ -292,13 +270,22 @@ func NewEmptyContext(cli client.Client, ns, app string) (Context, error) { return wfCtx, wfCtx.Commit() } -func newContext(cli client.Client, ns, app string) (*WorkflowContext, error) { +func newContext(cli client.Client, ns, app string, appUID types.UID) (*WorkflowContext, error) { var ( ctx = context.Background() store corev1.ConfigMap ) store.Name = generateStoreName(app) store.Namespace = ns + store.SetOwnerReferences([]metav1.OwnerReference{ + { + APIVersion: v1beta1.SchemeGroupVersion.String(), + Kind: v1beta1.ApplicationKind, + Name: app, + UID: appUID, + Controller: pointer.BoolPtr(true), + }, + }) if err := cli.Get(ctx, client.ObjectKey{Name: store.Name, Namespace: store.Namespace}, &store); err != nil { if kerrors.IsNotFound(err) { if err := cli.Create(ctx, &store); err != nil { diff --git a/pkg/workflow/context/context_test.go b/pkg/workflow/context/context_test.go index a87546250..b616367d4 100644 --- a/pkg/workflow/context/context_test.go +++ b/pkg/workflow/context/context_test.go @@ -259,14 +259,11 @@ func TestContext(t *testing.T) { }, } - wfCtx, err := NewContext(cli, "default", "app-v1") + wfCtx, err := NewContext(cli, "default", "app-v1", "testuid") assert.NilError(t, err) err = wfCtx.Commit() assert.NilError(t, err) - _, err = NewContext(cli, "default", "app-not-found") - assert.Equal(t, err != nil, true) - wfCtx, err = LoadContext(cli, "default", "app-v1") assert.NilError(t, err) err = wfCtx.Commit() @@ -276,7 +273,7 @@ func TestContext(t *testing.T) { _, err = LoadContext(cli, "default", "app-v1") assert.Equal(t, err != nil, true) - wfCtx, err = NewEmptyContext(cli, "default", "app-v1") + wfCtx, err = NewContext(cli, "default", "app-v1", "testuid") assert.NilError(t, err) assert.Equal(t, len(wfCtx.GetComponents()), 0) _, err = wfCtx.GetComponent("server") diff --git a/pkg/workflow/hooks/data_passing_test.go b/pkg/workflow/hooks/data_passing_test.go index 797641465..2197a33d3 100644 --- a/pkg/workflow/hooks/data_passing_test.go +++ b/pkg/workflow/hooks/data_passing_test.go @@ -106,7 +106,7 @@ func mockContext(t *testing.T) wfContext.Context { return nil }, } - wfCtx, err := wfContext.NewEmptyContext(cli, "default", "v1") + wfCtx, err := wfContext.NewContext(cli, "default", "v1", "testuid") require.NoError(t, err) return wfCtx } diff --git a/pkg/workflow/providers/kube/handle.go b/pkg/workflow/providers/kube/handle.go index fc178228e..606933ff1 100644 --- a/pkg/workflow/providers/kube/handle.go +++ b/pkg/workflow/providers/kube/handle.go @@ -41,9 +41,13 @@ const ( // Dispatcher is a client for apply resources. type Dispatcher func(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifests ...*unstructured.Unstructured) error +// Deleter is a client for delete resources. +type Deleter func(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifest *unstructured.Unstructured) error + type provider struct { - apply Dispatcher - cli client.Client + apply Dispatcher + delete Deleter + cli client.Client } // Apply create or update CR in cluster. @@ -170,18 +174,19 @@ func (h *provider) Delete(ctx wfContext.Context, v *value.Value, act types.Actio if err != nil { return err } - readCtx := multicluster.ContextWithClusterName(context.Background(), cluster) - if err := h.cli.Delete(readCtx, obj); err != nil { + deleteCtx := multicluster.ContextWithClusterName(context.Background(), cluster) + if err := h.delete(deleteCtx, cluster, common.WorkflowResourceCreator, obj); err != nil { return v.FillObject(err.Error(), "err") } return nil } // Install register handlers to provider discover. -func Install(p providers.Providers, cli client.Client, apply Dispatcher) { +func Install(p providers.Providers, cli client.Client, apply Dispatcher, deleter Deleter) { prd := &provider{ - apply: apply, - cli: cli, + apply: apply, + delete: deleter, + cli: cli, } p.Register(ProviderName, map[string]providers.Handler{ "apply": prd.Apply, diff --git a/pkg/workflow/providers/kube/handle_test.go b/pkg/workflow/providers/kube/handle_test.go index 6a62c75f8..a71fb3249 100644 --- a/pkg/workflow/providers/kube/handle_test.go +++ b/pkg/workflow/providers/kube/handle_test.go @@ -282,6 +282,12 @@ cluster: "" apply: func(ctx context.Context, _ string, _ common.ResourceCreatorRole, manifests ...*unstructured.Unstructured) error { return nil }, + delete: func(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifest *unstructured.Unstructured) error { + if err := k8sClient.Delete(ctx, manifest); err != nil { + return err + } + return nil + }, cli: k8sClient, } diff --git a/pkg/workflow/providers/multicluster/multicluster.go b/pkg/workflow/providers/multicluster/multicluster.go new file mode 100644 index 000000000..50952327e --- /dev/null +++ b/pkg/workflow/providers/multicluster/multicluster.go @@ -0,0 +1,157 @@ +/* +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 multicluster + +import ( + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/clustermanager" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/policy/envbinding" + wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" + "github.com/oam-dev/kubevela/pkg/workflow/providers" + wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" +) + +const ( + // ProviderName is provider name for install. + ProviderName = "multicluster" +) + +type provider struct { + client.Client + app *v1beta1.Application +} + +func (p *provider) ReadPlacementDecisions(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + policy, err := v.GetString("inputs", "policyName") + if err != nil { + return err + } + env, err := v.GetString("inputs", "envName") + if err != nil { + return err + } + decisions, exists, err := envbinding.ReadPlacementDecisions(p.app, policy, env) + if err != nil { + return err + } + if exists { + return v.FillObject(map[string]interface{}{"decisions": decisions}, "outputs") + } + return v.FillObject(map[string]interface{}{}, "outputs") +} + +func (p *provider) MakePlacementDecisions(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + policy, err := v.GetString("inputs", "policyName") + if err != nil { + return err + } + env, err := v.GetString("inputs", "envName") + if err != nil { + return err + } + val, err := v.LookupValue("inputs", "placement") + if err != nil { + return err + } + + // TODO detect env change + placement := &v1alpha1.EnvPlacement{} + if err = val.UnmarshalTo(placement); err != nil { + return errors.Wrapf(err, "failed to parse placement while making placement decision") + } + + var namespace, clusterName string + // check if namespace selector is valid + if placement.NamespaceSelector != nil { + if len(placement.NamespaceSelector.Labels) != 0 { + return errors.Errorf("invalid env %s: namespace selector in cluster-gateway does not support label selector for now", env) + } + namespace = placement.NamespaceSelector.Name + } + // check if cluster selector is valid + if placement.ClusterSelector != nil { + if len(placement.ClusterSelector.Labels) != 0 { + return errors.Errorf("invalid env %s: cluster selector does not support label selector for now", env) + } + clusterName = placement.ClusterSelector.Name + } + // set fallback cluster + if clusterName == "" { + clusterName = multicluster.ClusterLocalName + } + // check if target cluster exists + if clusterName != multicluster.ClusterLocalName { + if err = clustermanager.EnsureClusterExists(p, clusterName); err != nil { + return errors.Wrapf(err, "failed to get cluster %s for env %s", clusterName, env) + } + } + // write result back + decisions := []v1alpha1.PlacementDecision{{ + Cluster: clusterName, + Namespace: namespace, + }} + if err = envbinding.WritePlacementDecisions(p.app, policy, env, decisions); err != nil { + return err + } + return v.FillObject(map[string]interface{}{"decisions": decisions}, "outputs") +} + +func (p *provider) PatchApplication(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + env, err := v.GetString("inputs", "envName") + if err != nil { + return err + } + patch := v1alpha1.EnvPatch{} + selector := &v1alpha1.EnvSelector{} + + obj, err := v.LookupValue("inputs", "patch") + if err == nil { + if err = obj.UnmarshalTo(&patch); err != nil { + return errors.Wrapf(err, "failed to unmarshal patch for env %s", env) + } + } + obj, err = v.LookupValue("inputs", "selector") + if err == nil { + if err = obj.UnmarshalTo(selector); err != nil { + return errors.Wrapf(err, "failed to unmarshal selector for env %s", env) + } + } else { + selector = nil + } + + newApp, err := envbinding.PatchApplication(p.app, &patch, selector) + if err != nil { + return errors.Wrapf(err, "failed to patch app for env %s", env) + } + return v.FillObject(newApp, "outputs") +} + +// Install register handlers to provider discover. +func Install(p providers.Providers, c client.Client, app *v1beta1.Application) { + prd := &provider{Client: c, app: app} + p.Register(ProviderName, map[string]providers.Handler{ + "read-placement-decisions": prd.ReadPlacementDecisions, + "make-placement-decisions": prd.MakePlacementDecisions, + "patch-application": prd.PatchApplication, + }) +} diff --git a/pkg/workflow/providers/multicluster/multicluster_test.go b/pkg/workflow/providers/multicluster/multicluster_test.go new file mode 100644 index 000000000..4c5e236f4 --- /dev/null +++ b/pkg/workflow/providers/multicluster/multicluster_test.go @@ -0,0 +1,479 @@ +/* +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 multicluster + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/pkg/workflow/providers/mock" +) + +func TestReadPlacementDecisions(t *testing.T) { + testCases := []struct { + InputVal map[string]interface{} + OldCluster string + OldNamespace string + ExpectError string + ExpectDecisionExists bool + ExpectCluster string + ExpectNamespace string + }{{ + InputVal: map[string]interface{}{}, + ExpectError: "var(path=inputs.policyName) not exist", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + }, + ExpectError: "var(path=inputs.envName) not exist", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + }, + ExpectError: "", + ExpectDecisionExists: false, + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + }, + OldCluster: "example-cluster", + OldNamespace: "example-namespace", + ExpectError: "", + ExpectDecisionExists: true, + ExpectCluster: "example-cluster", + ExpectNamespace: "example-namespace", + }} + r := require.New(t) + for _, testCase := range testCases { + cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build() + app := &v1beta1.Application{} + p := &provider{ + Client: cli, + app: app, + } + act := &mock.Action{} + v, err := value.NewValue("", nil, "") + r.NoError(err) + r.NoError(v.FillObject(testCase.InputVal, "inputs")) + if testCase.ExpectCluster != "" || testCase.ExpectNamespace != "" { + pd := v1alpha1.PlacementDecision{ + Cluster: testCase.OldCluster, + Namespace: testCase.OldNamespace, + } + bs, err := json.Marshal(&v1alpha1.EnvBindingStatus{ + Envs: []v1alpha1.EnvStatus{{ + Env: "example-env", + Placements: []v1alpha1.PlacementDecision{pd}, + }}, + }) + r.NoError(err) + app.Status.PolicyStatus = []common2.PolicyStatus{{ + Name: "example-policy", + Type: v1alpha1.EnvBindingPolicyType, + Status: &runtime.RawExtension{Raw: bs}, + }} + } + err = p.ReadPlacementDecisions(nil, v, act) + if testCase.ExpectError == "" { + r.NoError(err) + } else { + r.Contains(err.Error(), testCase.ExpectError) + continue + } + outputs, err := v.LookupValue("outputs") + r.NoError(err) + md := map[string][]v1alpha1.PlacementDecision{} + r.NoError(outputs.UnmarshalTo(&md)) + if !testCase.ExpectDecisionExists { + r.Equal(0, len(md)) + } else { + r.Equal(1, len(md["decisions"])) + r.Equal(testCase.ExpectCluster, md["decisions"][0].Cluster) + r.Equal(testCase.ExpectNamespace, md["decisions"][0].Namespace) + } + } +} + +func TestMakePlacementDecisions(t *testing.T) { + multicluster.ClusterGatewaySecretNamespace = types.DefaultKubeVelaNS + testCases := []struct { + InputVal map[string]interface{} + OldCluster string + OldNamespace string + ExpectError string + ExpectCluster string + ExpectNamespace string + PreAddCluster string + }{{ + InputVal: map[string]interface{}{}, + ExpectError: "var(path=inputs.policyName) not exist", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + }, + ExpectError: "var(path=inputs.envName) not exist", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + }, + ExpectError: "var(path=inputs.placement) not exist", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + "placement": "example-placement", + }, + ExpectError: "failed to parse placement while making placement decision", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + "placement": map[string]interface{}{ + "namespaceSelector": map[string]interface{}{ + "labels": map[string]string{"key": "value"}, + }, + }, + }, + ExpectError: "namespace selector in cluster-gateway does not support label selector for now", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + "placement": map[string]interface{}{ + "clusterSelector": map[string]interface{}{ + "labels": map[string]string{"key": "value"}, + }, + }, + }, + ExpectError: "cluster selector does not support label selector for now", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + "placement": map[string]interface{}{}, + }, + ExpectError: "", + ExpectCluster: "local", + ExpectNamespace: "", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + "placement": map[string]interface{}{ + "clusterSelector": map[string]interface{}{ + "name": "example-cluster", + }, + "namespaceSelector": map[string]interface{}{ + "name": "example-namespace", + }, + }, + }, + ExpectError: "failed to get cluster", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + "placement": map[string]interface{}{ + "clusterSelector": map[string]interface{}{ + "name": "example-cluster", + }, + "namespaceSelector": map[string]interface{}{ + "name": "example-namespace", + }, + }, + }, + ExpectError: "", + ExpectCluster: "example-cluster", + ExpectNamespace: "example-namespace", + PreAddCluster: "example-cluster", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + "placement": map[string]interface{}{ + "clusterSelector": map[string]interface{}{ + "name": "example-cluster", + }, + "namespaceSelector": map[string]interface{}{ + "name": "example-namespace", + }, + }, + }, + OldCluster: "old-cluster", + OldNamespace: "old-namespace", + ExpectError: "", + ExpectCluster: "example-cluster", + ExpectNamespace: "example-namespace", + PreAddCluster: "example-cluster", + }, { + InputVal: map[string]interface{}{ + "policyName": "example-policy", + "envName": "example-env", + "placement": map[string]interface{}{ + "clusterSelector": map[string]interface{}{ + "name": "example-cluster", + }, + "namespaceSelector": map[string]interface{}{ + "name": "example-namespace", + }, + }, + }, + ExpectError: "", + ExpectCluster: "example-cluster", + ExpectNamespace: "example-namespace", + PreAddCluster: "example-cluster", + }} + + r := require.New(t) + for _, testCase := range testCases { + cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build() + app := &v1beta1.Application{} + p := &provider{ + Client: cli, + app: app, + } + act := &mock.Action{} + v, err := value.NewValue("", nil, "") + r.NoError(err) + r.NoError(v.FillObject(testCase.InputVal, "inputs")) + if testCase.PreAddCluster != "" { + r.NoError(cli.Create(context.Background(), &v1.Secret{ + ObjectMeta: v12.ObjectMeta{ + Namespace: multicluster.ClusterGatewaySecretNamespace, + Name: testCase.PreAddCluster, + }, + })) + } + if testCase.OldNamespace != "" || testCase.OldCluster != "" { + pd := v1alpha1.PlacementDecision{ + Cluster: testCase.OldNamespace, + Namespace: testCase.OldCluster, + } + bs, err := json.Marshal(&v1alpha1.EnvBindingStatus{ + Envs: []v1alpha1.EnvStatus{{ + Env: "example-env", + Placements: []v1alpha1.PlacementDecision{pd}, + }}, + }) + r.NoError(err) + app.Status.PolicyStatus = []common2.PolicyStatus{{ + Name: "example-policy", + Type: v1alpha1.EnvBindingPolicyType, + Status: &runtime.RawExtension{Raw: bs}, + }} + } + err = p.MakePlacementDecisions(nil, v, act) + if testCase.ExpectError == "" { + r.NoError(err) + } else { + r.Contains(err.Error(), testCase.ExpectError) + continue + } + outputs, err := v.LookupValue("outputs") + r.NoError(err) + md := map[string][]v1alpha1.PlacementDecision{} + r.NoError(outputs.UnmarshalTo(&md)) + r.Equal(1, len(md["decisions"])) + r.Equal(testCase.ExpectCluster, md["decisions"][0].Cluster) + r.Equal(testCase.ExpectNamespace, md["decisions"][0].Namespace) + r.Equal(1, len(app.Status.PolicyStatus)) + r.Equal(testCase.InputVal["policyName"], app.Status.PolicyStatus[0].Name) + r.Equal(v1alpha1.EnvBindingPolicyType, app.Status.PolicyStatus[0].Type) + status := &v1alpha1.EnvBindingStatus{} + r.NoError(json.Unmarshal(app.Status.PolicyStatus[0].Status.Raw, status)) + r.Equal(1, len(status.Envs)) + r.Equal(testCase.InputVal["envName"], status.Envs[0].Env) + r.Equal(1, len(status.Envs[0].Placements)) + r.Equal(testCase.ExpectNamespace, status.Envs[0].Placements[0].Namespace) + r.Equal(testCase.ExpectCluster, status.Envs[0].Placements[0].Cluster) + } +} + +func TestPatchApplication(t *testing.T) { + baseApp := &v1beta1.Application{Spec: v1beta1.ApplicationSpec{ + Components: []common2.ApplicationComponent{{ + Name: "comp-1", + Type: "webservice", + Properties: &runtime.RawExtension{Raw: []byte(`{"image":"base"}`)}, + }, { + Name: "comp-3", + Type: "webservice", + Properties: &runtime.RawExtension{Raw: []byte(`{"image":"ext"}`)}, + Traits: []common2.ApplicationTrait{{ + Type: "scaler", + Properties: &runtime.RawExtension{Raw: []byte(`{"replicas":3}`)}, + }, { + Type: "env", + Properties: &runtime.RawExtension{Raw: []byte(`{"env":{"key":"value"}}`)}, + }, { + Type: "labels", + Properties: &runtime.RawExtension{Raw: []byte(`{"lKey":"lVal"}`)}, + }}, + }}, + }} + testCases := []struct { + InputVal map[string]interface{} + ExpectError string + ExpectComponents []common2.ApplicationComponent + }{{ + InputVal: map[string]interface{}{}, + ExpectError: "var(path=inputs.envName) not exist", + }, { + InputVal: map[string]interface{}{ + "envName": "example-env", + }, + ExpectComponents: baseApp.Spec.Components, + }, { + InputVal: map[string]interface{}{ + "envName": "example-env", + "patch": "bad patch", + }, + ExpectError: "failed to unmarshal patch for env", + }, { + InputVal: map[string]interface{}{ + "envName": "example-env", + "selector": "bad selector", + }, + ExpectError: "failed to unmarshal selector for env", + }, { + InputVal: map[string]interface{}{ + "envName": "example-env", + "patch": map[string]interface{}{ + "components": []map[string]interface{}{{ + "name": "comp-0", + "type": "webservice", + }, { + "name": "comp-1", + "type": "worker", + "properties": map[string]interface{}{ + "image": "patch", + "port": 8080, + }, + }, { + "name": "comp-3", + "type": "webservice", + "properties": map[string]interface{}{ + "image": "patch", + "port": 8090, + }, + "traits": []map[string]interface{}{{ + "type": "scaler", + "properties": map[string]interface{}{"replicas": 5}, + }, { + "type": "env", + "properties": map[string]interface{}{"env": map[string]string{"Key": "Value"}}, + }, { + "type": "annotations", + "properties": map[string]interface{}{"aKey": "aVal"}}, + }, + }, { + "name": "comp-4", + "type": "webservice", + }}, + }, + "selector": map[string]interface{}{ + "components": []string{"comp-2", "comp-1", "comp-3", "comp-0"}, + }, + }, + ExpectComponents: []common2.ApplicationComponent{{ + Name: "comp-1", + Type: "worker", + Properties: &runtime.RawExtension{Raw: []byte(`{"image":"patch","port":8080}`)}, + }, { + Name: "comp-3", + Type: "webservice", + Properties: &runtime.RawExtension{Raw: []byte(`{"image":"patch","port":8090}`)}, + Traits: []common2.ApplicationTrait{{ + Type: "scaler", + Properties: &runtime.RawExtension{Raw: []byte(`{"replicas":5}`)}, + }, { + Type: "env", + Properties: &runtime.RawExtension{Raw: []byte(`{"env":{"Key":"Value","key":"value"}}`)}, + }, { + Type: "labels", + Properties: &runtime.RawExtension{Raw: []byte(`{"lKey":"lVal"}`)}, + }, { + Type: "annotations", + Properties: &runtime.RawExtension{Raw: []byte(`{"aKey":"aVal"}`)}, + }}, + }, { + Name: "comp-0", + Type: "webservice", + }}, + }} + r := require.New(t) + for _, testCase := range testCases { + cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build() + p := &provider{ + Client: cli, + app: baseApp, + } + act := &mock.Action{} + v, err := value.NewValue("", nil, "") + r.NoError(err) + r.NoError(v.FillObject(testCase.InputVal, "inputs")) + err = p.PatchApplication(nil, v, act) + if testCase.ExpectError == "" { + r.NoError(err) + } else { + r.Contains(err.Error(), testCase.ExpectError) + continue + } + outputs, err := v.LookupValue("outputs") + r.NoError(err) + patchApp := &v1beta1.Application{} + r.NoError(outputs.UnmarshalTo(patchApp)) + r.Equal(len(testCase.ExpectComponents), len(patchApp.Spec.Components)) + for idx, comp := range testCase.ExpectComponents { + _comp := patchApp.Spec.Components[idx] + r.Equal(comp.Name, _comp.Name) + r.Equal(comp.Type, _comp.Type) + if comp.Properties == nil { + r.Equal(comp.Properties, _comp.Properties) + } else { + r.Equal(string(comp.Properties.Raw), string(_comp.Properties.Raw)) + } + r.Equal(len(comp.Traits), len(_comp.Traits)) + for _idx, trait := range comp.Traits { + _trait := _comp.Traits[_idx] + r.Equal(trait.Type, _trait.Type) + if trait.Properties == nil { + r.Equal(trait.Properties, _trait.Properties) + } else { + r.Equal(string(trait.Properties.Raw), string(_trait.Properties.Raw)) + } + } + } + } +} diff --git a/pkg/workflow/providers/oam/apply.go b/pkg/workflow/providers/oam/apply.go index 167beb767..51b942cdc 100644 --- a/pkg/workflow/providers/oam/apply.go +++ b/pkg/workflow/providers/oam/apply.go @@ -41,32 +41,51 @@ const ( // ComponentApply apply oam component. type ComponentApply func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) +// ComponentRender render oam component. +type ComponentRender func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) + type provider struct { - apply ComponentApply - app *v1beta1.Application + render ComponentRender + apply ComponentApply + app *v1beta1.Application +} + +// ApplyComponent apply component. +func (p *provider) RenderComponent(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + comp, patcher, clusterName, overrideNamespace, err := lookUpValues(v) + if err != nil { + return err + } + workload, traits, err := p.render(*comp, patcher, clusterName, overrideNamespace) + if err != nil { + return err + } + + if workload != nil { + if err := v.FillObject(workload.Object, "output"); err != nil { + return errors.WithMessage(err, "FillOutput") + } + } + + for _, trait := range traits { + name := trait.GetLabels()[oam.TraitResource] + if name != "" { + if err := v.FillObject(trait.Object, "outputs", name); err != nil { + return errors.WithMessage(err, "FillOutputs") + } + } + } + + return nil } // ApplyComponent apply component. func (p *provider) ApplyComponent(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { - compSettings, err := v.LookupValue("value") + comp, patcher, clusterName, overrideNamespace, err := lookUpValues(v) if err != nil { return err } - comp := common.ApplicationComponent{} - - if err := compSettings.UnmarshalTo(&comp); err != nil { - return err - } - patcher, _ := v.LookupValue("patch") - clusterName, err := v.GetString("cluster") - if err != nil { - clusterName = "" - } - overrideNamespace, err := v.GetString("namespace") - if err != nil { - overrideNamespace = "" - } - workload, traits, healthy, err := p.apply(comp, patcher, clusterName, overrideNamespace) + workload, traits, healthy, err := p.apply(*comp, patcher, clusterName, overrideNamespace) if err != nil { return err } @@ -93,6 +112,31 @@ func (p *provider) ApplyComponent(ctx wfContext.Context, v *value.Value, act wfT return nil } +func lookUpValues(v *value.Value) (*common.ApplicationComponent, *value.Value, string, string, error) { + compSettings, err := v.LookupValue("value") + if err != nil { + return nil, nil, "", "", err + } + comp := &common.ApplicationComponent{} + + if err := compSettings.UnmarshalTo(comp); err != nil { + return nil, nil, "", "", err + } + patcher, err := v.LookupValue("patch") + if err != nil { + patcher = nil + } + clusterName, err := v.GetString("cluster") + if err != nil { + clusterName = "" + } + overrideNamespace, err := v.GetString("namespace") + if err != nil { + overrideNamespace = "" + } + return comp, patcher, clusterName, overrideNamespace, nil +} + // LoadComponent load component describe info in application. func (p *provider) LoadComponent(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { for _, comp := range p.app.Spec.Components { @@ -113,14 +157,27 @@ func (p *provider) LoadComponent(ctx wfContext.Context, v *value.Value, act wfTy return nil } +// LoadPolicies load policy describe info in application. +func (p *provider) LoadPolicies(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + for _, po := range p.app.Spec.Policies { + if err := v.FillObject(po, "value", po.Name); err != nil { + return err + } + } + return nil +} + // Install register handlers to provider discover. -func Install(p providers.Providers, app *v1beta1.Application, apply ComponentApply) { +func Install(p providers.Providers, app *v1beta1.Application, apply ComponentApply, render ComponentRender) { prd := &provider{ - apply: apply, - app: app.DeepCopy(), + render: render, + apply: apply, + app: app.DeepCopy(), } p.Register(ProviderName, map[string]providers.Handler{ - "component-apply": prd.ApplyComponent, - "load": prd.LoadComponent, + "component-render": prd.RenderComponent, + "component-apply": prd.ApplyComponent, + "load": prd.LoadComponent, + "load-policies": prd.LoadPolicies, }) } diff --git a/pkg/workflow/providers/oam/apply_test.go b/pkg/workflow/providers/oam/apply_test.go index 4acd592ed..fc63dea36 100644 --- a/pkg/workflow/providers/oam/apply_test.go +++ b/pkg/workflow/providers/oam/apply_test.go @@ -21,9 +21,10 @@ import ( "k8s.io/apimachinery/pkg/runtime" + "github.com/stretchr/testify/require" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "gotest.tools/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" @@ -32,23 +33,23 @@ import ( ) func TestParser(t *testing.T) { - + r := require.New(t) p := &provider{ apply: simpleComponentApplyForTest, } act := &mock.Action{} v, err := value.NewValue("", nil, "") - assert.NilError(t, err) + r.NoError(err) err = p.ApplyComponent(nil, v, act) - assert.Error(t, err, "var(path=value) not exist") + r.Equal(err.Error(), "var(path=value) not exist") v.FillObject(map[string]interface{}{}, "value") err = p.ApplyComponent(nil, v, act) - assert.NilError(t, err) + r.NoError(err) output, err := v.LookupValue("output") - assert.NilError(t, err) + r.NoError(err) outStr, err := output.String() - assert.NilError(t, err) - assert.Equal(t, outStr, `apiVersion: "v1" + r.NoError(err) + r.Equal(outStr, `apiVersion: "v1" kind: "Pod" metadata: { name: "rss-site" @@ -59,10 +60,10 @@ metadata: { `) outputs, err := v.LookupValue("outputs") - assert.NilError(t, err) + r.NoError(err) outsStr, err := outputs.String() - assert.NilError(t, err) - assert.Equal(t, outsStr, `service: { + r.NoError(err) + r.Equal(outsStr, `service: { apiVersion: "v1" kind: "Service" metadata: { @@ -74,15 +75,69 @@ metadata: { } `) - assert.Equal(t, act.Phase, "Wait") + r.Equal(act.Phase, "Wait") testHealthy = true act = &mock.Action{} _, err = value.NewValue("", nil, "") - assert.NilError(t, err) - assert.Equal(t, act.Phase, "") + r.NoError(err) + r.Equal(act.Phase, "") +} + +func TestRenderComponent(t *testing.T) { + r := require.New(t) + p := &provider{ + render: func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + }, + }, []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "core.oam.dev/v1alpha2", + "kind": "ManualScalerTrait", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "trait.oam.dev/resource": "scaler", + }, + }, + "spec": map[string]interface{}{"replicaCount": int64(10)}, + }, + }, + }, nil + }, + } + v, err := value.NewValue(`value: {}`, nil, "") + r.NoError(err) + err = p.RenderComponent(nil, v, nil) + r.NoError(err) + s, err := v.String() + r.NoError(err) + r.Equal(s, `value: {} +output: { + apiVersion: "apps/v1" + kind: "Deployment" +} +outputs: { + scaler: { + apiVersion: "core.oam.dev/v1alpha2" + kind: "ManualScalerTrait" + metadata: { + labels: { + "trait.oam.dev/resource": "scaler" + } + } + spec: { + replicaCount: 10 + } + } +} +`) } func TestLoadComponent(t *testing.T) { + r := require.New(t) p := &provider{ app: &v1beta1.Application{ Spec: v1beta1.ApplicationSpec{ @@ -97,12 +152,12 @@ func TestLoadComponent(t *testing.T) { }, } v, err := value.NewValue(``, nil, "") - assert.NilError(t, err) + r.NoError(err) err = p.LoadComponent(nil, v, nil) - assert.NilError(t, err) + r.NoError(err) s, err := v.String() - assert.NilError(t, err) - assert.Equal(t, s, `value: { + r.NoError(err) + r.Equal(s, `value: { c1: { name: *"c1" | _ type: *"web" | _ diff --git a/pkg/workflow/step/generator.go b/pkg/workflow/step/generator.go new file mode 100644 index 000000000..b4f4f4680 --- /dev/null +++ b/pkg/workflow/step/generator.go @@ -0,0 +1,104 @@ +/* +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 step + +import ( + "encoding/json" + "reflect" + + "github.com/pkg/errors" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/oam/util" +) + +// WorkflowStepGenerator generator generates workflow steps +type WorkflowStepGenerator interface { + Generate(app *v1beta1.Application, existingSteps []v1beta1.WorkflowStep) ([]v1beta1.WorkflowStep, error) +} + +// ChainWorkflowStepGenerator chains multiple workflow step generators +type ChainWorkflowStepGenerator struct { + generators []WorkflowStepGenerator +} + +// Generate generate workflow steps +func (g *ChainWorkflowStepGenerator) Generate(app *v1beta1.Application, existingSteps []v1beta1.WorkflowStep) (steps []v1beta1.WorkflowStep, err error) { + steps = existingSteps + for _, generator := range g.generators { + steps, err = generator.Generate(app, steps) + if err != nil { + return steps, errors.Wrapf(err, "generate step failed in WorkflowStepGenerator %s", reflect.TypeOf(generator).Name()) + } + } + return steps, nil +} + +// NewChainWorkflowStepGenerator create ChainWorkflowStepGenerator +func NewChainWorkflowStepGenerator(generators ...WorkflowStepGenerator) WorkflowStepGenerator { + return &ChainWorkflowStepGenerator{generators: generators} +} + +// ApplyComponentWorkflowStepGenerator generate apply-component workflow steps for all components in the application +type ApplyComponentWorkflowStepGenerator struct{} + +// Generate generate workflow steps +func (g *ApplyComponentWorkflowStepGenerator) Generate(app *v1beta1.Application, existingSteps []v1beta1.WorkflowStep) (steps []v1beta1.WorkflowStep, err error) { + if len(existingSteps) > 0 { + return existingSteps, nil + } + for _, comp := range app.Spec.Components { + steps = append(steps, v1beta1.WorkflowStep{ + Name: comp.Name, + Type: "apply-component", + Properties: util.Object2RawExtension(map[string]string{ + "component": comp.Name, + }), + }) + } + return +} + +// Deploy2EnvWorkflowStepGenerator generate deploy2env workflow steps for all envs in the application +type Deploy2EnvWorkflowStepGenerator struct{} + +// Generate generate workflow steps +func (g *Deploy2EnvWorkflowStepGenerator) Generate(app *v1beta1.Application, existingSteps []v1beta1.WorkflowStep) (steps []v1beta1.WorkflowStep, err error) { + if len(existingSteps) > 0 { + return existingSteps, nil + } + for _, policy := range app.Spec.Policies { + if policy.Type == v1alpha1.EnvBindingPolicyType && policy.Properties != nil { + spec := &v1alpha1.EnvBindingSpec{} + if err = json.Unmarshal(policy.Properties.Raw, spec); err != nil { + return + } + for _, env := range spec.Envs { + steps = append(steps, v1beta1.WorkflowStep{ + Name: "deploy-" + policy.Name + "-" + env.Name, + Type: "deploy2env", + Properties: util.Object2RawExtension(map[string]string{ + "policy": policy.Name, + "env": env.Name, + }), + }) + } + } + } + return +} diff --git a/pkg/workflow/step/generator_test.go b/pkg/workflow/step/generator_test.go new file mode 100644 index 000000000..f385b2dab --- /dev/null +++ b/pkg/workflow/step/generator_test.go @@ -0,0 +1,129 @@ +/* +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 step + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" +) + +func TestWorkflowStepGenerator(t *testing.T) { + testCases := []struct { + input []v1beta1.WorkflowStep + app *v1beta1.Application + output []v1beta1.WorkflowStep + hasError bool + }{{ + input: []v1beta1.WorkflowStep{{ + Name: "example-comp-1", + Type: "apply-component", + Properties: &runtime.RawExtension{Raw: []byte(`{"component":"example-comp-1"}`)}, + }}, + app: &v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "example-comp-1", + }, { + Name: "example-comp-2", + }}, + }, + }, + output: []v1beta1.WorkflowStep{{ + Name: "example-comp-1", + Type: "apply-component", + Properties: &runtime.RawExtension{Raw: []byte(`{"component":"example-comp-1"}`)}, + }}, + }, { + input: []v1beta1.WorkflowStep{}, + app: &v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "example-comp-1", + }, { + Name: "example-comp-2", + }}, + }, + }, + output: []v1beta1.WorkflowStep{{ + Name: "example-comp-1", + Type: "apply-component", + Properties: &runtime.RawExtension{Raw: []byte(`{"component":"example-comp-1"}`)}, + }, { + Name: "example-comp-2", + Type: "apply-component", + Properties: &runtime.RawExtension{Raw: []byte(`{"component":"example-comp-2"}`)}, + }}, + }, { + input: []v1beta1.WorkflowStep{}, + app: &v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "example-comp-1", + }}, + Policies: []v1beta1.AppPolicy{{ + Name: "example-policy", + Type: v1alpha1.EnvBindingPolicyType, + Properties: &runtime.RawExtension{Raw: []byte(`bad value`)}, + }}, + }, + }, + hasError: true, + }, { + input: []v1beta1.WorkflowStep{}, + app: &v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "example-comp-1", + }}, + Policies: []v1beta1.AppPolicy{{ + Name: "example-policy", + Type: v1alpha1.EnvBindingPolicyType, + Properties: &runtime.RawExtension{Raw: []byte(`{"envs":[{"name":"env-1"},{"name":"env-2"}]}`)}, + }}, + }, + }, + output: []v1beta1.WorkflowStep{{ + Name: "deploy-example-policy-env-1", + Type: "deploy2env", + Properties: &runtime.RawExtension{Raw: []byte(`{"env":"env-1","policy":"example-policy"}`)}, + }, { + Name: "deploy-example-policy-env-2", + Type: "deploy2env", + Properties: &runtime.RawExtension{Raw: []byte(`{"env":"env-2","policy":"example-policy"}`)}, + }}, + }} + generator := NewChainWorkflowStepGenerator( + &Deploy2EnvWorkflowStepGenerator{}, + &ApplyComponentWorkflowStepGenerator{}, + ) + r := require.New(t) + for _, testCase := range testCases { + output, err := generator.Generate(testCase.app, testCase.input) + if testCase.hasError { + r.Error(err) + continue + } + r.NoError(err) + r.Equal(testCase.output, output) + } +} diff --git a/pkg/workflow/workflow.go b/pkg/workflow/workflow.go index e3fd34cec..2b862591d 100644 --- a/pkg/workflow/workflow.go +++ b/pkg/workflow/workflow.go @@ -165,7 +165,7 @@ func (w *workflow) makeContext(appName string) (wfCtx wfContext.Context, err err return } - wfCtx, err = wfContext.NewEmptyContext(w.cli, w.app.Namespace, appName) + wfCtx, err = wfContext.NewContext(w.cli, w.app.Namespace, appName, w.app.GetUID()) if err != nil { err = errors.WithMessage(err, "new context") diff --git a/pkg/workflow/workflow_test.go b/pkg/workflow/workflow_test.go index e1635a3b3..e58bfc990 100644 --- a/pkg/workflow/workflow_test.go +++ b/pkg/workflow/workflow_test.go @@ -375,6 +375,7 @@ var _ = Describe("Test Workflow", func() { func makeTestCase(steps []oamcore.WorkflowStep) (*oamcore.Application, []wfTypes.TaskRunner) { app := &oamcore.Application{ + ObjectMeta: metav1.ObjectMeta{UID: "test-uid"}, Spec: oamcore.ApplicationSpec{ Workflow: &oamcore.Workflow{ Steps: steps, diff --git a/references/apis/types.go b/references/apis/types.go index ee1e93b04..97cdc8481 100644 --- a/references/apis/types.go +++ b/references/apis/types.go @@ -96,8 +96,9 @@ type CapabilityMeta struct { CapabilityCenterName string `json:"capabilityCenterName,omitempty"` } -// CapabilityCenterMeta used for dashboard restful API server -type CapabilityCenterMeta struct { - Name string `json:"name"` - URL string `json:"url"` +// RegistryConfig is used to store registry config in file +type RegistryConfig struct { + Name string `json:"name"` + URL string `json:"url"` + Token string `json:"token"` } diff --git a/references/appfile/app.go b/references/appfile/app.go index aa1397b49..4685305dc 100644 --- a/references/appfile/app.go +++ b/references/appfile/app.go @@ -84,7 +84,7 @@ func LoadApplication(namespace, appName string, c common.Args) (*v1beta1.Applica return app, nil } -// GetComponents will get oam components from Appfile. +// GetComponents will get oam components from v1beta1.Application. func GetComponents(app *v1beta1.Application) []string { var components []string for _, cmp := range app.Spec.Components { diff --git a/references/cli/capability.go b/references/cli/capability.go deleted file mode 100644 index c42620790..000000000 --- a/references/cli/capability.go +++ /dev/null @@ -1,292 +0,0 @@ -/* -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 cli - -import ( - "errors" - "fmt" - "strings" - - "github.com/spf13/cobra" - - "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" - common2 "github.com/oam-dev/kubevela/pkg/utils/common" - cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" - "github.com/oam-dev/kubevela/references/common" -) - -// CapabilityCommandGroup commands for capability center -func CapabilityCommandGroup(c common2.Args, ioStream cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "cap", - Short: "Manage capability centers and installing/uninstalling capabilities", - Long: "Manage capability centers and installing/uninstalling capabilities", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return c.SetConfig() - }, - Annotations: map[string]string{ - types.TagCommandType: types.TypeCap, - }, - } - cmd.AddCommand( - NewCenterCommand(ioStream), - NewCapListCommand(c, ioStream), - NewCapInstallCommand(c, ioStream), - NewCapUninstallCommand(c, ioStream), - ) - return cmd -} - -// NewCenterCommand Manage Capability Center -func NewCenterCommand(ioStream cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "center ", - Short: "Manage Capability Center", - Long: "Manage Capability Center with config, sync, list", - } - cmd.AddCommand( - NewCapCenterConfigCommand(ioStream), - NewCapCenterSyncCommand(ioStream), - NewCapCenterListCommand(ioStream), - NewCapCenterRemoveCommand(ioStream), - ) - return cmd -} - -// NewCapCenterConfigCommand Configure (add if not exist) a capability center, default is local (built-in capabilities) -func NewCapCenterConfigCommand(ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "config ", - Short: "Configure (add if not exist) a capability center, default is local (built-in capabilities)", - Long: "Configure (add if not exist) a capability center, default is local (built-in capabilities)", - Example: `vela cap center config mycenter https://github.com/oam-dev/catalog/tree/master/registry`, - RunE: func(cmd *cobra.Command, args []string) error { - argsLength := len(args) - if argsLength < 2 { - return errors.New("please set capability center with and ") - } - capName := args[0] - capURL := args[1] - token := cmd.Flag("token").Value.String() - if err := common.AddCapabilityCenter(capName, capURL, token); err != nil { - return err - } - ioStreams.Infof("Successfully configured capability center %s and sync from remote\n", capName) - return nil - }, - } - AddTokenVarFlags(cmd) - return cmd -} - -// NewCapInstallCommand Install capability into cluster -func NewCapInstallCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "install
/", - Short: "Install capability into cluster", - Long: "Install capability into cluster", - Example: `vela cap install mycenter/route`, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return c.SetConfig() - }, - RunE: func(cmd *cobra.Command, args []string) error { - var err error - argsLength := len(args) - if argsLength < 1 { - return errors.New("you must specify
/ for capability you want to install") - } - newClient, err := c.GetClient() - if err != nil { - return err - } - mapper, err := discoverymapper.New(c.Config) - if err != nil { - return err - } - if _, err = common.AddCapabilityIntoCluster(newClient, mapper, args[0]); err != nil { - return err - } - return nil - }, - } - AddTokenVarFlags(cmd) - return cmd -} - -// NewCapUninstallCommand Uninstall capability from cluster -func NewCapUninstallCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "uninstall ", - Short: "Uninstall capability from cluster", - Long: "Uninstall capability from cluster", - Example: `vela cap uninstall route`, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return c.SetConfig() - }, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return errors.New("you must specify for capability you want to uninstall") - } - newClient, err := c.GetClient() - if err != nil { - return err - } - name := args[0] - if strings.Contains(name, "/") { - l := strings.Split(name, "/") - if len(l) > 2 { - return fmt.Errorf("invalid format '%s', you can't contain more than one / in name", name) - } - name = l[1] - } - env, err := GetFlagEnvOrCurrent(cmd, c) - if err != nil { - return err - } - return common.RemoveCapability(env.Namespace, c, newClient, name, ioStreams) - }, - } - AddTokenVarFlags(cmd) - return cmd -} - -// NewCapCenterSyncCommand Sync capabilities from remote center, default to sync all centers -func NewCapCenterSyncCommand(ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "sync [centerName]", - Short: "Sync capabilities from remote center, default to sync all centers", - Long: "Sync capabilities from remote center, default to sync all centers", - Example: `vela cap center sync mycenter`, - RunE: func(cmd *cobra.Command, args []string) error { - var specified string - if len(args) > 0 { - specified = args[0] - } - if err := common.SyncCapabilityCenter(specified); err != nil { - return err - } - ioStreams.Info("sync finished") - return nil - }, - } - return cmd -} - -// NewCapListCommand List capabilities from cap-center -func NewCapListCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "ls [cap-center]", - Short: "List capabilities from cap-center", - Long: "List capabilities from cap-center", - Example: `vela cap ls`, - RunE: func(cmd *cobra.Command, args []string) error { - var repoName string - if len(args) > 0 { - repoName = args[0] - } - env, err := GetFlagEnvOrCurrent(cmd, c) - if err != nil { - return err - } - - err = printCenterCapabilities(env.Namespace, repoName, c, ioStreams, nil, "") - if err != nil { - return err - } - return nil - }, - } - return cmd -} - -// NewCapCenterListCommand List all capability centers -func NewCapCenterListCommand(ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "ls", - Short: "List all capability centers", - Long: "List all configured capability centers", - Example: `vela cap center ls`, - RunE: func(cmd *cobra.Command, args []string) error { - return listCapCenters(ioStreams) - }, - } - return cmd -} - -// NewCapCenterRemoveCommand Remove specified capability center -func NewCapCenterRemoveCommand(ioStreams cmdutil.IOStreams) *cobra.Command { - cmd := &cobra.Command{ - Use: "remove ", - Short: "Remove specified capability center", - Long: "Remove specified capability center", - Example: "vela cap center remove mycenter", - RunE: func(cmd *cobra.Command, args []string) error { - return removeCapCenter(args, ioStreams) - }, - } - return cmd -} - -func listCapCenters(ioStreams cmdutil.IOStreams) error { - table := newUITable() - table.MaxColWidth = 80 - table.AddRow("NAME", "ADDRESS") - capabilityCenterList, err := common.ListCapabilityCenters() - if err != nil { - return err - } - for _, c := range capabilityCenterList { - table.AddRow(c.Name, c.URL) - } - ioStreams.Info(table.String()) - return nil -} - -func removeCapCenter(args []string, ioStreams cmdutil.IOStreams) error { - if len(args) < 1 { - return errors.New("you must specify for capability center you want to remove") - } - centerName := args[0] - msg, err := common.RemoveCapabilityCenter(centerName) - if err == nil { - ioStreams.Info(msg) - } - return err -} -func printCenterCapabilities(namespace, repoName string, args common2.Args, ioStreams cmdutil.IOStreams, option *types.CapType, label string) error { - capabilityList, err := common.ListCapabilities(namespace, args, repoName) - if err != nil { - return err - } - table := newUITable() - table.AddRow("NAME", "CENTER", "TYPE", "DEFINITION", "STATUS", "APPLIES-TO") - - for _, c := range capabilityList { - if label != "" && !common.CheckLabelExistence(c.Labels, label) { - continue - } - if option == nil { - table.AddRow(c.Name, c.Center, c.Type, c.CrdName, c.Status, c.AppliesTo) - } - if option != nil && c.Type == *option { - table.AddRow(c.Name, c.Center, c.Type, c.CrdName, c.Status, c.AppliesTo) - } - } - ioStreams.Info(table.String()) - return nil -} diff --git a/references/cli/cli.go b/references/cli/cli.go index 1fa243357..840b32381 100644 --- a/references/cli/cli.go +++ b/references/cli/cli.go @@ -91,10 +91,9 @@ func NewCommand() *cobra.Command { // Workflows NewWorkflowCommand(commandArgs, ioStream), - // Capabilities - CapabilityCommandGroup(commandArgs, ioStream), + NewRegistryCommand(ioStream), NewTemplateCommand(ioStream), - NewTraitsCommand(commandArgs, ioStream), + NewTraitCommand(commandArgs, ioStream), NewComponentsCommand(commandArgs, ioStream), NewWorkloadsCommand(commandArgs, ioStream), DefinitionCommandGroup(commandArgs), diff --git a/references/cli/cluster.go b/references/cli/cluster.go index 570e6df2e..209f6331a 100644 --- a/references/cli/cluster.go +++ b/references/cli/cluster.go @@ -18,14 +18,29 @@ package cli import ( "context" + "fmt" - v1alpha12 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" "github.com/pkg/errors" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" + v13 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + errors2 "k8s.io/apimachinery/pkg/api/errors" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + types2 "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + ocmclusterv1 "open-cluster-management.io/api/cluster/v1" "sigs.k8s.io/controller-runtime/pkg/client" + v1alpha12 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" + "github.com/oam-dev/cluster-gateway/pkg/generated/clientset/versioned" + "github.com/oam-dev/cluster-register/pkg/hub" + "github.com/oam-dev/cluster-register/pkg/spoke" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/clustermanager" "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/utils/common" "github.com/oam-dev/kubevela/references/a/preimport" @@ -34,6 +49,15 @@ import ( const ( // FlagClusterName specifies the cluster name FlagClusterName = "name" + // FlagClusterManagementEngine specifies the cluster management type, eg: ocm + FlagClusterManagementEngine = "engine" + // FlagKubeConfigPath specifies the kubeconfig path + FlagKubeConfigPath = "kubeconfig-path" + + // ClusterGateWayClusterManagement cluster-gateway cluster management solution + ClusterGateWayClusterManagement = "cluster-gateway" + // OCMClusterManagement ocm cluster management solution + OCMClusterManagement = "ocm" ) // ClusterCommandGroup create a group of cluster command @@ -73,6 +97,7 @@ func ClusterCommandGroup(c common.Args) *cobra.Command { NewClusterJoinCommand(&c), NewClusterRenameCommand(&c), NewClusterDetachCommand(&c), + NewClusterProbeCommand(&c), ) return cmd } @@ -86,13 +111,13 @@ func NewClusterListCommand(c *common.Args) *cobra.Command { Long: "list child clusters managed by KubeVela", Args: cobra.ExactValidArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - secrets := v1.SecretList{} - if err := c.Client.List(context.Background(), &secrets, client.HasLabels{v1alpha12.LabelKeyClusterCredentialType}, client.InNamespace(multicluster.ClusterGatewaySecretNamespace)); err != nil { - return errors.Wrapf(err, "failed to get cluster secrets") - } table := newUITable().AddRow("CLUSTER", "TYPE", "ENDPOINT") - for _, secret := range secrets.Items { - table.AddRow(secret.Name, secret.GetLabels()[v1alpha12.LabelKeyClusterCredentialType], string(secret.Data["endpoint"])) + clusters, err := clustermanager.GetRegisteredClusters(c.Client) + if err != nil { + return errors.Wrap(err, "fail to get registered cluster") + } + for _, cluster := range clusters { + table.AddRow(cluster.Name, cluster.Type, cluster.EndPoint) } if len(table.Rows) == 1 { cmd.Println("No managed cluster found.") @@ -105,6 +130,30 @@ func NewClusterListCommand(c *common.Args) *cobra.Command { return cmd } +func ensureResourceTrackerCRDInstalled(c client.Client, clusterName string) error { + ctx := context.Background() + remoteCtx := multicluster.ContextWithClusterName(ctx, clusterName) + crdName := types2.NamespacedName{Name: "resourcetrackers." + v1beta1.Group} + if err := c.Get(remoteCtx, crdName, &v13.CustomResourceDefinition{}); err != nil { + if !errors2.IsNotFound(err) { + return errors.Wrapf(err, "failed to check resourcetracker crd in cluster %s", clusterName) + } + crd := &v13.CustomResourceDefinition{} + if err = c.Get(ctx, crdName, crd); err != nil { + return errors.Wrapf(err, "failed to get resourcetracker crd in hub cluster") + } + crd.ObjectMeta = v12.ObjectMeta{ + Name: crdName.Name, + Annotations: crd.Annotations, + Labels: crd.Labels, + } + if err = c.Create(remoteCtx, crd); err != nil { + return errors.Wrapf(err, "failed to create resourcetracker crd in cluster %s", clusterName) + } + } + return nil +} + // NewClusterJoinCommand create command to help user join cluster to multicluster management func NewClusterJoinCommand(c *common.Args) *cobra.Command { cmd := &cobra.Command{ @@ -115,23 +164,162 @@ func NewClusterJoinCommand(c *common.Args) *cobra.Command { "> vela cluster join my-child-cluster.kubeconfig --name example-cluster", Args: cobra.ExactValidArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + config, err := clientcmd.LoadFromFile(args[0]) + if err != nil { + return errors.Wrapf(err, "failed to get kubeconfig") + } + if len(config.CurrentContext) == 0 { + return fmt.Errorf("current-context is not set") + } + ctx, ok := config.Contexts[config.CurrentContext] + if !ok { + return fmt.Errorf("current-context %s not found", config.CurrentContext) + } + cluster, ok := config.Clusters[ctx.Cluster] + if !ok { + return fmt.Errorf("cluster %s not found", ctx.Cluster) + } + authInfo, ok := config.AuthInfos[ctx.AuthInfo] + if !ok { + return fmt.Errorf("authInfo %s not found", ctx.AuthInfo) + } // get ClusterName from flag or config clusterName, err := cmd.Flags().GetString(FlagClusterName) if err != nil { return errors.Wrapf(err, "failed to get cluster name flag") } - cluster, err := multicluster.JoinClusterByKubeConfig(context.Background(), c.Client, args[0], clusterName) + if clusterName == "" { + clusterName = ctx.Cluster + } + if clusterName == multicluster.ClusterLocalName { + return fmt.Errorf("cannot use `%s` as cluster name, it is reserved as the local cluster", multicluster.ClusterLocalName) + } + + clusterManagementType, err := cmd.Flags().GetString(FlagClusterManagementEngine) if err != nil { - return err + return errors.Wrapf(err, "failed to get cluster management type flag") + } + + if clusterManagementType == "" { + clusterManagementType = ClusterGateWayClusterManagement + } + + switch clusterManagementType { + case ClusterGateWayClusterManagement: + if err = registerClusterManagedByVela(c.Client, cluster, authInfo, clusterName); err != nil { + return err + } + case OCMClusterManagement: + if err = registerClusterManagedByOCM(c.Config, config, clusterName); err != nil { + return err + } } cmd.Printf("Successfully add cluster %s, endpoint: %s.\n", clusterName, cluster.Server) return nil }, } cmd.Flags().StringP(FlagClusterName, "n", "", "Specify the cluster name. If empty, it will use the cluster name in config file. Default to be empty.") + cmd.Flags().StringP(FlagClusterManagementEngine, "t", "", "Specify the cluster management engine. If empty, it will use cluster-gateway cluster management solution. Default to be empty.") return cmd } +func registerClusterManagedByVela(k8sClient client.Client, cluster *clientcmdapi.Cluster, authInfo *clientcmdapi.AuthInfo, clusterName string) error { + if err := clustermanager.EnsureClusterNotExists(k8sClient, clusterName); err != nil { + return errors.Wrapf(err, "cannot use cluster name %s", clusterName) + } + var credentialType v1alpha12.CredentialType + data := map[string][]byte{ + "endpoint": []byte(cluster.Server), + "ca.crt": cluster.CertificateAuthorityData, + } + if len(authInfo.Token) > 0 { + credentialType = v1alpha12.CredentialTypeServiceAccountToken + data["token"] = []byte(authInfo.Token) + } else { + credentialType = v1alpha12.CredentialTypeX509Certificate + data["tls.crt"] = authInfo.ClientCertificateData + data["tls.key"] = authInfo.ClientKeyData + } + secret := &v1.Secret{ + ObjectMeta: v12.ObjectMeta{ + Name: clusterName, + Namespace: multicluster.ClusterGatewaySecretNamespace, + Labels: map[string]string{ + v1alpha12.LabelKeyClusterCredentialType: string(credentialType), + }, + }, + Type: v1.SecretTypeOpaque, + Data: data, + } + if err := k8sClient.Create(context.Background(), secret); err != nil { + return errors.Wrapf(err, "failed to add cluster to kubernetes") + } + if err := ensureResourceTrackerCRDInstalled(k8sClient, clusterName); err != nil { + _ = k8sClient.Delete(context.Background(), secret) + return errors.Wrapf(err, "failed to ensure resourcetracker crd installed in cluster %s", clusterName) + } + return nil +} + +func registerClusterManagedByOCM(hubConfig *rest.Config, spokeConfig *clientcmdapi.Config, clusterName string) error { + ctx := context.Background() + hubCluster, err := hub.NewHubCluster(hubConfig) + if err != nil { + return errors.Wrap(err, "fail to create client connect to hub cluster") + } + + crdName := types2.NamespacedName{Name: "managedclusters." + ocmclusterv1.GroupName} + if err := hubCluster.Client.Get(context.Background(), crdName, &v13.CustomResourceDefinition{}); err != nil { + return err + } + + clusters, err := clustermanager.GetRegisteredClusters(hubCluster.Client) + if err != nil { + return err + } + + for _, cluster := range clusters { + if cluster.Name == clusterName { + return errors.Errorf("you have register a cluster named %s", clusterName) + } + } + + spokeRestConf, err := clientcmd.BuildConfigFromKubeconfigGetter("", func() (*clientcmdapi.Config, error) { + return spokeConfig, nil + }) + if err != nil { + return errors.Wrap(err, "fail to convert spoke-cluster kubeconfig") + } + + hubKubeToken, err := hubCluster.GenerateHubClusterKubeConfig(ctx, "") + if err != nil { + return errors.Wrap(err, "fail to generate the token for spoke-cluster") + } + + spokeCluster, err := spoke.NewSpokeCluster(clusterName, spokeRestConf, hubKubeToken) + if err != nil { + return errors.Wrap(err, "fail to connect spoke cluster") + } + + err = spokeCluster.InitSpokeClusterEnv(ctx) + if err != nil { + return errors.Wrap(err, "fail to prepare the env for spoke-cluster") + } + + csrCheck := newTrackingSpinner("wait for managed-cluster register request ...") + csrCheck.Start() + defer csrCheck.Stop() + ready, err := hubCluster.Wait4SpokeClusterReady(ctx, clusterName) + if err != nil || !ready { + return errors.Errorf("fail to waiting for register request") + } + + if err = hubCluster.RegisterSpokeCluster(ctx, spokeCluster.Name); err != nil { + return errors.Wrap(err, "fail to approve spoke cluster") + } + return nil +} + // NewClusterRenameCommand create command to help user rename cluster func NewClusterRenameCommand(c *common.Args) *cobra.Command { cmd := &cobra.Command{ @@ -141,8 +329,27 @@ func NewClusterRenameCommand(c *common.Args) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { oldClusterName := args[0] newClusterName := args[1] - if err := multicluster.RenameCluster(context.Background(), c.Client, oldClusterName, newClusterName); err != nil { - return err + if newClusterName == multicluster.ClusterLocalName { + return fmt.Errorf("cannot use `%s` as cluster name, it is reserved as the local cluster", multicluster.ClusterLocalName) + } + clusterSecret, err := multicluster.GetMutableClusterSecret(context.Background(), c.Client, oldClusterName) + if err != nil { + return errors.Wrapf(err, "cluster %s is not mutable now", oldClusterName) + } + if err := clustermanager.EnsureClusterNotExists(c.Client, newClusterName); err != nil { + return errors.Wrapf(err, "cannot set cluster name to %s", newClusterName) + } + if err := c.Client.Delete(context.Background(), clusterSecret); err != nil { + return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName) + } + clusterSecret.ObjectMeta = v12.ObjectMeta{ + Name: newClusterName, + Namespace: multicluster.ClusterGatewaySecretNamespace, + Labels: clusterSecret.Labels, + Annotations: clusterSecret.Annotations, + } + if err := c.Client.Create(context.Background(), clusterSecret); err != nil { + return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName) } cmd.Printf("Rename cluster %s to %s successfully.\n", oldClusterName, newClusterName) return nil @@ -159,12 +366,90 @@ func NewClusterDetachCommand(c *common.Args) *cobra.Command { Args: cobra.ExactValidArgs(1), RunE: func(cmd *cobra.Command, args []string) error { clusterName := args[0] - if err := multicluster.DetachCluster(context.Background(), c.Client, clusterName); err != nil { + if clusterName == multicluster.ClusterLocalName { + return fmt.Errorf("cannot delete `%s` cluster, it is reserved as the local cluster", multicluster.ClusterLocalName) + } + clusters, err := clustermanager.GetRegisteredClusters(c.Client) + if err != nil { return err } + var clusterType string + for _, cluster := range clusters { + if cluster.Name == clusterName { + clusterType = cluster.Type + } + } + if clusterType == "" { + return errors.Errorf("cluster %s is not regitsered", clusterName) + } + + switch clusterType { + case string(v1alpha12.CredentialTypeX509Certificate), string(v1alpha12.CredentialTypeServiceAccountToken): + clusterSecret, err := multicluster.GetMutableClusterSecret(context.Background(), c.Client, clusterName) + if err != nil { + return errors.Wrapf(err, "cluster %s is not mutable now", clusterName) + } + if err := c.Client.Delete(context.Background(), clusterSecret); err != nil { + return errors.Wrapf(err, "failed to detach cluster %s", clusterName) + } + case "ManagedCluster": + configPath, err := cmd.Flags().GetString(FlagKubeConfigPath) + if err != nil { + return errors.Wrapf(err, "failed to get cluster management type flag") + } + if configPath == "" { + return errors.New("kubeconfig-path shouldn't be empty") + } + config, err := clientcmd.LoadFromFile(configPath) + if err != nil { + return err + } + restConfig, err := clientcmd.BuildConfigFromKubeconfigGetter("", func() (*clientcmdapi.Config, error) { + return config, nil + }) + if err != nil { + return err + } + if err = spoke.CleanSpokeClusterEnv(restConfig); err != nil { + return err + } + managedCluster := ocmclusterv1.ManagedCluster{ + ObjectMeta: v12.ObjectMeta{ + Name: clusterName, + }, + } + if err = c.Client.Delete(context.Background(), &managedCluster); err != nil { + if !errors2.IsNotFound(err) { + return err + } + } + } cmd.Printf("Detach cluster %s successfully.\n", clusterName) return nil }, } + cmd.Flags().StringP(FlagKubeConfigPath, "p", "", "Specify the kubeconfig path of managed cluster. If you use ocm to manage your cluster, you must set the kubeconfig-path.") + return cmd +} + +// NewClusterProbeCommand create command to help user try health probe for existing cluster +func NewClusterProbeCommand(c *common.Args) *cobra.Command { + cmd := &cobra.Command{ + Use: "probe [CLUSTER_NAME]", + Short: "probe managed cluster", + Args: cobra.ExactValidArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clusterName := args[0] + if clusterName == multicluster.ClusterLocalName { + return errors.New("you must specify a remote cluster name") + } + content, err := versioned.NewForConfigOrDie(c.Config).ClusterV1alpha1().ClusterGateways().RESTClient(clusterName).Get().AbsPath("healthz").DoRaw(context.TODO()) + if err != nil { + return errors.Wrapf(err, "failed connect cluster %s", clusterName) + } + cmd.Printf("Connect to cluster %s successfully.\n%s\n", clusterName, string(content)) + return nil + }, + } return cmd } diff --git a/references/cli/components.go b/references/cli/components.go index cc08d79d8..8ba5d99a9 100644 --- a/references/cli/components.go +++ b/references/cli/components.go @@ -18,9 +18,10 @@ package cli import ( "context" - "fmt" + "encoding/json" "strings" + "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -30,97 +31,117 @@ import ( core "github.com/oam-dev/kubevela/apis/core.oam.dev" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" - oamutil "github.com/oam-dev/kubevela/pkg/oam/util" common2 "github.com/oam-dev/kubevela/pkg/utils/common" cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" "github.com/oam-dev/kubevela/references/common" - "github.com/oam-dev/kubevela/references/plugins" ) // NewComponentsCommand creates `components` command func NewComponentsCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { + var isDiscover bool cmd := &cobra.Command{ - Use: "components", - Aliases: []string{"comp", "component"}, - DisableFlagsInUseLine: true, - Short: "List components", - Long: "List components", - Example: `vela components`, + Use: "components", + Aliases: []string{"comp", "component"}, + Short: "List/get components", + Long: "List components & get components in registry", + Example: `vela comp`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return c.SetConfig() }, RunE: func(cmd *cobra.Command, args []string) error { - isDiscover, _ := cmd.Flags().GetBool("discover") - env, err := GetFlagEnvOrCurrent(cmd, c) - if err != nil { - return err + // parse label filter + if label != "" { + words := strings.Split(label, "=") + if len(words) < 2 { + return errors.New("label is invalid") + } + filter = createLabelFilter(words[0], words[1]) } - label, err := cmd.Flags().GetString(types.LabelArg) - if err != nil { - return err + var registry Registry + var err error + if isDiscover { + if regURL != "" { + ioStreams.Infof("Listing component definition from url: %s\n", regURL) + registry, err = NewRegistry(context.Background(), token, "temporary-registry", regURL) + if err != nil { + return errors.Wrap(err, "creating registry err, please check registry url") + } + } else { + ioStreams.Infof("Listing component definition from registry: %s\n", regName) + registry, err = GetRegistry(regName) + if err != nil { + return errors.Wrap(err, "get registry err") + } + } + return PrintComponentListFromRegistry(registry, ioStreams, filter) } - if label != "" && len(strings.Split(label, "=")) != 2 { - return fmt.Errorf("label %s is not in the right format", label) - } - - if !isDiscover { - return printComponentList(env.Namespace, c, ioStreams, label) - } - option := types.TypeComponentDefinition - err = printCenterCapabilities(env.Namespace, "", c, ioStreams, &option, label) - if err != nil { - return err - } - - return nil + return PrintInstalledCompDef(ioStreams, filter) }, Annotations: map[string]string{ types.TagCommandType: types.TypeCap, }, } - cmd.Flags().Bool("discover", false, "discover traits in capability centers") - cmd.Flags().String(types.LabelArg, "", "a label to filter components, the format is `--label type=terraform`") + cmd.SetOut(ioStreams.Out) + cmd.AddCommand( + NewCompGetCommand(c, ioStreams), + ) + cmd.Flags().BoolVar(&isDiscover, "discover", false, "discover traits in registries") + cmd.PersistentFlags().StringVar(®URL, "url", "", "specify the registry URL") + cmd.PersistentFlags().StringVar(®Name, "registry", DefaultRegistry, "specify the registry name") + cmd.PersistentFlags().StringVar(&token, "token", "", "specify token when using --url to specify registry url") + cmd.Flags().StringVar(&label, types.LabelArg, "", "a label to filter components, the format is `--label type=terraform`") cmd.SetOut(ioStreams.Out) return cmd } -func printComponentList(userNamespace string, c common2.Args, ioStreams cmdutil.IOStreams, label string) error { - def, err := common.ListRawComponentDefinitions(userNamespace, c) - if err != nil { - return err - } - - dm, err := c.GetDiscoveryMapper() - if err != nil { - return fmt.Errorf("get discoveryMapper error %w", err) - } - - table := newUITable() - table.AddRow("NAME", "NAMESPACE", "WORKLOAD", "DESCRIPTION") - - for _, r := range def { - if label != "" && !common.CheckLabelExistence(r.Labels, label) { - continue - } - var workload string - if r.Spec.Workload.Type != "" { - workload = r.Spec.Workload.Type - } else { - definition, err := oamutil.ConvertWorkloadGVK2Definition(dm, r.Spec.Workload.Definition) - if err != nil { - return fmt.Errorf("get workload definitionReference error %w", err) +// NewCompGetCommand creates `comp get` command +func NewCompGetCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "get ", + Short: "get component from registry", + Long: "get component from registry", + Example: "vela comp get ", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + ioStreams.Error("you must specify a component name") + return nil } - workload = definition.Name - } - table.AddRow(r.Name, r.Namespace, workload, plugins.GetDescription(r.Annotations)) + name := args[0] + var registry Registry + var err error + + if regURL != "" { + ioStreams.Infof("Getting component definition from url: %s\n", regURL) + registry, err = NewRegistry(context.Background(), token, "temporary-registry", regURL) + if err != nil { + return errors.Wrap(err, "creating registry err, please check registry url") + } + } else { + ioStreams.Infof("Getting component definition from registry: %s\n", regName) + registry, err = GetRegistry(regName) + if err != nil { + return errors.Wrap(err, "get registry err") + } + } + return errors.Wrap(InstallCompByNameFromRegistry(c, ioStreams, name, registry), "install component definition err") + + }, + } + return cmd +} + +// filterFunc to filter whether to print the capability +type filterFunc func(capability types.Capability) bool + +func createLabelFilter(key, value string) filterFunc { + return func(capability types.Capability) bool { + return capability.Labels[key] == value } - ioStreams.Info(table.String()) - return nil } // PrintComponentListFromRegistry print a table which shows all components from registry -func PrintComponentListFromRegistry(isDiscover bool, url string, ioStreams cmdutil.IOStreams) error { +func PrintComponentListFromRegistry(registry Registry, ioStreams cmdutil.IOStreams, filter filterFunc) error { var scheme = runtime.NewScheme() err := core.AddToScheme(scheme) if err != nil { @@ -135,8 +156,7 @@ func PrintComponentListFromRegistry(isDiscover bool, url string, ioStreams cmdut return err } - _, _ = ioStreams.Out.Write([]byte(fmt.Sprintf("Showing components from registry: %s\n", url))) - caps, err := getCapsFromRegistry(url) + caps, err := registry.ListCaps() if err != nil { return err } @@ -147,12 +167,12 @@ func PrintComponentListFromRegistry(isDiscover bool, url string, ioStreams cmdut return err } table := newUITable() - if isDiscover { - table.AddRow("NAME", "REGISTRY", "DEFINITION") - } else { - table.AddRow("NAME", "DEFINITION") - } + table.AddRow("NAME", "REGISTRY", "DEFINITION", "STATUS") for _, c := range caps { + + if filter != nil && !filter(c) { + continue + } c.Status = uninstalled if c.Type != types.TypeComponentDefinition { continue @@ -163,26 +183,16 @@ func PrintComponentListFromRegistry(isDiscover bool, url string, ioStreams cmdut } } - if c.Status == uninstalled && isDiscover { - table.AddRow(c.Name, "default", c.CrdName) - } - if c.Status == installed && !isDiscover { - table.AddRow(c.Name, c.CrdName) - } + table.AddRow(c.Name, "default", c.CrdName, c.Status) } ioStreams.Info(table.String()) return nil } -// InstallCompByName will install given componentName comp to cluster from registry -func InstallCompByName(args common2.Args, ioStream cmdutil.IOStreams, compName, regURL string) error { - - g, err := plugins.NewRegistry(context.Background(), "", "url-registry", regURL) - if err != nil { - return err - } - capObj, data, err := g.GetCap(compName) +// InstallCompByNameFromRegistry will install given componentName comp to cluster from registry +func InstallCompByNameFromRegistry(args common2.Args, ioStream cmdutil.IOStreams, compName string, registry Registry) error { + capObj, data, err := registry.GetCap(compName) if err != nil { return err } @@ -201,3 +211,38 @@ func InstallCompByName(args common2.Args, ioStream cmdutil.IOStreams, compName, return nil } + +// PrintInstalledCompDef will print all ComponentDefinition in cluster +func PrintInstalledCompDef(io cmdutil.IOStreams, filter filterFunc) error { + var list v1beta1.ComponentDefinitionList + err := clt.List(context.Background(), &list) + if err != nil { + return errors.Wrap(err, "get component definition list error") + } + dm, err := (&common2.Args{}).GetDiscoveryMapper() + if err != nil { + return errors.Wrap(err, "get discovery mapper error") + } + + table := newUITable() + table.AddRow("NAME", "DEFINITION") + + for _, cd := range list.Items { + data, err := json.Marshal(cd) + if err != nil { + io.Infof("error encoding definition: %s\n", cd.Name) + continue + } + capa, err := ParseCapability(dm, data) + if err != nil { + io.Errorf("error parsing capability: %s\n", cd.Name) + continue + } + if filter != nil && !filter(capa) { + continue + } + table.AddRow(capa.Name, capa.CrdName) + } + io.Infof(table.String()) + return nil +} diff --git a/references/cli/exec.go b/references/cli/exec.go index 9eef48fa8..be0141d61 100644 --- a/references/cli/exec.go +++ b/references/cli/exec.go @@ -22,9 +22,9 @@ import ( "strings" "time" + "github.com/pkg/errors" "github.com/spf13/cobra" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" cmdexec "k8s.io/kubectl/pkg/cmd/exec" @@ -32,7 +32,7 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/utils/common" "github.com/oam-dev/kubevela/pkg/utils/util" "github.com/oam-dev/kubevela/references/appfile" @@ -47,20 +47,21 @@ const ( // VelaExecOptions creates options for `exec` command type VelaExecOptions struct { - Cmd *cobra.Command - Args []string - Stdin bool - TTY bool - ServiceName string + Cmd *cobra.Command + Args []string + Stdin bool + TTY bool - context.Context + Ctx context.Context VelaC common.Args Env *types.EnvMeta App *v1beta1.Application - f k8scmdutil.Factory - kcExecOptions *cmdexec.ExecOptions - ClientSet kubernetes.Interface + resourceName string + resourceNamespace string + f k8scmdutil.Factory + kcExecOptions *cmdexec.ExecOptions + ClientSet kubernetes.Interface } // NewExecCommand creates `exec` command @@ -82,8 +83,10 @@ func NewExecCommand(c common.Args, ioStreams util.IOStreams) *cobra.Command { Short: "Execute command in a container", Long: "Execute command in a container", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - if err := c.SetConfig(); err != nil { - return err + if c.Config == nil { + if err := c.SetConfig(); err != nil { + return errors.Wrapf(err, "failed to set config for k8s client") + } } o.VelaC = c return nil @@ -117,19 +120,26 @@ func NewExecCommand(c common.Args, ioStreams util.IOStreams) *cobra.Command { Annotations: map[string]string{ types.TagCommandType: types.TypeApp, }, + Example: ` + # Get output from running 'date' command from app pod, using the first container by default + vela exec my-app -- date + + # Switch to raw terminal mode, sends stdin to 'bash' in containers of application my-app + # and sends stdout/stderr from 'bash' back to the client + kubectl exec my-app -i -t -- bash -il + `, } cmd.Flags().BoolVarP(&o.Stdin, "stdin", "i", defaultStdin, "Pass stdin to the container") cmd.Flags().BoolVarP(&o.TTY, "tty", "t", defaultTTY, "Stdin is a TTY") cmd.Flags().Duration(podRunningTimeoutFlag, defaultPodExecTimeout, "The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running", ) - cmd.Flags().StringVarP(&o.ServiceName, "svc", "s", "", "service name") + return cmd } // Init prepares the arguments accepted by the Exec command func (o *VelaExecOptions) Init(ctx context.Context, c *cobra.Command, argsIn []string) error { - o.Context = ctx o.Cmd = c o.Args = argsIn @@ -144,27 +154,28 @@ func (o *VelaExecOptions) Init(ctx context.Context, c *cobra.Command, argsIn []s } o.App = app - cf := genericclioptions.NewConfigFlags(true) - cf.Namespace = &o.Env.Namespace - o.f = k8scmdutil.NewFactory(k8scmdutil.NewMatchVersionFlags(cf)) - - if o.ClientSet == nil { - c, err := kubernetes.NewForConfig(o.VelaC.Config) - if err != nil { - return err - } - o.ClientSet = c + targetResource, err := common.AskToChooseOneEnvResource(o.App) + if err != nil { + return err } + + cf := genericclioptions.NewConfigFlags(true) + cf.Namespace = &targetResource.Namespace + o.f = k8scmdutil.NewFactory(k8scmdutil.NewMatchVersionFlags(cf)) + o.resourceName = targetResource.Name + o.Ctx = multicluster.ContextWithClusterName(ctx, targetResource.Cluster) + o.resourceNamespace = targetResource.Namespace + k8sClient, err := kubernetes.NewForConfig(o.VelaC.Config) + if err != nil { + return err + } + o.ClientSet = k8sClient return nil } // Complete loads data from the command environment func (o *VelaExecOptions) Complete() error { - compName, err := o.getComponentName() - if err != nil { - return err - } - podName, err := o.getPodName(compName) + podName, err := o.getPodName(o.resourceName) if err != nil { return err } @@ -173,53 +184,27 @@ func (o *VelaExecOptions) Complete() error { args := make([]string, len(o.Args)) copy(args, o.Args) - // args for kcExecOptions MUST be in such formart: + // args for kcExecOptions MUST be in such format: // [podName, COMMAND...] args[0] = podName return o.kcExecOptions.Complete(o.f, o.Cmd, args, 1) } -func (o *VelaExecOptions) getComponentName() (string, error) { - svcName := o.ServiceName - - if svcName != "" { - for _, cc := range o.App.Spec.Components { - if cc.Name == svcName { - return svcName, nil - } - } - o.Cmd.Printf("The service name '%s' is not valid\n", svcName) - } - - compName, err := common.AskToChooseOneService(appfile.GetComponents(o.App)) +func (o *VelaExecOptions) getPodName(resourceName string) (string, error) { + podList, err := o.ClientSet.CoreV1().Pods(o.resourceNamespace).List(o.Ctx, v1.ListOptions{}) if err != nil { return "", err } - return compName, nil -} - -func (o *VelaExecOptions) getPodName(compName string) (string, error) { - podList, err := o.ClientSet.CoreV1().Pods(o.Env.Namespace).List(o.Context, v1.ListOptions{ - LabelSelector: labels.Set(map[string]string{ - // TODO(roywang) except core workloads, not any workloads will pass these label to pod - // find a rigorous way to get pod by compname - oam.LabelAppComponent: compName, - }).String(), - }) - if err != nil { - return "", err - } - if podList != nil && len(podList.Items) == 0 { - return "", fmt.Errorf("cannot get pods") - } + var pods []string for _, p := range podList.Items { - if strings.HasPrefix(p.Name, compName+"-") { - return p.Name, nil + if strings.HasPrefix(p.Name, resourceName) { + pods = append(pods, p.Name) } } - // if no pod with name matched prefix as component name - // just return the first one - return podList.Items[0].Name, nil + if len(pods) < 1 { + return "", fmt.Errorf("no pods found created by resource %s", resourceName) + } + return common.AskToChooseOnePods(pods) } // Run executes a validated remote execution against a pod diff --git a/references/cli/help.go b/references/cli/help.go index 29b7f1983..fd532480c 100644 --- a/references/cli/help.go +++ b/references/cli/help.go @@ -62,8 +62,3 @@ func PrintHelpByTag(cmd *cobra.Command, all []*cobra.Command, tag string) { cmd.Println(table.String()) cmd.Println() } - -// AddTokenVarFlags adds token flag to a command -func AddTokenVarFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringP("token", "t", "", "Github Repo token") -} diff --git a/references/cli/logs.go b/references/cli/logs.go index 89c818ad1..a2fe90f90 100644 --- a/references/cli/logs.go +++ b/references/cli/logs.go @@ -24,6 +24,8 @@ import ( "text/template" "time" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/fatih/color" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -40,73 +42,71 @@ import ( // NewLogsCommand creates `logs` command to tail logs of application func NewLogsCommand(c common.Args, ioStreams util.IOStreams) *cobra.Command { - largs := &Args{C: c} - cmd := &cobra.Command{} - cmd.Use = "logs" - cmd.Short = "Tail logs for application" - cmd.Long = "Tail logs for application" - cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { - if err := c.SetConfig(); err != nil { - return err - } - largs.C = c - return nil - } - cmd.RunE = func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - ioStreams.Errorf("please specify app name") + largs := &Args{Args: c} + cmd := &cobra.Command{ + Use: "logs ", + Short: "Tail logs for application in multicluster", + Long: "Tail logs for application in multicluster", + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := c.SetConfig(); err != nil { + return err + } + largs.Args = c + largs.Args.Config.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper) return nil - } - env, err := GetFlagEnvOrCurrent(cmd, c) - if err != nil { - return err - } - app, err := appfile.LoadApplication(env.Namespace, args[0], c) - if err != nil { - return err - } - largs.App = app - largs.Env = env - ctx := context.Background() - if err := largs.Run(ctx, ioStreams); err != nil { - return err - } - return nil - } - cmd.Annotations = map[string]string{ - types.TagCommandType: types.TypeApp, + }, + RunE: func(cmd *cobra.Command, args []string) error { + app, err := appfile.LoadApplication(largs.Namespace, args[0], c) + if err != nil { + return err + } + largs.App = app + ctx := context.Background() + if err := largs.Run(ctx, ioStreams); err != nil { + return err + } + return nil + }, + Annotations: map[string]string{ + types.TagCommandType: types.TypeApp, + }, } cmd.Flags().StringVarP(&largs.Output, "output", "o", "default", "output format for logs, support: [default, raw, json]") + cmd.Flags().StringVarP(&largs.Namespace, "namespace", "n", "default", "application namespace") + return cmd } // Args creates arguments for `logs` command type Args struct { - Output string - Env *types.EnvMeta - C common.Args - App *v1beta1.Application + Output string + Args common.Args + Namespace string + App *v1beta1.Application } // Run refer to the implementation at https://github.com/oam-dev/stern/blob/master/stern/main.go func (l *Args) Run(ctx context.Context, ioStreams util.IOStreams) error { - clientSet, err := kubernetes.NewForConfig(l.C.Config) + clientSet, err := kubernetes.NewForConfig(l.Args.Config) if err != nil { return err } - compName, err := common.AskToChooseOneService(appfile.GetComponents(l.App)) + + selectedRes, err := common.AskToChooseOneEnvResource(l.App) if err != nil { return err } + ctx = multicluster.ContextWithClusterName(ctx, selectedRes.Cluster) // TODO(wonderflow): we could get labels from service to narrow the pods scope selected labelSelector := labels.Everything() - pod, err := regexp.Compile(compName + "-.*") + pod, err := regexp.Compile(selectedRes.Name + "-.*") if err != nil { - return fmt.Errorf("fail to compile '%s' for logs query", compName+".*") + return fmt.Errorf("fail to compile '%s' for logs query", selectedRes.Name+".*") } container := regexp.MustCompile(".*") - namespace := l.Env.Namespace + namespace := selectedRes.Namespace added, removed, err := stern.Watch(ctx, clientSet.CoreV1().Pods(namespace), pod, container, nil, stern.RUNNING, labelSelector) if err != nil { return err diff --git a/references/cli/registry.go b/references/cli/registry.go new file mode 100644 index 000000000..cad5b2a21 --- /dev/null +++ b/references/cli/registry.go @@ -0,0 +1,773 @@ +/* +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 cli + +import ( + "context" + "encoding/base64" + "encoding/xml" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/google/go-github/v32/github" + "golang.org/x/oauth2" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/pkg/utils/system" + apis "github.com/oam-dev/kubevela/references/apis" + "github.com/oam-dev/kubevela/references/plugins" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" +) + +// NewRegistryCommand Manage Capability Center +func NewRegistryCommand(ioStream cmdutil.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "registry ", + Short: "Manage Registry", + Long: "Manage Registry with config, remove, list", + } + cmd.AddCommand( + NewRegistryConfigCommand(ioStream), + NewRegistryListCommand(ioStream), + NewRegistryRemoveCommand(ioStream), + ) + return cmd +} + +// NewRegistryListCommand List all registry +func NewRegistryListCommand(ioStreams cmdutil.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "ls", + Short: "List all registry", + Long: "List all configured registry", + Example: `vela registry ls`, + RunE: func(cmd *cobra.Command, args []string) error { + return listCapRegistrys(ioStreams) + }, + } + return cmd +} + +// NewRegistryConfigCommand Configure (add if not exist) a registry, default is local (built-in capabilities) +func NewRegistryConfigCommand(ioStreams cmdutil.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "config ", + Short: "Configure (add if not exist) a registry, default is local (built-in capabilities)", + Long: "Configure (add if not exist) a registry, default is local (built-in capabilities)", + Example: `vela registry config my-registry https://github.com/oam-dev/catalog/tree/master/registry`, + RunE: func(cmd *cobra.Command, args []string) error { + argsLength := len(args) + if argsLength < 2 { + return errors.New("please set registry with and ") + } + capName := args[0] + capURL := args[1] + token := cmd.Flag("token").Value.String() + if err := addRegistry(capName, capURL, token); err != nil { + return err + } + ioStreams.Infof("Successfully configured registry %s\n", capName) + return nil + }, + } + cmd.PersistentFlags().StringP("token", "t", "", "Github Repo token") + return cmd +} + +// NewRegistryRemoveCommand Remove specified registry +func NewRegistryRemoveCommand(ioStreams cmdutil.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Aliases: []string{"rm"}, + Use: "remove ", + Short: "Remove specified registry", + Long: "Remove specified registry", + Example: "vela registry remove mycenter", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("you must specify for capability center you want to remove") + } + centerName := args[0] + msg, err := removeRegistry(centerName) + if err == nil { + ioStreams.Info(msg) + } + return err + }, + } + return cmd +} + +func listCapRegistrys(ioStreams cmdutil.IOStreams) error { + table := newUITable() + table.MaxColWidth = 80 + table.AddRow("NAME", "URL") + + registrys, err := ListRegistryConfig() + if err != nil { + return errors.Wrap(err, "list registry error") + } + for _, c := range registrys { + tokenShow := "" + if len(c.Token) > 0 { + tokenShow = "***" + } + table.AddRow(c.Name, c.URL, tokenShow) + } + ioStreams.Info(table.String()) + return nil +} + +// addRegistry will add a registry +func addRegistry(regName, regURL, regToken string) error { + regConfig := apis.RegistryConfig{ + Name: regName, URL: regURL, Token: regToken, + } + repos, err := ListRegistryConfig() + if err != nil { + return err + } + var updated bool + for idx, r := range repos { + if r.Name == regConfig.Name { + repos[idx] = regConfig + updated = true + break + } + } + if !updated { + repos = append(repos, regConfig) + } + if err = StoreRepos(repos); err != nil { + return err + } + return nil +} + +// removeRegistry will remove a registry from local +func removeRegistry(regName string) (string, error) { + var message string + var err error + + regConfigs, err := ListRegistryConfig() + if err != nil { + return message, err + } + found := false + for idx, r := range regConfigs { + if r.Name == regName { + regConfigs = append(regConfigs[:idx], regConfigs[idx+1:]...) + found = true + break + } + } + if !found { + return fmt.Sprintf("registry %s not found", regName), nil + } + if err = StoreRepos(regConfigs); err != nil { + return message, err + } + message = fmt.Sprintf("Successfully remove registry %s", regName) + return message, err +} + +// DefaultRegistry is default registry +const DefaultRegistry = "default" + +// Registry define a registry used to get and list types.Capability +type Registry interface { + GetName() string + GetURL() string + GetCap(addonName string) (types.Capability, []byte, error) + ListCaps() ([]types.Capability, error) +} + +// GithubRegistry is Registry's implementation treat github url as resource +type GithubRegistry struct { + URL string `json:"url"` + RegistryName string `json:"registry_name"` + client *github.Client + cfg *GithubContent + ctx context.Context +} + +// NewRegistryFromConfig return Registry interface to get capabilities +func NewRegistryFromConfig(config apis.RegistryConfig) (Registry, error) { + return NewRegistry(context.TODO(), config.Token, config.Name, config.URL) +} + +// NewRegistry will create a registry implementation +func NewRegistry(ctx context.Context, token, registryName string, regURL string) (Registry, error) { + tp, cfg, err := Parse(regURL) + if err != nil { + return nil, err + } + switch tp { + case TypeGithub: + var tc *http.Client + if token != "" { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc = oauth2.NewClient(ctx, ts) + } + return GithubRegistry{ + URL: cfg.URL, + RegistryName: registryName, + client: github.NewClient(tc), + cfg: &cfg.GithubContent, + ctx: ctx, + }, nil + case TypeOss: + var tc http.Client + return OssRegistry{ + Client: &tc, + BucketURL: fmt.Sprintf("https://%s/", cfg.BucketURL), + RegistryName: registryName, + }, nil + case TypeLocal: + _, err := os.Stat(cfg.AbsDir) + if os.IsNotExist(err) { + return LocalRegistry{}, err + } + return LocalRegistry{ + AbsPath: cfg.AbsDir, + RegistryName: registryName, + }, nil + case TypeUnknown: + return nil, fmt.Errorf("not supported url") + } + + return nil, fmt.Errorf("not supported url") +} + +// ListRegistryConfig will get all registry config stored in local +// this will return at least one config, which is DefaultRegistry +func ListRegistryConfig() ([]apis.RegistryConfig, error) { + + defaultRegistryConfig := apis.RegistryConfig{Name: DefaultRegistry, URL: "oss://registry.kubevela.net/"} + config, err := system.GetRepoConfig() + if err != nil { + return nil, err + } + data, err := os.ReadFile(filepath.Clean(config)) + if err != nil { + if os.IsNotExist(err) { + err := StoreRepos([]apis.RegistryConfig{defaultRegistryConfig}) + if err != nil { + return nil, errors.Wrap(err, "error initialize default registry") + } + return ListRegistryConfig() + } + return nil, err + } + var regConfigs []apis.RegistryConfig + if err = yaml.Unmarshal(data, ®Configs); err != nil { + return nil, err + } + haveDefault := false + for _, r := range regConfigs { + if r.URL == defaultRegistryConfig.URL { + haveDefault = true + break + } + } + if !haveDefault { + regConfigs = append(regConfigs, defaultRegistryConfig) + } + return regConfigs, nil +} + +// GetRegistry get a Registry implementation by name +func GetRegistry(regName string) (Registry, error) { + regConfigs, err := ListRegistryConfig() + if err != nil { + return nil, err + } + for _, conf := range regConfigs { + if conf.Name == regName { + return NewRegistryFromConfig(conf) + } + } + return nil, errors.Errorf("registry %s not found", regName) +} + +// GetName will return registry name +func (g GithubRegistry) GetName() string { + return g.RegistryName +} + +// GetURL will return github registry url +func (g GithubRegistry) GetURL() string { + return g.cfg.URL +} + +// ListCaps list all capabilities of registry +func (g GithubRegistry) ListCaps() ([]types.Capability, error) { + var addons []types.Capability + + itemContents, err := g.getRepoFile() + if err != nil { + return []types.Capability{}, err + } + for _, item := range itemContents { + capa, err := item.toCapability() + if err != nil { + fmt.Printf("parse definition of %s err %v\n", item.name, err) + continue + } + addons = append(addons, capa) + } + return addons, nil +} + +// GetCap return capability object and raw data specified by cap name +func (g GithubRegistry) GetCap(addonName string) (types.Capability, []byte, error) { + fileContent, _, _, err := g.client.Repositories.GetContents(context.Background(), g.cfg.Owner, g.cfg.Repo, fmt.Sprintf("%s/%s.yaml", g.cfg.Path, addonName), &github.RepositoryContentGetOptions{Ref: g.cfg.Ref}) + if err != nil { + return types.Capability{}, []byte{}, err + } + var data []byte + if *fileContent.Encoding == "base64" { + data, err = base64.StdEncoding.DecodeString(*fileContent.Content) + if err != nil { + fmt.Printf("decode github content %s err %s\n", fileContent.GetPath(), err) + } + } + repoFile := RegistryFile{ + data: data, + name: *fileContent.Name, + } + capa, err := repoFile.toCapability() + if err != nil { + return types.Capability{}, []byte{}, err + } + capa.Source = &types.Source{RepoName: g.RegistryName} + return capa, data, nil +} + +func (g *GithubRegistry) getRepoFile() ([]RegistryFile, error) { + var items []RegistryFile + _, dirs, _, err := g.client.Repositories.GetContents(g.ctx, g.cfg.Owner, g.cfg.Repo, g.cfg.Path, &github.RepositoryContentGetOptions{Ref: g.cfg.Ref}) + if err != nil { + return []RegistryFile{}, err + } + for _, repoItem := range dirs { + if *repoItem.Type != "file" { + continue + } + fileContent, _, _, err := g.client.Repositories.GetContents(g.ctx, g.cfg.Owner, g.cfg.Repo, *repoItem.Path, &github.RepositoryContentGetOptions{Ref: g.cfg.Ref}) + if err != nil { + fmt.Printf("Getting content URL %s error: %s\n", repoItem.GetURL(), err) + continue + } + var data []byte + if *fileContent.Encoding == "base64" { + data, err = base64.StdEncoding.DecodeString(*fileContent.Content) + if err != nil { + fmt.Printf("decode github content %s err %s\n", fileContent.GetPath(), err) + continue + } + } + items = append(items, RegistryFile{ + data: data, + name: *fileContent.Name, + }) + } + return items, nil +} + +// OssRegistry is Registry's implementation treat OSS url as resource +type OssRegistry struct { + *http.Client `json:"-"` + BucketURL string `json:"bucket_url"` + RegistryName string `json:"registry_name"` +} + +// GetName return name of OssRegistry +func (o OssRegistry) GetName() string { + return o.RegistryName +} + +// GetURL return URL of OssRegistry's bucket +func (o OssRegistry) GetURL() string { + return o.BucketURL +} + +// GetCap return capability object and raw data specified by cap name +func (o OssRegistry) GetCap(addonName string) (types.Capability, []byte, error) { + filename := addonName + ".yaml" + req, _ := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + o.BucketURL+filename, + nil, + ) + resp, err := o.Client.Do(req) + if err != nil { + return types.Capability{}, nil, err + } + data, err := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + return types.Capability{}, nil, err + } + rf := RegistryFile{ + data: data, + name: filename, + } + capa, err := rf.toCapability() + if err != nil { + return types.Capability{}, nil, err + } + capa.Source = &types.Source{RepoName: o.RegistryName} + + return capa, data, nil +} + +// ListCaps list all capabilities of registry +func (o OssRegistry) ListCaps() ([]types.Capability, error) { + rfs, err := o.getRegFiles() + if err != nil { + return []types.Capability{}, errors.Wrap(err, "Get raw files fail") + } + capas := make([]types.Capability, 0) + + for _, rf := range rfs { + capa, err := rf.toCapability() + if err != nil { + fmt.Printf("[WARN] Parse file %s fail: %s\n", rf.name, err.Error()) + } + capas = append(capas, capa) + } + return capas, nil +} + +func (o OssRegistry) getRegFiles() ([]RegistryFile, error) { + req, _ := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + o.BucketURL+"?list-type=2", + nil, + ) + resp, err := o.Client.Do(req) + if err != nil { + return []RegistryFile{}, err + } + data, err := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + return []RegistryFile{}, err + } + list := &ListBucketResult{} + err = xml.Unmarshal(data, list) + if err != nil { + return []RegistryFile{}, err + } + rfs := make([]RegistryFile, 0) + + for _, fileName := range list.File { + req, _ := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + o.BucketURL+fileName, + nil, + ) + resp, err := o.Client.Do(req) + if err != nil { + fmt.Printf("[WARN] %s download fail\n", fileName) + continue + } + data, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + rf := RegistryFile{ + data: data, + name: fileName, + } + rfs = append(rfs, rf) + + } + return rfs, nil +} + +// LocalRegistry is Registry's implementation treat local url as resource +type LocalRegistry struct { + AbsPath string `json:"abs_path"` + RegistryName string `json:"registry_name"` +} + +// GetName return name of LocalRegistry +func (l LocalRegistry) GetName() string { + return l.RegistryName +} + +// GetURL return path of LocalRegistry +func (l LocalRegistry) GetURL() string { + return l.AbsPath +} + +// GetCap return capability object and raw data specified by cap name +func (l LocalRegistry) GetCap(addonName string) (types.Capability, []byte, error) { + fileName := addonName + ".yaml" + filePath := fmt.Sprintf("%s/%s", l.AbsPath, fileName) + data, err := os.ReadFile(filePath) + if err != nil { + return types.Capability{}, []byte{}, err + } + file := RegistryFile{ + data: data, + name: fileName, + } + capa, err := file.toCapability() + if err != nil { + return types.Capability{}, []byte{}, err + } + capa.Source = &types.Source{RepoName: l.RegistryName} + + return capa, data, nil +} + +// ListCaps list all capabilities of registry +func (l LocalRegistry) ListCaps() ([]types.Capability, error) { + glob := filepath.Join(filepath.Clean(l.AbsPath), "*") + files, _ := filepath.Glob(glob) + capas := make([]types.Capability, 0) + for _, file := range files { + // nolint:gosec + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + capa, err := RegistryFile{ + data: data, + name: path.Base(file), + }.toCapability() + if err != nil { + fmt.Printf("parsing file: %s err: %s\n", file, err) + continue + } + capas = append(capas, capa) + } + return capas, nil +} + +func (item RegistryFile) toCapability() (types.Capability, error) { + dm, err := (&common.Args{}).GetDiscoveryMapper() + if err != nil { + return types.Capability{}, err + } + capability, err := ParseCapability(dm, item.data) + if err != nil { + return types.Capability{}, err + } + return capability, nil +} + +// RegistryFile describes a file item in registry +type RegistryFile struct { + data []byte // file content + name string // file's name +} + +// ListBucketResult describe a file list from OSS +type ListBucketResult struct { + File []string `xml:"Contents>Key"` + Count int `xml:"KeyCount"` +} + +// Content contains different type of content needed when building Registry +type Content struct { + OssContent + GithubContent + LocalContent +} + +// LocalContent for local registry +type LocalContent struct { + AbsDir string `json:"abs_dir"` +} + +// OssContent for oss registry +type OssContent struct { + BucketURL string `json:"bucket_url"` +} + +// GithubContent for registry +type GithubContent struct { + URL string `json:"url"` + Owner string `json:"owner"` + Repo string `json:"repo"` + Path string `json:"path"` + Ref string `json:"ref"` +} + +// TypeLocal represents github +const TypeLocal = "local" + +// TypeOss represent oss +const TypeOss = "oss" + +// TypeGithub represents github +const TypeGithub = "github" + +// TypeUnknown represents parse failed +const TypeUnknown = "unknown" + +// Parse will parse config from address +func Parse(addr string) (string, *Content, error) { + URL, err := url.Parse(addr) + if err != nil { + return "", nil, err + } + l := strings.Split(strings.TrimPrefix(URL.Path, "/"), "/") + switch URL.Scheme { + case "http", "https": + switch URL.Host { + case "github.com": + // We support two valid format: + // 1. https://github.com///tree// + // 2. https://github.com/// + if len(l) < 3 { + return "", nil, errors.New("invalid format " + addr) + } + if l[2] == "tree" { + // https://github.com///tree// + if len(l) < 5 { + return "", nil, errors.New("invalid format " + addr) + } + return TypeGithub, &Content{ + GithubContent: GithubContent{ + URL: addr, + Owner: l[0], + Repo: l[1], + Path: strings.Join(l[4:], "/"), + Ref: l[3], + }, + }, nil + } + // https://github.com/// + return TypeGithub, &Content{ + GithubContent: GithubContent{ + URL: addr, + Owner: l[0], + Repo: l[1], + Path: strings.Join(l[2:], "/"), + Ref: "", // use default branch + }, + }, + nil + case "api.github.com": + if len(l) != 5 { + return "", nil, errors.New("invalid format " + addr) + } + //https://api.github.com/repos///contents/ + return TypeGithub, &Content{ + GithubContent: GithubContent{ + URL: addr, + Owner: l[1], + Repo: l[2], + Path: l[4], + Ref: URL.Query().Get("ref"), + }, + }, + nil + default: + } + case "oss": + return TypeOss, &Content{ + OssContent: OssContent{ + BucketURL: URL.Host, + }, + }, nil + case "file": + return TypeLocal, &Content{ + LocalContent: LocalContent{ + AbsDir: URL.Path, + }, + }, nil + + } + + return TypeUnknown, nil, nil +} + +// StoreRepos will store registry repo locally +func StoreRepos(registries []apis.RegistryConfig) error { + config, err := system.GetRepoConfig() + if err != nil { + return err + } + data, err := yaml.Marshal(registries) + if err != nil { + return err + } + //nolint:gosec + return os.WriteFile(config, data, 0644) +} + +// ParseCapability will convert config from remote center to capability +func ParseCapability(mapper discoverymapper.DiscoveryMapper, data []byte) (types.Capability, error) { + var obj = unstructured.Unstructured{Object: make(map[string]interface{})} + err := yaml.Unmarshal(data, &obj.Object) + if err != nil { + return types.Capability{}, err + } + switch obj.GetKind() { + case "ComponentDefinition": + var cd v1beta1.ComponentDefinition + err = yaml.Unmarshal(data, &cd) + if err != nil { + return types.Capability{}, err + } + var workloadDefinitionRef string + if cd.Spec.Workload.Type != "" { + workloadDefinitionRef = cd.Spec.Workload.Type + } else { + ref, err := util.ConvertWorkloadGVK2Definition(mapper, cd.Spec.Workload.Definition) + if err != nil { + return types.Capability{}, err + } + workloadDefinitionRef = ref.Name + } + return plugins.HandleDefinition(cd.Name, workloadDefinitionRef, cd.Annotations, cd.Labels, cd.Spec.Extension, types.TypeComponentDefinition, nil, cd.Spec.Schematic) + case "TraitDefinition": + var td v1beta1.TraitDefinition + err = yaml.Unmarshal(data, &td) + if err != nil { + return types.Capability{}, err + } + return plugins.HandleDefinition(td.Name, td.Spec.Reference.Name, td.Annotations, td.Labels, td.Spec.Extension, types.TypeTrait, td.Spec.AppliesToWorkloads, td.Spec.Schematic) + case "ScopeDefinition": + // TODO(wonderflow): support scope definition here. + } + return types.Capability{}, fmt.Errorf("unknown definition Type %s", obj.GetKind()) +} diff --git a/references/plugins/registry_test.go b/references/cli/registry_test.go similarity index 56% rename from references/plugins/registry_test.go rename to references/cli/registry_test.go index 4a4990179..4b908875f 100644 --- a/references/plugins/registry_test.go +++ b/references/cli/registry_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package plugins +package cli import ( "context" @@ -63,3 +63,50 @@ func TestRegistry(t *testing.T) { assert.NotNil(t, data, testAddon) } } + +func TestParseURL(t *testing.T) { + cases := map[string]struct { + url string + exp *GithubContent + expType string + }{ + "api-github": { + url: "https://api.github.com/repos/oam-dev/catalog/contents/traits?ref=master", + expType: TypeGithub, + exp: &GithubContent{ + URL: "https://api.github.com/repos/oam-dev/catalog/contents/traits?ref=master", + Owner: "oam-dev", + Repo: "catalog", + Path: "traits", + Ref: "master", + }, + }, + "github-copy-path": { + url: "https://github.com/oam-dev/catalog/tree/master/repository", + expType: TypeGithub, + exp: &GithubContent{ + URL: "https://github.com/oam-dev/catalog/tree/master/repository", + Owner: "oam-dev", + Repo: "catalog", + Path: "repository", + Ref: "master", + }, + }, + "github-manual-write-path": { + url: "https://github.com/oam-dev/catalog/traits", + expType: TypeGithub, + exp: &GithubContent{ + URL: "https://github.com/oam-dev/catalog/traits", + Owner: "oam-dev", + Repo: "catalog", + Path: "traits", + }, + }, + } + for caseName, c := range cases { + tp, content, err := Parse(c.url) + assert.NoError(t, err, caseName) + assert.Equal(t, c.exp, &content.GithubContent, caseName) + assert.Equal(t, c.expType, tp, caseName) + } +} diff --git a/references/cli/traits.go b/references/cli/traits.go index 8537667da..edba23773 100644 --- a/references/cli/traits.go +++ b/references/cli/traits.go @@ -18,9 +18,10 @@ package cli import ( "context" - "fmt" + "encoding/json" "strings" + "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -33,77 +34,113 @@ import ( common2 "github.com/oam-dev/kubevela/pkg/utils/common" cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" "github.com/oam-dev/kubevela/references/common" - "github.com/oam-dev/kubevela/references/plugins" ) -// NewTraitsCommand creates `traits` command -func NewTraitsCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { +var ( + regName string + regURL string + token string + label string + filter filterFunc +) + +// NewTraitCommand creates `traits` command +func NewTraitCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { + var isDiscover bool cmd := &cobra.Command{ - Use: "traits", - Aliases: []string{"trait"}, - DisableFlagsInUseLine: true, - Short: "List traits", - Long: "List traits", - Example: `vela traits`, + Use: "trait", + Aliases: []string{"traits"}, + Short: "List/get traits", + Long: "List traits & get trait in registry", + Example: `vela trait`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return c.SetConfig() }, RunE: func(cmd *cobra.Command, args []string) error { - isDiscover, _ := cmd.Flags().GetBool("discover") - env, err := GetFlagEnvOrCurrent(cmd, c) - if err != nil { - return err - } - label, err := cmd.Flags().GetString(types.LabelArg) - if err != nil { - return err - } - if label != "" && len(strings.Split(label, "=")) != 2 { - return fmt.Errorf("label %s is not in the right format", label) - - } - if !isDiscover { - return printTraitList(env.Namespace, c, ioStreams, label) - } - option := types.TypeTrait - err = printCenterCapabilities(env.Namespace, "", c, ioStreams, &option, label) - if err != nil { - return err + // parse label filter + if label != "" { + words := strings.Split(label, "=") + if len(words) < 2 { + return errors.New("label is invalid") + } + filter = createLabelFilter(words[0], words[1]) } - return nil + var registry Registry + var err error + if isDiscover { + if regURL != "" { + ioStreams.Infof("Showing trait definition from url: %s\n", regURL) + registry, err = NewRegistry(context.Background(), token, "temporary-registry", regURL) + if err != nil { + return errors.Wrap(err, "creating registry err, please check registry url") + } + } else { + ioStreams.Infof("Showing trait definition from registry: %s\n", regName) + registry, err = GetRegistry(regName) + if err != nil { + return errors.Wrap(err, "get registry err") + } + } + return PrintTraitListFromRegistry(registry, ioStreams, filter) + + } + return PrintInstalledTraitDef(ioStreams, filter) }, Annotations: map[string]string{ types.TagCommandType: types.TypeCap, }, } - cmd.Flags().Bool("discover", false, "discover traits in capability centers") - cmd.Flags().String(types.LabelArg, "", "a label to filter components, the format is `--label type=terraform`") + cmd.SetOut(ioStreams.Out) + cmd.AddCommand( + NewTraitGetCommand(c, ioStreams), + ) + cmd.Flags().BoolVar(&isDiscover, "discover", false, "discover traits in registries") + cmd.PersistentFlags().StringVar(®URL, "url", "", "specify the registry URL") + cmd.PersistentFlags().StringVar(&token, "token", "", "specify token when using --url to specify registry url") + cmd.PersistentFlags().StringVar(®Name, "registry", DefaultRegistry, "specify the registry name") + cmd.Flags().StringVar(&label, types.LabelArg, "", "a label to filter components, the format is `--label type=terraform`") cmd.SetOut(ioStreams.Out) return cmd } -func printTraitList(userNamespace string, c common2.Args, ioStreams cmdutil.IOStreams, label string) error { - table := newUITable() - table.Wrap = true +// NewTraitGetCommand creates `trait get` command +func NewTraitGetCommand(c common2.Args, ioStreams cmdutil.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "get ", + Short: "get trait from registry", + Long: "get trait from registry", + Example: "vela trait get ", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + ioStreams.Error("you must specify the trait name") + return nil + } + name := args[0] + var registry Registry + var err error - traitDefinitionList, err := common.ListRawTraitDefinitions(userNamespace, c) - if err != nil { - return err + if regURL != "" { + ioStreams.Infof("Getting trait definition from url: %s\n", regURL) + registry, err = NewRegistry(context.Background(), token, "temporary-registry", regURL) + if err != nil { + return errors.Wrap(err, "creating registry err, please check registry url") + } + } else { + ioStreams.Infof("Getting trait definition from registry: %s\n", regName) + registry, err = GetRegistry(regName) + if err != nil { + return errors.Wrap(err, "get registry err") + } + } + return errors.Wrap(InstallTraitByNameFromRegistry(c, ioStreams, name, registry), "install trait definition err") + }, } - table.AddRow("NAME", "NAMESPACE", "APPLIES-TO", "CONFLICTS-WITH", "POD-DISRUPTIVE", "DESCRIPTION") - for _, t := range traitDefinitionList { - if label != "" && !common.CheckLabelExistence(t.Labels, label) { - continue - } - table.AddRow(t.Name, t.Namespace, strings.Join(t.Spec.AppliesToWorkloads, ","), strings.Join(t.Spec.ConflictsWith, ","), t.Spec.PodDisruptive, plugins.GetDescription(t.Annotations)) - } - ioStreams.Info(table.String()) - return nil + return cmd } // PrintTraitListFromRegistry print a table which shows all traits from registry -func PrintTraitListFromRegistry(isDiscover bool, url string, ioStreams cmdutil.IOStreams) error { +func PrintTraitListFromRegistry(registry Registry, ioStreams cmdutil.IOStreams, filter filterFunc) error { var scheme = runtime.NewScheme() err := core.AddToScheme(scheme) if err != nil { @@ -118,8 +155,7 @@ func PrintTraitListFromRegistry(isDiscover bool, url string, ioStreams cmdutil.I return err } - _, _ = ioStreams.Out.Write([]byte(fmt.Sprintf("Showing traits from registry: %s\n", url))) - caps, err := getCapsFromRegistry(url) + caps, err := registry.ListCaps() if err != nil { return err } @@ -131,12 +167,12 @@ func PrintTraitListFromRegistry(isDiscover bool, url string, ioStreams cmdutil.I if err != nil { return err } - if isDiscover { - table.AddRow("NAME", "REGISTRY", "DEFINITION", "APPLIES-TO") - } else { - table.AddRow("NAME", "DEFINITION", "APPLIES-TO") - } + + table.AddRow("NAME", "REGISTRY", "DEFINITION", "APPLIES-TO", "STATUS") for _, c := range caps { + if filter != nil && !filter(c) { + continue + } if c.Type != types.TypeTrait { continue } @@ -146,39 +182,16 @@ func PrintTraitListFromRegistry(isDiscover bool, url string, ioStreams cmdutil.I c.Status = installed } } - if c.Status == uninstalled && isDiscover { - table.AddRow(c.Name, "default", c.CrdName, c.AppliesTo) - } - if c.Status == installed && !isDiscover { - table.AddRow(c.Name, c.CrdName, c.AppliesTo) - } + table.AddRow(c.Name, "default", c.CrdName, c.AppliesTo, c.Status) } ioStreams.Info(table.String()) return nil } -// getCapsFromRegistry will retrieve caps from registry -func getCapsFromRegistry(regURL string) ([]types.Capability, error) { - g, err := plugins.NewRegistry(context.Background(), "", "url-registry", regURL) - if err != nil { - return []types.Capability{}, err - } - caps, err := g.ListCaps() - if err != nil { - return []types.Capability{}, err - } - return caps, nil -} - -// InstallTraitByName will install given traitName trait to cluster -func InstallTraitByName(args common2.Args, ioStream cmdutil.IOStreams, traitName, regURL string) error { - - g, err := plugins.NewRegistry(context.Background(), "", "url-registry", regURL) - if err != nil { - return err - } - capObj, data, err := g.GetCap(traitName) +// InstallTraitByNameFromRegistry will install given traitName trait to cluster +func InstallTraitByNameFromRegistry(args common2.Args, ioStream cmdutil.IOStreams, traitName string, registry Registry) error { + capObj, data, err := registry.GetCap(traitName) if err != nil { return err } @@ -200,8 +213,40 @@ func InstallTraitByName(args common2.Args, ioStream cmdutil.IOStreams, traitName return nil } -// DefaultRegistry is default capability center of kubectl-vela -var DefaultRegistry = "oss://registry.kubevela.net" +// PrintInstalledTraitDef will print all TraitDefinition in cluster +func PrintInstalledTraitDef(io cmdutil.IOStreams, filter filterFunc) error { + var list v1beta1.TraitDefinitionList + err := clt.List(context.Background(), &list) + if err != nil { + return errors.Wrap(err, "get trait definition list error") + } + dm, err := (&common2.Args{}).GetDiscoveryMapper() + if err != nil { + return errors.Wrap(err, "get discovery mapper error") + } + + table := newUITable() + table.AddRow("NAME", "APPLIES-TO") + + for _, td := range list.Items { + data, err := json.Marshal(td) + if err != nil { + io.Infof("error encoding definition: %s\n", td.Name) + continue + } + capa, err := ParseCapability(dm, data) + if err != nil { + io.Errorf("error parsing capability: %s\n", td.Name) + continue + } + if filter != nil && !filter(capa) { + continue + } + table.AddRow(capa.Name, capa.AppliesTo) + } + io.Infof(table.String()) + return nil +} const installed = "installed" const uninstalled = "uninstalled" diff --git a/references/cli/traits_test.go b/references/cli/traits_test.go index 503afbf2d..3e1f2a860 100644 --- a/references/cli/traits_test.go +++ b/references/cli/traits_test.go @@ -33,7 +33,7 @@ import ( func TestNewTraitsCommandPersistentPreRunE(t *testing.T) { io := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} fakeC := common2.Args{} - cmd := NewTraitsCommand(fakeC, io) + cmd := NewTraitCommand(fakeC, io) assert.Nil(t, cmd.PersistentPreRunE(new(cobra.Command), []string{})) } diff --git a/references/common/application.go b/references/common/application.go index 20d9bd32c..3d1498bae 100644 --- a/references/common/application.go +++ b/references/common/application.go @@ -23,13 +23,9 @@ import ( "fmt" "os" "path/filepath" - "sort" "strings" - "time" - "github.com/AlecAivazis/survey/v2" "github.com/pkg/errors" - "github.com/spf13/cobra" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/serializer/json" apitypes "k8s.io/apimachinery/pkg/types" @@ -40,27 +36,14 @@ import ( corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/oam" - oamutil "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils/apply" "github.com/oam-dev/kubevela/pkg/utils/common" cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" - "github.com/oam-dev/kubevela/references/apis" "github.com/oam-dev/kubevela/references/appfile" "github.com/oam-dev/kubevela/references/appfile/api" "github.com/oam-dev/kubevela/references/appfile/template" ) -// nolint:golint -const ( - DefaultChosenAllSvc = "ALL SERVICES" - FlagNotSet = "FlagNotSet" - FlagIsInvalid = "FlagIsInvalid" - FlagIsValid = "FlagIsValid" -) - -type componentMetaList []apis.ComponentMeta -type applicationMetaList []apis.ApplicationMeta - // AppfileOptions is some configuration that modify options for an Appfile type AppfileOptions struct { Kubecli client.Client @@ -75,26 +58,6 @@ type BuildResult struct { scopes []oam.Object } -func (comps componentMetaList) Len() int { - return len(comps) -} -func (comps componentMetaList) Swap(i, j int) { - comps[i], comps[j] = comps[j], comps[i] -} -func (comps componentMetaList) Less(i, j int) bool { - return comps[i].CreatedTime > comps[j].CreatedTime -} - -func (a applicationMetaList) Len() int { - return len(a) -} -func (a applicationMetaList) Swap(i, j int) { - a[i], a[j] = a[j], a[i] -} -func (a applicationMetaList) Less(i, j int) bool { - return a[i].CreatedTime > a[j].CreatedTime -} - // Option is option work with dashboard api server type Option struct { // Optional filter, if specified, only components in such app will be listed @@ -112,101 +75,6 @@ type DeleteOptions struct { C common.Args } -// ListApplications lists all applications -func ListApplications(ctx context.Context, c client.Reader, opt Option) ([]apis.ApplicationMeta, error) { - var applicationMetaList applicationMetaList - var appList corev1beta1.ApplicationList - if opt.AppName != "" { - var app corev1beta1.Application - if err := c.Get(ctx, client.ObjectKey{Name: opt.AppName, Namespace: opt.Namespace}, &app); err != nil { - return applicationMetaList, err - } - appList.Items = append(appList.Items, app) - } else { - err := c.List(ctx, &appList, &client.ListOptions{Namespace: opt.Namespace}) - if err != nil { - return applicationMetaList, err - } - } - for _, a := range appList.Items { - // ignore the deleted resource - if a.GetDeletionGracePeriodSeconds() != nil { - continue - } - applicationMeta, err := RetrieveApplicationStatusByName(ctx, c, a.Name, a.Namespace) - if err != nil { - return applicationMetaList, err - } - applicationMeta.Components = nil - applicationMetaList = append(applicationMetaList, applicationMeta) - } - sort.Stable(applicationMetaList) - return applicationMetaList, nil -} - -// ListApplicationConfigurations lists all OAM ApplicationConfiguration -func ListApplicationConfigurations(ctx context.Context, c client.Reader, opt Option) (corev1alpha2.ApplicationConfigurationList, error) { - var appConfigList corev1alpha2.ApplicationConfigurationList - - if opt.AppName != "" { - var appConfig corev1alpha2.ApplicationConfiguration - if err := c.Get(ctx, client.ObjectKey{Name: opt.AppName, Namespace: opt.Namespace}, &appConfig); err != nil { - return appConfigList, err - } - appConfigList.Items = append(appConfigList.Items, appConfig) - } else { - err := c.List(ctx, &appConfigList, &client.ListOptions{Namespace: opt.Namespace}) - if err != nil { - return appConfigList, err - } - } - return appConfigList, nil -} - -// ListComponents will list all components for dashboard -func ListComponents(ctx context.Context, c client.Reader, opt Option) ([]apis.ComponentMeta, error) { - var componentMetaList componentMetaList - var appConfigList corev1alpha2.ApplicationConfigurationList - var err error - if appConfigList, err = ListApplicationConfigurations(ctx, c, opt); err != nil { - return nil, err - } - - for _, a := range appConfigList.Items { - for _, com := range a.Spec.Components { - component, _, err := oamutil.GetComponent(ctx, c, com, opt.Namespace) - if err != nil { - return componentMetaList, err - } - componentMetaList = append(componentMetaList, apis.ComponentMeta{ - Name: com.ComponentName, - Status: types.StatusDeployed, - CreatedTime: a.ObjectMeta.CreationTimestamp.String(), - Component: *component, - AppConfig: a, - App: a.Name, - }) - } - } - sort.Stable(componentMetaList) - return componentMetaList, nil -} - -// RetrieveApplicationStatusByName will get app status -func RetrieveApplicationStatusByName(ctx context.Context, c client.Reader, applicationName string, - namespace string) (apis.ApplicationMeta, error) { - var applicationMeta apis.ApplicationMeta - var app corev1beta1.Application - if err := c.Get(ctx, client.ObjectKey{Name: applicationName, Namespace: namespace}, &app); err != nil { - return applicationMeta, err - } - applicationMeta.Name = app.Name - applicationMeta.Status = string(app.Status.Phase) - applicationMeta.CreatedTime = app.CreationTimestamp.Format(time.RFC3339) - - return applicationMeta, nil -} - // DeleteApp will delete app including server side func (o *DeleteOptions) DeleteApp() (string, error) { ctx := context.Background() @@ -274,64 +142,6 @@ func (o *DeleteOptions) DeleteComponent(io cmdutil.IOStreams) (string, error) { return fmt.Sprintf("component \"%s\" deleted from \"%s\"", o.CompName, o.AppName), nil } -func chooseSvc(services []string) (string, error) { - var svcName string - services = append(services, DefaultChosenAllSvc) - prompt := &survey.Select{ - Message: "Please choose one service: ", - Options: services, - Default: DefaultChosenAllSvc, - } - err := survey.AskOne(prompt, &svcName) - if err != nil { - return "", fmt.Errorf("failed to retrieve services of the application, err %w", err) - } - return svcName, nil -} - -// GetServicesWhenDescribingApplication gets the target services list either from cli `--svc` flag or from survey -func GetServicesWhenDescribingApplication(cmd *cobra.Command, app *api.Application) ([]string, error) { - var svcFlag string - var svcFlagStatus string - // to store the value of flag `--svc` set in Cli, or selected value in survey - var targetServices []string - if svcFlag = cmd.Flag("svc").Value.String(); svcFlag == "" { - svcFlagStatus = FlagNotSet - } else { - svcFlagStatus = FlagIsInvalid - } - // all services name of the application `appName` - var services []string - for svcName := range app.Services { - services = append(services, svcName) - if svcFlag == svcName { - svcFlagStatus = FlagIsValid - targetServices = append(targetServices, svcName) - } - } - totalServices := len(services) - if svcFlagStatus == FlagNotSet && totalServices == 1 { - targetServices = services - } - if svcFlagStatus == FlagIsInvalid || (svcFlagStatus == FlagNotSet && totalServices > 1) { - if svcFlagStatus == FlagIsInvalid { - cmd.Printf("The service name '%s' is not valid\n", svcFlag) - } - chosenSvc, err := chooseSvc(services) - if err != nil { - return []string{}, err - } - - if chosenSvc == DefaultChosenAllSvc { - targetServices = services - } else { - targetServices = targetServices[:0] - targetServices = append(targetServices, chosenSvc) - } - } - return targetServices, nil -} - func saveAndLoadRemoteAppfile(url string) (*api.AppFile, error) { body, err := common.HTTPGet(context.Background(), url) if err != nil { diff --git a/references/common/capability.go b/references/common/capability.go deleted file mode 100644 index 622e29413..000000000 --- a/references/common/capability.go +++ /dev/null @@ -1,520 +0,0 @@ -/* -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 common - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/oam-dev/kubevela/pkg/utils/common" - - corev1 "k8s.io/api/core/v1" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" - - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" - "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/pkg/utils/helm" - "github.com/oam-dev/kubevela/pkg/utils/system" - cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" - "github.com/oam-dev/kubevela/references/apis" - "github.com/oam-dev/kubevela/references/plugins" -) - -// AddCapabilityCenter will add a cap center -func AddCapabilityCenter(capName, capURL, capToken string) error { - repos, err := plugins.LoadRepos() - if err != nil { - return err - } - config := &plugins.CapCenterConfig{ - Name: capName, - Address: capURL, - Token: capToken, - } - var updated bool - for idx, r := range repos { - if r.Name == config.Name { - repos[idx] = *config - updated = true - break - } - } - if !updated { - repos = append(repos, *config) - } - if err = plugins.StoreRepos(repos); err != nil { - return err - } - return SyncCapabilityFromCenter(capName, capURL, capToken) -} - -// SyncCapabilityFromCenter will sync all capabilities from center -func SyncCapabilityFromCenter(capName, capURL, capToken string) error { - client, err := plugins.NewCenterClient(context.Background(), capName, capURL, capToken) - if err != nil { - return err - } - return client.SyncCapabilityFromCenter() -} - -// AddCapabilityIntoCluster will add a capability into K8s cluster, it is equal to apply a definition yaml and run `vela workloads/traits` -func AddCapabilityIntoCluster(c client.Client, mapper discoverymapper.DiscoveryMapper, capability string) (string, error) { - ss := strings.Split(capability, "/") - if len(ss) < 2 { - return "", errors.New("invalid format for " + capability + ", please follow format
/") - } - repoName := ss[0] - name := ss[1] - ioStreams := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} - if err := InstallCapability(c, mapper, repoName, name, ioStreams); err != nil { - return "", err - } - return fmt.Sprintf("Successfully installed capability %s from %s", name, repoName), nil -} - -// InstallCapability will add a cap into K8s cluster and install it's controller(helm charts) -func InstallCapability(client client.Client, mapper discoverymapper.DiscoveryMapper, centerName, capabilityName string, ioStreams cmdutil.IOStreams) error { - dir, _ := system.GetCapCenterDir() - repoDir := filepath.Join(dir, centerName) - tp, err := GetCapabilityFromCenter(mapper, centerName, capabilityName) - if err != nil { - return err - } - defDir, _ := system.GetCapabilityDir() - fileContent, err := os.ReadFile(filepath.Clean(filepath.Join(repoDir, tp.Name+".yaml"))) - if err != nil { - return err - } - switch tp.Type { - case types.TypeComponentDefinition: - err = InstallComponentDefinition(client, fileContent, ioStreams, &tp) - if err != nil { - return err - } - case types.TypeTrait: - err = InstallTraitDefinition(client, mapper, fileContent, ioStreams, &tp) - if err != nil { - return err - } - case types.TypeScope: - // TODO(wonderflow): support install scope here - case types.TypeWorkload: - return fmt.Errorf("unsupported capability type %v", types.TypeWorkload) - default: - return fmt.Errorf("unsupported type: %v", tp.Type) - } - - success := plugins.SinkTemp2Local([]types.Capability{tp}, defDir) - if success == 1 { - ioStreams.Infof("Successfully installed capability %s from %s\n", capabilityName, centerName) - } - return nil -} - -// InstallComponentDefinition will add a component into K8s cluster and install it's controller -func InstallComponentDefinition(client client.Client, workloadData []byte, ioStreams cmdutil.IOStreams, tp *types.Capability) error { - var cd v1beta1.ComponentDefinition - var err error - if err = yaml.Unmarshal(workloadData, &cd); err != nil { - return err - } - cd.Namespace = types.DefaultKubeVelaNS - ioStreams.Info("Installing component capability " + cd.Name) - if tp.Install != nil { - tp.Source.ChartName = tp.Install.Helm.Name - if err = helm.InstallHelmChart(ioStreams, tp.Install.Helm); err != nil { - return err - } - err = addSourceIntoExtension(cd.Spec.Extension, tp.Source) - if err != nil { - return err - } - } - if cd.Spec.Workload.Type == "" { - tp.CrdInfo = &types.CRDInfo{ - APIVersion: cd.Spec.Workload.Definition.APIVersion, - Kind: cd.Spec.Workload.Definition.Kind, - } - } - if err = client.Create(context.Background(), &cd); err != nil && !apierrors.IsAlreadyExists(err) { - return err - } - return nil -} - -// InstallTraitDefinition will add a trait into K8s cluster and install it's controller -func InstallTraitDefinition(client client.Client, mapper discoverymapper.DiscoveryMapper, traitdata []byte, ioStreams cmdutil.IOStreams, cap *types.Capability) error { - var td v1beta1.TraitDefinition - var err error - if err = yaml.Unmarshal(traitdata, &td); err != nil { - return err - } - td.Namespace = types.DefaultKubeVelaNS - ioStreams.Info("Installing trait capability " + td.Name) - if cap.Install != nil { - cap.Source.ChartName = cap.Install.Helm.Name - if err = helm.InstallHelmChart(ioStreams, cap.Install.Helm); err != nil { - return err - } - err = addSourceIntoExtension(td.Spec.Extension, cap.Source) - if err != nil { - return err - } - } - if err = HackForStandardTrait(*cap, client); err != nil { - return err - } - gvk, err := util.GetGVKFromDefinition(mapper, td.Spec.Reference) - if err != nil { - return err - } - cap.CrdInfo = &types.CRDInfo{ - APIVersion: v1.GroupVersion{ - Group: gvk.Group, - Version: gvk.Version, - }.String(), - Kind: gvk.Kind, - } - if err = client.Create(context.Background(), &td); err != nil && !apierrors.IsAlreadyExists(err) { - return err - } - return nil -} - -// HackForStandardTrait will do some hack install for standard capability -func HackForStandardTrait(tp types.Capability, client client.Client) error { - switch tp.Name { - case "metrics": - // metrics trait will rely on a Prometheus instance to be installed - // make sure the chart is a prometheus operator - if tp.Install == nil { - break - } - if tp.Install.Helm.Namespace == "monitoring" && tp.Install.Helm.Name == "kube-prometheus-stack" { - if err := InstallPrometheusInstance(client); err != nil { - return err - } - } - default: - } - return nil -} - -// GetCapabilityFromCenter will list all synced capabilities from cap center and return the specified one -func GetCapabilityFromCenter(mapper discoverymapper.DiscoveryMapper, repoName, addonName string) (types.Capability, error) { - dir, _ := system.GetCapCenterDir() - repoDir := filepath.Join(dir, repoName) - templates, err := plugins.LoadCapabilityFromSyncedCenter(mapper, repoDir) - if err != nil { - return types.Capability{}, err - } - for _, t := range templates { - if t.Name == addonName { - t.Source = &types.Source{RepoName: repoName} - return t, nil - } - } - return types.Capability{}, fmt.Errorf("%s/%s not exist, try 'vela cap center sync %s' to sync from remote", repoName, addonName, repoName) -} - -// ListCapabilityCenters will list all capabilities from center -func ListCapabilityCenters() ([]apis.CapabilityCenterMeta, error) { - var capabilityCenterList []apis.CapabilityCenterMeta - centers, err := plugins.LoadRepos() - if err != nil { - return capabilityCenterList, err - } - for _, c := range centers { - capabilityCenterList = append(capabilityCenterList, apis.CapabilityCenterMeta{ - Name: c.Name, - URL: c.Address, - }) - } - return capabilityCenterList, nil -} - -// SyncCapabilityCenter will sync capabilities from center to local -func SyncCapabilityCenter(capabilityCenterName string) error { - repos, err := plugins.LoadRepos() - if err != nil { - return err - } - if len(repos) == 0 { - return fmt.Errorf("no capability center configured") - } - find := false - if capabilityCenterName != "" { - for idx, r := range repos { - if r.Name == capabilityCenterName { - repos = []plugins.CapCenterConfig{repos[idx]} - find = true - break - } - } - if !find { - return fmt.Errorf("%s center not exist", capabilityCenterName) - } - } - ctx := context.Background() - for _, d := range repos { - client, err := plugins.NewCenterClient(ctx, d.Name, d.Address, d.Token) - if err != nil { - return err - } - err = client.SyncCapabilityFromCenter() - if err != nil { - return err - } - } - return nil -} - -// RemoveCapabilityFromCluster will remove a capability from cluster. -// 1. remove definition 2. uninstall chart 3. remove local files -func RemoveCapabilityFromCluster(userNamespace string, c common.Args, client client.Client, capabilityName string) (string, error) { - ioStreams := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} - if err := RemoveCapability(userNamespace, c, client, capabilityName, ioStreams); err != nil { - return "", err - } - msg := fmt.Sprintf("%s removed successfully", capabilityName) - return msg, nil -} - -// RemoveCapability will remove a capability from cluster. -// 1. remove definition 2. uninstall chart 3. remove local files -func RemoveCapability(userNamespace string, c common.Args, client client.Client, capabilityName string, ioStreams cmdutil.IOStreams) error { - // TODO(wonderflow): make sure no apps is using this capability - caps, err := plugins.LoadAllInstalledCapability(userNamespace, c) - if err != nil { - return err - } - for _, w := range caps { - if w.Name == capabilityName { - return uninstallCap(client, w, ioStreams) - } - } - return errors.New(capabilityName + " not exist") -} - -func uninstallCap(cli client.Client, cap types.Capability, ioStreams cmdutil.IOStreams) error { - // 1. Remove WorkloadDefinition or TraitDefinition - ctx := context.Background() - var obj client.Object - switch cap.Type { - case types.TypeTrait: - obj = &v1beta1.TraitDefinition{ObjectMeta: v1.ObjectMeta{Name: cap.Name, Namespace: types.DefaultKubeVelaNS}} - case types.TypeWorkload: - obj = &v1beta1.WorkloadDefinition{ObjectMeta: v1.ObjectMeta{Name: cap.Name, Namespace: types.DefaultKubeVelaNS}} - case types.TypeScope: - return fmt.Errorf("uninstall scope capability was not supported yet") - case types.TypeComponentDefinition: - obj = &v1beta1.ComponentDefinition{ObjectMeta: v1.ObjectMeta{Name: cap.Name, Namespace: types.DefaultKubeVelaNS}} - default: - return fmt.Errorf("unsupported type: %v", cap.Type) - } - if err := cli.Delete(ctx, obj); err != nil { - return err - } - - if cap.Install != nil && cap.Install.Helm.Name != "" { - // 2. Remove Helm chart if there is - if cap.Install.Helm.Namespace == "" { - cap.Install.Helm.Namespace = types.DefaultKubeVelaNS - } - if err := helm.Uninstall(ioStreams, cap.Install.Helm.Name, cap.Install.Helm.Namespace, cap.Name); err != nil { - return err - } - } - - // 3. Remove local capability file - capdir, _ := system.GetCapabilityDir() - switch cap.Type { - case types.TypeTrait: - if err := os.Remove(filepath.Join(capdir, "traits", cap.Name)); err != nil { - return err - } - case types.TypeWorkload: - if err := os.Remove(filepath.Join(capdir, "workloads", cap.Name)); err != nil { - return err - } - case types.TypeScope: - // TODO(wonderflow): add scope remove here. - case types.TypeComponentDefinition: - if err := os.Remove(filepath.Join(capdir, "components", cap.Name)); err != nil { - return err - } - default: - return fmt.Errorf("unsupported type: %v", cap.Type) - } - ioStreams.Infof("Successfully uninstalled capability %s", cap.Name) - return nil -} - -// ListCapabilities will list all caps from specified center -func ListCapabilities(userNamespace string, c common.Args, capabilityCenterName string) ([]types.Capability, error) { - var capabilityList []types.Capability - dir, err := system.GetCapCenterDir() - if err != nil { - return capabilityList, err - } - if capabilityCenterName != "" { - return listCenterCapabilities(userNamespace, c, filepath.Join(dir, capabilityCenterName)) - } - dirs, err := os.ReadDir(dir) - if err != nil { - return capabilityList, err - } - for _, dd := range dirs { - if !dd.IsDir() { - continue - } - caps, err := listCenterCapabilities(userNamespace, c, filepath.Join(dir, dd.Name())) - if err != nil { - return capabilityList, err - } - capabilityList = append(capabilityList, caps...) - } - return capabilityList, nil -} -func listCenterCapabilities(userNamespace string, c common.Args, repoDir string) ([]types.Capability, error) { - dm, err := c.GetDiscoveryMapper() - if err != nil { - return nil, err - } - templates, err := plugins.LoadCapabilityFromSyncedCenter(dm, repoDir) - if err != nil { - return templates, err - } - if len(templates) < 1 { - return templates, nil - } - baseDir := filepath.Base(repoDir) - components := gatherComponents(userNamespace, c, templates) - for i, p := range templates { - status := checkInstallStatus(userNamespace, c, p) - convertedApplyTo := ConvertApplyTo(p.AppliesTo, components) - templates[i].Center = baseDir - templates[i].Status = status - templates[i].AppliesTo = convertedApplyTo - } - return templates, nil -} - -// RemoveCapabilityCenter will remove a cap center from local -func RemoveCapabilityCenter(centerName string) (string, error) { - var message string - var err error - dir, _ := system.GetCapCenterDir() - repoDir := filepath.Join(dir, centerName) - // 1.remove capability center dir - if _, err := os.Stat(repoDir); err != nil { - if os.IsNotExist(err) { - err = fmt.Errorf("%s capability center has not successfully synced", centerName) - return message, err - } - } - if err = os.RemoveAll(repoDir); err != nil { - return message, err - } - // 2.remove center from capability center config - repos, err := plugins.LoadRepos() - if err != nil { - return message, err - } - for idx, r := range repos { - if r.Name == centerName { - repos = append(repos[:idx], repos[idx+1:]...) - break - } - } - if err = plugins.StoreRepos(repos); err != nil { - return message, err - } - message = fmt.Sprintf("%s capability center removed successfully", centerName) - return message, err -} - -func gatherComponents(userNamespace string, c common.Args, templates []types.Capability) []types.Capability { - components, err := plugins.LoadInstalledCapabilityWithType(userNamespace, c, types.TypeComponentDefinition) - if err != nil { - components = make([]types.Capability, 0) - } - for _, t := range templates { - if t.Type == types.TypeComponentDefinition { - components = append(components, t) - } - } - return components -} - -func checkInstallStatus(userNamespace string, c common.Args, tmp types.Capability) string { - var status = "uninstalled" - installed, _ := plugins.LoadInstalledCapabilityWithType(userNamespace, c, tmp.Type) - for _, i := range installed { - if i.Name == tmp.Name && i.CrdName == tmp.CrdName { - return "installed" - } - } - return status -} - -func addSourceIntoExtension(in *runtime.RawExtension, source *types.Source) error { - var extension map[string]interface{} - err := json.Unmarshal(in.Raw, &extension) - if err != nil { - return err - } - extension["source"] = source - data, err := json.Marshal(extension) - if err != nil { - return err - } - in.Raw = data - return nil -} - -// GetCapabilityConfigMap gets the ConfigMap which stores the information of a capability -func GetCapabilityConfigMap(kubeClient client.Client, capabilityName string) (corev1.ConfigMap, error) { - cmName := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, capabilityName) - var cm corev1.ConfigMap - err := kubeClient.Get(context.Background(), client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: cmName}, &cm) - return cm, err -} - -// CheckLabelExistence checks whether a label `key=value` exists in definition labels -func CheckLabelExistence(labels map[string]string, label string) bool { - splitLabel := strings.Split(label, "=") - k, v := splitLabel[0], splitLabel[1] - if labelValue, ok := labels[k]; ok { - if labelValue == v { - return true - } - } - return false -} diff --git a/references/common/component.go b/references/common/component.go deleted file mode 100644 index b64161aaa..000000000 --- a/references/common/component.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -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 common - -import ( - "context" - - "github.com/oam-dev/kubevela/references/apis" - - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// RetrieveComponent will get component status -func RetrieveComponent(ctx context.Context, c client.Reader, applicationName, componentName, - namespace string) (apis.ComponentMeta, error) { - var componentMeta apis.ComponentMeta - applicationMeta, err := RetrieveApplicationStatusByName(ctx, c, applicationName, namespace) - if err != nil { - return componentMeta, err - } - - for _, com := range applicationMeta.Components { - if com.Name != componentName { - continue - } - return com, nil - } - return componentMeta, nil -} diff --git a/references/common/registry.go b/references/common/registry.go new file mode 100644 index 000000000..85d4824d5 --- /dev/null +++ b/references/common/registry.go @@ -0,0 +1,159 @@ +/* +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 common + +import ( + "context" + "encoding/json" + "strings" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils/helm" + cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" +) + +// InstallComponentDefinition will add a component into K8s cluster and install its controller +func InstallComponentDefinition(client client.Client, componentData []byte, ioStreams cmdutil.IOStreams, tp *types.Capability) error { + var cd v1beta1.ComponentDefinition + var err error + if componentData == nil { + return errors.New("componentData is nil") + } + if err = yaml.Unmarshal(componentData, &cd); err != nil { + return err + } + cd.Namespace = types.DefaultKubeVelaNS + ioStreams.Info("Installing component: " + cd.Name) + if tp.Install != nil { + tp.Source.ChartName = tp.Install.Helm.Name + if err = helm.InstallHelmChart(ioStreams, tp.Install.Helm); err != nil { + return err + } + err = addSourceIntoExtension(cd.Spec.Extension, tp.Source) + if err != nil { + return err + } + } + if cd.Spec.Workload.Type == "" { + tp.CrdInfo = &types.CRDInfo{ + APIVersion: cd.Spec.Workload.Definition.APIVersion, + Kind: cd.Spec.Workload.Definition.Kind, + } + } + if err = client.Create(context.Background(), &cd); err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + return nil +} + +// InstallTraitDefinition will add a trait into K8s cluster and install it's controller +func InstallTraitDefinition(client client.Client, mapper discoverymapper.DiscoveryMapper, traitdata []byte, ioStreams cmdutil.IOStreams, cap *types.Capability) error { + var td v1beta1.TraitDefinition + var err error + if err = yaml.Unmarshal(traitdata, &td); err != nil { + return err + } + td.Namespace = types.DefaultKubeVelaNS + ioStreams.Info("Installing trait " + td.Name) + if cap.Install != nil { + cap.Source.ChartName = cap.Install.Helm.Name + if err = helm.InstallHelmChart(ioStreams, cap.Install.Helm); err != nil { + return err + } + err = addSourceIntoExtension(td.Spec.Extension, cap.Source) + if err != nil { + return err + } + } + if err = HackForStandardTrait(*cap, client); err != nil { + return err + } + gvk, err := util.GetGVKFromDefinition(mapper, td.Spec.Reference) + if err != nil { + return err + } + cap.CrdInfo = &types.CRDInfo{ + APIVersion: v1.GroupVersion{ + Group: gvk.Group, + Version: gvk.Version, + }.String(), + Kind: gvk.Kind, + } + if err = client.Create(context.Background(), &td); err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + return nil +} + +// HackForStandardTrait will do some hack install for standard registry +func HackForStandardTrait(tp types.Capability, client client.Client) error { + switch tp.Name { + case "metrics": + // metrics trait will rely on a Prometheus instance to be installed + // make sure the chart is a prometheus operator + if tp.Install == nil { + break + } + if tp.Install.Helm.Namespace == "monitoring" && tp.Install.Helm.Name == "kube-prometheus-stack" { + if err := InstallPrometheusInstance(client); err != nil { + return err + } + } + default: + } + return nil +} + +func addSourceIntoExtension(in *runtime.RawExtension, source *types.Source) error { + var extension map[string]interface{} + err := json.Unmarshal(in.Raw, &extension) + if err != nil { + return err + } + extension["source"] = source + data, err := json.Marshal(extension) + if err != nil { + return err + } + in.Raw = data + return nil +} + +// CheckLabelExistence checks whether a label `key=value` exists in definition labels +func CheckLabelExistence(labels map[string]string, label string) bool { + splitLabel := strings.Split(label, "=") + if len(splitLabel) < 2 { + return false + } + k, v := splitLabel[0], splitLabel[1] + if labelValue, ok := labels[k]; ok { + if labelValue == v { + return true + } + } + return false +} diff --git a/references/common/capability_test.go b/references/common/registry_test.go similarity index 100% rename from references/common/capability_test.go rename to references/common/registry_test.go diff --git a/references/common/trait.go b/references/common/trait.go index f26a298ff..8daf736cd 100644 --- a/references/common/trait.go +++ b/references/common/trait.go @@ -18,7 +18,6 @@ package common import ( "context" - "fmt" "strings" plur "github.com/gertd/go-pluralize" @@ -29,46 +28,8 @@ import ( "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils/common" - "github.com/oam-dev/kubevela/references/plugins" ) -// ListTraitDefinitions will list all definition include traits and workloads -func ListTraitDefinitions(userNamespace string, c common.Args, workloadName *string) ([]types.Capability, error) { - var traitList []types.Capability - traits, err := plugins.LoadInstalledCapabilityWithType(userNamespace, c, types.TypeTrait) - if err != nil { - return traitList, err - } - workloads, err := plugins.LoadInstalledCapabilityWithType(userNamespace, c, types.TypeComponentDefinition) - if err != nil { - return traitList, err - } - traitList = convertAllApplyToList(traits, workloads, workloadName) - return traitList, nil -} - -// ListRawTraitDefinitions will list raw definition -func ListRawTraitDefinitions(userNamespace string, c common.Args) ([]v1beta1.TraitDefinition, error) { - client, err := c.GetClient() - if err != nil { - return nil, err - } - ctx := util.SetNamespaceInCtx(context.Background(), userNamespace) - traitList := v1beta1.TraitDefinitionList{} - ns := ctx.Value(util.AppDefinitionNamespace).(string) - if err = client.List(ctx, &traitList, client2.InNamespace(ns)); err != nil { - return nil, err - } - if ns == oam.SystemDefinitonNamespace { - return traitList.Items, nil - } - sysTraitList := v1beta1.TraitDefinitionList{} - if err = client.List(ctx, &sysTraitList, client2.InNamespace(oam.SystemDefinitonNamespace)); err != nil { - return nil, err - } - return append(traitList.Items, sysTraitList.Items...), nil -} - // ListRawWorkloadDefinitions will list raw definition func ListRawWorkloadDefinitions(userNamespace string, c common.Args) ([]v1beta1.WorkloadDefinition, error) { client, err := c.GetClient() @@ -91,63 +52,6 @@ func ListRawWorkloadDefinitions(userNamespace string, c common.Args) ([]v1beta1. return append(workloadList.Items, sysWorkloadList.Items...), nil } -// ListRawComponentDefinitions will list raw component definition -func ListRawComponentDefinitions(userNamespace string, c common.Args) ([]v1beta1.ComponentDefinition, error) { - client, err := c.GetClient() - if err != nil { - return nil, err - } - ctx := util.SetNamespaceInCtx(context.Background(), userNamespace) - ns := ctx.Value(util.AppDefinitionNamespace).(string) - componentList := v1beta1.ComponentDefinitionList{} - if err = client.List(ctx, &componentList, client2.InNamespace(ns)); err != nil { - return nil, err - } - if ns == oam.SystemDefinitonNamespace { - return componentList.Items, nil - } - sysComponentList := v1beta1.ComponentDefinitionList{} - if err = client.List(ctx, &sysComponentList, client2.InNamespace(oam.SystemDefinitonNamespace)); err != nil { - return nil, err - } - return append(componentList.Items, sysComponentList.Items...), nil -} - -// GetTraitDefinition will get trait capability with applyTo converted -func GetTraitDefinition(userNamespace string, c common.Args, workloadName *string, traitType string) (types.Capability, error) { - var traitDef types.Capability - traitCap, err := plugins.GetInstalledCapabilityWithCapName(types.TypeTrait, traitType) - if err != nil { - return traitDef, err - } - workloadsCap, err := plugins.LoadInstalledCapabilityWithType(userNamespace, c, types.TypeComponentDefinition) - if err != nil { - return traitDef, err - } - traitList := convertAllApplyToList([]types.Capability{traitCap}, workloadsCap, workloadName) - if len(traitList) != 1 { - return traitDef, fmt.Errorf("could not get installed capability by %s", traitType) - } - traitDef = traitList[0] - return traitDef, nil -} - -func convertAllApplyToList(traits []types.Capability, workloads []types.Capability, workloadName *string) []types.Capability { - var traitList []types.Capability - for _, t := range traits { - convertedApplyTo := ConvertApplyTo(t.AppliesTo, workloads) - if *workloadName != "" { - if !in(convertedApplyTo, *workloadName) { - continue - } - convertedApplyTo = []string{*workloadName} - } - t.AppliesTo = convertedApplyTo - traitList = append(traitList, t) - } - return traitList -} - // ConvertApplyTo will convert applyTo slice to workload capability name if CRD matches func ConvertApplyTo(applyTo []string, workloads []types.Capability) []string { var converted []string diff --git a/references/plugins/local.go b/references/plugins/local.go index d2a0a8c11..150487156 100644 --- a/references/plugins/local.go +++ b/references/plugins/local.go @@ -17,18 +17,11 @@ limitations under the License. package plugins import ( - "bytes" "context" - "encoding/json" "fmt" - "os" - "path/filepath" - "strings" "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" "github.com/oam-dev/kubevela/pkg/utils/common" - "github.com/oam-dev/kubevela/pkg/utils/system" ) // LoadCapabilityByName will load capability from local by name @@ -94,176 +87,3 @@ func LoadInstalledCapabilityWithType(userNamespace string, c common.Args, capT t } return nil, nil } - -// GetInstalledCapabilityWithCapName will get cap by alias -func GetInstalledCapabilityWithCapName(capT types.CapType, capName string) (types.Capability, error) { - dir, err := system.GetCapabilityDir() - if err != nil { - return types.Capability{}, err - } - return loadInstalledCapabilityWithCapName(dir, capT, capName) -} - -// leave dir as argument for test convenience -func loadInstalledCapabilityWithType(dir string, capT types.CapType) ([]types.Capability, error) { - dir = GetSubDir(dir, capT) - return loadInstalledCapability(dir, "") -} - -func loadInstalledCapabilityWithCapName(dir string, capT types.CapType, capName string) (types.Capability, error) { - var cap types.Capability - dir = GetSubDir(dir, capT) - capList, err := loadInstalledCapability(dir, capName) - if err != nil { - return cap, err - } else if len(capList) != 1 { - return cap, fmt.Errorf("could not get installed capability by %s", capName) - } - return capList[0], nil -} - -func loadInstalledCapability(dir string, name string) ([]types.Capability, error) { - var tmps []types.Capability - files, err := os.ReadDir(dir) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, err - } - for _, f := range files { - if f.IsDir() { - continue - } - if strings.HasSuffix(f.Name(), ".cue") { - continue - } - data, err := os.ReadFile(filepath.Clean(filepath.Join(dir, f.Name()))) - if err != nil { - fmt.Printf("read file %s err %v\n", f.Name(), err) - continue - } - var tmp types.Capability - decoder := json.NewDecoder(bytes.NewBuffer(data)) - decoder.UseNumber() - if err = decoder.Decode(&tmp); err != nil { - fmt.Printf("ignore invalid format file: %s\n", f.Name()) - continue - } - // Get the specified installed capability: workload or trait - if name != "" { - if name == f.Name() { - tmps = append(tmps, tmp) - break - } - continue - } - tmps = append(tmps, tmp) - } - return tmps, nil -} - -// GetSubDir will get dir for capability -func GetSubDir(dir string, capT types.CapType) string { - switch capT { - case types.TypeWorkload: - return filepath.Join(dir, "workloads") - case types.TypeTrait: - return filepath.Join(dir, "traits") - case types.TypeScope: - return filepath.Join(dir, "scopes") - case types.TypeComponentDefinition: - return filepath.Join(dir, "components") - default: - } - return dir -} - -// SinkTemp2Local will sink template to local file -func SinkTemp2Local(templates []types.Capability, dir string) int { - success := 0 - for _, tmp := range templates { - subDir := GetSubDir(dir, tmp.Type) - _, _ = system.CreateIfNotExist(subDir) - data, err := json.Marshal(tmp) - if err != nil { - fmt.Printf("sync %s err: %v\n", tmp.Name, err) - continue - } - //nolint:gosec - err = os.WriteFile(filepath.Join(subDir, tmp.Name), data, 0644) - if err != nil { - fmt.Printf("sync %s err: %v\n", tmp.Name, err) - continue - } - success++ - } - return success -} - -// RemoveLegacyTemps will remove capability definitions under `dir` but not included in `retainedTemps`. -func RemoveLegacyTemps(retainedTemps []types.Capability, dir string) int { - success := 0 - var retainedFiles []string - subDirs := []string{GetSubDir(dir, types.TypeComponentDefinition), GetSubDir(dir, types.TypeTrait)} - for _, tmp := range retainedTemps { - subDir := GetSubDir(dir, tmp.Type) - tmpFilePath := filepath.Join(subDir, tmp.Name) - retainedFiles = append(retainedFiles, tmpFilePath) - } - - for _, subDir := range subDirs { - if err := filepath.Walk(subDir, func(path string, info os.FileInfo, err error) error { - if info == nil || info.IsDir() { - // omit subDir or subDir not exist - return nil - } - for _, retainedFile := range retainedFiles { - if retainedFile == path { - return nil - } - } - if err := os.Remove(path); err != nil { - fmt.Printf("remove legacy %s err: %v\n", path, err) - return err - } - success++ - return nil - }); err != nil { - continue - } - } - return success -} - -// LoadCapabilityFromSyncedCenter will load capability from dir -func LoadCapabilityFromSyncedCenter(mapper discoverymapper.DiscoveryMapper, dir string) ([]types.Capability, error) { - var tmps []types.Capability - files, err := os.ReadDir(dir) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, err - } - for _, f := range files { - if f.IsDir() { - continue - } - if strings.HasSuffix(f.Name(), ".cue") { - continue - } - data, err := os.ReadFile(filepath.Clean(filepath.Join(dir, f.Name()))) - if err != nil { - fmt.Printf("read file %s err %v\n", f.Name(), err) - continue - } - tmp, err := ParseCapability(mapper, data) - if err != nil { - fmt.Printf("get definition of %s err %v\n", f.Name(), err) - continue - } - tmps = append(tmps, tmp) - } - return tmps, nil -} diff --git a/references/plugins/local_test.go b/references/plugins/local_test.go index 52b0cd709..ca6f8e691 100644 --- a/references/plugins/local_test.go +++ b/references/plugins/local_test.go @@ -17,12 +17,7 @@ limitations under the License. package plugins import ( - "os" - "testing" - "github.com/oam-dev/kubevela/apis/types" - - "github.com/stretchr/testify/assert" ) var ( @@ -60,105 +55,3 @@ var ( }, } ) - -func TestLocalSink(t *testing.T) { - - cases := map[string]struct { - dir string - tmps []types.Capability - Type types.CapType - expDef []types.Capability - err error - }{ - "Test No Templates": { - dir: "vela-test1", - tmps: nil, - }, - "Test Only Workload": { - dir: "vela-test2", - tmps: []types.Capability{deployment, statefulset}, - Type: types.TypeComponentDefinition, - expDef: []types.Capability{deployment, statefulset}, - }, - "Test Only Trait": { - dir: "vela-test3", - tmps: []types.Capability{route}, - Type: types.TypeTrait, - expDef: []types.Capability{route}, - }, - "Test Only Workload But want trait": { - dir: "vela-test3", - tmps: []types.Capability{deployment, statefulset}, - Type: types.TypeTrait, - expDef: nil, - }, - "Test Both have Workload and trait But want Workload": { - dir: "vela-test4", - tmps: []types.Capability{deployment, route, statefulset}, - Type: types.TypeComponentDefinition, - expDef: []types.Capability{deployment, statefulset}, - }, - "Test Both have Workload and trait But want Trait": { - dir: "vela-test5", - tmps: []types.Capability{deployment, route, statefulset}, - Type: types.TypeTrait, - expDef: []types.Capability{route}, - }, - } - for name, c := range cases { - testInDir(t, name, c.dir, c.tmps, c.expDef, c.Type, c.err) - } -} - -func testInDir(t *testing.T, casename, dir string, tmps, defexp []types.Capability, Type types.CapType, err1 error) { - err := os.MkdirAll(dir, 0755) - assert.NoError(t, err, casename) - defer os.RemoveAll(dir) - number := SinkTemp2Local(tmps, dir) - assert.Equal(t, len(tmps), number) - if Type != "" { - gotDef, err := loadInstalledCapabilityWithType(dir, Type) - assert.NoError(t, err, casename) - assert.Equal(t, defexp, gotDef, casename) - } -} - -func TestRemoveLegacyTemps(t *testing.T) { - - cases := []struct { - caseName string - newTemps []types.Capability - rmNum int - }{ - { - caseName: "remove all", - newTemps: []types.Capability{}, - rmNum: 3, - }, - { - caseName: "nothing removed", - newTemps: []types.Capability{deployment, statefulset, route}, - rmNum: 0, - }, - { - caseName: "remove part of existings", - newTemps: []types.Capability{statefulset, route}, - rmNum: 1, - }, - } - for _, c := range cases { - runInDirRemoveLegacyTemps(t, c.caseName, c.newTemps, c.rmNum) - } -} - -func runInDirRemoveLegacyTemps(t *testing.T, caseName string, newTemps []types.Capability, rmNum int) { - dir := "vela-test-rm-temps" - err := os.MkdirAll(dir, 0755) - assert.NoError(t, err, caseName) - defer os.RemoveAll(dir) - existingTemps := []types.Capability{deployment, statefulset, route} - number := SinkTemp2Local(existingTemps, dir) - assert.Equal(t, 3, number) - resultRemoveNum := RemoveLegacyTemps(newTemps, dir) - assert.Equal(t, rmNum, resultRemoveNum, caseName) -} diff --git a/references/plugins/references.go b/references/plugins/references.go index 741bab9bb..6556bb2f5 100644 --- a/references/plugins/references.go +++ b/references/plugins/references.go @@ -708,6 +708,13 @@ type CommonSchema struct { // GenerateHelmAndKubeProperties get all properties of a Helm/Kube Category type capability func (ref *ParseReference) GenerateHelmAndKubeProperties(ctx context.Context, capability *types.Capability) ([]CommonReference, []ConsoleReference, error) { cmName := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, capability.Name) + switch capability.Type { + case types.TypeComponentDefinition: + cmName = fmt.Sprintf("component-%s", cmName) + case types.TypeTrait: + cmName = fmt.Sprintf("trait-%s", cmName) + default: + } var cm v1.ConfigMap commonRefs = make([]CommonReference, 0) if err := ref.Client.Get(ctx, client.ObjectKey{Namespace: capability.Namespace, Name: cmName}, &cm); err != nil { diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go index 5a005b8af..8ca9850ad 100644 --- a/test/e2e-apiserver-test/addon_test.go +++ b/test/e2e-apiserver-test/addon_test.go @@ -8,14 +8,15 @@ import ( "os" "time" - "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/pkg/utils/common" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/pkg/apiserver/model" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" ) diff --git a/test/e2e-multicluster-test/multicluster_rollout_test.go b/test/e2e-multicluster-test/multicluster_rollout_test.go index 3a0c4cb16..83a7de092 100644 --- a/test/e2e-multicluster-test/multicluster_rollout_test.go +++ b/test/e2e-multicluster-test/multicluster_rollout_test.go @@ -1,12 +1,9 @@ /* 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. @@ -38,7 +35,7 @@ import ( "sigs.k8s.io/yaml" ) -var _ = Describe("Test MultiClustet Rollout", func() { +var _ = Describe("Test MultiCluster Rollout", func() { Context("Test Runtime Cluster Rollout", func() { var namespace string var hubCtx context.Context @@ -56,7 +53,7 @@ var _ = Describe("Test MultiClustet Rollout", func() { AfterEach(func() { cleanUpNamespace(hubCtx, workerCtx, namespace) ns := v1.Namespace{} - Eventually(func() error { return k8sClient.Get(hubCtx, types.NamespacedName{Name: namespace}, &ns) }, 300*time.Second, 300*time.Millisecond).Should(util.NotFoundMatcher{}) + Eventually(func() error { return k8sClient.Get(hubCtx, types.NamespacedName{Name: namespace}, &ns) }, 300*time.Second).Should(util.NotFoundMatcher{}) }) verifySucceed := func(componentRevision string) { @@ -99,7 +96,7 @@ var _ = Describe("Test MultiClustet Rollout", func() { return fmt.Errorf("source deploy still exist") } return nil - }, time.Second*360, 300*time.Millisecond).Should(BeNil()) + }, time.Second*360).Should(BeNil()) } It("Test Rollout whole feature in runtime cluster ", func() { @@ -123,7 +120,7 @@ var _ = Describe("Test MultiClustet Rollout", func() { return err } return nil - }, 500*time.Millisecond, 30*time.Second).Should(BeNil()) + }, 30*time.Second).Should(BeNil()) verifySucceed(componentName + "-v2") By("revert to v1, should guarantee compRev v1 still exist") @@ -141,7 +138,7 @@ var _ = Describe("Test MultiClustet Rollout", func() { return err } return nil - }, 500*time.Millisecond, 30*time.Second).Should(BeNil()) + }, 30*time.Second).Should(BeNil()) verifySucceed(componentName + "-v1") }) @@ -173,7 +170,7 @@ var _ = Describe("Test MultiClustet Rollout", func() { return fmt.Errorf("comp status workload check don't work") } return nil - }, 300*time.Millisecond, 30*time.Second).Should(BeNil()) + }, 30*time.Second).Should(BeNil()) By("update application to v2") checkApp := &v1beta1.Application{} Eventually(func() error { @@ -185,7 +182,7 @@ var _ = Describe("Test MultiClustet Rollout", func() { return err } return nil - }, 500*time.Millisecond, 30*time.Second).Should(BeNil()) + }, 30*time.Second).Should(BeNil()) verifySucceed(componentName + "-v2") Eventually(func() error { // Note: KubeVela will only check the workload of the target revision @@ -207,7 +204,7 @@ var _ = Describe("Test MultiClustet Rollout", func() { return fmt.Errorf("comp status workload check don't work") } return nil - }, 300*time.Millisecond, 30*time.Second).Should(BeNil()) + }, 60*time.Second).Should(BeNil()) }) }) }) diff --git a/test/e2e-multicluster-test/multicluster_test.go b/test/e2e-multicluster-test/multicluster_test.go index c284aa6ef..e16f43235 100644 --- a/test/e2e-multicluster-test/multicluster_test.go +++ b/test/e2e-multicluster-test/multicluster_test.go @@ -18,6 +18,7 @@ package e2e_multicluster_test import ( "context" + "encoding/json" "fmt" "io/ioutil" "os" @@ -29,12 +30,14 @@ import ( v13 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" v14 "k8s.io/api/rbac/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" v12 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/multicluster" ) @@ -90,6 +93,35 @@ var _ = Describe("Test multicluster scenario", func() { Expect(out).ShouldNot(ContainSubstring(newClusterName)) }) + It("Test detach cluster with application use", func() { + const testClusterName = "test-cluster" + _, err := execCommand("cluster", "join", "/tmp/worker.kubeconfig", "--name", testClusterName) + Expect(err).Should(Succeed()) + app := &v1beta1.Application{} + bs, err := ioutil.ReadFile("./testdata/app/example-lite-envbinding-app.yaml") + Expect(err).Should(Succeed()) + appYaml := strings.ReplaceAll(string(bs), "TEST_CLUSTER_NAME", testClusterName) + Expect(yaml.Unmarshal([]byte(appYaml), app)).Should(Succeed()) + ctx := context.Background() + err = k8sClient.Create(ctx, app) + Expect(err).Should(Succeed()) + namespacedName := client.ObjectKeyFromObject(app) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, namespacedName, app)).Should(Succeed()) + g.Expect(len(app.Status.PolicyStatus)).ShouldNot(Equal(0)) + }, 30*time.Second).Should(Succeed()) + _, err = execCommand("cluster", "detach", testClusterName) + Expect(err).ShouldNot(Succeed()) + err = k8sClient.Delete(ctx, app) + Expect(err).Should(Succeed()) + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, namespacedName, app) + g.Expect(kerrors.IsNotFound(err)).Should(BeTrue()) + }, 30*time.Second).Should(Succeed()) + _, err = execCommand("cluster", "detach", testClusterName) + Expect(err).Should(Succeed()) + }) + It("Test generate service account kubeconfig", func() { _, workerCtx := initializeContext() // create service account kubeconfig in worker cluster @@ -173,6 +205,7 @@ var _ = Describe("Test multicluster scenario", func() { // 2. Namespace selector. // 3. A special cluster: local cluster // 4. Component selector. + By("apply application") app := &v1beta1.Application{} bs, err := ioutil.ReadFile("./testdata/app/example-envbinding-app.yaml") Expect(err).Should(Succeed()) @@ -182,6 +215,7 @@ var _ = Describe("Test multicluster scenario", func() { err = k8sClient.Create(hubCtx, app) Expect(err).Should(Succeed()) var hubDeployName string + By("wait application resource ready") Eventually(func(g Gomega) { // check deployments in clusters deploys := &v13.DeploymentList{} @@ -194,10 +228,12 @@ var _ = Describe("Test multicluster scenario", func() { deploys = &v13.DeploymentList{} g.Expect(k8sClient.List(workerCtx, deploys, client.InNamespace(prodNamespace))).Should(Succeed()) g.Expect(len(deploys.Items)).Should(Equal(2)) - }, 2*time.Minute).Should(Succeed()) + }, time.Minute).Should(Succeed()) Expect(hubDeployName).Should(Equal("data-worker")) // delete application + By("delete application") Expect(k8sClient.Delete(hubCtx, app)).Should(Succeed()) + By("wait application resource delete") Eventually(func(g Gomega) { // check deployments in clusters deploys := &v13.DeploymentList{} @@ -206,7 +242,81 @@ var _ = Describe("Test multicluster scenario", func() { deploys = &v13.DeploymentList{} g.Expect(k8sClient.List(workerCtx, deploys, client.InNamespace(namespace))).Should(Succeed()) g.Expect(len(deploys.Items)).Should(Equal(0)) - }, 2*time.Minute).Should(Succeed()) + }, time.Minute).Should(Succeed()) + }) + + It("Test create EnvBinding Application with trait disable and without workflow, delete env, change env and add env", func() { + // This test is going to cover multiple functions, including + // 1. disable trait + // 2. auto deploy2env workflow + // 3. delete env + // 4. change cluster in env + // 5. add env + By("apply application") + app := &v1beta1.Application{} + bs, err := ioutil.ReadFile("./testdata/app/example-envbinding-app-wo-workflow.yaml") + Expect(err).Should(Succeed()) + appYaml := strings.ReplaceAll(string(bs), "TEST_NAMESPACE", testNamespace) + Expect(yaml.Unmarshal([]byte(appYaml), app)).Should(Succeed()) + app.SetNamespace(testNamespace) + namespacedName := client.ObjectKeyFromObject(app) + err = k8sClient.Create(hubCtx, app) + Expect(err).Should(Succeed()) + By("wait application resource ready") + Eventually(func(g Gomega) { + // check deployments in clusters + deploys := &v13.DeploymentList{} + g.Expect(k8sClient.List(hubCtx, deploys, client.InNamespace(testNamespace))).Should(Succeed()) + g.Expect(len(deploys.Items)).Should(Equal(1)) + g.Expect(int(*deploys.Items[0].Spec.Replicas)).Should(Equal(2)) + g.Expect(k8sClient.List(workerCtx, deploys, client.InNamespace(testNamespace))).Should(Succeed()) + g.Expect(len(deploys.Items)).Should(Equal(1)) + g.Expect(int(*deploys.Items[0].Spec.Replicas)).Should(Equal(1)) + }, time.Minute).Should(Succeed()) + By("test delete env") + spec := &v1alpha1.EnvBindingSpec{} + Expect(json.Unmarshal(app.Spec.Policies[0].Properties.Raw, spec)).Should(Succeed()) + envs := spec.Envs + bs, err = json.Marshal(&v1alpha1.EnvBindingSpec{Envs: []v1alpha1.EnvConfig{envs[0]}}) + Expect(err).Should(Succeed()) + Expect(k8sClient.Get(hubCtx, namespacedName, app)).Should(Succeed()) + app.Spec.Policies[0].Properties.Raw = bs + Expect(k8sClient.Update(hubCtx, app)).Should(Succeed()) + Eventually(func(g Gomega) { + deploys := &v13.DeploymentList{} + g.Expect(k8sClient.List(workerCtx, deploys, client.InNamespace(testNamespace))).Should(Succeed()) + g.Expect(len(deploys.Items)).Should(Equal(0)) + }, time.Minute).Should(Succeed()) + By("test change env cluster name") + envs[0].Placement.ClusterSelector.Name = WorkerClusterName + bs, err = json.Marshal(&v1alpha1.EnvBindingSpec{Envs: []v1alpha1.EnvConfig{envs[0]}}) + Expect(err).Should(Succeed()) + Expect(k8sClient.Get(hubCtx, namespacedName, app)).Should(Succeed()) + app.Spec.Policies[0].Properties.Raw = bs + Expect(k8sClient.Update(hubCtx, app)).Should(Succeed()) + Eventually(func(g Gomega) { + deploys := &v13.DeploymentList{} + g.Expect(k8sClient.List(hubCtx, deploys, client.InNamespace(testNamespace))).Should(Succeed()) + g.Expect(len(deploys.Items)).Should(Equal(0)) + g.Expect(k8sClient.List(workerCtx, deploys, client.InNamespace(testNamespace))).Should(Succeed()) + g.Expect(len(deploys.Items)).Should(Equal(1)) + }, time.Minute).Should(Succeed()) + By("test add env") + envs[1].Placement.ClusterSelector.Name = multicluster.ClusterLocalName + bs, err = json.Marshal(&v1alpha1.EnvBindingSpec{Envs: envs}) + Expect(err).Should(Succeed()) + Expect(k8sClient.Get(hubCtx, namespacedName, app)).Should(Succeed()) + app.Spec.Policies[0].Properties.Raw = bs + Expect(k8sClient.Update(hubCtx, app)).Should(Succeed()) + Eventually(func(g Gomega) { + deploys := &v13.DeploymentList{} + g.Expect(k8sClient.List(hubCtx, deploys, client.InNamespace(testNamespace))).Should(Succeed()) + g.Expect(len(deploys.Items)).Should(Equal(1)) + g.Expect(int(*deploys.Items[0].Spec.Replicas)).Should(Equal(1)) + g.Expect(k8sClient.List(workerCtx, deploys, client.InNamespace(testNamespace))).Should(Succeed()) + g.Expect(len(deploys.Items)).Should(Equal(1)) + g.Expect(int(*deploys.Items[0].Spec.Replicas)).Should(Equal(2)) + }, time.Minute).Should(Succeed()) }) }) diff --git a/test/e2e-multicluster-test/suite_test.go b/test/e2e-multicluster-test/suite_test.go index 7cf327e1d..ad27694da 100644 --- a/test/e2e-multicluster-test/suite_test.go +++ b/test/e2e-multicluster-test/suite_test.go @@ -18,13 +18,16 @@ package e2e_multicluster_test import ( "bytes" + "context" "testing" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/utils/common" "github.com/oam-dev/kubevela/references/cli" @@ -70,6 +73,16 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - _, err := execCommand("cluster", "detach", WorkerClusterName) - Expect(err).Should(Succeed()) + Eventually(func(g Gomega) { + apps := &v1beta1.ApplicationList{} + Expect(k8sClient.List(context.Background(), apps)).Should(Succeed()) + for _, app := range apps.Items { + Expect(k8sClient.Delete(context.Background(), app.DeepCopy())).Should(Succeed()) + } + Expect(len(apps.Items)).Should(Equal(0)) + }, 3*time.Minute).Should(Succeed()) + Eventually(func(g Gomega) { + _, err := execCommand("cluster", "detach", WorkerClusterName) + Expect(err).Should(Succeed()) + }, time.Minute).Should(Succeed()) }) diff --git a/test/e2e-multicluster-test/testdata/app/example-envbinding-app-wo-workflow.yaml b/test/e2e-multicluster-test/testdata/app/example-envbinding-app-wo-workflow.yaml new file mode 100644 index 000000000..15484dc85 --- /dev/null +++ b/test/e2e-multicluster-test/testdata/app/example-envbinding-app-wo-workflow.yaml @@ -0,0 +1,38 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: example-app + namespace: TEST_NAMESPACE # to be replaced +spec: + components: + - name: hello-world-server + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 2 + policies: + - name: example-multi-env-policy + type: env-binding + properties: + envs: + - name: test + placement: # selecting the cluster to deploy to + clusterSelector: + name: local + + - name: staging + placement: # selecting the cluster to deploy to + clusterSelector: + name: cluster-worker + patch: + components: + - name: hello-world-server + type: webservice + traits: + - type: scaler + disable: true + diff --git a/test/e2e-multicluster-test/testdata/app/example-lite-envbinding-app.yaml b/test/e2e-multicluster-test/testdata/app/example-lite-envbinding-app.yaml new file mode 100644 index 000000000..3e4012284 --- /dev/null +++ b/test/e2e-multicluster-test/testdata/app/example-lite-envbinding-app.yaml @@ -0,0 +1,32 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: example-lite-app + namespace: default +spec: + components: + - name: data-worker + type: worker + properties: + image: busybox + cmd: + - sleep + - '1000000' + policies: + - name: example-multi-env-policy + type: env-binding + properties: + envs: + - name: test + placement: + clusterSelector: + name: TEST_CLUSTER_NAME + + workflow: + steps: + # deploy to test env + - name: deploy-test + type: deploy2env + properties: + policy: example-multi-env-policy + env: test diff --git a/test/e2e-test/helm_app_test.go b/test/e2e-test/helm_app_test.go index 3792f3df9..a140d56f0 100644 --- a/test/e2e-test/helm_app_test.go +++ b/test/e2e-test/helm_app_test.go @@ -372,7 +372,7 @@ var _ = Describe("Test application containing helm module", func() { It("Test store JSON schema of Helm Chart in ConfigMap", func() { By("Get the ConfigMap") - cmName := fmt.Sprintf("schema-%s", cdName) + cmName := fmt.Sprintf("component-schema-%s", cdName) Eventually(func() error { cm := &corev1.ConfigMap{} if err := k8sClient.Get(ctx, client.ObjectKey{Name: cmName, Namespace: namespace}, cm); err != nil { diff --git a/test/e2e-test/kube_app_test.go b/test/e2e-test/kube_app_test.go index 4c18a22ab..f79c07011 100644 --- a/test/e2e-test/kube_app_test.go +++ b/test/e2e-test/kube_app_test.go @@ -342,7 +342,7 @@ spec: It("Test store JSON schema of Kube parameter in ConfigMap", func() { By("Get the ConfigMap") - cmName := fmt.Sprintf("schema-%s", cdName) + cmName := fmt.Sprintf("component-schema-%s", cdName) Eventually(func() error { cm := &corev1.ConfigMap{} if err := k8sClient.Get(ctx, client.ObjectKey{Name: cmName, Namespace: namespace}, cm); err != nil { diff --git a/test/e2e-test/rollout_trait_test.go b/test/e2e-test/rollout_trait_test.go index a3ee7df74..4db9cb26b 100644 --- a/test/e2e-test/rollout_trait_test.go +++ b/test/e2e-test/rollout_trait_test.go @@ -22,17 +22,16 @@ import ( "fmt" "time" - "github.com/oam-dev/kubevela/pkg/oam" - - "sigs.k8s.io/yaml" - v1 "k8s.io/api/apps/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1" + "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils/common" @@ -154,12 +153,13 @@ var _ = Describe("rollout related e2e-test,rollout trait test", func() { return fmt.Errorf("source deploy still exist") } return nil - }, time.Second*360, 300*time.Millisecond).Should(BeNil()) + }, time.Second*60, 300*time.Millisecond).Should(BeNil()) } BeforeEach(func() { By("Start to run a test, init whole env") namespaceName = randomNamespaceName("rollout-trait-e2e-test") + app = v1beta1.Application{} createNamespace() createAllDef() componentName = "express-server" @@ -217,7 +217,7 @@ var _ = Describe("rollout related e2e-test,rollout trait test", func() { if err = k8sClient.Get(ctx, appKey, checkApp); err != nil { return err } - checkApp.Spec.Components[0].Traits[0].Properties.Raw = []byte(`{"targetRevision":"express-server-v2"}`) + checkApp.Spec.Components[0].Traits[0].Properties.Raw = []byte(`{"targetRevision":"express-server-v2","firstBatchReplicas":1,"secondBatchReplicas":1}`) if err = k8sClient.Update(ctx, checkApp); err != nil { return err } @@ -320,6 +320,95 @@ var _ = Describe("rollout related e2e-test,rollout trait test", func() { return nil }, 30*time.Second, 300*time.Millisecond).Should(BeNil()) }) + + It("rollout scale up adnd down without rollout batches", func() { + By("first scale operation") + Expect(common.ReadYamlToObject("testdata/rollout/deployment/application.yaml", &app)).Should(BeNil()) + app.Namespace = namespaceName + Expect(k8sClient.Create(ctx, &app)).Should(BeNil()) + + verifySuccess("express-server-v1") + By("scale again to targetSize 4") + appKey := types.NamespacedName{Namespace: namespaceName, Name: app.Name} + checkApp := &v1beta1.Application{} + Eventually(func() error { + if err = k8sClient.Get(ctx, appKey, checkApp); err != nil { + return err + } + // scale up without rollout batches, test rollout controller will fill default batches + checkApp.Spec.Components[0].Traits[0].Properties.Raw = + []byte(`{"targetSize":4}`) + if err = k8sClient.Update(ctx, checkApp); err != nil { + return err + } + return nil + }, 30*time.Second, 300*time.Millisecond).Should(BeNil()) + Eventually(func() error { + checkRollout := v1alpha1.Rollout{} + if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: namespaceName, Name: componentName}, &checkRollout); err != nil { + return err + } + if *checkRollout.Spec.RolloutPlan.TargetSize != 4 { + return fmt.Errorf("rollout targetSize haven't update") + } + if len(checkRollout.Spec.RolloutPlan.RolloutBatches) != 1 { + return fmt.Errorf("fail to fill rollout batches") + } + if checkRollout.Spec.RolloutPlan.RolloutBatches[0].Replicas != intstr.FromInt(2) { + return fmt.Errorf("fill rollout batches missmatch") + } + return nil + }, 30*time.Second, 300*time.Millisecond).Should(BeNil()) + verifySuccess("express-server-v1") + checkApp = &v1beta1.Application{} + By("update application upgrade to v2") + Eventually(func() error { + if err = k8sClient.Get(ctx, appKey, checkApp); err != nil { + return err + } + checkApp.Spec.Components[0].Properties.Raw = []byte(`{"image":"stefanprodan/podinfo:4.0.3","cpu":"0.1"}`) + checkApp.Spec.Components[0].Traits[0].Properties.Raw = + []byte(`{"firstBatchReplicas":2,"secondBatchReplicas":2,"targetSize":4}`) + if err = k8sClient.Update(ctx, checkApp); err != nil { + return err + } + return nil + }, 30*time.Second, 300*time.Millisecond).Should(BeNil()) + verifySuccess("express-server-v2") + + By("scale down to targetSize 2") + appKey = types.NamespacedName{Namespace: namespaceName, Name: app.Name} + checkApp = &v1beta1.Application{} + Eventually(func() error { + if err = k8sClient.Get(ctx, appKey, checkApp); err != nil { + return err + } + // scale down without rollout batches, test rollout controller will fill default batches + checkApp.Spec.Components[0].Traits[0].Properties.Raw = + []byte(`{"targetSize":2}`) + if err = k8sClient.Update(ctx, checkApp); err != nil { + return err + } + return nil + }, 30*time.Second, 300*time.Millisecond).Should(BeNil()) + Eventually(func() error { + checkRollout := v1alpha1.Rollout{} + if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: namespaceName, Name: componentName}, &checkRollout); err != nil { + return err + } + if *checkRollout.Spec.RolloutPlan.TargetSize != 2 { + return fmt.Errorf("rollout targetSize haven't update") + } + if len(checkRollout.Spec.RolloutPlan.RolloutBatches) != 1 { + return fmt.Errorf("fail to fill rollout batches") + } + if checkRollout.Spec.RolloutPlan.RolloutBatches[0].Replicas != intstr.FromInt(2) { + return fmt.Errorf("fill rollout batches missmatch") + } + return nil + }, 30*time.Second, 300*time.Millisecond).Should(BeNil()) + verifySuccess("express-server-v2") + }) }) const ( @@ -396,9 +485,11 @@ spec: componentName: context.name rolloutPlan: { rolloutStrategy: "DecreaseFirst" - rolloutBatches:[ + if parameter.firstBatchReplicas != _|_ && parameter.secondBatchReplicas != _|_ { + rolloutBatches:[ { replicas: parameter.firstBatchReplicas}, { replicas: parameter.secondBatchReplicas}] + } targetSize: parameter.targetSize if parameter["batchPartition"] != _|_ { batchPartition: parameter.batchPartition @@ -410,8 +501,8 @@ spec: parameter: { targetRevision: *context.revision|string targetSize: *2|int - firstBatchReplicas: *1|int - secondBatchReplicas: *1|int + firstBatchReplicas?: int + secondBatchReplicas?: int batchPartition?: int }` ) diff --git a/test/e2e-test/testdata/rollout/deployment/application.yaml b/test/e2e-test/testdata/rollout/deployment/application.yaml index c5440bc97..2b467db2a 100644 --- a/test/e2e-test/testdata/rollout/deployment/application.yaml +++ b/test/e2e-test/testdata/rollout/deployment/application.yaml @@ -11,4 +11,6 @@ spec: traits: - type: rollout properties: - targetSize: 2 \ No newline at end of file + targetSize: 2 + firstBatchReplicas: 1 + secondBatchReplicas: 1 \ No newline at end of file diff --git a/vela-templates/addons/auto-gen/keda.yaml b/vela-templates/addons/auto-gen/keda.yaml deleted file mode 100644 index fd2e104bb..000000000 --- a/vela-templates/addons/auto-gen/keda.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: core.oam.dev/v1beta1 -kind: Application -metadata: - annotations: - addons.oam.dev/description: KEDA is a Kubernetes-based Event Driven Autoscaler. - name: keda - namespace: vela-system -spec: - components: - - name: keda - properties: - chart: keda - repoType: helm - url: https://kedacore.github.io/charts - type: helm - workflow: - steps: - - name: checking-depends-on - properties: - name: fluxcd - namespace: vela-system - type: depends-on-app - - name: apply-resources - type: apply-application -status: {} diff --git a/vela-templates/addons/auto-gen/prometheus.yaml b/vela-templates/addons/auto-gen/prometheus.yaml deleted file mode 100644 index beed8a9f9..000000000 --- a/vela-templates/addons/auto-gen/prometheus.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: core.oam.dev/v1beta1 -kind: Application -metadata: - annotations: - addons.oam.dev/description: Prometheus is an open-source systems monitoring and - alerting toolkit - name: prometheus - namespace: vela-system -spec: - components: - - name: prometheus - properties: - chart: premetheus - repoType: helm - url: https://prometheus-community.github.io/helm-charts - type: helm - workflow: - steps: - - name: checking-depends-on - properties: - name: fluxcd - namespace: vela-system - type: depends-on-app - - name: apply-resources - type: apply-application -status: {} diff --git a/vela-templates/addons/auto-gen/terraform-provider-alibaba.yaml b/vela-templates/addons/auto-gen/terraform-provider-alibaba.yaml index 6be1ca7fb..1df824fad 100644 --- a/vela-templates/addons/auto-gen/terraform-provider-alibaba.yaml +++ b/vela-templates/addons/auto-gen/terraform-provider-alibaba.yaml @@ -5,7 +5,7 @@ metadata: addons.oam.dev/description: Kubernetes Terraform Controller for Alibaba Cloud addons.oam.dev/name: terraform/provider-alibaba name: terraform-provider-alibaba - namespace: default + namespace: vela-system spec: components: - name: alibaba-account-creds diff --git a/vela-templates/addons/auto-gen/terraform-provider-aws.yaml b/vela-templates/addons/auto-gen/terraform-provider-aws.yaml index 560cb1fea..cb83db655 100644 --- a/vela-templates/addons/auto-gen/terraform-provider-aws.yaml +++ b/vela-templates/addons/auto-gen/terraform-provider-aws.yaml @@ -5,7 +5,7 @@ metadata: addons.oam.dev/description: Kubernetes Terraform Controller for AWS addons.oam.dev/name: terraform/provider-aws name: terraform-provider-aws - namespace: default + namespace: vela-system spec: components: - name: aws-account-creds diff --git a/vela-templates/addons/auto-gen/terraform-provider-azure.yaml b/vela-templates/addons/auto-gen/terraform-provider-azure.yaml index d8b0e96f7..2ed3a6d16 100644 --- a/vela-templates/addons/auto-gen/terraform-provider-azure.yaml +++ b/vela-templates/addons/auto-gen/terraform-provider-azure.yaml @@ -5,7 +5,7 @@ metadata: addons.oam.dev/description: Kubernetes Terraform Controller for Azure addons.oam.dev/name: terraform/provider-azure name: terraform-provider-azure - namespace: default + namespace: vela-system spec: components: - name: azure-account-creds diff --git a/vela-templates/addons/keda/readme.md b/vela-templates/addons/keda/readme.md deleted file mode 100644 index 8cb7a9f0f..000000000 --- a/vela-templates/addons/keda/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# keda - -keda \ No newline at end of file diff --git a/vela-templates/addons/keda/template.yaml b/vela-templates/addons/keda/template.yaml deleted file mode 100644 index 716636b5e..000000000 --- a/vela-templates/addons/keda/template.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: core.oam.dev/v1beta1 -kind: Application -metadata: - annotations: - addons.oam.dev/description: "KEDA is a Kubernetes-based Event Driven Autoscaler." - name: keda - namespace: vela-system -spec: - workflow: - steps: - - name: checking-depends-on - type: depends-on-app - properties: - name: fluxcd - namespace: vela-system - - name: apply-resources - type: apply-application - components: - - name: keda - type: helm - properties: - repoType: helm - url: https://kedacore.github.io/charts - chart: keda -{{ range .ResourceFiles }} - - name: {{ .Name }} - type: raw - properties: -{{ .Content | indent 8 }} {{ end }} -{{ range .DefinitionFiles }} - - name: {{ .Name }} - type: raw - properties: -{{ .Content | indent 8 }} {{ end }} diff --git a/vela-templates/addons/prometheus/readme.md b/vela-templates/addons/prometheus/readme.md deleted file mode 100644 index 4917cfd76..000000000 --- a/vela-templates/addons/prometheus/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# prometheus - -prometheus diff --git a/vela-templates/addons/prometheus/template.yaml b/vela-templates/addons/prometheus/template.yaml deleted file mode 100644 index e3ba0b39d..000000000 --- a/vela-templates/addons/prometheus/template.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: core.oam.dev/v1beta1 -kind: Application -metadata: - annotations: - addons.oam.dev/description: "Prometheus is an open-source systems monitoring and alerting toolkit" - name: prometheus - namespace: vela-system -spec: - workflow: - steps: - - name: checking-depends-on - type: depends-on-app - properties: - name: fluxcd - namespace: vela-system - - name: apply-resources - type: apply-application - components: - - name: prometheus - type: helm - properties: - repoType: helm - url: https://prometheus-community.github.io/helm-charts - chart: premetheus -{{ range .DefinitionFiles }} - - name: {{ .Name }} - type: raw - properties: -{{ .Content | indent 4 }} {{ end }} diff --git a/vela-templates/addons/terraform-provider-alibaba/template.yaml b/vela-templates/addons/terraform-provider-alibaba/template.yaml index a77f6075f..6c98e9b5b 100644 --- a/vela-templates/addons/terraform-provider-alibaba/template.yaml +++ b/vela-templates/addons/terraform-provider-alibaba/template.yaml @@ -5,7 +5,7 @@ metadata: addons.oam.dev/description: Kubernetes Terraform Controller for Alibaba Cloud addons.oam.dev/name: terraform/provider-alibaba name: terraform-provider-alibaba - namespace: default + namespace: vela-system spec: components: - name: alibaba-account-creds diff --git a/vela-templates/addons/terraform-provider-aws/template.yaml b/vela-templates/addons/terraform-provider-aws/template.yaml index c997ac499..84f7c49ee 100644 --- a/vela-templates/addons/terraform-provider-aws/template.yaml +++ b/vela-templates/addons/terraform-provider-aws/template.yaml @@ -5,7 +5,7 @@ metadata: addons.oam.dev/description: Kubernetes Terraform Controller for AWS addons.oam.dev/name: terraform/provider-aws name: terraform-provider-aws - namespace: default + namespace: vela-system spec: components: - name: aws-account-creds diff --git a/vela-templates/addons/terraform-provider-azure/template.yaml b/vela-templates/addons/terraform-provider-azure/template.yaml index 027556a50..fb431b05d 100644 --- a/vela-templates/addons/terraform-provider-azure/template.yaml +++ b/vela-templates/addons/terraform-provider-azure/template.yaml @@ -5,7 +5,7 @@ metadata: addons.oam.dev/description: Kubernetes Terraform Controller for Azure addons.oam.dev/name: terraform/provider-azure name: terraform-provider-azure - namespace: default + namespace: vela-system spec: components: - name: azure-account-creds diff --git a/vela-templates/definitions/internal/nocalhost.cue b/vela-templates/definitions/internal/nocalhost.cue index b8a442df9..3277d45e1 100644 --- a/vela-templates/definitions/internal/nocalhost.cue +++ b/vela-templates/definitions/internal/nocalhost.cue @@ -14,11 +14,31 @@ nocalhost: { } template: { + outputs: { + nocalhostService: { + apiVersion: "v1" + kind: "Service" + metadata: name: context.name + spec: { + selector: "app.oam.dev/component": context.name + ports: [ + { + port: parameter.port + targetPort: parameter.port + }, + ] + type: "ClusterIP" + } + } + } + patch: { metadata: annotations: { "dev.nocalhost/application-name": context.appName "dev.nocalhost/application-namespace": context.namespace "dev.nocalhost": json.Marshal({ + name: context.name + serviceType: parameter.serviceType "containers": [ { "name": context.name @@ -26,7 +46,24 @@ template: { if parameter.gitUrl != _|_ { "gitUrl": parameter.gitUrl } - "image": parameter.image + if parameter.image == "go" { + "image": "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/golang:latest" + } + if parameter.image == "java" { + "image": "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/java:latest" + } + if parameter.image == "python" { + "image": "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/python:latest" + } + if parameter.image == "node" { + "image": "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/node:latest" + } + if parameter.image == "ruby" { + "image": "nocalhost-docker.pkg.coding.net/nocalhost/dev-images/ruby:latest" + } + if parameter.image != "go" && parameter.image != "java" && parameter.image != "python" && parameter.image != "node" && parameter.image != "ruby" { + "image": parameter.image + } "shell": parameter.shell "workDir": parameter.workDir if parameter.storageClass != _|_ { @@ -65,30 +102,36 @@ template: { if parameter.portForward != _|_ { "portForward": parameter.portForward } + if parameter.portForward == _|_ { + "portForward": ["\(parameter.port)" + ":" + "\(parameter.port)"] + } } }, ] }) } } + language: "go" | "java" | "python" | "node" | "ruby" parameter: { + port: int + serviceType: *"deployment" | string gitUrl?: string - image: string + image: language | string shell: *"bash" | string workDir: *"/home/nocalhost-dev" | string storageClass?: string - command?: { - run?: [...string] - debug?: [...string] + command: { + run: *["sh", "run.sh"] | [...string] + debug: *["sh", "debug.sh"] | [...string] } debug?: { remoteDebugPort?: int } hotReload: *true | bool sync: { - type: *"send" | string - filePattern?: [...string] - ignoreFilePattern?: [...string] + type: *"send" | string + filePattern: *["./"] | [...string] + ignoreFilePattern: *[".git", ".vscode", ".idea", ".gradle", "build"] | [...string] } env?: [...{ name: string diff --git a/vela-templates/definitions/internal/rollout.cue b/vela-templates/definitions/internal/rollout.cue index 826fdc097..82c396f52 100644 --- a/vela-templates/definitions/internal/rollout.cue +++ b/vela-templates/definitions/internal/rollout.cue @@ -20,8 +20,10 @@ template: { componentName: context.name rolloutPlan: { rolloutStrategy: "IncreaseFirst" - rolloutBatches: parameter.rolloutBatches - targetSize: parameter.targetSize + if parameter.rolloutBatches != _|_ { + rolloutBatches: parameter.rolloutBatches + } + targetSize: parameter.targetSize if parameter["batchPartition"] != _|_ { batchPartition: parameter.batchPartition } @@ -32,7 +34,7 @@ template: { parameter: { targetRevision: *context.revision | string targetSize: int - rolloutBatches: [...rolloutBatch] + rolloutBatches?: [...rolloutBatch] batchPartition?: int } diff --git a/vela-templates/definitions/internal/webhook-notification.cue b/vela-templates/definitions/internal/webhook-notification.cue index 5e2f4f169..6132c69f8 100644 --- a/vela-templates/definitions/internal/webhook-notification.cue +++ b/vela-templates/definitions/internal/webhook-notification.cue @@ -96,17 +96,17 @@ template: { url?: string value?: string style?: string - text?: text + text?: textType confirm?: { - title: text - text: text - confirm: text - deny: text + title: textType + text: textType + confirm: textType + deny: textType style?: string } options?: [...option] initial_options?: [...option] - placeholder?: text + placeholder?: textType initial_date?: string image_url?: string alt_text?: string @@ -123,7 +123,7 @@ template: { }] } - text: { + textType: { type: string text: string emoji?: bool @@ -131,9 +131,9 @@ template: { } option: { - text: text + text: textType value: string - description?: text + description?: textType url?: string } From 49ba77c0d37ed6f91826c813cd005b9f80f90d27 Mon Sep 17 00:00:00 2001 From: qiaozp <47812250+chivalryq@users.noreply.github.com> Date: Wed, 10 Nov 2021 12:41:29 +0800 Subject: [PATCH 28/59] Feat: add addon registry update API (#2671) * add addon registry update API * add detailed cache, fix cache bug * use PUT * add Reads * use UpdateAddonRegistryRequest --- pkg/apiserver/rest/apis/v1/types.go | 4 ++ pkg/apiserver/rest/usecase/addon.go | 38 ++++++++++++++++--- pkg/apiserver/rest/utils/bcode/addon.go | 3 ++ .../rest/webservice/addon_registry.go | 36 +++++++++++++++++- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index a010d0e6d..27eefbf4a 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -59,6 +59,10 @@ type CreateAddonRegistryRequest struct { Git *model.GitAddonSource `json:"git,omitempty" validate:"required"` } +type UpdateAddonRegistryRequest struct { + Git *model.GitAddonSource `json:"git,omitempty" validate:"required"` +} + // AddonRegistryMeta defines the format for a single addon registry type AddonRegistryMeta struct { Name string `json:"name" validate:"required"` diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index b4d6706b5..0487f9706 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -66,9 +66,10 @@ const ( // AddonUsecase addon usecase type AddonUsecase interface { - GetAddonRegistryModel(ctx context.Context, name string) (*model.AddonRegistry, error) + GetAddonRegistry(ctx context.Context, name string) (*model.AddonRegistry, error) CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) DeleteAddonRegistry(ctx context.Context, name string) error + UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) StatusAddon(name string) (*apis.AddonStatusResponse, error) @@ -143,6 +144,13 @@ func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, } } +// getCacheKeyWithDetailFLag will get right cache key for given registry and detailed, to split different +func getCacheKeyWithDetailFLag(registry string, detailed bool) string { + if detailed { + return registry + "detailed" + } + return registry +} func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) { var addons []*apis.DetailAddonResponse rs, err := u.ListAddonRegistries(ctx) @@ -156,15 +164,15 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, regist } var gitAddons []*apis.DetailAddonResponse - if u.isRegistryCacheUpToDate(registry) { - gitAddons = u.getRegistryCache(registry) + if u.isRegistryCacheUpToDate(getCacheKeyWithDetailFLag(registry, detailed)) { + gitAddons = u.getRegistryCache(getCacheKeyWithDetailFLag(registry, detailed)) } else { gitAddons, err = getAddonsFromGit(r.Git.URL, r.Git.Path, r.Git.Token, detailed) if err != nil { log.Logger.Errorf("fail to get addons from registry %s", r.Name) continue } - u.putRegistryCache(registry, gitAddons) + u.putRegistryCache(getCacheKeyWithDetailFLag(registry, detailed), gitAddons) } addons = mergeAddons(addons, gitAddons) @@ -208,7 +216,7 @@ func (u *addonUsecaseImpl) CreateAddonRegistry(ctx context.Context, req apis.Cre }, nil } -func (u *addonUsecaseImpl) GetAddonRegistryModel(ctx context.Context, name string) (*model.AddonRegistry, error) { +func (u *addonUsecaseImpl) GetAddonRegistry(ctx context.Context, name string) (*model.AddonRegistry, error) { var r = model.AddonRegistry{ Name: name, } @@ -219,6 +227,26 @@ func (u *addonUsecaseImpl) GetAddonRegistryModel(ctx context.Context, name strin return &r, nil } +func (u addonUsecaseImpl) UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) { + var r = model.AddonRegistry{ + Name: name, + } + err := u.addonRegistryDS.Get(ctx, &r) + if err != nil { + return nil, bcode.ErrAddonRegistryNotExist + } + r.Git = req.Git + err = u.addonRegistryDS.Put(ctx, &r) + if err != nil { + return nil, err + } + + return &apis.AddonRegistryMeta{ + Name: r.Name, + Git: r.Git, + }, nil +} + func (u *addonUsecaseImpl) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) { var r = model.AddonRegistry{} diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go index 699f5bc51..b84bdc45b 100644 --- a/pkg/apiserver/rest/utils/bcode/addon.go +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -33,6 +33,9 @@ var ( // ErrAddonRegistryRateLimit addon registry is rate limited by Github ErrAddonRegistryRateLimit = NewBcode(400, 50004, "Exceed Github rate limit") + // ErrAddonRegistryNotExist addon registry doesn't exist + ErrAddonRegistryNotExist = NewBcode(400, 50006, "addon registry doesn't exist") + // ErrAddonRender fail to render addon application ErrAddonRender = NewBcode(500, 50010, "addon render fail") diff --git a/pkg/apiserver/rest/webservice/addon_registry.go b/pkg/apiserver/rest/webservice/addon_registry.go index 2b44d9690..2fa89d573 100644 --- a/pkg/apiserver/rest/webservice/addon_registry.go +++ b/pkg/apiserver/rest/webservice/addon_registry.go @@ -70,6 +70,15 @@ func (s *addonRegistryWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.AddonRegistryMeta{})) + ws.Route(ws.PUT("/{name}").To(s.updateAddonRegistry). + Doc("update an addon registry"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdateAddonRegistryRequest{}). + Param(ws.PathParameter("name", "identifier of the addon registry").DataType("string")). + Returns(200, "", apis.AddonRegistryMeta{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.AddonRegistryMeta{})) + return ws } @@ -100,7 +109,7 @@ func (s *addonRegistryWebService) createAddonRegistry(req *restful.Request, res } func (s *addonRegistryWebService) deleteAddonRegistry(req *restful.Request, res *restful.Response) { - r, err := s.addonUsecase.GetAddonRegistryModel(req.Request.Context(), req.PathParameter("name")) + r, err := s.addonUsecase.GetAddonRegistry(req.Request.Context(), req.PathParameter("name")) if err != nil { bcode.ReturnError(req, res, err) return @@ -128,3 +137,28 @@ func (s *addonRegistryWebService) listAddonRegistry(req *restful.Request, res *r return } } + +func (s *addonRegistryWebService) updateAddonRegistry(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var updateReq apis.UpdateAddonRegistryRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + meta, err := s.addonUsecase.UpdateAddonRegistry(req.Request.Context(), req.PathParameter("name"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(meta); err != nil { + bcode.ReturnError(req, res, err) + return + } +} From 8a3b7b6a056ee6f47b1f8c443800a5e74b0e2306 Mon Sep 17 00:00:00 2001 From: yangsoon Date: Thu, 11 Nov 2021 13:40:14 +0800 Subject: [PATCH 29/59] Feat: add query provider and get view template from configmap (#2619) --- .../velaql-views/componet-pod-view.yaml | 107 +++ pkg/stdlib/op.cue | 4 + pkg/stdlib/packages.go | 17 +- pkg/stdlib/pkgs/kube.cue | 1 + pkg/stdlib/pkgs/query.cue | 27 + pkg/stdlib/pkgs/time.cue | 22 + pkg/stdlib/ql.cue | 11 + pkg/velaql/parse.go | 24 +- pkg/velaql/parse_test.go | 8 +- pkg/velaql/providers/query/collector.go | 346 ++++++++++ pkg/velaql/providers/query/handler.go | 151 +++++ pkg/velaql/providers/query/handler_test.go | 623 ++++++++++++++++++ pkg/velaql/providers/query/suite_test.go | 76 +++ pkg/velaql/{suit_test.go => suite_test.go} | 5 +- pkg/velaql/testdata/apply-object.yaml | 50 +- pkg/velaql/testdata/read-object.yaml | 102 ++- pkg/velaql/view.go | 56 +- pkg/workflow/providers/kube/handle.go | 3 +- pkg/workflow/providers/time/date.go | 89 +++ pkg/workflow/providers/time/date_test.go | 170 +++++ pkg/workflow/tasks/discover.go | 26 +- pkg/workflow/tasks/template/load.go | 40 +- pkg/workflow/tasks/template/load_test.go | 2 +- .../testdata/component-pod-view.yaml | 92 +++ .../testdata/read-view.yaml | 53 ++ test/e2e-apiserver-test/velaql_test.go | 193 +++++- 26 files changed, 2151 insertions(+), 147 deletions(-) create mode 100644 docs/examples/velaql-views/componet-pod-view.yaml create mode 100644 pkg/stdlib/pkgs/query.cue create mode 100644 pkg/stdlib/pkgs/time.cue create mode 100644 pkg/stdlib/ql.cue create mode 100644 pkg/velaql/providers/query/collector.go create mode 100644 pkg/velaql/providers/query/handler.go create mode 100644 pkg/velaql/providers/query/handler_test.go create mode 100644 pkg/velaql/providers/query/suite_test.go rename pkg/velaql/{suit_test.go => suite_test.go} (95%) create mode 100644 pkg/workflow/providers/time/date.go create mode 100644 pkg/workflow/providers/time/date_test.go create mode 100644 test/e2e-apiserver-test/testdata/component-pod-view.yaml create mode 100644 test/e2e-apiserver-test/testdata/read-view.yaml diff --git a/docs/examples/velaql-views/componet-pod-view.yaml b/docs/examples/velaql-views/componet-pod-view.yaml new file mode 100644 index 000000000..b45d7fc77 --- /dev/null +++ b/docs/examples/velaql-views/componet-pod-view.yaml @@ -0,0 +1,107 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: component-pod-view + namespace: vela-system +data: + template: | + import ( + "vela/ql" + "vela/op" + "list" + ) + + parameter: { + name: string + namespace: string + componentName: string + } + + application: ql.#ListResourcesInApp & { + app: { + name: parameter.name + namespace: parameter.namespace + components: [parameter.componentName] + } + } + + app: application.list[0] + resources: app.components[0].resources + + podsMap: op.#Steps & { + for i, resource in resources { + "\(i)": ql.#CollectPods & { + value: resource.object + cluster: resource.cluster + } + } + } + + podsWithCluster: {for i, pods in podsMap { + "\(i)": [ + for podObj in pods.list { + cluster: pods.cluster + obj: podObj + }, + ] + }} + + flatPods: list.FlattenN([ for pods in podsWithCluster { + pods + }], 1) + + podStatus: op.#Steps & { + for i, pod in flatPods { + "\(i)": op.#Steps & { + name: pod.obj.metadata.name + containers: {for container in pod.obj.status.containerStatuses { + "\(container.name)": { + image: container.image + state: container.state + } + }} + events: ql.#SearchEvents & { + value: pod.obj + cluster: pod.cluster + } + metrics: ql.#Read & { + cluster: pod.cluster + value: { + apiVersion: "metrics.k8s.io/v1beta1" + kind: "PodMetrics" + metadata: { + name: pod.obj.metadata.name + namespace: pod.obj.metadata.namespace + } + } + } + } + } + } + + status: { + podList: [ for podInfo in podStatus { + name: podInfo.name + containers: [ for containerName, container in podInfo.containers { + name: containerName + image: container.image + state: container.state + if podInfo.metrics.err == _|_ { + usage: {for containerUsage in podInfo.metrics.value.containers { + if containerUsage.name == containerName { + cpu: containerUsage.usage.cpu + memory: containerUsage.usage.memory + } + }} + } + }] + events: [ for event in podInfo.events.list { + type: event.type + reason: event.reason + message: event.message + firstTimestamp: event.firstTimestamp + }] + }] + } + + diff --git a/pkg/stdlib/op.cue b/pkg/stdlib/op.cue index 8071f8ab6..923c25ed9 100644 --- a/pkg/stdlib/op.cue +++ b/pkg/stdlib/op.cue @@ -117,6 +117,10 @@ import ( #ConvertString: convert.#String +#DateToTimestamp: time.#DateToTimestamp + +#TimestampToDate: time.#TimestampToDate + #SendEmail: email.#Send #Load: oam.#LoadComponets diff --git a/pkg/stdlib/packages.go b/pkg/stdlib/packages.go index 9a2ed616f..869211ad8 100644 --- a/pkg/stdlib/packages.go +++ b/pkg/stdlib/packages.go @@ -26,7 +26,7 @@ import ( ) var ( - //go:embed pkgs op.cue + //go:embed pkgs op.cue ql.cue fs embed.FS ) @@ -43,17 +43,26 @@ func GetPackages(tagTempl string) (map[string]string, error) { return nil, err } - pkgContent := string(opBytes) + "\n" + qlBytes, err := fs.ReadFile("ql.cue") + if err != nil { + return nil, err + } + + opContent := string(opBytes) + "\n" + qlContent := string(qlBytes) + "\n" for _, file := range files { body, err := fs.ReadFile("pkgs/" + file.Name()) if err != nil { return nil, err } - pkgContent += fmt.Sprintf("%s: {\n%s\n}\n", strings.TrimSuffix(file.Name(), ".cue"), string(body)) + pkgContent := fmt.Sprintf("%s: {\n%s\n}\n", strings.TrimSuffix(file.Name(), ".cue"), string(body)) + opContent += pkgContent + qlContent += pkgContent } return map[string]string{ - "vela/op": pkgContent + "\n" + tagTempl, + "vela/op": opContent + "\n" + tagTempl, + "vela/ql": qlContent + "\n" + tagTempl, }, nil } diff --git a/pkg/stdlib/pkgs/kube.cue b/pkg/stdlib/pkgs/kube.cue index a825ef1e3..0e6727f09 100644 --- a/pkg/stdlib/pkgs/kube.cue +++ b/pkg/stdlib/pkgs/kube.cue @@ -27,6 +27,7 @@ matchingLabels?: {...} } list?: {...} + ... } #Delete: { diff --git a/pkg/stdlib/pkgs/query.cue b/pkg/stdlib/pkgs/query.cue new file mode 100644 index 000000000..32661cb7f --- /dev/null +++ b/pkg/stdlib/pkgs/query.cue @@ -0,0 +1,27 @@ +#ListResourcesInApp: { + #do: "listResourcesInApp" + #provider: "query" + app: { + name: string + namespace: string + components?: [...string] + enableHistoryQuery?: bool + } + ... +} + +#CollectPods: { + #do: "collectPods" + #provider: "query" + value: {...} + cluster: string + ... +} + +#SearchEvents: { + #do: "searchEvents" + #provider: "query" + value: {...} + cluster: string + ... +} diff --git a/pkg/stdlib/pkgs/time.cue b/pkg/stdlib/pkgs/time.cue new file mode 100644 index 000000000..f3945b6a2 --- /dev/null +++ b/pkg/stdlib/pkgs/time.cue @@ -0,0 +1,22 @@ +#DateToTimestamp: { + #do: "timestamp" + #provider: "time" + + date: string + layout: *"" | string + + timestamp?: int64 + ... +} + +#TimestampToDate: { + #do: "date" + #provider: "time" + + timestamp: int64 + layout: *"" | string + location: *"" | string + + date?: string + ... +} diff --git a/pkg/stdlib/ql.cue b/pkg/stdlib/ql.cue new file mode 100644 index 000000000..48e3a7d4a --- /dev/null +++ b/pkg/stdlib/ql.cue @@ -0,0 +1,11 @@ +#Read: kube.#Read + +#List: kube.#List + +#Delete: kube.#Delete + +#ListResourcesInApp: query.#ListResourcesInApp + +#CollectPods: query.#CollectPods + +#SearchEvents: query.#SearchEvents diff --git a/pkg/velaql/parse.go b/pkg/velaql/parse.go index 2b2b3d8e1..6aab4b18e 100644 --- a/pkg/velaql/parse.go +++ b/pkg/velaql/parse.go @@ -24,8 +24,8 @@ import ( "github.com/pkg/errors" ) -// Query contains query data -type Query struct { +// QueryView contains query data +type QueryView struct { View string Parameter map[string]interface{} Export string @@ -56,16 +56,16 @@ func init() { kvRegexp = regexp.MustCompile(PatternKV) } -// ParseVelaQL parse velaQL to Query -func ParseVelaQL(ql string) (Query, error) { - query := Query{ +// ParseVelaQL parse velaQL to QueryView +func ParseVelaQL(ql string) (QueryView, error) { + qv := QueryView{ Export: DefaultExportValue, } groupNames := qlRegexp.SubexpNames() matched := qlRegexp.FindStringSubmatch(ql) if len(matched) != len(groupNames) || (len(matched) != 0 && matched[0] != ql) { - return query, errors.New("fail to parse the velaQL") + return qv, errors.New("fail to parse the velaQL") } result := make(map[string]string, len(groupNames)) @@ -76,19 +76,19 @@ func ParseVelaQL(ql string) (Query, error) { } if len(result["view"]) == 0 { - return query, errors.New("view name shouldn't be empty") + return qv, errors.New("view name shouldn't be empty") } - query.View = result[KeyWordView] + qv.View = result[KeyWordView] if len(result[KeyWordExport]) != 0 { - query.Export = result[KeyWordExport] + qv.Export = result[KeyWordExport] } var err error - query.Parameter, err = ParseParameter(result[KeyWordParameter]) + qv.Parameter, err = ParseParameter(result[KeyWordParameter]) if err != nil { - return query, err + return qv, err } - return query, nil + return qv, nil } // ParseParameter parse parameter to map[string]interface{} diff --git a/pkg/velaql/parse_test.go b/pkg/velaql/parse_test.go index 2088a8a5a..1867cf606 100644 --- a/pkg/velaql/parse_test.go +++ b/pkg/velaql/parse_test.go @@ -26,7 +26,7 @@ import ( func TestParseVelaQL(t *testing.T) { testcases := []struct { ql string - query Query + query QueryView err error }{{ ql: `view{test=,test1=hello}.output`, @@ -39,21 +39,21 @@ func TestParseVelaQL(t *testing.T) { err: errors.New("fail to parse the velaQL"), }, { ql: `view{test=1,app="name"}`, - query: Query{ + query: QueryView{ View: "view", Export: "status", }, err: nil, }, { ql: `view{test=1,app="name"}.Export`, - query: Query{ + query: QueryView{ View: "view", Export: "Export", }, err: nil, }, { ql: `view{test=true}.output.value.spec`, - query: Query{ + query: QueryView{ View: "view", Export: "output.value.spec", }, diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go new file mode 100644 index 000000000..7007c2a7a --- /dev/null +++ b/pkg/velaql/providers/query/collector.go @@ -0,0 +1,346 @@ +/* + 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 query + +import ( + "context" + "fmt" + "reflect" + + kruise "github.com/openkruise/kruise-api/apps/v1alpha1" + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/dispatch" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" + oamutil "github.com/oam-dev/kubevela/pkg/oam/util" +) + +// Collector collect resource created by application +type Collector struct { + k8sClient client.Client + opt Option +} + +// NewCollector create a collector +func NewCollector(cli client.Client, opt Option) *Collector { + return &Collector{ + k8sClient: cli, + opt: opt, + } +} + +// CollectResourceFromApp collect resources created by application +func (c *Collector) CollectResourceFromApp() ([]AppResources, error) { + if c.opt.EnableHistoryQuery { + return c.CollectHistoryResourceFromApp() + } + return c.CollectLatestResourceFromApp() +} + +// CollectLatestResourceFromApp collect resources created by latest application +func (c *Collector) CollectLatestResourceFromApp() ([]AppResources, error) { + ctx := context.Background() + app := new(v1beta1.Application) + appKey := client.ObjectKey{Name: c.opt.Name, Namespace: c.opt.Namespace} + if err := c.k8sClient.Get(ctx, appKey, app); err != nil { + return nil, err + } + + var revision int64 + if app.Status.LatestRevision != nil { + revision = app.Status.LatestRevision.Revision + } + appRevName := fmt.Sprintf("%s-v%d", app.Name, revision) + comps := make(map[string][]Resource, len(app.Spec.Components)) + for _, rsrcRef := range app.Status.AppliedResources { + compName, obj, err := getObjectCreatedByComponent(c.k8sClient, rsrcRef.ObjectReference, rsrcRef.Cluster, appRevName) + if err != nil { + return nil, err + } + if len(compName) == 0 { + continue + } + comps[compName] = append(comps[compName], Resource{ + Cluster: rsrcRef.Cluster, + Object: obj, + }) + } + compResList := c.extractComponentResourceWithOption(comps) + if len(compResList) == 0 { + return nil, nil + } + + return []AppResources{{ + Revision: revision, + Metadata: app.ObjectMeta, + Components: compResList, + }}, nil +} + +// CollectHistoryResourceFromApp collect history resources created by application +func (c *Collector) CollectHistoryResourceFromApp() ([]AppResources, error) { + var appResList []AppResources + rts, err := listResourceTrackers(c.k8sClient, c.opt.Name, c.opt.Namespace) + if err != nil { + return nil, err + } + appResList = make([]AppResources, 0, len(rts)) + for _, rt := range rts { + if len(rt.Status.TrackedResources) == 0 { + continue + } + appRevName := dispatch.ExtractAppRevisionName(rt.Name, c.opt.Namespace) + revision, err := oamutil.ExtractRevisionNum(appRevName, "-") + if err != nil { + return nil, err + } + comps := make(map[string][]Resource) + for _, trackedResourceRef := range rt.Status.TrackedResources { + compName, obj, err := getObjectCreatedByComponent(c.k8sClient, trackedResourceRef, "", appRevName) + if err != nil { + return nil, err + } + if len(compName) == 0 { + continue + } + comps[compName] = append(comps[compName], Resource{ + Cluster: "", + Object: obj, + }) + } + compResList := c.extractComponentResourceWithOption(comps) + if len(compResList) != 0 { + appResList = append(appResList, AppResources{ + Revision: int64(revision), + Metadata: rt.ObjectMeta, + Components: compResList, + }) + } + } + return appResList, nil +} + +func (c *Collector) extractComponentResourceWithOption(comps map[string][]Resource) []Component { + var result []Component + + // if not specify component, return all components resource created by app + if len(c.opt.Components) == 0 { + for name, resource := range comps { + result = append(result, Component{ + Name: name, + Resources: resource, + }) + } + return result + } + + for _, compName := range c.opt.Components { + if len(comps[compName]) == 0 { + continue + } + result = append(result, Component{ + Name: compName, + Resources: comps[compName], + }) + } + return result +} + +// listResourceTrackers list all resourceTracker with specified app +func listResourceTrackers(cli client.Client, appName, appNs string) ([]v1beta1.ResourceTracker, error) { + listOpts := []client.ListOption{ + client.MatchingLabels{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }} + rtList := &v1beta1.ResourceTrackerList{} + ctx := context.Background() + if err := cli.List(ctx, rtList, listOpts...); err != nil { + klog.ErrorS(err, "Failed to list Resource tracker of app", "name", appName) + return nil, err + } + return rtList.Items, nil +} + +// getObjectCreatedByComponent get k8s obj created by components +func getObjectCreatedByComponent(cli client.Client, objRef corev1.ObjectReference, cluster string, appRevName string) (componentName string, obj *unstructured.Unstructured, err error) { + ctx := multicluster.ContextWithClusterName(context.Background(), cluster) + obj = new(unstructured.Unstructured) + obj.SetGroupVersionKind(objRef.GroupVersionKind()) + obj.SetNamespace(objRef.Namespace) + obj.SetName(objRef.Name) + + key := client.ObjectKeyFromObject(obj) + if key.Namespace == "" { + key.Namespace = "default" + } + if err = cli.Get(ctx, key, obj); err != nil { + if kerrors.IsNotFound(err) { + return "", nil, nil + } + return "", nil, err + } + if obj.GetLabels()[oam.LabelAppRevision] != appRevName { + return + } + componentName = obj.GetLabels()[oam.LabelAppComponent] + return +} + +var standardWorkloads = []schema.GroupVersionKind{ + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.Deployment{}).Name()), + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.ReplicaSet{}).Name()), + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.StatefulSet{}).Name()), + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.DaemonSet{}).Name()), + batchv1.SchemeGroupVersion.WithKind(reflect.TypeOf(batchv1.Job{}).Name()), + kruise.SchemeGroupVersion.WithKind(reflect.TypeOf(kruise.CloneSet{}).Name()), +} + +var podCollectorMap = map[schema.GroupVersionKind]PodCollector{ + batchv1.SchemeGroupVersion.WithKind(reflect.TypeOf(batchv1.CronJob{}).Name()): cronJobPodCollector, + batchv1beta1.SchemeGroupVersion.WithKind(reflect.TypeOf(batchv1beta1.CronJob{}).Name()): cronJobPodCollector, +} + +// PodCollector collector pod created by workload +type PodCollector func(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) + +// NewPodCollector create a PodCollector +func NewPodCollector(gvk schema.GroupVersionKind) PodCollector { + for _, workload := range standardWorkloads { + if gvk == workload { + return standardWorkloadPodCollector + } + } + + collector, ok := podCollectorMap[gvk] + if !ok { + return func(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + return nil, nil + } + } + return collector +} + +// standardWorkloadPodCollector collect pods created by standard workload +func standardWorkloadPodCollector(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + ctx := multicluster.ContextWithClusterName(context.Background(), cluster) + selectorPath := []string{"spec", "selector", "matchLabels"} + labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), selectorPath...) + + if err != nil { + return nil, err + } + if !found { + return nil, errors.Errorf("fail to find matchLabels from %s %s", obj.GroupVersionKind().String(), klog.KObj(obj)) + } + + listOpts := []client.ListOption{ + client.MatchingLabels(labels), + client.InNamespace(obj.GetNamespace()), + } + + podList := corev1.PodList{} + if err := cli.List(ctx, &podList, listOpts...); err != nil { + return nil, err + } + + pods := make([]*unstructured.Unstructured, len(podList.Items)) + for i := range podList.Items { + pod, err := util.Object2Unstructured(podList.Items[i]) + if err != nil { + return nil, err + } + pod.SetGroupVersionKind( + corev1.SchemeGroupVersion.WithKind( + reflect.TypeOf(corev1.Pod{}).Name(), + ), + ) + pods[i] = pod + } + return pods, nil +} + +// cronJobPodCollector collect pods created by cronjob +func cronJobPodCollector(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + ctx := multicluster.ContextWithClusterName(context.Background(), cluster) + + jobList := new(batchv1.JobList) + if err := cli.List(ctx, jobList, client.InNamespace(obj.GetNamespace())); err != nil { + return nil, err + } + + uid := obj.GetUID() + var jobs []batchv1.Job + for _, job := range jobList.Items { + for _, owner := range job.GetOwnerReferences() { + if owner.Kind == reflect.TypeOf(batchv1.CronJob{}).Name() && owner.UID == uid { + jobs = append(jobs, job) + } + } + } + + var pods []*unstructured.Unstructured + for _, job := range jobs { + labels := job.Spec.Selector.MatchLabels + listOpts := []client.ListOption{ + client.MatchingLabels(labels), + client.InNamespace(job.GetNamespace()), + } + podList := corev1.PodList{} + if err := cli.List(ctx, &podList, listOpts...); err != nil { + return nil, err + } + + items := make([]*unstructured.Unstructured, len(podList.Items)) + for i := range podList.Items { + pod, err := util.Object2Unstructured(podList.Items[i]) + if err != nil { + return nil, err + } + pod.SetGroupVersionKind( + corev1.SchemeGroupVersion.WithKind( + reflect.TypeOf(corev1.Pod{}).Name(), + ), + ) + items[i] = pod + } + pods = append(pods, items...) + } + return pods, nil +} + +func getEventFieldSelector(obj *unstructured.Unstructured) fields.Selector { + field := fields.Set{} + field["involvedObject.name"] = obj.GetName() + field["involvedObject.namespace"] = obj.GetNamespace() + field["involvedObject.kind"] = obj.GetObjectKind().GroupVersionKind().Kind + field["involvedObject.uid"] = string(obj.GetUID()) + return field.AsSelector() +} diff --git a/pkg/velaql/providers/query/handler.go b/pkg/velaql/providers/query/handler.go new file mode 100644 index 000000000..ab58d60a2 --- /dev/null +++ b/pkg/velaql/providers/query/handler.go @@ -0,0 +1,151 @@ +/* + 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 query + +import ( + stdctx "context" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/multicluster" + wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" + "github.com/oam-dev/kubevela/pkg/workflow/providers" + "github.com/oam-dev/kubevela/pkg/workflow/types" +) + +const ( + // ProviderName is provider name for install. + ProviderName = "query" +) + +type provider struct { + cli client.Client +} + +// AppResources represent resources created by app +type AppResources struct { + Revision int64 `json:"revision"` + Metadata metav1.ObjectMeta `json:"metadata"` + Components []Component `json:"components"` +} + +// Component group resources rendered by ApplicationComponent +type Component struct { + Name string `json:"name"` + Resources []Resource `json:"resources"` +} + +// Resource refer to an object with cluster info +type Resource struct { + Cluster string `json:"cluster"` + Object *unstructured.Unstructured `json:"object"` +} + +// Option is the query option +type Option struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Components []string `json:"components,omitempty"` + EnableHistoryQuery bool `json:"enableHistoryQuery,omitempty"` +} + +// ListResourcesInApp lists CRs created by Application +func (h *provider) ListResourcesInApp(ctx wfContext.Context, v *value.Value, act types.Action) error { + val, err := v.LookupValue("app") + if err != nil { + return err + } + opt := Option{} + if err = val.UnmarshalTo(&opt); err != nil { + return err + } + collector := NewCollector(h.cli, opt) + appResList, err := collector.CollectResourceFromApp() + if err != nil { + return v.FillObject(err.Error(), "err") + } + return v.FillObject(appResList, "list") +} + +func (h *provider) CollectPods(ctx wfContext.Context, v *value.Value, act types.Action) error { + val, err := v.LookupValue("value") + if err != nil { + return err + } + cluster, err := v.GetString("cluster") + if err != nil { + return err + } + + obj := new(unstructured.Unstructured) + if err = val.UnmarshalTo(obj); err != nil { + return err + } + + collector := NewPodCollector(obj.GroupVersionKind()) + pods, err := collector(h.cli, obj, cluster) + if err != nil { + return v.FillObject(err.Error(), "err") + } + return v.FillObject(pods, "list") +} + +func (h *provider) SearchEvents(ctx wfContext.Context, v *value.Value, act types.Action) error { + val, err := v.LookupValue("value") + if err != nil { + return err + } + cluster, err := v.GetString("cluster") + if err != nil { + return err + } + obj := new(unstructured.Unstructured) + if err = val.UnmarshalTo(obj); err != nil { + return err + } + + listCtx := multicluster.ContextWithClusterName(stdctx.Background(), cluster) + fieldSelector := getEventFieldSelector(obj) + eventList := corev1.EventList{} + listOpts := []client.ListOption{ + client.InNamespace(obj.GetNamespace()), + client.MatchingFieldsSelector{ + Selector: fieldSelector, + }, + } + if err := h.cli.List(listCtx, &eventList, listOpts...); err != nil { + return v.FillObject(err.Error(), "err") + } + return v.FillObject(eventList.Items, "list") +} + +// Install register handlers to provider discover. +func Install(p providers.Providers, cli client.Client) { + prd := &provider{ + cli: cli, + } + + p.Register(ProviderName, map[string]providers.Handler{ + "listResourcesInApp": prd.ListResourcesInApp, + "collectPods": prd.CollectPods, + "searchEvents": prd.SearchEvents, + }) +} diff --git a/pkg/velaql/providers/query/handler_test.go b/pkg/velaql/providers/query/handler_test.go new file mode 100644 index 000000000..dcc214bf9 --- /dev/null +++ b/pkg/velaql/providers/query/handler_test.go @@ -0,0 +1,623 @@ +/* + 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 query + +import ( + "encoding/json" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/workflow/providers" +) + +type AppResourcesList struct { + List []AppResources `json:"list,omitempty"` + App interface{} `json:"app"` + Err string `json:"err,omitempty"` +} + +type PodList struct { + List []*unstructured.Unstructured `json:"list"` + Value interface{} `json:"value"` + Cluster string `json:"cluster"` +} + +var _ = Describe("Test Query Provider", func() { + var baseDeploy *v1.Deployment + var baseService *corev1.Service + var basePod *corev1.Pod + + BeforeEach(func() { + baseDeploy = new(v1.Deployment) + Expect(yaml.Unmarshal([]byte(deploymentYaml), baseDeploy)).Should(BeNil()) + + baseService = new(corev1.Service) + Expect(yaml.Unmarshal([]byte(serviceYaml), baseService)).Should(BeNil()) + + basePod = new(corev1.Pod) + Expect(yaml.Unmarshal([]byte(podYaml), basePod)).Should(BeNil()) + }) + + Context("Test ListResourcesInApp", func() { + It("Test list latest resources created by application", func() { + namespace := "test" + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + + app := v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "web", + Type: "webservice", + Properties: util.Object2RawExtension(map[string]string{ + "image": "busybox", + }), + Traits: []common.ApplicationTrait{{ + Type: "expose", + Properties: util.Object2RawExtension(map[string]interface{}{ + "ports": []int{8000}, + }), + }}, + }}, + }, + } + + Expect(k8sClient.Create(ctx, &app)).Should(BeNil()) + oldApp := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&app), oldApp)).Should(BeNil()) + oldApp.Status.LatestRevision = &common.Revision{ + Revision: 1, + } + oldApp.Status.AppliedResources = []common.ClusterObjectReference{{ + Cluster: "", + Creator: "workflow", + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Service", + Namespace: namespace, + Name: "web", + }, + }, { + Cluster: "", + Creator: "workflow", + ObjectReference: corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: namespace, + Name: "web", + }, + }} + Eventually(func() error { + err := k8sClient.Status().Update(ctx, oldApp) + if err != nil { + return err + } + return nil + }, 300*time.Microsecond, 3*time.Second).Should(BeNil()) + + appDeploy := baseDeploy.DeepCopy() + appDeploy.SetName("web") + appDeploy.SetNamespace(namespace) + appDeploy.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: "test-v1", + }) + Expect(k8sClient.Create(ctx, appDeploy)).Should(BeNil()) + + appService := baseService.DeepCopy() + appService.SetName("web") + appService.SetNamespace(namespace) + appService.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: "test-v1", + }) + Expect(k8sClient.Create(ctx, appService)).Should(BeNil()) + + prd := provider{cli: k8sClient} + opt := `app: { + name: "test" + namespace: "test" + components: ["web"] + }` + v, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, v, nil)).Should(BeNil()) + + type AppResourcesList struct { + List []AppResources `json:"list"` + App interface{} `json:"app"` + } + appResList := new(AppResourcesList) + Expect(v.UnmarshalTo(appResList)).Should(BeNil()) + + Expect(len(appResList.List)).Should(Equal(1)) + Expect(len(appResList.List[0].Components)).Should(Equal(1)) + Expect(len(appResList.List[0].Components[0].Resources)).Should(Equal(2)) + + Expect(appResList.List[0].Components[0].Resources[0].Object.GroupVersionKind()).Should(Equal(oldApp.Status.AppliedResources[0].GroupVersionKind())) + Expect(appResList.List[0].Components[0].Resources[1].Object.GroupVersionKind()).Should(Equal(oldApp.Status.AppliedResources[1].GroupVersionKind())) + }) + + It("Test list legacy resources created by application", func() { + appName := "test-legacy" + appNs := "test-legacy" + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: appNs}} + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + for i := 1; i <= 5; i++ { + rt := new(v1beta1.ResourceTracker) + rt.SetName(fmt.Sprintf("%s-v%d-%s", appName, i, appNs)) + rt.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, rt)).Should(BeNil()) + oldRT := new(v1beta1.ResourceTracker) + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(rt), oldRT); err != nil { + return err + } + oldRT.Status.TrackedResources = []corev1.ObjectReference{{ + APIVersion: "v1", + Kind: "Service", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", i), + }, { + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", i), + }} + if err := k8sClient.Status().Update(ctx, oldRT); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + appDeploy := baseDeploy.DeepCopy() + appDeploy.SetName(fmt.Sprintf("web-v%d", i)) + appDeploy.SetNamespace(appNs) + appDeploy.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, i), + }) + Expect(k8sClient.Create(ctx, appDeploy)).Should(BeNil()) + + appService := baseService.DeepCopy() + appService.SetName(fmt.Sprintf("web-v%d", i)) + appService.SetNamespace(appNs) + appService.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, i), + }) + Expect(k8sClient.Create(ctx, appService)).Should(BeNil()) + } + + prd := provider{cli: k8sClient} + opt := `app: { + name: "test-legacy" + namespace: "test-legacy" + components: ["web"] + enableHistoryQuery: true + }` + v, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, v, nil)).Should(BeNil()) + + type AppResourcesList struct { + List []AppResources `json:"list"` + App interface{} `json:"app"` + } + appResList := new(AppResourcesList) + Expect(v.UnmarshalTo(appResList)).Should(BeNil()) + + Expect(len(appResList.List)).Should(Equal(5)) + for _, app := range appResList.List { + Expect(len(app.Components)).Should(Equal(1)) + Expect(app.Components[0].Resources[0].Object.GroupVersionKind()).Should(Equal((&corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Service", + }).GroupVersionKind())) + Expect(app.Components[0].Resources[1].Object.GroupVersionKind()).Should(Equal((&corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + }).GroupVersionKind())) + } + }) + + It("Test list legacy resources meet complex scene", func() { + appName := "test-legacy-complex" + appNs := "test-legacy-complex" + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: appNs}} + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + for i := 1; i <= 2; i++ { + rt := new(v1beta1.ResourceTracker) + rt.SetName(fmt.Sprintf("%s-v%d-%s", appName, i, appNs)) + rt.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, rt)).Should(BeNil()) + oldRT := new(v1beta1.ResourceTracker) + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(rt), oldRT); err != nil { + return err + } + oldRT.Status.TrackedResources = []corev1.ObjectReference{{ + APIVersion: "v1", + Kind: "Service", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", i), + }, { + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", i), + }} + if err := k8sClient.Status().Update(ctx, oldRT); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + appDeploy := baseDeploy.DeepCopy() + appDeploy.SetName(fmt.Sprintf("web-v%d", i)) + appDeploy.SetNamespace(appNs) + appDeploy.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, i), + }) + Expect(k8sClient.Create(ctx, appDeploy)).Should(BeNil()) + + appService := baseService.DeepCopy() + appService.SetName(fmt.Sprintf("web-v%d", i)) + appService.SetNamespace(appNs) + appService.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, i), + }) + Expect(k8sClient.Create(ctx, appService)).Should(BeNil()) + } + + By("create resourceTracker without trackedResource") + emptyRT := new(v1beta1.ResourceTracker) + emptyRT.SetName(fmt.Sprintf("%s-%s", appName, appNs)) + emptyRT.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, emptyRT)).Should(BeNil()) + + prd := provider{cli: k8sClient} + opt := `app: { + name: "test-legacy-complex" + namespace: "test-legacy-complex" + components: [] + enableHistoryQuery: true + }` + v, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, v, nil)).Should(BeNil()) + + appResList := new(AppResourcesList) + Expect(v.UnmarshalTo(appResList)).Should(BeNil()) + Expect(len(appResList.List)).Should(Equal(2)) + + By("create resourceTracker tracked an un-exist resource") + rt := new(v1beta1.ResourceTracker) + rt.SetName(fmt.Sprintf("%s-v%d-%s", appName, 3, appNs)) + rt.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, rt)).Should(BeNil()) + + oldRT := new(v1beta1.ResourceTracker) + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(rt), oldRT); err != nil { + return err + } + oldRT.Status.TrackedResources = []corev1.ObjectReference{{ + APIVersion: "v1", + Kind: "Service", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", 3), + }, { + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", 3), + }} + if err := k8sClient.Status().Update(ctx, oldRT); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + appService := baseService.DeepCopy() + appService.SetName(fmt.Sprintf("web-v%d", 4)) + appService.SetNamespace(appNs) + appService.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, 4), + }) + Expect(k8sClient.Create(ctx, appService)).Should(BeNil()) + + newV, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, newV, nil)).Should(BeNil()) + appResList = new(AppResourcesList) + Expect(v.UnmarshalTo(appResList)).Should(BeNil()) + Expect(len(appResList.List)).Should(Equal(2)) + + By("create resourceTracker tracked with wrong name") + wrongNameRT := new(v1beta1.ResourceTracker) + wrongNameRT.SetName("test-1") + wrongNameRT.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, wrongNameRT)).Should(BeNil()) + + oldRT = new(v1beta1.ResourceTracker) + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(wrongNameRT), oldRT); err != nil { + return err + } + oldRT.Status.TrackedResources = []corev1.ObjectReference{{ + APIVersion: "v1", + Kind: "Service", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", 4), + }, { + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", 4), + }} + if err := k8sClient.Status().Update(ctx, oldRT); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + newV, err = value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, newV, nil)).Should(BeNil()) + appResList = new(AppResourcesList) + Expect(newV.UnmarshalTo(appResList)).Should(BeNil()) + Expect(len(appResList.Err)).ShouldNot(Equal(0)) + }) + + It("Test list resource with incomplete parameter", func() { + optWithoutApp := "" + prd := provider{cli: k8sClient} + newV, err := value.NewValue(optWithoutApp, nil, "") + Expect(err).Should(BeNil()) + err = prd.ListResourcesInApp(nil, newV, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=app) not exist")) + }) + }) + + Context("Test CollectPods", func() { + It("Test collect pod from workload deployment", func() { + deploy := baseDeploy.DeepCopy() + deploy.SetName("test-collect-pod") + deploy.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + oam.LabelAppComponent: "test", + }, + } + deploy.Spec.Template.ObjectMeta.SetLabels(map[string]string{ + oam.LabelAppComponent: "test", + }) + Expect(k8sClient.Create(ctx, deploy)).Should(BeNil()) + for i := 1; i <= 5; i++ { + pod := basePod.DeepCopy() + pod.SetName(fmt.Sprintf("test-collect-pod-%d", i)) + pod.SetLabels(map[string]string{ + oam.LabelAppComponent: "test", + }) + Expect(k8sClient.Create(ctx, pod)).Should(BeNil()) + } + + prd := provider{cli: k8sClient} + unstructuredDeploy, err := util.Object2Unstructured(deploy) + Expect(err).Should(BeNil()) + unstructuredDeploy.SetGroupVersionKind((&corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + }).GroupVersionKind()) + + deployJson, err := json.Marshal(unstructuredDeploy) + + opt := fmt.Sprintf(`value: %s +cluster: ""`, deployJson) + v, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.CollectPods(nil, v, nil)).Should(BeNil()) + + podList := new(PodList) + Expect(v.UnmarshalTo(podList)).Should(BeNil()) + Expect(len(podList.List)).Should(Equal(5)) + for _, pod := range podList.List { + Expect(pod.GroupVersionKind()).Should(Equal((&corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Pod", + }).GroupVersionKind())) + } + }) + + It("Test collect pod with incomplete parameter", func() { + emptyOpt := "" + prd := provider{cli: k8sClient} + v, err := value.NewValue(emptyOpt, nil, "") + Expect(err).Should(BeNil()) + err = prd.CollectPods(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=value) not exist")) + + optWithoutCluster := `value: {}` + v, err = value.NewValue(optWithoutCluster, nil, "") + Expect(err).Should(BeNil()) + err = prd.CollectPods(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=cluster) not exist")) + + optWithWrongValue := `value: {test: 1} +cluster: "test"` + v, err = value.NewValue(optWithWrongValue, nil, "") + Expect(err).Should(BeNil()) + err = prd.CollectPods(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + }) + }) + + Context("Test search event from k8s object", func() { + It("Test search event with incomplete parameter", func() { + emptyOpt := "" + prd := provider{cli: k8sClient} + v, err := value.NewValue(emptyOpt, nil, "") + Expect(err).Should(BeNil()) + err = prd.SearchEvents(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=value) not exist")) + + optWithoutCluster := `value: {}` + v, err = value.NewValue(optWithoutCluster, nil, "") + Expect(err).Should(BeNil()) + err = prd.SearchEvents(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=cluster) not exist")) + + optWithWrongValue := `value: {} +cluster: "test"` + v, err = value.NewValue(optWithWrongValue, nil, "") + Expect(err).Should(BeNil()) + err = prd.SearchEvents(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + }) + }) + + It("Test install provider", func() { + p := providers.NewProviders() + Install(p, k8sClient) + h, ok := p.GetHandler("query", "listResourcesInApp") + Expect(h).ShouldNot(BeNil()) + Expect(ok).Should(Equal(true)) + h, ok = p.GetHandler("query", "collectPods") + Expect(h).ShouldNot(BeNil()) + Expect(ok).Should(Equal(true)) + h, ok = p.GetHandler("query", "searchEvents") + Expect(h).ShouldNot(BeNil()) + }) +}) + +var deploymentYaml = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.oam.dev/app-revision-hash: ee69f7ed168cd8fa + app.oam.dev/appRevision: first-vela-app-v1 + app.oam.dev/component: express-server + app.oam.dev/name: first-vela-app + app.oam.dev/resourceType: WORKLOAD + app.oam.dev/revision: express-server-v1 + oam.dev/render-hash: ee2d39b553b6ef03 + workload.oam.dev/type: webservice + name: express-server + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app.oam.dev/component: express-server + template: + metadata: + labels: + app.oam.dev/component: express-server + spec: + containers: + - image: crccheck/hello-world + imagePullPolicy: Always + name: express-server + ports: + - containerPort: 8000 + protocol: TCP +` + +var serviceYaml = ` +apiVersion: v1 +kind: Service +metadata: + labels: + app.oam.dev/app-revision-hash: ee69f7ed168cd8fa + app.oam.dev/appRevision: first-vela-app-v1 + app.oam.dev/component: express-server + app.oam.dev/name: first-vela-app + app.oam.dev/resourceType: TRAIT + app.oam.dev/revision: express-server-v1 + oam.dev/render-hash: bebe99ac3e9607d0 + trait.oam.dev/resource: service + trait.oam.dev/type: ingress-1-20 + name: express-server + namespace: default +spec: + ports: + - port: 8000 + protocol: TCP + targetPort: 8000 + selector: + app.oam.dev/component: express-server +` + +var podYaml = ` +apiVersion: v1 +kind: Pod +metadata: + labels: + app.oam.dev/component: express-server + name: express-server-b77f4476b-4mt5m + namespace: default +spec: + containers: + - image: crccheck/hello-world + imagePullPolicy: Always + name: express-server-1 + ports: + - containerPort: 8000 + protocol: TCP +` diff --git a/pkg/velaql/providers/query/suite_test.go b/pkg/velaql/providers/query/suite_test.go new file mode 100644 index 000000000..4df64b7f1 --- /dev/null +++ b/pkg/velaql/providers/query/suite_test.go @@ -0,0 +1,76 @@ +/* + 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 query + +import ( + "context" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/oam-dev/kubevela/pkg/utils/common" + "k8s.io/utils/pointer" +) + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context + +var _ = BeforeSuite(func(done Done) { + By("bootstrapping test environment") + + testEnv = &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute * 3, + ControlPlaneStopTimeout: time.Minute, + UseExistingCluster: pointer.BoolPtr(false), + CRDDirectoryPaths: []string{"../../../../charts/vela-core/crds"}, + } + + By("start kube test env") + var err error + cfg, err = testEnv.Start() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + By("new kube client") + cfg.Timeout = time.Minute * 2 + k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) + + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) + + ctx = context.Background() + Expect(err).To(BeNil()) + close(done) +}, 240) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +func TestQueryProvider(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VelaQL Suite") +} diff --git a/pkg/velaql/suit_test.go b/pkg/velaql/suite_test.go similarity index 95% rename from pkg/velaql/suit_test.go rename to pkg/velaql/suite_test.go index e1cdb20b3..3187742f3 100644 --- a/pkg/velaql/suit_test.go +++ b/pkg/velaql/suite_test.go @@ -31,7 +31,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/cue/packages" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" @@ -43,8 +42,8 @@ var k8sClient client.Client var testEnv *envtest.Environment var viewHandler *ViewHandler var pod corev1.Pod -var readView v1beta1.WorkflowStepDefinition -var applyView v1beta1.WorkflowStepDefinition +var readView corev1.ConfigMap +var applyView corev1.ConfigMap var _ = BeforeSuite(func(done Done) { rand.Seed(time.Now().UnixNano()) diff --git a/pkg/velaql/testdata/apply-object.yaml b/pkg/velaql/testdata/apply-object.yaml index 9e5b24f82..8155e28b2 100644 --- a/pkg/velaql/testdata/apply-object.yaml +++ b/pkg/velaql/testdata/apply-object.yaml @@ -1,33 +1,29 @@ -apiVersion: core.oam.dev/v1beta1 -kind: WorkflowStepDefinition +apiVersion: v1 +kind: ConfigMap metadata: - annotations: - definition.oam.dev/description: Apply raw kubernetes objects for your workflow steps name: apply-object namespace: vela-system -spec: - schematic: - cue: - template: | - import ( - "vela/op" - ) +data: + template: | + import ( + "vela/op" + ) - apply: op.#Apply & { - value: { - apiVersion: parameter.apiVersion - kind: parameter.kind - metadata: { - name: parameter.name - } - } + apply: op.#Apply & { + value: { + apiVersion: parameter.apiVersion + kind: parameter.kind + metadata: { + name: parameter.name + } } + } - objStatus: { - apply.value - } - parameter: { - apiVersion: string - kind: string - name: string - } \ No newline at end of file + objStatus: { + apply.value + } + parameter: { + apiVersion: string + kind: string + name: string + } diff --git a/pkg/velaql/testdata/read-object.yaml b/pkg/velaql/testdata/read-object.yaml index d701b63c9..061beeb5e 100644 --- a/pkg/velaql/testdata/read-object.yaml +++ b/pkg/velaql/testdata/read-object.yaml @@ -1,61 +1,55 @@ -apiVersion: core.oam.dev/v1beta1 -kind: WorkflowStepDefinition +apiVersion: v1 +kind: ConfigMap metadata: - annotations: - definition.oam.dev/description: Read objects for your workflow steps name: read-object namespace: vela-system -spec: - schematic: - cue: - template: | - import ( - "vela/op" - ) +data: + template: | + import ( + "vela/op" + ) - output: { - if parameter.apiVersion == _|_ && parameter.kind == _|_ { - op.#Read & { - value: { - apiVersion: "core.oam.dev/v1beta1" - kind: "Application" - metadata: { - name: parameter.name - if parameter.namespace != _|_ { - namespace: parameter.namespace - } - } - } - } - } - if parameter.apiVersion != _|_ || parameter.kind != _|_ { - op.#Read & { - value: { - apiVersion: parameter.apiVersion - kind: parameter.kind - metadata: { - name: parameter.name - if parameter.namespace != _|_ { - namespace: parameter.namespace - } - } - } - } - } + output: { + if parameter.apiVersion == _|_ && parameter.kind == _|_ { + op.#Read & { + value: { + apiVersion: "core.oam.dev/v1beta1" + kind: "Application" + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } } - - objStatus: { - output.value.status - } - - parameter: { - // +usage=Specify the apiVersion of the object, defaults to core.oam.dev/v1beta1 - apiVersion?: string - // +usage=Specify the kind of the object, defaults to Application - kind?: string - // +usage=Specify the name of the object - name: string - // +usage=Specify the namespace of the object - namespace?: string + if parameter.apiVersion != _|_ || parameter.kind != _|_ { + op.#Read & { + value: { + apiVersion: parameter.apiVersion + kind: parameter.kind + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } } + } + objStatus: { + output.value.status + } + parameter: { + // +usage=Specify the apiVersion of the object, defaults to core.oam.dev/v1beta1 + apiVersion?: string + // +usage=Specify the kind of the object, defaults to Application + kind?: string + // +usage=Specify the name of the object + name: string + // +usage=Specify the namespace of the object + namespace?: string + } diff --git a/pkg/velaql/view.go b/pkg/velaql/view.go index d242fce90..8c8f1dc07 100644 --- a/pkg/velaql/view.go +++ b/pkg/velaql/view.go @@ -29,25 +29,29 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/cue/model/value" "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" oamutil "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils" "github.com/oam-dev/kubevela/pkg/utils/apply" - "github.com/oam-dev/kubevela/pkg/workflow/providers" - "github.com/oam-dev/kubevela/pkg/workflow/providers/kube" "github.com/oam-dev/kubevela/pkg/workflow/tasks" wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" ) -const qlNs = "vela-system" +const ( + qlNs = "vela-system" + + // ViewTaskPhaseSucceeded means view task run succeeded. + ViewTaskPhaseSucceeded = "succeeded" +) // ViewHandler view handler type ViewHandler struct { - cli client.Client - workflowStep v1beta1.WorkflowStep - dm discoverymapper.DiscoveryMapper - pd *packages.PackageDiscover - namespace string + cli client.Client + viewTask v1beta1.WorkflowStep + dm discoverymapper.DiscoveryMapper + pd *packages.PackageDiscover + namespace string } // NewViewHandler new view handler @@ -61,32 +65,27 @@ func NewViewHandler(cli client.Client, dm discoverymapper.DiscoveryMapper, pd *p } // QueryView generate view step -func (v *ViewHandler) QueryView(ctx context.Context, query Query) (*value.Value, error) { - outputsTemplate := fmt.Sprintf(OutputsTemplate, query.Export, query.Export) +func (handler *ViewHandler) QueryView(ctx context.Context, qv QueryView) (*value.Value, error) { + outputsTemplate := fmt.Sprintf(OutputsTemplate, qv.Export, qv.Export) queryKey := QueryParameterKey{} if err := json.Unmarshal([]byte(outputsTemplate), &queryKey); err != nil { return nil, err } - v.workflowStep = v1beta1.WorkflowStep{ - Name: fmt.Sprintf("%s-%s", query.View, query.Export), - Type: query.View, - Properties: oamutil.Object2RawExtension(query.Parameter), + handler.viewTask = v1beta1.WorkflowStep{ + Name: fmt.Sprintf("%s-%s", qv.View, qv.Export), + Type: qv.View, + Properties: oamutil.Object2RawExtension(qv.Parameter), Outputs: queryKey.Outputs, } - ctx = oamutil.SetNamespaceInCtx(ctx, v.namespace) - handlerProviders := providers.NewProviders() - kube.Install(handlerProviders, v.cli, v.dispatch, v.delete) - taskDiscover := tasks.NewTaskDiscover(handlerProviders, v.pd, v.cli, v.dm) - genTask, err := taskDiscover.GetTaskGenerator(ctx, v.workflowStep.Type) + taskDiscover := tasks.NewViewTaskDiscover(handler.pd, handler.cli, handler.dispatch, handler.delete, handler.namespace) + genTask, err := taskDiscover.GetTaskGenerator(ctx, handler.viewTask.Type) if err != nil { return nil, err } - runner, err := genTask(v.workflowStep, &wfTypes.GeneratorOptions{ - ID: utils.RandomString(10), - }) + runner, err := genTask(handler.viewTask, &wfTypes.GeneratorOptions{ID: utils.RandomString(10)}) if err != nil { return nil, err } @@ -99,14 +98,15 @@ func (v *ViewHandler) QueryView(ctx context.Context, query Query) (*value.Value, if err != nil { return nil, err } - if status.Phase != common.WorkflowStepPhaseSucceeded { + if string(status.Phase) != ViewTaskPhaseSucceeded { return nil, errors.Errorf("failed to query the view %s", status.Message) } - return viewCtx.GetVar(query.Export) + return viewCtx.GetVar(qv.Export) } -func (v *ViewHandler) dispatch(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifests ...*unstructured.Unstructured) error { - applicator := apply.NewAPIApplicator(v.cli) +func (handler *ViewHandler) dispatch(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifests ...*unstructured.Unstructured) error { + ctx = multicluster.ContextWithClusterName(ctx, cluster) + applicator := apply.NewAPIApplicator(handler.cli) for _, manifest := range manifests { if err := applicator.Apply(ctx, manifest); err != nil { return err @@ -115,8 +115,8 @@ func (v *ViewHandler) dispatch(ctx context.Context, cluster string, owner common return nil } -func (v *ViewHandler) delete(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifest *unstructured.Unstructured) error { - return v.cli.Delete(ctx, manifest) +func (handler *ViewHandler) delete(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifest *unstructured.Unstructured) error { + return handler.cli.Delete(ctx, manifest) } // QueryParameterKey query parameter key diff --git a/pkg/workflow/providers/kube/handle.go b/pkg/workflow/providers/kube/handle.go index 606933ff1..7d6f15cbb 100644 --- a/pkg/workflow/providers/kube/handle.go +++ b/pkg/workflow/providers/kube/handle.go @@ -19,12 +19,11 @@ package kube import ( "context" - "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/pkg/cue/model" "github.com/oam-dev/kubevela/pkg/cue/model/value" "github.com/oam-dev/kubevela/pkg/multicluster" diff --git a/pkg/workflow/providers/time/date.go b/pkg/workflow/providers/time/date.go new file mode 100644 index 000000000..bf79e64a2 --- /dev/null +++ b/pkg/workflow/providers/time/date.go @@ -0,0 +1,89 @@ +/* + 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 time + +import ( + "time" + + "github.com/oam-dev/kubevela/pkg/cue/model/value" + wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" + "github.com/oam-dev/kubevela/pkg/workflow/providers" + "github.com/oam-dev/kubevela/pkg/workflow/types" +) + +const ( + // ProviderName is provider name for install. + ProviderName = "time" +) + +type provider struct { +} + +func (h *provider) Timestamp(ctx wfContext.Context, v *value.Value, act types.Action) error { + date, err := v.GetString("date") + if err != nil { + return err + } + layout, err := v.GetString("layout") + if err != nil { + return err + } + if layout == "" { + layout = time.RFC3339 + } + t, err := time.Parse(layout, date) + if err != nil { + return err + } + return v.FillObject(t.Unix(), "timestamp") +} + +func (h *provider) Date(ctx wfContext.Context, v *value.Value, act types.Action) error { + timestamp, err := v.GetInt64("timestamp") + if err != nil { + return err + } + layout, err := v.GetString("layout") + if err != nil { + return err + } + locationName, err := v.GetString("location") + if err != nil { + return err + } + + if layout == "" { + layout = time.RFC3339 + } + + location, err := time.LoadLocation(locationName) + if err != nil { + return err + } + t := time.Unix(timestamp, 0) + t.In(location) + return v.FillObject(t.Format(layout), "date") +} + +// Install register handlers to provider discover. +func Install(p providers.Providers) { + prd := &provider{} + p.Register(ProviderName, map[string]providers.Handler{ + "timestamp": prd.Timestamp, + "date": prd.Date, + }) +} diff --git a/pkg/workflow/providers/time/date_test.go b/pkg/workflow/providers/time/date_test.go new file mode 100644 index 000000000..a92b070bd --- /dev/null +++ b/pkg/workflow/providers/time/date_test.go @@ -0,0 +1,170 @@ +/* + 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 time + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/workflow/providers" +) + +func TestTimestamp(t *testing.T) { + testcases := map[string]struct { + from string + expected int64 + expectedErr error + }{ + "test convert date with default time layout": { + from: `date: "2021-11-07T01:47:51Z" +layout: ""`, + expected: 1636249671, + expectedErr: nil, + }, + "test convert date with RFC3339 layout": { + from: `date: "2021-11-07T01:47:51Z" +layout: "2006-01-02T15:04:05Z07:00"`, + expected: 1636249671, + expectedErr: nil, + }, + "test convert date with RFC1123 layout": { + from: `date: "Fri, 01 Mar 2019 15:00:00 GMT" +layout: "Mon, 02 Jan 2006 15:04:05 MST"`, + expected: 1551452400, + expectedErr: nil, + }, + "test convert date without time layout": { + from: `date: "2021-11-07T01:47:51Z"`, + expected: 0, + expectedErr: errors.New("var(path=layout) not exist"), + }, + "test convert without date": { + from: ``, + expected: 0, + expectedErr: errors.New("var(path=date) not exist"), + }, + "test convert date with wrong time layout": { + from: `date: "2021-11-07T01:47:51Z" +layout: "Mon, 02 Jan 2006 15:04:05 MST"`, + expected: 0, + expectedErr: errors.New(`parsing time "2021-11-07T01:47:51Z" as "Mon, 02 Jan 2006 15:04:05 MST": cannot parse "2021-11-07T01:47:51Z" as "Mon"`), + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + r := require.New(t) + v, err := value.NewValue(tc.from, nil, "") + r.NoError(err) + prd := &provider{} + err = prd.Timestamp(nil, v, nil) + if tc.expectedErr != nil { + r.Equal(tc.expectedErr.Error(), err.Error()) + return + } + r.NoError(err) + expected, err := v.LookupValue("timestamp") + r.NoError(err) + ret, err := expected.CueValue().Int64() + r.NoError(err) + r.Equal(tc.expected, ret) + }) + } +} + +func TestDate(t *testing.T) { + testcases := map[string]struct { + from string + expected string + expectedErr error + }{ + "test convert timestamp to default time layout": { + from: `timestamp: 1636249671 +layout: "" +location: ""`, + expected: "2021-11-07T09:47:51+08:00", + expectedErr: nil, + }, + "test convert date to RFC3339 layout": { + from: `timestamp: 1636249671 +layout: "2006-01-02T15:04:05Z07:00" +location: ""`, + expected: "2021-11-07T09:47:51+08:00", + expectedErr: nil, + }, + "test convert date to RFC1123 layout": { + from: `timestamp: 1551452400 +layout: "Mon, 02 Jan 2006 15:04:05 MST" +location: ""`, + expected: "Fri, 01 Mar 2019 23:00:00 CST", + expectedErr: nil, + }, + "test convert date without time layout": { + from: `timestamp: 1551452400 +location: ""`, + expected: "", + expectedErr: errors.New("var(path=layout) not exist"), + }, + "test convert date without time location": { + from: `timestamp: 1551452400 +layout: "Mon, 02 Jan 2006 15:04:05 MST"`, + expected: "", + expectedErr: errors.New("var(path=location) not exist"), + }, + "test convert without timestamp": { + from: ``, + expected: "", + expectedErr: errors.New("var(path=timestamp) not exist"), + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + r := require.New(t) + v, err := value.NewValue(tc.from, nil, "") + r.NoError(err) + prd := &provider{} + err = prd.Date(nil, v, nil) + if tc.expectedErr != nil { + r.Equal(tc.expectedErr.Error(), err.Error()) + return + } + r.NoError(err) + expected, err := v.LookupValue("date") + r.NoError(err) + ret, err := expected.CueValue().String() + r.NoError(err) + r.Equal(tc.expected, ret) + }) + } +} + +func TestInstall(t *testing.T) { + p := providers.NewProviders() + Install(p) + h, ok := p.GetHandler("time", "timestamp") + r := require.New(t) + r.Equal(ok, true) + r.Equal(h != nil, true) + + h, ok = p.GetHandler("time", "date") + r.Equal(ok, true) + r.Equal(h != nil, true) +} diff --git a/pkg/workflow/tasks/discover.go b/pkg/workflow/tasks/discover.go index 387b01667..fb0f5d88b 100644 --- a/pkg/workflow/tasks/discover.go +++ b/pkg/workflow/tasks/discover.go @@ -26,11 +26,14 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/cue/packages" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/velaql/providers/query" wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" "github.com/oam-dev/kubevela/pkg/workflow/providers" "github.com/oam-dev/kubevela/pkg/workflow/providers/convert" "github.com/oam-dev/kubevela/pkg/workflow/providers/email" "github.com/oam-dev/kubevela/pkg/workflow/providers/http" + "github.com/oam-dev/kubevela/pkg/workflow/providers/kube" + "github.com/oam-dev/kubevela/pkg/workflow/providers/time" "github.com/oam-dev/kubevela/pkg/workflow/providers/workspace" "github.com/oam-dev/kubevela/pkg/workflow/tasks/custom" "github.com/oam-dev/kubevela/pkg/workflow/tasks/template" @@ -40,7 +43,7 @@ import ( type taskDiscover struct { builtins map[string]types.TaskGenerator remoteTaskDiscover *custom.TaskLoader - templateLoader *template.Loader + templateLoader template.Loader } // GetTaskGenerator get task generator by name. @@ -76,7 +79,7 @@ func NewTaskDiscover(providerHandlers providers.Providers, pd *packages.PackageD http.Install(providerHandlers) convert.Install(providerHandlers) email.Install(providerHandlers) - templateLoader := template.NewTemplateLoader(cli, dm) + templateLoader := template.NewWorkflowStepTemplateLoader(cli, dm) return &taskDiscover{ builtins: map[string]types.TaskGenerator{ "suspend": suspend, @@ -110,3 +113,22 @@ func (tr *suspendTaskRunner) Run(ctx wfContext.Context, options *types.TaskRunOp func (tr *suspendTaskRunner) Pending(ctx wfContext.Context) bool { return false } + +// NewViewTaskDiscover will create a client for load task generator. +func NewViewTaskDiscover(pd *packages.PackageDiscover, cli client.Client, apply kube.Dispatcher, delete kube.Deleter, viewNs string) types.TaskDiscover { + handlerProviders := providers.NewProviders() + + // install builtin provider + query.Install(handlerProviders, cli) + time.Install(handlerProviders) + kube.Install(handlerProviders, cli, apply, delete) + http.Install(handlerProviders) + convert.Install(handlerProviders) + email.Install(handlerProviders) + + templateLoader := template.NewViewTemplateLoader(cli, viewNs) + return &taskDiscover{ + remoteTaskDiscover: custom.NewTaskLoader(templateLoader.LoadTaskTemplate, pd, handlerProviders), + templateLoader: templateLoader, + } +} diff --git a/pkg/workflow/tasks/template/load.go b/pkg/workflow/tasks/template/load.go index ec75c1fdc..29316aa6e 100644 --- a/pkg/workflow/tasks/template/load.go +++ b/pkg/workflow/tasks/template/load.go @@ -22,6 +22,7 @@ import ( "path/filepath" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/types" @@ -39,13 +40,18 @@ const ( ) // Loader load task definition template. -type Loader struct { +type Loader interface { + LoadTaskTemplate(ctx context.Context, name string) (string, error) +} + +// WorkflowStepLoader load workflowStep task definition template. +type WorkflowStepLoader struct { client client.Client dm discoverymapper.DiscoveryMapper } // LoadTaskTemplate gets the workflowStep definition. -func (loader *Loader) LoadTaskTemplate(ctx context.Context, name string) (string, error) { +func (loader *WorkflowStepLoader) LoadTaskTemplate(ctx context.Context, name string) (string, error) { files, err := templateFS.ReadDir(templateDir) if err != nil { return "", err @@ -71,10 +77,34 @@ func (loader *Loader) LoadTaskTemplate(ctx context.Context, name string) (string return "", errors.New("custom workflowStep only support cue") } -// NewTemplateLoader create a task template loader. -func NewTemplateLoader(client client.Client, dm discoverymapper.DiscoveryMapper) *Loader { - return &Loader{ +// NewWorkflowStepTemplateLoader create a task template loader. +func NewWorkflowStepTemplateLoader(client client.Client, dm discoverymapper.DiscoveryMapper) Loader { + return &WorkflowStepLoader{ client: client, dm: dm, } } + +// ViewLoader load view task definition template. +type ViewLoader struct { + client client.Client + namespace string +} + +// LoadTaskTemplate gets the workflowStep definition. +func (loader *ViewLoader) LoadTaskTemplate(ctx context.Context, name string) (string, error) { + cm := new(corev1.ConfigMap) + cmKey := client.ObjectKey{Name: name, Namespace: loader.namespace} + if err := loader.client.Get(ctx, cmKey, cm); err != nil { + return "", errors.Wrapf(err, "fail to get view template %v from configMap", cmKey) + } + return cm.Data["template"], nil +} + +// NewViewTemplateLoader create a view task template loader. +func NewViewTemplateLoader(client client.Client, namespace string) Loader { + return &ViewLoader{ + client: client, + namespace: namespace, + } +} diff --git a/pkg/workflow/tasks/template/load_test.go b/pkg/workflow/tasks/template/load_test.go index 0658c8967..b0a2192f4 100644 --- a/pkg/workflow/tasks/template/load_test.go +++ b/pkg/workflow/tasks/template/load_test.go @@ -51,7 +51,7 @@ func TestLoad(t *testing.T) { }, } tdm := mock.NewMockDiscoveryMapper() - loader := NewTemplateLoader(cli, tdm) + loader := NewWorkflowStepTemplateLoader(cli, tdm) tmpl, err := loader.LoadTaskTemplate(context.Background(), "builtin-apply-component") assert.NilError(t, err) diff --git a/test/e2e-apiserver-test/testdata/component-pod-view.yaml b/test/e2e-apiserver-test/testdata/component-pod-view.yaml new file mode 100644 index 000000000..0c5428f84 --- /dev/null +++ b/test/e2e-apiserver-test/testdata/component-pod-view.yaml @@ -0,0 +1,92 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: component-pod-view + namespace: vela-system +data: + template: | + import ( + "vela/ql" + "vela/op" + "list" + ) + + parameter: { + name: string + namespace: string + componentName: string + } + + application: ql.#ListResourcesInApp & { + app: { + name: parameter.name + namespace: parameter.namespace + components: [parameter.componentName] + } + } + + app: application.list[0] + resources: app.components[0].resources + + podsMap: op.#Steps & { + for i, resource in resources { + "\(i)": ql.#CollectPods & { + value: resource.object + cluster: resource.cluster + } + } + } + + podsWithCluster: {for i, pods in podsMap { + "\(i)": [ + for podObj in pods.list { + cluster: pods.cluster + obj: podObj + }, + ] + }} + + flatPods: list.FlattenN([ for pods in podsWithCluster { + pods + }], 1) + + podStatus: op.#Steps & { + for i, pod in flatPods { + "\(i)": op.#Steps & { + name: pod.obj.metadata.name + containers: {for container in pod.obj.status.containerStatuses { + "\(container.name)": { + image: container.image + state: container.state + } + }} + events: ql.#SearchEvents & { + value: pod.obj + cluster: pod.cluster + } + metrics: ql.#Read & { + cluster: pod.cluster + value: { + apiVersion: "metrics.k8s.io/v1beta1" + kind: "PodMetrics" + metadata: { + name: pod.obj.metadata.name + namespace: pod.obj.metadata.namespace + } + } + } + } + } + } + + status: { + podList: [ for podInfo in podStatus { + name: podInfo.name + containers: [ for containerName, container in podInfo.containers { + containerName + }] + events: podInfo.events.list + }] + } + + diff --git a/test/e2e-apiserver-test/testdata/read-view.yaml b/test/e2e-apiserver-test/testdata/read-view.yaml new file mode 100644 index 000000000..8d3199b76 --- /dev/null +++ b/test/e2e-apiserver-test/testdata/read-view.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: read-view + namespace: vela-system +data: + template: | + import ( + "vela/op" + ) + + output: { + if parameter.apiVersion == _|_ && parameter.kind == _|_ { + op.#Read & { + value: { + apiVersion: "core.oam.dev/v1beta1" + kind: "Application" + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } + } + if parameter.apiVersion != _|_ || parameter.kind != _|_ { + op.#Read & { + value: { + apiVersion: parameter.apiVersion + kind: parameter.kind + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } + } + } + + parameter: { + // +usage=Specify the apiVersion of the object, defaults to core.oam.dev/v1beta1 + apiVersion?: string + // +usage=Specify the kind of the object, defaults to Application + kind?: string + // +usage=Specify the name of the object + name: string + // +usage=Specify the namespace of the object + namespace?: string + } + diff --git a/test/e2e-apiserver-test/velaql_test.go b/test/e2e-apiserver-test/velaql_test.go index fd53cdca6..25ad5db65 100644 --- a/test/e2e-apiserver-test/velaql_test.go +++ b/test/e2e-apiserver-test/velaql_test.go @@ -22,27 +22,50 @@ import ( "encoding/json" "fmt" "net/http" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/pkg/errors" + batchv1beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" apiv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils/common" ) +type PodStatus struct { + Name string `json:"name"` + Containers []string `json:"containers"` + Events interface{} `json:"events"` +} +type Status struct { + PodList []PodStatus `json:"podList"` +} + var _ = Describe("Test velaQL rest api", func() { namespace := "test-velaql" appName := "example-app" + component1Name := "ql-webservice" + component2Name := "ql-worker" var app v1beta1.Application + var readView corev1.ConfigMap It("Test query application status via view", func() { + Expect(common.ReadYamlToObject("./testdata/read-view.yaml", &readView)).Should(BeNil()) + Expect(k8sClient.Create(context.Background(), &readView)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + Expect(common.ReadYamlToObject("./testdata/example-app.yaml", &app)).Should(BeNil()) + app.Spec.Components[0].Name = component1Name + app.Spec.Components[1].Name = component2Name + req := apiv1.ApplicationRequest{ Components: app.Spec.Components, - Policies: app.Spec.Policies, - Workflow: app.Spec.Workflow, } bodyByte, err := json.Marshal(req) Expect(err).Should(BeNil()) @@ -55,9 +78,19 @@ var _ = Describe("Test velaQL rest api", func() { Expect(res).ShouldNot(BeNil()) Expect(res.StatusCode).Should(Equal(200)) - Expect(err).Should(BeNil()) + oldApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, oldApp); err != nil { + return err + } + if oldApp.Status.Phase != common2.ApplicationRunning { + return errors.New("application is not ready") + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + queryRes, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s}.%s", "read-object", appName, namespace, "output.value.spec"), + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s}.%s", "read-view", appName, namespace, "output.value.spec"), ) Expect(err).Should(BeNil()) Expect(queryRes.StatusCode).Should(Equal(200)) @@ -71,7 +104,6 @@ var _ = Describe("Test velaQL rest api", func() { Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, &existApp)).Should(BeNil()) Expect(len(appSpec.Components)).Should(Equal(len(existApp.Spec.Components))) - Expect(len(appSpec.Workflow.Steps)).Should(Equal(len(existApp.Spec.Workflow.Steps))) }) It("Test query application status with wrong velaQL", func() { @@ -81,4 +113,155 @@ var _ = Describe("Test velaQL rest api", func() { Expect(err).Should(BeNil()) Expect(queryRes.StatusCode).Should(Equal(400)) }) + + It("Test query application component view", func() { + componentView := new(corev1.ConfigMap) + Expect(common.ReadYamlToObject("./testdata/component-pod-view.yaml", componentView)).Should(BeNil()) + Expect(k8sClient.Create(context.Background(), componentView)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + oldApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, oldApp); err != nil { + return err + } + if oldApp.Status.Phase != common2.ApplicationRunning { + return errors.New("application is not ready") + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "component-pod-view", appName, namespace, component1Name, "status"), + ) + Expect(err).Should(BeNil()) + Expect(queryRes.StatusCode).Should(Equal(200)) + + defer queryRes.Body.Close() + status := new(Status) + err = json.NewDecoder(queryRes.Body).Decode(status) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(status.PodList)).Should(Equal(1)) + Expect(status.PodList[0].Containers[0]).Should(Equal(component1Name)) + + Eventually(func() error { + queryRes1, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "component-pod-view", appName, namespace, component2Name, "status"), + ) + if err != nil { + return err + } + if queryRes1.StatusCode != 200 { + return errors.Errorf("status code is %d", queryRes1.StatusCode) + } + defer queryRes1.Body.Close() + status1 := new(Status) + err = json.NewDecoder(queryRes1.Body).Decode(status1) + if err != nil { + return err + } + if len(status1.PodList) != 1 { + return errors.New("pod number is zero") + } + if status1.PodList[0].Containers[0] != component2Name { + return errors.New("container name is not correct") + } + return nil + }, 10*time.Second, 300*time.Microsecond).Should(BeNil()) + }) + + It("Test collect pod from cronJob", func() { + cronJob := new(v1beta1.ComponentDefinition) + Expect(yaml.Unmarshal([]byte(cronJobComponentDefinition), cronJob)).Should(BeNil()) + Expect(k8sClient.Create(context.Background(), cronJob)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + oldApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, oldApp); err != nil { + return err + } + oldApp.Spec.Components[1].Type = "cronjob" + oldApp.Spec.Components[1].Properties = util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "1"}, + }) + if err := k8sClient.Update(context.Background(), oldApp); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + newApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(oldApp), newApp); err != nil { + return err + } + if newApp.Status.Phase != common2.ApplicationRunning { + return errors.New("application is not ready") + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + newWorkload := new(batchv1beta1.CronJob) + Eventually(func() error { + return k8sClient.Get(context.Background(), client.ObjectKey{Name: component2Name, Namespace: namespace}, newWorkload) + }, 10*time.Second, 300*time.Microsecond).Should(BeNil()) + + Eventually(func() error { + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "component-pod-view", appName, namespace, component2Name, "status"), + ) + if err != nil { + return err + } + if queryRes.StatusCode != 200 { + return errors.Errorf("status code is %d", queryRes.StatusCode) + } + defer queryRes.Body.Close() + status := new(Status) + err = json.NewDecoder(queryRes.Body).Decode(status) + if err != nil { + return err + } + if len(status.PodList) == 0 { + return errors.New("pod list is 0") + } + return nil + }, 2*time.Minute, 3*time.Microsecond).Should(BeNil()) + }) }) + +var cronJobComponentDefinition = ` +apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + annotations: {} + name: cronjob + namespace: vela-system +spec: + schematic: + cue: + template: | + output: { + apiVersion: "batch/v1beta1" + kind: "CronJob" + metadata: name: context.name + spec: { + schedule: "*/1 * * * *" + jobTemplate: spec: template: spec: { + containers: [{ + name: context.name + image: parameter.image + imagePullPolicy: "IfNotPresent" + command: parameter.cmd + }] + restartPolicy: "OnFailure" + } + } + } + parameter: { + image: string + cmd: [...string] + } + workload: + type: autodetects.core.oam.dev +` From 3e68f8a83bcc8eea8fcee10a5af878e354465dc8 Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Fri, 12 Nov 2021 11:58:13 +0800 Subject: [PATCH 30/59] Feat: change model name (#2688) * Feat: change mode name * Fix: fix e2e test bug Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 589 ++++++++++-------- pkg/apiserver/datastore/datastore_test.go | 4 +- .../datastore/kubeapi/kubeapi_test.go | 26 +- .../datastore/mongodb/mongodb_test.go | 24 +- pkg/apiserver/model/application.go | 46 +- pkg/apiserver/model/cluster.go | 26 +- pkg/apiserver/model/workflow.go | 12 +- pkg/apiserver/rest/apis/v1/types.go | 129 ++-- .../{applicationplan.go => application.go} | 256 ++++---- ...cationplan_test.go => application_test.go} | 106 ++-- .../rest/usecase/testdata/ui-schema.yaml | 108 ++-- pkg/apiserver/rest/usecase/workflow.go | 46 +- pkg/apiserver/rest/usecase/workflow_test.go | 10 +- pkg/apiserver/rest/utils/bcode/application.go | 12 +- .../{applicationplan.go => application.go} | 224 +++---- .../rest/webservice/validate_test.go | 12 +- pkg/apiserver/rest/webservice/webservice.go | 2 +- pkg/apiserver/rest/webservice/workflow.go | 38 +- test/e2e-apiserver-test/application_test.go | 50 +- .../e2e-apiserver-test/testdata/workflow.json | 11 +- 20 files changed, 913 insertions(+), 818 deletions(-) rename pkg/apiserver/rest/usecase/{applicationplan.go => application.go} (75%) rename pkg/apiserver/rest/usecase/{applicationplan_test.go => application_test.go} (77%) rename pkg/apiserver/rest/webservice/{applicationplan.go => application.go} (70%) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 3aa644229..18e866c9d 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -322,7 +322,7 @@ } } }, - "/api/v1/applicationplans": { + "/api/v1/applications": { "get": { "consumes": [ "application/xml", @@ -333,10 +333,10 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "list all application plans", - "operationId": "listApplicationPlans", + "summary": "list all applications", + "operationId": "listApplications", "parameters": [ { "type": "string", @@ -360,7 +360,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ListApplicationPlanResponse" + "$ref": "#/definitions/v1.ListApplicationResponse" } }, "400": { @@ -380,24 +380,24 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "create one application plan", - "operationId": "createApplicationPlan", + "summary": "create one application ", + "operationId": "createApplication", "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateApplicationPlanRequest" + "$ref": "#/definitions/v1.CreateApplicationRequest" } } ], "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ApplicationPlanBase" + "$ref": "#/definitions/v1.ApplicationBase" } }, "400": { @@ -408,7 +408,7 @@ } } }, - "/api/v1/applicationplans/{name}": { + "/api/v1/applications/{name}": { "get": { "consumes": [ "application/xml", @@ -419,14 +419,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "detail one application plan", - "operationId": "detailApplicationPlan", + "summary": "detail one application ", + "operationId": "detailApplication", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -435,7 +435,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.DetailApplicationPlanResponse" + "$ref": "#/definitions/v1.DetailApplicationResponse" } }, "400": { @@ -455,14 +455,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "update one application plan", - "operationId": "updateApplicationPlan", + "summary": "update one application ", + "operationId": "updateApplication", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -472,14 +472,14 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpdateApplicationPlanRequest" + "$ref": "#/definitions/v1.UpdateApplicationRequest" } } ], "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ApplicationPlanBase" + "$ref": "#/definitions/v1.ApplicationBase" } }, "400": { @@ -499,14 +499,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], "summary": "delete one application", - "operationId": "deleteApplicationPlan", + "operationId": "deleteApplication", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -526,7 +526,7 @@ } } }, - "/api/v1/applicationplans/{name}/componentplans": { + "/api/v1/applications/{name}/components": { "get": { "consumes": [ "application/xml", @@ -537,14 +537,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "gets the componentplan topology of the application", + "summary": "gets the list of application components", "operationId": "listApplicationComponents", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -559,7 +559,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ComponentPlanListResponse" + "$ref": "#/definitions/v1.ComponentListResponse" } }, "400": { @@ -579,14 +579,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "create component plan for application plan", + "summary": "create component for application ", "operationId": "createComponent", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -596,14 +596,14 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateComponentPlanRequest" + "$ref": "#/definitions/v1.CreateComponentRequest" } } ], "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.ComponentPlanBase" + "$ref": "#/definitions/v1.ComponentBase" } }, "400": { @@ -614,7 +614,7 @@ } } }, - "/api/v1/applicationplans/{name}/componentplans/{componentName}": { + "/api/v1/applications/{name}/components/{componentName}": { "get": { "consumes": [ "application/xml", @@ -625,14 +625,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "detail component plan for application plan", + "summary": "detail component for application ", "operationId": "detailComponent", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -641,7 +641,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.DetailComponentPlanResponse" + "$ref": "#/definitions/v1.DetailComponentResponse" } }, "400": { @@ -652,7 +652,7 @@ } } }, - "/api/v1/applicationplans/{name}/deploy": { + "/api/v1/applications/{name}/deploy": { "post": { "consumes": [ "application/xml", @@ -663,14 +663,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], "summary": "deploy or upgrade the application", "operationId": "deployApplication", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -690,7 +690,7 @@ } } }, - "/api/v1/applicationplans/{name}/envs": { + "/api/v1/applications/{name}/envs": { "post": { "consumes": [ "application/xml", @@ -701,14 +701,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "creating an application environment plan", + "summary": "creating an application environment ", "operationId": "createApplicationEnv", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -718,7 +718,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateApplicationEnvPlanRequest" + "$ref": "#/definitions/v1.CreateApplicationEnvRequest" } } ], @@ -736,7 +736,7 @@ } } }, - "/api/v1/applicationplans/{name}/envs/{envName}": { + "/api/v1/applications/{name}/envs/{envName}": { "put": { "consumes": [ "application/xml", @@ -747,21 +747,21 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "set application plan differences in the specified environment", + "summary": "set application differences in the specified environment", "operationId": "updateApplicationEnvBinding", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "envName", "in": "path", "required": true @@ -771,7 +771,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.PutApplicationPlanEnvRequest" + "$ref": "#/definitions/v1.PutApplicationEnvRequest" } } ], @@ -798,21 +798,21 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], - "summary": "delete an application environment plan", + "summary": "delete an application environment ", "operationId": "deleteApplicationEnv", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "envName", "in": "path", "required": true @@ -832,7 +832,7 @@ } } }, - "/api/v1/applicationplans/{name}/policies": { + "/api/v1/applications/{name}/policies": { "get": { "consumes": [ "application/xml", @@ -843,14 +843,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], "summary": "list policy for application", "operationId": "listApplicationPolicies", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -879,7 +879,7 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], "summary": "create policy for application", "operationId": "createApplicationPolicy", @@ -914,7 +914,7 @@ } } }, - "/api/v1/applicationplans/{name}/policies/{policyName}": { + "/api/v1/applications/{name}/policies/{policyName}": { "get": { "consumes": [ "application/xml", @@ -925,7 +925,7 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], "summary": "detail policy for application", "operationId": "detailApplicationPolicy", @@ -968,7 +968,7 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], "summary": "update policy for application", "operationId": "updateApplicationPolicy", @@ -1019,7 +1019,7 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], "summary": "detail policy for application", "operationId": "deleteApplicationPolicy", @@ -1053,7 +1053,7 @@ } } }, - "/api/v1/applicationplans/{name}/template": { + "/api/v1/applications/{name}/template": { "post": { "consumes": [ "application/xml", @@ -1064,14 +1064,14 @@ "application/xml" ], "tags": [ - "applicationplan" + "application" ], "summary": "create one application template", "operationId": "publishApplicationTemplate", "parameters": [ { "type": "string", - "description": "identifier of the application plan", + "description": "identifier of the application ", "name": "name", "in": "path", "required": true @@ -1605,9 +1605,9 @@ "parameters": [ { "enum": [ + "component", "trait", - "workflowstep", - "component" + "workflowstep" ], "type": "string", "description": "query the definition type", @@ -1795,7 +1795,7 @@ } } }, - "/api/v1/workflowplans": { + "/api/v1/workflows": { "get": { "consumes": [ "application/xml", @@ -1806,7 +1806,7 @@ "application/xml" ], "tags": [ - "workflowplan" + "workflow" ], "summary": "list application workflow", "operationId": "listApplicationWorkflows", @@ -1847,7 +1847,7 @@ "application/xml" ], "tags": [ - "workflowplan" + "workflow" ], "summary": "create application workflow", "operationId": "createApplicationWorkflow", @@ -1857,7 +1857,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.CreateWorkflowPlanRequest" + "$ref": "#/definitions/v1.CreateWorkflowRequest" } } ], @@ -1880,7 +1880,7 @@ } } }, - "/api/v1/workflowplans/{name}": { + "/api/v1/workflows/{name}": { "get": { "consumes": [ "application/xml", @@ -1891,7 +1891,7 @@ "application/xml" ], "tags": [ - "workflowplan" + "workflow" ], "summary": "detail application workflow", "operationId": "detailWorkflow", @@ -1926,7 +1926,7 @@ "application/xml" ], "tags": [ - "workflowplan" + "workflow" ], "summary": "update application workflow config", "operationId": "updateWorkflow", @@ -1943,7 +1943,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.UpdateWorkflowPlanRequest" + "$ref": "#/definitions/v1.UpdateWorkflowRequest" } } ], @@ -1969,7 +1969,7 @@ "application/xml" ], "tags": [ - "workflowplan" + "workflow" ], "summary": "deletet workflow", "operationId": "deleteWorkflow", @@ -1995,7 +1995,7 @@ } } }, - "/api/v1/workflowplans/{name}/records": { + "/api/v1/workflows/{name}/records": { "get": { "consumes": [ "application/xml", @@ -2006,7 +2006,7 @@ "application/xml" ], "tags": [ - "workflowplan" + "workflow" ], "summary": "query application workflow execution record", "operationId": "listWorkflowRecords", @@ -2046,7 +2046,7 @@ } } }, - "/api/v1/workflowplans/{name}/records/{record}": { + "/api/v1/workflows/{name}/records/{record}": { "get": { "consumes": [ "application/xml", @@ -2057,7 +2057,7 @@ "application/xml" ], "tags": [ - "workflowplan" + "workflow" ], "summary": "query application workflow execution record detail", "operationId": "detailWorkflowRecord", @@ -2274,11 +2274,11 @@ }, "common.AppRolloutStatus": { "required": [ - "rollingState", + "batchRollingState", "currentBatch", + "rollingState", "upgradedReplicas", "upgradedReadyReplicas", - "batchRollingState", "lastTargetAppRevision" ], "properties": { @@ -2355,6 +2355,12 @@ "type": "integer", "format": "int64" }, + "policy": { + "type": "array", + "items": { + "$ref": "#/definitions/common.PolicyStatus" + } + }, "resourceTracker": { "$ref": "#/definitions/v1.ObjectReference" }, @@ -2529,6 +2535,23 @@ } } }, + "common.PolicyStatus": { + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "common.Revision": { "required": [ "name", @@ -2743,10 +2766,10 @@ "type": "string" } }, - "model.ApplicationComponentPlan": { + "model.ApplicationComponent": { "required": [ - "updateTime", "createTime", + "updateTime", "appPrimaryKey", "creator", "name", @@ -2815,7 +2838,7 @@ "traits": { "type": "array", "items": { - "$ref": "#/definitions/model.ApplicationTraitPlan" + "$ref": "#/definitions/model.ApplicationTrait" } }, "type": { @@ -2827,11 +2850,19 @@ } } }, - "model.ApplicationTraitPlan": { + "model.ApplicationTrait": { "required": [ + "alias", + "description", "type" ], "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, "properties": { "$ref": "#/definitions/model.JSONStruct" }, @@ -3222,59 +3253,7 @@ } } }, - "v1.ApplicationDeployRequest": { - "required": [ - "workflowName", - "commit", - "sourceType", - "force" - ], - "properties": { - "commit": { - "type": "string" - }, - "force": { - "type": "boolean" - }, - "sourceType": { - "type": "string" - }, - "workflowName": { - "type": "string" - } - } - }, - "v1.ApplicationDeployResponse": { - "required": [ - "version", - "status", - "reason", - "deployUser", - "commit", - "sourceType" - ], - "properties": { - "commit": { - "type": "string" - }, - "deployUser": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "sourceType": { - "type": "string" - }, - "status": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "v1.ApplicationPlanBase": { + "v1.ApplicationBase": { "required": [ "name", "alias", @@ -3333,6 +3312,58 @@ } } }, + "v1.ApplicationDeployRequest": { + "required": [ + "workflowName", + "commit", + "sourceType", + "force" + ], + "properties": { + "commit": { + "type": "string" + }, + "force": { + "type": "boolean" + }, + "sourceType": { + "type": "string" + }, + "workflowName": { + "type": "string" + } + } + }, + "v1.ApplicationDeployResponse": { + "required": [ + "version", + "status", + "reason", + "deployUser", + "commit", + "sourceType" + ], + "properties": { + "commit": { + "type": "string" + }, + "deployUser": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "status": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "v1.ApplicationRequest": { "required": [ "components" @@ -3560,7 +3591,7 @@ } } }, - "v1.ComponentPlanBase": { + "v1.ComponentBase": { "required": [ "name", "alias", @@ -3622,15 +3653,15 @@ } } }, - "v1.ComponentPlanListResponse": { + "v1.ComponentListResponse": { "required": [ - "componentplans" + "components" ], "properties": { - "componentplans": { + "components": { "type": "array", "items": { - "$ref": "#/definitions/v1.ComponentPlanBase" + "$ref": "#/definitions/v1.ComponentBase" } } } @@ -3699,10 +3730,10 @@ } } }, - "v1.CreateApplicationEnvPlanRequest": { + "v1.CreateApplicationEnvRequest": { "required": [ - "name", - "clusterSelector" + "clusterSelector", + "name" ], "properties": { "alias": { @@ -3722,7 +3753,7 @@ } } }, - "v1.CreateApplicationPlanRequest": { + "v1.CreateApplicationRequest": { "required": [ "name", "namespace", @@ -3732,9 +3763,6 @@ "alias": { "type": "string" }, - "deploy": { - "type": "boolean" - }, "description": { "type": "string" }, @@ -3782,6 +3810,30 @@ } } }, + "v1.CreateApplicationTrait": { + "required": [ + "name", + "type", + "properties" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "v1.CreateCloudClusterRequest": { "required": [ "accessKeyID", @@ -3868,7 +3920,7 @@ } } }, - "v1.CreateComponentPlanRequest": { + "v1.CreateComponentRequest": { "required": [ "name", "componentType" @@ -3909,6 +3961,12 @@ }, "properties": { "type": "string" + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.CreateApplicationTrait" + } } } }, @@ -3948,7 +4006,7 @@ } } }, - "v1.CreateWorkflowPlanRequest": { + "v1.CreateWorkflowRequest": { "required": [ "appName", "name", @@ -4006,6 +4064,8 @@ "version", "description", "icon", + "schema", + "uiSchema", "definitions", "parameters", "cue_templates", @@ -4051,12 +4111,21 @@ "parameters": { "type": "string" }, + "schema": { + "type": "string" + }, "tags": { "type": "array", "items": { "type": "string" } }, + "uiSchema": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.UIParameter" + } + }, "url": { "type": "string" }, @@ -4071,17 +4140,17 @@ } } }, - "v1.DetailApplicationPlanResponse": { + "v1.DetailApplicationResponse": { "required": [ - "alias", - "namespace", - "gatewayRule", "name", - "description", - "createTime", + "namespace", "updateTime", "icon", "status", + "gatewayRule", + "alias", + "description", + "createTime", "policies", "status", "resourceInfo", @@ -4151,20 +4220,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "alias", + "name", + "provider", + "apiServerURL", "kubeConfigSecret", + "alias", + "status", + "dashboardURL", "createTime", + "updateTime", + "labels", + "reason", + "kubeConfig", "description", "icon", - "labels", - "provider", - "kubeConfig", - "updateTime", - "name", - "status", - "reason", - "apiServerURL", - "dashboardURL", "resourceInfo" ], "properties": { @@ -4220,15 +4289,15 @@ } } }, - "v1.DetailComponentPlanResponse": { + "v1.DetailComponentResponse": { "required": [ - "name", - "type", - "appPrimaryKey", - "creator", "updateTime", + "type", + "creator", "createTime", - "alias" + "appPrimaryKey", + "alias", + "name" ], "properties": { "alias": { @@ -4292,7 +4361,7 @@ "traits": { "type": "array", "items": { - "$ref": "#/definitions/model.ApplicationTraitPlan" + "$ref": "#/definitions/model.ApplicationTrait" } }, "type": { @@ -4323,13 +4392,13 @@ }, "v1.DetailPolicyResponse": { "required": [ - "updateTime", - "name", "type", "description", "creator", "properties", - "createTime" + "createTime", + "updateTime", + "name" ], "properties": { "createTime": { @@ -4357,58 +4426,12 @@ } } }, - "v1.DetailWorkflowPlanResponse": { - "required": [ - "updateTime", - "name", - "alias", - "description", - "enable", - "default", - "createTime", - "workflowRecord" - ], - "properties": { - "alias": { - "type": "string" - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "default": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "enable": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowStep" - } - }, - "updateTime": { - "type": "string", - "format": "date-time" - }, - "workflowRecord": { - "$ref": "#/definitions/v1.WorkflowRecord" - } - } - }, "v1.DetailWorkflowRecordResponse": { "required": [ + "name", "namespace", "suspend", "terminated", - "name", "deployTime", "deployUser", "commit", @@ -4452,6 +4475,52 @@ } } }, + "v1.DetailWorkflowResponse": { + "required": [ + "description", + "enable", + "default", + "createTime", + "updateTime", + "name", + "alias", + "workflowRecord" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "workflowRecord": { + "$ref": "#/definitions/v1.WorkflowRecord" + } + } + }, "v1.EmptyResponse": {}, "v1.EnableAddonRequest": { "properties": { @@ -4555,19 +4624,6 @@ } } }, - "v1.ListApplicationPlanResponse": { - "required": [ - "applicationplans" - ], - "properties": { - "applicationplans": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ApplicationPlanBase" - } - } - } - }, "v1.ListApplicationPolicy": { "required": [ "policies" @@ -4581,6 +4637,19 @@ } } }, + "v1.ListApplicationResponse": { + "required": [ + "applications" + ], + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationBase" + } + } + } + }, "v1.ListCloudClusterCreationResponse": { "required": [ "creations" @@ -4664,19 +4733,6 @@ } } }, - "v1.ListWorkflowPlanResponse": { - "required": [ - "workflowplans" - ], - "properties": { - "workflowplans": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowPlanBase" - } - } - } - }, "v1.ListWorkflowRecordsResponse": { "required": [ "records", @@ -4695,6 +4751,19 @@ } } }, + "v1.ListWorkflowResponse": { + "required": [ + "workflows" + ], + "properties": { + "workflows": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowBase" + } + } + } + }, "v1.ManagedFieldsEntry": { "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", "properties": { @@ -4987,7 +5056,7 @@ } } }, - "v1.PutApplicationPlanEnvRequest": { + "v1.PutApplicationEnvRequest": { "properties": { "alias": { "type": "string" @@ -5016,7 +5085,7 @@ } } }, - "v1.UpdateApplicationPlanRequest": { + "v1.UpdateApplicationRequest": { "properties": { "alias": { "type": "string" @@ -5053,7 +5122,7 @@ } } }, - "v1.UpdateWorkflowPlanRequest": { + "v1.UpdateWorkflowRequest": { "required": [ "enable", "default" @@ -5082,7 +5151,7 @@ "v1.VelaQLViewResponse": { "type": "object" }, - "v1.WorkflowPlanBase": { + "v1.WorkflowBase": { "required": [ "name", "alias", diff --git a/pkg/apiserver/datastore/datastore_test.go b/pkg/apiserver/datastore/datastore_test.go index c6fd219c3..26feeff76 100644 --- a/pkg/apiserver/datastore/datastore_test.go +++ b/pkg/apiserver/datastore/datastore_test.go @@ -30,7 +30,7 @@ import ( var _ = Describe("Test new entity function", func() { It("Test new application entity", func() { - var app model.ApplicationPlan + var app model.Application new, err := NewEntity(&app) Expect(err).To(BeNil()) json.Unmarshal([]byte(`{"name":"demo"}`), new) @@ -40,7 +40,7 @@ var _ = Describe("Test new entity function", func() { }) It("Test new multiple application entity", func() { - var app model.ApplicationPlan + var app model.Application var list []Entity var n = 3 for n > 0 { diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go index b5d8c0e63..bdb52f62f 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go @@ -88,22 +88,22 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(kubeStore).ToNot(BeNil()) It("Test add funtion", func() { - err := kubeStore.Add(context.TODO(), &model.ApplicationPlan{Name: "kubevela-app", Description: "default"}) + err := kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) Expect(err).ToNot(HaveOccurred()) }) It("Test batch add funtion", func() { var datas = []datastore.Entity{ - &model.ApplicationPlan{Name: "kubevela-app-2", Description: "this is demo 2"}, - &model.ApplicationPlan{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.ApplicationPlan{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, + &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, + &model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, } err := kubeStore.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) var datas2 = []datastore.Entity{ - &model.ApplicationPlan{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, - &model.ApplicationPlan{Name: "kubevela-app-2", Description: "this is demo 2"}, + &model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, + &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = kubeStore.BatchAdd(context.TODO(), datas2) equal := cmp.Diff(strings.Contains(err.Error(), "save components occur error"), true) @@ -111,7 +111,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test get funtion", func() { - app := &model.ApplicationPlan{Name: "kubevela-app"} + app := &model.Application{Name: "kubevela-app"} err := kubeStore.Get(context.TODO(), app) Expect(err).Should(BeNil()) diff := cmp.Diff(app.Description, "default") @@ -119,11 +119,11 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test put funtion", func() { - err := kubeStore.Put(context.TODO(), &model.ApplicationPlan{Name: "kubevela-app", Description: "this is demo"}) + err := kubeStore.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) Expect(err).ToNot(HaveOccurred()) }) It("Test index", func() { - var app = model.ApplicationPlan{ + var app = model.Application{ Namespace: "test", } selector, err := labels.Parse(fmt.Sprintf("table=%s", app.TableName())) @@ -137,7 +137,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(cmp.Diff(selector.String(), "namespace=test,table=vela_application")).Should(BeEmpty()) }) It("Test list function", func() { - var app model.ApplicationPlan + var app model.Application list, err := kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) Expect(err).ShouldNot(HaveOccurred()) diff := cmp.Diff(len(list), 4) @@ -166,7 +166,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test count function", func() { - var app model.ApplicationPlan + var app model.Application count, err := kubeStore.Count(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) Expect(count).Should(Equal(int64(4))) @@ -178,7 +178,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test isExist function", func() { - var app model.ApplicationPlan + var app model.Application app.Name = "kubevela-app-3" exist, err := kubeStore.IsExist(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) @@ -193,7 +193,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { }) It("Test delete funtion", func() { - var app model.ApplicationPlan + var app model.Application app.Name = "kubevela-app" err := kubeStore.Delete(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/apiserver/datastore/mongodb/mongodb_test.go b/pkg/apiserver/datastore/mongodb/mongodb_test.go index ac5f884f6..3e947d028 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb_test.go +++ b/pkg/apiserver/datastore/mongodb/mongodb_test.go @@ -56,22 +56,22 @@ var _ = BeforeSuite(func(done Done) { var _ = Describe("Test mongodb datastore driver", func() { It("Test add funtion", func() { - err := mongodbDriver.Add(context.TODO(), &model.ApplicationPlan{Name: "kubevela-app", Description: "default"}) + err := mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) Expect(err).ToNot(HaveOccurred()) }) It("Test batch add funtion", func() { var datas = []datastore.Entity{ - &model.ApplicationPlan{Name: "kubevela-app-2", Description: "this is demo 2"}, - &model.ApplicationPlan{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.ApplicationPlan{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, + &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, + &model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, } err := mongodbDriver.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) var datas2 = []datastore.Entity{ - &model.ApplicationPlan{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, - &model.ApplicationPlan{Name: "kubevela-app-2", Description: "this is demo 2"}, + &model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, + &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = mongodbDriver.BatchAdd(context.TODO(), datas2) equal := cmp.Diff(strings.Contains(err.Error(), "save components occur error"), true) @@ -79,7 +79,7 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test get funtion", func() { - app := &model.ApplicationPlan{Name: "kubevela-app"} + app := &model.Application{Name: "kubevela-app"} err := mongodbDriver.Get(context.TODO(), app) Expect(err).Should(BeNil()) diff := cmp.Diff(app.Description, "default") @@ -87,11 +87,11 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test put funtion", func() { - err := mongodbDriver.Put(context.TODO(), &model.ApplicationPlan{Name: "kubevela-app", Description: "this is demo"}) + err := mongodbDriver.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) Expect(err).ToNot(HaveOccurred()) }) It("Test list funtion", func() { - var app model.ApplicationPlan + var app model.Application list, err := mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) Expect(err).ShouldNot(HaveOccurred()) diff := cmp.Diff(len(list), 4) @@ -120,7 +120,7 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test count function", func() { - var app model.ApplicationPlan + var app model.Application count, err := mongodbDriver.Count(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) Expect(count).Should(Equal(int64(4))) @@ -132,7 +132,7 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test isExist funtion", func() { - var app model.ApplicationPlan + var app model.Application app.Name = "kubevela-app-3" exist, err := mongodbDriver.IsExist(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) @@ -147,7 +147,7 @@ var _ = Describe("Test mongodb datastore driver", func() { }) It("Test delete funtion", func() { - var app model.ApplicationPlan + var app model.Application app.Name = "kubevela-app" err := mongodbDriver.Delete(context.TODO(), &app) Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 397660bf7..518250b88 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -23,11 +23,11 @@ import ( ) func init() { - RegistModel(&ApplicationComponentPlan{}, &ApplicationPolicyPlan{}, &ApplicationPlan{}, &DeployEvent{}) + RegistModel(&ApplicationComponent{}, &ApplicationPolicy{}, &Application{}, &DeployEvent{}) } -// ApplicationPlan application delivery plan model -type ApplicationPlan struct { +// Application application delivery model +type Application struct { Model Name string `json:"name"` Alias string `json:"alias"` @@ -39,17 +39,17 @@ type ApplicationPlan struct { } // TableName return custom table name -func (a *ApplicationPlan) TableName() string { +func (a *Application) TableName() string { return tableNamePrefix + "application" } // PrimaryKey return custom primary key -func (a *ApplicationPlan) PrimaryKey() string { +func (a *Application) PrimaryKey() string { return a.Name } // Index return custom index -func (a *ApplicationPlan) Index() map[string]string { +func (a *Application) Index() map[string]string { index := make(map[string]string) if a.Name != "" { index["name"] = a.Name @@ -81,8 +81,8 @@ type ComponentSelector struct { Components []string `json:"components"` } -// ApplicationComponentPlan component database model -type ApplicationComponentPlan struct { +// ApplicationComponent component database model +type ApplicationComponent struct { Model AppPrimaryKey string `json:"appPrimaryKey"` Description string `json:"description,omitempty"` @@ -100,24 +100,24 @@ type ApplicationComponentPlan struct { Inputs common.StepInputs `json:"inputs,omitempty"` Outputs common.StepOutputs `json:"outputs,omitempty"` // Traits define the trait of one component, the type must be array to keep the order. - Traits []ApplicationTraitPlan `json:"traits,omitempty"` - // scopes in ApplicationComponentPlan defines the component-level scopes + Traits []ApplicationTrait `json:"traits,omitempty"` + // scopes in ApplicationComponent defines the component-level scopes // the format is pairs, the key represents type of `ScopeDefinition` while the value represent the name of scope instance. Scopes map[string]string `json:"scopes,omitempty"` } // TableName return custom table name -func (a *ApplicationComponentPlan) TableName() string { +func (a *ApplicationComponent) TableName() string { return tableNamePrefix + "application_component" } // PrimaryKey return custom primary key -func (a *ApplicationComponentPlan) PrimaryKey() string { +func (a *ApplicationComponent) PrimaryKey() string { return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Name) } // Index return custom index -func (a *ApplicationComponentPlan) Index() map[string]string { +func (a *ApplicationComponent) Index() map[string]string { index := make(map[string]string) if a.Name != "" { index["name"] = a.Name @@ -131,8 +131,8 @@ func (a *ApplicationComponentPlan) Index() map[string]string { return index } -// ApplicationPolicyPlan app policy -type ApplicationPolicyPlan struct { +// ApplicationPolicy app policy +type ApplicationPolicy struct { Model AppPrimaryKey string `json:"appPrimaryKey"` Name string `json:"name"` @@ -143,17 +143,17 @@ type ApplicationPolicyPlan struct { } // TableName return custom table name -func (a *ApplicationPolicyPlan) TableName() string { +func (a *ApplicationPolicy) TableName() string { return tableNamePrefix + "application_policy" } // PrimaryKey return custom primary key -func (a *ApplicationPolicyPlan) PrimaryKey() string { +func (a *ApplicationPolicy) PrimaryKey() string { return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Name) } // Index return custom index -func (a *ApplicationPolicyPlan) Index() map[string]string { +func (a *ApplicationPolicy) Index() map[string]string { index := make(map[string]string) if a.Name != "" { index["name"] = a.Name @@ -167,10 +167,12 @@ func (a *ApplicationPolicyPlan) Index() map[string]string { return index } -// ApplicationTraitPlan application trait -type ApplicationTraitPlan struct { - Type string `json:"type"` - Properties *JSONStruct `json:"properties,omitempty"` +// ApplicationTrait application trait +type ApplicationTrait struct { + Alias string `json:"alias"` + Description string `json:"description"` + Type string `json:"type"` + Properties *JSONStruct `json:"properties,omitempty"` } // DeployEventInit event status init diff --git a/pkg/apiserver/model/cluster.go b/pkg/apiserver/model/cluster.go index 553f6b3cf..c222e238b 100644 --- a/pkg/apiserver/model/cluster.go +++ b/pkg/apiserver/model/cluster.go @@ -39,20 +39,18 @@ const ( // Cluster describes the model of cluster in apiserver type Cluster struct { Model - Name string `json:"name"` - Alias string `json:"alias"` - Description string `json:"description"` - Icon string `json:"icon"` - Labels map[string]string `json:"labels"` - Status string `json:"status"` - Reason string `json:"reason"` - - Provider ProviderInfo `json:"provider"` - APIServerURL string `json:"apiServerURL"` - DashboardURL string `json:"dashboardURL"` - - KubeConfig string `json:"kubeConfig"` - KubeConfigSecret string `json:"kubeConfigSecret"` + Name string `json:"name"` + Alias string `json:"alias"` + Description string `json:"description"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels"` + Status string `json:"status"` + Reason string `json:"reason"` + Provider ProviderInfo `json:"provider"` + APIServerURL string `json:"apiServerURL"` + DashboardURL string `json:"dashboardURL"` + KubeConfig string `json:"kubeConfig"` + KubeConfigSecret string `json:"kubeConfigSecret"` } // TableName table name for datastore diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index 261f76070..bb96fd602 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -24,12 +24,12 @@ import ( ) func init() { - RegistModel(&WorkflowPlan{}) + RegistModel(&Workflow{}) RegistModel(&WorkflowRecord{}) } -// WorkflowPlan application delivery plan database model -type WorkflowPlan struct { +// Workflow application delivery database model +type Workflow struct { Model Name string `json:"name"` Alias string `json:"alias"` @@ -55,17 +55,17 @@ type WorkflowStep struct { } // TableName return custom table name -func (w *WorkflowPlan) TableName() string { +func (w *Workflow) TableName() string { return tableNamePrefix + "workflow" } // PrimaryKey return custom primary key -func (w *WorkflowPlan) PrimaryKey() string { +func (w *Workflow) PrimaryKey() string { return w.Name } // Index return custom primary key -func (w *WorkflowPlan) Index() map[string]string { +func (w *Workflow) Index() map[string]string { index := make(map[string]string) if w.Name != "" { index["name"] = w.Name diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 27eefbf4a..656cca717 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -244,16 +244,16 @@ type ClusterBase struct { Reason string `json:"reason"` } -// ListApplicatioPlanOptions list application plan query options -type ListApplicatioPlanOptions struct { +// ListApplicatioOptions list application query options +type ListApplicatioOptions struct { Namespace string `json:"namespace"` Cluster string `json:"cluster"` Query string `json:"query"` } -// ListApplicationPlanResponse list applications by query params -type ListApplicationPlanResponse struct { - ApplicationPlans []*ApplicationPlanBase `json:"applicationplans"` +// ListApplicationResponse list applications by query params +type ListApplicationResponse struct { + Applications []*ApplicationBase `json:"applications"` } // EnvBindList env bind list @@ -269,8 +269,8 @@ func (e EnvBindList) ContainCluster(name string) bool { return false } -// ApplicationPlanBase application base model -type ApplicationPlanBase struct { +// ApplicationBase application base model +type ApplicationBase struct { Name string `json:"name"` Alias string `json:"alias"` Namespace string `json:"namespace"` @@ -303,8 +303,8 @@ type GatewayRule struct { ComponentPort int32 `json:"componentPort"` } -// CreateApplicationPlanRequest create application plan request body -type CreateApplicationPlanRequest struct { +// CreateApplicationRequest create application request body +type CreateApplicationRequest struct { Name string `json:"name" validate:"checkname"` Alias string `json:"alias" validate:"checkalias" optional:"true"` Namespace string `json:"namespace" validate:"checkname"` @@ -313,12 +313,10 @@ type CreateApplicationPlanRequest struct { Labels map[string]string `json:"labels,omitempty"` EnvBind []*EnvBind `json:"envBind,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"` } -// UpdateApplicationPlanRequest update application plan base config -type UpdateApplicationPlanRequest struct { +// UpdateApplicationRequest update application base config +type UpdateApplicationRequest struct { Alias string `json:"alias" validate:"checkalias" optional:"true"` Description string `json:"description" optional:"true"` Icon string `json:"icon" optional:"true"` @@ -346,9 +344,9 @@ type ComponentSelector struct { Components []string `json:"components"` } -// DetailApplicationPlanResponse application plan detail -type DetailApplicationPlanResponse struct { - ApplicationPlanBase +// DetailApplicationResponse application detail +type DetailApplicationResponse struct { + ApplicationBase Policies []string `json:"policies"` Status string `json:"status"` ResourceInfo ApplicationResourceInfo `json:"resourceInfo"` @@ -368,8 +366,8 @@ type ApplicationResourceInfo struct { // Others, such as: Memory、CPU、GPU、Storage } -// ComponentPlanBase component plan base model -type ComponentPlanBase struct { +// ComponentBase component base model +type ComponentBase struct { Name string `json:"name"` Alias string `json:"alias"` Description string `json:"description"` @@ -384,31 +382,31 @@ type ComponentPlanBase struct { UpdateTime time.Time `json:"updateTime"` } -// ComponentPlanListResponse list component plan -type ComponentPlanListResponse struct { - ComponentPlans []*ComponentPlanBase `json:"componentplans"` +// ComponentListResponse list component +type ComponentListResponse struct { + Components []*ComponentBase `json:"components"` } -// CreateComponentPlanRequest create component plan request model -type CreateComponentPlanRequest struct { - Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias" optional:"true"` - Description string `json:"description" optional:"true"` - Icon string `json:"icon" optional:"true"` - Labels map[string]string `json:"labels,omitempty"` - ComponentType string `json:"componentType" validate:"checkname"` - EnvNames []string `json:"envNames,omitempty" optional:"true"` - Properties string `json:"properties,omitempty"` - DependsOn []string `json:"dependsOn" optional:"true"` +// CreateComponentRequest create component request model +type CreateComponentRequest struct { + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon" optional:"true"` + Labels map[string]string `json:"labels,omitempty"` + ComponentType string `json:"componentType" validate:"checkname"` + EnvNames []string `json:"envNames,omitempty" optional:"true"` + Properties string `json:"properties,omitempty"` + DependsOn []string `json:"dependsOn" optional:"true"` + Traits []*CreateApplicationTrait `json:"traits,omitempty" optional:"true"` } -// DetailComponentPlanResponse detail component plan model -type DetailComponentPlanResponse struct { - model.ApplicationComponentPlan - //TODO: Status +// DetailComponentResponse detail component model +type DetailComponentResponse struct { + model.ApplicationComponent } -// ListApplicationComponentOptions list app plan component list +// ListApplicationComponentOptions list app component list type ListApplicationComponentOptions struct { EnvName string `json:"envName"` } @@ -535,8 +533,8 @@ type PolicyDefinition struct { Parameters []types.Parameter `json:"parameters"` } -// CreateWorkflowPlanRequest create workflow plan request -type CreateWorkflowPlanRequest struct { +// CreateWorkflowRequest create workflow request +type CreateWorkflowRequest struct { AppName string `json:"appName" validate:"checkname"` Name string `json:"name" validate:"checkname"` Alias string `json:"alias" validate:"checkalias" optional:"true"` @@ -546,8 +544,8 @@ type CreateWorkflowPlanRequest struct { Default bool `json:"default"` } -// UpdateWorkflowPlanRequest update or create application workflow -type UpdateWorkflowPlanRequest struct { +// UpdateWorkflowRequest update or create application workflow +type UpdateWorkflowRequest struct { Alias string `json:"alias" validate:"checkalias" optional:"true"` Description string `json:"description" optional:"true"` Steps []WorkflowStep `json:"steps,omitempty"` @@ -568,20 +566,20 @@ type WorkflowStep struct { Outputs common.StepOutputs `json:"outputs,omitempty" optional:"true"` } -// DetailWorkflowPlanResponse detail workflow response -type DetailWorkflowPlanResponse struct { - WorkflowPlanBase +// DetailWorkflowResponse detail workflow response +type DetailWorkflowResponse struct { + WorkflowBase Steps []WorkflowStep `json:"steps,omitempty"` LastRecord *WorkflowRecord `json:"workflowRecord"` } -// ListWorkflowPlanResponse list application workflows -type ListWorkflowPlanResponse struct { - WorkflowPlans []*WorkflowPlanBase `json:"workflowplans"` +// ListWorkflowResponse list application workflows +type ListWorkflowResponse struct { + Workflows []*WorkflowBase `json:"workflows"` } -// WorkflowPlanBase workflow base model -type WorkflowPlanBase struct { +// WorkflowBase workflow base model +type WorkflowBase struct { Name string `json:"name"` Alias string `json:"alias"` Description string `json:"description"` @@ -642,15 +640,40 @@ type ApplicationDeployResponse struct { // VelaQLViewResponse query response type VelaQLViewResponse map[string]interface{} -// PutApplicationPlanEnvRequest set diff request -type PutApplicationPlanEnvRequest struct { +// PutApplicationEnvRequest set diff request +type PutApplicationEnvRequest struct { ComponentSelector *ComponentSelector `json:"componentSelector,omitempty"` Alias *string `json:"alias,omitempty" validate:"checkalias" optional:"true"` Description *string `json:"description,omitempty" optional:"true"` ClusterSelector *ClusterSelector `json:"clusterSelector,omitempty"` } -// CreateApplicationEnvPlanRequest new application env plan -type CreateApplicationEnvPlanRequest struct { +// CreateApplicationEnvRequest new application env +type CreateApplicationEnvRequest struct { EnvBind } + +// CreateApplicationTrait create application triat req +type CreateApplicationTrait struct { + Type string `json:"type" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Properties string `json:"properties"` +} + +// UpdateApplicationTrait update application trait req +type UpdateApplicationTrait struct { + Alias *string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description *string `json:"description,omitempty" optional:"true"` + Properties *string `json:"properties"` +} + +// ApplicationTrait application trait +type ApplicationTrait struct { + Name string `json:"name"` + Type string `json:"type"` + Alias string `json:"alias,omitempty"` + Description string `json:"description,omitempty"` + // Properties json data + Properties *model.JSONStruct `json:"properties"` +} diff --git a/pkg/apiserver/rest/usecase/applicationplan.go b/pkg/apiserver/rest/usecase/application.go similarity index 75% rename from pkg/apiserver/rest/usecase/applicationplan.go rename to pkg/apiserver/rest/usecase/application.go index 6086901d7..ef8345bca 100644 --- a/pkg/apiserver/rest/usecase/applicationplan.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -58,27 +58,30 @@ const ( // ApplicationUsecase application usecase type ApplicationUsecase interface { - ListApplicationPlans(ctx context.Context, listOptions apisv1.ListApplicatioPlanOptions) ([]*apisv1.ApplicationPlanBase, error) - GetApplicationPlan(ctx context.Context, appName string) (*model.ApplicationPlan, error) - DetailApplicationPlan(ctx context.Context, app *model.ApplicationPlan) (*apisv1.DetailApplicationPlanResponse, error) - PublishApplicationTemplate(ctx context.Context, app *model.ApplicationPlan) (*apisv1.ApplicationTemplateBase, error) - CreateApplicationPlan(context.Context, apisv1.CreateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) - UpdateApplicationPlan(context.Context, *model.ApplicationPlan, apisv1.UpdateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) - DeleteApplicationPlan(ctx context.Context, app *model.ApplicationPlan) error - Deploy(ctx context.Context, app *model.ApplicationPlan, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) - ListComponents(ctx context.Context, app *model.ApplicationPlan, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentPlanBase, error) - AddComponent(ctx context.Context, app *model.ApplicationPlan, com apisv1.CreateComponentPlanRequest) (*apisv1.ComponentPlanBase, error) - DetailComponent(ctx context.Context, app *model.ApplicationPlan, componentName string) (*apisv1.DetailComponentPlanResponse, error) - DeleteComponent(ctx context.Context, app *model.ApplicationPlan, componentName string) error - ListPolicies(ctx context.Context, app *model.ApplicationPlan) ([]*apisv1.PolicyBase, error) - AddPolicy(ctx context.Context, app *model.ApplicationPlan, policy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) - DetailPolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) (*apisv1.DetailPolicyResponse, error) - DeletePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) error - UpdatePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) - GetApplicationPlanEnvBindingPolicy(ctx context.Context, app *model.ApplicationPlan) (*v1alpha1.EnvBindingSpec, error) - UpdateApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, envName string, diff apisv1.PutApplicationPlanEnvRequest) (*apisv1.EnvBind, error) - CreateApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, env apisv1.CreateApplicationEnvPlanRequest) (*apisv1.EnvBind, error) - DeleteApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, envName string) error + ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) + GetApplication(ctx context.Context, appName string) (*model.Application, error) + DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) + PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) + CreateApplication(context.Context, apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) + UpdateApplication(context.Context, *model.Application, apisv1.UpdateApplicationRequest) (*apisv1.ApplicationBase, error) + DeleteApplication(ctx context.Context, app *model.Application) error + Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) + ListComponents(ctx context.Context, app *model.Application, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentBase, error) + AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) + DetailComponent(ctx context.Context, app *model.Application, componentName string) (*apisv1.DetailComponentResponse, error) + DeleteComponent(ctx context.Context, app *model.Application, componentName string) error + ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) + AddPolicy(ctx context.Context, app *model.Application, policy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) + DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) + DeletePolicy(ctx context.Context, app *model.Application, policyName string) error + UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) + GetApplicationEnvBindingPolicy(ctx context.Context, app *model.Application) (*v1alpha1.EnvBindingSpec, error) + UpdateApplicationEnvBinding(ctx context.Context, app *model.Application, envName string, diff apisv1.PutApplicationEnvRequest) (*apisv1.EnvBind, error) + CreateApplicationEnvBinding(ctx context.Context, app *model.Application, env apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBind, error) + DeleteApplicationEnvBinding(ctx context.Context, app *model.Application, envName string) error + CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTrait) (*apisv1.ApplicationTrait, error) + DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string) error + UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string, req apisv1.UpdateApplicationTrait) (*apisv1.ApplicationTrait, error) } type applicationUsecaseImpl struct { @@ -102,9 +105,9 @@ func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUseca } } -// ListApplicationPlans list applications -func (c *applicationUsecaseImpl) ListApplicationPlans(ctx context.Context, listOptions apisv1.ListApplicatioPlanOptions) ([]*apisv1.ApplicationPlanBase, error) { - var app = model.ApplicationPlan{} +// ListApplications list applications +func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) { + var app = model.Application{} if listOptions.Namespace != "" { app.Namespace = listOptions.Namespace } @@ -112,9 +115,9 @@ func (c *applicationUsecaseImpl) ListApplicationPlans(ctx context.Context, listO if err != nil { return nil, err } - var list []*apisv1.ApplicationPlanBase + var list []*apisv1.ApplicationBase for _, entity := range entitys { - appBase := c.converAppModelToBase(entity.(*model.ApplicationPlan)) + appBase := c.converAppModelToBase(entity.(*model.Application)) if listOptions.Query != "" && !(strings.Contains(appBase.Alias, listOptions.Query) || strings.Contains(appBase.Name, listOptions.Query) || @@ -132,9 +135,9 @@ func (c *applicationUsecaseImpl) ListApplicationPlans(ctx context.Context, listO return list, nil } -// GetApplicationPlan get application model -func (c *applicationUsecaseImpl) GetApplicationPlan(ctx context.Context, appName string) (*model.ApplicationPlan, error) { - var app = model.ApplicationPlan{ +// GetApplication get application model +func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName string) (*model.Application, error) { + var app = model.Application{ Name: appName, } if err := c.ds.Get(ctx, &app); err != nil { @@ -146,8 +149,8 @@ func (c *applicationUsecaseImpl) GetApplicationPlan(ctx context.Context, appName return &app, nil } -// DetailApplicationPlan detail application plan info -func (c *applicationUsecaseImpl) DetailApplicationPlan(ctx context.Context, app *model.ApplicationPlan) (*apisv1.DetailApplicationPlanResponse, error) { +// DetailApplication detail application info +func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) { base := c.converAppModelToBase(app) policys, err := c.queryApplicationPolicys(ctx, app) if err != nil { @@ -161,9 +164,9 @@ func (c *applicationUsecaseImpl) DetailApplicationPlan(ctx context.Context, app for _, p := range policys { policyNames = append(policyNames, p.Name) } - var detail = &apisv1.DetailApplicationPlanResponse{ - ApplicationPlanBase: *base, - Policies: policyNames, + var detail = &apisv1.DetailApplicationResponse{ + ApplicationBase: *base, + Policies: policyNames, ResourceInfo: apisv1.ApplicationResourceInfo{ ComponentNum: len(components), }, @@ -173,14 +176,14 @@ func (c *applicationUsecaseImpl) DetailApplicationPlan(ctx context.Context, app } // PublishApplicationTemplate publish app template -func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, app *model.ApplicationPlan) (*apisv1.ApplicationTemplateBase, error) { +func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) { //TODO: return nil, nil } -// CreateApplicationPlan create application -func (c *applicationUsecaseImpl) CreateApplicationPlan(ctx context.Context, req apisv1.CreateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) { - application := model.ApplicationPlan{ +// CreateApplication create application +func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) { + application := model.Application{ Name: req.Name, Alias: req.Alias, Description: req.Description, @@ -197,8 +200,6 @@ func (c *applicationUsecaseImpl) CreateApplicationPlan(ctx context.Context, req if exit { return nil, bcode.ErrApplicationExist } - // check can deploy - var canDeploy bool if req.YamlConfig != "" { var oamApp v1beta1.Application if err := yaml.Unmarshal([]byte(req.YamlConfig), &oamApp); err != nil { @@ -238,7 +239,7 @@ func (c *applicationUsecaseImpl) CreateApplicationPlan(ctx context.Context, req Outputs: step.Outputs, }) } - _, err := c.workflowUsecase.CreateWorkflow(ctx, &application, apisv1.CreateWorkflowPlanRequest{ + _, err := c.workflowUsecase.CreateWorkflow(ctx, &application, apisv1.CreateWorkflowRequest{ AppName: application.PrimaryKey(), Name: application.Name, Description: "Created automatically.", @@ -250,13 +251,11 @@ func (c *applicationUsecaseImpl) CreateApplicationPlan(ctx context.Context, req return nil, err } } - // you can deploy only if the application contains components - canDeploy = len(oamApp.Spec.Components) > 0 } // build-in create env binding policy if len(req.EnvBind) > 0 { - if _, err := c.createApplictionPlanEnvBindingPolicy(ctx, &application, req.EnvBind); err != nil { + if _, err := c.createApplictionEnvBindingPolicy(ctx, &application, req.EnvBind); err != nil { return nil, err } } @@ -270,19 +269,10 @@ func (c *applicationUsecaseImpl) CreateApplicationPlan(ctx context.Context, req } // render app base info. base := c.converAppModelToBase(&application) - // deploy to cluster if need. - if req.Deploy && canDeploy { - if _, err := c.Deploy(ctx, &application, apisv1.ApplicationDeployRequest{ - Commit: "init create auto deploy", - SourceType: "web", - }); err != nil { - return nil, err - } - } return base, nil } -func (c *applicationUsecaseImpl) UpdateApplicationPlan(ctx context.Context, app *model.ApplicationPlan, req apisv1.UpdateApplicationPlanRequest) (*apisv1.ApplicationPlanBase, error) { +func (c *applicationUsecaseImpl) UpdateApplication(ctx context.Context, app *model.Application, req apisv1.UpdateApplicationRequest) (*apisv1.ApplicationBase, error) { app.Alias = req.Alias app.Description = req.Description app.Labels = req.Labels @@ -293,18 +283,18 @@ func (c *applicationUsecaseImpl) UpdateApplicationPlan(ctx context.Context, app return c.converAppModelToBase(app), nil } -func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, app *model.ApplicationPlan, components []common.ApplicationComponent) error { +func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, app *model.Application, components []common.ApplicationComponent) error { var componentModels []datastore.Entity for _, component := range components { // TODO: Check whether the component type is supported. - var traits []model.ApplicationTraitPlan + var traits []model.ApplicationTrait for _, trait := range component.Traits { properties, err := model.NewJSONStruct(trait.Properties) if err != nil { log.Logger.Errorf("parse trait properties failire %w", err) return bcode.ErrInvalidProperties } - traits = append(traits, model.ApplicationTraitPlan{ + traits = append(traits, model.ApplicationTrait{ Type: trait.Type, Properties: properties, }) @@ -314,7 +304,7 @@ func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, a log.Logger.Errorf("parse component properties failire %w", err) return bcode.ErrInvalidProperties } - componentModel := model.ApplicationComponentPlan{ + componentModel := model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), Name: component.Name, Type: component.Type, @@ -332,8 +322,8 @@ func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, a return c.ds.BatchAdd(ctx, componentModels) } -func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.ApplicationPlan, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentPlanBase, error) { - var component = model.ApplicationComponentPlan{ +func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.Application, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentBase, error) { + var component = model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), } components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) @@ -343,9 +333,9 @@ func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model. envComponents := map[string]bool{} componentSelectorDefine := false if op.EnvName != "" { - envbinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) + envbinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) if err != nil && !errors.Is(err, bcode.ErrApplicationNotEnv) { - log.Logger.Errorf("query app plan env binding policy config failure %s", err.Error()) + log.Logger.Errorf("query app env binding policy config failure %s", err.Error()) } if envbinding != nil { for _, env := range envbinding.Envs { @@ -359,9 +349,9 @@ func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model. } } - var list []*apisv1.ComponentPlanBase + var list []*apisv1.ComponentBase for _, component := range components { - pm := component.(*model.ApplicationComponentPlan) + pm := component.(*model.ApplicationComponent) if !componentSelectorDefine || envComponents[pm.Name] { list = append(list, c.converComponentModelToBase(pm)) } @@ -371,8 +361,8 @@ func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model. // DetailComponent detail app component // TODO: Add status data about the component. -func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model.ApplicationPlan, policyName string) (*apisv1.DetailComponentPlanResponse, error) { - var component = model.ApplicationComponentPlan{ +func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailComponentResponse, error) { + var component = model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), Name: policyName, } @@ -380,13 +370,13 @@ func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model if err != nil { return nil, err } - return &apisv1.DetailComponentPlanResponse{ - ApplicationComponentPlan: component, + return &apisv1.DetailComponentResponse{ + ApplicationComponent: component, }, nil } -func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.ApplicationComponentPlan) *apisv1.ComponentPlanBase { - return &apisv1.ComponentPlanBase{ +func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.ApplicationComponent) *apisv1.ComponentBase { + return &apisv1.ComponentBase{ Name: m.Name, Alias: m.Alias, Description: m.Description, @@ -401,7 +391,7 @@ func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.Application } // ListPolicies list application policies -func (c *applicationUsecaseImpl) ListPolicies(ctx context.Context, app *model.ApplicationPlan) ([]*apisv1.PolicyBase, error) { +func (c *applicationUsecaseImpl) ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) { policies, err := c.queryApplicationPolicys(ctx, app) if err != nil { return nil, err @@ -413,7 +403,7 @@ func (c *applicationUsecaseImpl) ListPolicies(ctx context.Context, app *model.Ap return list, nil } -func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.ApplicationPolicyPlan) *apisv1.PolicyBase { +func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.ApplicationPolicy) *apisv1.PolicyBase { pb := &apisv1.PolicyBase{ Name: policy.Name, Type: policy.Type, @@ -426,25 +416,25 @@ func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.Applicati return pb } -func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app *model.ApplicationPlan, policys []v1beta1.AppPolicy) error { +func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app *model.Application, policys []v1beta1.AppPolicy) error { var policyModels []datastore.Entity - var envbindingPolicy *model.ApplicationPolicyPlan + var envbindingPolicy *model.ApplicationPolicy for _, policy := range policys { properties, err := model.NewJSONStruct(policy.Properties) if err != nil { log.Logger.Errorf("parse trait properties failire %w", err) return bcode.ErrInvalidProperties } - appPolicyPlan := &model.ApplicationPolicyPlan{ + appPolicy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Name: policy.Name, Type: policy.Type, Properties: properties, } if policy.Type != string(EnvBindPolicy) { - policyModels = append(policyModels, appPolicyPlan) + policyModels = append(policyModels, appPolicy) } else { - envbindingPolicy = appPolicyPlan + envbindingPolicy = appPolicy } } // If multiple configurations are configured, enable only the last one. @@ -475,8 +465,8 @@ func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app return c.ds.BatchAdd(ctx, policyModels) } -func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, app *model.ApplicationPlan) (list []*model.ApplicationPolicyPlan, err error) { - var policy = model.ApplicationPolicyPlan{ +func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, app *model.Application) (list []*model.ApplicationPolicy, err error) { + var policy = model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), } policys, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) @@ -484,14 +474,14 @@ func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, ap return nil, err } for _, policy := range policys { - pm := policy.(*model.ApplicationPolicyPlan) + pm := policy.(*model.ApplicationPolicy) list = append(list, pm) } return } -func (c *applicationUsecaseImpl) GetApplicationPlanEnvBindingPolicy(ctx context.Context, app *model.ApplicationPlan) (*v1alpha1.EnvBindingSpec, error) { - var policy = model.ApplicationPolicyPlan{ +func (c *applicationUsecaseImpl) GetApplicationEnvBindingPolicy(ctx context.Context, app *model.Application) (*v1alpha1.EnvBindingSpec, error) { + var policy = model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Type: string(EnvBindPolicy), Name: EnvBindPolicyDefaultName, @@ -511,8 +501,8 @@ func (c *applicationUsecaseImpl) GetApplicationPlanEnvBindingPolicy(ctx context. } // nolint -func (c *applicationUsecaseImpl) createApplictionPlanEnvBindingPolicy(ctx context.Context, app *model.ApplicationPlan, envbinds apisv1.EnvBindList) (*model.ApplicationPolicyPlan, error) { - policy := &model.ApplicationPolicyPlan{ +func (c *applicationUsecaseImpl) createApplictionEnvBindingPolicy(ctx context.Context, app *model.Application, envbinds apisv1.EnvBindList) (*model.ApplicationPolicy, error) { + policy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Name: EnvBindPolicyDefaultName, Description: "build-in create", @@ -539,8 +529,8 @@ func (c *applicationUsecaseImpl) createApplictionPlanEnvBindingPolicy(ctx contex // DetailPolicy detail app policy // TODO: Add status data about the policy. -func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) (*apisv1.DetailPolicyResponse, error) { - var policy = model.ApplicationPolicyPlan{ +func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) { + var policy = model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Name: policyName, } @@ -556,7 +546,7 @@ func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Ap // 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, app *model.ApplicationPlan, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) { +func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) { // step1: Render oam application version := utils.GenerateVersion("") oamApp, err := c.renderOAMApplication(ctx, app, req.WorkflowName, version) @@ -630,7 +620,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat }, nil } -func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMoel *model.ApplicationPlan, reqWorkflowName, version string) (*v1beta1.Application, error) { +func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMoel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) { var app = &v1beta1.Application{ TypeMeta: metav1.TypeMeta{ Kind: "Application", @@ -645,7 +635,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo }, }, } - var component = model.ApplicationComponentPlan{ + var component = model.ApplicationComponent{ AppPrimaryKey: appMoel.PrimaryKey(), } components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) @@ -656,7 +646,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return nil, bcode.ErrNoComponent } - var policy = model.ApplicationPolicyPlan{ + var policy = model.ApplicationPolicy{ AppPrimaryKey: appMoel.PrimaryKey(), } policies, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) @@ -665,7 +655,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } for _, entity := range components { - component := entity.(*model.ApplicationComponentPlan) + component := entity.(*model.ApplicationComponent) var traits []common.ApplicationTrait for _, trait := range component.Traits { aTrait := common.ApplicationTrait{ @@ -689,7 +679,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } for _, entity := range policies { - policy := entity.(*model.ApplicationPolicyPlan) + policy := entity.(*model.ApplicationPolicy) apolicy := v1beta1.AppPolicy{ Name: policy.Name, Type: policy.Type, @@ -700,9 +690,9 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo app.Spec.Policies = append(app.Spec.Policies, apolicy) } - // Priority 1 uses the requested workflow as release plan. - // Priority 2 uses the default workflow as release plan. - var workflow *model.WorkflowPlan + // Priority 1 uses the requested workflow as release . + // Priority 2 uses the default workflow as release . + var workflow *model.Workflow if reqWorkflowName != "" { workflow, err = c.workflowUsecase.GetWorkflow(ctx, reqWorkflowName) if err != nil { @@ -736,8 +726,8 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return app, nil } -func (c *applicationUsecaseImpl) converAppModelToBase(app *model.ApplicationPlan) *apisv1.ApplicationPlanBase { - appBase := &apisv1.ApplicationPlanBase{ +func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *apisv1.ApplicationBase { + appBase := &apisv1.ApplicationBase{ Name: app.Name, Alias: app.Alias, Namespace: app.Namespace, @@ -763,8 +753,8 @@ func (c *applicationUsecaseImpl) converAppModelToBase(app *model.ApplicationPlan return appBase } -// DeleteApplicationPlan delete application plan -func (c *applicationUsecaseImpl) DeleteApplicationPlan(ctx context.Context, app *model.ApplicationPlan) error { +// DeleteApplication delete application +func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *model.Application) error { // TODO: check app can be deleted // query all components to deleted @@ -783,14 +773,14 @@ func (c *applicationUsecaseImpl) DeleteApplicationPlan(ctx context.Context, app } for _, component := range components { - err := c.ds.Delete(ctx, &model.ApplicationComponentPlan{AppPrimaryKey: app.PrimaryKey(), Name: component.Name}) + err := c.ds.Delete(ctx, &model.ApplicationComponent{AppPrimaryKey: app.PrimaryKey(), Name: component.Name}) if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { log.Logger.Errorf("delete component %s in app %s failure %s", component.Name, app.Name, err.Error()) } } for _, policy := range policies { - err := c.ds.Delete(ctx, &model.ApplicationPolicyPlan{AppPrimaryKey: app.PrimaryKey(), Name: policy.Name}) + err := c.ds.Delete(ctx, &model.ApplicationPolicy{AppPrimaryKey: app.PrimaryKey(), Name: policy.Name}) if err != nil && errors.Is(err, datastore.ErrRecordNotExist) { log.Logger.Errorf("delete policy %s in app %s failure %s", policy.Name, app.Name, err.Error()) } @@ -799,8 +789,8 @@ func (c *applicationUsecaseImpl) DeleteApplicationPlan(ctx context.Context, app return c.ds.Delete(ctx, app) } -func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.ApplicationPlan, com apisv1.CreateComponentPlanRequest) (*apisv1.ComponentPlanBase, error) { - componentModel := model.ApplicationComponentPlan{ +func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) { + componentModel := model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), Description: com.Description, Labels: com.Labels, @@ -824,7 +814,7 @@ func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Ap log.Logger.Warnf("add component for app %s failure %s", app.PrimaryKey(), err.Error()) return nil, err } - return &apisv1.ComponentPlanBase{ + return &apisv1.ComponentBase{ Name: componentModel.Name, Description: componentModel.Description, Labels: componentModel.Labels, @@ -837,8 +827,8 @@ func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Ap }, nil } -func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.ApplicationPlan, componentName string) error { - var component = model.ApplicationComponentPlan{ +func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.Application, componentName string) error { + var component = model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), Name: componentName, } @@ -852,8 +842,8 @@ func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model return nil } -func (c *applicationUsecaseImpl) AddPolicy(ctx context.Context, app *model.ApplicationPlan, createpolicy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) { - policyModel := model.ApplicationPolicyPlan{ +func (c *applicationUsecaseImpl) AddPolicy(ctx context.Context, app *model.Application, createpolicy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) { + policyModel := model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Description: createpolicy.Description, // TODO: Get user information from ctx and assign a value. @@ -884,8 +874,8 @@ func (c *applicationUsecaseImpl) AddPolicy(ctx context.Context, app *model.Appli }, nil } -func (c *applicationUsecaseImpl) DeletePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string) error { - var policy = model.ApplicationPolicyPlan{ +func (c *applicationUsecaseImpl) DeletePolicy(ctx context.Context, app *model.Application, policyName string) error { + var policy = model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Name: policyName, } @@ -899,8 +889,8 @@ func (c *applicationUsecaseImpl) DeletePolicy(ctx context.Context, app *model.Ap return nil } -func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.ApplicationPlan, policyName string, policyUpdate apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) { - var policy = model.ApplicationPolicyPlan{ +func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policyUpdate apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) { + var policy = model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Name: policyName, } @@ -928,14 +918,14 @@ func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.Ap }, nil } -// UpdateApplicationEnvBindingPlan update application env binding diff -func (c *applicationUsecaseImpl) UpdateApplicationEnvBindingPlan( +// UpdateApplicationEnvBinding update application env binding diff +func (c *applicationUsecaseImpl) UpdateApplicationEnvBinding( ctx context.Context, - app *model.ApplicationPlan, + app *model.Application, envName string, - envUpdate apisv1.PutApplicationPlanEnvRequest) (*apisv1.EnvBind, error) { + envUpdate apisv1.PutApplicationEnvRequest) (*apisv1.EnvBind, error) { // update env-binding policy - envBinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) + envBinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) if err != nil { return nil, err } @@ -955,7 +945,7 @@ func (c *applicationUsecaseImpl) UpdateApplicationEnvBindingPlan( log.Logger.Errorf("new env binding properties failure,%s", err.Error()) return nil, bcode.ErrInvalidProperties } - policy := &model.ApplicationPolicyPlan{ + policy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Name: EnvBindPolicyDefaultName, } @@ -1007,21 +997,21 @@ func (c *applicationUsecaseImpl) UpdateApplicationEnvBindingPlan( return re, nil } -// CreateApplicationEnvBindingPlan create application env plan -func (c *applicationUsecaseImpl) CreateApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, envReq apisv1.CreateApplicationEnvPlanRequest) (*apisv1.EnvBind, error) { +// CreateApplicationEnvBinding create application env +func (c *applicationUsecaseImpl) CreateApplicationEnvBinding(ctx context.Context, app *model.Application, envReq apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBind, error) { for _, env := range app.EnvBinds { if env.Name == envReq.Name { return nil, bcode.ErrApplicationEnvExist } } - envBinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) + envBinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) if err != nil { if !errors.Is(err, bcode.ErrApplicationNotEnv) { return nil, err } } if envBinding == nil { - _, err := c.createApplictionPlanEnvBindingPolicy(ctx, app, []*apisv1.EnvBind{&envReq.EnvBind}) + _, err := c.createApplictionEnvBindingPolicy(ctx, app, []*apisv1.EnvBind{&envReq.EnvBind}) if err != nil { return nil, err } @@ -1033,7 +1023,7 @@ func (c *applicationUsecaseImpl) CreateApplicationEnvBindingPlan(ctx context.Con log.Logger.Errorf("new env binding properties failure,%s", err.Error()) return nil, bcode.ErrInvalidProperties } - policy := &model.ApplicationPolicyPlan{ + policy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Name: EnvBindPolicyDefaultName, } @@ -1051,15 +1041,15 @@ func (c *applicationUsecaseImpl) CreateApplicationEnvBindingPlan(ctx context.Con return &envReq.EnvBind, nil } -// DeleteApplicationEnvBindingPlan delete application env binding plan -func (c *applicationUsecaseImpl) DeleteApplicationEnvBindingPlan(ctx context.Context, app *model.ApplicationPlan, envName string) error { +// DeleteApplicationEnvBinding delete application env binding +func (c *applicationUsecaseImpl) DeleteApplicationEnvBinding(ctx context.Context, app *model.Application, envName string) error { for i, envBind := range app.EnvBinds { if envBind.Name == envName { app.EnvBinds = append(app.EnvBinds[0:i], app.EnvBinds[i+1:]...) } } - envBinding, err := c.GetApplicationPlanEnvBindingPolicy(ctx, app) + envBinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) if err != nil { return err } @@ -1073,7 +1063,7 @@ func (c *applicationUsecaseImpl) DeleteApplicationEnvBindingPlan(ctx context.Con log.Logger.Errorf("new env binding properties failure,%s", err.Error()) return bcode.ErrInvalidProperties } - policy := &model.ApplicationPolicyPlan{ + policy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), Name: EnvBindPolicyDefaultName, } @@ -1090,6 +1080,18 @@ func (c *applicationUsecaseImpl) DeleteApplicationEnvBindingPlan(ctx context.Con return nil } +func (c *applicationUsecaseImpl) CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTrait) (*apisv1.ApplicationTrait, error) { + return nil, nil +} + +func (c *applicationUsecaseImpl) DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string) error { + return nil +} + +func (c *applicationUsecaseImpl) UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string, req apisv1.UpdateApplicationTrait) (*apisv1.ApplicationTrait, error) { + return nil, nil +} + func createEnvBind(envBind apisv1.EnvBind) v1alpha1.EnvConfig { placement := v1alpha1.EnvPlacement{ ClusterSelector: &common.ClusterSelector{ diff --git a/pkg/apiserver/rest/usecase/applicationplan_test.go b/pkg/apiserver/rest/usecase/application_test.go similarity index 77% rename from pkg/apiserver/rest/usecase/applicationplan_test.go rename to pkg/apiserver/rest/usecase/application_test.go index b57a495db..db6778ad4 100644 --- a/pkg/apiserver/rest/usecase/applicationplan_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -50,7 +50,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test CreateApplication function", func() { By("test sample create") - req := v1.CreateApplicationPlanRequest{ + req := v1.CreateApplicationRequest{ Name: "test-app", Namespace: "test-app-namespace", Description: "this is a test app", @@ -70,18 +70,18 @@ var _ = Describe("Test application usecase function", func() { }, }}, } - base, err := appUsecase.CreateApplicationPlan(context.TODO(), req) + base, err := appUsecase.CreateApplication(context.TODO(), req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) - _, err = appUsecase.CreateApplicationPlan(context.TODO(), req) + _, err = appUsecase.CreateApplication(context.TODO(), req) equal := cmp.Equal(err, bcode.ErrApplicationExist, cmpopts.EquateErrors()) Expect(equal).Should(BeTrue()) By("test with oam yaml config create") bs, err := ioutil.ReadFile("./testdata/example-app.yaml") Expect(err).Should(Succeed()) - req = v1.CreateApplicationPlanRequest{ + req = v1.CreateApplicationRequest{ Name: "test-app-sadasd", Namespace: "test-app-namespace", Description: "this is a test app", @@ -89,11 +89,11 @@ var _ = Describe("Test application usecase function", func() { Labels: map[string]string{"test": "true"}, YamlConfig: string(bs), } - base, err = appUsecase.CreateApplicationPlan(context.TODO(), req) + base, err = appUsecase.CreateApplication(context.TODO(), req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) - req = v1.CreateApplicationPlanRequest{ + req = v1.CreateApplicationRequest{ Name: "test-app-sadasd2", Namespace: "test-app-namespace", Description: "this is a test app", @@ -101,14 +101,14 @@ var _ = Describe("Test application usecase function", func() { Labels: map[string]string{"test": "true"}, YamlConfig: "asdasdasdasd", } - base, err = appUsecase.CreateApplicationPlan(context.TODO(), req) + base, err = appUsecase.CreateApplication(context.TODO(), req) equal = cmp.Equal(err, bcode.ErrApplicationConfig, cmpopts.EquateErrors()) Expect(equal).Should(BeTrue()) Expect(base).Should(BeNil()) bs, err = ioutil.ReadFile("./testdata/example-app-error.yaml") Expect(err).Should(Succeed()) - req = v1.CreateApplicationPlanRequest{ + req = v1.CreateApplicationRequest{ Name: "test-app-sadasd3", Namespace: "test-app-namespace", Description: "this is a test app", @@ -116,12 +116,12 @@ var _ = Describe("Test application usecase function", func() { Labels: map[string]string{"test": "true"}, YamlConfig: string(bs), } - _, err = appUsecase.CreateApplicationPlan(context.TODO(), req) + _, err = appUsecase.CreateApplication(context.TODO(), req) equal = cmp.Equal(err, bcode.ErrInvalidProperties, cmpopts.EquateErrors()) Expect(equal).Should(BeTrue()) By("Test create app with env binding") - req = v1.CreateApplicationPlanRequest{ + req = v1.CreateApplicationRequest{ Name: "test-app-sadasd4", Namespace: "test-app-namespace", Description: "this is a test app", @@ -146,52 +146,52 @@ var _ = Describe("Test application usecase function", func() { }, }, } - appBase, err := appUsecase.CreateApplicationPlan(context.TODO(), req) + appBase, err := appUsecase.CreateApplication(context.TODO(), req) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(appBase.EnvBind), 2)).Should(BeEmpty()) - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd4") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) }) - It("Test GetApplicationPlanEnvBindingPolicy", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd4") + It("Test GetApplicationEnvBindingPolicy", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") Expect(err).Should(BeNil()) - envBinding, err := appUsecase.GetApplicationPlanEnvBindingPolicy(context.TODO(), appModel) + envBinding, err := appUsecase.GetApplicationEnvBindingPolicy(context.TODO(), appModel) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(envBinding.Envs), 2)).Should(BeEmpty()) }) - It("Test UpdateApplicationPlanEnvBindingDiff", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + It("Test UpdateApplicationEnvBindingDiff", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) - _, err = appUsecase.UpdateApplicationEnvBindingPlan(context.TODO(), appModel, "staging", v1.PutApplicationPlanEnvRequest{ + _, err = appUsecase.UpdateApplicationEnvBinding(context.TODO(), appModel, "staging", v1.PutApplicationEnvRequest{ ComponentSelector: &v1.ComponentSelector{Components: []string{"hello-world-server"}}, }) Expect(err).Should(BeNil()) }) It("Test ListApplications function", func() { - apps, err := appUsecase.ListApplicationPlans(context.TODO(), v1.ListApplicatioPlanOptions{}) + apps, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioOptions{}) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(apps), 3)).Should(BeEmpty()) }) It("Test DetailApplication function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - detail, err := appUsecase.DetailApplicationPlan(context.TODO(), appModel) + detail, err := appUsecase.DetailApplication(context.TODO(), appModel) Expect(err).Should(BeNil()) Expect(cmp.Diff(detail.ResourceInfo.ComponentNum, 2)).Should(BeEmpty()) Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) }) It("Test GetWorkflow function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -201,7 +201,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test ListPolicies function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -213,7 +213,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test ListComponents function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -239,7 +239,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test DetailComponent function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -251,7 +251,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test DetailPolicy function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -262,10 +262,10 @@ var _ = Describe("Test application usecase function", func() { }) It("Test AddComponent function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - base, err := appUsecase.AddComponent(context.TODO(), appModel, v1.CreateComponentPlanRequest{ + base, err := appUsecase.AddComponent(context.TODO(), appModel, v1.CreateComponentRequest{ Name: "test2", Description: "this is a test2 component", Labels: map[string]string{}, @@ -278,7 +278,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test DetailComponent function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) detailResponse, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") @@ -289,7 +289,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test AddPolicy function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ @@ -309,7 +309,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test DetailPolicy function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, "env-binding-2") @@ -319,7 +319,7 @@ var _ = Describe("Test application usecase function", func() { }) It("Test UpdatePolicy function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) base, err := appUsecase.UpdatePolicy(context.TODO(), appModel, "env-binding-2", v1.UpdatePolicyRequest{ @@ -331,34 +331,34 @@ var _ = Describe("Test application usecase function", func() { Expect((*base.Properties)["envs"]).Should(BeEmpty()) }) It("Test DeletePolicy function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) err = appUsecase.DeletePolicy(context.TODO(), appModel, "env-binding-2") Expect(err).Should(BeNil()) }) It("Test DeleteComponent function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) err = appUsecase.DeleteComponent(context.TODO(), appModel, "test2") Expect(err).Should(BeNil()) }) - It("Test CreateApplicationEnvBindingPlan function", func() { - req := v1.CreateApplicationPlanRequest{ + It("Test CreateApplicationEnvBinding function", func() { + req := v1.CreateApplicationRequest{ Name: "not-have-env-bind", Namespace: "test-app-namespace", Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, } - _, err := appUsecase.CreateApplicationPlan(context.TODO(), req) + _, err := appUsecase.CreateApplication(context.TODO(), req) Expect(err).Should(BeNil()) - appModel4, err := appUsecase.GetApplicationPlan(context.TODO(), "not-have-env-bind") + appModel4, err := appUsecase.GetApplication(context.TODO(), "not-have-env-bind") Expect(err).Should(BeNil()) By("test create first env") - env4, err := appUsecase.CreateApplicationEnvBindingPlan(context.TODO(), appModel4, v1.CreateApplicationEnvPlanRequest{ + env4, err := appUsecase.CreateApplicationEnvBinding(context.TODO(), appModel4, v1.CreateApplicationEnvRequest{ EnvBind: v1.EnvBind{ Name: "prod2", Alias: "生产环境", @@ -371,15 +371,15 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(env4).ShouldNot(BeNil()) - appModelNew, err := appUsecase.GetApplicationPlan(context.TODO(), "not-have-env-bind") + appModelNew, err := appUsecase.GetApplication(context.TODO(), "not-have-env-bind") Expect(err).Should(BeNil()) Expect(cmp.Diff(len(appModelNew.EnvBinds), 1)).Should(BeEmpty()) By("test create not first env") - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - env, err := appUsecase.CreateApplicationEnvBindingPlan(context.TODO(), appModel, v1.CreateApplicationEnvPlanRequest{ + env, err := appUsecase.CreateApplicationEnvBinding(context.TODO(), appModel, v1.CreateApplicationEnvRequest{ EnvBind: v1.EnvBind{ Name: "prod2", Alias: "生产环境", @@ -392,20 +392,20 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(env).ShouldNot(BeNil()) - appModelNew, err = appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModelNew, err = appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(len(appModelNew.EnvBinds), 4)).Should(BeEmpty()) - spec, err := appUsecase.GetApplicationPlanEnvBindingPolicy(context.TODO(), appModelNew) + spec, err := appUsecase.GetApplicationEnvBindingPolicy(context.TODO(), appModelNew) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(spec.Envs), 4)).Should(BeEmpty()) }) - It("Test CreateApplicationEnvBindingPlan function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + It("Test CreateApplicationEnvBinding function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - env, err := appUsecase.UpdateApplicationEnvBindingPlan(context.TODO(), appModel, "prod2", v1.PutApplicationPlanEnvRequest{ + env, err := appUsecase.UpdateApplicationEnvBinding(context.TODO(), appModel, "prod2", v1.PutApplicationEnvRequest{ ComponentSelector: &v1.ComponentSelector{ Components: []string{}, }, @@ -420,16 +420,16 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(len(components), 0)).Should(BeEmpty()) }) - It("Test DeleteApplicationEnvBindingPlan function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + It("Test DeleteApplicationEnvBinding function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - err = appUsecase.DeleteApplicationEnvBindingPlan(context.TODO(), appModel, "prod2") + err = appUsecase.DeleteApplicationEnvBinding(context.TODO(), appModel, "prod2") Expect(err).Should(BeNil()) }) It("Test Deploy Application function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) res, err := appUsecase.Deploy(context.TODO(), appModel, v1.ApplicationDeployRequest{ @@ -447,9 +447,9 @@ var _ = Describe("Test application usecase function", func() { }) It("Test DeleteApplication function", func() { - appModel, err := appUsecase.GetApplicationPlan(context.TODO(), "test-app-sadasd") + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) - err = appUsecase.DeleteApplicationPlan(context.TODO(), appModel) + err = appUsecase.DeleteApplication(context.TODO(), appModel) Expect(err).Should(BeNil()) components, err := appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{}) Expect(err).Should(BeNil()) diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index 62f1f59ad..bb7666c07 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -32,14 +32,6 @@ label: ReadinessProbe检测 sort: 4 subParameters: - - description: Number of seconds after which the probe times out. - jsonKey: timeoutSeconds - label: TimeoutSeconds - sort: 100 - uiType: Number - validate: - defaultValue: 1 - required: true - description: Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute @@ -160,8 +152,22 @@ required: true uiType: KV validate: {} + - description: Number of seconds after which the probe times out. + jsonKey: timeoutSeconds + label: TimeoutSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true uiType: Group validate: {} +- description: Commands to run in the container + jsonKey: cmd + label: Cmd + sort: 100 + uiType: Strings + validate: {} - description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core) jsonKey: cpu @@ -226,21 +232,6 @@ validate: {} uiType: Structs validate: {} -- description: If addRevisionLabel is true, the appRevision label will be added to - the underlying pods - jsonKey: addRevisionLabel - label: AddRevisionLabel - sort: 100 - uiType: Switch - validate: - defaultValue: false - required: true -- description: Commands to run in the container - jsonKey: cmd - label: Cmd - sort: 100 - uiType: Strings - validate: {} - description: Specify image pull secrets for your service jsonKey: imagePullSecrets label: ImagePullSecrets @@ -252,6 +243,15 @@ label: LivenessProbe sort: 100 subParameters: + - description: Number of consecutive failures required to determine the container + is not alive (liveness probe) or not ready (readiness probe). + jsonKey: failureThreshold + label: FailureThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 3 + required: true - description: Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute @@ -371,44 +371,13 @@ required: true uiType: KV validate: {} - - description: Number of consecutive failures required to determine the container - is not alive (liveness probe) or not ready (readiness probe). - jsonKey: failureThreshold - label: FailureThreshold - sort: 100 - uiType: Number - validate: - defaultValue: 3 - required: true uiType: KV validate: {} -- description: Which port do you want customer traffic sent to - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - defaultValue: 80 - required: true - description: Declare volumes and volumeMounts jsonKey: volumes label: Volumes sort: 100 subParameters: - - description: "" - jsonKey: mountPath - label: MountPath - sort: 100 - uiType: Input - validate: - required: true - - description: "" - jsonKey: name - label: Name - sort: 100 - uiType: Input - validate: - required: true - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' jsonKey: type label: Type @@ -425,5 +394,36 @@ - label: EmptyDir value: emptyDir required: true + - description: "" + jsonKey: mountPath + label: MountPath + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true uiType: Structs validate: {} +- description: If addRevisionLabel is true, the appRevision label will be added to + the underlying pods + jsonKey: addRevisionLabel + label: AddRevisionLabel + sort: 100 + uiType: Switch + validate: + defaultValue: false + required: true +- description: Which port do you want customer traffic sent to + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + defaultValue: 80 + required: true diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 9a228bac5..7df6b40fa 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -35,13 +35,13 @@ import ( // WorkflowUsecase workflow manage api type WorkflowUsecase interface { - ListApplicationWorkflow(ctx context.Context, app *model.ApplicationPlan, enable *bool) ([]*apisv1.WorkflowPlanBase, error) - GetWorkflow(ctx context.Context, workflowName string) (*model.WorkflowPlan, error) - DetailWorkflow(ctx context.Context, workflow *model.WorkflowPlan) (*apisv1.DetailWorkflowPlanResponse, error) - GetApplicationDefaultWorkflow(ctx context.Context, app *model.ApplicationPlan) (*model.WorkflowPlan, error) + ListApplicationWorkflow(ctx context.Context, app *model.Application, enable *bool) ([]*apisv1.WorkflowBase, error) + GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) + DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) + GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) DeleteWorkflow(ctx context.Context, workflowName string) error - CreateWorkflow(ctx context.Context, app *model.ApplicationPlan, req apisv1.CreateWorkflowPlanRequest) (*apisv1.DetailWorkflowPlanResponse, error) - UpdateWorkflow(ctx context.Context, workflow *model.WorkflowPlan, req apisv1.UpdateWorkflowPlanRequest) (*apisv1.DetailWorkflowPlanResponse, error) + CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) ListWorkflowRecords(ctx context.Context, workflowName string, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) DetailWorkflowRecord(ctx context.Context, workflowName, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) } @@ -57,7 +57,7 @@ type workflowUsecaseImpl struct { // DeleteWorkflow delete application workflow func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, workflowName string) error { - var workflow = &model.WorkflowPlan{ + var workflow = &model.Workflow{ Name: workflowName, } if err := w.ds.Delete(ctx, workflow); err != nil { @@ -69,7 +69,7 @@ func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, workflowName s return nil } -func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.ApplicationPlan, req apisv1.CreateWorkflowPlanRequest) (*apisv1.DetailWorkflowPlanResponse, error) { +func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { var steps []model.WorkflowStep for _, step := range req.Steps { properties, err := model.NewJSONStructByString(step.Properties) @@ -86,7 +86,7 @@ func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.App }) } // It is allowed to set multiple workflows as default, and only one takes effect. - var workflow = model.WorkflowPlan{ + var workflow = model.Workflow{ Steps: steps, Name: req.Name, Enable: req.Enable, @@ -100,7 +100,7 @@ func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.App return w.DetailWorkflow(ctx, &workflow) } -func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *model.WorkflowPlan, req apisv1.UpdateWorkflowPlanRequest) (*apisv1.DetailWorkflowPlanResponse, error) { +func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { var steps []model.WorkflowStep for _, step := range req.Steps { properties, err := model.NewJSONStructByString(step.Properties) @@ -128,7 +128,7 @@ func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *mode } // DetailWorkflow detail workflow -func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.WorkflowPlan) (*apisv1.DetailWorkflowPlanResponse, error) { +func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) { var steps []apisv1.WorkflowStep for _, step := range workflow.Steps { apiStep := apisv1.WorkflowStep{ @@ -143,8 +143,8 @@ func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *mode } steps = append(steps, apiStep) } - return &apisv1.DetailWorkflowPlanResponse{ - WorkflowPlanBase: apisv1.WorkflowPlanBase{ + return &apisv1.DetailWorkflowResponse{ + WorkflowBase: apisv1.WorkflowBase{ Name: workflow.Name, Description: workflow.Description, Enable: workflow.Enable, @@ -157,8 +157,8 @@ func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *mode } // GetWorkflow get workflow model -func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName string) (*model.WorkflowPlan, error) { - var workflow = model.WorkflowPlan{ +func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) { + var workflow = model.Workflow{ Name: workflowName, } if err := w.ds.Get(ctx, &workflow); err != nil { @@ -168,8 +168,8 @@ func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName stri } // ListApplicationWorkflow list application workflows -func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app *model.ApplicationPlan, enable *bool) ([]*apisv1.WorkflowPlanBase, error) { - var workflow = model.WorkflowPlan{ +func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app *model.Application, enable *bool) ([]*apisv1.WorkflowBase, error) { + var workflow = model.Workflow{ AppPrimaryKey: app.PrimaryKey(), } if enable != nil { @@ -179,10 +179,10 @@ func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app * if err != nil { return nil, err } - var list []*apisv1.WorkflowPlanBase + var list []*apisv1.WorkflowBase for _, workflow := range workflows { - wm := workflow.(*model.WorkflowPlan) - list = append(list, &apisv1.WorkflowPlanBase{ + wm := workflow.(*model.Workflow) + list = append(list, &apisv1.WorkflowBase{ Name: wm.Name, Description: wm.Description, Enable: wm.Enable, @@ -195,8 +195,8 @@ func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app * } // GetApplicationDefaultWorkflow get application default workflow -func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, app *model.ApplicationPlan) (*model.WorkflowPlan, error) { - var workflow = model.WorkflowPlan{ +func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) { + var workflow = model.Workflow{ AppPrimaryKey: app.PrimaryKey(), Default: true, } @@ -205,7 +205,7 @@ func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, return nil, err } if len(workflows) > 0 { - return workflows[0].(*model.WorkflowPlan), nil + return workflows[0].(*model.Workflow), nil } return nil, bcode.ErrWorkflowNoDefault } diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 9477d2f77..37c36a491 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -40,22 +40,22 @@ var _ = Describe("Test workflow usecase functions", func() { workflowUsecase = &workflowUsecaseImpl{ds: ds} }) It("Test CreateWorkflow function", func() { - req := apisv1.CreateWorkflowPlanRequest{ + req := apisv1.CreateWorkflowRequest{ Name: "test-workflow-1", Description: "this is a workflow", } - base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.ApplicationPlan{ + base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ Name: "test-app", }, req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) - req = apisv1.CreateWorkflowPlanRequest{ + req = apisv1.CreateWorkflowRequest{ Name: "test-workflow-2", Description: "this is test workflow", Default: true, } - base, err = workflowUsecase.CreateWorkflow(context.TODO(), &model.ApplicationPlan{ + base, err = workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ Name: "test-app", }, req) Expect(err).Should(BeNil()) @@ -63,7 +63,7 @@ var _ = Describe("Test workflow usecase functions", func() { }) It("Test GetApplicationDefaultWorkflow function", func() { - workflow, err := workflowUsecase.GetApplicationDefaultWorkflow(context.TODO(), &model.ApplicationPlan{ + workflow, err := workflowUsecase.GetApplicationDefaultWorkflow(context.TODO(), &model.Application{ Name: "test-app", }) Expect(err).Should(BeNil()) diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index 3d2f06037..bd69a1e3b 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -23,7 +23,7 @@ var ErrApplicationConfig = NewBcode(400, 10000, "application config does not com var ErrComponentTypeNotSupport = NewBcode(400, 10001, "An unsupported component type was used.") // ErrApplicationExist application is exist -var ErrApplicationExist = NewBcode(400, 10002, "application plan name is exist") +var ErrApplicationExist = NewBcode(400, 10002, "application name is exist") // ErrInvalidProperties properties(trait or component or others) is invalid var ErrInvalidProperties = NewBcode(400, 10003, "properties is invalid") @@ -38,10 +38,10 @@ var ErrDeployApplyFail = NewBcode(500, 10005, "application deploy apply failure" var ErrNoComponent = NewBcode(200, 10006, "application not have components, can not deploy") // ErrApplicationComponetExist application component is exist -var ErrApplicationComponetExist = NewBcode(400, 10007, "application component plan is exist") +var ErrApplicationComponetExist = NewBcode(400, 10007, "application component is exist") // ErrApplicationComponetNotExist application component is not exist -var ErrApplicationComponetNotExist = NewBcode(404, 10008, "application component plan is not exist") +var ErrApplicationComponetNotExist = NewBcode(404, 10008, "application component is not exist") // ErrApplicationPolicyExist application policy is exist var ErrApplicationPolicyExist = NewBcode(400, 10009, "application policy is exist") @@ -53,10 +53,10 @@ var ErrApplicationPolicyNotExist = NewBcode(404, 10010, "application policy is n var ErrCreateNamespace = NewBcode(500, 10011, "auto create namespace failure") // ErrApplicationNotExist application is not exist -var ErrApplicationNotExist = NewBcode(404, 10012, "application plan name is not exist") +var ErrApplicationNotExist = NewBcode(404, 10012, "application name is not exist") // ErrApplicationNotEnv no env binding policy -var ErrApplicationNotEnv = NewBcode(404, 10013, "application plan not set env binding policy") +var ErrApplicationNotEnv = NewBcode(404, 10013, "application not set env binding policy") // ErrApplicationEnvExist application env is exist -var ErrApplicationEnvExist = NewBcode(400, 10014, "application env plan is exist") +var ErrApplicationEnvExist = NewBcode(400, 10014, "application env is exist") diff --git a/pkg/apiserver/rest/webservice/applicationplan.go b/pkg/apiserver/rest/webservice/application.go similarity index 70% rename from pkg/apiserver/rest/webservice/applicationplan.go rename to pkg/apiserver/rest/webservice/application.go index 709faca8c..7ed62ec16 100644 --- a/pkg/apiserver/rest/webservice/applicationplan.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -29,101 +29,101 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) -type applicationPlanWebService struct { +type applicationWebService struct { applicationUsecase usecase.ApplicationUsecase } -// NewApplicationPlanWebService new application manage webservice -func NewApplicationPlanWebService(applicationUsecase usecase.ApplicationUsecase) WebService { - return &applicationPlanWebService{ +// NewApplicationWebService new application manage webservice +func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase) WebService { + return &applicationWebService{ applicationUsecase: applicationUsecase, } } -func (c *applicationPlanWebService) GetWebService() *restful.WebService { +func (c *applicationWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) - ws.Path(versionPrefix+"/applicationplans"). + ws.Path(versionPrefix+"/applications"). Consumes(restful.MIME_XML, restful.MIME_JSON). Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for application manage") - tags := []string{"applicationplan"} + tags := []string{"application"} - ws.Route(ws.GET("/").To(c.listApplicationPlans). - Doc("list all application plans"). + 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")). Param(ws.QueryParameter("namespace", "Namespace-based search").DataType("string")). Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")). - Returns(200, "", apis.ListApplicationPlanResponse{}). + Returns(200, "", apis.ListApplicationResponse{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ListApplicationPlanResponse{})) + Writes(apis.ListApplicationResponse{})) - ws.Route(ws.POST("/").To(c.createApplicationPlan). - Doc("create one application plan"). + ws.Route(ws.POST("/").To(c.createApplication). + Doc("create one application "). Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateApplicationPlanRequest{}). - Returns(200, "", apis.ApplicationPlanBase{}). + Reads(apis.CreateApplicationRequest{}). + Returns(200, "", apis.ApplicationBase{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ApplicationPlanBase{})) + Writes(apis.ApplicationBase{})) - ws.Route(ws.DELETE("/{name}").To(c.deleteApplicationPlan). + ws.Route(ws.DELETE("/{name}").To(c.deleteApplication). Doc("delete one application"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Returns(200, "", apis.EmptyResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}").To(c.detailApplicationPlan). - Doc("detail one application plan"). + ws.Route(ws.GET("/{name}").To(c.detailApplication). + Doc("detail one application "). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). - Returns(200, "", apis.DetailApplicationPlanResponse{}). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Returns(200, "", apis.DetailApplicationResponse{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.DetailApplicationPlanResponse{})) + Writes(apis.DetailApplicationResponse{})) - ws.Route(ws.PUT("/{name}").To(c.updateApplicationPlan). - Doc("update one application plan"). + ws.Route(ws.PUT("/{name}").To(c.updateApplication). + Doc("update one application "). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). - Reads(apis.UpdateApplicationPlanRequest{}). - Returns(200, "", apis.ApplicationPlanBase{}). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Reads(apis.UpdateApplicationRequest{}). + Returns(200, "", apis.ApplicationBase{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ApplicationPlanBase{})) + Writes(apis.ApplicationBase{})) ws.Route(ws.PUT("/{name}/envs/{envName}").To(c.updateApplicationEnvBinding). - Doc("set application plan differences in the specified environment"). + Doc("set application differences in the specified environment"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). Filter(c.envCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). - Param(ws.PathParameter("envName", "identifier of the application plan").DataType("string")). - Reads(apis.PutApplicationPlanEnvRequest{}). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the application ").DataType("string")). + Reads(apis.PutApplicationEnvRequest{}). Returns(200, "", apis.EnvBind{}). Returns(400, "", bcode.Bcode{}). Writes(apis.EnvBind{})) ws.Route(ws.POST("/{name}/envs").To(c.createApplicationEnv). - Doc("creating an application environment plan"). + Doc("creating an application environment "). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). - Reads(apis.CreateApplicationEnvPlanRequest{}). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Reads(apis.CreateApplicationEnvRequest{}). Returns(200, "", apis.EnvBind{}). Returns(400, "", bcode.Bcode{}). Writes(apis.EmptyResponse{})) ws.Route(ws.DELETE("/{name}/envs/{envName}").To(c.deleteApplicationEnv). - Doc("delete an application environment plan"). + Doc("delete an application environment "). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). Filter(c.envCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). - Param(ws.PathParameter("envName", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the application ").DataType("string")). Returns(200, "", apis.EmptyResponse{}). Returns(404, "", bcode.Bcode{}). Writes(apis.EmptyResponse{})) @@ -132,7 +132,7 @@ func (c *applicationPlanWebService) GetWebService() *restful.WebService { Doc("create one application template"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Reads(apis.CreateApplicationTemplateRequest{}). Returns(200, "", apis.ApplicationTemplateBase{}). Returns(400, "", bcode.Bcode{}). @@ -142,44 +142,44 @@ func (c *applicationPlanWebService) GetWebService() *restful.WebService { Doc("deploy or upgrade the application"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Returns(200, "", apis.ApplicationDeployRequest{}). Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationDeployResponse{})) - ws.Route(ws.GET("/{name}/componentplans").To(c.listApplicationComponents). - Doc("gets the componentplan topology of the application"). + ws.Route(ws.GET("/{name}/components").To(c.listApplicationComponents). + Doc("gets the list of application components"). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Param(ws.QueryParameter("envName", "list components that deployed in define env").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). - Returns(200, "", apis.ComponentPlanListResponse{}). + Returns(200, "", apis.ComponentListResponse{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ComponentPlanListResponse{})) + Writes(apis.ComponentListResponse{})) - ws.Route(ws.POST("/{name}/componentplans").To(c.createComponent). - Doc("create component plan for application plan"). + ws.Route(ws.POST("/{name}/components").To(c.createComponent). + Doc("create component for application "). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateComponentPlanRequest{}). - Returns(200, "", apis.ComponentPlanBase{}). + Reads(apis.CreateComponentRequest{}). + Returns(200, "", apis.ComponentBase{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ComponentPlanBase{})) + Writes(apis.ComponentBase{})) - ws.Route(ws.GET("/{name}/componentplans/{componentName}").To(c.detailComponent). - Doc("detail component plan for application plan"). + ws.Route(ws.GET("/{name}/components/{componentName}").To(c.detailComponent). + Doc("detail component for application "). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). - Returns(200, "", apis.DetailComponentPlanResponse{}). + Returns(200, "", apis.DetailComponentResponse{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.DetailComponentPlanResponse{})) + Writes(apis.DetailComponentResponse{})) ws.Route(ws.GET("/{name}/policies").To(c.listApplicationPolicies). Doc("list policy for application"). Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application plan").DataType("string")). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Returns(200, "", apis.ListApplicationPolicy{}). Returns(400, "", bcode.Bcode{}). @@ -228,8 +228,8 @@ func (c *applicationPlanWebService) GetWebService() *restful.WebService { return ws } -func (c *applicationPlanWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - app, err := c.applicationUsecase.GetApplicationPlan(req.Request.Context(), req.PathParameter("name")) +func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app, err := c.applicationUsecase.GetApplication(req.Request.Context(), req.PathParameter("name")) if err != nil { bcode.ReturnError(req, res, err) return @@ -238,9 +238,9 @@ func (c *applicationPlanWebService) appCheckFilter(req *restful.Request, res *re chain.ProcessFilter(req, res) } -func (c *applicationPlanWebService) envCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) - envBinding, err := c.applicationUsecase.GetApplicationPlanEnvBindingPolicy(req.Request.Context(), app) +func (c *applicationWebService) envCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + envBinding, err := c.applicationUsecase.GetApplicationEnvBindingPolicy(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) return @@ -255,9 +255,9 @@ func (c *applicationPlanWebService) envCheckFilter(req *restful.Request, res *re bcode.ReturnError(req, res, bcode.ErrApplicationNotEnv) } -func (c *applicationPlanWebService) createApplicationPlan(req *restful.Request, res *restful.Response) { +func (c *applicationWebService) createApplication(req *restful.Request, res *restful.Response) { // Verify the validity of parameters - var createReq apis.CreateApplicationPlanRequest + var createReq apis.CreateApplicationRequest if err := req.ReadEntity(&createReq); err != nil { bcode.ReturnError(req, res, err) return @@ -267,7 +267,7 @@ func (c *applicationPlanWebService) createApplicationPlan(req *restful.Request, return } // Call the usecase layer code - appBase, err := c.applicationUsecase.CreateApplicationPlan(req.Request.Context(), createReq) + appBase, err := c.applicationUsecase.CreateApplication(req.Request.Context(), createReq) if err != nil { log.Logger.Errorf("create application failure %s", err.Error()) bcode.ReturnError(req, res, err) @@ -281,8 +281,8 @@ func (c *applicationPlanWebService) createApplicationPlan(req *restful.Request, } } -func (c *applicationPlanWebService) listApplicationPlans(req *restful.Request, res *restful.Response) { - apps, err := c.applicationUsecase.ListApplicationPlans(req.Request.Context(), apis.ListApplicatioPlanOptions{ +func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { + apps, err := c.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioOptions{ Namespace: req.QueryParameter("namespace"), Cluster: req.QueryParameter("cluster"), Query: req.QueryParameter("query"), @@ -291,15 +291,15 @@ func (c *applicationPlanWebService) listApplicationPlans(req *restful.Request, r bcode.ReturnError(req, res, err) return } - if err := res.WriteEntity(apis.ListApplicationPlanResponse{ApplicationPlans: apps}); err != nil { + if err := res.WriteEntity(apis.ListApplicationResponse{Applications: apps}); err != nil { bcode.ReturnError(req, res, err) return } } -func (c *applicationPlanWebService) detailApplicationPlan(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) - detail, err := c.applicationUsecase.DetailApplicationPlan(req.Request.Context(), app) +func (c *applicationWebService) detailApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.DetailApplication(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) return @@ -310,8 +310,8 @@ func (c *applicationPlanWebService) detailApplicationPlan(req *restful.Request, } } -func (c *applicationPlanWebService) publishApplicationTemplate(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) publishApplicationTemplate(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) base, err := c.applicationUsecase.PublishApplicationTemplate(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) @@ -324,8 +324,8 @@ func (c *applicationPlanWebService) publishApplicationTemplate(req *restful.Requ } // deployApplication TODO: return event model -func (c *applicationPlanWebService) deployApplication(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) deployApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters var createReq apis.ApplicationDeployRequest if err := req.ReadEntity(&createReq); err != nil { @@ -347,9 +347,9 @@ func (c *applicationPlanWebService) deployApplication(req *restful.Request, res } } -func (c *applicationPlanWebService) deleteApplicationPlan(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) - err := c.applicationUsecase.DeleteApplicationPlan(req.Request.Context(), app) +func (c *applicationWebService) deleteApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.applicationUsecase.DeleteApplication(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) return @@ -360,8 +360,8 @@ func (c *applicationPlanWebService) deleteApplicationPlan(req *restful.Request, } } -func (c *applicationPlanWebService) listApplicationComponents(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) listApplicationComponents(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) components, err := c.applicationUsecase.ListComponents(req.Request.Context(), app, apis.ListApplicationComponentOptions{ EnvName: req.QueryParameter("envName"), }) @@ -369,16 +369,16 @@ func (c *applicationPlanWebService) listApplicationComponents(req *restful.Reque bcode.ReturnError(req, res, err) return } - if err := res.WriteEntity(apis.ComponentPlanListResponse{ComponentPlans: components}); err != nil { + if err := res.WriteEntity(apis.ComponentListResponse{Components: components}); err != nil { bcode.ReturnError(req, res, err) return } } -func (c *applicationPlanWebService) createComponent(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) createComponent(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters - var createReq apis.CreateComponentPlanRequest + var createReq apis.CreateComponentRequest if err := req.ReadEntity(&createReq); err != nil { bcode.ReturnError(req, res, err) return @@ -398,8 +398,8 @@ func (c *applicationPlanWebService) createComponent(req *restful.Request, res *r } } -func (c *applicationPlanWebService) detailComponent(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) detailComponent(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) detail, err := c.applicationUsecase.DetailComponent(req.Request.Context(), app, req.PathParameter("componentName")) if err != nil { bcode.ReturnError(req, res, err) @@ -411,8 +411,8 @@ func (c *applicationPlanWebService) detailComponent(req *restful.Request, res *r } } -func (c *applicationPlanWebService) createApplicationPolicy(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) createApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters var createReq apis.CreatePolicyRequest if err := req.ReadEntity(&createReq); err != nil { @@ -434,8 +434,8 @@ func (c *applicationPlanWebService) createApplicationPolicy(req *restful.Request } } -func (c *applicationPlanWebService) listApplicationPolicies(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) listApplicationPolicies(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) policies, err := c.applicationUsecase.ListPolicies(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) @@ -447,8 +447,8 @@ func (c *applicationPlanWebService) listApplicationPolicies(req *restful.Request } } -func (c *applicationPlanWebService) detailApplicationPolicy(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) detailApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) detail, err := c.applicationUsecase.DetailPolicy(req.Request.Context(), app, req.PathParameter("policyName")) if err != nil { bcode.ReturnError(req, res, err) @@ -460,8 +460,8 @@ func (c *applicationPlanWebService) detailApplicationPolicy(req *restful.Request } } -func (c *applicationPlanWebService) deleteApplicationPolicy(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) deleteApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) err := c.applicationUsecase.DeletePolicy(req.Request.Context(), app, req.PathParameter("policyName")) if err != nil { bcode.ReturnError(req, res, err) @@ -473,8 +473,8 @@ func (c *applicationPlanWebService) deleteApplicationPolicy(req *restful.Request } } -func (c *applicationPlanWebService) updateApplicationPolicy(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) updateApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters var updateReq apis.UpdatePolicyRequest if err := req.ReadEntity(&updateReq); err != nil { @@ -496,10 +496,10 @@ func (c *applicationPlanWebService) updateApplicationPolicy(req *restful.Request } } -func (c *applicationPlanWebService) updateApplicationEnvBinding(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) updateApplicationEnvBinding(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters - var updateReq apis.PutApplicationPlanEnvRequest + var updateReq apis.PutApplicationEnvRequest if err := req.ReadEntity(&updateReq); err != nil { bcode.ReturnError(req, res, err) return @@ -508,7 +508,7 @@ func (c *applicationPlanWebService) updateApplicationEnvBinding(req *restful.Req bcode.ReturnError(req, res, err) return } - diff, err := c.applicationUsecase.UpdateApplicationEnvBindingPlan(req.Request.Context(), app, req.PathParameter("envName"), updateReq) + diff, err := c.applicationUsecase.UpdateApplicationEnvBinding(req.Request.Context(), app, req.PathParameter("envName"), updateReq) if err != nil { bcode.ReturnError(req, res, err) return @@ -519,10 +519,10 @@ func (c *applicationPlanWebService) updateApplicationEnvBinding(req *restful.Req } } -func (c *applicationPlanWebService) updateApplicationPlan(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) updateApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters - var updateReq apis.UpdateApplicationPlanRequest + var updateReq apis.UpdateApplicationRequest if err := req.ReadEntity(&updateReq); err != nil { bcode.ReturnError(req, res, err) return @@ -531,7 +531,7 @@ func (c *applicationPlanWebService) updateApplicationPlan(req *restful.Request, bcode.ReturnError(req, res, err) return } - base, err := c.applicationUsecase.UpdateApplicationPlan(req.Request.Context(), app, updateReq) + base, err := c.applicationUsecase.UpdateApplication(req.Request.Context(), app, updateReq) if err != nil { bcode.ReturnError(req, res, err) return @@ -542,10 +542,10 @@ func (c *applicationPlanWebService) updateApplicationPlan(req *restful.Request, } } -func (c *applicationPlanWebService) createApplicationEnv(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) +func (c *applicationWebService) createApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters - var createReq apis.CreateApplicationEnvPlanRequest + var createReq apis.CreateApplicationEnvRequest if err := req.ReadEntity(&createReq); err != nil { bcode.ReturnError(req, res, err) return @@ -554,7 +554,7 @@ func (c *applicationPlanWebService) createApplicationEnv(req *restful.Request, r bcode.ReturnError(req, res, err) return } - base, err := c.applicationUsecase.CreateApplicationEnvBindingPlan(req.Request.Context(), app, createReq) + base, err := c.applicationUsecase.CreateApplicationEnvBinding(req.Request.Context(), app, createReq) if err != nil { bcode.ReturnError(req, res, err) return @@ -565,9 +565,9 @@ func (c *applicationPlanWebService) createApplicationEnv(req *restful.Request, r } } -func (c *applicationPlanWebService) deleteApplicationEnv(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.ApplicationPlan) - err := c.applicationUsecase.DeleteApplicationEnvBindingPlan(req.Request.Context(), app, req.PathParameter("envName")) +func (c *applicationWebService) deleteApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.applicationUsecase.DeleteApplicationEnvBinding(req.Request.Context(), app, req.PathParameter("envName")) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/validate_test.go b/pkg/apiserver/rest/webservice/validate_test.go index 8605450f7..ba0e2102d 100644 --- a/pkg/apiserver/rest/webservice/validate_test.go +++ b/pkg/apiserver/rest/webservice/validate_test.go @@ -27,40 +27,40 @@ import ( 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.CreateApplicationPlanRequest{ + var app0 = apisv1.CreateApplicationRequest{ Name: "a", Namespace: "namespace", } err := validate.Struct(&app0) Expect(err).ShouldNot(BeNil()) - var app1 = apisv1.CreateApplicationPlanRequest{ + var app1 = apisv1.CreateApplicationRequest{ Name: "Asdasd", Namespace: "namespace", } err = validate.Struct(&app1) Expect(err).ShouldNot(BeNil()) - var app2 = apisv1.CreateApplicationPlanRequest{ + var app2 = apisv1.CreateApplicationRequest{ Name: "asdasd asdasd ++", Namespace: "namespace", } err = validate.Struct(&app2) Expect(err).ShouldNot(BeNil()) - var app3 = apisv1.CreateApplicationPlanRequest{ + var app3 = apisv1.CreateApplicationRequest{ Name: "asdasd", Namespace: "namespace", } err = validate.Struct(&app3) Expect(err).Should(BeNil()) - var app4 = apisv1.CreateApplicationPlanRequest{ + var app4 = apisv1.CreateApplicationRequest{ Name: "asdasd-asdasd", Namespace: "namespace", } err = validate.Struct(&app4) Expect(err).Should(BeNil()) - var component = apisv1.CreateComponentPlanRequest{ + var component = apisv1.CreateComponentRequest{ Name: "asdasd-asdasd", ComponentType: "alibaba-ack", } diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 1f06da88f..6634610d4 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -67,7 +67,7 @@ func Init(ds datastore.DataStore) { definitionUsecase := usecase.NewDefinitionUsecase() addonUsecase := usecase.NewAddonUsecase(ds) RegistWebService(NewClusterWebService(clusterUsecase)) - RegistWebService(NewApplicationPlanWebService(applicationUsecase)) + RegistWebService(NewApplicationWebService(applicationUsecase)) RegistWebService(NewNamespaceWebService(namespaceUsecase)) RegistWebService(NewDefinitionWebservice(definitionUsecase)) RegistWebService(NewAddonWebService(addonUsecase)) diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index e831e956a..9e84d35af 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -46,45 +46,45 @@ type workflowWebService struct { func (w *workflowWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) - ws.Path(versionPrefix+"/workflowplans"). + 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{"workflowplan"} + tags := []string{"workflow"} ws.Route(ws.GET("/").To(w.listApplicationWorkflows). Doc("list application workflow"). Param(ws.QueryParameter("appName", "identifier of the application.").DataType("string").Required(true)). Param(ws.QueryParameter("enable", "query based on enable status").DataType("boolean")). Metadata(restfulspec.KeyOpenAPITags, tags). - Returns(200, "", apis.ListWorkflowPlanResponse{}). - Writes(apis.ListWorkflowPlanResponse{}).Do(returns200, returns500)) + Returns(200, "", apis.ListWorkflowResponse{}). + Writes(apis.ListWorkflowResponse{}).Do(returns200, returns500)) ws.Route(ws.POST("/").To(w.createApplicationWorkflow). Doc("create application workflow"). Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateWorkflowPlanRequest{}). - Returns(200, "create success", apis.DetailWorkflowPlanResponse{}). + Reads(apis.CreateWorkflowRequest{}). + Returns(200, "create success", apis.DetailWorkflowResponse{}). Returns(400, "create failure", bcode.Bcode{}). - Writes(apis.DetailWorkflowPlanResponse{}).Do(returns200, returns500)) + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) ws.Route(ws.GET("/{name}").To(w.detailWorkflow). Doc("detail application workflow"). Param(ws.PathParameter("name", "identifier of the workflow.").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(w.workflowCheckFilter). - Returns(200, "create success", apis.DetailWorkflowPlanResponse{}). - Writes(apis.DetailWorkflowPlanResponse{}).Do(returns200, returns500)) + Returns(200, "create success", apis.DetailWorkflowResponse{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) ws.Route(ws.PUT("/{name}").To(w.updateWorkflow). Doc("update application workflow config"). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(w.workflowCheckFilter). Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Reads(apis.UpdateWorkflowPlanRequest{}). - Returns(200, "", apis.DetailWorkflowPlanResponse{}). - Writes(apis.DetailWorkflowPlanResponse{}).Do(returns200, returns500)) + Reads(apis.UpdateWorkflowRequest{}). + Returns(200, "", apis.DetailWorkflowResponse{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) ws.Route(ws.DELETE("/{name}").To(w.deleteWorkflow). Doc("deletet workflow"). @@ -130,7 +130,7 @@ func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res bcode.ReturnError(req, res, bcode.ErrMustQueryByApp) return } - app, err := w.applicationUsecase.GetApplicationPlan(req.Request.Context(), req.QueryParameter("appName")) + app, err := w.applicationUsecase.GetApplication(req.Request.Context(), req.QueryParameter("appName")) if err != nil { bcode.ReturnError(req, res, err) return @@ -145,7 +145,7 @@ func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res bcode.ReturnError(req, res, err) return } - if err := res.WriteEntity(apis.ListWorkflowPlanResponse{WorkflowPlans: workflows}); err != nil { + if err := res.WriteEntity(apis.ListWorkflowResponse{Workflows: workflows}); err != nil { bcode.ReturnError(req, res, err) return } @@ -153,7 +153,7 @@ func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res *restful.Response) { // Verify the validity of parameters - var createReq apis.CreateWorkflowPlanRequest + var createReq apis.CreateWorkflowRequest if err := req.ReadEntity(&createReq); err != nil { bcode.ReturnError(req, res, err) return @@ -162,7 +162,7 @@ func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res bcode.ReturnError(req, res, err) return } - app, err := w.applicationUsecase.GetApplicationPlan(req.Request.Context(), createReq.AppName) + app, err := w.applicationUsecase.GetApplication(req.Request.Context(), createReq.AppName) if err != nil { bcode.ReturnError(req, res, err) return @@ -183,7 +183,7 @@ func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res } func (w *workflowWebService) detailWorkflow(req *restful.Request, res *restful.Response) { - workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.WorkflowPlan) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) detail, err := w.workflowUsecase.DetailWorkflow(req.Request.Context(), workflow) if err != nil { bcode.ReturnError(req, res, err) @@ -196,9 +196,9 @@ func (w *workflowWebService) detailWorkflow(req *restful.Request, res *restful.R } func (w *workflowWebService) updateWorkflow(req *restful.Request, res *restful.Response) { - workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.WorkflowPlan) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) // Verify the validity of parameters - var updateReq apis.UpdateWorkflowPlanRequest + var updateReq apis.UpdateWorkflowRequest if err := req.ReadEntity(&updateReq); err != nil { bcode.ReturnError(req, res, err) return diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index 71a0b2fb6..d7967662f 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -36,7 +36,7 @@ import ( var _ = Describe("Test application rest api", func() { It("Test create app", func() { defer GinkgoRecover() - var req = apisv1.CreateApplicationPlanRequest{ + var req = apisv1.CreateApplicationRequest{ Name: "test-app-sadasd", Namespace: "test-app-namespace", Description: "this is a test app", @@ -48,13 +48,13 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans", "application/json", bytes.NewBuffer(bodyByte)) + 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.ApplicationPlanBase + var appBase apisv1.ApplicationBase err = json.NewDecoder(res.Body).Decode(&appBase) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(appBase.Name, req.Name)).Should(BeEmpty()) @@ -66,7 +66,7 @@ var _ = Describe("Test application rest api", func() { It("Test delete app", func() { defer GinkgoRecover() - req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd", nil) + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd", nil) Expect(err).ShouldNot(HaveOccurred()) res, err := http.DefaultClient.Do(req) Expect(err).ShouldNot(HaveOccurred()) @@ -78,7 +78,7 @@ var _ = Describe("Test application rest api", func() { defer GinkgoRecover() bs, err := ioutil.ReadFile("./testdata/example-app.yaml") Expect(err).Should(Succeed()) - var req = apisv1.CreateApplicationPlanRequest{ + var req = apisv1.CreateApplicationRequest{ Name: "test-app-sadasd", Namespace: "test-app-namespace", Description: "this is a test app", @@ -88,13 +88,13 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans", "application/json", bytes.NewBuffer(bodyByte)) + 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.ApplicationPlanBase + var appBase apisv1.ApplicationBase err = json.NewDecoder(res.Body).Decode(&appBase) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(appBase.Name, req.Name)).Should(BeEmpty()) @@ -105,21 +105,21 @@ var _ = Describe("Test application rest api", func() { It("Test list components", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/componentplans") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components") 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 components apisv1.ComponentPlanListResponse + var components apisv1.ComponentListResponse err = json.NewDecoder(res.Body).Decode(&components) Expect(err).ShouldNot(HaveOccurred()) - Expect(cmp.Diff(len(components.ComponentPlans), 2)).Should(BeEmpty()) + Expect(cmp.Diff(len(components.Components), 2)).Should(BeEmpty()) }) It("Test list policies", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies") Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -133,7 +133,7 @@ var _ = Describe("Test application rest api", func() { It("Test get workflow", func() { // defer GinkgoRecover() - // res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies") + // res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies") // Expect(err).ShouldNot(HaveOccurred()) // Expect(res).ShouldNot(BeNil()) // Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -147,13 +147,13 @@ var _ = Describe("Test application rest api", func() { It("Test detail application", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd") 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 detail apisv1.DetailApplicationPlanResponse + var detail apisv1.DetailApplicationResponse err = json.NewDecoder(res.Body).Decode(&detail) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) @@ -168,7 +168,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/deploy", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/deploy", "application/json", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -188,7 +188,7 @@ var _ = Describe("Test application rest api", func() { It("Test create component", func() { defer GinkgoRecover() - var req = apisv1.CreateComponentPlanRequest{ + var req = apisv1.CreateComponentRequest{ Name: "test2", Description: "this is a test2 component", Labels: map[string]string{}, @@ -198,13 +198,13 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/componentplans", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components", "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 response apisv1.ComponentPlanBase + var response apisv1.ComponentBase err = json.NewDecoder(res.Body).Decode(&response) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(response.ComponentType, "worker")).Should(BeEmpty()) @@ -212,13 +212,13 @@ var _ = Describe("Test application rest api", func() { It("Test detail component", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/componentplans/test2") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2") 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 response apisv1.DetailComponentPlanResponse + var response apisv1.DetailComponentResponse err = json.NewDecoder(res.Body).Decode(&response) Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(len(response.DependsOn), 1)).Should(BeEmpty()) @@ -233,7 +233,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 400)).Should(BeEmpty()) @@ -245,7 +245,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte2, err := json.Marshal(req2) Expect(err).ShouldNot(HaveOccurred()) - res, err = http.Post("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte2)) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte2)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -260,7 +260,7 @@ var _ = Describe("Test application rest api", func() { It("Test detail application policy", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies/test2") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2") Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -280,7 +280,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte2, err := json.Marshal(req2) Expect(err).ShouldNot(HaveOccurred()) - req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies/test2", bytes.NewBuffer(bodyByte2)) + req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2", bytes.NewBuffer(bodyByte2)) Expect(err).ShouldNot(HaveOccurred()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) @@ -298,7 +298,7 @@ var _ = Describe("Test application rest api", func() { It("Test delete application policy", func() { defer GinkgoRecover() - req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applicationplans/test-app-sadasd/policies/test2", nil) + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2", nil) Expect(err).ShouldNot(HaveOccurred()) res, err := http.DefaultClient.Do(req) Expect(err).ShouldNot(HaveOccurred()) diff --git a/test/e2e-apiserver-test/testdata/workflow.json b/test/e2e-apiserver-test/testdata/workflow.json index e218e2bc1..0255aa1eb 100644 --- a/test/e2e-apiserver-test/testdata/workflow.json +++ b/test/e2e-apiserver-test/testdata/workflow.json @@ -1,14 +1,15 @@ { - "appName": "appplanName", + "appName": "appName", "name": "workflowName", "alias": "workflowAlias", - "description": "workflow plan description", + "description": "workflow description", "enable": true, "default": true, "steps": [ - { "name": "deploy-test", - "type": "deploy2env", - "properties":"" + { + "name": "deploy-test", + "type": "deploy2env", + "properties": "" } ] } \ No newline at end of file From 722ed480e96c0eb0dc4d8678c106f65c392b61b2 Mon Sep 17 00:00:00 2001 From: yangsoon Date: Fri, 12 Nov 2021 14:34:26 +0800 Subject: [PATCH 31/59] Feat: add more views (#2689) Co-authored-by: yangsoon --- .../velaql-views/component-pod-view.yaml | 70 ++++++++++++++++ .../templates/velaql-views/pod-view.yaml | 80 +++++++++++++++++++ .../templates/velaql-views/resource-view.yaml | 59 ++++++++++++++ pkg/apiserver/rest/usecase/velaql.go | 1 + pkg/velaql/providers/query/collector.go | 8 +- .../testdata/component-pod-view.yaml | 2 +- test/e2e-apiserver-test/velaql_test.go | 6 +- 7 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 charts/vela-core/templates/velaql-views/component-pod-view.yaml create mode 100644 charts/vela-core/templates/velaql-views/pod-view.yaml create mode 100644 charts/vela-core/templates/velaql-views/resource-view.yaml diff --git a/charts/vela-core/templates/velaql-views/component-pod-view.yaml b/charts/vela-core/templates/velaql-views/component-pod-view.yaml new file mode 100644 index 000000000..55aebfc73 --- /dev/null +++ b/charts/vela-core/templates/velaql-views/component-pod-view.yaml @@ -0,0 +1,70 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: component-pod-view + namespace: {{.Values.systemDefinitionNamespace}} +data: + template: | + import ( + "vela/ql" + "vela/op" + "list" + ) + + parameter: { + name: string + namespace: string + componentName: string + } + + appList: ql.#ListResourcesInApp & { + app: { + name: parameter.name + namespace: parameter.namespace + components: [parameter.componentName] + } + } + + if appList.err == _|_ { + appRev: appList.list[0].revision + resources: appList.list[0].components[0].resources + collectedPods: op.#Steps & { + for i, resource in resources { + "\(i)": ql.#CollectPods & { + value: resource.object + cluster: resource.cluster + } + } + } + + podsWithCluster: {for i, pods in collectedPods { + "\(i)": [ + for podObj in pods.list { + cluster: pods.cluster + obj: podObj + }, + ] + }} + + flatPods: list.FlattenN([ for pods in podsWithCluster { + pods + }], 1) + + status: { + podList: [ for pod in flatPods { + clusterName: pod.cluster + revision: appRev + podName: pod.obj.metadata.name + podIP: pod.obj.status.podIP + status: pod.obj.status.phase + hostIP: pod.obj.status.hostIP + nodeName: pod.obj.spec.nodeName + }] + } + } + + if appList.err != _|_ { + status: { + error: appList.err + } + } \ No newline at end of file diff --git a/charts/vela-core/templates/velaql-views/pod-view.yaml b/charts/vela-core/templates/velaql-views/pod-view.yaml new file mode 100644 index 000000000..9ba8258d0 --- /dev/null +++ b/charts/vela-core/templates/velaql-views/pod-view.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: pod-view + namespace: {{.Values.systemDefinitionNamespace}} +data: + template: | + import ( + "vela/ql" + ) + + parameter: { + name: string + namespace: string + cluster: *"" | string + } + + pod: ql.#Read & { + value: { + apiVersion: "v1" + kind: "Pod" + metadata: { + name: parameter.name + namespace: parameter.namespace + } + } + cluster: parameter.cluster + } + + eventList: ql.#SearchEvents & { + value: { + apiVersion: "v1" + kind: "Pod" + metadata: pod.value.metadata + } + cluster: parameter.cluster + } + + usageMetrics: ql.#Read & { + cluster: parameter.cluster + value: { + apiVersion: "metrics.k8s.io/v1beta1" + kind: "PodMetrics" + metadata: { + name: parameter.name + namespace: parameter.namespace + } + } + } + + status: { + if pod.err == _|_ { + containers: [ for container in pod.value.spec.containers { + name: container.name + image: container.image + status: {for containerStatus in pod.value.status.containerStatuses { + if containerStatus.name == container.name { + state: containerStatus.state + restartCount: containerStatus.restartCount + } + }} + resource: container.resources + if usageMetrics.err == _|_ { + usageResource: {for containerUsage in usageMetrics.value.containers { + if containerUsage.name == container.name { + cpu: containerUsage.usage.cpu + memory: containerUsage.usage.memory + } + }} + } + }] + if eventList.err == _|_ { + events: eventList.list + } + } + if pod.err != _|_ { + error: pod.err + } + } + diff --git a/charts/vela-core/templates/velaql-views/resource-view.yaml b/charts/vela-core/templates/velaql-views/resource-view.yaml new file mode 100644 index 000000000..18632c38c --- /dev/null +++ b/charts/vela-core/templates/velaql-views/resource-view.yaml @@ -0,0 +1,59 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: resource-view + namespace: {{.Values.systemDefinitionNamespace}} +data: + template: | + import ( + "vela/ql" + ) + + parameter: { + type: string + namespace: string + cluster: *"" | string + } + + schema: { + "secret": { + apiVersion: "v1" + kind: "Secret" + } + "configMap": { + apiVersion: "v1" + kind: "ConfigMap" + } + "pvc": { + apiVersion: "v1" + kind: "PersistentVolumeClaim" + } + "storageClass": { + apiVersion: "storage.k8s.io/v1" + kind: "StorageClass" + } + } + + List: ql.#List & { + resource: schema[parameter.type] + filter: { + namespace: parameter.namespace + } + cluster: parameter.cluster + } + + + status: { + if List.err == _|_ { + if len(List.list.items) == 0 { + error: "failed to list \(parameter.type) in namespace \(parameter.namespace)" + } + if len(List.list.items) != 0 { + list: List.list.items + } + } + + if List.err != _|_ { + error: List.err + } + } diff --git a/pkg/apiserver/rest/usecase/velaql.go b/pkg/apiserver/rest/usecase/velaql.go index 065469ced..0d42ec8db 100644 --- a/pkg/apiserver/rest/usecase/velaql.go +++ b/pkg/apiserver/rest/usecase/velaql.go @@ -73,6 +73,7 @@ func (v *velaQLUsecaseImpl) QueryView(ctx context.Context, velaQL string) (*apis queryValue, err := velaql.NewViewHandler(v.kubeClient, v.dm, v.pd).QueryView(ctx, query) if err != nil { + log.Logger.Errorf("fail to query the view %s", err.Error()) return nil, bcode.ErrViewQuery } diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go index 7007c2a7a..a8a156a18 100644 --- a/pkg/velaql/providers/query/collector.go +++ b/pkg/velaql/providers/query/collector.go @@ -94,7 +94,7 @@ func (c *Collector) CollectLatestResourceFromApp() ([]AppResources, error) { } compResList := c.extractComponentResourceWithOption(comps) if len(compResList) == 0 { - return nil, nil + return nil, errors.Errorf("fail to find resources created by %v", c.opt.Components) } return []AppResources{{ @@ -144,6 +144,9 @@ func (c *Collector) CollectHistoryResourceFromApp() ([]AppResources, error) { }) } } + if len(appResList) == 0 { + return nil, errors.Errorf("fail to find resources created by %v", c.opt.Components) + } return appResList, nil } @@ -153,6 +156,9 @@ func (c *Collector) extractComponentResourceWithOption(comps map[string][]Resour // if not specify component, return all components resource created by app if len(c.opt.Components) == 0 { for name, resource := range comps { + if len(resource) == 0 { + continue + } result = append(result, Component{ Name: name, Resources: resource, diff --git a/test/e2e-apiserver-test/testdata/component-pod-view.yaml b/test/e2e-apiserver-test/testdata/component-pod-view.yaml index 0c5428f84..0c8545bd8 100644 --- a/test/e2e-apiserver-test/testdata/component-pod-view.yaml +++ b/test/e2e-apiserver-test/testdata/component-pod-view.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: component-pod-view + name: test-component-pod-view namespace: vela-system data: template: | diff --git a/test/e2e-apiserver-test/velaql_test.go b/test/e2e-apiserver-test/velaql_test.go index 25ad5db65..1f117d9ed 100644 --- a/test/e2e-apiserver-test/velaql_test.go +++ b/test/e2e-apiserver-test/velaql_test.go @@ -131,7 +131,7 @@ var _ = Describe("Test velaQL rest api", func() { }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) queryRes, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "component-pod-view", appName, namespace, component1Name, "status"), + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "test-component-pod-view", appName, namespace, component1Name, "status"), ) Expect(err).Should(BeNil()) Expect(queryRes.StatusCode).Should(Equal(200)) @@ -145,7 +145,7 @@ var _ = Describe("Test velaQL rest api", func() { Eventually(func() error { queryRes1, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "component-pod-view", appName, namespace, component2Name, "status"), + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "test-component-pod-view", appName, namespace, component2Name, "status"), ) if err != nil { return err @@ -208,7 +208,7 @@ var _ = Describe("Test velaQL rest api", func() { Eventually(func() error { queryRes, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "component-pod-view", appName, namespace, component2Name, "status"), + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "test-component-pod-view", appName, namespace, component2Name, "status"), ) if err != nil { return err From e1c64540f4cb07e316c3648214ce067e12d5e6bc Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Fri, 12 Nov 2021 17:19:34 +0800 Subject: [PATCH 32/59] Feat: change api spec (#2695) * Feat: change api spec * Feat: change DeployEvent to ApplicationRevision * Fix: fix test bug * Fix: fix unit test bug Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 266 +++++++++++------- pkg/apiserver/model/application.go | 31 +- pkg/apiserver/model/deliverytarget.go | 69 +++++ pkg/apiserver/model/workflow.go | 2 - pkg/apiserver/rest/apis/v1/types.go | 113 +++++--- pkg/apiserver/rest/usecase/application.go | 175 ++++++------ .../rest/usecase/application_test.go | 64 ++--- .../rest/usecase/testdata/ui-schema.yaml | 178 ++++++------ pkg/apiserver/rest/usecase/workflow.go | 19 +- pkg/apiserver/rest/usecase/workflow_test.go | 10 +- pkg/apiserver/rest/webservice/application.go | 16 +- pkg/velaql/providers/query/collector.go | 5 +- pkg/velaql/providers/query/handler_test.go | 3 +- test/e2e-apiserver-test/application_test.go | 12 +- 14 files changed, 551 insertions(+), 412 deletions(-) create mode 100644 pkg/apiserver/model/deliverytarget.go diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 18e866c9d..2ab4b4890 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -82,6 +82,50 @@ } }, "/api/v1/addon_registries/{name}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "update an addon registry", + "operationId": "updateAddonRegistry", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateAddonRegistryRequest" + } + }, + { + "type": "string", + "description": "identifier of the addon registry", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, "delete": { "consumes": [ "application/xml", @@ -346,14 +390,14 @@ }, { "type": "string", - "description": "Namespace-based search", + "description": "The namespace of the managed cluster", "name": "namespace", "in": "query" }, { "type": "string", - "description": "Cluster-based search", - "name": "cluster", + "description": "Name of the application delivery target", + "name": "target", "in": "query" } ], @@ -725,7 +769,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.EnvBind" + "$ref": "#/definitions/v1.EnvBinding" } }, "400": { @@ -778,7 +822,7 @@ "responses": { "200": { "schema": { - "$ref": "#/definitions/v1.EnvBind" + "$ref": "#/definitions/v1.EnvBinding" } }, "400": { @@ -1605,9 +1649,9 @@ "parameters": [ { "enum": [ - "component", "trait", - "workflowstep" + "workflowstep", + "component" ], "type": "string", "description": "query the definition type", @@ -2276,9 +2320,9 @@ "required": [ "batchRollingState", "currentBatch", + "upgradedReadyReplicas", "rollingState", "upgradedReplicas", - "upgradedReadyReplicas", "lastTargetAppRevision" ], "properties": { @@ -3276,10 +3320,10 @@ "description": { "type": "string" }, - "envBind": { + "envBinding": { "type": "array", "items": { - "$ref": "#/definitions/v1.EnvBind" + "$ref": "#/definitions/v1.EnvBinding" } }, "gatewayRule": { @@ -3315,18 +3359,18 @@ "v1.ApplicationDeployRequest": { "required": [ "workflowName", - "commit", - "sourceType", + "note", + "triggerType", "force" ], "properties": { - "commit": { - "type": "string" - }, "force": { "type": "boolean" }, - "sourceType": { + "note": { + "type": "string" + }, + "triggerType": { "type": "string" }, "workflowName": { @@ -3336,27 +3380,27 @@ }, "v1.ApplicationDeployResponse": { "required": [ - "version", - "status", "reason", "deployUser", - "commit", - "sourceType" + "note", + "triggerType", + "version", + "status" ], "properties": { - "commit": { + "deployUser": { "type": "string" }, - "deployUser": { + "note": { "type": "string" }, "reason": { "type": "string" }, - "sourceType": { + "status": { "type": "string" }, - "status": { + "triggerType": { "type": "string" }, "version": { @@ -3419,6 +3463,36 @@ } } }, + "v1.ApplicationRevisionBase": { + "required": [ + "version", + "status", + "reason", + "deployUser", + "note", + "triggerType" + ], + "properties": { + "deployUser": { + "type": "string" + }, + "note": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggerType": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "v1.ApplicationTemplateBase": { "required": [ "templateName", @@ -3578,19 +3652,6 @@ } } }, - "v1.ClusterSelector": { - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - } - } - }, "v1.ComponentBase": { "required": [ "name", @@ -3732,16 +3793,13 @@ }, "v1.CreateApplicationEnvRequest": { "required": [ - "clusterSelector", - "name" + "name", + "targetNames" ], "properties": { "alias": { "type": "string" }, - "clusterSelector": { - "$ref": "#/definitions/v1.ClusterSelector" - }, "componentSelector": { "$ref": "#/definitions/v1.ComponentSelector" }, @@ -3750,6 +3808,12 @@ }, "name": { "type": "string" + }, + "targetNames": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -3757,19 +3821,23 @@ "required": [ "name", "namespace", - "icon" + "icon", + "component" ], "properties": { "alias": { "type": "string" }, + "component": { + "$ref": "#/definitions/v1.CreateComponentRequest" + }, "description": { "type": "string" }, - "envBind": { + "envBinding": { "type": "array", "items": { - "$ref": "#/definitions/v1.EnvBind" + "$ref": "#/definitions/v1.EnvBinding" } }, "icon": { @@ -3812,7 +3880,6 @@ }, "v1.CreateApplicationTrait": { "required": [ - "name", "type", "properties" ], @@ -3823,9 +3890,6 @@ "description": { "type": "string" }, - "name": { - "type": "string" - }, "properties": { "type": "string" }, @@ -3941,12 +4005,6 @@ "description": { "type": "string" }, - "envNames": { - "type": "array", - "items": { - "type": "string" - } - }, "icon": { "type": "string" }, @@ -4010,7 +4068,6 @@ "required": [ "appName", "name", - "enable", "default" ], "properties": { @@ -4026,9 +4083,6 @@ "description": { "type": "string" }, - "enable": { - "type": "boolean" - }, "name": { "type": "string" }, @@ -4142,15 +4196,15 @@ }, "v1.DetailApplicationResponse": { "required": [ + "alias", + "createTime", + "gatewayRule", "name", "namespace", + "description", "updateTime", "icon", "status", - "gatewayRule", - "alias", - "description", - "createTime", "policies", "status", "resourceInfo", @@ -4167,10 +4221,10 @@ "description": { "type": "string" }, - "envBind": { + "envBinding": { "type": "array", "items": { - "$ref": "#/definitions/v1.EnvBind" + "$ref": "#/definitions/v1.EnvBinding" } }, "gatewayRule": { @@ -4220,20 +4274,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "name", + "updateTime", + "alias", + "description", + "icon", "provider", "apiServerURL", "kubeConfigSecret", - "alias", "status", - "dashboardURL", - "createTime", - "updateTime", "labels", "reason", + "dashboardURL", + "createTime", + "name", "kubeConfig", - "description", - "icon", "resourceInfo" ], "properties": { @@ -4292,12 +4346,12 @@ "v1.DetailComponentResponse": { "required": [ "updateTime", - "type", - "creator", - "createTime", "appPrimaryKey", - "alias", - "name" + "type", + "createTime", + "name", + "creator", + "alias" ], "properties": { "alias": { @@ -4392,13 +4446,13 @@ }, "v1.DetailPolicyResponse": { "required": [ + "name", "type", "description", "creator", "properties", "createTime", - "updateTime", - "name" + "updateTime" ], "properties": { "createTime": { @@ -4434,13 +4488,10 @@ "terminated", "deployTime", "deployUser", - "commit", - "sourceType" + "note", + "triggerType" ], "properties": { - "commit": { - "type": "string" - }, "deployTime": { "type": "string", "format": "date-time" @@ -4454,7 +4505,7 @@ "namespace": { "type": "string" }, - "sourceType": { + "note": { "type": "string" }, "startTime": { @@ -4472,18 +4523,21 @@ }, "terminated": { "type": "boolean" + }, + "triggerType": { + "type": "string" } } }, "v1.DetailWorkflowResponse": { "required": [ - "description", - "enable", - "default", "createTime", "updateTime", "name", "alias", + "description", + "enable", + "default", "workflowRecord" ], "properties": { @@ -4548,18 +4602,15 @@ } } }, - "v1.EnvBind": { + "v1.EnvBinding": { "required": [ "name", - "clusterSelector" + "targetNames" ], "properties": { "alias": { "type": "string" }, - "clusterSelector": { - "$ref": "#/definitions/v1.ClusterSelector" - }, "componentSelector": { "$ref": "#/definitions/v1.ComponentSelector" }, @@ -4568,6 +4619,12 @@ }, "name": { "type": "string" + }, + "targetNames": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -4823,10 +4880,10 @@ }, "v1.NamespaceDetailResponse": { "required": [ + "updateTime", "name", "description", - "createTime", - "updateTime" + "createTime" ], "properties": { "createTime": { @@ -5057,18 +5114,24 @@ } }, "v1.PutApplicationEnvRequest": { + "required": [ + "targetNames" + ], "properties": { "alias": { "type": "string" }, - "clusterSelector": { - "$ref": "#/definitions/v1.ClusterSelector" - }, "componentSelector": { "$ref": "#/definitions/v1.ComponentSelector" }, "description": { "type": "string" + }, + "targetNames": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -5085,6 +5148,13 @@ } } }, + "v1.UpdateAddonRegistryRequest": { + "properties": { + "git": { + "$ref": "#/definitions/model.GitAddonSource" + } + } + }, "v1.UpdateApplicationRequest": { "properties": { "alias": { diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 518250b88..4abbfc50a 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -23,7 +23,7 @@ import ( ) func init() { - RegistModel(&ApplicationComponent{}, &ApplicationPolicy{}, &Application{}, &DeployEvent{}) + RegistModel(&ApplicationComponent{}, &ApplicationPolicy{}, &Application{}, &ApplicationRevision{}) } // Application application delivery model @@ -35,7 +35,7 @@ type Application struct { Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` - EnvBinds []*EnvBind `json:"envBinds,omitempty"` + EnvBinding []*EnvBinding `json:"envBinding,omitempty"` } // TableName return custom table name @@ -60,13 +60,14 @@ func (a *Application) Index() map[string]string { return index } -// EnvBind application env bind -type EnvBind struct { +// EnvBinding application env binding +type EnvBinding struct { Name string `json:"name"` Alias string `json:"alias"` Description string `json:"description,omitempty"` - ClusterSelector ClusterSelector `json:"clusterSelector"` + TargetNames []string `json:"targetNames"` ComponentSelector *ComponentSelector `json:"componentSelector"` + //TODO: componentPatchs } // ClusterSelector cluster selector @@ -187,8 +188,8 @@ var DeployEventComplete = "complete" // DeployEventFail event status failure var DeployEventFail = "failure" -// DeployEvent record each application deployment event. -type DeployEvent struct { +// ApplicationRevision be created when an application initiates deployment and describes the phased version of the application. +type ApplicationRevision struct { Model AppPrimaryKey string `json:"appPrimaryKey"` Version string `json:"version"` @@ -203,26 +204,26 @@ type DeployEvent struct { DeployUser string `json:"deployUser"` // Information that users can note. - Commit string `json:"commit"` - // SourceType the event trigger source, Web or API - SourceType string `json:"sourceType"` + Note string `json:"note"` + // TriggerType the event trigger source, Web or API + TriggerType string `json:"triggerType"` // WorkflowName deploy controller by workflow WorkflowName string `json:"workflowName"` } // TableName return custom table name -func (a *DeployEvent) TableName() string { +func (a *ApplicationRevision) TableName() string { return tableNamePrefix + "deploy_event" } // PrimaryKey return custom primary key -func (a *DeployEvent) PrimaryKey() string { +func (a *ApplicationRevision) PrimaryKey() string { return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Version) } // Index return custom index -func (a *DeployEvent) Index() map[string]string { +func (a *ApplicationRevision) Index() map[string]string { index := make(map[string]string) if a.Version != "" { index["version"] = a.Version @@ -236,8 +237,8 @@ func (a *DeployEvent) Index() map[string]string { if a.Status != "" { index["status"] = a.Status } - if a.SourceType != "" { - index["sourceType"] = a.SourceType + if a.TriggerType != "" { + index["triggerType"] = a.TriggerType } return index } diff --git a/pkg/apiserver/model/deliverytarget.go b/pkg/apiserver/model/deliverytarget.go new file mode 100644 index 000000000..7608c59aa --- /dev/null +++ b/pkg/apiserver/model/deliverytarget.go @@ -0,0 +1,69 @@ +/* +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 + +func init() { + RegistModel(&DeliveryTarget{}) +} + +// DeliveryTarget defines the delivery target information for the application +// It includes kubernetes clusters or cloud service providers +type DeliveryTarget struct { + Model + Name string `json:"name"` + Namespace string `json:"namespace"` + Alias string `json:"alias,omitempty"` + Description string `json:"description,omitempty"` + Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` + Cloud *CloudTarget `json:"cloud,omitempty"` +} + +// TableName return custom table name +func (d *DeliveryTarget) TableName() string { + return tableNamePrefix + "delivery_target" +} + +// PrimaryKey return custom primary key +func (d *DeliveryTarget) PrimaryKey() string { + return d.Name +} + +// Index return custom index +func (d *DeliveryTarget) Index() map[string]string { + index := make(map[string]string) + if d.Name != "" { + index["name"] = d.Name + } + if d.Namespace != "" { + index["namespace"] = d.Namespace + } + return index +} + +// KubernetesTarget kubernetes delivery target +type KubernetesTarget struct { + ClusterName string `json:"clusterName" validate:"checkname"` + Namespace string `json:"namespace" optional:"true"` +} + +// CloudTarget cloud target +type CloudTarget struct { + TerraformProviderName string `json:"providerName" validate:"required"` + Region string `json:"region" validate:"required"` + Zone string `json:"zone" optional:"true"` + VpcID string `json:"vpcID" optional:"true"` +} diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index bb96fd602..29160d950 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -34,7 +34,6 @@ type Workflow struct { Name string `json:"name"` Alias string `json:"alias"` Description string `json:"description"` - Enable bool `json:"enable"` // Workflow used by the default Default bool `json:"default"` AppPrimaryKey string `json:"appPrimaryKey"` @@ -74,7 +73,6 @@ func (w *Workflow) Index() map[string]string { index["appPrimaryKey"] = w.AppPrimaryKey } index["default"] = strconv.FormatBool(w.Default) - index["enable"] = strconv.FormatBool(w.Enable) return index } diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 656cca717..37aebc221 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -59,6 +59,7 @@ type CreateAddonRegistryRequest struct { Git *model.GitAddonSource `json:"git,omitempty" validate:"required"` } +// UpdateAddonRegistryRequest update addon registry request body type UpdateAddonRegistryRequest struct { Git *model.GitAddonSource `json:"git,omitempty" validate:"required"` } @@ -246,9 +247,9 @@ type ClusterBase struct { // ListApplicatioOptions list application query options type ListApplicatioOptions struct { - Namespace string `json:"namespace"` - Cluster string `json:"cluster"` - Query string `json:"query"` + Namespace string `json:"namespace"` + TargetName string `json:"targetName"` + Query string `json:"query"` } // ListApplicationResponse list applications by query params @@ -256,13 +257,13 @@ type ListApplicationResponse struct { Applications []*ApplicationBase `json:"applications"` } -// EnvBindList env bind list -type EnvBindList []*EnvBind +// EnvBindingList env binding list +type EnvBindingList []*EnvBinding -// ContainCluster contain cluster name -func (e EnvBindList) ContainCluster(name string) bool { +// ContainTarget contain cluster name +func (e EnvBindingList) ContainTarget(name string) bool { for _, eb := range e { - if eb.ClusterSelector.Name == name { + if utils.StringsContain(eb.TargetNames, name) { return true } } @@ -280,7 +281,7 @@ type ApplicationBase struct { Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` Status string `json:"status"` - EnvBind EnvBindList `json:"envBind,omitempty"` + EnvBinding EnvBindingList `json:"envBinding,omitempty"` GatewayRuleList []GatewayRule `json:"gatewayRule"` } @@ -305,14 +306,15 @@ type GatewayRule struct { // CreateApplicationRequest create application request body type CreateApplicationRequest struct { - Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias" optional:"true"` - Namespace string `json:"namespace" validate:"checkname"` - Description string `json:"description" optional:"true"` - Icon string `json:"icon"` - Labels map[string]string `json:"labels,omitempty"` - EnvBind []*EnvBind `json:"envBind,omitempty"` - YamlConfig string `json:"yamlConfig,omitempty"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Namespace string `json:"namespace" validate:"checkname"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels,omitempty"` + EnvBinding []*EnvBinding `json:"envBinding,omitempty"` + YamlConfig string `json:"yamlConfig,omitempty"` + Component *CreateComponentRequest `json:"component"` } // UpdateApplicationRequest update application base config @@ -323,12 +325,12 @@ type UpdateApplicationRequest struct { Labels map[string]string `json:"labels,omitempty"` } -// EnvBind application env bind -type EnvBind struct { +// EnvBinding application env binding +type EnvBinding struct { Name string `json:"name" validate:"checkname"` Alias string `json:"alias" validate:"checkalias" optional:"true"` Description string `json:"description,omitempty" optional:"true"` - ClusterSelector ClusterSelector `json:"clusterSelector"` + TargetNames []string `json:"targetNames"` ComponentSelector *ComponentSelector `json:"componentSelector" optional:"true"` } @@ -395,7 +397,6 @@ type CreateComponentRequest struct { Icon string `json:"icon" optional:"true"` Labels map[string]string `json:"labels,omitempty"` ComponentType string `json:"componentType" validate:"checkname"` - EnvNames []string `json:"envNames,omitempty" optional:"true"` Properties string `json:"properties,omitempty"` DependsOn []string `json:"dependsOn" optional:"true"` Traits []*CreateApplicationTrait `json:"traits,omitempty" optional:"true"` @@ -540,7 +541,6 @@ type CreateWorkflowRequest struct { Alias string `json:"alias" validate:"checkalias" optional:"true"` Description string `json:"description" optional:"true"` Steps []WorkflowStep `json:"steps,omitempty"` - Enable bool `json:"enable"` Default bool `json:"default"` } @@ -600,9 +600,9 @@ type DetailWorkflowRecordResponse struct { WorkflowRecord DeployTime time.Time `json:"deployTime"` DeployUser string `json:"deployUser"` - Commit string `json:"commit"` - // SourceType the event trigger source, Web or API - SourceType string `json:"sourceType"` + Note string `json:"note"` + // TriggerType the event trigger source, Web or API + TriggerType string `json:"triggerType"` } // WorkflowRecord workflow record @@ -619,22 +619,16 @@ type WorkflowRecord struct { type ApplicationDeployRequest struct { WorkflowName string `json:"workflowName"` // User note message, optional - Commit string `json:"commit"` - // SourceType the event trigger source, Web or API - SourceType string `json:"sourceType" validate:"oneof=web api"` + Note string `json:"note"` + // TriggerType the event trigger source, Web or API + TriggerType string `json:"triggerType" validate:"oneof=web api"` // Force set to True to ignore unfinished events. Force bool `json:"force"` } -// ApplicationDeployResponse deploy response +// ApplicationDeployResponse application deploy response body type ApplicationDeployResponse struct { - Version string `json:"version"` - Status string `json:"status"` - Reason string `json:"reason"` - DeployUser string `json:"deployUser"` - Commit string `json:"commit"` - // SourceType the event trigger source, Web or API - SourceType string `json:"sourceType"` + ApplicationRevisionBase } // VelaQLViewResponse query response @@ -645,12 +639,12 @@ type PutApplicationEnvRequest struct { ComponentSelector *ComponentSelector `json:"componentSelector,omitempty"` Alias *string `json:"alias,omitempty" validate:"checkalias" optional:"true"` Description *string `json:"description,omitempty" optional:"true"` - ClusterSelector *ClusterSelector `json:"clusterSelector,omitempty"` + TargetNames *[]string `json:"targetNames"` } // CreateApplicationEnvRequest new application env type CreateApplicationEnvRequest struct { - EnvBind + EnvBinding } // CreateApplicationTrait create application triat req @@ -677,3 +671,46 @@ type ApplicationTrait struct { // Properties json data Properties *model.JSONStruct `json:"properties"` } + +// CreateDeliveryTarget create delivery target request body +type CreateDeliveryTarget struct { + Name string `json:"name" validate:"checkname"` + Namespace string `json:"namespace" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` + Cloud *CloudTarget `json:"cloud,omitempty"` +} + +// UpdateDeliveryTarget only support full quantity update +type UpdateDeliveryTarget struct { + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` + Cloud *CloudTarget `json:"cloud,omitempty"` +} + +// KubernetesTarget kubernetes delivery target +type KubernetesTarget struct { + ClusterName string `json:"clusterName" validate:"checkname"` + Namespace string `json:"namespace" optional:"true"` +} + +// CloudTarget cloud target +type CloudTarget struct { + TerraformProviderName string `json:"providerName" validate:"required"` + Region string `json:"region" validate:"required"` + Zone string `json:"zone" optional:"true"` + VpcID string `json:"vpcID" optional:"true"` +} + +// ApplicationRevisionBase application revision base spec +type ApplicationRevisionBase struct { + Version string `json:"version"` + Status string `json:"status"` + Reason string `json:"reason"` + DeployUser string `json:"deployUser"` + Note string `json:"note"` + // SourceType the event trigger source, Web or API + TriggerType string `json:"triggerType"` +} diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index ef8345bca..deafffe86 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -49,11 +49,11 @@ import ( type PolicyType string const ( - // EnvBindPolicy Multiple environment distribution policy - EnvBindPolicy PolicyType = "env-binding" + // EnvBindingPolicy Multiple environment distribution policy + EnvBindingPolicy PolicyType = "env-binding" - // EnvBindPolicyDefaultName default policy name - EnvBindPolicyDefaultName string = "env-bindings" + // EnvBindingPolicyDefaultName default policy name + EnvBindingPolicyDefaultName string = "env-bindings" ) // ApplicationUsecase application usecase @@ -76,8 +76,8 @@ type ApplicationUsecase interface { DeletePolicy(ctx context.Context, app *model.Application, policyName string) error UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) GetApplicationEnvBindingPolicy(ctx context.Context, app *model.Application) (*v1alpha1.EnvBindingSpec, error) - UpdateApplicationEnvBinding(ctx context.Context, app *model.Application, envName string, diff apisv1.PutApplicationEnvRequest) (*apisv1.EnvBind, error) - CreateApplicationEnvBinding(ctx context.Context, app *model.Application, env apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBind, error) + UpdateApplicationEnvBinding(ctx context.Context, app *model.Application, envName string, diff apisv1.PutApplicationEnvRequest) (*apisv1.EnvBinding, error) + CreateApplicationEnvBinding(ctx context.Context, app *model.Application, env apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) DeleteApplicationEnvBinding(ctx context.Context, app *model.Application, envName string) error CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTrait) (*apisv1.ApplicationTrait, error) DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string) error @@ -124,7 +124,7 @@ func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptio strings.Contains(appBase.Description, listOptions.Query)) { continue } - if listOptions.Cluster != "" && !appBase.EnvBind.ContainCluster(listOptions.Cluster) { + if listOptions.TargetName != "" && !appBase.EnvBinding.ContainTarget(listOptions.TargetName) { continue } list = append(list, appBase) @@ -244,7 +244,6 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis Name: application.Name, Description: "Created automatically.", Steps: steps, - Enable: true, Default: true, }) if err != nil { @@ -254,12 +253,17 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis } // build-in create env binding policy - if len(req.EnvBind) > 0 { - if _, err := c.createApplictionEnvBindingPolicy(ctx, &application, req.EnvBind); err != nil { + if len(req.EnvBinding) > 0 { + if _, err := c.createApplictionEnvBindingPolicy(ctx, &application, req.EnvBinding); err != nil { + return nil, err + } + } + if req.Component != nil { + _, err = c.AddComponent(ctx, &application, *req.Component) + if err != nil { return nil, err } } - // add application to db. if err := c.ds.Add(ctx, &application); err != nil { if errors.Is(err, datastore.ErrRecordExist) { @@ -431,7 +435,7 @@ func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app Type: policy.Type, Properties: properties, } - if policy.Type != string(EnvBindPolicy) { + if policy.Type != string(EnvBindingPolicy) { policyModels = append(policyModels, appPolicy) } else { envbindingPolicy = appPolicy @@ -439,27 +443,21 @@ func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app } // If multiple configurations are configured, enable only the last one. if envbindingPolicy != nil { - envbindingPolicy.Name = EnvBindPolicyDefaultName + envbindingPolicy.Name = EnvBindingPolicyDefaultName policyModels = append(policyModels, envbindingPolicy) var envBindingSpec v1alpha1.EnvBindingSpec if err := json.Unmarshal([]byte(envbindingPolicy.Properties.JSON()), &envBindingSpec); err != nil { return fmt.Errorf("unmarshal env binding policy failure %w", err) } for _, env := range envBindingSpec.Envs { - envBind := &model.EnvBind{ + envBind := &model.EnvBinding{ Name: env.Name, Description: "", } if env.Selector != nil { envBind.ComponentSelector = (*model.ComponentSelector)(env.Selector) } - if env.Placement.ClusterSelector != nil { - envBind.ClusterSelector.Name = env.Placement.ClusterSelector.Name - } - if env.Placement.NamespaceSelector != nil { - envBind.ClusterSelector.Namespace = env.Placement.NamespaceSelector.Name - } - app.EnvBinds = append(app.EnvBinds, envBind) + app.EnvBinding = append(app.EnvBinding, envBind) } } return c.ds.BatchAdd(ctx, policyModels) @@ -483,8 +481,8 @@ func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, ap func (c *applicationUsecaseImpl) GetApplicationEnvBindingPolicy(ctx context.Context, app *model.Application) (*v1alpha1.EnvBindingSpec, error) { var policy = model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), - Type: string(EnvBindPolicy), - Name: EnvBindPolicyDefaultName, + Type: string(EnvBindingPolicy), + Name: EnvBindingPolicyDefaultName, } err := c.ds.Get(ctx, &policy) if err != nil { @@ -501,18 +499,19 @@ func (c *applicationUsecaseImpl) GetApplicationEnvBindingPolicy(ctx context.Cont } // nolint -func (c *applicationUsecaseImpl) createApplictionEnvBindingPolicy(ctx context.Context, app *model.Application, envbinds apisv1.EnvBindList) (*model.ApplicationPolicy, error) { +func (c *applicationUsecaseImpl) createApplictionEnvBindingPolicy(ctx context.Context, app *model.Application, envbinds apisv1.EnvBindingList) (*model.ApplicationPolicy, error) { policy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindPolicyDefaultName, + Name: EnvBindingPolicyDefaultName, Description: "build-in create", - Type: string(EnvBindPolicy), + Type: string(EnvBindingPolicy), Creator: "", } var envBindingSpec v1alpha1.EnvBindingSpec for _, envBind := range envbinds { + // TODO: check delivery target envBindingSpec.Envs = append(envBindingSpec.Envs, createEnvBind(*envBind)) - app.EnvBinds = append(app.EnvBinds, createModelEnvBind(*envBind)) + app.EnvBinding = append(app.EnvBinding, createModelEnvBind(*envBind)) } properties, err := model.NewJSONStructByStruct(envBindingSpec) if err != nil { @@ -556,28 +555,28 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat configByte, _ := yaml.Marshal(oamApp) // step2: check and create deploy event if !req.Force { - var lastEvent = model.DeployEvent{ + var lastVersion = model.ApplicationRevision{ AppPrimaryKey: app.PrimaryKey(), } - list, err := c.ds.List(ctx, &lastEvent, &datastore.ListOptions{PageSize: 1, Page: 1}) + list, err := c.ds.List(ctx, &lastVersion, &datastore.ListOptions{PageSize: 1, Page: 1}) if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { - log.Logger.Errorf("query last app event failure %s", err.Error()) + log.Logger.Errorf("query app latest revision failure %s", err.Error()) return nil, bcode.ErrDeployEventConflict } - if len(list) > 0 && list[0].(*model.DeployEvent).Status != model.DeployEventComplete { + if len(list) > 0 && list[0].(*model.ApplicationRevision).Status != model.DeployEventComplete { return nil, bcode.ErrDeployEventConflict } } - var deployEvent = &model.DeployEvent{ + var deployEvent = &model.ApplicationRevision{ AppPrimaryKey: app.PrimaryKey(), Version: version, ApplyAppConfig: string(configByte), Status: model.DeployEventInit, // TODO: Get user information from ctx and assign a value. DeployUser: "", - Commit: req.Commit, - SourceType: req.SourceType, + Note: req.Note, + TriggerType: req.TriggerType, WorkflowName: oamApp.Annotations[oam.AnnotationWorkflowName], } @@ -611,12 +610,14 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat // step5: update deploy event status return &apisv1.ApplicationDeployResponse{ - Version: deployEvent.Version, - Status: deployEvent.Status, - Reason: deployEvent.Reason, - DeployUser: deployEvent.DeployUser, - Commit: deployEvent.Commit, - SourceType: deployEvent.SourceType, + ApplicationRevisionBase: apisv1.ApplicationRevisionBase{ + Version: deployEvent.Version, + Status: deployEvent.Status, + Reason: deployEvent.Reason, + DeployUser: deployEvent.DeployUser, + Note: deployEvent.Note, + TriggerType: deployEvent.TriggerType, + }, }, nil } @@ -737,17 +738,17 @@ func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *a Icon: app.Icon, Labels: app.Labels, } - for _, envBind := range app.EnvBinds { - apiEnvBind := &apisv1.EnvBind{ - Name: envBind.Name, - Alias: envBind.Alias, - Description: envBind.Description, - ClusterSelector: apisv1.ClusterSelector(envBind.ClusterSelector), + for _, envBind := range app.EnvBinding { + apiEnvBind := &apisv1.EnvBinding{ + Name: envBind.Name, + Alias: envBind.Alias, + Description: envBind.Description, + TargetNames: envBind.TargetNames, } if envBind.ComponentSelector != nil { apiEnvBind.ComponentSelector = (*apisv1.ComponentSelector)(envBind.ComponentSelector) } - appBase.EnvBind = append(appBase.EnvBind, apiEnvBind) + appBase.EnvBinding = append(appBase.EnvBinding, apiEnvBind) } // TODO: get and render app status return appBase @@ -923,7 +924,7 @@ func (c *applicationUsecaseImpl) UpdateApplicationEnvBinding( ctx context.Context, app *model.Application, envName string, - envUpdate apisv1.PutApplicationEnvRequest) (*apisv1.EnvBind, error) { + envUpdate apisv1.PutApplicationEnvRequest) (*apisv1.EnvBinding, error) { // update env-binding policy envBinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) if err != nil { @@ -947,7 +948,7 @@ func (c *applicationUsecaseImpl) UpdateApplicationEnvBinding( } policy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindPolicyDefaultName, + Name: EnvBindingPolicyDefaultName, } if err := c.ds.Get(ctx, policy); err != nil { return nil, err @@ -956,40 +957,37 @@ func (c *applicationUsecaseImpl) UpdateApplicationEnvBinding( if err := c.ds.Put(ctx, policy); err != nil { return nil, err } - var envBind model.EnvBind + var envBind model.EnvBinding // update env-binding base - for i, env := range app.EnvBinds { + for i, env := range app.EnvBinding { if env.Name == envName { if envUpdate.Description != nil { - app.EnvBinds[i].Description = *envUpdate.Description + app.EnvBinding[i].Description = *envUpdate.Description } if envUpdate.Alias != nil { - app.EnvBinds[i].Alias = *envUpdate.Alias + app.EnvBinding[i].Alias = *envUpdate.Alias } - if envUpdate.ClusterSelector != nil { - app.EnvBinds[i].ClusterSelector = model.ClusterSelector{ - Name: envUpdate.ClusterSelector.Name, - Namespace: envUpdate.ClusterSelector.Namespace, - } + if envUpdate.TargetNames != nil { + app.EnvBinding[i].TargetNames = *envUpdate.TargetNames } if envUpdate.ComponentSelector == nil { - app.EnvBinds[i].ComponentSelector = nil + app.EnvBinding[i].ComponentSelector = nil } else { - app.EnvBinds[i].ComponentSelector = &model.ComponentSelector{ + app.EnvBinding[i].ComponentSelector = &model.ComponentSelector{ Components: envUpdate.ComponentSelector.Components, } } - envBind = *app.EnvBinds[i] + envBind = *app.EnvBinding[i] } } if err := c.ds.Put(ctx, app); err != nil { return nil, err } - re := &apisv1.EnvBind{ - Name: envBind.Name, - Alias: envBind.Alias, - Description: envBind.Description, - ClusterSelector: apisv1.ClusterSelector(envBind.ClusterSelector), + re := &apisv1.EnvBinding{ + Name: envBind.Name, + Alias: envBind.Alias, + Description: envBind.Description, + TargetNames: envBind.TargetNames, } if envBind.ComponentSelector != nil { re.ComponentSelector = (*apisv1.ComponentSelector)(envBind.ComponentSelector) @@ -998,8 +996,8 @@ func (c *applicationUsecaseImpl) UpdateApplicationEnvBinding( } // CreateApplicationEnvBinding create application env -func (c *applicationUsecaseImpl) CreateApplicationEnvBinding(ctx context.Context, app *model.Application, envReq apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBind, error) { - for _, env := range app.EnvBinds { +func (c *applicationUsecaseImpl) CreateApplicationEnvBinding(ctx context.Context, app *model.Application, envReq apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) { + for _, env := range app.EnvBinding { if env.Name == envReq.Name { return nil, bcode.ErrApplicationEnvExist } @@ -1011,13 +1009,13 @@ func (c *applicationUsecaseImpl) CreateApplicationEnvBinding(ctx context.Context } } if envBinding == nil { - _, err := c.createApplictionEnvBindingPolicy(ctx, app, []*apisv1.EnvBind{&envReq.EnvBind}) + _, err := c.createApplictionEnvBindingPolicy(ctx, app, []*apisv1.EnvBinding{&envReq.EnvBinding}) if err != nil { return nil, err } } else { - app.EnvBinds = append(app.EnvBinds, createModelEnvBind(envReq.EnvBind)) - envBinding.Envs = append(envBinding.Envs, createEnvBind(envReq.EnvBind)) + app.EnvBinding = append(app.EnvBinding, createModelEnvBind(envReq.EnvBinding)) + envBinding.Envs = append(envBinding.Envs, createEnvBind(envReq.EnvBinding)) properties, err := model.NewJSONStructByStruct(envBinding) if err != nil { log.Logger.Errorf("new env binding properties failure,%s", err.Error()) @@ -1025,7 +1023,7 @@ func (c *applicationUsecaseImpl) CreateApplicationEnvBinding(ctx context.Context } policy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindPolicyDefaultName, + Name: EnvBindingPolicyDefaultName, } if err := c.ds.Get(ctx, policy); err != nil { return nil, err @@ -1038,15 +1036,15 @@ func (c *applicationUsecaseImpl) CreateApplicationEnvBinding(ctx context.Context if err := c.ds.Put(ctx, app); err != nil { return nil, err } - return &envReq.EnvBind, nil + return &envReq.EnvBinding, nil } // DeleteApplicationEnvBinding delete application env binding func (c *applicationUsecaseImpl) DeleteApplicationEnvBinding(ctx context.Context, app *model.Application, envName string) error { - for i, envBind := range app.EnvBinds { + for i, envBind := range app.EnvBinding { if envBind.Name == envName { - app.EnvBinds = append(app.EnvBinds[0:i], app.EnvBinds[i+1:]...) + app.EnvBinding = append(app.EnvBinding[0:i], app.EnvBinding[i+1:]...) } } envBinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) @@ -1065,7 +1063,7 @@ func (c *applicationUsecaseImpl) DeleteApplicationEnvBinding(ctx context.Context } policy := &model.ApplicationPolicy{ AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindPolicyDefaultName, + Name: EnvBindingPolicyDefaultName, } if err := c.ds.Get(ctx, policy); err != nil { return err @@ -1092,17 +1090,8 @@ func (c *applicationUsecaseImpl) UpdateApplicationTrait(ctx context.Context, app return nil, nil } -func createEnvBind(envBind apisv1.EnvBind) v1alpha1.EnvConfig { - placement := v1alpha1.EnvPlacement{ - ClusterSelector: &common.ClusterSelector{ - Name: envBind.ClusterSelector.Name, - }, - } - if envBind.ClusterSelector.Namespace != "" { - placement.NamespaceSelector = &v1alpha1.NamespaceSelector{ - Name: envBind.ClusterSelector.Namespace, - } - } +func createEnvBind(envBind apisv1.EnvBinding) v1alpha1.EnvConfig { + placement := v1alpha1.EnvPlacement{} var componentSelector *v1alpha1.EnvSelector if envBind.ComponentSelector != nil { componentSelector = &v1alpha1.EnvSelector{ @@ -1116,12 +1105,12 @@ func createEnvBind(envBind apisv1.EnvBind) v1alpha1.EnvConfig { } } -func createModelEnvBind(envBind apisv1.EnvBind) *model.EnvBind { - re := model.EnvBind{ - Name: envBind.Name, - Description: envBind.Description, - Alias: envBind.Alias, - ClusterSelector: model.ClusterSelector(envBind.ClusterSelector), +func createModelEnvBind(envBind apisv1.EnvBinding) *model.EnvBinding { + re := model.EnvBinding{ + Name: envBind.Name, + Description: envBind.Description, + Alias: envBind.Alias, + //ClusterSelector: model.ClusterSelector(envBind.ClusterSelector), } if envBind.ComponentSelector != nil { re.ComponentSelector = (*model.ComponentSelector)(envBind.ComponentSelector) diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index db6778ad4..f5d0f3e3b 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -54,20 +54,14 @@ var _ = Describe("Test application usecase function", func() { Name: "test-app", Namespace: "test-app-namespace", Description: "this is a test app", - EnvBind: []*v1.EnvBind{{ + EnvBinding: []*v1.EnvBinding{{ Name: "dev", Description: "dev env", - ClusterSelector: v1.ClusterSelector{ - Name: "dev", - Namespace: "devnamespace", - }, + TargetNames: []string{"dev-target"}, }, { Name: "test", Description: "test env", - ClusterSelector: v1.ClusterSelector{ - Name: "dev", - Namespace: "testnamespace", - }, + TargetNames: []string{"test-target"}, }}, } base, err := appUsecase.CreateApplication(context.TODO(), req) @@ -127,28 +121,23 @@ var _ = Describe("Test application usecase function", func() { Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, - EnvBind: []*v1.EnvBind{ + EnvBinding: []*v1.EnvBinding{ { Name: "dev", Alias: "Chinese Word", Description: "This is a dev env", - ClusterSelector: v1.ClusterSelector{ - Name: "dev-cluster", - }, + TargetNames: []string{"dev-target"}, }, { - Name: "prob", - Description: "This is a prob env", - ClusterSelector: v1.ClusterSelector{ - Name: "prob-cluster", - Namespace: "prob", - }, + Name: "prod", + Description: "This is a prod env", + TargetNames: []string{"prod-target"}, }, }, } appBase, err := appUsecase.CreateApplication(context.TODO(), req) Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(appBase.EnvBind), 2)).Should(BeEmpty()) + Expect(cmp.Diff(len(appBase.EnvBinding), 2)).Should(BeEmpty()) appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") Expect(err).Should(BeNil()) @@ -156,7 +145,7 @@ var _ = Describe("Test application usecase function", func() { }) - It("Test GetApplicationEnvBindingPolicy", func() { + It("Test GetApplicationEnvBindingingPolicy", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") Expect(err).Should(BeNil()) envBinding, err := appUsecase.GetApplicationEnvBindingPolicy(context.TODO(), appModel) @@ -164,7 +153,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(len(envBinding.Envs), 2)).Should(BeEmpty()) }) - It("Test UpdateApplicationEnvBindingDiff", func() { + It("Test UpdateApplicationEnvBindingingDiff", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) _, err = appUsecase.UpdateApplicationEnvBinding(context.TODO(), appModel, "staging", v1.PutApplicationEnvRequest{ @@ -195,9 +184,8 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - detail, err := workflowUsecase.GetWorkflow(context.TODO(), "test-app-sadasd") + _, err = workflowUsecase.GetWorkflow(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) - Expect(cmp.Diff(detail.Enable, true)).Should(BeEmpty()) }) It("Test ListPolicies function", func() { @@ -255,7 +243,7 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, EnvBindPolicyDefaultName) + detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, EnvBindingPolicyDefaultName) Expect(err).Should(BeNil()) Expect(cmp.Diff(detail.Type, "env-binding")).Should(BeEmpty()) Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) @@ -293,7 +281,7 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ - Name: EnvBindPolicyDefaultName, + Name: EnvBindingPolicyDefaultName, Description: "this is a test2 policy", Type: "env-binding", Properties: ``, @@ -359,13 +347,11 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) By("test create first env") env4, err := appUsecase.CreateApplicationEnvBinding(context.TODO(), appModel4, v1.CreateApplicationEnvRequest{ - EnvBind: v1.EnvBind{ + EnvBinding: v1.EnvBinding{ Name: "prod2", Alias: "生产环境", Description: "这是一个用户某客户的生产环境", - ClusterSelector: v1.ClusterSelector{ - Name: "prob", - }, + TargetNames: []string{"prod-target"}, }, }) Expect(err).Should(BeNil()) @@ -373,20 +359,18 @@ var _ = Describe("Test application usecase function", func() { appModelNew, err := appUsecase.GetApplication(context.TODO(), "not-have-env-bind") Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(appModelNew.EnvBinds), 1)).Should(BeEmpty()) + Expect(cmp.Diff(len(appModelNew.EnvBinding), 1)).Should(BeEmpty()) By("test create not first env") appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) env, err := appUsecase.CreateApplicationEnvBinding(context.TODO(), appModel, v1.CreateApplicationEnvRequest{ - EnvBind: v1.EnvBind{ + EnvBinding: v1.EnvBinding{ Name: "prod2", Alias: "生产环境", Description: "这是一个用户某客户的生产环境", - ClusterSelector: v1.ClusterSelector{ - Name: "prob", - }, + TargetNames: []string{"prod-target"}, }, }) Expect(err).Should(BeNil()) @@ -394,14 +378,14 @@ var _ = Describe("Test application usecase function", func() { appModelNew, err = appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(appModelNew.EnvBinds), 4)).Should(BeEmpty()) + Expect(cmp.Diff(len(appModelNew.EnvBinding), 4)).Should(BeEmpty()) spec, err := appUsecase.GetApplicationEnvBindingPolicy(context.TODO(), appModelNew) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(spec.Envs), 4)).Should(BeEmpty()) }) - It("Test CreateApplicationEnvBinding function", func() { + It("Test UpdateApplicationEnvBinding function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -420,7 +404,7 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(len(components), 0)).Should(BeEmpty()) }) - It("Test DeleteApplicationEnvBinding function", func() { + It("Test DeleteApplicationEnvBindinging function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) @@ -433,8 +417,8 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) res, err := appUsecase.Deploy(context.TODO(), appModel, v1.ApplicationDeployRequest{ - Commit: "unit test deploy", - SourceType: "api", + Note: "unit test deploy", + TriggerType: "api", }) Expect(err).Should(BeNil()) Expect(cmp.Diff(res.Status, model.DeployEventRunning)).Should(BeEmpty()) diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index bb7666c07..29310f096 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -32,26 +32,6 @@ label: ReadinessProbe检测 sort: 4 subParameters: - - description: Instructions for assessing container health by executing a command. - Either this attribute or the httpGet attribute or the tcpSocket attribute MUST - be specified. This attribute is mutually exclusive with both the httpGet attribute - and the tcpSocket attribute. - jsonKey: exec - label: Exec - sort: 100 - subParameters: - - description: A command to be executed inside the container to assess its health. - Each space delimited token of the command is a separate array element. Commands - exiting 0 are considered to be successful probes, whilst all other exit codes - are considered failures. - jsonKey: command - label: Command - sort: 100 - uiType: Strings - validate: - required: true - uiType: KV - validate: {} - description: Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe). jsonKey: failureThreshold @@ -69,14 +49,6 @@ label: HttpGet sort: 100 subParameters: - - description: The TCP socket within the container to which the HTTP GET request - should be directed. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - description: "" jsonKey: httpHeaders label: HttpHeaders @@ -106,6 +78,14 @@ uiType: Input validate: required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true uiType: KV validate: {} - description: Number of seconds after the container is started before the first @@ -160,21 +140,37 @@ validate: defaultValue: 1 required: true + - description: Instructions for assessing container health by executing a command. + Either this attribute or the httpGet attribute or the tcpSocket attribute MUST + be specified. This attribute is mutually exclusive with both the httpGet attribute + and the tcpSocket attribute. + jsonKey: exec + label: Exec + sort: 100 + subParameters: + - description: A command to be executed inside the container to assess its health. + Each space delimited token of the command is a separate array element. Commands + exiting 0 are considered to be successful probes, whilst all other exit codes + are considered failures. + jsonKey: command + label: Command + sort: 100 + uiType: Strings + validate: + required: true + uiType: KV + validate: {} uiType: Group validate: {} -- description: Commands to run in the container - jsonKey: cmd - label: Cmd +- description: If addRevisionLabel is true, the appRevision label will be added to + the underlying pods + jsonKey: addRevisionLabel + label: AddRevisionLabel sort: 100 - uiType: Strings - validate: {} -- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` - (1 CPU core) - jsonKey: cpu - label: Cpu - sort: 100 - uiType: CPUNumber - validate: {} + uiType: Switch + validate: + defaultValue: false + required: true - description: Define arguments by using environment variables disable: false jsonKey: env @@ -232,17 +228,31 @@ validate: {} uiType: Structs validate: {} -- description: Specify image pull secrets for your service - jsonKey: imagePullSecrets - label: ImagePullSecrets - sort: 100 - uiType: Strings - validate: {} - description: Instructions for assessing whether the container is alive. jsonKey: livenessProbe label: LivenessProbe sort: 100 subParameters: + - description: Instructions for assessing container health by executing a command. + Either this attribute or the httpGet attribute or the tcpSocket attribute MUST + be specified. This attribute is mutually exclusive with both the httpGet attribute + and the tcpSocket attribute. + jsonKey: exec + label: Exec + sort: 100 + subParameters: + - description: A command to be executed inside the container to assess its health. + Each space delimited token of the command is a separate array element. Commands + exiting 0 are considered to be successful probes, whilst all other exit codes + are considered failures. + jsonKey: command + label: Command + sort: 100 + uiType: Strings + validate: + required: true + uiType: KV + validate: {} - description: Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe). jsonKey: failureThreshold @@ -260,6 +270,14 @@ label: HttpGet sort: 100 subParameters: + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true - description: "" jsonKey: httpHeaders label: HttpHeaders @@ -289,14 +307,6 @@ uiType: Input validate: required: true - - description: The TCP socket within the container to which the HTTP GET request - should be directed. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true uiType: KV validate: {} - description: Number of seconds after the container is started before the first @@ -351,26 +361,6 @@ validate: defaultValue: 1 required: true - - description: Instructions for assessing container health by executing a command. - Either this attribute or the httpGet attribute or the tcpSocket attribute MUST - be specified. This attribute is mutually exclusive with both the httpGet attribute - and the tcpSocket attribute. - jsonKey: exec - label: Exec - sort: 100 - subParameters: - - description: A command to be executed inside the container to assess its health. - Each space delimited token of the command is a separate array element. Commands - exiting 0 are considered to be successful probes, whilst all other exit codes - are considered failures. - jsonKey: command - label: Command - sort: 100 - uiType: Strings - validate: - required: true - uiType: KV - validate: {} uiType: KV validate: {} - description: Declare volumes and volumeMounts @@ -378,6 +368,13 @@ label: Volumes sort: 100 subParameters: + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' jsonKey: type label: Type @@ -401,24 +398,27 @@ uiType: Input validate: required: true - - description: "" - jsonKey: name - label: Name - sort: 100 - uiType: Input - validate: - required: true uiType: Structs validate: {} -- description: If addRevisionLabel is true, the appRevision label will be added to - the underlying pods - jsonKey: addRevisionLabel - label: AddRevisionLabel +- description: Commands to run in the container + jsonKey: cmd + label: Cmd sort: 100 - uiType: Switch - validate: - defaultValue: false - required: true + uiType: Strings + validate: {} +- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` + (1 CPU core) + jsonKey: cpu + label: Cpu + sort: 100 + uiType: CPUNumber + validate: {} +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + sort: 100 + uiType: Strings + validate: {} - description: Which port do you want customer traffic sent to jsonKey: port label: Port diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 7df6b40fa..c411ffff2 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -89,7 +89,6 @@ func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.App var workflow = model.Workflow{ Steps: steps, Name: req.Name, - Enable: req.Enable, Description: req.Description, Default: req.Default, AppPrimaryKey: app.PrimaryKey(), @@ -120,7 +119,6 @@ func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *mode workflow.Description = req.Description // It is allowed to set multiple workflows as default, and only one takes effect. workflow.Default = req.Default - workflow.Enable = req.Enable if err := w.ds.Put(ctx, workflow); err != nil { return nil, err } @@ -147,7 +145,6 @@ func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *mode WorkflowBase: apisv1.WorkflowBase{ Name: workflow.Name, Description: workflow.Description, - Enable: workflow.Enable, Default: workflow.Default, CreateTime: workflow.CreateTime, UpdateTime: workflow.UpdateTime, @@ -172,9 +169,6 @@ func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app * var workflow = model.Workflow{ AppPrimaryKey: app.PrimaryKey(), } - if enable != nil { - workflow.Enable = *enable - } workflows, err := w.ds.List(ctx, &workflow, &datastore.ListOptions{}) if err != nil { return nil, err @@ -185,7 +179,6 @@ func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app * list = append(list, &apisv1.WorkflowBase{ Name: wm.Name, Description: wm.Description, - Enable: wm.Enable, Default: wm.Default, CreateTime: wm.CreateTime, UpdateTime: wm.UpdateTime, @@ -250,21 +243,21 @@ func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflow } version := strings.TrimPrefix(recordName, fmt.Sprintf("%s-", record.AppPrimaryKey)) - var deployEvent = model.DeployEvent{ + var revision = model.ApplicationRevision{ AppPrimaryKey: record.AppPrimaryKey, Version: version, } - err = w.ds.Get(ctx, &deployEvent) + err = w.ds.Get(ctx, &revision) if err != nil { return nil, err } return &apisv1.DetailWorkflowRecordResponse{ WorkflowRecord: *convertFromRecordModel(&record), - DeployTime: deployEvent.CreateTime, - DeployUser: deployEvent.DeployUser, - Commit: deployEvent.Commit, - SourceType: deployEvent.SourceType, + DeployTime: revision.CreateTime, + DeployUser: revision.DeployUser, + Note: revision.Note, + TriggerType: revision.TriggerType, }, nil } diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 37c36a491..eda2d996e 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -108,17 +108,17 @@ var _ = Describe("Test workflow usecase functions", func() { }) Expect(err).Should(BeNil()) - var deployEvent = &model.DeployEvent{ + var deployEvent = &model.ApplicationRevision{ AppPrimaryKey: "test", Version: "123", Status: model.DeployEventInit, DeployUser: "test-user", - Commit: "test-commit", - SourceType: "API", + Note: "test-commit", + TriggerType: "API", WorkflowName: "test-workflow-name", } - err = workflowUsecase.createTestDeployEvent(context.TODO(), deployEvent) + err = workflowUsecase.createTestApplicationRevision(context.TODO(), deployEvent) Expect(err).Should(BeNil()) detail, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-123") @@ -166,7 +166,7 @@ status: suspend: false terminated: false` -func (w *workflowUsecaseImpl) createTestDeployEvent(ctx context.Context, deployEvent *model.DeployEvent) error { +func (w *workflowUsecaseImpl) createTestApplicationRevision(ctx context.Context, deployEvent *model.ApplicationRevision) error { if err := w.ds.Add(ctx, deployEvent); err != nil { return err } diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 7ed62ec16..a2df2ceb1 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -53,8 +53,8 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Doc("list all applications"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). - Param(ws.QueryParameter("namespace", "Namespace-based search").DataType("string")). - Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")). + Param(ws.QueryParameter("namespace", "The namespace of the managed cluster").DataType("string")). + Param(ws.QueryParameter("target", "Name of the application delivery target").DataType("string")). Returns(200, "", apis.ListApplicationResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.ListApplicationResponse{})) @@ -103,9 +103,9 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Param(ws.PathParameter("envName", "identifier of the application ").DataType("string")). Reads(apis.PutApplicationEnvRequest{}). - Returns(200, "", apis.EnvBind{}). + Returns(200, "", apis.EnvBinding{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.EnvBind{})) + Writes(apis.EnvBinding{})) ws.Route(ws.POST("/{name}/envs").To(c.createApplicationEnv). Doc("creating an application environment "). @@ -113,7 +113,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Reads(apis.CreateApplicationEnvRequest{}). - Returns(200, "", apis.EnvBind{}). + Returns(200, "", apis.EnvBinding{}). Returns(400, "", bcode.Bcode{}). Writes(apis.EmptyResponse{})) @@ -283,9 +283,9 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { apps, err := c.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioOptions{ - Namespace: req.QueryParameter("namespace"), - Cluster: req.QueryParameter("cluster"), - Query: req.QueryParameter("query"), + Namespace: req.QueryParameter("namespace"), + TargetName: req.QueryParameter("target"), + Query: req.QueryParameter("query"), }) if err != nil { bcode.ReturnError(req, res, err) diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go index a8a156a18..5aa74d247 100644 --- a/pkg/velaql/providers/query/collector.go +++ b/pkg/velaql/providers/query/collector.go @@ -38,7 +38,6 @@ import ( "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/dispatch" "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" - "github.com/oam-dev/kubevela/pkg/oam/util" oamutil "github.com/oam-dev/kubevela/pkg/oam/util" ) @@ -279,7 +278,7 @@ func standardWorkloadPodCollector(cli client.Client, obj *unstructured.Unstructu pods := make([]*unstructured.Unstructured, len(podList.Items)) for i := range podList.Items { - pod, err := util.Object2Unstructured(podList.Items[i]) + pod, err := oamutil.Object2Unstructured(podList.Items[i]) if err != nil { return nil, err } @@ -326,7 +325,7 @@ func cronJobPodCollector(cli client.Client, obj *unstructured.Unstructured, clus items := make([]*unstructured.Unstructured, len(podList.Items)) for i := range podList.Items { - pod, err := util.Object2Unstructured(podList.Items[i]) + pod, err := oamutil.Object2Unstructured(podList.Items[i]) if err != nil { return nil, err } diff --git a/pkg/velaql/providers/query/handler_test.go b/pkg/velaql/providers/query/handler_test.go index dcc214bf9..270fa920b 100644 --- a/pkg/velaql/providers/query/handler_test.go +++ b/pkg/velaql/providers/query/handler_test.go @@ -462,7 +462,7 @@ var _ = Describe("Test Query Provider", func() { }).GroupVersionKind()) deployJson, err := json.Marshal(unstructuredDeploy) - + Expect(err).Should(BeNil()) opt := fmt.Sprintf(`value: %s cluster: ""`, deployJson) v, err := value.NewValue(opt, nil, "") @@ -541,6 +541,7 @@ cluster: "test"` Expect(h).ShouldNot(BeNil()) Expect(ok).Should(Equal(true)) h, ok = p.GetHandler("query", "searchEvents") + Expect(ok).Should(Equal(true)) Expect(h).ShouldNot(BeNil()) }) }) diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index d7967662f..2648d6b0b 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -42,9 +42,7 @@ var _ = Describe("Test application rest api", func() { Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, - EnvBind: []*apisv1.EnvBind{{Name: "dev-env", ClusterSelector: apisv1.ClusterSelector{ - Name: "dev-cluster", - }}}, + EnvBinding: []*apisv1.EnvBinding{{Name: "dev-env", TargetNames: []string{"test-target"}}}, } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) @@ -61,7 +59,7 @@ var _ = Describe("Test application rest api", func() { Expect(cmp.Diff(appBase.Description, req.Description)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Namespace, req.Namespace)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Labels["test"], req.Labels["test"])).Should(BeEmpty()) - Expect(cmp.Diff(appBase.EnvBind[0].Name, "dev-env")).Should(BeEmpty()) + Expect(cmp.Diff(appBase.EnvBinding[0].Name, "dev-env")).Should(BeEmpty()) }) It("Test delete app", func() { @@ -162,9 +160,9 @@ var _ = Describe("Test application rest api", func() { It("Test deploy application", func() { defer GinkgoRecover() var req = apisv1.ApplicationDeployRequest{ - Commit: "test apply", - SourceType: "web", - Force: false, + Note: "test apply", + TriggerType: "web", + Force: false, } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) From 4eb9cc114e592bab17c678657e4ad76877ed665d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=99=93=E5=85=B5?= <596908030@qq.com> Date: Mon, 15 Nov 2021 10:28:41 +0800 Subject: [PATCH 33/59] Feat: add delivery-target API (#2703) * Feat: add delivery-target API * Fix: for unit test Co-authored-by: zhuxiaobing --- pkg/apiserver/rest/apis/v1/types.go | 31 ++- pkg/apiserver/rest/usecase/delivery_target.go | 162 +++++++++++++++ .../rest/usecase/delivery_target_test.go | 76 +++++++ .../rest/utils/bcode/delivery_target.go | 23 +++ .../rest/webservice/delivery_target.go | 194 ++++++++++++++++++ pkg/apiserver/rest/webservice/webservice.go | 2 + 6 files changed, 485 insertions(+), 3 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/delivery_target.go create mode 100644 pkg/apiserver/rest/usecase/delivery_target_test.go create mode 100644 pkg/apiserver/rest/utils/bcode/delivery_target.go create mode 100644 pkg/apiserver/rest/webservice/delivery_target.go diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 37aebc221..bf07ec3da 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -34,6 +34,8 @@ var ( CtxKeyApplication = "application" // CtxKeyWorkflow request context key of workflow CtxKeyWorkflow = "workflow" + // CtxKeyDeliveryTarget request context key of workflow + CtxKeyDeliveryTarget = "delivery-target" // CtxKeyApplicationEnvBinding request context key of env binding CtxKeyApplicationEnvBinding = "envbinding-policy" ) @@ -672,8 +674,8 @@ type ApplicationTrait struct { Properties *model.JSONStruct `json:"properties"` } -// CreateDeliveryTarget create delivery target request body -type CreateDeliveryTarget struct { +// CreateDeliveryTargetRequest create delivery target request body +type CreateDeliveryTargetRequest struct { Name string `json:"name" validate:"checkname"` Namespace string `json:"namespace" validate:"checkname"` Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` @@ -683,7 +685,7 @@ type CreateDeliveryTarget struct { } // UpdateDeliveryTarget only support full quantity update -type UpdateDeliveryTarget struct { +type UpdateDeliveryTargetRequest struct { Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` Description string `json:"description,omitempty" optional:"true"` Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` @@ -704,6 +706,29 @@ type CloudTarget struct { VpcID string `json:"vpcID" optional:"true"` } +// DetailDeliveryTargetResponse detail deliveryTarget response +type DetailDeliveryTargetResponse struct { + DeliveryTargetBase +} + +// ListDeliveryTargetBaseResponse list application workflows +type ListDeliveryTargetResponse struct { + DeliveryTargets []DeliveryTargetBase `json:"deliveryTargets"` + Total int64 `json:"total"` +} + +// DeliveryTargetBase deliveryTarget base model +type DeliveryTargetBase struct { + Name string `json:"name" validate:"checkname"` + Namespace string `json:"namespace" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` + Cloud *CloudTarget `json:"cloud,omitempty"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + // ApplicationRevisionBase application revision base spec type ApplicationRevisionBase struct { Version string `json:"version"` diff --git a/pkg/apiserver/rest/usecase/delivery_target.go b/pkg/apiserver/rest/usecase/delivery_target.go new file mode 100644 index 000000000..de2db1bb1 --- /dev/null +++ b/pkg/apiserver/rest/usecase/delivery_target.go @@ -0,0 +1,162 @@ +/* +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" + "errors" + + "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" +) + +// DeliveryTargetUsecase deliveryTarget manage api +type DeliveryTargetUsecase interface { + GetDeliveryTarget(ctx context.Context, deliveryTargetName string) (*model.DeliveryTarget, error) + DetailDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget) (*apisv1.DetailDeliveryTargetResponse, error) + DeleteDeliveryTarget(ctx context.Context, deliveryTargetName string) error + CreateDeliveryTarget(ctx context.Context, req apisv1.CreateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) + UpdateDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget, req apisv1.UpdateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) + ListDeliveryTargets(ctx context.Context, page, pageSize int) (*apisv1.ListDeliveryTargetResponse, error) +} + +// NewDeliveryTargetUsecase new DeliveryTarget usecase +func NewDeliveryTargetUsecase(ds datastore.DataStore) DeliveryTargetUsecase { + return &deliveryTargetUsecaseImpl{ds: ds} +} + +type deliveryTargetUsecaseImpl struct { + ds datastore.DataStore +} + +func (dt *deliveryTargetUsecaseImpl) ListDeliveryTargets(ctx context.Context, page, pageSize int) (*apisv1.ListDeliveryTargetResponse, error) { + deliveryTarget := model.DeliveryTarget{} + deliveryTargets, err := dt.ds.List(ctx, &deliveryTarget, &datastore.ListOptions{Page: page, PageSize: pageSize}) + if err != nil { + return nil, err + } + + resp := &apisv1.ListDeliveryTargetResponse{ + DeliveryTargets: []apisv1.DeliveryTargetBase{}, + } + for _, raw := range deliveryTargets { + dt, ok := raw.(*model.DeliveryTarget) + if ok { + resp.DeliveryTargets = append(resp.DeliveryTargets, *convertFromDeliveryTargetModel(dt)) + } + } + count, err := dt.ds.Count(ctx, &deliveryTarget) + if err != nil { + return nil, err + } + resp.Total = count + + return resp, nil +} + +// DeleteDeliveryTarget delete application DeliveryTarget +func (dt *deliveryTargetUsecaseImpl) DeleteDeliveryTarget(ctx context.Context, DeliveryTargetName string) error { + deliveryTarget := &model.DeliveryTarget{ + Name: DeliveryTargetName, + } + if err := dt.ds.Delete(ctx, deliveryTarget); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrDeliveryTargetNotExist + } + return err + } + return nil +} + +func (dt *deliveryTargetUsecaseImpl) CreateDeliveryTarget(ctx context.Context, req apisv1.CreateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) { + deliveryTarget := convertCreateReqToDeliveryTargetModel(req) + // check deliveryTarget name. + exit, err := dt.ds.IsExist(ctx, &deliveryTarget) + if err != nil { + log.Logger.Errorf("check application name is exist failure %s", err.Error()) + return nil, bcode.ErrDeliveryTargetExist + } + if exit { + return nil, bcode.ErrDeliveryTargetExist + } + if err := dt.ds.Add(ctx, &deliveryTarget); err != nil { + return nil, err + } + return dt.DetailDeliveryTarget(ctx, &deliveryTarget) +} + +func (dt *deliveryTargetUsecaseImpl) UpdateDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget, req apisv1.UpdateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) { + deliveryTargetModel := convertUpdateReqToDeliveryTargetModel(deliveryTarget, req) + if err := dt.ds.Put(ctx, deliveryTargetModel); err != nil { + return nil, err + } + return dt.DetailDeliveryTarget(ctx, deliveryTargetModel) +} + +// DetailDeliveryTarget detail DeliveryTarget +func (dt *deliveryTargetUsecaseImpl) DetailDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget) (*apisv1.DetailDeliveryTargetResponse, error) { + return &apisv1.DetailDeliveryTargetResponse{ + DeliveryTargetBase: *convertFromDeliveryTargetModel(deliveryTarget), + }, nil +} + +// GetDeliveryTarget get DeliveryTarget model +func (dt *deliveryTargetUsecaseImpl) GetDeliveryTarget(ctx context.Context, deliveryTargetName string) (*model.DeliveryTarget, error) { + deliveryTarget := &model.DeliveryTarget{ + Name: deliveryTargetName, + } + if err := dt.ds.Get(ctx, deliveryTarget); err != nil { + return nil, err + } + return deliveryTarget, nil +} + +func convertUpdateReqToDeliveryTargetModel(deliveryTarget *model.DeliveryTarget, req apisv1.UpdateDeliveryTargetRequest) *model.DeliveryTarget { + deliveryTarget.Alias = req.Alias + deliveryTarget.Description = req.Description + deliveryTarget.Kubernetes = (*model.KubernetesTarget)(req.Kubernetes) + deliveryTarget.Cloud = (*model.CloudTarget)(req.Cloud) + return deliveryTarget +} + +func convertCreateReqToDeliveryTargetModel(req apisv1.CreateDeliveryTargetRequest) model.DeliveryTarget { + deliveryTarget := model.DeliveryTarget{ + Name: req.Name, + Namespace: req.Namespace, + Alias: req.Alias, + Description: req.Description, + Kubernetes: (*model.KubernetesTarget)(req.Kubernetes), + Cloud: (*model.CloudTarget)(req.Cloud), + } + return deliveryTarget +} + +func convertFromDeliveryTargetModel(deliveryTarget *model.DeliveryTarget) *apisv1.DeliveryTargetBase { + return &apisv1.DeliveryTargetBase{ + Name: deliveryTarget.Name, + Namespace: deliveryTarget.Namespace, + Alias: deliveryTarget.Alias, + Description: deliveryTarget.Description, + Kubernetes: (*apisv1.KubernetesTarget)(deliveryTarget.Kubernetes), + Cloud: (*apisv1.CloudTarget)(deliveryTarget.Cloud), + CreateTime: deliveryTarget.CreateTime, + UpdateTime: deliveryTarget.UpdateTime, + } +} diff --git a/pkg/apiserver/rest/usecase/delivery_target_test.go b/pkg/apiserver/rest/usecase/delivery_target_test.go new file mode 100644 index 000000000..f42c60d64 --- /dev/null +++ b/pkg/apiserver/rest/usecase/delivery_target_test.go @@ -0,0 +1,76 @@ +/* +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" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +var _ = Describe("Test delivery target usecase functions", func() { + var ( + deliveryTargetUsecase *deliveryTargetUsecaseImpl + ) + BeforeEach(func() { + deliveryTargetUsecase = &deliveryTargetUsecaseImpl{ds: ds} + }) + It("Test CreateDeliveryTarget function", func() { + req := apisv1.CreateDeliveryTargetRequest{ + Name: "test-delivery-target", + Namespace: "test-namespace", + Alias: "test-alias", + Description: "this is a deliveryTarget", + Kubernetes: &apisv1.KubernetesTarget{ClusterName: "cluster-dev", Namespace: "dev"}, + Cloud: &apisv1.CloudTarget{TerraformProviderName: "provider", Region: "us-1"}, + } + base, err := deliveryTargetUsecase.CreateDeliveryTarget(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + }) + + It("Test GetDeliveryTarget function", func() { + deliveryTarget, err := deliveryTargetUsecase.GetDeliveryTarget(context.TODO(), "test-delivery-target") + Expect(err).Should(BeNil()) + Expect(deliveryTarget).ShouldNot(BeNil()) + Expect(cmp.Diff(deliveryTarget.Name, "test-delivery-target")).Should(BeEmpty()) + }) + + It("Test ListDeliveryTargets function", func() { + _, err := deliveryTargetUsecase.ListDeliveryTargets(context.TODO(), 1, 1) + Expect(err).Should(BeNil()) + }) + + It("Test DetailDeliveryTarget function", func() { + detail, err := deliveryTargetUsecase.DetailDeliveryTarget(context.TODO(), + &model.DeliveryTarget{ + Name: "test-delivery-target", + Namespace: "test-namespace", + Alias: "test-alias", + Description: "this is a deliveryTarget", + Kubernetes: &model.KubernetesTarget{ClusterName: "cluster-dev", Namespace: "dev"}, + Cloud: &model.CloudTarget{TerraformProviderName: "provider", Region: "us-1"}}) + Expect(err).Should(BeNil()) + Expect(detail.Name).Should(Equal("test-delivery-target")) + }) +}) diff --git a/pkg/apiserver/rest/utils/bcode/delivery_target.go b/pkg/apiserver/rest/utils/bcode/delivery_target.go new file mode 100644 index 000000000..32c019068 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/delivery_target.go @@ -0,0 +1,23 @@ +/* +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 + +// ErrDeliveryTargetExist deliveryTarget is exist +var ErrDeliveryTargetExist = NewBcode(400, 20006, "deliveryTarget is exist") + +// ErrDeliveryTargetNotExist deliveryTarget is not exist +var ErrDeliveryTargetNotExist = NewBcode(404, 20007, "deliveryTarget is not exist") diff --git a/pkg/apiserver/rest/webservice/delivery_target.go b/pkg/apiserver/rest/webservice/delivery_target.go new file mode 100644 index 000000000..c7b21d18b --- /dev/null +++ b/pkg/apiserver/rest/webservice/delivery_target.go @@ -0,0 +1,194 @@ +/* +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 ( + "context" + + restfulspec "github.com/emicklei/go-restful-openapi/v2" + restful "github.com/emicklei/go-restful/v3" + + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + 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" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +// NewDeliveryTargetWebService new deliveryTarget webservice +func NewDeliveryTargetWebService(deliveryTargetUsecase usecase.DeliveryTargetUsecase) WebService { + return &DeliveryTargetWebService{ + deliveryTargetUsecase: deliveryTargetUsecase, + } +} + +type DeliveryTargetWebService struct { + deliveryTargetUsecase usecase.DeliveryTargetUsecase +} + +func (dt *DeliveryTargetWebService) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/deliveryTargets"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for deliveryTarget manage") + + tags := []string{"deliveryTarget"} + + ws.Route(ws.GET("/").To(dt.listDeliveryTargets). + Doc("list deliveryTarget"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListDeliveryTargetResponse{}). + Writes(apis.ListDeliveryTargetResponse{}).Do(returns200, returns500)) + + ws.Route(ws.POST("/").To(dt.createDeliveryTarget). + Doc("create deliveryTarget"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateDeliveryTargetRequest{}). + Returns(200, "create success", apis.DetailDeliveryTargetResponse{}). + Returns(400, "create failure", bcode.Bcode{}). + Writes(apis.DetailDeliveryTargetResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}").To(dt.detailDeliveryTarget). + Doc("detail deliveryTarget"). + Param(ws.PathParameter("name", "identifier of the deliveryTarget.").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(dt.deliveryTargetCheckFilter). + Returns(200, "create success", apis.DetailDeliveryTargetResponse{}). + Writes(apis.DetailDeliveryTargetResponse{}).Do(returns200, returns500)) + + ws.Route(ws.PUT("/{name}").To(dt.updateDeliveryTarget). + Doc("update application DeliveryTarget config"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(dt.deliveryTargetCheckFilter). + Param(ws.PathParameter("name", "identifier of the deliveryTarget").DataType("string")). + Reads(apis.UpdateDeliveryTargetRequest{}). + Returns(200, "", apis.DetailDeliveryTargetResponse{}). + Writes(apis.DetailDeliveryTargetResponse{}).Do(returns200, returns500)) + + ws.Route(ws.DELETE("/{name}").To(dt.deleteDeliveryTarget). + Doc("deletet DeliveryTarget"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(dt.deliveryTargetCheckFilter). + Param(ws.PathParameter("name", "identifier of the deliveryTarget").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{}).Do(returns200, returns500)) + + return ws +} + +func (dt *DeliveryTargetWebService) createDeliveryTarget(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateDeliveryTargetRequest + 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 + deliveryTargetDetail, err := dt.deliveryTargetUsecase.CreateDeliveryTarget(req.Request.Context(), createReq) + if err != nil { + log.Logger.Errorf("create delivery-target failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + // Write back response data + if err := res.WriteEntity(deliveryTargetDetail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (dt *DeliveryTargetWebService) deliveryTargetCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + deliveryTarget, err := dt.deliveryTargetUsecase.GetDeliveryTarget(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyDeliveryTarget, deliveryTarget)) + chain.ProcessFilter(req, res) +} + +func (dt *DeliveryTargetWebService) detailDeliveryTarget(req *restful.Request, res *restful.Response) { + deliveryTarget := req.Request.Context().Value(&apis.CtxKeyDeliveryTarget).(*model.DeliveryTarget) + detail, err := dt.deliveryTargetUsecase.DetailDeliveryTarget(req.Request.Context(), deliveryTarget) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (dt *DeliveryTargetWebService) updateDeliveryTarget(req *restful.Request, res *restful.Response) { + deliveryTarget := req.Request.Context().Value(&apis.CtxKeyDeliveryTarget).(*model.DeliveryTarget) + // Verify the validity of parameters + var updateReq apis.UpdateDeliveryTargetRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + detail, err := dt.deliveryTargetUsecase.UpdateDeliveryTarget(req.Request.Context(), deliveryTarget, updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (dt *DeliveryTargetWebService) deleteDeliveryTarget(req *restful.Request, res *restful.Response) { + if err := dt.deliveryTargetUsecase.DeleteDeliveryTarget(req.Request.Context(), req.PathParameter("name")); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (dt *DeliveryTargetWebService) listDeliveryTargets(req *restful.Request, res *restful.Response) { + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + deliveryTargets, err := dt.deliveryTargetUsecase.ListDeliveryTargets(req.Request.Context(), page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(deliveryTargets); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 6634610d4..8463ca307 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -66,6 +66,7 @@ func Init(ds datastore.DataStore) { velaQLUsecase := usecase.NewVelaQLUsecase() definitionUsecase := usecase.NewDefinitionUsecase() addonUsecase := usecase.NewAddonUsecase(ds) + deliveryTargetUsecase := usecase.NewDeliveryTargetUsecase(ds) RegistWebService(NewClusterWebService(clusterUsecase)) RegistWebService(NewApplicationWebService(applicationUsecase)) RegistWebService(NewNamespaceWebService(namespaceUsecase)) @@ -75,5 +76,6 @@ func Init(ds datastore.DataStore) { RegistWebService(NewOAMApplication(oamApplicationUsecase)) RegistWebService(&policyDefinitionWebservice{}) RegistWebService(NewWorkflowWebService(workflowUsecase, applicationUsecase)) + RegistWebService(NewDeliveryTargetWebService(deliveryTargetUsecase)) RegistWebService(NewVelaQLWebService(velaQLUsecase)) } From 27490c4bce2388f6ad0e49a9d8acbcca32f6e8d9 Mon Sep 17 00:00:00 2001 From: qiaozp <47812250+chivalryq@users.noreply.github.com> Date: Mon, 15 Nov 2021 12:16:32 +0800 Subject: [PATCH 34/59] Refactor: refactor addon for later reusing code in CLI (#2708) * refactor addon for later reuse code in CLI * fix import --- apis/types/capability.go | 54 ++- pkg/addon/addon.go | 450 ++++++++++++++++++++ pkg/addon/error.go | 28 ++ pkg/apiserver/model/addon.go | 11 +- pkg/apiserver/rest/apis/v1/types.go | 52 +-- pkg/apiserver/rest/usecase/addon.go | 507 +++-------------------- pkg/apiserver/rest/utils/bcode/addon.go | 18 +- pkg/apiserver/rest/webservice/addon.go | 4 +- pkg/velaql/providers/query/suite_test.go | 3 +- test/e2e-apiserver-test/addon_test.go | 5 +- 10 files changed, 617 insertions(+), 515 deletions(-) create mode 100644 pkg/addon/addon.go create mode 100644 pkg/addon/error.go diff --git a/apis/types/capability.go b/apis/types/capability.go index a2c96650c..79dd77610 100644 --- a/apis/types/capability.go +++ b/apis/types/capability.go @@ -19,12 +19,14 @@ package types import ( "encoding/json" + "cuelang.org/go/cue" + "github.com/getkin/kin-openapi/openapi3" + "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/runtime" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - - "cuelang.org/go/cue" - "github.com/spf13/pflag" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" ) // Source record the source of Capability @@ -185,3 +187,49 @@ type Capability struct { KubeTemplate runtime.RawExtension `json:"kubetemplate,omitempty"` KubeParameter []common.KubeParameter `json:"kubeparameter,omitempty"` } + +// Addon contains all information represent an addon +type Addon struct { + AddonMeta + + APISchema *openapi3.Schema `json:"schema"` + UISchema []*utils.UIParameter `json:"uiSchema"` + + // More details about the addon, e.g. README + Detail string `json:"detail,omitempty"` + Definitions []AddonElementFile `json:"definitions"` + Parameters string `json:"parameters"` + CUETemplates []AddonElementFile `json:"cue_templates"` + YAMLTemplates []AddonElementFile `json:"yaml_templates,omitempty"` + AppTemplate *v1beta1.Application `json:"app_template"` +} + +// AddonMeta defines the format for a single addon +type AddonMeta struct { + Name string `json:"name" validate:"required"` + Version string `json:"version"` + Description string `json:"description"` + Icon string `json:"icon"` + URL string `json:"url,omitempty"` + Tags []string `json:"tags,omitempty"` + DeployTo *AddonDeployTo `json:"deploy_to,omitempty"` + Dependencies []*AddonDependency `json:"dependencies,omitempty"` +} + +// AddonDeployTo defines where the addon to deploy to +type AddonDeployTo struct { + ControlPlane bool `json:"control_plane"` + RuntimeCluster bool `json:"runtime_cluster"` +} + +// AddonDependency defines the other addons it depends on +type AddonDependency struct { + Name string `json:"name,omitempty"` +} + +// AddonElementFile can be addon's definition or addon's component +type AddonElementFile struct { + Data string + Name string + Path []string +} diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go new file mode 100644 index 000000000..1fca68d94 --- /dev/null +++ b/pkg/addon/addon.go @@ -0,0 +1,450 @@ +package addon + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "path" + "path/filepath" + "strings" + "time" + + "sigs.k8s.io/yaml" + + "cuelang.org/go/cue" + cueyaml "cuelang.org/go/encoding/yaml" + "github.com/getkin/kin-openapi/openapi3" + "github.com/google/go-github/v32/github" + "github.com/pkg/errors" + "golang.org/x/oauth2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + + common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + utils2 "github.com/oam-dev/kubevela/pkg/controller/utils" + cuemodel "github.com/oam-dev/kubevela/pkg/cue/model" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils" + addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +const ( + // ReadmeFileName is the addon readme file name + ReadmeFileName string = "readme.md" + + // MetadataFileName is the addon meatadata.yaml file name + MetadataFileName string = "metadata.yaml" + + // TemplateFileName is the addon template.yaml dir name + TemplateFileName string = "template.yaml" + + // ResourcesDirName is the addon resources/ dir name + ResourcesDirName string = "resources" + + // DefinitionsDirName is the addon definitions/ dir name + DefinitionsDirName string = "definitions" +) + +type gitHelper struct { + Client *github.Client + Meta *utils.Content +} + +// GitAddonSource defines the information about the Git as addon source +type GitAddonSource struct { + URL string `json:"url,omitempty" validate:"required"` + Path string `json:"path,omitempty"` + Token string `json:"token,omitempty"` +} + +// GetAddon get a detailed addon info from GitAddonSource +func GetAddon(name string, git *GitAddonSource) (*types.Addon, error) { + addons, err := ListAddons(true, git) + if err != nil { + return nil, err + } + + for _, addon := range addons { + if addon.Name == name { + return addon, nil + } + } + return nil, errors.New("addon not exist") +} + +// ListAddons list addons' info from GitAddonSource, if not detailed, result only contains types.AddonMeta +func ListAddons(detailed bool, git *GitAddonSource) ([]*types.Addon, error) { + var gitAddons []*types.Addon + gitAddons, err := getAddonsFromGit(git.URL, git.Path, git.Token, detailed) + if err != nil { + return nil, err + } + return gitAddons, nil +} + +func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*types.Addon, error) { + var addons []*types.Addon + + gith, err := createGitHelper(baseURL, dir, token) + if err != nil { + return nil, err + } + _, items, err := gith.readRepo(gith.Meta.Path) + if err != nil { + return nil, err + } + + for _, subItems := range items { + if subItems.GetType() != "dir" { + continue + } + addonRes := &types.Addon{} + _, files, err := gith.readRepo(subItems.GetPath()) + if err != nil { + return nil, err + } + for _, file := range files { + var err error + + switch strings.ToLower(file.GetName()) { + case ReadmeFileName: + if !detailed { + break + } + err = readReadme(addonRes, gith, file) + case MetadataFileName: + err = readMetadata(addonRes, gith, file) + addonRes.Name = addonutil.TransAddonName(addonRes.Name) + case DefinitionsDirName: + if !detailed { + break + } + err = readDefinitions(addonRes, gith, file) + case ResourcesDirName: + if !detailed { + break + } + err = readResources(addonRes, gith, file) + case TemplateFileName: + if !detailed { + break + } + err = readTemplate(addonRes, gith, file) + } + + if err != nil { + return nil, err + } + } + + if detailed && addonRes.Parameters != "" { + err = genAddonAPISchema(addonRes) + if err != nil { + continue + } + } + addons = append(addons, addonRes) + } + return addons, nil +} + +func readTemplate(addon *types.Addon, h *gitHelper, file *github.RepositoryContent) error { + content, _, err := h.readRepo(*file.Path) + if err != nil { + return err + } + data, err := content.GetContent() + if err != nil { + return err + } + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + addon.AppTemplate = &v1beta1.Application{} + _, _, err = dec.Decode([]byte(data), nil, addon.AppTemplate) + if err != nil { + return err + } + return nil +} + +func readResources(addon *types.Addon, h *gitHelper, dir *github.RepositoryContent) error { + dirPath := strings.Split(dir.GetPath(), "/") + dirPath, err := cutPathUntil(dirPath, ResourcesDirName) + if err != nil { + return err + } + + _, files, err := h.readRepo(*dir.Path) + if err != nil { + return err + } + for _, file := range files { + switch file.GetType() { + case "file": + content, _, err := h.readRepo(*file.Path) + if err != nil { + return err + } + b, err := content.GetContent() + if err != nil { + return err + } + + if file.GetName() == "parameter.cue" { + addon.Parameters = b + break + } + switch filepath.Ext(file.GetName()) { + case ".cue": + addon.CUETemplates = append(addon.CUETemplates, types.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) + default: + addon.YAMLTemplates = append(addon.YAMLTemplates, types.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) + } + case "dir": + err = readResources(addon, h, file) + if err != nil { + return err + } + } + } + return nil +} + +func readDefinitions(addon *types.Addon, h *gitHelper, dir *github.RepositoryContent) error { + dirPath := strings.Split(dir.GetPath(), "/") + dirPath, err := cutPathUntil(dirPath, DefinitionsDirName) + if err != nil { + return err + } + _, files, err := h.readRepo(*dir.Path) + if err != nil { + return err + } + for _, file := range files { + switch file.GetType() { + case "file": + content, _, err := h.readRepo(*file.Path) + if err != nil { + return err + } + b, err := content.GetContent() + if err != nil { + return err + } + addon.Definitions = append(addon.Definitions, types.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) + case "dir": + err = readDefinitions(addon, h, file) + if err != nil { + return err + } + } + } + return nil +} + +func readMetadata(addon *types.Addon, h *gitHelper, file *github.RepositoryContent) error { + content, _, err := h.readRepo(*file.Path) + if err != nil { + return err + } + b, err := content.GetContent() + if err != nil { + return err + } + return yaml.Unmarshal([]byte(b), &addon.AddonMeta) +} + +func readReadme(addon *types.Addon, h *gitHelper, file *github.RepositoryContent) error { + content, _, err := h.readRepo(*file.Path) + if err != nil { + return err + } + addon.Detail, err = content.GetContent() + return err +} + +func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { + var ts oauth2.TokenSource + if token != "" { + ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + } + tc := oauth2.NewClient(context.Background(), ts) + tc.Timeout = time.Second * 10 + cli := github.NewClient(tc) + + baseURL = strings.TrimSuffix(baseURL, ".git") + u, err := url.Parse(baseURL) + if err != nil { + return nil, errors.New("addon registry invalid") + } + u.Path = path.Join(u.Path, dir) + _, gitmeta, err := utils.Parse(u.String()) + if err != nil { + return nil, errors.New("addon registry invalid") + } + + return &gitHelper{ + Client: cli, + Meta: gitmeta, + }, nil +} + +func (h *gitHelper) readRepo(path string) (*github.RepositoryContent, []*github.RepositoryContent, error) { + file, items, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, path, nil) + if err != nil { + return nil, nil, WrapErrRateLimit(err) + } + return file, items, nil +} + +func genAddonAPISchema(addonRes *types.Addon) error { + param, err := utils2.PrepareParameterCue(addonRes.Name, addonRes.Parameters) + if err != nil { + return err + } + var r cue.Runtime + cueInst, err := r.Compile("-", param) + if err != nil { + return err + } + data, err := common.GenOpenAPI(cueInst) + if err != nil { + return err + } + schema := &openapi3.Schema{} + if err := schema.UnmarshalJSON(data); err != nil { + return err + } + addonRes.APISchema = schema + return nil +} + +func cutPathUntil(path []string, end string) ([]string, error) { + for i, d := range path { + if d == end { + return path[i:], nil + } + } + return nil, errors.New("cut path fail, target directory name not found") +} + +// RenderApplication render a K8s application +func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.Application, error) { + if args == nil { + args = map[string]string{} + } + app := addon.AppTemplate + if app == nil { + app = &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, + ObjectMeta: metav1.ObjectMeta{ + Name: Convert2AppName(addon.Name), + Namespace: types.DefaultKubeVelaNS, + Labels: map[string]string{ + oam.LabelAddonName: addon.Name, + }, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common2.ApplicationComponent{}, + }, + } + } + app.Name = Convert2AppName(addon.Name) + app.Labels = util.MergeMapOverrideWithDst(app.Labels, map[string]string{oam.LabelAddonName: addon.Name}) + + for _, tmpl := range addon.YAMLTemplates { + comp, err := renderRawComponent(tmpl) + if err != nil { + return nil, err + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + for _, tmpl := range addon.CUETemplates { + comp, err := renderCUETemplate(tmpl, addon.Parameters, args) + if err != nil { + return nil, ErrRenderCueTmpl + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + for _, def := range addon.Definitions { + comp, err := renderRawComponent(def) + if err != nil { + return nil, err + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + + return app, nil +} + +// renderRawComponent will return a component in raw type from string +func renderRawComponent(elem types.AddonElementFile) (*common2.ApplicationComponent, error) { + baseRawComponent := common2.ApplicationComponent{ + Type: "raw", + Name: strings.Join(append(elem.Path, elem.Name), "-"), + } + obj := &unstructured.Unstructured{} + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + _, _, err := dec.Decode([]byte(elem.Data), nil, obj) + if err != nil { + return nil, err + } + baseRawComponent.Properties = util.Object2RawExtension(obj) + return &baseRawComponent, nil +} + +// renderCUETemplate will return a component from cue template +func renderCUETemplate(elem types.AddonElementFile, parameters string, args map[string]string) (*common2.ApplicationComponent, error) { + bt, err := json.Marshal(args) + if err != nil { + return nil, err + } + var paramFile = cuemodel.ParameterFieldName + ": {}" + if string(bt) != "null" { + paramFile = fmt.Sprintf("%s: %s", cuemodel.ParameterFieldName, string(bt)) + } + param := fmt.Sprintf("%s\n%s", paramFile, parameters) + v, err := value.NewValue(param, nil, "") + if err != nil { + return nil, err + } + out, err := v.LookupByScript(fmt.Sprintf("{%s}", elem.Data)) + if err != nil { + return nil, err + } + compContent, err := out.LookupValue("output") + if err != nil { + return nil, err + } + b, err := cueyaml.Encode(compContent.CueValue()) + if err != nil { + return nil, err + } + comp := common2.ApplicationComponent{ + Name: strings.Join(append(elem.Path, elem.Name), "-"), + } + err = yaml.Unmarshal(b, &comp) + if err != nil { + return nil, err + } + + return &comp, err +} + +const addonAppPrefix = "addon-" + +// Convert2AppName - +func Convert2AppName(name string) string { + return addonAppPrefix + name +} + +// Convert2AddonName - +func Convert2AddonName(name string) string { + return strings.TrimPrefix(name, addonAppPrefix) +} diff --git a/pkg/addon/error.go b/pkg/addon/error.go new file mode 100644 index 000000000..df98357f5 --- /dev/null +++ b/pkg/addon/error.go @@ -0,0 +1,28 @@ +package addon + +import ( + "github.com/google/go-github/v32/github" + "github.com/pkg/errors" +) + +// NewAddonError will return an +func NewAddonError(msg string) error { + return errors.New(msg) +} + +var ( + // ErrRenderCueTmpl is error when render addon's cue file + ErrRenderCueTmpl = NewAddonError("fail to render cue tmpl") + + // ErrRateLimit means exceed github access rate limit + ErrRateLimit = NewAddonError("exceed github access rate limit") +) + +// WrapErrRateLimit return ErrRateLimit if is the situation, or return error directly +func WrapErrRateLimit(err error) error { + _, ok := err.(*github.RateLimitError) + if ok { + return ErrRateLimit + } + return err +} diff --git a/pkg/apiserver/model/addon.go b/pkg/apiserver/model/addon.go index 0088a858c..d9f8e86ab 100644 --- a/pkg/apiserver/model/addon.go +++ b/pkg/apiserver/model/addon.go @@ -16,19 +16,14 @@ limitations under the License. package model +import "github.com/oam-dev/kubevela/pkg/addon" + // AddonRegistry defines the data model of a AddonRegistry type AddonRegistry struct { Model Name string `json:"name"` - Git *GitAddonSource `json:"git,omitempty"` -} - -// GitAddonSource defines the information about the Git as addon source -type GitAddonSource struct { - URL string `json:"url,omitempty" validate:"required"` - Path string `json:"path,omitempty"` - Token string `json:"token,omitempty"` + Git *addon.GitAddonSource `json:"git,omitempty"` } // TableName return custom table name diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index bf07ec3da..84b551e91 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -19,10 +19,11 @@ package v1 import ( "time" + "github.com/oam-dev/kubevela/pkg/addon" + "github.com/getkin/kin-openapi/openapi3" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/model" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" @@ -58,18 +59,18 @@ type EmptyResponse struct{} // CreateAddonRegistryRequest defines the format for addon registry create request type CreateAddonRegistryRequest struct { Name string `json:"name" validate:"checkname"` - Git *model.GitAddonSource `json:"git,omitempty" validate:"required"` + Git *addon.GitAddonSource `json:"git,omitempty" validate:"required"` } -// UpdateAddonRegistryRequest update addon registry request body +// UpdateAddonRegistryRequest defines the format for addon registry update request type UpdateAddonRegistryRequest struct { - Git *model.GitAddonSource `json:"git,omitempty" validate:"required"` + Git *addon.GitAddonSource `json:"git,omitempty" validate:"required"` } // AddonRegistryMeta defines the format for a single addon registry type AddonRegistryMeta struct { Name string `json:"name" validate:"required"` - Git *model.GitAddonSource `json:"git,omitempty"` + Git *addon.GitAddonSource `json:"git,omitempty"` } // ListAddonRegistryResponse list addon registry @@ -85,53 +86,18 @@ type EnableAddonRequest struct { // ListAddonResponse defines the format for addon list response type ListAddonResponse struct { - Addons []*AddonMeta `json:"addons"` -} - -// AddonDeployTo defines where the addon to deploy to -type AddonDeployTo struct { - ControlPlane bool `json:"control_plane"` - RuntimeCluster bool `json:"runtime_cluster"` -} - -// AddonDependency defines the other addons it depends on -type AddonDependency struct { - Name string `json:"name,omitempty"` -} - -// AddonMeta defines the format for a single addon -type AddonMeta struct { - Name string `json:"name" validate:"required"` - Version string `json:"version"` - Description string `json:"description"` - Icon string `json:"icon"` - URL string `json:"url,omitempty"` - Tags []string `json:"tags,omitempty"` - DeployTo *AddonDeployTo `json:"deploy_to,omitempty"` - Dependencies []*AddonDependency `json:"dependencies,omitempty"` -} - -// AddonElementFile can be addon's definition or addon's component -type AddonElementFile struct { - Data string - Name string - Path []string + Addons []*types.AddonMeta `json:"addons"` } // DetailAddonResponse defines the format for showing the addon details type DetailAddonResponse struct { - AddonMeta + types.AddonMeta APISchema *openapi3.Schema `json:"schema"` UISchema []*utils.UIParameter `json:"uiSchema"` // More details about the addon, e.g. README - Detail string `json:"detail,omitempty"` - Definitions []AddonElementFile `json:"definitions"` - Parameters string `json:"parameters"` - CUETemplates []AddonElementFile `json:"cue_templates"` - YAMLTemplates []AddonElementFile `json:"yaml_templates,omitempty"` - AppTemplate *v1beta1.Application `json:"app_template"` + Detail string `json:"detail,omitempty"` } // AddonStatusResponse defines the format of addon status response diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 0487f9706..f6abcd2b9 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -2,35 +2,19 @@ package usecase import ( "context" - "encoding/json" "errors" - "fmt" - "net/url" - "path" - "path/filepath" "sort" "strings" "time" - "cuelang.org/go/cue" - "github.com/getkin/kin-openapi/openapi3" - - utils2 "github.com/oam-dev/kubevela/pkg/controller/utils" - "github.com/oam-dev/kubevela/pkg/utils/common" - - cueyaml "cuelang.org/go/encoding/yaml" - "github.com/google/go-github/v32/github" - "golang.org/x/oauth2" errors2 "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" + pkgaddon "github.com/oam-dev/kubevela/pkg/addon" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" @@ -38,32 +22,9 @@ import ( apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" restutils "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" - cuemodel "github.com/oam-dev/kubevela/pkg/cue/model" - "github.com/oam-dev/kubevela/pkg/cue/model/value" - "github.com/oam-dev/kubevela/pkg/oam" - "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/pkg/utils" - addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/apply" ) -const ( - // AddonReadmeFileName is the addon readme file name - AddonReadmeFileName string = "readme.md" - - // AddonMetadataFileName is the addon meatadata.yaml file name - AddonMetadataFileName string = "metadata.yaml" - - // AddonTemplateFileName is the addon template.yaml dir name - AddonTemplateFileName string = "template.yaml" - - // AddonResourcesDirName is the addon resources/ dir name - AddonResourcesDirName string = "resources" - - // AddonDefinitionsDirName is the addon definitions/ dir name - AddonDefinitionsDirName string = "definitions" -) - // AddonUsecase addon usecase type AddonUsecase interface { GetAddonRegistry(ctx context.Context, name string) (*model.AddonRegistry, error) @@ -78,6 +39,16 @@ type AddonUsecase interface { DisableAddon(ctx context.Context, name string) error } +// AddonImpl2AddonRes convert types.Addon to the type apiserver need +func AddonImpl2AddonRes(impl *types.Addon) *apis.DetailAddonResponse { + return &apis.DetailAddonResponse{ + AddonMeta: impl.AddonMeta, + APISchema: impl.APISchema, + UISchema: impl.UISchema, + Detail: impl.Detail, + } +} + // NewAddonUsecase returns a addon usecase func NewAddonUsecase(ds datastore.DataStore) AddonUsecase { kubecli, err := clients.GetKubeClient() @@ -118,7 +89,7 @@ func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, var app v1beta1.Application err := u.kubeClient.Get(context.Background(), client.ObjectKey{ Namespace: types.DefaultKubeVelaNS, - Name: AddonName2AppName(name), + Name: pkgaddon.Convert2AppName(name), }, &app) if err != nil { if errors2.IsNotFound(err) { @@ -151,8 +122,13 @@ func getCacheKeyWithDetailFLag(registry string, detailed bool) string { } return registry } + func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) { - var addons []*apis.DetailAddonResponse + if u.isRegistryCacheUpToDate(getCacheKeyWithDetailFLag(registry, detailed)) { + return u.getRegistryCache(getCacheKeyWithDetailFLag(registry, detailed)), nil + } + var addons []*types.Addon + var listAddons []*types.Addon rs, err := u.ListAddonRegistries(ctx) if err != nil { return nil, err @@ -162,24 +138,16 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, regist if registry != "" && r.Name != registry { continue } - - var gitAddons []*apis.DetailAddonResponse - if u.isRegistryCacheUpToDate(getCacheKeyWithDetailFLag(registry, detailed)) { - gitAddons = u.getRegistryCache(getCacheKeyWithDetailFLag(registry, detailed)) - } else { - gitAddons, err = getAddonsFromGit(r.Git.URL, r.Git.Path, r.Git.Token, detailed) - if err != nil { - log.Logger.Errorf("fail to get addons from registry %s", r.Name) - continue - } - u.putRegistryCache(getCacheKeyWithDetailFLag(registry, detailed), gitAddons) + listAddons, err = pkgaddon.ListAddons(detailed, r.Git) + if err != nil { + log.Logger.Errorf("fail to get addons from registry %s", r.Name) + continue } - - addons = mergeAddons(addons, gitAddons) + addons = mergeAddons(addons, listAddons) } if query != "" { - var filtered []*apis.DetailAddonResponse + var filtered []*types.Addon for i, addon := range addons { if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) { filtered = append(filtered, addons[i]) @@ -192,7 +160,12 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, regist return addons[i].Name < addons[j].Name }) - return addons, nil + var addonRes []*apis.DetailAddonResponse + for _, a := range addons { + addonRes = append(addonRes, AddonImpl2AddonRes(a)) + } + u.putRegistryCache(getCacheKeyWithDetailFLag(registry, detailed), addonRes) + return addonRes, nil } func (u *addonUsecaseImpl) DeleteAddonRegistry(ctx context.Context, name string) error { @@ -264,70 +237,35 @@ func (u *addonUsecaseImpl) ListAddonRegistries(ctx context.Context) ([]*apis.Add return list, nil } -func renderApplication(addon *apis.DetailAddonResponse, args *apis.EnableAddonRequest) (*v1beta1.Application, error) { - if args == nil { - args = &apis.EnableAddonRequest{Args: map[string]string{}} - } - app := addon.AppTemplate - if app == nil { - app = &v1beta1.Application{ - TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, - ObjectMeta: metav1.ObjectMeta{ - Name: AddonName2AppName(addon.Name), - Namespace: types.DefaultKubeVelaNS, - Labels: map[string]string{ - oam.LabelAddonName: addon.Name, - }, - }, - Spec: v1beta1.ApplicationSpec{ - Components: []common2.ApplicationComponent{}, - }, - } - } - app.Name = AddonName2AppName(addon.Name) - app.Labels = util.MergeMapOverrideWithDst(app.Labels, map[string]string{oam.LabelAddonName: addon.Name}) - - for _, tmpl := range addon.YAMLTemplates { - comp, err := renderRawComponent(tmpl) - if err != nil { - return nil, err - } - app.Spec.Components = append(app.Spec.Components, *comp) - } - for _, tmpl := range addon.CUETemplates { - comp, err := renderCUETemplate(tmpl, addon.Parameters, args.Args) - if err != nil { - log.Logger.Errorf("failed to render CUE template: %v", err) - return nil, bcode.ErrAddonRender - } - app.Spec.Components = append(app.Spec.Components, *comp) - } - for _, def := range addon.Definitions { - comp, err := renderRawComponent(def) - if err != nil { - return nil, err - } - app.Spec.Components = append(app.Spec.Components, *comp) - } - - return app, nil -} - func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error { - addon, err := u.GetAddon(ctx, name, "", true) + registries, err := u.ListAddonRegistries(ctx) if err != nil { return err } - app, err := renderApplication(addon, &args) - if err != nil { - return err + for _, r := range registries { + addon, err := pkgaddon.GetAddon(name, r.Git) + + if err != nil && err == bcode.ErrAddonNotExist { + continue + } else if err != nil { + return bcode.WrapGithubRateLimitErr(err) + } + + // render default ui schema + addon.UISchema = renderDefaultUISchema(addon.APISchema) + + app, err := pkgaddon.RenderApplication(addon, args.Args) + if err != nil { + return err + } + err = u.kubeClient.Create(ctx, app) + if err != nil { + log.Logger.Errorf("apply application fail: %s", err.Error()) + return bcode.ErrAddonApply + } + return nil } - err = u.kubeClient.Create(ctx, app) - if err != nil { - log.Logger.Errorf("apply application fail: %s", err.Error()) - return bcode.ErrAddonApply - } - return nil + return bcode.ErrAddonNotExist } func (u *addonUsecaseImpl) getRegistryCache(name string) []*apis.DetailAddonResponse { @@ -350,7 +288,7 @@ func (u *addonUsecaseImpl) DisableAddon(ctx context.Context, name string) error app := &v1beta1.Application{ TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, ObjectMeta: metav1.ObjectMeta{ - Name: AddonName2AppName(name), + Name: pkgaddon.Convert2AppName(name), Namespace: types.DefaultKubeVelaNS, }, } @@ -362,59 +300,6 @@ func (u *addonUsecaseImpl) DisableAddon(ctx context.Context, name string) error return nil } -// renderRawComponent will return a component in raw type from string -func renderRawComponent(elem apis.AddonElementFile) (*common2.ApplicationComponent, error) { - baseRawComponent := common2.ApplicationComponent{ - Type: "raw", - Name: strings.Join(append(elem.Path, elem.Name), "-"), - } - obj := &unstructured.Unstructured{} - dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) - _, _, err := dec.Decode([]byte(elem.Data), nil, obj) - if err != nil { - return nil, err - } - baseRawComponent.Properties = util.Object2RawExtension(obj) - return &baseRawComponent, nil -} - -// renderCUETemplate will return a component from cue template -func renderCUETemplate(elem apis.AddonElementFile, parameters string, args map[string]string) (*common2.ApplicationComponent, error) { - bt, err := json.Marshal(args) - if err != nil { - return nil, err - } - var paramFile = cuemodel.ParameterFieldName + ": {}" - if string(bt) != "null" { - paramFile = fmt.Sprintf("%s: %s", cuemodel.ParameterFieldName, string(bt)) - } - param := fmt.Sprintf("%s\n%s", paramFile, parameters) - v, err := value.NewValue(param, nil, "") - if err != nil { - return nil, err - } - out, err := v.LookupByScript(fmt.Sprintf("{%s}", elem.Data)) - if err != nil { - return nil, err - } - compContent, err := out.LookupValue("output") - if err != nil { - return nil, err - } - b, err := cueyaml.Encode(compContent.CueValue()) - if err != nil { - return nil, err - } - comp := common2.ApplicationComponent{ - Name: strings.Join(append(elem.Path, elem.Name), "-"), - } - err = yaml.Unmarshal(b, &comp) - if err != nil { - return nil, err - } - - return &comp, err -} func addonRegistryModelFromCreateAddonRegistryRequest(req apis.CreateAddonRegistryRequest) *model.AddonRegistry { return &model.AddonRegistry{ Name: req.Name, @@ -422,7 +307,7 @@ func addonRegistryModelFromCreateAddonRegistryRequest(req apis.CreateAddonRegist } } -func mergeAddons(a1, a2 []*apis.DetailAddonResponse) []*apis.DetailAddonResponse { +func mergeAddons(a1, a2 []*types.Addon) []*types.Addon { for _, item := range a2 { if hasAddon(a1, item.Name) { continue @@ -432,7 +317,7 @@ func mergeAddons(a1, a2 []*apis.DetailAddonResponse) []*apis.DetailAddonResponse return a1 } -func hasAddon(addons []*apis.DetailAddonResponse, name string) bool { +func hasAddon(addons []*types.Addon, name string) bool { for _, addon := range addons { if addon.Name == name { return true @@ -441,272 +326,6 @@ func hasAddon(addons []*apis.DetailAddonResponse, name string) bool { return false } -type gitHelper struct { - Client *github.Client - Meta *utils.Content -} - -func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*apis.DetailAddonResponse, error) { - var addons []*apis.DetailAddonResponse - - gith, err := createGitHelper(baseURL, dir, token) - if err != nil { - return nil, err - } - dirs, err := readRepo(gith) - if err != nil { - return nil, err - } - - for _, subItems := range dirs { - if subItems.GetType() != "dir" { - continue - } - addonRes := &apis.DetailAddonResponse{} - _, files, _, err := gith.Client.Repositories.GetContents(context.Background(), gith.Meta.Owner, gith.Meta.Repo, subItems.GetPath(), nil) - if err != nil { - if bcode.IsGithubRateLimit(err) { - return nil, bcode.ErrAddonRegistryRateLimit - } - log.Logger.Errorf("failed to read dir %s: %v", subItems.GetPath(), err) - continue - } - for _, file := range files { - var err error - - switch strings.ToLower(file.GetName()) { - case AddonReadmeFileName: - if !detailed { - break - } - err = readReadme(addonRes, gith, file) - case AddonMetadataFileName: - err = readMetadata(addonRes, gith, file) - addonRes.Name = addonutil.TransAddonName(addonRes.Name) - case AddonDefinitionsDirName: - if !detailed { - break - } - err = readDefinitions(addonRes, gith, file) - case AddonResourcesDirName: - if !detailed { - break - } - err = readResources(addonRes, gith, file) - case AddonTemplateFileName: - if !detailed { - break - } - err = readTemplate(addonRes, gith, file) - } - - if err != nil { - if bcode.IsGithubRateLimit(err) { - return nil, bcode.ErrAddonRegistryRateLimit - } - log.Logger.Errorf("failed to read file %s: %v", file.GetPath(), err) - continue - } - } - - if detailed && addonRes.Parameters != "" { - err = genAddonAPISchema(addonRes) - if err != nil { - continue - } - // render default ui schema - addonRes.UISchema = renderDefaultUISchema(addonRes.APISchema) - } - addons = append(addons, addonRes) - } - return addons, nil -} - -func genAddonAPISchema(addonRes *apis.DetailAddonResponse) error { - param, err := utils2.PrepareParameterCue(addonRes.Name, addonRes.Parameters) - if err != nil { - return err - } - var r cue.Runtime - cueInst, err := r.Compile("-", param) - if err != nil { - return err - } - data, err := common.GenOpenAPI(cueInst) - if err != nil { - log.Logger.Errorf("fail to generate openAPI json schema for addon: %s, err: %s", addonRes.Name, err) - return err - } - schema := &openapi3.Schema{} - if err := schema.UnmarshalJSON(data); err != nil { - log.Logger.Errorf("fail to unmarshal openAPI json schema for addon %s, err: %s", addonRes.Name, err) - return err - } - addonRes.APISchema = schema - return nil -} - -func cutPathUntil(path []string, end string) ([]string, error) { - for i, d := range path { - if d == end { - return path[i:], nil - } - } - return nil, errors.New("cut path fail, target directory name not found") -} - -func readTemplate(addon *apis.DetailAddonResponse, h *gitHelper, file *github.RepositoryContent) error { - content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) - if err != nil { - return err - } - data, err := content.GetContent() - if err != nil { - return err - } - dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) - addon.AppTemplate = &v1beta1.Application{} - _, _, err = dec.Decode([]byte(data), nil, addon.AppTemplate) - if err != nil { - return err - } - return nil -} - -func readResources(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.RepositoryContent) error { - dirPath := strings.Split(dir.GetPath(), "/") - dirPath, err := cutPathUntil(dirPath, AddonResourcesDirName) - if err != nil { - return err - } - - _, files, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *dir.Path, nil) - if err != nil { - return err - } - for _, file := range files { - switch file.GetType() { - case "file": - content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) - if err != nil { - return err - } - b, err := content.GetContent() - if err != nil { - return err - } - - if file.GetName() == "parameter.cue" { - addon.Parameters = b - break - } - switch filepath.Ext(file.GetName()) { - case ".cue": - addon.CUETemplates = append(addon.CUETemplates, apis.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) - default: - addon.YAMLTemplates = append(addon.YAMLTemplates, apis.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) - } - case "dir": - err = readResources(addon, h, file) - if err != nil { - return err - } - } - } - return nil -} - -func readDefinitions(addon *apis.DetailAddonResponse, h *gitHelper, dir *github.RepositoryContent) error { - dirPath := strings.Split(dir.GetPath(), "/") - dirPath, err := cutPathUntil(dirPath, AddonDefinitionsDirName) - if err != nil { - return err - } - - _, files, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *dir.Path, nil) - if err != nil { - return err - } - for _, file := range files { - switch file.GetType() { - case "file": - content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) - if err != nil { - return err - } - b, err := content.GetContent() - if err != nil { - return err - } - addon.Definitions = append(addon.Definitions, apis.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) - case "dir": - err = readDefinitions(addon, h, file) - if err != nil { - return err - } - } - } - return nil -} - -func readMetadata(addon *apis.DetailAddonResponse, h *gitHelper, file *github.RepositoryContent) error { - content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) - if err != nil { - return err - } - b, err := content.GetContent() - if err != nil { - return err - } - return yaml.Unmarshal([]byte(b), &addon.AddonMeta) -} - -func readReadme(addon *apis.DetailAddonResponse, h *gitHelper, file *github.RepositoryContent) error { - content, _, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, *file.Path, nil) - if err != nil { - return err - } - addon.Detail, err = content.GetContent() - return err -} - -func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { - var ts oauth2.TokenSource - if token != "" { - ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) - } - tc := oauth2.NewClient(context.Background(), ts) - tc.Timeout = time.Second * 10 - cli := github.NewClient(tc) - - baseURL = strings.TrimSuffix(baseURL, ".git") - u, err := url.Parse(baseURL) - if err != nil { - log.Logger.Errorf("parsing %s failed: %v", baseURL, err) - return nil, bcode.ErrAddonRegistryInvalid - } - u.Path = path.Join(u.Path, dir) - _, gitmeta, err := utils.Parse(u.String()) - if err != nil { - log.Logger.Errorf("parsing %s failed: %v", u.String(), err) - return nil, bcode.ErrAddonRegistryInvalid - } - - return &gitHelper{ - Client: cli, - Meta: gitmeta, - }, nil -} - -func readRepo(h *gitHelper) ([]*github.RepositoryContent, error) { - _, dirs, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, h.Meta.Path, nil) - if err != nil { - log.Logger.Errorf("readRepo fail: %v", err) - return nil, bcode.WrapGithubRateLimitErr(err) - } - return dirs, nil -} - // ConvertAddonRegistryModel2AddonRegistryMeta will convert from model to AddonRegistryMeta func ConvertAddonRegistryModel2AddonRegistryMeta(r *model.AddonRegistry) *apis.AddonRegistryMeta { return &apis.AddonRegistryMeta{ @@ -714,15 +333,3 @@ func ConvertAddonRegistryModel2AddonRegistryMeta(r *model.AddonRegistry) *apis.A Git: r.Git, } } - -const addonAppPrefix = "addon-" - -// AddonName2AppName - -func AddonName2AppName(name string) string { - return addonAppPrefix + name -} - -// AppName2addonName - -func AppName2addonName(name string) string { - return strings.TrimPrefix(name, addonAppPrefix) -} diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go index b84bdc45b..cd1d9a200 100644 --- a/pkg/apiserver/rest/utils/bcode/addon.go +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -17,7 +17,9 @@ limitations under the License. package bcode import ( - "github.com/google/go-github/v32/github" + "github.com/pkg/errors" + + pkgaddon "github.com/oam-dev/kubevela/pkg/addon" ) var ( @@ -49,17 +51,19 @@ var ( ErrGetAddonApplication = NewBcode(500, 50013, "fail to get addon application") ) -// IsGithubRateLimit check if error is github rate limit -func IsGithubRateLimit(err error) bool { - // nolint - _, ok := err.(*github.RateLimitError) - return ok +// isGithubRateLimit check if error is github rate limit +func isGithubRateLimit(err error) bool { + return errors.Is(err, pkgaddon.ErrRateLimit) } // WrapGithubRateLimitErr wraps error if it is github rate limit func WrapGithubRateLimitErr(err error) error { - if IsGithubRateLimit(err) { + if isGithubRateLimit(err) { return ErrAddonRegistryRateLimit } return err } + +func NewBcodeWrapErr(httpCode, businessCode int32, err error, message string) error { + return NewBcode(httpCode, businessCode, errors.Wrap(err, message).Error()) +} diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 79aa99123..9423656ba 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -20,6 +20,8 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" "github.com/emicklei/go-restful/v3" + "github.com/oam-dev/kubevela/apis/types" + 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" @@ -103,7 +105,7 @@ func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response return } - var addons []*apis.AddonMeta + var addons []*types.AddonMeta for _, d := range detailAddons { addons = append(addons, &d.AddonMeta) diff --git a/pkg/velaql/providers/query/suite_test.go b/pkg/velaql/providers/query/suite_test.go index 4df64b7f1..a1afed4ac 100644 --- a/pkg/velaql/providers/query/suite_test.go +++ b/pkg/velaql/providers/query/suite_test.go @@ -27,8 +27,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - "github.com/oam-dev/kubevela/pkg/utils/common" "k8s.io/utils/pointer" + + "github.com/oam-dev/kubevela/pkg/utils/common" ) var cfg *rest.Config diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go index 8ca9850ad..90f9b0965 100644 --- a/test/e2e-apiserver-test/addon_test.go +++ b/test/e2e-apiserver-test/addon_test.go @@ -8,6 +8,8 @@ import ( "os" "time" + "github.com/oam-dev/kubevela/pkg/addon" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" @@ -17,7 +19,6 @@ import ( "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils/common" - "github.com/oam-dev/kubevela/pkg/apiserver/model" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" ) @@ -41,7 +42,7 @@ func get(path string) *http.Response { var _ = Describe("Test addon rest api", func() { createReq := apis.CreateAddonRegistryRequest{ Name: "test-addon-registry-1", - Git: &model.GitAddonSource{ + Git: &addon.GitAddonSource{ URL: "https://github.com/oam-dev/catalog", Path: "addons/", Token: os.Getenv("GITHUB_TOKEN"), From 1c7e0c054d55795f0da52f67adaa5aaf887d9063 Mon Sep 17 00:00:00 2001 From: qiaozp <47812250+chivalryq@users.noreply.github.com> Date: Mon, 15 Nov 2021 17:17:07 +0800 Subject: [PATCH 35/59] Fix: add detail cache, fix uiSchema (#2716) * add detail cache, fix uischema * remove --- pkg/addon/addon.go | 6 +- pkg/apiserver/rest/usecase/addon.go | 113 ++++++++++++++---------- pkg/apiserver/rest/webservice/addon.go | 2 +- pkg/controller/utils/capability.go | 10 +-- pkg/controller/utils/capability_test.go | 2 +- 5 files changed, 77 insertions(+), 56 deletions(-) diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 1fca68d94..e0aa1bc6e 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -14,7 +14,6 @@ import ( "cuelang.org/go/cue" cueyaml "cuelang.org/go/encoding/yaml" - "github.com/getkin/kin-openapi/openapi3" "github.com/google/go-github/v32/github" "github.com/pkg/errors" "golang.org/x/oauth2" @@ -317,10 +316,11 @@ func genAddonAPISchema(addonRes *types.Addon) error { if err != nil { return err } - schema := &openapi3.Schema{} - if err := schema.UnmarshalJSON(data); err != nil { + schema, err := utils2.ConvertOpenAPISchema2SwaggerObject(data) + if err != nil { return err } + utils2.FixOpenAPISchema("",schema) addonRes.APISchema = schema return nil } diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index f6abcd2b9..fcfc32171 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -3,6 +3,7 @@ package usecase import ( "context" "errors" + "fmt" "sort" "strings" "time" @@ -34,7 +35,7 @@ type AddonUsecase interface { ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) StatusAddon(name string) (*apis.AddonStatusResponse, error) - GetAddon(ctx context.Context, name string, registry string, detailed bool) (*apis.DetailAddonResponse, error) + GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error DisableAddon(ctx context.Context, name string) error } @@ -70,16 +71,26 @@ type addonUsecaseImpl struct { apply apply.Applicator } -// GetAddon will get addon information, if detailed is not set, addon's componennt and internal definition won't be returned -func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string, registry string, detailed bool) (*apis.DetailAddonResponse, error) { - addons, err := u.ListAddons(ctx, detailed, registry, "") - if err != nil { - return nil, err - } - - for _, addon := range addons { - if addon.Name == name { - return addon, nil +// GetAddon will get addon information +func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) { + var addons []*types.Addon + cacheKey := getCacheKeyWithListOptions(registry, true, "") + if u.isRegistryCacheUpToDate(cacheKey) { + addons = u.getRegistryCache(cacheKey) + for _, a := range addons { + if a.Name == name { + return AddonImpl2AddonRes(a), nil + } + } + } else { + addonDetails, err := u.ListAddons(ctx, true, registry, "") + if err != nil { + return nil, err + } + for _, a := range addonDetails { + if a.Name == name { + return a, nil + } } } return nil, bcode.ErrAddonNotExist @@ -115,56 +126,66 @@ func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, } } -// getCacheKeyWithDetailFLag will get right cache key for given registry and detailed, to split different -func getCacheKeyWithDetailFLag(registry string, detailed bool) string { +// getCacheKeyWithListOptions will get right cache key for given method registry and detailed, to split different +func getCacheKeyWithListOptions(registry string, detailed bool, query string) string { + var d string if detailed { - return registry + "detailed" + d = "detailed" } - return registry + return fmt.Sprintf("%s/%s/%s", registry, d, query) } func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) { - if u.isRegistryCacheUpToDate(getCacheKeyWithDetailFLag(registry, detailed)) { - return u.getRegistryCache(getCacheKeyWithDetailFLag(registry, detailed)), nil - } var addons []*types.Addon var listAddons []*types.Addon - rs, err := u.ListAddonRegistries(ctx) - if err != nil { - return nil, err - } - - for _, r := range rs { - if registry != "" && r.Name != registry { - continue - } - listAddons, err = pkgaddon.ListAddons(detailed, r.Git) + cacheKey := getCacheKeyWithListOptions(registry, detailed, query) + if u.isRegistryCacheUpToDate(cacheKey) { + addons = u.getRegistryCache(cacheKey) + } else { + rs, err := u.ListAddonRegistries(ctx) if err != nil { - log.Logger.Errorf("fail to get addons from registry %s", r.Name) - continue + return nil, err } - addons = mergeAddons(addons, listAddons) - } - if query != "" { - var filtered []*types.Addon - for i, addon := range addons { - if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) { - filtered = append(filtered, addons[i]) + for _, r := range rs { + if registry != "" && r.Name != registry { + continue + } + listAddons, err = pkgaddon.ListAddons(detailed, r.Git) + if err != nil { + log.Logger.Errorf("fail to get addons from registry %s", r.Name) + continue + } + addons = mergeAddons(addons, listAddons) + } + + if query != "" { + var filtered []*types.Addon + for i, addon := range addons { + if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) { + filtered = append(filtered, addons[i]) + } + } + addons = filtered + } + sort.Slice(addons, func(i, j int) bool { + return addons[i].Name < addons[j].Name + }) + + if detailed { + for _, addon := range addons { + // render default ui schema + addon.UISchema = renderDefaultUISchema(addon.APISchema) } } - addons = filtered - } - sort.Slice(addons, func(i, j int) bool { - return addons[i].Name < addons[j].Name - }) + u.putRegistryCache(cacheKey, addons) + } var addonRes []*apis.DetailAddonResponse for _, a := range addons { addonRes = append(addonRes, AddonImpl2AddonRes(a)) } - u.putRegistryCache(getCacheKeyWithDetailFLag(registry, detailed), addonRes) return addonRes, nil } @@ -268,11 +289,11 @@ func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args ap return bcode.ErrAddonNotExist } -func (u *addonUsecaseImpl) getRegistryCache(name string) []*apis.DetailAddonResponse { - return u.addonRegistryCache[name].GetData().([]*apis.DetailAddonResponse) +func (u *addonUsecaseImpl) getRegistryCache(name string) []*types.Addon { + return u.addonRegistryCache[name].GetData().([]*types.Addon) } -func (u *addonUsecaseImpl) putRegistryCache(name string, addons []*apis.DetailAddonResponse) { +func (u *addonUsecaseImpl) putRegistryCache(name string, addons []*types.Addon) { u.addonRegistryCache[name] = restutils.NewMemoryCache(addons, time.Minute*3) } diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 9423656ba..6b15f77da 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -120,7 +120,7 @@ func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response func (s *addonWebService) detailAddon(req *restful.Request, res *restful.Response) { name := req.PathParameter("name") - addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name, "", true) + addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name, "") if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/controller/utils/capability.go b/pkg/controller/utils/capability.go index 11b89d208..cf405d2e0 100644 --- a/pkg/controller/utils/capability.go +++ b/pkg/controller/utils/capability.go @@ -482,7 +482,7 @@ func getOpenAPISchema(capability types.Capability, pd *packages.PackageDiscover) if err != nil { return nil, err } - fixOpenAPISchema("", schema) + FixOpenAPISchema("", schema) parameter, err := schema.MarshalJSON() if err != nil { @@ -558,18 +558,18 @@ func PrepareParameterCue(capabilityName, capabilityTemplate string) (string, err return template, nil } -// fixOpenAPISchema fixes tainted `description` filed, missing of title `field`. -func fixOpenAPISchema(name string, schema *openapi3.Schema) { +// FixOpenAPISchema fixes tainted `description` filed, missing of title `field`. +func FixOpenAPISchema(name string, schema *openapi3.Schema) { t := schema.Type switch t { case "object": for k, v := range schema.Properties { s := v.Value - fixOpenAPISchema(k, s) + FixOpenAPISchema(k, s) } case "array": if schema.Items != nil { - fixOpenAPISchema("", schema.Items.Value) + FixOpenAPISchema("", schema.Items.Value) } } if name != "" { diff --git a/pkg/controller/utils/capability_test.go b/pkg/controller/utils/capability_test.go index b8ef7caa1..44d0b490a 100644 --- a/pkg/controller/utils/capability_test.go +++ b/pkg/controller/utils/capability_test.go @@ -216,7 +216,7 @@ func TestFixOpenAPISchema(t *testing.T) { t.Run(name, func(t *testing.T) { swagger, _ := openapi3.NewSwaggerLoader().LoadSwaggerFromFile(filepath.Join(TestDir, tc.inputFile)) schema := swagger.Components.Schemas[model.ParameterFieldName].Value - fixOpenAPISchema("", schema) + FixOpenAPISchema("", schema) fixedSchema, _ := schema.MarshalJSON() expectedSchema, _ := os.ReadFile(filepath.Join(TestDir, tc.fixedFile)) assert.Equal(t, string(fixedSchema), string(expectedSchema)) From 43aa05673e3a466320c4bddc71791cc0a41a4cb9 Mon Sep 17 00:00:00 2001 From: wyike Date: Mon, 15 Nov 2021 21:04:03 +0800 Subject: [PATCH 36/59] Feat: manage trait (#2702) * Feat: manage trait fix test * fix test --- pkg/apiserver/rest/apis/v1/types.go | 32 +++---- pkg/apiserver/rest/usecase/application.go | 82 +++++++++++++--- .../rest/usecase/application_test.go | 62 ++++++++++++ pkg/apiserver/rest/utils/bcode/application.go | 6 ++ pkg/apiserver/rest/webservice/application.go | 94 +++++++++++++++++++ test/e2e-apiserver-test/application_test.go | 53 +++++++++++ 6 files changed, 302 insertions(+), 27 deletions(-) diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 84b551e91..a35d803c3 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -359,15 +359,15 @@ type ComponentListResponse struct { // CreateComponentRequest create component request model type CreateComponentRequest struct { - Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias" optional:"true"` - Description string `json:"description" optional:"true"` - Icon string `json:"icon" optional:"true"` - Labels map[string]string `json:"labels,omitempty"` - ComponentType string `json:"componentType" validate:"checkname"` - Properties string `json:"properties,omitempty"` - DependsOn []string `json:"dependsOn" optional:"true"` - Traits []*CreateApplicationTrait `json:"traits,omitempty" optional:"true"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon" optional:"true"` + Labels map[string]string `json:"labels,omitempty"` + ComponentType string `json:"componentType" validate:"checkname"` + Properties string `json:"properties,omitempty"` + DependsOn []string `json:"dependsOn" optional:"true"` + Traits []*CreateApplicationTraitRequest `json:"traits,omitempty" optional:"true"` } // DetailComponentResponse detail component model @@ -615,19 +615,19 @@ type CreateApplicationEnvRequest struct { EnvBinding } -// CreateApplicationTrait create application triat req -type CreateApplicationTrait struct { +// CreateApplicationTraitRequest create application triat req +type CreateApplicationTraitRequest struct { Type string `json:"type" validate:"checkname"` Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` Description string `json:"description,omitempty" optional:"true"` Properties string `json:"properties"` } -// UpdateApplicationTrait update application trait req -type UpdateApplicationTrait struct { - Alias *string `json:"alias,omitempty" validate:"checkalias" optional:"true"` - Description *string `json:"description,omitempty" optional:"true"` - Properties *string `json:"properties"` +// UpdateApplicationTraitRequest update application trait req +type UpdateApplicationTraitRequest struct { + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Properties string `json:"properties"` } // ApplicationTrait application trait diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index deafffe86..f13bb73c8 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -79,9 +79,9 @@ type ApplicationUsecase interface { UpdateApplicationEnvBinding(ctx context.Context, app *model.Application, envName string, diff apisv1.PutApplicationEnvRequest) (*apisv1.EnvBinding, error) CreateApplicationEnvBinding(ctx context.Context, app *model.Application, env apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) DeleteApplicationEnvBinding(ctx context.Context, app *model.Application, envName string) error - CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTrait) (*apisv1.ApplicationTrait, error) - DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string) error - UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string, req apisv1.UpdateApplicationTrait) (*apisv1.ApplicationTrait, error) + CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) + DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error + UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string, req apisv1.UpdateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) } type applicationUsecaseImpl struct { @@ -365,10 +365,10 @@ func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model. // DetailComponent detail app component // TODO: Add status data about the component. -func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailComponentResponse, error) { +func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model.Application, compName string) (*apisv1.DetailComponentResponse, error) { var component = model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), - Name: policyName, + Name: compName, } err := c.ds.Get(ctx, &component) if err != nil { @@ -1078,16 +1078,76 @@ func (c *applicationUsecaseImpl) DeleteApplicationEnvBinding(ctx context.Context return nil } -func (c *applicationUsecaseImpl) CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTrait) (*apisv1.ApplicationTrait, error) { - return nil, nil +func (c *applicationUsecaseImpl) CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) { + var comp = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: component.Name, + } + if err := c.ds.Get(ctx, &comp); err != nil { + return nil, err + } + for _, trait := range comp.Traits { + if trait.Type == req.Type { + return nil, bcode.ErrTraitAlreadyExist + } + } + properties, err := model.NewJSONStructByString(req.Properties) + if err != nil { + log.Logger.Errorf("new trait failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + trait := model.ApplicationTrait{Type: req.Type, Properties: properties} + comp.Traits = append(comp.Traits, trait) + if err := c.ds.Put(ctx, &comp); err != nil { + return nil, err + } + return &apisv1.ApplicationTrait{Type: trait.Type, Properties: properties}, nil } -func (c *applicationUsecaseImpl) DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string) error { - return nil +func (c *applicationUsecaseImpl) DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error { + var comp = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: component.Name, + } + if err := c.ds.Get(ctx, &comp); err != nil { + return err + } + for i, trait := range comp.Traits { + if trait.Type == traitType { + comp.Traits = append(comp.Traits[:i], comp.Traits[i+1:]...) + if err := c.ds.Put(ctx, &comp); err != nil { + return err + } + return nil + } + } + return bcode.ErrTraitNotExist } -func (c *applicationUsecaseImpl) UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traiName string, req apisv1.UpdateApplicationTrait) (*apisv1.ApplicationTrait, error) { - return nil, nil +func (c *applicationUsecaseImpl) UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string, req apisv1.UpdateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) { + var comp = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: component.Name, + } + if err := c.ds.Get(ctx, &comp); err != nil { + return nil, err + } + for i, trait := range comp.Traits { + if trait.Type == traitType { + properties, err := model.NewJSONStructByString(req.Properties) + if err != nil { + log.Logger.Errorf("update trait failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + trait.Properties = properties + comp.Traits[i] = trait + if err := c.ds.Put(ctx, &comp); err != nil { + return nil, err + } + return &apisv1.ApplicationTrait{Type: trait.Type, Properties: properties}, nil + } + } + return nil, bcode.ErrTraitNotExist } func createEnvBind(envBind apisv1.EnvBinding) v1alpha1.EnvConfig { diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index f5d0f3e3b..3a0eaf480 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -325,6 +325,68 @@ var _ = Describe("Test application usecase function", func() { err = appUsecase.DeletePolicy(context.TODO(), appModel, "env-binding-2") Expect(err).Should(BeNil()) }) + + It("Test add application trait", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + res, err := appUsecase.CreateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, v1.CreateApplicationTraitRequest{ + Type: "Ingress", + Properties: `{"domain":"www.test.com"}`, + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(res.Type, "Ingress")).Should(BeEmpty()) + comp, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + Expect(comp).ShouldNot(BeNil()) + Expect(len(comp.Traits)).Should(BeEquivalentTo(1)) + Expect(comp.Traits[0].Properties.JSON()).Should(BeEquivalentTo(`{"domain":"www.test.com"}`)) + }) + + It("Test add application a dup trait", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + _, err = appUsecase.CreateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, v1.CreateApplicationTraitRequest{ + Type: "Ingress", + Properties: `{"domain":"www.dup.com"}`, + }) + Expect(err).ShouldNot(BeNil()) + }) + + It("Test update application trait", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + res, err := appUsecase.UpdateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, "Ingress", v1.UpdateApplicationTraitRequest{ + Properties: `{"domain":"www.test1.com"}`, + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(res.Type, "Ingress")).Should(BeEmpty()) + comp, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + Expect(comp).ShouldNot(BeNil()) + Expect(len(comp.Traits)).Should(BeEquivalentTo(1)) + Expect(comp.Traits[0].Properties.JSON()).Should(BeEquivalentTo(`{"domain":"www.test1.com"}`)) + }) + + It("Test update a not exist", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + _, err = appUsecase.UpdateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, "Ingress-1-20", v1.UpdateApplicationTraitRequest{ + Properties: `{"domain":"www.test1.com"}`, + }) + Expect(err).ShouldNot(BeNil()) + }) + + It("Test delete an exist trait", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + err = appUsecase.DeleteApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, "Ingress") + Expect(err).Should(BeNil()) + app, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + Expect(app).ShouldNot(BeNil()) + Expect(len(app.Traits)).Should(BeEquivalentTo(0)) + }) + It("Test DeleteComponent function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index bd69a1e3b..03dc6767d 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -60,3 +60,9 @@ var ErrApplicationNotEnv = NewBcode(404, 10013, "application not set env binding // ErrApplicationEnvExist application env is exist var ErrApplicationEnvExist = NewBcode(400, 10014, "application env is exist") + +// ErrTraitNotExist trait is not exist +var ErrTraitNotExist = NewBcode(400, 10015, "trait is not exist") + +// ErrTraitAlreadyExist trait is already exist +var ErrTraitAlreadyExist = NewBcode(400, 10016, "trait is already exist") diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index a2df2ceb1..78a6c6e22 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -225,6 +225,40 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(200, "", apis.DetailPolicyResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.DetailPolicyResponse{})) + + ws.Route(ws.POST("/{name}/components/{compName}/traits").To(c.addApplicationTrait). + Doc("add trait for a component"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateApplicationTraitRequest{}). + Returns(200, "", apis.EmptyResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationTrait{})) + + ws.Route(ws.PUT("/{name}/components/{compName}/traits/{traitType}").To(c.updateApplicationTrait). + Doc("update trait from a component"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). + Param(ws.PathParameter("traitType", "identifier of the type of trait").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdateApplicationTraitRequest{}). + Returns(200, "", apis.ApplicationTrait{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationTrait{})) + + ws.Route(ws.DELETE("/{name}/components/{compName}/traits/{traitType}").To(c.deleteApplicationTrait). + Doc("delete trait from a component"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). + Param(ws.PathParameter("traitType", "identifier of the type of trait").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ApplicationTrait{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) return ws } @@ -577,3 +611,63 @@ func (c *applicationWebService) deleteApplicationEnv(req *restful.Request, res * return } } + +func (c *applicationWebService) addApplicationTrait(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + var createReq apis.CreateApplicationTraitRequest + 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 + } + trait, err := c.applicationUsecase.CreateApplicationTrait(req.Request.Context(), app, + &model.ApplicationComponent{Name: req.PathParameter("compName")}, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(trait); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) updateApplicationTrait(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + var updateReq apis.UpdateApplicationTraitRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + trait, err := c.applicationUsecase.UpdateApplicationTrait(req.Request.Context(), app, + &model.ApplicationComponent{Name: req.PathParameter("compName")}, req.PathParameter("traitType"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(trait); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) deleteApplicationTrait(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.applicationUsecase.DeleteApplicationTrait(req.Request.Context(), app, + &model.ApplicationComponent{Name: req.PathParameter("compName")}, req.PathParameter("traitType")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index 2648d6b0b..3f3e93b5b 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -222,6 +222,59 @@ var _ = Describe("Test application rest api", func() { Expect(cmp.Diff(len(response.DependsOn), 1)).Should(BeEmpty()) }) + It("Test add trait", func() { + defer GinkgoRecover() + var req = apisv1.CreateApplicationTraitRequest{ + Type: "ingress", + Properties: `{"domain": "www.test.com"}`, + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2/traits", "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 response apisv1.ApplicationTrait + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Properties.JSON(), `{"domain":"www.test.com"}`)).Should(BeEmpty()) + }) + + It("Test update trait", func() { + defer GinkgoRecover() + var req2 = apisv1.CreateApplicationTraitRequest{ + Type: "ingress", + Properties: `{"domain": "www.test1.com"}`, + } + bodyByte, err := json.Marshal(req2) + Expect(err).ShouldNot(HaveOccurred()) + req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2/traits/ingress", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + req.Header.Set("Content-Type", "application/json") + res, err := http.DefaultClient.Do(req) + 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 response apisv1.ApplicationTrait + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Properties.JSON(), `{"domain":"www.test1.com"}`)).Should(BeEmpty()) + }) + + It("Test delete trait", func() { + defer GinkgoRecover() + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2/traits/ingress", nil) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + }) + It("Test create application policy", func() { defer GinkgoRecover() var req = apisv1.CreatePolicyRequest{ From ba1f8e479378833f38b341fad7be89c4e76707b3 Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Tue, 16 Nov 2021 14:45:49 +0800 Subject: [PATCH 37/59] Feat: change the model to support multiple environments (#2721) * Feat: change swagger config * Feat: change the model to support multiple environments. * Feat: support query targets by namespace * Fix: fix definition unit test case Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 1120 +++++++++++------ pkg/addon/addon.go | 2 +- pkg/addon/error.go | 3 +- pkg/apiserver/model/application.go | 21 +- pkg/apiserver/model/deliverytarget.go | 24 +- pkg/apiserver/model/workflow.go | 4 + pkg/apiserver/rest/apis/v1/types.go | 95 +- pkg/apiserver/rest/usecase/addon.go | 2 +- pkg/apiserver/rest/usecase/application.go | 65 +- .../rest/usecase/application_test.go | 2 +- pkg/apiserver/rest/usecase/definition_test.go | 4 +- pkg/apiserver/rest/usecase/delivery_target.go | 23 +- .../rest/usecase/delivery_target_test.go | 10 +- .../usecase/testdata/ui-custom-schema.yaml | 69 +- .../rest/usecase/testdata/ui-schema.yaml | 464 +++---- pkg/apiserver/rest/usecase/workflow_test.go | 2 +- pkg/apiserver/rest/utils/bcode/addon.go | 1 + pkg/apiserver/rest/utils/bcode/application.go | 4 +- pkg/apiserver/rest/utils/uiswagger.go | 8 +- pkg/apiserver/rest/webservice/application.go | 26 +- .../rest/webservice/delivery_target.go | 5 +- test/e2e-apiserver-test/application_test.go | 2 +- 22 files changed, 1171 insertions(+), 785 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 2ab4b4890..046ae2f77 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -658,6 +658,169 @@ } } }, + "/api/v1/applications/{name}/components/{compName}/traits": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "add trait for a component", + "operationId": "addApplicationTrait", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the component", + "name": "compName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationTraitRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/components/{compName}/traits/{traitType}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update trait from a component", + "operationId": "updateApplicationTrait", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the component", + "name": "compName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the type of trait", + "name": "traitType", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateApplicationTraitRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationTrait" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "delete trait from a component", + "operationId": "deleteApplicationTrait", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the component", + "name": "compName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the type of trait", + "name": "traitType", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationTrait" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/applications/{name}/components/{componentName}": { "get": { "consumes": [ @@ -856,7 +1019,7 @@ }, { "type": "string", - "description": "identifier of the application ", + "description": "identifier of the application envbinding", "name": "envName", "in": "path", "required": true @@ -876,6 +1039,51 @@ } } }, + "/api/v1/applications/{name}/envs/{envName}/status": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "get application status", + "operationId": "getApplicationStatus", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application envbinding", + "name": "envName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/applications/{name}/policies": { "get": { "consumes": [ @@ -1649,9 +1857,9 @@ "parameters": [ { "enum": [ + "component", "trait", - "workflowstep", - "component" + "workflowstep" ], "type": "string", "description": "query the definition type", @@ -1722,6 +1930,191 @@ } } }, + "/api/v1/deliveryTargets": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "deliveryTarget" + ], + "summary": "list deliveryTarget", + "operationId": "listDeliveryTargets", + "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": [ + "deliveryTarget" + ], + "summary": "create deliveryTarget", + "operationId": "createDeliveryTarget", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateDeliveryTargetRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "400": { + "description": "create failure", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/deliveryTargets/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "deliveryTarget" + ], + "summary": "detail deliveryTarget", + "operationId": "detailDeliveryTarget", + "parameters": [ + { + "type": "string", + "description": "identifier of the deliveryTarget.", + "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": [ + "deliveryTarget" + ], + "summary": "update application DeliveryTarget config", + "operationId": "updateDeliveryTarget", + "parameters": [ + { + "type": "string", + "description": "identifier of the deliveryTarget", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateDeliveryTargetRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "deliveryTarget" + ], + "summary": "deletet DeliveryTarget", + "operationId": "deleteDeliveryTarget", + "parameters": [ + { + "type": "string", + "description": "identifier of the deliveryTarget", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, "/api/v1/namespaces": { "get": { "consumes": [ @@ -2256,6 +2649,19 @@ } }, "definitions": { + "addon.GitAddonSource": { + "properties": { + "path": { + "type": "string" + }, + "token": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "bcode.Bcode": { "required": [ "BusinessCode", @@ -2318,10 +2724,10 @@ }, "common.AppRolloutStatus": { "required": [ - "batchRollingState", - "currentBatch", "upgradedReadyReplicas", "rollingState", + "batchRollingState", + "currentBatch", "upgradedReplicas", "lastTargetAppRevision" ], @@ -2982,19 +3388,6 @@ } } }, - "model.GitAddonSource": { - "properties": { - "path": { - "type": "string" - }, - "token": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, "model.JSONStruct": { "type": "object" }, @@ -3043,6 +3436,67 @@ } } }, + "types.AddonDependency": { + "properties": { + "name": { + "type": "string" + } + } + }, + "types.AddonDeployTo": { + "required": [ + "control_plane", + "runtime_cluster" + ], + "properties": { + "control_plane": { + "type": "boolean" + }, + "runtime_cluster": { + "type": "boolean" + } + } + }, + "types.AddonMeta": { + "required": [ + "name", + "version", + "description", + "icon" + ], + "properties": { + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/types.AddonDependency" + } + }, + "deploy_to": { + "$ref": "#/definitions/types.AddonDeployTo" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "url": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "types.Parameter": { "required": [ "name" @@ -3079,6 +3533,23 @@ } }, "types.Parameter.default": {}, + "utils.GroupOption": { + "required": [ + "label", + "keys" + ], + "properties": { + "keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "label": { + "type": "string" + } + } + }, "utils.Option": { "required": [ "label", @@ -3122,7 +3593,7 @@ "subParameterGroupOption": { "type": "array", "items": { - "$ref": "#/definitions/utils.UIParameter.subParameterGroupOption" + "$ref": "#/definitions/utils.GroupOption" } }, "subParameters": { @@ -3189,95 +3660,13 @@ } } }, - "v1.AddonDependency": { - "properties": { - "name": { - "type": "string" - } - } - }, - "v1.AddonDeployTo": { - "required": [ - "control_plane", - "runtime_cluster" - ], - "properties": { - "control_plane": { - "type": "boolean" - }, - "runtime_cluster": { - "type": "boolean" - } - } - }, - "v1.AddonElementFile": { - "required": [ - "Data", - "Name", - "Path" - ], - "properties": { - "Data": { - "type": "string" - }, - "Name": { - "type": "string" - }, - "Path": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "v1.AddonMeta": { - "required": [ - "name", - "version", - "description", - "icon" - ], - "properties": { - "dependencies": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonDependency" - } - }, - "deploy_to": { - "$ref": "#/definitions/v1.AddonDeployTo" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "url": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, "v1.AddonRegistryMeta": { "required": [ "name" ], "properties": { "git": { - "$ref": "#/definitions/model.GitAddonSource" + "$ref": "#/definitions/addon.GitAddonSource" }, "name": { "type": "string" @@ -3305,9 +3694,7 @@ "description", "createTime", "updateTime", - "icon", - "status", - "gatewayRule" + "icon" ], "properties": { "alias": { @@ -3326,12 +3713,6 @@ "$ref": "#/definitions/v1.EnvBinding" } }, - "gatewayRule": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.GatewayRule" - } - }, "icon": { "type": "string" }, @@ -3347,9 +3728,6 @@ "namespace": { "type": "string" }, - "status": { - "type": "string" - }, "updateTime": { "type": "string", "format": "date-time" @@ -3493,6 +3871,20 @@ } } }, + "v1.ApplicationStatusResponse": { + "required": [ + "envName", + "status" + ], + "properties": { + "envName": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/common.AppStatus" + } + } + }, "v1.ApplicationTemplateBase": { "required": [ "templateName", @@ -3547,6 +3939,30 @@ } } }, + "v1.ApplicationTrait": { + "required": [ + "name", + "type", + "properties" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + } + } + }, "v1.ClusterBase": { "required": [ "name", @@ -3652,6 +4068,19 @@ } } }, + "v1.ClusterTarget": { + "required": [ + "clusterName" + ], + "properties": { + "clusterName": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, "v1.ComponentBase": { "required": [ "name", @@ -3784,7 +4213,7 @@ ], "properties": { "git": { - "$ref": "#/definitions/model.GitAddonSource" + "$ref": "#/definitions/addon.GitAddonSource" }, "name": { "type": "string" @@ -3793,8 +4222,8 @@ }, "v1.CreateApplicationEnvRequest": { "required": [ - "name", - "targetNames" + "targetNames", + "name" ], "properties": { "alias": { @@ -3878,7 +4307,7 @@ } } }, - "v1.CreateApplicationTrait": { + "v1.CreateApplicationTraitRequest": { "required": [ "type", "properties" @@ -4023,11 +4452,37 @@ "traits": { "type": "array", "items": { - "$ref": "#/definitions/v1.CreateApplicationTrait" + "$ref": "#/definitions/v1.CreateApplicationTraitRequest" } } } }, + "v1.CreateDeliveryTargetRequest": { + "required": [ + "name", + "namespace" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kubernetes": { + "$ref": "#/definitions/v1.ClusterTarget" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "variable": { + "type": "object" + } + } + }, "v1.CreateNamespaceRequest": { "required": [ "name", @@ -4112,43 +4567,60 @@ } } }, - "v1.DetailAddonResponse": { + "v1.DeliveryTargetBase": { "required": [ "name", + "namespace", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "kubernetes": { + "$ref": "#/definitions/v1.ClusterTarget" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "variable": { + "type": "object" + } + } + }, + "v1.DetailAddonResponse": { + "required": [ "version", "description", "icon", + "name", "schema", - "uiSchema", - "definitions", - "parameters", - "cue_templates", - "app_template" + "uiSchema" ], "properties": { - "app_template": { - "$ref": "#/definitions/v1beta1.Application" - }, - "cue_templates": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonElementFile" - } - }, - "definitions": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonElementFile" - } - }, "dependencies": { "type": "array", "items": { - "$ref": "#/definitions/v1.AddonDependency" + "$ref": "#/definitions/types.AddonDependency" } }, "deploy_to": { - "$ref": "#/definitions/v1.AddonDeployTo" + "$ref": "#/definitions/types.AddonDeployTo" }, "description": { "type": "string" @@ -4162,9 +4634,6 @@ "name": { "type": "string" }, - "parameters": { - "type": "string" - }, "schema": { "type": "string" }, @@ -4185,26 +4654,18 @@ }, "version": { "type": "string" - }, - "yaml_templates": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.AddonElementFile" - } } } }, "v1.DetailApplicationResponse": { "required": [ - "alias", - "createTime", - "gatewayRule", "name", - "namespace", + "alias", "description", - "updateTime", "icon", - "status", + "namespace", + "createTime", + "updateTime", "policies", "status", "resourceInfo", @@ -4227,12 +4688,6 @@ "$ref": "#/definitions/v1.EnvBinding" } }, - "gatewayRule": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.GatewayRule" - } - }, "icon": { "type": "string" }, @@ -4274,20 +4729,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "updateTime", - "alias", - "description", - "icon", - "provider", - "apiServerURL", - "kubeConfigSecret", - "status", "labels", + "apiServerURL", "reason", "dashboardURL", + "description", + "status", + "alias", + "provider", + "kubeConfigSecret", "createTime", "name", "kubeConfig", + "updateTime", + "icon", "resourceInfo" ], "properties": { @@ -4345,13 +4800,13 @@ }, "v1.DetailComponentResponse": { "required": [ - "updateTime", - "appPrimaryKey", - "type", "createTime", "name", + "appPrimaryKey", + "updateTime", "creator", - "alias" + "alias", + "type" ], "properties": { "alias": { @@ -4444,15 +4899,51 @@ } } }, + "v1.DetailDeliveryTargetResponse": { + "required": [ + "createTime", + "updateTime", + "name", + "namespace" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "kubernetes": { + "$ref": "#/definitions/v1.ClusterTarget" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "variable": { + "type": "object" + } + } + }, "v1.DetailPolicyResponse": { "required": [ + "updateTime", "name", "type", "description", "creator", "properties", - "createTime", - "updateTime" + "createTime" ], "properties": { "createTime": { @@ -4531,13 +5022,13 @@ }, "v1.DetailWorkflowResponse": { "required": [ - "createTime", "updateTime", "name", "alias", "description", "enable", "default", + "createTime", "workflowRecord" ], "properties": { @@ -4628,33 +5119,6 @@ } } }, - "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.ListAddonRegistryResponse": { "required": [ "registrys" @@ -4676,7 +5140,7 @@ "addons": { "type": "array", "items": { - "$ref": "#/definitions/v1.AddonMeta" + "$ref": "#/definitions/types.AddonMeta" } } } @@ -4764,6 +5228,24 @@ } } }, + "v1.ListDeliveryTargetResponse": { + "required": [ + "deliveryTargets", + "total" + ], + "properties": { + "deliveryTargets": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.DeliveryTargetBase" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, "v1.ListNamespaceResponse": { "required": [ "namespaces" @@ -4821,39 +5303,6 @@ } } }, - "v1.ManagedFieldsEntry": { - "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", - "properties": { - "apiVersion": { - "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", - "type": "string" - }, - "fieldsType": { - "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", - "type": "string" - }, - "fieldsV1": { - "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", - "type": "string" - }, - "manager": { - "description": "Manager is an identifier of the workflow managing these fields.", - "type": "string" - }, - "operation": { - "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", - "type": "string" - }, - "subresource": { - "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", - "type": "string" - }, - "time": { - "description": "Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply'", - "type": "string" - } - } - }, "v1.NamespaceBase": { "required": [ "name", @@ -4880,10 +5329,10 @@ }, "v1.NamespaceDetailResponse": { "required": [ + "createTime", "updateTime", "name", - "description", - "createTime" + "description" ], "properties": { "createTime": { @@ -4902,92 +5351,6 @@ } } }, - "v1.ObjectMeta": { - "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", - "properties": { - "annotations": { - "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "clusterName": { - "description": "The name of the cluster which the object belongs to. This is used to distinguish resources with same name and namespace in different clusters. This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request.", - "type": "string" - }, - "creationTimestamp": { - "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - "type": "string" - }, - "deletionGracePeriodSeconds": { - "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", - "type": "integer", - "format": "int64" - }, - "deletionTimestamp": { - "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - "type": "string" - }, - "finalizers": { - "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", - "type": "array", - "items": { - "type": "string" - } - }, - "generateName": { - "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will NOT return a 409 - instead, it will either return 201 Created or 500 with Reason ServerTimeout indicating a unique name could not be found in the time allotted, and the client should retry (optionally after the time indicated in the Retry-After header).\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", - "type": "string" - }, - "generation": { - "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", - "type": "integer", - "format": "int64" - }, - "labels": { - "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "managedFields": { - "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", - "type": "array", - "items": { - "$ref": "#/definitions/v1.ManagedFieldsEntry" - } - }, - "name": { - "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", - "type": "string" - }, - "namespace": { - "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces", - "type": "string" - }, - "ownerReferences": { - "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", - "type": "array", - "items": { - "$ref": "#/definitions/v1.OwnerReference" - } - }, - "resourceVersion": { - "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", - "type": "string" - }, - "selfLink": { - "description": "SelfLink is a URL representing this object. Populated by the system. Read-only.\n\nDEPRECATED Kubernetes will stop propagating this field in 1.20 release and the field is planned to be removed in 1.21 release.", - "type": "string" - }, - "uid": { - "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: http://kubernetes.io/docs/user-guide/identifiers#uids", - "type": "string" - } - } - }, "v1.ObjectReference": { "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", "properties": { @@ -5021,41 +5384,6 @@ } } }, - "v1.OwnerReference": { - "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", - "required": [ - "apiVersion", - "kind", - "name", - "uid" - ], - "properties": { - "apiVersion": { - "description": "API version of the referent.", - "type": "string" - }, - "blockOwnerDeletion": { - "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", - "type": "boolean" - }, - "controller": { - "description": "If true, this reference points to the managing controller.", - "type": "boolean" - }, - "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: http://kubernetes.io/docs/user-guide/identifiers#names", - "type": "string" - }, - "uid": { - "description": "UID of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#uids", - "type": "string" - } - } - }, "v1.PolicyBase": { "required": [ "name", @@ -5135,23 +5463,10 @@ } } }, - "v1.TypeMeta": { - "description": "TypeMeta describes an individual object in an API response or request with strings representing the type of the object and its API schema version. Structures that are versioned or persisted should inline TypeMeta.", - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - } - } - }, "v1.UpdateAddonRegistryRequest": { "properties": { "git": { - "$ref": "#/definitions/model.GitAddonSource" + "$ref": "#/definitions/addon.GitAddonSource" } } }, @@ -5174,6 +5489,38 @@ } } }, + "v1.UpdateApplicationTraitRequest": { + "required": [ + "properties" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "properties": { + "type": "string" + } + } + }, + "v1.UpdateDeliveryTargetRequest": { + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kubernetes": { + "$ref": "#/definitions/v1.ClusterTarget" + }, + "variable": { + "type": "object" + } + } + }, "v1.UpdatePolicyRequest": { "required": [ "description", @@ -5552,27 +5899,6 @@ } } }, - "v1beta1.Application": { - "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "$ref": "#/definitions/v1.ObjectMeta" - }, - "spec": { - "$ref": "#/definitions/v1beta1.ApplicationSpec" - }, - "status": { - "$ref": "#/definitions/common.AppStatus" - } - } - }, "v1beta1.ApplicationSpec": { "required": [ "components" diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index e0aa1bc6e..8940fd801 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -320,7 +320,7 @@ func genAddonAPISchema(addonRes *types.Addon) error { if err != nil { return err } - utils2.FixOpenAPISchema("",schema) + utils2.FixOpenAPISchema("", schema) addonRes.APISchema = schema return nil } diff --git a/pkg/addon/error.go b/pkg/addon/error.go index df98357f5..244345b78 100644 --- a/pkg/addon/error.go +++ b/pkg/addon/error.go @@ -20,8 +20,7 @@ var ( // WrapErrRateLimit return ErrRateLimit if is the situation, or return error directly func WrapErrRateLimit(err error) error { - _, ok := err.(*github.RateLimitError) - if ok { + if errors.As(err, &github.RateLimitError{}) { return ErrRateLimit } return err diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 4abbfc50a..bf37fadfd 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -176,17 +176,17 @@ type ApplicationTrait struct { Properties *JSONStruct `json:"properties,omitempty"` } -// DeployEventInit event status init -var DeployEventInit = "init" +// RevisionStatusInit event status init +var RevisionStatusInit = "init" -// DeployEventRunning event status running -var DeployEventRunning = "running" +// RevisionStatusRunning event status running +var RevisionStatusRunning = "running" -// DeployEventComplete event status complete -var DeployEventComplete = "complete" +// RevisionStatusComplete event status complete +var RevisionStatusComplete = "complete" -// DeployEventFail event status failure -var DeployEventFail = "failure" +// RevisionStatusFail event status failure +var RevisionStatusFail = "failure" // ApplicationRevision be created when an application initiates deployment and describes the phased version of the application. type ApplicationRevision struct { @@ -214,7 +214,7 @@ type ApplicationRevision struct { // TableName return custom table name func (a *ApplicationRevision) TableName() string { - return tableNamePrefix + "deploy_event" + return tableNamePrefix + "application_revision" } // PrimaryKey return custom primary key @@ -231,6 +231,9 @@ func (a *ApplicationRevision) Index() map[string]string { if a.AppPrimaryKey != "" { index["appPrimaryKey"] = a.AppPrimaryKey } + if a.WorkflowName != "" { + index["workflowName"] = a.WorkflowName + } if a.DeployUser != "" { index["deployUser"] = a.DeployUser } diff --git a/pkg/apiserver/model/deliverytarget.go b/pkg/apiserver/model/deliverytarget.go index 7608c59aa..158dbe1e3 100644 --- a/pkg/apiserver/model/deliverytarget.go +++ b/pkg/apiserver/model/deliverytarget.go @@ -24,12 +24,12 @@ func init() { // It includes kubernetes clusters or cloud service providers type DeliveryTarget struct { Model - Name string `json:"name"` - Namespace string `json:"namespace"` - Alias string `json:"alias,omitempty"` - Description string `json:"description,omitempty"` - Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` - Cloud *CloudTarget `json:"cloud,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Alias string `json:"alias,omitempty"` + Description string `json:"description,omitempty"` + Cluster *ClusterTarget `json:"kubernetes,omitempty"` + Variable map[string]interface{} `json:"variable,omitempty"` } // TableName return custom table name @@ -54,16 +54,8 @@ func (d *DeliveryTarget) Index() map[string]string { return index } -// KubernetesTarget kubernetes delivery target -type KubernetesTarget struct { +// ClusterTarget kubernetes delivery target +type ClusterTarget struct { ClusterName string `json:"clusterName" validate:"checkname"` Namespace string `json:"namespace" optional:"true"` } - -// CloudTarget cloud target -type CloudTarget struct { - TerraformProviderName string `json:"providerName" validate:"required"` - Region string `json:"region" validate:"required"` - Zone string `json:"zone" optional:"true"` - VpcID string `json:"vpcID" optional:"true"` -} diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index 29160d950..b7785dfbc 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -37,6 +37,7 @@ type Workflow struct { // Workflow used by the default Default bool `json:"default"` AppPrimaryKey string `json:"appPrimaryKey"` + EnvName string `json:"envName"` Steps []WorkflowStep `json:"steps,omitempty"` } @@ -72,6 +73,9 @@ func (w *Workflow) Index() map[string]string { if w.AppPrimaryKey != "" { index["appPrimaryKey"] = w.AppPrimaryKey } + if w.EnvName != "" { + index["envName"] = w.EnvName + } index["default"] = strconv.FormatBool(w.Default) return index } diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index a35d803c3..ad011b622 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -240,36 +240,21 @@ func (e EnvBindingList) ContainTarget(name string) bool { // ApplicationBase application base model type ApplicationBase struct { - Name string `json:"name"` - Alias string `json:"alias"` - Namespace string `json:"namespace"` - Description string `json:"description"` - CreateTime time.Time `json:"createTime"` - UpdateTime time.Time `json:"updateTime"` - Icon string `json:"icon"` - Labels map[string]string `json:"labels,omitempty"` - Status string `json:"status"` - EnvBinding EnvBindingList `json:"envBinding,omitempty"` - GatewayRuleList []GatewayRule `json:"gatewayRule"` + Name string `json:"name"` + Alias string `json:"alias"` + Namespace string `json:"namespace"` + Description string `json:"description"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels,omitempty"` + EnvBinding EnvBindingList `json:"envBinding,omitempty"` } -// RuleType gateway rule type -type RuleType string - -const ( - // HTTPRule Layer 7 HTTP policy. - HTTPRule RuleType = "http" - // StreamRule Layer 4 policy, such as TCP and UDP - StreamRule RuleType = "stream" -) - -// GatewayRule application gateway rule -type GatewayRule struct { - RuleType RuleType `json:"ruleType"` - Address string `json:"address"` - Protocol string `json:"protocol"` - ComponentName string `json:"componentName"` - ComponentPort int32 `json:"componentPort"` +// ApplicationStatusResponse application status response body +type ApplicationStatusResponse struct { + EnvName string `json:"envName"` + Status *common.AppStatus `json:"status"` } // CreateApplicationRequest create application request body @@ -370,7 +355,7 @@ type CreateComponentRequest struct { Traits []*CreateApplicationTraitRequest `json:"traits,omitempty" optional:"true"` } -// DetailComponentResponse detail component model +// DetailComponentResponse detail component response body type DetailComponentResponse struct { model.ApplicationComponent } @@ -642,42 +627,34 @@ type ApplicationTrait struct { // CreateDeliveryTargetRequest create delivery target request body type CreateDeliveryTargetRequest struct { - Name string `json:"name" validate:"checkname"` - Namespace string `json:"namespace" validate:"checkname"` - Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` - Description string `json:"description,omitempty" optional:"true"` - Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` - Cloud *CloudTarget `json:"cloud,omitempty"` + Name string `json:"name" validate:"checkname"` + Namespace string `json:"namespace" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Cluster *ClusterTarget `json:"kubernetes,omitempty"` + Variable map[string]interface{} `json:"variable,omitempty"` } -// UpdateDeliveryTarget only support full quantity update +// UpdateDeliveryTargetRequest only support full quantity update type UpdateDeliveryTargetRequest struct { - Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` - Description string `json:"description,omitempty" optional:"true"` - Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` - Cloud *CloudTarget `json:"cloud,omitempty"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Cluster *ClusterTarget `json:"kubernetes,omitempty"` + Variable map[string]interface{} `json:"variable,omitempty"` } -// KubernetesTarget kubernetes delivery target -type KubernetesTarget struct { +// ClusterTarget kubernetes delivery target +type ClusterTarget struct { ClusterName string `json:"clusterName" validate:"checkname"` Namespace string `json:"namespace" optional:"true"` } -// CloudTarget cloud target -type CloudTarget struct { - TerraformProviderName string `json:"providerName" validate:"required"` - Region string `json:"region" validate:"required"` - Zone string `json:"zone" optional:"true"` - VpcID string `json:"vpcID" optional:"true"` -} - // DetailDeliveryTargetResponse detail deliveryTarget response type DetailDeliveryTargetResponse struct { DeliveryTargetBase } -// ListDeliveryTargetBaseResponse list application workflows +// ListDeliveryTargetResponse list delivery target response body type ListDeliveryTargetResponse struct { DeliveryTargets []DeliveryTargetBase `json:"deliveryTargets"` Total int64 `json:"total"` @@ -685,14 +662,14 @@ type ListDeliveryTargetResponse struct { // DeliveryTargetBase deliveryTarget base model type DeliveryTargetBase struct { - Name string `json:"name" validate:"checkname"` - Namespace string `json:"namespace" validate:"checkname"` - Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` - Description string `json:"description,omitempty" optional:"true"` - Kubernetes *KubernetesTarget `json:"kubernetes,omitempty"` - Cloud *CloudTarget `json:"cloud,omitempty"` - CreateTime time.Time `json:"createTime"` - UpdateTime time.Time `json:"updateTime"` + Name string `json:"name" validate:"checkname"` + Namespace string `json:"namespace" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Cluster *ClusterTarget `json:"kubernetes,omitempty"` + Variable map[string]interface{} `json:"variable,omitempty"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // ApplicationRevisionBase application revision base spec diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index fcfc32171..7ca09dc36 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -266,7 +266,7 @@ func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args ap for _, r := range registries { addon, err := pkgaddon.GetAddon(name, r.Git) - if err != nil && err == bcode.ErrAddonNotExist { + if err != nil && errors.Is(err, bcode.ErrAddonNotExist) { continue } else if err != nil { return bcode.WrapGithubRateLimitErr(err) diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index f13bb73c8..fb3c87308 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -60,6 +60,7 @@ const ( type ApplicationUsecase interface { ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) GetApplication(ctx context.Context, appName string) (*model.Application, error) + GetApplicationStatus(ctx context.Context, app *model.Application, envName string) (*common.AppStatus, error) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) CreateApplication(context.Context, apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) @@ -175,6 +176,19 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod return detail, nil } +// GetApplicationStatus get application status from controller cluster +func (c *applicationUsecaseImpl) GetApplicationStatus(ctx context.Context, appmodel *model.Application, envName string) (*common.AppStatus, error) { + var app v1beta1.Application + err := c.kubeClient.Get(ctx, types.NamespacedName{Namespace: appmodel.Namespace, Name: converAppName(appmodel, envName)}, &app) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + return &app.Status, nil +} + // PublishApplicationTemplate publish app template func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) { //TODO: @@ -561,18 +575,18 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat list, err := c.ds.List(ctx, &lastVersion, &datastore.ListOptions{PageSize: 1, Page: 1}) if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { log.Logger.Errorf("query app latest revision failure %s", err.Error()) - return nil, bcode.ErrDeployEventConflict + return nil, bcode.ErrDeployConflict } - if len(list) > 0 && list[0].(*model.ApplicationRevision).Status != model.DeployEventComplete { - return nil, bcode.ErrDeployEventConflict + if len(list) > 0 && list[0].(*model.ApplicationRevision).Status != model.RevisionStatusComplete { + return nil, bcode.ErrDeployConflict } } - var deployEvent = &model.ApplicationRevision{ + var appRevision = &model.ApplicationRevision{ AppPrimaryKey: app.PrimaryKey(), Version: version, ApplyAppConfig: string(configByte), - Status: model.DeployEventInit, + Status: model.RevisionStatusInit, // TODO: Get user information from ctx and assign a value. DeployUser: "", Note: req.Note, @@ -580,7 +594,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat WorkflowName: oamApp.Annotations[oam.AnnotationWorkflowName], } - if err := c.ds.Add(ctx, deployEvent); err != nil { + if err := c.ds.Add(ctx, appRevision); err != nil { return nil, err } // step3: check and create namespace @@ -595,28 +609,28 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat // step4: apply to controller cluster err = c.apply.Apply(ctx, oamApp) if err != nil { - deployEvent.Status = model.DeployEventFail - deployEvent.Reason = err.Error() - if err := c.ds.Put(ctx, deployEvent); err != nil { + appRevision.Status = model.RevisionStatusFail + appRevision.Reason = err.Error() + if err := c.ds.Put(ctx, appRevision); err != nil { log.Logger.Warnf("update deploy event failure %s", err.Error()) } log.Logger.Errorf("deploy app %s failure %s", app.PrimaryKey(), err.Error()) return nil, bcode.ErrDeployApplyFail } - deployEvent.Status = model.DeployEventRunning - if err := c.ds.Put(ctx, deployEvent); err != nil { + appRevision.Status = model.RevisionStatusRunning + if err := c.ds.Put(ctx, appRevision); err != nil { log.Logger.Warnf("update deploy event failure %s", err.Error()) } // step5: update deploy event status return &apisv1.ApplicationDeployResponse{ ApplicationRevisionBase: apisv1.ApplicationRevisionBase{ - Version: deployEvent.Version, - Status: deployEvent.Status, - Reason: deployEvent.Reason, - DeployUser: deployEvent.DeployUser, - Note: deployEvent.Note, - TriggerType: deployEvent.TriggerType, + Version: appRevision.Version, + Status: appRevision.Status, + Reason: appRevision.Reason, + DeployUser: appRevision.DeployUser, + Note: appRevision.Note, + TriggerType: appRevision.TriggerType, }, }, nil } @@ -667,7 +681,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } traits = append(traits, aTrait) } - app.Spec.Components = append(app.Spec.Components, common.ApplicationComponent{ + bc := common.ApplicationComponent{ Name: component.Name, Type: component.Type, ExternalRevision: component.ExternalRevision, @@ -676,7 +690,12 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo Outputs: component.Outputs, Traits: traits, Scopes: component.Scopes, - }) + Properties: component.Properties.RawExtension(), + } + if component.Properties != nil { + bc.Properties = component.Properties.RawExtension() + } + app.Spec.Components = append(app.Spec.Components, bc) } for _, entity := range policies { @@ -750,7 +769,6 @@ func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *a } appBase.EnvBinding = append(appBase.EnvBinding, apiEnvBind) } - // TODO: get and render app status return appBase } @@ -939,6 +957,7 @@ func (c *applicationUsecaseImpl) UpdateApplicationEnvBinding( Components: envUpdate.ComponentSelector.Components, } } + } } properties, err := model.NewJSONStructByStruct(envBinding) @@ -1170,10 +1189,14 @@ func createModelEnvBind(envBind apisv1.EnvBinding) *model.EnvBinding { Name: envBind.Name, Description: envBind.Description, Alias: envBind.Alias, - //ClusterSelector: model.ClusterSelector(envBind.ClusterSelector), + TargetNames: envBind.TargetNames, } if envBind.ComponentSelector != nil { re.ComponentSelector = (*model.ComponentSelector)(envBind.ComponentSelector) } return &re } + +func converAppName(app *model.Application, envName string) string { + return fmt.Sprintf("%s-%s", app.Name, envName) +} diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 3a0eaf480..3e33399d3 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -483,7 +483,7 @@ var _ = Describe("Test application usecase function", func() { TriggerType: "api", }) Expect(err).Should(BeNil()) - Expect(cmp.Diff(res.Status, model.DeployEventRunning)).Should(BeEmpty()) + Expect(cmp.Diff(res.Status, model.RevisionStatusRunning)).Should(BeEmpty()) var oam v1beta1.Application err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: appModel.Name, Namespace: appModel.Namespace}, &oam) diff --git a/pkg/apiserver/rest/usecase/definition_test.go b/pkg/apiserver/rest/usecase/definition_test.go index c640ca42b..fc61e110c 100644 --- a/pkg/apiserver/rest/usecase/definition_test.go +++ b/pkg/apiserver/rest/usecase/definition_test.go @@ -149,8 +149,8 @@ var _ = Describe("Test namespace usecase functions", func() { fmt.Printf("%s=> %d", schema.JSONKey, schema.Sort) } Expect(cmp.Diff(len(uiSchema), 12)).Should(BeEmpty()) - Expect(cmp.Diff(uiSchema[3].JSONKey, "readinessProbe")).Should(BeEmpty()) - Expect(cmp.Diff(len(uiSchema[3].SubParameters), 8)).Should(BeEmpty()) + Expect(cmp.Diff(uiSchema[7].JSONKey, "readinessProbe")).Should(BeEmpty()) + Expect(cmp.Diff(len(uiSchema[7].SubParameters), 8)).Should(BeEmpty()) outdata, err := yaml.Marshal(uiSchema) Expect(err).Should(Succeed()) diff --git a/pkg/apiserver/rest/usecase/delivery_target.go b/pkg/apiserver/rest/usecase/delivery_target.go index de2db1bb1..8b0d470e4 100644 --- a/pkg/apiserver/rest/usecase/delivery_target.go +++ b/pkg/apiserver/rest/usecase/delivery_target.go @@ -34,7 +34,7 @@ type DeliveryTargetUsecase interface { DeleteDeliveryTarget(ctx context.Context, deliveryTargetName string) error CreateDeliveryTarget(ctx context.Context, req apisv1.CreateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) UpdateDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget, req apisv1.UpdateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) - ListDeliveryTargets(ctx context.Context, page, pageSize int) (*apisv1.ListDeliveryTargetResponse, error) + ListDeliveryTargets(ctx context.Context, page, pageSize int, namespace string) (*apisv1.ListDeliveryTargetResponse, error) } // NewDeliveryTargetUsecase new DeliveryTarget usecase @@ -46,8 +46,11 @@ type deliveryTargetUsecaseImpl struct { ds datastore.DataStore } -func (dt *deliveryTargetUsecaseImpl) ListDeliveryTargets(ctx context.Context, page, pageSize int) (*apisv1.ListDeliveryTargetResponse, error) { +func (dt *deliveryTargetUsecaseImpl) ListDeliveryTargets(ctx context.Context, page, pageSize int, namespace string) (*apisv1.ListDeliveryTargetResponse, error) { deliveryTarget := model.DeliveryTarget{} + if namespace != "" { + deliveryTarget.Namespace = namespace + } deliveryTargets, err := dt.ds.List(ctx, &deliveryTarget, &datastore.ListOptions{Page: page, PageSize: pageSize}) if err != nil { return nil, err @@ -72,9 +75,9 @@ func (dt *deliveryTargetUsecaseImpl) ListDeliveryTargets(ctx context.Context, pa } // DeleteDeliveryTarget delete application DeliveryTarget -func (dt *deliveryTargetUsecaseImpl) DeleteDeliveryTarget(ctx context.Context, DeliveryTargetName string) error { +func (dt *deliveryTargetUsecaseImpl) DeleteDeliveryTarget(ctx context.Context, deliveryTargetName string) error { deliveryTarget := &model.DeliveryTarget{ - Name: DeliveryTargetName, + Name: deliveryTargetName, } if err := dt.ds.Delete(ctx, deliveryTarget); err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { @@ -131,8 +134,8 @@ func (dt *deliveryTargetUsecaseImpl) GetDeliveryTarget(ctx context.Context, deli func convertUpdateReqToDeliveryTargetModel(deliveryTarget *model.DeliveryTarget, req apisv1.UpdateDeliveryTargetRequest) *model.DeliveryTarget { deliveryTarget.Alias = req.Alias deliveryTarget.Description = req.Description - deliveryTarget.Kubernetes = (*model.KubernetesTarget)(req.Kubernetes) - deliveryTarget.Cloud = (*model.CloudTarget)(req.Cloud) + deliveryTarget.Cluster = (*model.ClusterTarget)(req.Cluster) + deliveryTarget.Variable = req.Variable return deliveryTarget } @@ -142,8 +145,8 @@ func convertCreateReqToDeliveryTargetModel(req apisv1.CreateDeliveryTargetReques Namespace: req.Namespace, Alias: req.Alias, Description: req.Description, - Kubernetes: (*model.KubernetesTarget)(req.Kubernetes), - Cloud: (*model.CloudTarget)(req.Cloud), + Cluster: (*model.ClusterTarget)(req.Cluster), + Variable: req.Variable, } return deliveryTarget } @@ -154,8 +157,8 @@ func convertFromDeliveryTargetModel(deliveryTarget *model.DeliveryTarget) *apisv Namespace: deliveryTarget.Namespace, Alias: deliveryTarget.Alias, Description: deliveryTarget.Description, - Kubernetes: (*apisv1.KubernetesTarget)(deliveryTarget.Kubernetes), - Cloud: (*apisv1.CloudTarget)(deliveryTarget.Cloud), + Cluster: (*apisv1.ClusterTarget)(deliveryTarget.Cluster), + Variable: deliveryTarget.Variable, CreateTime: deliveryTarget.CreateTime, UpdateTime: deliveryTarget.UpdateTime, } diff --git a/pkg/apiserver/rest/usecase/delivery_target_test.go b/pkg/apiserver/rest/usecase/delivery_target_test.go index f42c60d64..78f466136 100644 --- a/pkg/apiserver/rest/usecase/delivery_target_test.go +++ b/pkg/apiserver/rest/usecase/delivery_target_test.go @@ -40,8 +40,8 @@ var _ = Describe("Test delivery target usecase functions", func() { Namespace: "test-namespace", Alias: "test-alias", Description: "this is a deliveryTarget", - Kubernetes: &apisv1.KubernetesTarget{ClusterName: "cluster-dev", Namespace: "dev"}, - Cloud: &apisv1.CloudTarget{TerraformProviderName: "provider", Region: "us-1"}, + Cluster: &apisv1.ClusterTarget{ClusterName: "cluster-dev", Namespace: "dev"}, + Variable: map[string]interface{}{"terraform-provider": "provider", "region": "us-1"}, } base, err := deliveryTargetUsecase.CreateDeliveryTarget(context.TODO(), req) Expect(err).Should(BeNil()) @@ -57,7 +57,7 @@ var _ = Describe("Test delivery target usecase functions", func() { }) It("Test ListDeliveryTargets function", func() { - _, err := deliveryTargetUsecase.ListDeliveryTargets(context.TODO(), 1, 1) + _, err := deliveryTargetUsecase.ListDeliveryTargets(context.TODO(), 1, 1, "") Expect(err).Should(BeNil()) }) @@ -68,8 +68,8 @@ var _ = Describe("Test delivery target usecase functions", func() { Namespace: "test-namespace", Alias: "test-alias", Description: "this is a deliveryTarget", - Kubernetes: &model.KubernetesTarget{ClusterName: "cluster-dev", Namespace: "dev"}, - Cloud: &model.CloudTarget{TerraformProviderName: "provider", Region: "us-1"}}) + Cluster: &model.ClusterTarget{ClusterName: "cluster-dev", Namespace: "dev"}, + Variable: map[string]interface{}{"terraform-provider": "provider", "region": "us-1"}}) Expect(err).Should(BeNil()) Expect(detail.Name).Should(Equal("test-delivery-target")) }) diff --git a/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml index cc65ecbdf..03bfd5081 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml @@ -1,34 +1,34 @@ -- description: Specify image pull policy for your service - disable: false - jsonKey: imagePullPolicy - label: 镜像更新策略 - uiType: Select - validate: - options: - - label: 镜像不存在时更新 - value: IfNotPresent - - label: 总是更新 - value: Always - - label: 永不更新 - value: Never - sort: 2 +- uiType: ImageInput + jsonKey: image + label: 服务镜像 + sort: 1 - description: Specifies the attributes of the memory resource required for the container. disable: false jsonKey: memory - label: Memory uiType: MemoryNumber sort: 3 + label: 分配内存 - uiType: CPUNumber jsonKey: cpu + sort: 5 + label: 分配CPU +- jsonKey: cmd + label: 启动参数 + sort: 7 - description: Define arguments by using environment variables disable: false jsonKey: env - label: Env + label: 环境变量 + sort: 9 subParameterGroupOption: - - - name - - value - - - name - - valueFrom + - label: Add By Value + keys: + - name + - value + - label: Add By Secret + keys: + - name + - valueFrom subParameters: - description: Specifies a source the value of this var should come from disable: false @@ -47,10 +47,31 @@ uiType: SecretKeySelect uiType: Structs validate: {} -- uiType: ImageInput - jsonKey: image - sort: 1 +- jsonKey: port + label: 端口设置 + sort: 10 +- jsonKey: volumes + label: 持久化存储 + sort: 11 - jsonKey: readinessProbe uiType: Group label: ReadinessProbe检测 - sort: 4 \ No newline at end of file + sort: 13 +- jsonKey: livenessProbe + uiType: Group + label: LivenessProbe检测 + sort: 15 +- description: Specify image pull policy for your service + disable: false + jsonKey: imagePullPolicy + label: 镜像更新策略 + uiType: Select + sort: 17 + validate: + options: + - label: 镜像不存在时更新 + value: IfNotPresent + - label: 总是更新 + value: Always + - label: 永不更新 + value: Never \ No newline at end of file diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index 29310f096..bc313008f 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -1,186 +1,44 @@ - description: Which image would you like to use for your service jsonKey: image - label: Image + label: 服务镜像 sort: 1 uiType: ImageInput validate: required: true -- description: Specify image pull policy for your service - disable: false - jsonKey: imagePullPolicy - label: 镜像更新策略 - sort: 2 - uiType: Select - validate: - options: - - label: 镜像不存在时更新 - value: IfNotPresent - - label: 总是更新 - value: Always - - label: 永不更新 - value: Never - description: Specifies the attributes of the memory resource required for the container. disable: false jsonKey: memory - label: Memory + label: 分配内存 sort: 3 uiType: MemoryNumber validate: {} -- description: Instructions for assessing whether the container is in a suitable state - to serve traffic. - jsonKey: readinessProbe - label: ReadinessProbe检测 - sort: 4 - subParameters: - - description: Number of consecutive failures required to determine the container - is not alive (liveness probe) or not ready (readiness probe). - jsonKey: failureThreshold - label: FailureThreshold - sort: 100 - uiType: Number - validate: - defaultValue: 3 - required: true - - description: Instructions for assessing container health by executing an HTTP - GET request. Either this attribute or the exec attribute or the tcpSocket attribute - MUST be specified. This attribute is mutually exclusive with both the exec attribute - and the tcpSocket attribute. - jsonKey: httpGet - label: HttpGet - sort: 100 - subParameters: - - description: "" - jsonKey: httpHeaders - label: HttpHeaders - sort: 100 - subParameters: - - description: "" - jsonKey: name - label: Name - sort: 100 - uiType: Input - validate: - required: true - - description: "" - jsonKey: value - label: Value - sort: 100 - uiType: Input - validate: - required: true - uiType: Structs - validate: {} - - description: The endpoint, relative to the port, to which the HTTP GET request - should be directed. - jsonKey: path - label: Path - sort: 100 - uiType: Input - validate: - required: true - - description: The TCP socket within the container to which the HTTP GET request - should be directed. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - uiType: KV - validate: {} - - description: Number of seconds after the container is started before the first - probe is initiated. - jsonKey: initialDelaySeconds - label: InitialDelaySeconds - sort: 100 - uiType: Number - validate: - defaultValue: 0 - required: true - - description: How often, in seconds, to execute the probe. - jsonKey: periodSeconds - label: PeriodSeconds - sort: 100 - uiType: Number - validate: - defaultValue: 10 - required: true - - description: Minimum consecutive successes for the probe to be considered successful - after having failed. - jsonKey: successThreshold - label: SuccessThreshold - sort: 100 - uiType: Number - validate: - defaultValue: 1 - required: true - - description: Instructions for assessing container health by probing a TCP socket. - Either this attribute or the exec attribute or the httpGet attribute MUST be - specified. This attribute is mutually exclusive with both the exec attribute - and the httpGet attribute. - jsonKey: tcpSocket - label: TcpSocket - sort: 100 - subParameters: - - description: The TCP socket within the container that should be probed to assess - container health. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - uiType: KV - validate: {} - - description: Number of seconds after which the probe times out. - jsonKey: timeoutSeconds - label: TimeoutSeconds - sort: 100 - uiType: Number - validate: - defaultValue: 1 - required: true - - description: Instructions for assessing container health by executing a command. - Either this attribute or the httpGet attribute or the tcpSocket attribute MUST - be specified. This attribute is mutually exclusive with both the httpGet attribute - and the tcpSocket attribute. - jsonKey: exec - label: Exec - sort: 100 - subParameters: - - description: A command to be executed inside the container to assess its health. - Each space delimited token of the command is a separate array element. Commands - exiting 0 are considered to be successful probes, whilst all other exit codes - are considered failures. - jsonKey: command - label: Command - sort: 100 - uiType: Strings - validate: - required: true - uiType: KV - validate: {} - uiType: Group +- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` + (1 CPU core) + jsonKey: cpu + label: 分配CPU + sort: 5 + uiType: CPUNumber + validate: {} +- description: Commands to run in the container + jsonKey: cmd + label: 启动参数 + sort: 7 + uiType: Strings validate: {} -- description: If addRevisionLabel is true, the appRevision label will be added to - the underlying pods - jsonKey: addRevisionLabel - label: AddRevisionLabel - sort: 100 - uiType: Switch - validate: - defaultValue: false - required: true - description: Define arguments by using environment variables disable: false jsonKey: env - label: Env - sort: 100 + label: 环境变量 + sort: 9 subParameterGroupOption: - - - name + - keys: + - name - value - - - name + label: Add By Value + - keys: + - name - valueFrom + label: Add By Secret subParameters: - description: Environment variable name jsonKey: name @@ -228,10 +86,191 @@ validate: {} uiType: Structs validate: {} +- description: Which port do you want customer traffic sent to + jsonKey: port + label: 端口设置 + sort: 10 + uiType: Number + validate: + defaultValue: 80 + required: true +- description: Declare volumes and volumeMounts + jsonKey: volumes + label: 持久化存储 + sort: 11 + subParameters: + - description: "" + jsonKey: mountPath + label: MountPath + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' + jsonKey: type + label: Type + sort: 100 + uiType: Select + validate: + options: + - label: Pvc + value: pvc + - label: ConfigMap + value: configMap + - label: Secret + value: secret + - label: EmptyDir + value: emptyDir + required: true + uiType: Structs + validate: {} +- description: Instructions for assessing whether the container is in a suitable state + to serve traffic. + jsonKey: readinessProbe + label: ReadinessProbe检测 + sort: 13 + subParameters: + - description: How often, in seconds, to execute the probe. + jsonKey: periodSeconds + label: PeriodSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 10 + required: true + - description: Minimum consecutive successes for the probe to be considered successful + after having failed. + jsonKey: successThreshold + label: SuccessThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} + - description: Number of seconds after which the probe times out. + jsonKey: timeoutSeconds + label: TimeoutSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by executing a command. + Either this attribute or the httpGet attribute or the tcpSocket attribute MUST + be specified. This attribute is mutually exclusive with both the httpGet attribute + and the tcpSocket attribute. + jsonKey: exec + label: Exec + sort: 100 + subParameters: + - description: A command to be executed inside the container to assess its health. + Each space delimited token of the command is a separate array element. Commands + exiting 0 are considered to be successful probes, whilst all other exit codes + are considered failures. + jsonKey: command + label: Command + sort: 100 + uiType: Strings + validate: + required: true + uiType: KV + validate: {} + - description: Number of consecutive failures required to determine the container + is not alive (liveness probe) or not ready (readiness probe). + jsonKey: failureThreshold + label: FailureThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 3 + required: true + - description: Instructions for assessing container health by executing an HTTP + GET request. Either this attribute or the exec attribute or the tcpSocket attribute + MUST be specified. This attribute is mutually exclusive with both the exec attribute + and the tcpSocket attribute. + jsonKey: httpGet + label: HttpGet + sort: 100 + subParameters: + - description: "" + jsonKey: httpHeaders + label: HttpHeaders + sort: 100 + subParameters: + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: value + label: Value + sort: 100 + uiType: Input + validate: + required: true + uiType: Structs + validate: {} + - description: The endpoint, relative to the port, to which the HTTP GET request + should be directed. + jsonKey: path + label: Path + sort: 100 + uiType: Input + validate: + required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} + - description: Number of seconds after the container is started before the first + probe is initiated. + jsonKey: initialDelaySeconds + label: InitialDelaySeconds + sort: 100 + uiType: Number + validate: + defaultValue: 0 + required: true + uiType: Group + validate: {} - description: Instructions for assessing whether the container is alive. jsonKey: livenessProbe - label: LivenessProbe - sort: 100 + label: LivenessProbe检测 + sort: 15 subParameters: - description: Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST @@ -270,14 +309,6 @@ label: HttpGet sort: 100 subParameters: - - description: The TCP socket within the container to which the HTTP GET request - should be directed. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - description: "" jsonKey: httpHeaders label: HttpHeaders @@ -307,6 +338,14 @@ uiType: Input validate: required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true uiType: KV validate: {} - description: Number of seconds after the container is started before the first @@ -361,69 +400,34 @@ validate: defaultValue: 1 required: true - uiType: KV - validate: {} -- description: Declare volumes and volumeMounts - jsonKey: volumes - label: Volumes - sort: 100 - subParameters: - - description: "" - jsonKey: name - label: Name - sort: 100 - uiType: Input - validate: - required: true - - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' - jsonKey: type - label: Type - sort: 100 - uiType: Select - validate: - options: - - label: Pvc - value: pvc - - label: ConfigMap - value: configMap - - label: Secret - value: secret - - label: EmptyDir - value: emptyDir - required: true - - description: "" - jsonKey: mountPath - label: MountPath - sort: 100 - uiType: Input - validate: - required: true - uiType: Structs - validate: {} -- description: Commands to run in the container - jsonKey: cmd - label: Cmd - sort: 100 - uiType: Strings - validate: {} -- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` - (1 CPU core) - jsonKey: cpu - label: Cpu - sort: 100 - uiType: CPUNumber + uiType: Group validate: {} +- description: Specify image pull policy for your service + disable: false + jsonKey: imagePullPolicy + label: 镜像更新策略 + sort: 17 + uiType: Select + validate: + options: + - label: 镜像不存在时更新 + value: IfNotPresent + - label: 总是更新 + value: Always + - label: 永不更新 + value: Never - description: Specify image pull secrets for your service jsonKey: imagePullSecrets label: ImagePullSecrets sort: 100 uiType: Strings validate: {} -- description: Which port do you want customer traffic sent to - jsonKey: port - label: Port +- description: If addRevisionLabel is true, the appRevision label will be added to + the underlying pods + jsonKey: addRevisionLabel + label: AddRevisionLabel sort: 100 - uiType: Number + uiType: Switch validate: - defaultValue: 80 + defaultValue: false required: true diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index eda2d996e..1137ad143 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -111,7 +111,7 @@ var _ = Describe("Test workflow usecase functions", func() { var deployEvent = &model.ApplicationRevision{ AppPrimaryKey: "test", Version: "123", - Status: model.DeployEventInit, + Status: model.RevisionStatusInit, DeployUser: "test-user", Note: "test-commit", TriggerType: "API", diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go index cd1d9a200..e57f23abe 100644 --- a/pkg/apiserver/rest/utils/bcode/addon.go +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -64,6 +64,7 @@ func WrapGithubRateLimitErr(err error) error { return err } +// NewBcodeWrapErr new bcode error func NewBcodeWrapErr(httpCode, businessCode int32, err error, message string) error { return NewBcode(httpCode, businessCode, errors.Wrap(err, message).Error()) } diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index 03dc6767d..db8d27e22 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -28,8 +28,8 @@ var ErrApplicationExist = NewBcode(400, 10002, "application name is exist") // ErrInvalidProperties properties(trait or component or others) is invalid var ErrInvalidProperties = NewBcode(400, 10003, "properties is invalid") -// ErrDeployEventConflict Occurs when a new event is triggered before the last deployment event has completed. -var ErrDeployEventConflict = NewBcode(400, 10004, "application deploy event conflict") +// ErrDeployConflict Occurs when a new event is triggered before the last deployment event has completed. +var ErrDeployConflict = NewBcode(400, 10004, "application deploy conflict") // ErrDeployApplyFail Failed to update an application to the control cluster. var ErrDeployApplyFail = NewBcode(500, 10005, "application deploy apply failure") diff --git a/pkg/apiserver/rest/utils/uiswagger.go b/pkg/apiserver/rest/utils/uiswagger.go index b58b5622d..c635382d5 100644 --- a/pkg/apiserver/rest/utils/uiswagger.go +++ b/pkg/apiserver/rest/utils/uiswagger.go @@ -31,10 +31,16 @@ type UIParameter struct { UIType string `json:"uiType"` // means only can be read. Disable *bool `json:"disable,omitempty"` - SubParameterGroupOption [][]string `json:"subParameterGroupOption,omitempty"` + SubParameterGroupOption []GroupOption `json:"subParameterGroupOption,omitempty"` SubParameters []*UIParameter `json:"subParameters,omitempty"` } +// GroupOption define multiple data structure composition options. +type GroupOption struct { + Label string `json:"label"` + Keys []string `json:"keys"` +} + // Validate parameter validate rule type Validate struct { Required bool `json:"required,omitempty"` diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 78a6c6e22..6a36ffba9 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -107,6 +107,16 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.EnvBinding{})) + ws.Route(ws.GET("/{name}/envs/{envName}/status").To(c.getApplicationStatus). + Doc("get application status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string")). + Returns(200, "", apis.ApplicationStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationStatusResponse{})) + ws.Route(ws.POST("/{name}/envs").To(c.createApplicationEnv). Doc("creating an application environment "). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -123,7 +133,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Filter(c.appCheckFilter). Filter(c.envCheckFilter). Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). - Param(ws.PathParameter("envName", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string")). Returns(200, "", apis.EmptyResponse{}). Returns(404, "", bcode.Bcode{}). Writes(apis.EmptyResponse{})) @@ -671,3 +681,17 @@ func (c *applicationWebService) deleteApplicationTrait(req *restful.Request, res return } } + +func (c *applicationWebService) getApplicationStatus(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + status, err := c.applicationUsecase.GetApplicationStatus(req.Request.Context(), app, req.PathParameter("envName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(apis.ApplicationStatusResponse{Status: status}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/delivery_target.go b/pkg/apiserver/rest/webservice/delivery_target.go index c7b21d18b..17e6fcdb4 100644 --- a/pkg/apiserver/rest/webservice/delivery_target.go +++ b/pkg/apiserver/rest/webservice/delivery_target.go @@ -37,10 +37,12 @@ func NewDeliveryTargetWebService(deliveryTargetUsecase usecase.DeliveryTargetUse } } +// DeliveryTargetWebService delivery target web service type DeliveryTargetWebService struct { deliveryTargetUsecase usecase.DeliveryTargetUsecase } +// GetWebService get web service func (dt *DeliveryTargetWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) ws.Path(versionPrefix+"/deliveryTargets"). @@ -53,6 +55,7 @@ func (dt *DeliveryTargetWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(dt.listDeliveryTargets). Doc("list deliveryTarget"). Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("namesapce", "Query the delivery target belonging to a namespace").DataType("string")). Returns(200, "", apis.ListDeliveryTargetResponse{}). Writes(apis.ListDeliveryTargetResponse{}).Do(returns200, returns500)) @@ -181,7 +184,7 @@ func (dt *DeliveryTargetWebService) listDeliveryTargets(req *restful.Request, re return } - deliveryTargets, err := dt.deliveryTargetUsecase.ListDeliveryTargets(req.Request.Context(), page, pageSize) + deliveryTargets, err := dt.deliveryTargetUsecase.ListDeliveryTargets(req.Request.Context(), page, pageSize, req.QueryParameter("namespace")) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index 3f3e93b5b..48387fab7 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -175,7 +175,7 @@ var _ = Describe("Test application rest api", func() { var response apisv1.ApplicationDeployResponse err = json.NewDecoder(res.Body).Decode(&response) Expect(err).ShouldNot(HaveOccurred()) - Expect(cmp.Diff(response.Status, model.DeployEventRunning)).Should(BeEmpty()) + Expect(cmp.Diff(response.Status, model.RevisionStatusRunning)).Should(BeEmpty()) var oam v1beta1.Application err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: "test-app-sadasd", Namespace: "test-app-namespace"}, &oam) From c42ea7c948ff3058ba44acb540d8bdcbf814066d Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Tue, 16 Nov 2021 14:47:13 +0800 Subject: [PATCH 38/59] Feat: add record status sync (#2627) * Feat: add record status sync * fix typo * optimize the code * fix the port * fix go mod * fix rebase --- cmd/apiserver/main.go | 14 ++- go.mod | 1 + pkg/apiserver/model/application.go | 3 + pkg/apiserver/rest/rest_server.go | 80 ++++++++++++++++ pkg/apiserver/rest/usecase/workflow.go | 100 ++++++++++++++++++-- pkg/apiserver/rest/usecase/workflow_test.go | 75 +++++++++++---- pkg/workflow/workflow.go | 7 ++ 7 files changed, 251 insertions(+), 29 deletions(-) diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 4e9714ddf..6b5f253cf 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -24,10 +24,12 @@ import ( "os" "os/signal" "syscall" + "time" restfulspec "github.com/emicklei/go-restful-openapi/v2" "github.com/go-openapi/spec" + "github.com/google/uuid" "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/rest" "github.com/oam-dev/kubevela/version" @@ -40,6 +42,9 @@ func main() { flag.StringVar(&s.restCfg.Datastore.Type, "datastore-type", "kubeapi", "Metadata storage driver type, support kubeapi and mongodb") flag.StringVar(&s.restCfg.Datastore.Database, "datastore-database", "kubevela", "Metadata storage database name, takes effect when the storage driver is mongodb.") flag.StringVar(&s.restCfg.Datastore.URL, "datastore-url", "", "Metadata storage database url,takes effect when the storage driver is mongodb.") + flag.StringVar(&s.restCfg.LeaderConfig.ID, "id", uuid.New().String(), "the holder identity name") + flag.StringVar(&s.restCfg.LeaderConfig.LockName, "lock-name", "apiserver-lock", "the lease lock resource name") + flag.DurationVar(&s.restCfg.LeaderConfig.Duration, "duration", time.Second*5, "the lease lock resource name") flag.Parse() if len(os.Args) > 2 && os.Args[1] == "build-swagger" { @@ -71,9 +76,10 @@ func main() { } srvc := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) go func() { - if err := s.run(); err != nil { + if err := s.run(ctx); err != nil { log.Logger.Errorf("failed to run apiserver: %v", err) } close(srvc) @@ -84,7 +90,9 @@ func main() { select { case <-term: log.Logger.Infof("Received SIGTERM, exiting gracefully...") + cancel() case <-srvc: + cancel() os.Exit(1) } log.Logger.Infof("See you next time!") @@ -95,11 +103,9 @@ type Server struct { restCfg rest.Config } -func (s *Server) run() error { +func (s *Server) run(ctx context.Context) error { log.Logger.Infof("KubeVela information: version: %v, gitRevision: %v", version.VelaVersion, version.GitRevision) - ctx := context.Background() - server, err := rest.New(s.restCfg) if err != nil { return fmt.Errorf("create apiserver failed : %w ", err) diff --git a/go.mod b/go.mod index 61d4e26d7..41b2d9728 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/go-playground/validator/v10 v10.9.0 github.com/google/go-cmp v0.5.6 github.com/google/go-github/v32 v32.1.0 + github.com/google/uuid v1.1.2 github.com/gosuri/uitable v0.0.4 github.com/hashicorp/hcl/v2 v2.9.1 github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index bf37fadfd..4a9d88af9 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -188,6 +188,9 @@ var RevisionStatusComplete = "complete" // RevisionStatusFail event status failure var RevisionStatusFail = "failure" +// DeployEventTerminated event status terminated +var DeployEventTerminated = "terminated" + // ApplicationRevision be created when an application initiates deployment and describes the phased version of the application. type ApplicationRevision struct { Model diff --git a/pkg/apiserver/rest/rest_server.go b/pkg/apiserver/rest/rest_server.go index 8ee324af0..2c74cc86d 100644 --- a/pkg/apiserver/rest/rest_server.go +++ b/pkg/apiserver/rest/rest_server.go @@ -20,15 +20,23 @@ import ( "context" "fmt" "net/http" + "os" + "time" restfulspec "github.com/emicklei/go-restful-openapi/v2" "github.com/emicklei/go-restful/v3" "github.com/go-openapi/spec" + "k8s.io/client-go/tools/leaderelection" + "k8s.io/client-go/tools/leaderelection/resourcelock" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/datastore/kubeapi" "github.com/oam-dev/kubevela/pkg/apiserver/datastore/mongodb" "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" "github.com/oam-dev/kubevela/pkg/apiserver/rest/webservice" ) @@ -43,6 +51,15 @@ type Config struct { // Datastore config Datastore datastore.Config + + // LeaderConfig for leader election + LeaderConfig leaderConfig +} + +type leaderConfig struct { + ID string + LockName string + Duration time.Duration } // APIServer interface for call api server @@ -85,9 +102,72 @@ func New(cfg Config) (a APIServer, err error) { func (s *restServer) Run(ctx context.Context) error { s.RegisterServices() + + l, err := s.setupLeaderElection() + if err != nil { + return err + } + + go func() { + leaderelection.RunOrDie(ctx, *l) + }() + return s.startHTTP(ctx) } +func (s *restServer) setupLeaderElection() (*leaderelection.LeaderElectionConfig, error) { + restCfg := ctrl.GetConfigOrDie() + + rl, err := resourcelock.NewFromKubeconfig(resourcelock.LeasesResourceLock, types.DefaultKubeVelaNS, s.cfg.LeaderConfig.LockName, resourcelock.ResourceLockConfig{ + Identity: s.cfg.LeaderConfig.ID, + }, restCfg, time.Second*10) + if err != nil { + klog.ErrorS(err, "Unable to setup the resource lock") + return nil, err + } + + return &leaderelection.LeaderElectionConfig{ + Lock: rl, + LeaseDuration: time.Second * 15, + RenewDeadline: time.Second * 10, + RetryPeriod: time.Second * 2, + Callbacks: leaderelection.LeaderCallbacks{ + OnStartedLeading: func(ctx context.Context) { + s.runLeader(ctx, s.cfg.LeaderConfig.Duration) + }, + OnStoppedLeading: func() { + klog.Infof("leader lost: %s", s.cfg.LeaderConfig.ID) + os.Exit(0) + }, + OnNewLeader: func(identity string) { + if identity == s.cfg.LeaderConfig.ID { + return + } + klog.Infof("new leader elected: %s", identity) + }, + }, + ReleaseOnCancel: true, + }, nil +} + +func (s restServer) runLeader(ctx context.Context, duration time.Duration) { + w := usecase.NewWorkflowUsecase(s.dataStore) + + t := time.NewTicker(duration) + defer t.Stop() + + for { + select { + case <-t.C: + if err := w.SyncWorkflowRecord(ctx); err != nil { + klog.ErrorS(err, "syncWorkflowRecordError") + } + case <-ctx.Done(): + return + } + } +} + // RegisterServices register web service func (s *restServer) RegisterServices() restfulspec.Config { webservice.Init(s.dataStore) diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index c411ffff2..3a876e348 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -23,7 +23,12 @@ import ( "strings" appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" @@ -31,6 +36,11 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/workflow/recorder" +) + +const ( + labelControllerRevisionSync = "apiserver.oam.dev/cr-sync" ) // WorkflowUsecase workflow manage api @@ -44,15 +54,24 @@ type WorkflowUsecase interface { UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) ListWorkflowRecords(ctx context.Context, workflowName string, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) DetailWorkflowRecord(ctx context.Context, workflowName, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) + SyncWorkflowRecord(ctx context.Context) error } // NewWorkflowUsecase new workflow usecase func NewWorkflowUsecase(ds datastore.DataStore) WorkflowUsecase { - return &workflowUsecaseImpl{ds: ds} + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + return &workflowUsecaseImpl{ + ds: ds, + kubeClient: kubecli, + } } type workflowUsecaseImpl struct { - ds datastore.DataStore + ds datastore.DataStore + kubeClient client.Client } // DeleteWorkflow delete application workflow @@ -261,11 +280,80 @@ func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflow }, nil } -func (w *workflowUsecaseImpl) createWorkflowRecord(ctx context.Context, revision *appsv1.ControllerRevision) error { - app, err := util.RawExtension2Application(revision.Data) +func (w *workflowUsecaseImpl) SyncWorkflowRecord(ctx context.Context) error { + crList := &appsv1.ControllerRevisionList{} + matchLabels := metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: labelControllerRevisionSync, + Operator: metav1.LabelSelectorOpDoesNotExist, + }, + { + Key: recorder.LabelRecordVersion, + Operator: metav1.LabelSelectorOpExists, + }, + }, + } + selector, err := metav1.LabelSelectorAsSelector(&matchLabels) if err != nil { return err } + if err := w.kubeClient.List(ctx, crList, &client.ListOptions{ + LabelSelector: selector, + }); err != nil { + return err + } + + for i, cr := range crList.Items { + app, err := util.RawExtension2Application(cr.Data) + if err != nil { + klog.ErrorS(err, "failed to get app data", "controller revision name", cr.Name) + continue + } + + if err := w.createWorkflowRecord(ctx, app, strings.TrimPrefix(cr.Name, "record-")); err != nil && !errors.Is(err, datastore.ErrRecordExist) { + klog.ErrorS(err, "failed to create workflow record", "controller revision name", cr.Name) + continue + } + + err = w.updateRecordApplicationRevisionStatus(ctx, app.Name, strings.TrimPrefix(cr.Name, fmt.Sprintf("record-%s-", app.Name)), app.Status.Workflow.Terminated) + if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { + klog.ErrorS(err, "failed to update deploy event status", "controller revision name", cr.Name) + continue + } + + crList.Items[i].Labels[labelControllerRevisionSync] = "true" + if err := w.kubeClient.Update(ctx, &crList.Items[i]); err != nil { + klog.ErrorS(err, "failed to update annotation", "controller revision name", cr.Name) + continue + } + } + + return nil +} + +func (w *workflowUsecaseImpl) updateRecordApplicationRevisionStatus(ctx context.Context, appPrimaryKey, version string, terminated bool) error { + var applicationRevision = &model.ApplicationRevision{ + AppPrimaryKey: appPrimaryKey, + Version: version, + } + if err := w.ds.Get(ctx, applicationRevision); err != nil { + return err + } + if terminated { + applicationRevision.Status = model.DeployEventTerminated + } else { + applicationRevision.Status = model.DeployEventComplete + } + + if err := w.ds.Put(ctx, applicationRevision); err != nil { + return err + } + + return nil +} + +func (w *workflowUsecaseImpl) createWorkflowRecord(ctx context.Context, app *v1beta1.Application, revisionName string) error { if app.Annotations == nil || app.Annotations[oam.AnnotationWorkflowName] == "" { return fmt.Errorf("missing workflow name") } @@ -274,8 +362,8 @@ func (w *workflowUsecaseImpl) createWorkflowRecord(ctx context.Context, revision return w.ds.Add(ctx, &model.WorkflowRecord{ WorkflowPrimaryKey: app.Annotations[oam.AnnotationWorkflowName], AppPrimaryKey: app.Name, - Name: strings.TrimPrefix(revision.Name, "record-"), - Namespace: revision.Namespace, + Name: strings.TrimPrefix(revisionName, "record-"), + Namespace: app.Namespace, StartTime: status.StartTime.Time, Suspend: status.Suspend, Terminated: status.Terminated, diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 1137ad143..08e645c3f 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -18,6 +18,7 @@ package usecase import ( "context" + "encoding/json" "fmt" "github.com/google/go-cmp/cmp" @@ -28,6 +29,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/yaml" + "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/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" ) @@ -37,7 +40,7 @@ var _ = Describe("Test workflow usecase functions", func() { workflowUsecase *workflowUsecaseImpl ) BeforeEach(func() { - workflowUsecase = &workflowUsecaseImpl{ds: ds} + workflowUsecase = &workflowUsecaseImpl{ds: ds, kubeClient: k8sClient} }) It("Test CreateWorkflow function", func() { req := apisv1.CreateWorkflowRequest{ @@ -75,16 +78,11 @@ var _ = Describe("Test workflow usecase functions", func() { By("create some controller revisions to test list workflow records") raw, err := yaml.YAMLToJSON([]byte(yamlStr)) Expect(err).Should(BeNil()) + app := &v1beta1.Application{} + err = json.Unmarshal(raw, app) + Expect(err).Should(BeNil()) for i := 0; i < 3; i++ { - err := workflowUsecase.createWorkflowRecord(context.TODO(), &appsv1.ControllerRevision{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("record-test-%v", i), - Namespace: "default", - }, - Data: runtime.RawExtension{ - Raw: raw, - }, - }) + err := workflowUsecase.createWorkflowRecord(context.TODO(), app, fmt.Sprintf("record-test-%v", i)) Expect(err).Should(BeNil()) } @@ -97,15 +95,10 @@ var _ = Describe("Test workflow usecase functions", func() { By("create one controller revision to test detail workflow record") raw, err := yaml.YAMLToJSON([]byte(yamlStr)) Expect(err).Should(BeNil()) - err = workflowUsecase.createWorkflowRecord(context.TODO(), &appsv1.ControllerRevision{ - ObjectMeta: metav1.ObjectMeta{ - Name: "record-test-123", - Namespace: "default", - }, - Data: runtime.RawExtension{ - Raw: raw, - }, - }) + app := &v1beta1.Application{} + err = json.Unmarshal(raw, app) + Expect(err).Should(BeNil()) + err = workflowUsecase.createWorkflowRecord(context.TODO(), app, "record-test-123") Expect(err).Should(BeNil()) var deployEvent = &model.ApplicationRevision{ @@ -126,6 +119,50 @@ var _ = Describe("Test workflow usecase functions", func() { Expect(detail.WorkflowRecord.Name).Should(Equal("test-123")) Expect(detail.DeployUser).Should(Equal("test-user")) }) + + It("Test SyncWorkflowRecord function", func() { + By("create one controller revision to test sync workflow record") + ctx := context.Background() + raw, err := yaml.YAMLToJSON([]byte(yamlStr)) + Expect(err).Should(BeNil()) + cr := &appsv1.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "record-test-1234", + Namespace: "default", + Labels: map[string]string{"vela.io/wf-revision": "1234"}, + }, + Data: runtime.RawExtension{Raw: raw}, + } + err = workflowUsecase.kubeClient.Create(ctx, cr) + Expect(err).Should(BeNil()) + + By("create one deploy event to test sync workflow record") + var deployEvent = &model.ApplicationRevision{ + AppPrimaryKey: "test", + Version: "1234", + Status: model.DeployEventInit, + DeployUser: "test-user", + WorkflowName: "test-workflow-name", + } + + err = workflowUsecase.createTestApplicationRevision(context.TODO(), deployEvent) + Expect(err).Should(BeNil()) + + err = workflowUsecase.SyncWorkflowRecord(ctx) + Expect(err).Should(BeNil()) + + By("check the record") + app := &v1beta1.Application{} + err = json.Unmarshal(raw, app) + Expect(err).Should(BeNil()) + err = workflowUsecase.createWorkflowRecord(context.TODO(), app, "test-1234") + Expect(err).Should(Equal(datastore.ErrRecordExist)) + + By("check the deploy event") + err = workflowUsecase.ds.Get(ctx, deployEvent) + Expect(err).Should(BeNil()) + Expect(deployEvent.Status).Should(Equal(model.DeployEventComplete)) + }) }) var yamlStr = `apiVersion: core.oam.dev/v1beta1 diff --git a/pkg/workflow/workflow.go b/pkg/workflow/workflow.go index 2b862591d..fa83b2f7c 100644 --- a/pkg/workflow/workflow.go +++ b/pkg/workflow/workflow.go @@ -30,6 +30,7 @@ import ( oamcore "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/controller/utils" "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" "github.com/oam-dev/kubevela/pkg/workflow/recorder" @@ -131,6 +132,12 @@ func (w *workflow) ExecuteSteps(ctx context.Context, appRev *oamcore.Application // Trace record the workflow execute history. func (w *workflow) Trace() error { + // add annotation for apiserver sync + if w.app.Annotations == nil { + w.app.Annotations = make(map[string]string) + } + w.app.Annotations[oam.AnnotationWorkflowName] = w.app.Name + data, err := json.Marshal(w.app) if err != nil { return err From 1465aba1778d13701f13bfc0b64e257d7a68b0be Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Tue, 16 Nov 2021 21:31:24 +0800 Subject: [PATCH 39/59] Fix: fix build bug (#2724) * Fix: fix build bug * Fix: change api spec Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 102 +++++++++++------- pkg/apiserver/model/application.go | 4 +- pkg/apiserver/model/deliverytarget.go | 2 +- pkg/apiserver/rest/apis/v1/types.go | 6 +- pkg/apiserver/rest/usecase/workflow.go | 4 +- pkg/apiserver/rest/usecase/workflow_test.go | 4 +- .../rest/webservice/delivery_target.go | 4 +- 7 files changed, 74 insertions(+), 52 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 046ae2f77..8afb9ddf0 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -1945,6 +1945,26 @@ ], "summary": "list deliveryTarget", "operationId": "listDeliveryTargets", + "parameters": [ + { + "type": "string", + "description": "Query the delivery target belonging to a namespace", + "name": "namesapce", + "in": "query" + }, + { + "type": "integer", + "description": "Page for paging", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "PageSize for paging", + "name": "pageSize", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -3758,12 +3778,12 @@ }, "v1.ApplicationDeployResponse": { "required": [ - "reason", "deployUser", "note", "triggerType", "version", - "status" + "status", + "reason" ], "properties": { "deployUser": { @@ -4466,12 +4486,12 @@ "alias": { "type": "string" }, + "cluster": { + "$ref": "#/definitions/v1.ClusterTarget" + }, "description": { "type": "string" }, - "kubernetes": { - "$ref": "#/definitions/v1.ClusterTarget" - }, "name": { "type": "string" }, @@ -4578,6 +4598,9 @@ "alias": { "type": "string" }, + "cluster": { + "$ref": "#/definitions/v1.ClusterTarget" + }, "createTime": { "type": "string", "format": "date-time" @@ -4585,9 +4608,6 @@ "description": { "type": "string" }, - "kubernetes": { - "$ref": "#/definitions/v1.ClusterTarget" - }, "name": { "type": "string" }, @@ -4659,13 +4679,13 @@ }, "v1.DetailApplicationResponse": { "required": [ - "name", - "alias", - "description", "icon", - "namespace", + "description", "createTime", "updateTime", + "name", + "alias", + "namespace", "policies", "status", "resourceInfo", @@ -4729,20 +4749,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "labels", - "apiServerURL", - "reason", - "dashboardURL", - "description", - "status", - "alias", - "provider", "kubeConfigSecret", - "createTime", - "name", - "kubeConfig", - "updateTime", + "description", "icon", + "provider", + "dashboardURL", + "labels", + "status", + "apiServerURL", + "kubeConfig", + "createTime", + "updateTime", + "alias", + "reason", + "name", "resourceInfo" ], "properties": { @@ -4802,11 +4822,11 @@ "required": [ "createTime", "name", - "appPrimaryKey", - "updateTime", - "creator", "alias", - "type" + "type", + "creator", + "updateTime", + "appPrimaryKey" ], "properties": { "alias": { @@ -4910,6 +4930,9 @@ "alias": { "type": "string" }, + "cluster": { + "$ref": "#/definitions/v1.ClusterTarget" + }, "createTime": { "type": "string", "format": "date-time" @@ -4917,9 +4940,6 @@ "description": { "type": "string" }, - "kubernetes": { - "$ref": "#/definitions/v1.ClusterTarget" - }, "name": { "type": "string" }, @@ -4937,13 +4957,13 @@ }, "v1.DetailPolicyResponse": { "required": [ - "updateTime", "name", "type", "description", "creator", "properties", - "createTime" + "createTime", + "updateTime" ], "properties": { "createTime": { @@ -5022,13 +5042,13 @@ }, "v1.DetailWorkflowResponse": { "required": [ + "default", + "createTime", "updateTime", "name", "alias", "description", "enable", - "default", - "createTime", "workflowRecord" ], "properties": { @@ -5329,10 +5349,10 @@ }, "v1.NamespaceDetailResponse": { "required": [ - "createTime", - "updateTime", "name", - "description" + "description", + "createTime", + "updateTime" ], "properties": { "createTime": { @@ -5510,12 +5530,12 @@ "alias": { "type": "string" }, + "cluster": { + "$ref": "#/definitions/v1.ClusterTarget" + }, "description": { "type": "string" }, - "kubernetes": { - "$ref": "#/definitions/v1.ClusterTarget" - }, "variable": { "type": "object" } diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 4a9d88af9..8ea19a341 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -188,8 +188,8 @@ var RevisionStatusComplete = "complete" // RevisionStatusFail event status failure var RevisionStatusFail = "failure" -// DeployEventTerminated event status terminated -var DeployEventTerminated = "terminated" +// RevisionStatusTerminated event status terminated +var RevisionStatusTerminated = "terminated" // ApplicationRevision be created when an application initiates deployment and describes the phased version of the application. type ApplicationRevision struct { diff --git a/pkg/apiserver/model/deliverytarget.go b/pkg/apiserver/model/deliverytarget.go index 158dbe1e3..78d98acef 100644 --- a/pkg/apiserver/model/deliverytarget.go +++ b/pkg/apiserver/model/deliverytarget.go @@ -28,7 +28,7 @@ type DeliveryTarget struct { Namespace string `json:"namespace"` Alias string `json:"alias,omitempty"` Description string `json:"description,omitempty"` - Cluster *ClusterTarget `json:"kubernetes,omitempty"` + Cluster *ClusterTarget `json:"cluster,omitempty"` Variable map[string]interface{} `json:"variable,omitempty"` } diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index ad011b622..92d73a352 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -631,7 +631,7 @@ type CreateDeliveryTargetRequest struct { Namespace string `json:"namespace" validate:"checkname"` Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` Description string `json:"description,omitempty" optional:"true"` - Cluster *ClusterTarget `json:"kubernetes,omitempty"` + Cluster *ClusterTarget `json:"cluster,omitempty"` Variable map[string]interface{} `json:"variable,omitempty"` } @@ -639,7 +639,7 @@ type CreateDeliveryTargetRequest struct { type UpdateDeliveryTargetRequest struct { Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` Description string `json:"description,omitempty" optional:"true"` - Cluster *ClusterTarget `json:"kubernetes,omitempty"` + Cluster *ClusterTarget `json:"cluster,omitempty"` Variable map[string]interface{} `json:"variable,omitempty"` } @@ -666,7 +666,7 @@ type DeliveryTargetBase struct { Namespace string `json:"namespace" validate:"checkname"` Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` Description string `json:"description,omitempty" optional:"true"` - Cluster *ClusterTarget `json:"kubernetes,omitempty"` + Cluster *ClusterTarget `json:"cluster,omitempty"` Variable map[string]interface{} `json:"variable,omitempty"` CreateTime time.Time `json:"createTime"` UpdateTime time.Time `json:"updateTime"` diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 3a876e348..63ef8febd 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -341,9 +341,9 @@ func (w *workflowUsecaseImpl) updateRecordApplicationRevisionStatus(ctx context. return err } if terminated { - applicationRevision.Status = model.DeployEventTerminated + applicationRevision.Status = model.RevisionStatusTerminated } else { - applicationRevision.Status = model.DeployEventComplete + applicationRevision.Status = model.RevisionStatusComplete } if err := w.ds.Put(ctx, applicationRevision); err != nil { diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 08e645c3f..76cefc920 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -140,7 +140,7 @@ var _ = Describe("Test workflow usecase functions", func() { var deployEvent = &model.ApplicationRevision{ AppPrimaryKey: "test", Version: "1234", - Status: model.DeployEventInit, + Status: model.RevisionStatusInit, DeployUser: "test-user", WorkflowName: "test-workflow-name", } @@ -161,7 +161,7 @@ var _ = Describe("Test workflow usecase functions", func() { By("check the deploy event") err = workflowUsecase.ds.Get(ctx, deployEvent) Expect(err).Should(BeNil()) - Expect(deployEvent.Status).Should(Equal(model.DeployEventComplete)) + Expect(deployEvent.Status).Should(Equal(model.RevisionStatusComplete)) }) }) diff --git a/pkg/apiserver/rest/webservice/delivery_target.go b/pkg/apiserver/rest/webservice/delivery_target.go index 17e6fcdb4..8168cbd53 100644 --- a/pkg/apiserver/rest/webservice/delivery_target.go +++ b/pkg/apiserver/rest/webservice/delivery_target.go @@ -55,7 +55,9 @@ func (dt *DeliveryTargetWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/").To(dt.listDeliveryTargets). Doc("list deliveryTarget"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namesapce", "Query the delivery target belonging to a namespace").DataType("string")). + Param(ws.QueryParameter("namesapce", "Query the delivery target belonging to a namespace").DataType("string")). + Param(ws.QueryParameter("page", "Page for paging").DataType("integer")). + Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("integer")). Returns(200, "", apis.ListDeliveryTargetResponse{}). Writes(apis.ListDeliveryTargetResponse{}).Do(returns200, returns500)) From 0555623b3e80d4b8c601396b4eb77c1d31a5154a Mon Sep 17 00:00:00 2001 From: wyike Date: Wed, 17 Nov 2021 13:06:30 +0800 Subject: [PATCH 40/59] Feat: add triat alias description time (#2727) * Feat: add triat alias description time * Fix: remove modle --- pkg/apiserver/model/application.go | 3 +++ pkg/apiserver/rest/apis/v1/types.go | 2 ++ pkg/apiserver/rest/usecase/application.go | 12 +++++++----- pkg/apiserver/rest/usecase/application_test.go | 18 +++++++++++++++--- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 8ea19a341..00dbb2779 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -18,6 +18,7 @@ package model import ( "fmt" + "time" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" ) @@ -174,6 +175,8 @@ type ApplicationTrait struct { Description string `json:"description"` Type string `json:"type"` Properties *JSONStruct `json:"properties,omitempty"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // RevisionStatusInit event status init diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 92d73a352..9ea42175f 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -623,6 +623,8 @@ type ApplicationTrait struct { Description string `json:"description,omitempty"` // Properties json data Properties *model.JSONStruct `json:"properties"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // CreateDeliveryTargetRequest create delivery target request body diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index fb3c87308..92d83e76d 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -23,6 +23,7 @@ import ( "fmt" "sort" "strings" + "time" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -1115,12 +1116,12 @@ func (c *applicationUsecaseImpl) CreateApplicationTrait(ctx context.Context, app log.Logger.Errorf("new trait failure,%s", err.Error()) return nil, bcode.ErrInvalidProperties } - trait := model.ApplicationTrait{Type: req.Type, Properties: properties} + trait := model.ApplicationTrait{CreateTime: time.Now(), Type: req.Type, Properties: properties, Alias: req.Alias, Description: req.Description} comp.Traits = append(comp.Traits, trait) if err := c.ds.Put(ctx, &comp); err != nil { return nil, err } - return &apisv1.ApplicationTrait{Type: trait.Type, Properties: properties}, nil + return &apisv1.ApplicationTrait{Type: trait.Type, Properties: properties, Alias: req.Alias, Description: req.Description, CreateTime: trait.CreateTime}, nil } func (c *applicationUsecaseImpl) DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error { @@ -1158,12 +1159,13 @@ func (c *applicationUsecaseImpl) UpdateApplicationTrait(ctx context.Context, app log.Logger.Errorf("update trait failure,%s", err.Error()) return nil, bcode.ErrInvalidProperties } - trait.Properties = properties - comp.Traits[i] = trait + updatedTrait := model.ApplicationTrait{CreateTime: trait.CreateTime, UpdateTime: time.Now(), Properties: properties, Type: traitType, Alias: req.Alias, Description: req.Description} + comp.Traits[i] = updatedTrait if err := c.ds.Put(ctx, &comp); err != nil { return nil, err } - return &apisv1.ApplicationTrait{Type: trait.Type, Properties: properties}, nil + return &apisv1.ApplicationTrait{Type: trait.Type, Properties: properties, + Alias: updatedTrait.Alias, Description: updatedTrait.Description, CreateTime: updatedTrait.CreateTime, UpdateTime: updatedTrait.UpdateTime}, nil } } return nil, bcode.ErrTraitNotExist diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 3e33399d3..64e7502ff 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -329,9 +329,13 @@ var _ = Describe("Test application usecase function", func() { It("Test add application trait", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) + alias := "alias" + description := "description" res, err := appUsecase.CreateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, v1.CreateApplicationTraitRequest{ - Type: "Ingress", - Properties: `{"domain":"www.test.com"}`, + Type: "Ingress", + Properties: `{"domain":"www.test.com"}`, + Alias: alias, + Description: description, }) Expect(err).Should(BeNil()) Expect(cmp.Diff(res.Type, "Ingress")).Should(BeEmpty()) @@ -340,6 +344,8 @@ var _ = Describe("Test application usecase function", func() { Expect(comp).ShouldNot(BeNil()) Expect(len(comp.Traits)).Should(BeEquivalentTo(1)) Expect(comp.Traits[0].Properties.JSON()).Should(BeEquivalentTo(`{"domain":"www.test.com"}`)) + Expect(comp.Traits[0].Alias).Should(BeEquivalentTo(alias)) + Expect(comp.Traits[0].Description).Should(BeEquivalentTo(description)) }) It("Test add application a dup trait", func() { @@ -355,8 +361,12 @@ var _ = Describe("Test application usecase function", func() { It("Test update application trait", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) + alias := "newAlias" + description := "newDescription" res, err := appUsecase.UpdateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, "Ingress", v1.UpdateApplicationTraitRequest{ - Properties: `{"domain":"www.test1.com"}`, + Properties: `{"domain":"www.test1.com"}`, + Alias: alias, + Description: description, }) Expect(err).Should(BeNil()) Expect(cmp.Diff(res.Type, "Ingress")).Should(BeEmpty()) @@ -365,6 +375,8 @@ var _ = Describe("Test application usecase function", func() { Expect(comp).ShouldNot(BeNil()) Expect(len(comp.Traits)).Should(BeEquivalentTo(1)) Expect(comp.Traits[0].Properties.JSON()).Should(BeEquivalentTo(`{"domain":"www.test1.com"}`)) + Expect(comp.Traits[0].Alias).Should(BeEquivalentTo(alias)) + Expect(comp.Traits[0].Description).Should(BeEquivalentTo(description)) }) It("Test update a not exist", func() { From ba4a28fa0d072791177e2e64a8216af0953d18c6 Mon Sep 17 00:00:00 2001 From: Somefive Date: Wed, 17 Nov 2021 15:12:06 +0800 Subject: [PATCH 41/59] Feat: cherry-pick #2720 into apiserver: support list runtime cluster (#2730) * Feat: support list runtime cluster (#2720) * Fix: style --- .../defwithtemplate/deploy2runtime.yaml | 44 +++++++++++++++++++ .../defwithtemplate/deploy2runtime.yaml | 44 +++++++++++++++++++ cmd/apiserver/main.go | 2 +- .../examples/multicluster/deploy2runtime.yaml | 21 +++++++++ pkg/apiserver/model/application.go | 2 +- pkg/multicluster/utils.go | 9 ++++ pkg/stdlib/op.cue | 8 ++++ pkg/stdlib/pkgs/multicluster.cue | 13 +++++- .../providers/multicluster/multicluster.go | 15 +++++++ .../multicluster/multicluster_test.go | 31 +++++++++++++ pkg/workflow/providers/oam/apply.go | 2 +- .../definitions/internal/deploy2runtime.cue | 39 ++++++++++++++++ 12 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml create mode 100644 charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml create mode 100644 docs/examples/multicluster/deploy2runtime.yaml create mode 100644 vela-templates/definitions/internal/deploy2runtime.cue diff --git a/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml b/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml new file mode 100644 index 000000000..5b007b43d --- /dev/null +++ b/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml @@ -0,0 +1,44 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/deploy2runtime.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Deploy application to runtime clusters + name: deploy2runtime + namespace: {{.Values.systemDefinitionNamespace}} +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + app: op.#Steps & { + load: op.#Load @step(1) + clusters: [...string] + if parameter.clusters == _|_ { + listClusters: op.#ListClusters @step(2) + clusters: listClusters.outputs.clusters + } + if parameter.clusters != _|_ { + clusters: parameter.clusters + } + + apply: op.#Steps & { + for _, cluster_ in clusters { + for name, c in load.value { + "\(cluster_)-\(name)": op.#ApplyComponent & { + value: c + cluster: cluster_ + } @step(3) + } + } + } + } + parameter: { + // +usage=Declare the runtime clusters to apply, if empty, all runtime clusters will be used + clusters?: [...string] + } + diff --git a/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml b/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml new file mode 100644 index 000000000..5b007b43d --- /dev/null +++ b/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml @@ -0,0 +1,44 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/deploy2runtime.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Deploy application to runtime clusters + name: deploy2runtime + namespace: {{.Values.systemDefinitionNamespace}} +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + app: op.#Steps & { + load: op.#Load @step(1) + clusters: [...string] + if parameter.clusters == _|_ { + listClusters: op.#ListClusters @step(2) + clusters: listClusters.outputs.clusters + } + if parameter.clusters != _|_ { + clusters: parameter.clusters + } + + apply: op.#Steps & { + for _, cluster_ in clusters { + for name, c in load.value { + "\(cluster_)-\(name)": op.#ApplyComponent & { + value: c + cluster: cluster_ + } @step(3) + } + } + } + } + parameter: { + // +usage=Declare the runtime clusters to apply, if empty, all runtime clusters will be used + clusters?: [...string] + } + diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 6b5f253cf..d211b6186 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -28,8 +28,8 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" "github.com/go-openapi/spec" - "github.com/google/uuid" + "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/rest" "github.com/oam-dev/kubevela/version" diff --git a/docs/examples/multicluster/deploy2runtime.yaml b/docs/examples/multicluster/deploy2runtime.yaml new file mode 100644 index 000000000..3461f21a8 --- /dev/null +++ b/docs/examples/multicluster/deploy2runtime.yaml @@ -0,0 +1,21 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: runtime-app + namespace: default +spec: + components: + - name: default-webservice + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + + workflow: + steps: + # by default, this step will deploy this application to all runtime clusters + - name: deploy-all + type: deploy2runtime + # uncomment the following part to deploy this application only to selected clusters + # properties: + # clusters: ["cluster-a", "cluster-b"] diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 00dbb2779..32323277f 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -191,7 +191,7 @@ var RevisionStatusComplete = "complete" // RevisionStatusFail event status failure var RevisionStatusFail = "failure" -// RevisionStatusTerminated event status terminated +// RevisionStatusTerminated event status terminated var RevisionStatusTerminated = "terminated" // ApplicationRevision be created when an application initiates deployment and describes the phased version of the application. diff --git a/pkg/multicluster/utils.go b/pkg/multicluster/utils.go index 4cff7f6ff..06b19193d 100644 --- a/pkg/multicluster/utils.go +++ b/pkg/multicluster/utils.go @@ -170,3 +170,12 @@ func GetMulticlusterKubernetesClient() (client.Client, *rest.Config, error) { k8sClient, err := Initialize(k8sConfig, false) return k8sClient, k8sConfig, err } + +// ListExistingClusterSecrets list existing cluster secrets +func ListExistingClusterSecrets(ctx context.Context, c client.Client) ([]v1.Secret, error) { + secrets := &v1.SecretList{} + if err := c.List(ctx, secrets, client.InNamespace(ClusterGatewaySecretNamespace), client.HasLabels{v1alpha1.LabelKeyClusterCredentialType}); err != nil { + return nil, errors2.Wrapf(err, "failed to list cluster secrets") + } + return secrets.Items, nil +} diff --git a/pkg/stdlib/op.cue b/pkg/stdlib/op.cue index 923c25ed9..b3b4f39a4 100644 --- a/pkg/stdlib/op.cue +++ b/pkg/stdlib/op.cue @@ -107,6 +107,14 @@ import ( #ApplyEnvBindApp: multicluster.#ApplyEnvBindApp +#LoadPolicies: oam.#LoadPolicies + +#ListClusters: multicluster.#ListClusters + +#MakePlacementDecisions: multicluster.#MakePlacementDecisions + +#PatchApplication: multicluster.#PatchApplication + #HTTPGet: http.#Do & {method: "GET"} #HTTPPost: http.#Do & {method: "POST"} diff --git a/pkg/stdlib/pkgs/multicluster.cue b/pkg/stdlib/pkgs/multicluster.cue index 7b65c6cef..119c54231 100644 --- a/pkg/stdlib/pkgs/multicluster.cue +++ b/pkg/stdlib/pkgs/multicluster.cue @@ -30,8 +30,8 @@ #do: "read-placement-decisions" inputs: { - policy: string - envName: string + policyName: string + envName: string } outputs: { @@ -123,3 +123,12 @@ } } } + +#ListClusters: { + #provider: "multicluster" + #do: "list-clusters" + + outputs: { + clusters: [...string] + } +} diff --git a/pkg/workflow/providers/multicluster/multicluster.go b/pkg/workflow/providers/multicluster/multicluster.go index 50952327e..39dd45995 100644 --- a/pkg/workflow/providers/multicluster/multicluster.go +++ b/pkg/workflow/providers/multicluster/multicluster.go @@ -17,6 +17,8 @@ limitations under the License. package multicluster import ( + "context" + "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/client" @@ -146,6 +148,18 @@ func (p *provider) PatchApplication(ctx wfContext.Context, v *value.Value, act w return v.FillObject(newApp, "outputs") } +func (p *provider) ListClusters(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + secrets, err := multicluster.ListExistingClusterSecrets(context.Background(), p.Client) + if err != nil { + return err + } + var clusters []string + for _, secret := range secrets { + clusters = append(clusters, secret.Name) + } + return v.FillObject(clusters, "outputs", "clusters") +} + // Install register handlers to provider discover. func Install(p providers.Providers, c client.Client, app *v1beta1.Application) { prd := &provider{Client: c, app: app} @@ -153,5 +167,6 @@ func Install(p providers.Providers, c client.Client, app *v1beta1.Application) { "read-placement-decisions": prd.ReadPlacementDecisions, "make-placement-decisions": prd.MakePlacementDecisions, "patch-application": prd.PatchApplication, + "list-clusters": prd.ListClusters, }) } diff --git a/pkg/workflow/providers/multicluster/multicluster_test.go b/pkg/workflow/providers/multicluster/multicluster_test.go index 4c5e236f4..53c97f743 100644 --- a/pkg/workflow/providers/multicluster/multicluster_test.go +++ b/pkg/workflow/providers/multicluster/multicluster_test.go @@ -21,6 +21,7 @@ import ( "encoding/json" "testing" + v1alpha12 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" v12 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -477,3 +478,33 @@ func TestPatchApplication(t *testing.T) { } } } + +func TestListClusters(t *testing.T) { + multicluster.ClusterGatewaySecretNamespace = types.DefaultKubeVelaNS + r := require.New(t) + cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build() + clusterNames := []string{"cluster-a", "cluster-b"} + for _, secretName := range clusterNames { + secret := &v1.Secret{} + secret.Name = secretName + secret.Namespace = multicluster.ClusterGatewaySecretNamespace + secret.Labels = map[string]string{v1alpha12.LabelKeyClusterCredentialType: "X509"} + r.NoError(cli.Create(context.Background(), secret)) + } + app := &v1beta1.Application{} + p := &provider{ + Client: cli, + app: app, + } + act := &mock.Action{} + v, err := value.NewValue("", nil, "") + r.NoError(err) + r.NoError(p.ListClusters(nil, v, act)) + outputs, err := v.LookupValue("outputs") + r.NoError(err) + obj := struct { + Clusters []string `json:"clusters"` + }{} + r.NoError(outputs.UnmarshalTo(&obj)) + r.Equal(clusterNames, obj.Clusters) +} diff --git a/pkg/workflow/providers/oam/apply.go b/pkg/workflow/providers/oam/apply.go index 51b942cdc..be767abb2 100644 --- a/pkg/workflow/providers/oam/apply.go +++ b/pkg/workflow/providers/oam/apply.go @@ -50,7 +50,7 @@ type provider struct { app *v1beta1.Application } -// ApplyComponent apply component. +// RenderComponent render component func (p *provider) RenderComponent(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { comp, patcher, clusterName, overrideNamespace, err := lookUpValues(v) if err != nil { diff --git a/vela-templates/definitions/internal/deploy2runtime.cue b/vela-templates/definitions/internal/deploy2runtime.cue new file mode 100644 index 000000000..b7862e878 --- /dev/null +++ b/vela-templates/definitions/internal/deploy2runtime.cue @@ -0,0 +1,39 @@ +import ( + "vela/op" +) + +"deploy2runtime": { + type: "workflow-step" + annotations: {} + labels: {} + description: "Deploy application to runtime clusters" +} +template: { + app: op.#Steps & { + load: op.#Load @step(1) + clusters: [...string] + if parameter.clusters == _|_ { + listClusters: op.#ListClusters @step(2) + clusters: listClusters.outputs.clusters + } + if parameter.clusters != _|_ { + clusters: parameter.clusters + } + + apply: op.#Steps & { + for _, cluster_ in clusters { + for name, c in load.value { + "\(cluster_)-\(name)": op.#ApplyComponent & { + value: c + cluster: cluster_ + } @step(3) + } + } + } + } + + parameter: { + // +usage=Declare the runtime clusters to apply, if empty, all runtime clusters will be used + clusters?: [...string] + } +} From 2fc0f1cd2b445c1dbd5eabed1ec15bcd94138d8c Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Wed, 17 Nov 2021 15:29:36 +0800 Subject: [PATCH 42/59] Feat: add app revision list and detail api (#2722) * Feat: add app revision list and detail api * add envName and status filter * make swagger doc * revert ui-schema --- docs/apidoc/swagger.json | 323 ++++++++++++++++-- pkg/apiserver/model/application.go | 5 + pkg/apiserver/rest/apis/v1/types.go | 23 +- pkg/apiserver/rest/usecase/application.go | 78 ++++- .../rest/usecase/application_test.go | 45 +++ pkg/apiserver/rest/usecase/workflow.go | 20 +- pkg/apiserver/rest/usecase/workflow_test.go | 1 + pkg/apiserver/rest/webservice/application.go | 54 +++ pkg/apiserver/rest/webservice/workflow.go | 4 +- 9 files changed, 499 insertions(+), 54 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 8afb9ddf0..c4cc5281b 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -1305,6 +1305,113 @@ } } }, + "/api/v1/applications/{name}/revisions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list revisions for application", + "operationId": "listApplicationRevisions", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "query identifier of the env", + "name": "envName", + "in": "query" + }, + { + "type": "string", + "description": "query identifier of the status", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "query the page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "query the page size number", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListRevisionsResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/revisions/{revision}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail revision for application", + "operationId": "detailApplicationRevision", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application revision", + "name": "revision", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailRevisionResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/applications/{name}/template": { "post": { "consumes": [ @@ -1857,9 +1964,9 @@ "parameters": [ { "enum": [ + "workflowstep", "component", - "trait", - "workflowstep" + "trait" ], "type": "string", "description": "query the definition type", @@ -2477,17 +2584,15 @@ }, { "type": "integer", - "description": "Query the page number.", + "description": "query the page number", "name": "page", - "in": "path", - "required": true + "in": "query" }, { "type": "integer", - "description": "Query the page size number.", + "description": "query the page size number", "name": "pageSize", - "in": "path", - "required": true + "in": "query" } ], "responses": { @@ -2744,11 +2849,11 @@ }, "common.AppRolloutStatus": { "required": [ - "upgradedReadyReplicas", "rollingState", "batchRollingState", "currentBatch", "upgradedReplicas", + "upgradedReadyReplicas", "lastTargetAppRevision" ], "properties": { @@ -3320,6 +3425,61 @@ } } }, + "model.ApplicationRevision": { + "required": [ + "createTime", + "updateTime", + "appPrimaryKey", + "version", + "status", + "reason", + "deployUser", + "note", + "triggerType", + "workflowName", + "envName" + ], + "properties": { + "appPrimaryKey": { + "type": "string" + }, + "applyAppConfig": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "envName": { + "type": "string" + }, + "note": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggerType": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + }, + "workflowName": { + "type": "string" + } + } + }, "model.ApplicationTrait": { "required": [ "alias", @@ -3778,17 +3938,26 @@ }, "v1.ApplicationDeployResponse": { "required": [ - "deployUser", - "note", - "triggerType", + "createTime", "version", "status", - "reason" + "reason", + "deployUser", + "note", + "envName", + "triggerType" ], "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, "deployUser": { "type": "string" }, + "envName": { + "type": "string" + }, "note": { "type": "string" }, @@ -3863,17 +4032,26 @@ }, "v1.ApplicationRevisionBase": { "required": [ + "createTime", "version", "status", "reason", "deployUser", "note", + "envName", "triggerType" ], "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, "deployUser": { "type": "string" }, + "envName": { + "type": "string" + }, "note": { "type": "string" }, @@ -4625,10 +4803,10 @@ }, "v1.DetailAddonResponse": { "required": [ - "version", "description", "icon", "name", + "version", "schema", "uiSchema" ], @@ -4679,13 +4857,13 @@ }, "v1.DetailApplicationResponse": { "required": [ - "icon", + "name", + "namespace", "description", "createTime", "updateTime", - "name", + "icon", "alias", - "namespace", "policies", "status", "resourceInfo", @@ -4749,20 +4927,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "kubeConfigSecret", - "description", + "createTime", + "alias", "icon", + "status", + "reason", "provider", "dashboardURL", + "updateTime", + "name", "labels", - "status", + "description", "apiServerURL", "kubeConfig", - "createTime", - "updateTime", - "alias", - "reason", - "name", + "kubeConfigSecret", "resourceInfo" ], "properties": { @@ -4820,13 +4998,13 @@ }, "v1.DetailComponentResponse": { "required": [ - "createTime", - "name", - "alias", - "type", + "appPrimaryKey", "creator", + "name", + "type", + "createTime", "updateTime", - "appPrimaryKey" + "alias" ], "properties": { "alias": { @@ -4921,10 +5099,10 @@ }, "v1.DetailDeliveryTargetResponse": { "required": [ - "createTime", "updateTime", "name", - "namespace" + "namespace", + "createTime" ], "properties": { "alias": { @@ -4957,13 +5135,13 @@ }, "v1.DetailPolicyResponse": { "required": [ + "createTime", + "updateTime", "name", "type", "description", "creator", - "properties", - "createTime", - "updateTime" + "properties" ], "properties": { "createTime": { @@ -4991,12 +5169,67 @@ } } }, + "v1.DetailRevisionResponse": { + "required": [ + "triggerType", + "workflowName", + "createTime", + "appPrimaryKey", + "reason", + "deployUser", + "note", + "updateTime", + "version", + "status", + "envName" + ], + "properties": { + "appPrimaryKey": { + "type": "string" + }, + "applyAppConfig": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "envName": { + "type": "string" + }, + "note": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggerType": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + }, + "workflowName": { + "type": "string" + } + } + }, "v1.DetailWorkflowRecordResponse": { "required": [ + "terminated", "name", "namespace", "suspend", - "terminated", "deployTime", "deployUser", "note", @@ -5292,6 +5525,24 @@ } } }, + "v1.ListRevisionsResponse": { + "required": [ + "revisions", + "total" + ], + "properties": { + "revisions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationRevisionBase" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, "v1.ListWorkflowRecordsResponse": { "required": [ "records", diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 32323277f..e26e47e95 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -216,6 +216,8 @@ type ApplicationRevision struct { // WorkflowName deploy controller by workflow WorkflowName string `json:"workflowName"` + // EnvName is the env name of this application revision + EnvName string `json:"envName"` } // TableName return custom table name @@ -249,5 +251,8 @@ func (a *ApplicationRevision) Index() map[string]string { if a.TriggerType != "" { index["triggerType"] = a.TriggerType } + if a.EnvName != "" { + index["envName"] = a.EnvName + } return index } diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 9ea42175f..6ac439710 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -676,11 +676,24 @@ type DeliveryTargetBase struct { // ApplicationRevisionBase application revision base spec type ApplicationRevisionBase struct { - Version string `json:"version"` - Status string `json:"status"` - Reason string `json:"reason"` - DeployUser string `json:"deployUser"` - Note string `json:"note"` + CreateTime time.Time `json:"createTime"` + Version string `json:"version"` + Status string `json:"status"` + Reason string `json:"reason"` + DeployUser string `json:"deployUser"` + Note string `json:"note"` + EnvName string `json:"envName"` // SourceType the event trigger source, Web or API TriggerType string `json:"triggerType"` } + +// ListRevisionsResponse list application revisions +type ListRevisionsResponse struct { + Revisions []ApplicationRevisionBase `json:"revisions"` + Total int64 `json:"total"` +} + +// DetailRevisionResponse get application revision detail +type DetailRevisionResponse struct { + model.ApplicationRevision +} diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index 92d83e76d..ce96d9d8a 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -84,6 +84,8 @@ type ApplicationUsecase interface { CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string, req apisv1.UpdateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) + ListRevisions(ctx context.Context, appName, envName, status string, page, pageSize int) (*apisv1.ListRevisionsResponse, error) + DetailRevision(ctx context.Context, appName, revisionName string) (*apisv1.DetailRevisionResponse, error) } type applicationUsecaseImpl struct { @@ -583,6 +585,11 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat } } + workflow, err := c.workflowUsecase.GetWorkflow(ctx, oamApp.Annotations[oam.AnnotationWorkflowName]) + if err != nil { + return nil, err + } + var appRevision = &model.ApplicationRevision{ AppPrimaryKey: app.PrimaryKey(), Version: version, @@ -593,6 +600,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat Note: req.Note, TriggerType: req.TriggerType, WorkflowName: oamApp.Annotations[oam.AnnotationWorkflowName], + EnvName: workflow.EnvName, } if err := c.ds.Add(ctx, appRevision); err != nil { @@ -636,23 +644,23 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat }, nil } -func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMoel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) { +func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appModel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) { var app = &v1beta1.Application{ TypeMeta: metav1.TypeMeta{ Kind: "Application", APIVersion: "core.oam.dev/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ - Name: appMoel.Name, - Namespace: appMoel.Namespace, - Labels: appMoel.Labels, + Name: appModel.Name, + Namespace: appModel.Namespace, + Labels: appModel.Labels, Annotations: map[string]string{ oam.AnnotationDeployVersion: version, }, }, } var component = model.ApplicationComponent{ - AppPrimaryKey: appMoel.PrimaryKey(), + AppPrimaryKey: appModel.PrimaryKey(), } components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) if err != nil { @@ -663,7 +671,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } var policy = model.ApplicationPolicy{ - AppPrimaryKey: appMoel.PrimaryKey(), + AppPrimaryKey: appModel.PrimaryKey(), } policies, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) if err != nil { @@ -720,7 +728,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo return nil, err } } else { - workflow, err = c.workflowUsecase.GetApplicationDefaultWorkflow(ctx, appMoel) + workflow, err = c.workflowUsecase.GetApplicationDefaultWorkflow(ctx, appModel) if err != nil && !errors.Is(err, bcode.ErrWorkflowNoDefault) { return nil, err } @@ -1171,6 +1179,62 @@ func (c *applicationUsecaseImpl) UpdateApplicationTrait(ctx context.Context, app return nil, bcode.ErrTraitNotExist } +func (c *applicationUsecaseImpl) ListRevisions(ctx context.Context, appName, envName, status string, page, pageSize int) (*apisv1.ListRevisionsResponse, error) { + var revision = model.ApplicationRevision{ + AppPrimaryKey: appName, + } + if envName != "" { + revision.EnvName = envName + } + if status != "" { + revision.Status = status + } + + revisions, err := c.ds.List(ctx, &revision, &datastore.ListOptions{Page: page, PageSize: pageSize}) + if err != nil { + return nil, err + } + + resp := &apisv1.ListRevisionsResponse{ + Revisions: []apisv1.ApplicationRevisionBase{}, + } + for _, raw := range revisions { + r, ok := raw.(*model.ApplicationRevision) + if ok { + resp.Revisions = append(resp.Revisions, apisv1.ApplicationRevisionBase{ + CreateTime: r.CreateTime, + Version: r.Version, + Status: r.Status, + Reason: r.Reason, + DeployUser: r.DeployUser, + Note: r.Note, + EnvName: r.EnvName, + TriggerType: r.TriggerType, + }) + } + } + count, err := c.ds.Count(ctx, &revision) + if err != nil { + return nil, err + } + resp.Total = count + + return resp, nil +} + +func (c *applicationUsecaseImpl) DetailRevision(ctx context.Context, appName, revisionName string) (*apisv1.DetailRevisionResponse, error) { + var revision = model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: revisionName, + } + if err := c.ds.Get(ctx, &revision); err != nil { + return nil, err + } + return &apisv1.DetailRevisionResponse{ + ApplicationRevision: revision, + }, nil +} + func createEnvBind(envBind apisv1.EnvBinding) v1alpha1.EnvConfig { placement := v1alpha1.EnvPlacement{} var componentSelector *v1alpha1.EnvSelector diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 64e7502ff..91548b388 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -18,6 +18,7 @@ package usecase import ( "context" + "fmt" "io/ioutil" "strings" @@ -516,4 +517,48 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) Expect(cmp.Diff(len(policies), 0)).Should(BeEmpty()) }) + + It("Test ListRevisions function", func() { + for i := 0; i < 3; i++ { + appModel := &model.ApplicationRevision{ + AppPrimaryKey: "test-app", + Version: fmt.Sprintf("%d", i), + EnvName: fmt.Sprintf("env-%d", i), + Status: model.RevisionStatusRunning, + } + if i == 0 { + appModel.Status = model.RevisionStatusTerminated + } + err := workflowUsecase.createTestApplicationRevision(context.TODO(), appModel) + Expect(err).Should(BeNil()) + } + revisions, err := appUsecase.ListRevisions(context.TODO(), "test-app", "", "", 0, 10) + Expect(err).Should(BeNil()) + Expect(revisions.Total).Should(Equal(int64(3))) + + revisions, err = appUsecase.ListRevisions(context.TODO(), "test-app", "env-0", "", 0, 10) + Expect(err).Should(BeNil()) + Expect(revisions.Total).Should(Equal(int64(1))) + + revisions, err = appUsecase.ListRevisions(context.TODO(), "test-app", "", "terminated", 0, 10) + Expect(err).Should(BeNil()) + Expect(revisions.Total).Should(Equal(int64(1))) + + revisions, err = appUsecase.ListRevisions(context.TODO(), "test-app", "env-1", "terminated", 0, 10) + Expect(err).Should(BeNil()) + Expect(revisions.Total).Should(Equal(int64(0))) + }) + + It("Test DetailRevisions function", func() { + err := workflowUsecase.createTestApplicationRevision(context.TODO(), &model.ApplicationRevision{ + AppPrimaryKey: "test-app", + Version: "123", + DeployUser: "test-user", + }) + Expect(err).Should(BeNil()) + revision, err := appUsecase.DetailRevision(context.TODO(), "test-app", "123") + Expect(err).Should(BeNil()) + Expect(revision.Version).Should(Equal("123")) + Expect(revision.DeployUser).Should(Equal("test-user")) + }) }) diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 63ef8febd..37dc4e27e 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -311,12 +311,27 @@ func (w *workflowUsecaseImpl) SyncWorkflowRecord(ctx context.Context) error { continue } + if app.Annotations == nil { + klog.ErrorS(err, "empty application annotation", "controller revision name", cr.Name) + continue + } + + if _, ok := app.Annotations[oam.AnnotationWorkflowName]; !ok { + klog.ErrorS(err, "missing application workflow name", "controller revision name", cr.Name) + continue + } + revisionName, ok := app.Annotations[oam.AnnotationDeployVersion] + if !ok { + klog.ErrorS(err, "failed to get application revision name", "controller revision name", cr.Name) + continue + } + if err := w.createWorkflowRecord(ctx, app, strings.TrimPrefix(cr.Name, "record-")); err != nil && !errors.Is(err, datastore.ErrRecordExist) { klog.ErrorS(err, "failed to create workflow record", "controller revision name", cr.Name) continue } - err = w.updateRecordApplicationRevisionStatus(ctx, app.Name, strings.TrimPrefix(cr.Name, fmt.Sprintf("record-%s-", app.Name)), app.Status.Workflow.Terminated) + err = w.updateRecordApplicationRevisionStatus(ctx, app.Name, revisionName, app.Status.Workflow.Terminated) if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { klog.ErrorS(err, "failed to update deploy event status", "controller revision name", cr.Name) continue @@ -354,9 +369,6 @@ func (w *workflowUsecaseImpl) updateRecordApplicationRevisionStatus(ctx context. } func (w *workflowUsecaseImpl) createWorkflowRecord(ctx context.Context, app *v1beta1.Application, revisionName string) error { - if app.Annotations == nil || app.Annotations[oam.AnnotationWorkflowName] == "" { - return fmt.Errorf("missing workflow name") - } status := app.Status.Workflow return w.ds.Add(ctx, &model.WorkflowRecord{ diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 76cefc920..37f35d7e6 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -170,6 +170,7 @@ kind: Application metadata: annotations: app.oam.dev/workflowName: test-workflow-name + app.oam.dev/deployVersion: "1234" name: test namespace: default spec: diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 6a36ffba9..b2d706618 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -26,6 +26,7 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/model" 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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) @@ -269,6 +270,29 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(200, "", apis.ApplicationTrait{}). Returns(400, "", bcode.Bcode{}). Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/{name}/revisions").To(c.listApplicationRevisions). + Doc("list revisions for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.QueryParameter("envName", "query identifier of the env").DataType("string")). + Param(ws.QueryParameter("status", "query identifier of the status").DataType("string")). + Param(ws.QueryParameter("page", "query the page number").DataType("integer")). + Param(ws.QueryParameter("pageSize", "query the page size number").DataType("integer")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListRevisionsResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListRevisionsResponse{})) + + ws.Route(ws.GET("/{name}/revisions/{revision}").To(c.detailApplicationRevision). + Doc("detail revision for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("revision", "identifier of the application revision").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.DetailRevisionResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailRevisionResponse{})) return ws } @@ -695,3 +719,33 @@ func (c *applicationWebService) getApplicationStatus(req *restful.Request, res * return } } + +func (c *applicationWebService) listApplicationRevisions(req *restful.Request, res *restful.Response) { + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + revisions, err := c.applicationUsecase.ListRevisions(req.Request.Context(), req.PathParameter("name"), req.QueryParameter("envName"), req.QueryParameter("status"), page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(revisions); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) detailApplicationRevision(req *restful.Request, res *restful.Response) { + detail, err := c.applicationUsecase.DetailRevision(req.Request.Context(), req.PathParameter("name"), req.PathParameter("revision")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 9e84d35af..8ccbecd83 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -99,8 +99,8 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(w.workflowCheckFilter). - Param(ws.PathParameter("page", "Query the page number.").DataType("integer")). - Param(ws.PathParameter("pageSize", "Query the page size number.").DataType("integer")). + Param(ws.QueryParameter("page", "query the page number").DataType("integer")). + Param(ws.QueryParameter("pageSize", "query the page size number").DataType("integer")). Returns(200, "", apis.ListWorkflowRecordsResponse{}). Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) From 530f15879552bf1a420117696d7df898a9fcf798 Mon Sep 17 00:00:00 2001 From: yangsoon Date: Thu, 18 Nov 2021 11:53:38 +0800 Subject: [PATCH 43/59] Feat: velaql support query the resources created by helmrelease (#2726) Co-authored-by: yangsoon --- .github/workflows/apiserver-test.yaml | 7 + .../velaql-views/component-pod-view.yaml | 50 +++--- .../templates/velaql-views/resource-view.yaml | 7 +- .../velaql-views/componet-pod-view.yaml | 107 ----------- docs/examples/velaql-views/usage.md | 169 ++++++++++++++++++ go.mod | 3 +- go.sum | 18 +- pkg/stdlib/pkgs/query.cue | 1 + pkg/velaql/providers/query/collector.go | 115 +++++++++--- pkg/velaql/providers/query/handler.go | 25 ++- test/e2e-apiserver-test/addon_test.go | 10 +- .../testdata/component-pod-view.yaml | 21 +-- test/e2e-apiserver-test/velaql_test.go | 75 ++++++++ 13 files changed, 418 insertions(+), 190 deletions(-) delete mode 100644 docs/examples/velaql-views/componet-pod-view.yaml create mode 100644 docs/examples/velaql-views/usage.md diff --git a/.github/workflows/apiserver-test.yaml b/.github/workflows/apiserver-test.yaml index 66e8e0275..8773d206e 100644 --- a/.github/workflows/apiserver-test.yaml +++ b/.github/workflows/apiserver-test.yaml @@ -86,6 +86,13 @@ jobs: make e2e-cleanup make e2e-setup-core + make vela-cli + bin/vela addon enable fluxcd + timeout 600s bash -c -- 'while true; do kubectl get ns flux-system; if [ $? -eq 0 ] ; then break; else sleep 5; fi;done' + kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=vela-core,app.kubernetes.io/instance=kubevela -n vela-system --timeout=600s + kubectl wait --for=condition=Ready pod -l app=source-controller -n flux-system --timeout=600s + kubectl wait --for=condition=Ready pod -l app=helm-controller -n flux-system --timeout=600s + - name: Run apiserver unit test run: make unit-test-apiserver diff --git a/charts/vela-core/templates/velaql-views/component-pod-view.yaml b/charts/vela-core/templates/velaql-views/component-pod-view.yaml index 55aebfc73..6b2de40d3 100644 --- a/charts/vela-core/templates/velaql-views/component-pod-view.yaml +++ b/charts/vela-core/templates/velaql-views/component-pod-view.yaml @@ -8,13 +8,13 @@ data: import ( "vela/ql" "vela/op" - "list" ) parameter: { name: string namespace: string componentName: string + cluster: *"" | string } appList: ql.#ListResourcesInApp & { @@ -22,13 +22,15 @@ data: name: parameter.name namespace: parameter.namespace components: [parameter.componentName] + cluster: parameter.cluster } } if appList.err == _|_ { - appRev: appList.list[0].revision - resources: appList.list[0].components[0].resources - collectedPods: op.#Steps & { + appRev: appList.list[0].revision + appPublishVersion: appList.list[0].publishVersion + resources: appList.list[0].components[0].resources + collectedPods: op.#Steps & { for i, resource in resources { "\(i)": ql.#CollectPods & { value: resource.object @@ -36,29 +38,25 @@ data: } } } - - podsWithCluster: {for i, pods in collectedPods { - "\(i)": [ - for podObj in pods.list { - cluster: pods.cluster - obj: podObj - }, - ] - }} - flatPods: list.FlattenN([ for pods in podsWithCluster { - pods - }], 1) + podsWithCluster: [ for pods in collectedPods for podObj in pods.list { + cluster: pods.cluster + obj: podObj + }] status: { - podList: [ for pod in flatPods { - clusterName: pod.cluster - revision: appRev - podName: pod.obj.metadata.name - podIP: pod.obj.status.podIP - status: pod.obj.status.phase - hostIP: pod.obj.status.hostIP - nodeName: pod.obj.spec.nodeName + podList: [ for pod in podsWithCluster { + clusterName: pod.cluster + revision: appRev + publishVersion: appPublishVersion + podName: pod.obj.metadata.name + status: pod.obj.status.phase + // refer to https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase + if status != "Pending" && status != "Unknown" { + podIP: pod.obj.status.podIP + hostIP: pod.obj.status.hostIP + nodeName: pod.obj.spec.nodeName + } }] } } @@ -67,4 +65,6 @@ data: status: { error: appList.err } - } \ No newline at end of file + } + + diff --git a/charts/vela-core/templates/velaql-views/resource-view.yaml b/charts/vela-core/templates/velaql-views/resource-view.yaml index 18632c38c..a6e33e727 100644 --- a/charts/vela-core/templates/velaql-views/resource-view.yaml +++ b/charts/vela-core/templates/velaql-views/resource-view.yaml @@ -11,7 +11,7 @@ data: parameter: { type: string - namespace: string + namespace: *"" | string cluster: *"" | string } @@ -32,6 +32,10 @@ data: apiVersion: "storage.k8s.io/v1" kind: "StorageClass" } + "ns": { + apiVersion: "v1" + kind: "Namespace" + } } List: ql.#List & { @@ -42,7 +46,6 @@ data: cluster: parameter.cluster } - status: { if List.err == _|_ { if len(List.list.items) == 0 { diff --git a/docs/examples/velaql-views/componet-pod-view.yaml b/docs/examples/velaql-views/componet-pod-view.yaml deleted file mode 100644 index b45d7fc77..000000000 --- a/docs/examples/velaql-views/componet-pod-view.yaml +++ /dev/null @@ -1,107 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: component-pod-view - namespace: vela-system -data: - template: | - import ( - "vela/ql" - "vela/op" - "list" - ) - - parameter: { - name: string - namespace: string - componentName: string - } - - application: ql.#ListResourcesInApp & { - app: { - name: parameter.name - namespace: parameter.namespace - components: [parameter.componentName] - } - } - - app: application.list[0] - resources: app.components[0].resources - - podsMap: op.#Steps & { - for i, resource in resources { - "\(i)": ql.#CollectPods & { - value: resource.object - cluster: resource.cluster - } - } - } - - podsWithCluster: {for i, pods in podsMap { - "\(i)": [ - for podObj in pods.list { - cluster: pods.cluster - obj: podObj - }, - ] - }} - - flatPods: list.FlattenN([ for pods in podsWithCluster { - pods - }], 1) - - podStatus: op.#Steps & { - for i, pod in flatPods { - "\(i)": op.#Steps & { - name: pod.obj.metadata.name - containers: {for container in pod.obj.status.containerStatuses { - "\(container.name)": { - image: container.image - state: container.state - } - }} - events: ql.#SearchEvents & { - value: pod.obj - cluster: pod.cluster - } - metrics: ql.#Read & { - cluster: pod.cluster - value: { - apiVersion: "metrics.k8s.io/v1beta1" - kind: "PodMetrics" - metadata: { - name: pod.obj.metadata.name - namespace: pod.obj.metadata.namespace - } - } - } - } - } - } - - status: { - podList: [ for podInfo in podStatus { - name: podInfo.name - containers: [ for containerName, container in podInfo.containers { - name: containerName - image: container.image - state: container.state - if podInfo.metrics.err == _|_ { - usage: {for containerUsage in podInfo.metrics.value.containers { - if containerUsage.name == containerName { - cpu: containerUsage.usage.cpu - memory: containerUsage.usage.memory - } - }} - } - }] - events: [ for event in podInfo.events.list { - type: event.type - reason: event.reason - message: event.message - firstTimestamp: event.firstTimestamp - }] - }] - } - - diff --git a/docs/examples/velaql-views/usage.md b/docs/examples/velaql-views/usage.md new file mode 100644 index 000000000..dca9d764f --- /dev/null +++ b/docs/examples/velaql-views/usage.md @@ -0,0 +1,169 @@ + + +### Usage + +You can use velaQL with a syntax similar to promeQL. + +The syntax format of velaQL is as follows: + +```sql +view{parameter1=value1}.statusKey +``` + +1. `view` represents different query views, we have built a few views: `component-pod-view`,`pod-view`,`resource-view` +2. `parameter1=value1` represents query configuration items +3. `statusKey` represents the aggregate result of the query, default is `status` + +### component-pod-view + +#### describe + +List the pods created by specified component + +#### parameter + +``` +parameter: { + name: string // application name + namespace: string // application namespace + componentName: string // component name + cluster?: string // cluster name(Optional) +} +``` + +#### statusKey + +`status` + +#### query result + +``` +// query successful +status: { + podList: [{ + clusterName: string + revision: string + publishVersion: string + podName: string + status: string + podIP: string + hostIP: string + nodeName: string + }] +} + +// query failed +status: { + error: string +} +``` + +#### demo + +```sql +component-pod-view{name=demo,namespace=default,cluster=prod,componentName=web}.status +``` + +### pod-view + +#### describe + +Query the pods detail infomation + +#### parameter + +``` +parameter: { + name: string // pod name + namespace: string // pod namespace + cluster?: string // cluster name(Optional) +} +``` + +#### statusKey + +`status` + +#### query result + +``` +// query successful +status: { + containers: [ { + name: string + image: string + status: { + state: string + restartCount: string + } + resource: { + limits: { + cpu: string + memory: string + } + requests: { + cpu: string + memory: string + } + } + usageResource: { + cpu: string + memory: string + } + }] + events: [...corev1.Event] +} + +// query failed +status: { + error: string +} +``` + +#### demo + +``` +pod-view{name=demo,namespace=default,cluster=prod}.status +``` + +### resource-view + +#### describe + +List resources + +#### parameter + +``` +parameter: { + type: "ns" | "secret" | "configMap" | "pvc" | "storageClass" + namespace: *"" | string // Optional + cluster: *"" | string // Optional +} +``` + +#### statusKey + +`status` + +#### query result + +``` +// query successful +status: { + list: [...k8sObject] +} + +// query failed +status: { + error: string +} +``` + +#### demo + +``` +resource-view{type=ns,cluster=prod}.status +``` + + diff --git a/go.mod b/go.mod index 41b2d9728..4b3e9a742 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,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/fluxcd/helm-controller/api v0.12.1 github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/gertd/go-pluralize v0.1.7 github.com/getkin/kin-openapi v0.34.0 @@ -68,7 +69,7 @@ require ( istio.io/api v0.0.0-20210128181506-0c4b8e54850f istio.io/client-go v0.0.0-20210128182905-ee2edd059e02 k8s.io/api v0.22.1 - k8s.io/apiextensions-apiserver v0.21.3 + k8s.io/apiextensions-apiserver v0.22.1 k8s.io/apimachinery v0.22.1 k8s.io/cli-runtime v0.21.0 k8s.io/client-go v0.22.1 diff --git a/go.sum b/go.sum index bfb98c7b3..e5d5b17aa 100644 --- a/go.sum +++ b/go.sum @@ -479,6 +479,14 @@ github.com/fatih/structtag v1.1.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/helm-controller/api v0.12.1 h1:rDyhMPvbhCxslqiNNG4nlfDCeYgrk6D+1ZKLsBS/Irs= +github.com/fluxcd/helm-controller/api v0.12.1/go.mod h1:zWmzV0s2SU4rEIGLPTt+dsaMs40OsNQgSgOATgJmxB0= +github.com/fluxcd/pkg/apis/kustomize v0.1.0 h1:sauL+KHmZ0zV2ZgpsLMyDzCQudBTtaFzSys+rXn9g9w= +github.com/fluxcd/pkg/apis/kustomize v0.1.0/go.mod h1:gEl+W5cVykCC3RfrCaqe+Pz+j4lKl2aeR4dxsom/zII= +github.com/fluxcd/pkg/apis/meta v0.10.0 h1:N7wVGHC1cyPdT87hrDC7UwCwRwnZdQM46PBSLjG2rlE= +github.com/fluxcd/pkg/apis/meta v0.10.0/go.mod h1:CW9X9ijMTpNe7BwnokiUOrLl/h13miwVr/3abEQLbKE= +github.com/fluxcd/pkg/runtime v0.12.0 h1:BPZZ8bBkimpqGAPXqOf3LTaw+tcw6HgbWyCuzbbsJGs= +github.com/fluxcd/pkg/runtime v0.12.0/go.mod h1:EyaTR2TOYcjL5U//C4yH3bt2tvTgIOSXpVRbWxUn/C4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -888,6 +896,7 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.12.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -898,6 +907,7 @@ github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= @@ -2441,9 +2451,11 @@ k8s.io/apiextensions-apiserver v0.17.0/go.mod h1:XiIFUakZywkUl54fVXa7QTEHcqQz9HG k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY= k8s.io/apiextensions-apiserver v0.18.6/go.mod h1:lv89S7fUysXjLZO7ke783xOwVTm6lKizADfvUM/SS/M= k8s.io/apiextensions-apiserver v0.21.0/go.mod h1:gsQGNtGkc/YoDG9loKI0V+oLZM4ljRPjc/sql5tmvzc= +k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= k8s.io/apiextensions-apiserver v0.21.2/go.mod h1:+Axoz5/l3AYpGLlhJDfcVQzCerVYq3K3CvDMvw6X1RA= -k8s.io/apiextensions-apiserver v0.21.3 h1:+B6biyUWpqt41kz5x6peIsljlsuwvNAp/oFax/j2/aY= k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8LGDkprcd+KbsE= +k8s.io/apiextensions-apiserver v0.22.1 h1:YSJYzlFNFSfUle+yeEXX0lSQyLEoxoPJySRupepb0gE= +k8s.io/apiextensions-apiserver v0.22.1/go.mod h1:HeGmorjtRmRLE+Q8dJu6AYRoZccvCMsghwS8XTUYb2c= k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010/go.mod h1:Waf/xTS2FGRrgXCkO5FP3XxTOWh0qLf2QhL1qFZZ/R8= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= @@ -2470,6 +2482,7 @@ k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw= k8s.io/apiserver v0.18.6/go.mod h1:Zt2XvTHuaZjBz6EFYzpp+X4hTmgWGy8AthNVnTdm3Wg= k8s.io/apiserver v0.21.0/go.mod h1:w2YSn4/WIwYuxG5zJmcqtRdtqgW/J2JRgFAqps3bBpg= +k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= k8s.io/apiserver v0.21.2/go.mod h1:lN4yBoGyiNT7SC1dmNk0ue6a5Wi6O3SWOIw91TsucQw= k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU= k8s.io/apiserver v0.22.1 h1:Ul9Iv8OMB2s45h2tl5XWPpAZo1VPIJ/6N+MESeed7L8= @@ -2501,6 +2514,7 @@ k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRV k8s.io/code-generator v0.18.6/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= k8s.io/code-generator v0.20.0/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= k8s.io/code-generator v0.21.0/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= +k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= k8s.io/code-generator v0.21.2/go.mod h1:8mXJDCB7HcRo1xiEQstcguZkbxZaqeUOrO9SsicWs3U= k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= k8s.io/code-generator v0.22.1/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= @@ -2511,6 +2525,7 @@ k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmD k8s.io/component-base v0.18.6/go.mod h1:knSVsibPR5K6EW2XOjEHik6sdU5nCvKMrzMt2D4In14= k8s.io/component-base v0.20.10/go.mod h1:ZKOEin1xu68aJzxgzl5DZSp5J1IrjAOPlPN90/t6OI8= k8s.io/component-base v0.21.0/go.mod h1:qvtjz6X0USWXbgmbfXR+Agik4RZ3jv2Bgr5QnZzdPYw= +k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= k8s.io/component-base v0.21.2/go.mod h1:9lvmIThzdlrJj5Hp8Z/TOgIkdfsNARQ1pT+3PByuiuc= k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ= k8s.io/component-base v0.22.1 h1:SFqIXsEN3v3Kkr1bS6rstrs1wd45StJqbtgbQ4nRQdo= @@ -2592,6 +2607,7 @@ sigs.k8s.io/apiserver-runtime v1.0.3-0.20210913073608-0663f60bfee2 h1:c6RYHA1wUg sigs.k8s.io/apiserver-runtime v1.0.3-0.20210913073608-0663f60bfee2/go.mod h1:gvPfh5FX3Wi3kIRpkh7qvY0i/DQl3SDpRtvqMGZE3Vo= sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo= sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= +sigs.k8s.io/controller-runtime v0.9.0/go.mod h1:TgkfvrhhEw3PlI0BRL/5xM+89y3/yc0ZDfdbTl84si8= sigs.k8s.io/controller-runtime v0.9.2/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk= sigs.k8s.io/controller-runtime v0.9.5 h1:WThcFE6cqctTn2jCZprLICO6BaKZfhsT37uAapTNfxc= sigs.k8s.io/controller-runtime v0.9.5/go.mod h1:q6PpkM5vqQubEKUKOM6qr06oXGzOBcCby1DA9FbyZeA= diff --git a/pkg/stdlib/pkgs/query.cue b/pkg/stdlib/pkgs/query.cue index 32661cb7f..1e8112f0e 100644 --- a/pkg/stdlib/pkgs/query.cue +++ b/pkg/stdlib/pkgs/query.cue @@ -5,6 +5,7 @@ name: string namespace: string components?: [...string] + cluster?: string enableHistoryQuery?: bool } ... diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go index 5aa74d247..7a685ad5a 100644 --- a/pkg/velaql/providers/query/collector.go +++ b/pkg/velaql/providers/query/collector.go @@ -21,6 +21,7 @@ import ( "fmt" "reflect" + "github.com/oam-dev/kubevela/pkg/workflow/types" kruise "github.com/openkruise/kruise-api/apps/v1alpha1" "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" @@ -41,22 +42,22 @@ import ( oamutil "github.com/oam-dev/kubevela/pkg/oam/util" ) -// Collector collect resource created by application -type Collector struct { +// AppCollector collect resource created by application +type AppCollector struct { k8sClient client.Client opt Option } -// NewCollector create a collector -func NewCollector(cli client.Client, opt Option) *Collector { - return &Collector{ +// NewAppCollector create a app collector +func NewAppCollector(cli client.Client, opt Option) *AppCollector { + return &AppCollector{ k8sClient: cli, opt: opt, } } // CollectResourceFromApp collect resources created by application -func (c *Collector) CollectResourceFromApp() ([]AppResources, error) { +func (c *AppCollector) CollectResourceFromApp() ([]AppResources, error) { if c.opt.EnableHistoryQuery { return c.CollectHistoryResourceFromApp() } @@ -64,7 +65,7 @@ func (c *Collector) CollectResourceFromApp() ([]AppResources, error) { } // CollectLatestResourceFromApp collect resources created by latest application -func (c *Collector) CollectLatestResourceFromApp() ([]AppResources, error) { +func (c *AppCollector) CollectLatestResourceFromApp() ([]AppResources, error) { ctx := context.Background() app := new(v1beta1.Application) appKey := client.ObjectKey{Name: c.opt.Name, Namespace: c.opt.Namespace} @@ -76,9 +77,14 @@ func (c *Collector) CollectLatestResourceFromApp() ([]AppResources, error) { if app.Status.LatestRevision != nil { revision = app.Status.LatestRevision.Revision } + publishVersion := app.GetAnnotations()[types.AnnotationPublishVersion] + appRevName := fmt.Sprintf("%s-v%d", app.Name, revision) comps := make(map[string][]Resource, len(app.Spec.Components)) for _, rsrcRef := range app.Status.AppliedResources { + if c.opt.Cluster != "" && c.opt.Cluster != rsrcRef.Cluster { + continue + } compName, obj, err := getObjectCreatedByComponent(c.k8sClient, rsrcRef.ObjectReference, rsrcRef.Cluster, appRevName) if err != nil { return nil, err @@ -97,14 +103,15 @@ func (c *Collector) CollectLatestResourceFromApp() ([]AppResources, error) { } return []AppResources{{ - Revision: revision, - Metadata: app.ObjectMeta, - Components: compResList, + Revision: revision, + Metadata: app.ObjectMeta, + Components: compResList, + PublishVersion: publishVersion, }}, nil } // CollectHistoryResourceFromApp collect history resources created by application -func (c *Collector) CollectHistoryResourceFromApp() ([]AppResources, error) { +func (c *AppCollector) CollectHistoryResourceFromApp() ([]AppResources, error) { var appResList []AppResources rts, err := listResourceTrackers(c.k8sClient, c.opt.Name, c.opt.Namespace) if err != nil { @@ -149,7 +156,7 @@ func (c *Collector) CollectHistoryResourceFromApp() ([]AppResources, error) { return appResList, nil } -func (c *Collector) extractComponentResourceWithOption(comps map[string][]Resource) []Component { +func (c *AppCollector) extractComponentResourceWithOption(comps map[string][]Resource) []Component { var result []Component // if not specify component, return all components resource created by app @@ -243,14 +250,12 @@ func NewPodCollector(gvk schema.GroupVersionKind) PodCollector { return standardWorkloadPodCollector } } - - collector, ok := podCollectorMap[gvk] - if !ok { - return func(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { - return nil, nil - } + if collector, ok := podCollectorMap[gvk]; ok { + return collector + } + return func(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + return nil, nil } - return collector } // standardWorkloadPodCollector collect pods created by standard workload @@ -310,8 +315,8 @@ func cronJobPodCollector(cli client.Client, obj *unstructured.Unstructured, clus } } } - var pods []*unstructured.Unstructured + podGVK := corev1.SchemeGroupVersion.WithKind(reflect.TypeOf(corev1.Pod{}).Name()) for _, job := range jobs { labels := job.Spec.Selector.MatchLabels listOpts := []client.ListOption{ @@ -329,11 +334,7 @@ func cronJobPodCollector(cli client.Client, obj *unstructured.Unstructured, clus if err != nil { return nil, err } - pod.SetGroupVersionKind( - corev1.SchemeGroupVersion.WithKind( - reflect.TypeOf(corev1.Pod{}).Name(), - ), - ) + pod.SetGroupVersionKind(podGVK) items[i] = pod } pods = append(pods, items...) @@ -341,6 +342,70 @@ func cronJobPodCollector(cli client.Client, obj *unstructured.Unstructured, clus return pods, nil } +// HelmReleaseCollector HelmRelease resources collector +type HelmReleaseCollector struct { + matchLabels map[string]string + workloadsGVK []schema.GroupVersionKind + cli client.Client +} + +// NewHelmReleaseCollector create a HelmRelease collector +func NewHelmReleaseCollector(cli client.Client, hr *unstructured.Unstructured) *HelmReleaseCollector { + return &HelmReleaseCollector{ + // matchLabels for resources created by HelmRelease refer to + // https://github.com/fluxcd/helm-controller/blob/main/internal/runner/post_renderer_origin_labels.go#L31 + matchLabels: map[string]string{ + "helm.toolkit.fluxcd.io/name": hr.GetName(), + "helm.toolkit.fluxcd.io/namespace": hr.GetNamespace(), + }, + workloadsGVK: []schema.GroupVersionKind{ + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.Deployment{}).Name()), + }, + cli: cli, + } +} + +// CollectWorkloads collect workloads of HelmRelease +func (c *HelmReleaseCollector) CollectWorkloads(cluster string) ([]*unstructured.Unstructured, error) { + ctx := multicluster.ContextWithClusterName(context.Background(), cluster) + listOptions := []client.ListOption{ + client.MatchingLabels(c.matchLabels), + } + var workloads []*unstructured.Unstructured + for _, workloadGVK := range c.workloadsGVK { + unstructuredObjList := &unstructured.UnstructuredList{} + unstructuredObjList.SetGroupVersionKind(workloadGVK) + if err := c.cli.List(ctx, unstructuredObjList, listOptions...); err != nil { + return nil, err + } + items := unstructuredObjList.Items + for i := range items { + items[i].SetGroupVersionKind(workloadGVK) + workloads = append(workloads, &items[i]) + } + } + return workloads, nil +} + +// helmReleasePodCollector collect pods created by helmRelease +func helmReleasePodCollector(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + hc := NewHelmReleaseCollector(cli, obj) + workloads, err := hc.CollectWorkloads(cluster) + if err != nil { + return nil, err + } + var pods []*unstructured.Unstructured + for _, workload := range workloads { + collector := NewPodCollector(workload.GroupVersionKind()) + podList, err := collector(cli, workload, cluster) + if err != nil { + return nil, err + } + pods = append(pods, podList...) + } + return pods, nil +} + func getEventFieldSelector(obj *unstructured.Unstructured) fields.Selector { field := fields.Set{} field["involvedObject.name"] = obj.GetName() diff --git a/pkg/velaql/providers/query/handler.go b/pkg/velaql/providers/query/handler.go index ab58d60a2..3ac094795 100644 --- a/pkg/velaql/providers/query/handler.go +++ b/pkg/velaql/providers/query/handler.go @@ -19,6 +19,7 @@ package query import ( stdctx "context" + fluxcdv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -42,9 +43,10 @@ type provider struct { // AppResources represent resources created by app type AppResources struct { - Revision int64 `json:"revision"` - Metadata metav1.ObjectMeta `json:"metadata"` - Components []Component `json:"components"` + Revision int64 `json:"revision"` + PublishVersion string `json:"publishVersion"` + Metadata metav1.ObjectMeta `json:"metadata"` + Components []Component `json:"components"` } // Component group resources rendered by ApplicationComponent @@ -64,6 +66,7 @@ type Option struct { Name string `json:"name"` Namespace string `json:"namespace"` Components []string `json:"components,omitempty"` + Cluster string `json:"cluster,omitempty"` EnableHistoryQuery bool `json:"enableHistoryQuery,omitempty"` } @@ -77,7 +80,7 @@ func (h *provider) ListResourcesInApp(ctx wfContext.Context, v *value.Value, act if err = val.UnmarshalTo(&opt); err != nil { return err } - collector := NewCollector(h.cli, opt) + collector := NewAppCollector(h.cli, opt) appResList, err := collector.CollectResourceFromApp() if err != nil { return v.FillObject(err.Error(), "err") @@ -94,14 +97,22 @@ func (h *provider) CollectPods(ctx wfContext.Context, v *value.Value, act types. if err != nil { return err } - obj := new(unstructured.Unstructured) if err = val.UnmarshalTo(obj); err != nil { return err } - collector := NewPodCollector(obj.GroupVersionKind()) - pods, err := collector(h.cli, obj, cluster) + var pods []*unstructured.Unstructured + var collector PodCollector + + switch obj.GroupVersionKind() { + case fluxcdv2beta1.GroupVersion.WithKind(fluxcdv2beta1.HelmReleaseKind): + collector = helmReleasePodCollector + default: + collector = NewPodCollector(obj.GroupVersionKind()) + } + + pods, err = collector(h.cli, obj, cluster) if err != nil { return v.FillObject(err.Error(), "err") } diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go index 90f9b0965..9e78252c0 100644 --- a/test/e2e-apiserver-test/addon_test.go +++ b/test/e2e-apiserver-test/addon_test.go @@ -8,18 +8,16 @@ import ( "os" "time" - "github.com/oam-dev/kubevela/pkg/addon" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" + "github.com/oam-dev/kubevela/pkg/addon" + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils/common" - - apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" ) const baseURL = "http://127.0.0.1:8000" @@ -86,9 +84,9 @@ var _ = Describe("Test addon rest api", func() { }, } args := common.Args{} - client, err := args.GetClient() + k8sClient, err := args.GetClient() Expect(err).Should(BeNil()) - Expect(client.Create(context.Background(), &ns)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + Expect(k8sClient.Create(context.Background(), &ns)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) defer GinkgoRecover() req := apis.EnableAddonRequest{ diff --git a/test/e2e-apiserver-test/testdata/component-pod-view.yaml b/test/e2e-apiserver-test/testdata/component-pod-view.yaml index 0c8545bd8..fc8b431f0 100644 --- a/test/e2e-apiserver-test/testdata/component-pod-view.yaml +++ b/test/e2e-apiserver-test/testdata/component-pod-view.yaml @@ -8,7 +8,6 @@ data: import ( "vela/ql" "vela/op" - "list" ) parameter: { @@ -37,21 +36,13 @@ data: } } - podsWithCluster: {for i, pods in podsMap { - "\(i)": [ - for podObj in pods.list { - cluster: pods.cluster - obj: podObj - }, - ] - }} - - flatPods: list.FlattenN([ for pods in podsWithCluster { - pods - }], 1) + podsWithCluster: [ for i, pods in podsMap for podObj in pods.list { + cluster: pods.cluster + obj: podObj + }] podStatus: op.#Steps & { - for i, pod in flatPods { + for i, pod in podsWithCluster { "\(i)": op.#Steps & { name: pod.obj.metadata.name containers: {for container in pod.obj.status.containerStatuses { @@ -88,5 +79,3 @@ data: events: podInfo.events.list }] } - - diff --git a/test/e2e-apiserver-test/velaql_test.go b/test/e2e-apiserver-test/velaql_test.go index 1f117d9ed..964bbcfa7 100644 --- a/test/e2e-apiserver-test/velaql_test.go +++ b/test/e2e-apiserver-test/velaql_test.go @@ -228,6 +228,65 @@ var _ = Describe("Test velaQL rest api", func() { return nil }, 2*time.Minute, 3*time.Microsecond).Should(BeNil()) }) + + It("Test collect pod from helmRelease", func() { + appWithHelm := new(v1beta1.Application) + Expect(yaml.Unmarshal([]byte(podInfoApp), appWithHelm)).Should(BeNil()) + req := apiv1.ApplicationRequest{ + Components: appWithHelm.Spec.Components, + } + bodyByte, err := json.Marshal(req) + Expect(err).Should(BeNil()) + res, err := http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appWithHelm.Name), + "application/json", + bytes.NewBuffer(bodyByte), + ) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(res.StatusCode).Should(Equal(200)) + + newApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appWithHelm.Name, Namespace: namespace}, newApp); err != nil { + return err + } + if newApp.Status.Phase != common2.ApplicationRunning { + return errors.New("application is not ready") + } + return nil + }, 2*time.Minute, 1*time.Second).Should(BeNil()) + + Eventually(func() error { + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "component-pod-view", appWithHelm.Name, namespace, "podinfo", "status"), + ) + if err != nil { + return err + } + if queryRes.StatusCode != 200 { + return errors.Errorf("status code is %d", queryRes.StatusCode) + } + defer queryRes.Body.Close() + + type queryResult struct { + PodList []interface{} `json:"podList,omitempty"` + Error interface{} `json:"error,omitempty"` + } + status := new(queryResult) + err = json.NewDecoder(queryRes.Body).Decode(status) + if err != nil { + return err + } + if status.Error != nil { + return errors.Errorf("error %v", status.Error) + } + if len(status.PodList) == 0 { + return errors.New("pod list is 0") + } + return nil + }, 2*time.Minute, 300*time.Microsecond).Should(BeNil()) + }) }) var cronJobComponentDefinition = ` @@ -265,3 +324,19 @@ spec: workload: type: autodetects.core.oam.dev ` + +var podInfoApp = ` +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: podinfo +spec: + components: + - name: podinfo + type: helm + properties: + chart: podinfo + url: https://stefanprodan.github.io/podinfo + repoType: helm + version: 5.1.2 +` From bade23cecfb9c7d1e552cba3eca1277a4ce2759c Mon Sep 17 00:00:00 2001 From: qiaozp <47812250+chivalryq@users.noreply.github.com> Date: Thu, 18 Nov 2021 12:24:01 +0800 Subject: [PATCH 44/59] Feat: add addon arguments API (#2732) * temp * test * move to status --- pkg/addon/addon.go | 19 +++++++++++ pkg/apiserver/rest/apis/v1/types.go | 7 +++- pkg/apiserver/rest/usecase/addon.go | 44 +++++++++++++++++++------ pkg/apiserver/rest/utils/bcode/addon.go | 9 +++++ pkg/apiserver/rest/webservice/addon.go | 2 +- 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 8940fd801..d8e7935b4 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + v1 "k8s.io/api/core/v1" "net/url" "path" "path/filepath" @@ -438,6 +439,7 @@ func renderCUETemplate(elem types.AddonElementFile, parameters string, args map[ } const addonAppPrefix = "addon-" +const addonSecPrefix = "addon-secret-" // Convert2AppName - func Convert2AppName(name string) string { @@ -448,3 +450,20 @@ func Convert2AppName(name string) string { func Convert2AddonName(name string) string { return strings.TrimPrefix(name, addonAppPrefix) } + +func RenderArgsSecret(addon *types.Addon, args map[string]string) *v1.Secret { + sec := v1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{ + Name: Convert2SecName(addon.Name), + Namespace: types.DefaultKubeVelaNS, + }, + StringData: args, + Type: v1.SecretTypeOpaque, + } + return &sec +} + +func Convert2SecName(name string) string { + return addonSecPrefix + name +} diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 6ac439710..e52a7743e 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -102,7 +102,8 @@ type DetailAddonResponse struct { // AddonStatusResponse defines the format of addon status response type AddonStatusResponse struct { - Phase AddonPhase `json:"phase"` + Phase AddonPhase `json:"phase"` + Args map[string]string `json:"args"` EnablingProgress *EnablingProgress `json:"enabling_progress,omitempty"` } @@ -113,6 +114,10 @@ type EnablingProgress struct { TotalComponents int `json:"total_components"` } +type AddonArgsResponse struct { + Args map[string]string `json:"args"` +} + // AccessKeyRequest request parameters to access cloud provider type AccessKeyRequest struct { AccessKeyID string `json:"accessKeyID"` diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 7ca09dc36..154f83856 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + v1 "k8s.io/api/core/v1" "sort" "strings" "time" @@ -34,7 +35,7 @@ type AddonUsecase interface { UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) - StatusAddon(name string) (*apis.AddonStatusResponse, error) + StatusAddon(ctx context.Context,name string) (*apis.AddonStatusResponse, error) GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error DisableAddon(ctx context.Context, name string) error @@ -96,7 +97,7 @@ func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string, registry s return nil, bcode.ErrAddonNotExist } -func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, error) { +func (u *addonUsecaseImpl) StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error) { var app v1beta1.Application err := u.kubeClient.Get(context.Background(), client.ObjectKey{ Namespace: types.DefaultKubeVelaNS, @@ -113,11 +114,24 @@ func (u *addonUsecaseImpl) StatusAddon(name string) (*apis.AddonStatusResponse, } switch app.Status.Phase { - case common2.ApplicationRunning, common2.ApplicationWorkflowFinished: - return &apis.AddonStatusResponse{ + case common2.ApplicationRunning: + res := apis.AddonStatusResponse{ Phase: apis.AddonPhaseEnabled, EnablingProgress: nil, - }, nil + } + var sec v1.Secret + err := u.kubeClient.Get(ctx, client.ObjectKey{ + Namespace: types.DefaultKubeVelaNS, + Name: pkgaddon.Convert2SecName(name), + }, &sec) + if err != nil { + return nil, bcode.ErrAddonSecretGet + } + res.Args = make(map[string]string, len(sec.Data)) + for k, v := range sec.Data { + res.Args[k] = string(v) + } + return &res, nil default: return &apis.AddonStatusResponse{ Phase: apis.AddonPhaseEnabling, @@ -272,18 +286,28 @@ func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args ap return bcode.WrapGithubRateLimitErr(err) } - // render default ui schema - addon.UISchema = renderDefaultUISchema(addon.APISchema) - app, err := pkgaddon.RenderApplication(addon, args.Args) if err != nil { - return err + return bcode.ErrAddonRender } + + err = u.kubeClient.Get(ctx, client.ObjectKey{Namespace: app.GetNamespace(), Name: app.GetName()}, app) + if err == nil { + return bcode.ErrAddonIsEnabled + } + err = u.kubeClient.Create(ctx, app) if err != nil { - log.Logger.Errorf("apply application fail: %s", err.Error()) + log.Logger.Errorf("create application fail: %s", err.Error()) return bcode.ErrAddonApply } + + sec := pkgaddon.RenderArgsSecret(addon, args.Args) + err = u.apply.Apply(ctx, sec) + if err != nil { + return bcode.ErrAddonSecretApply + } + return nil } return bcode.ErrAddonNotExist diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go index e57f23abe..2e7566a10 100644 --- a/pkg/apiserver/rest/utils/bcode/addon.go +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -49,6 +49,15 @@ var ( // ErrGetAddonApplication fail to get addon application ErrGetAddonApplication = NewBcode(500, 50013, "fail to get addon application") + + // ErrAddonIsEnabled means addon has been enabled + ErrAddonIsEnabled = NewBcode(500, 50014, "addon has been enabled") + + // ErrAddonSecretApply means fail to apply addon argument secret + ErrAddonSecretApply = NewBcode(500, 50015, "fail to apply addon argument secret") + + // ErrAddonSecretGet means fail to get addon argument secret + ErrAddonSecretGet = NewBcode(500, 50016, "fail to get addon argument secret") ) // isGithubRateLimit check if error is github rate limit diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 6b15f77da..e41dfda32 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -168,7 +168,7 @@ func (s *addonWebService) disableAddon(req *restful.Request, res *restful.Respon func (s *addonWebService) statusAddon(req *restful.Request, res *restful.Response) { name := req.PathParameter("name") - status, err := s.addonUsecase.StatusAddon(name) + status, err := s.addonUsecase.StatusAddon(req.Request.Context(), name) if err != nil { bcode.ReturnError(req, res, err) return From b6a14e435b187304f162f3595377c85646a2bff5 Mon Sep 17 00:00:00 2001 From: Hongchao Deng Date: Thu, 18 Nov 2021 04:32:14 -0500 Subject: [PATCH 45/59] Feat: EnableAddon supports runtime cluster (#2739) * Feat: EnableAddon supports runtime cluster If use runtime cluster mode, the definitions will be applied to control plane k8s directly, not included in the Application object. * add owner * comment --- pkg/addon/addon.go | 65 ++++++++++++++++++++++------- pkg/apiserver/rest/usecase/addon.go | 48 +++++++++++---------- 2 files changed, 76 insertions(+), 37 deletions(-) diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index d8e7935b4..927b7c523 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -4,13 +4,14 @@ import ( "context" "encoding/json" "fmt" - v1 "k8s.io/api/core/v1" "net/url" "path" "path/filepath" "strings" "time" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" "cuelang.org/go/cue" @@ -336,7 +337,7 @@ func cutPathUntil(path []string, end string) ([]string, error) { } // RenderApplication render a K8s application -func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.Application, error) { +func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.Application, []*unstructured.Unstructured, error) { if args == nil { args = map[string]string{} } @@ -362,26 +363,62 @@ func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.App for _, tmpl := range addon.YAMLTemplates { comp, err := renderRawComponent(tmpl) if err != nil { - return nil, err + return nil, nil, err } app.Spec.Components = append(app.Spec.Components, *comp) } for _, tmpl := range addon.CUETemplates { comp, err := renderCUETemplate(tmpl, addon.Parameters, args) if err != nil { - return nil, ErrRenderCueTmpl - } - app.Spec.Components = append(app.Spec.Components, *comp) - } - for _, def := range addon.Definitions { - comp, err := renderRawComponent(def) - if err != nil { - return nil, err + return nil, nil, ErrRenderCueTmpl } app.Spec.Components = append(app.Spec.Components, *comp) } - return app, nil + var defObjs []*unstructured.Unstructured + + if isDeployToRuntimeOnly(addon) { + // Runtime cluster mode needs to deploy definitions to control plane k8s. + for _, def := range addon.Definitions { + obj, err := renderObject(def) + if err != nil { + return nil, nil, err + } + defObjs = append(defObjs, obj) + } + app.Spec.Workflow.Steps = append(app.Spec.Workflow.Steps, + v1beta1.WorkflowStep{ + Name: "deploy-all", + Type: "deploy2runtime", + }) + } else { + for _, def := range addon.Definitions { + comp, err := renderRawComponent(def) + if err != nil { + return nil, nil, err + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + } + + return app, defObjs, nil +} + +func isDeployToRuntimeOnly(addon *types.Addon) bool { + if addon.DeployTo == nil { + return false + } + return addon.DeployTo.RuntimeCluster +} + +func renderObject(elem types.AddonElementFile) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + _, _, err := dec.Decode([]byte(elem.Data), nil, obj) + if err != nil { + return nil, err + } + return obj, nil } // renderRawComponent will return a component in raw type from string @@ -390,9 +427,7 @@ func renderRawComponent(elem types.AddonElementFile) (*common2.ApplicationCompon Type: "raw", Name: strings.Join(append(elem.Path, elem.Name), "-"), } - obj := &unstructured.Unstructured{} - dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) - _, _, err := dec.Decode([]byte(elem.Data), nil, obj) + obj, err := renderObject(elem) if err != nil { return nil, err } diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 154f83856..86871cf7c 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -4,13 +4,14 @@ import ( "context" "errors" "fmt" - v1 "k8s.io/api/core/v1" "sort" "strings" "time" + v1 "k8s.io/api/core/v1" errors2 "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" @@ -35,7 +36,7 @@ type AddonUsecase interface { UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) - StatusAddon(ctx context.Context,name string) (*apis.AddonStatusResponse, error) + StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error) GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error DisableAddon(ctx context.Context, name string) error @@ -74,24 +75,13 @@ type addonUsecaseImpl struct { // GetAddon will get addon information func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) { - var addons []*types.Addon - cacheKey := getCacheKeyWithListOptions(registry, true, "") - if u.isRegistryCacheUpToDate(cacheKey) { - addons = u.getRegistryCache(cacheKey) - for _, a := range addons { - if a.Name == name { - return AddonImpl2AddonRes(a), nil - } - } - } else { - addonDetails, err := u.ListAddons(ctx, true, registry, "") - if err != nil { - return nil, err - } - for _, a := range addonDetails { - if a.Name == name { - return a, nil - } + addonDetails, err := u.ListAddons(ctx, true, registry, "") + if err != nil { + return nil, err + } + for _, a := range addonDetails { + if a.Name == name { + return a, nil } } return nil, bcode.ErrAddonNotExist @@ -286,7 +276,7 @@ func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args ap return bcode.WrapGithubRateLimitErr(err) } - app, err := pkgaddon.RenderApplication(addon, args.Args) + app, defs, err := pkgaddon.RenderApplication(addon, args.Args) if err != nil { return bcode.ErrAddonRender } @@ -296,12 +286,21 @@ func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args ap return bcode.ErrAddonIsEnabled } - err = u.kubeClient.Create(ctx, app) + err = u.apply.Apply(ctx, app) if err != nil { log.Logger.Errorf("create application fail: %s", err.Error()) return bcode.ErrAddonApply } + for _, def := range defs { + addOwner(def, app) + err = u.apply.Apply(ctx, def) + if err != nil { + log.Logger.Errorf("apply definition fail: %v", err) + return bcode.ErrAddonApply + } + } + sec := pkgaddon.RenderArgsSecret(addon, args.Args) err = u.apply.Apply(ctx, sec) if err != nil { @@ -313,6 +312,11 @@ func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args ap return bcode.ErrAddonNotExist } +func addOwner(child *unstructured.Unstructured, app *v1beta1.Application) { + child.SetOwnerReferences(append(child.GetOwnerReferences(), + *metav1.NewControllerRef(app, v1beta1.ApplicationKindVersionKind))) +} + func (u *addonUsecaseImpl) getRegistryCache(name string) []*types.Addon { return u.addonRegistryCache[name].GetData().([]*types.Addon) } From ffd25a4cbf73ce1e89ccab07e40d899bac2fbb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=99=93=E5=85=B5?= <596908030@qq.com> Date: Fri, 19 Nov 2021 10:37:40 +0800 Subject: [PATCH 46/59] Feat: refactor envbinding for adapt policy placement (#2731) * Feat: refactor envbinding for adapt policy placement * Fix: refactor envbinding logic * Fix: fix some bug * Fix: fix unit test * Fix: fix unit test * Fix: fix unit test * Fix: fix unit test * Fix: fix unit test ... * Fix: fix unit test Co-authored-by: zhuxiaobing --- pkg/apiserver/model/application.go | 11 - pkg/apiserver/model/envbinding.go | 57 +++ pkg/apiserver/rest/apis/v1/types.go | 31 +- pkg/apiserver/rest/usecase/application.go | 419 ++++++------------ .../rest/usecase/application_test.go | 122 +---- pkg/apiserver/rest/usecase/delivery_target.go | 12 +- pkg/apiserver/rest/usecase/envbinding.go | 288 ++++++++++++ pkg/apiserver/rest/usecase/workflow.go | 7 + pkg/apiserver/rest/utils/bcode/application.go | 2 +- .../rest/utils/bcode/delivery_target.go | 7 +- pkg/apiserver/rest/utils/bcode/envbinding.go | 32 ++ pkg/apiserver/rest/webservice/application.go | 278 ++++++------ .../rest/webservice/delivery_target.go | 25 +- pkg/apiserver/rest/webservice/webservice.go | 9 +- test/e2e-apiserver-test/application_test.go | 1 - 15 files changed, 751 insertions(+), 550 deletions(-) create mode 100644 pkg/apiserver/model/envbinding.go create mode 100644 pkg/apiserver/rest/usecase/envbinding.go create mode 100644 pkg/apiserver/rest/utils/bcode/envbinding.go diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index e26e47e95..7538634ec 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -36,7 +36,6 @@ type Application struct { Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` - EnvBinding []*EnvBinding `json:"envBinding,omitempty"` } // TableName return custom table name @@ -61,16 +60,6 @@ func (a *Application) Index() map[string]string { return index } -// EnvBinding application env binding -type EnvBinding struct { - Name string `json:"name"` - Alias string `json:"alias"` - Description string `json:"description,omitempty"` - TargetNames []string `json:"targetNames"` - ComponentSelector *ComponentSelector `json:"componentSelector"` - //TODO: componentPatchs -} - // ClusterSelector cluster selector type ClusterSelector struct { Name string `json:"name"` diff --git a/pkg/apiserver/model/envbinding.go b/pkg/apiserver/model/envbinding.go new file mode 100644 index 000000000..c4821521e --- /dev/null +++ b/pkg/apiserver/model/envbinding.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 model + +import "fmt" + +func init() { + RegistModel(&EnvBinding{}) +} + +// EnvBinding application env binding +type EnvBinding struct { + Model + AppPrimaryKey string `json:"appPrimaryKey"` + Name string `json:"name"` + Alias string `json:"alias"` + Description string `json:"description,omitempty"` + TargetNames []string `json:"targetNames"` + ComponentSelector *ComponentSelector `json:"componentSelector"` + //TODO: componentPatchs +} + +// TableName return custom table name +func (e *EnvBinding) TableName() string { + return tableNamePrefix + "envbinding" +} + +// PrimaryKey return custom primary key +func (e *EnvBinding) PrimaryKey() string { + return fmt.Sprintf("%s-%s", e.AppPrimaryKey, e.Name) +} + +// Index return custom index +func (e *EnvBinding) Index() map[string]string { + index := make(map[string]string) + if e.Name != "" { + index["name"] = e.Name + } + if e.AppPrimaryKey != "" { + index["appPrimaryKey"] = e.AppPrimaryKey + } + return index +} diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index e52a7743e..0afde2958 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -253,7 +253,6 @@ type ApplicationBase struct { UpdateTime time.Time `json:"updateTime"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` - EnvBinding EnvBindingList `json:"envBinding,omitempty"` } // ApplicationStatusResponse application status response body @@ -292,6 +291,21 @@ type EnvBinding struct { ComponentSelector *ComponentSelector `json:"componentSelector" optional:"true"` } +// EnvBindingBase application env binding +type EnvBindingBase struct { + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + TargetNames []string `json:"targetNames"` + ComponentSelector *ComponentSelector `json:"componentSelector" optional:"true"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +type DetailEnvBindingResponse struct { + EnvBindingBase +} + // ClusterSelector cluster selector type ClusterSelector struct { Name string `json:"name" validate:"checkname"` @@ -308,6 +322,7 @@ type ComponentSelector struct { type DetailApplicationResponse struct { ApplicationBase Policies []string `json:"policies"` + EnvBindings []string `json:"envBindings"` Status string `json:"status"` ResourceInfo ApplicationResourceInfo `json:"resourceInfo"` WorkflowStatus []WorkflowStepStatus `json:"workflowStatus"` @@ -500,6 +515,7 @@ type CreateWorkflowRequest struct { Description string `json:"description" optional:"true"` Steps []WorkflowStep `json:"steps,omitempty"` Default bool `json:"default"` + EnvName string `json:"envName"` } // UpdateWorkflowRequest update or create application workflow @@ -509,6 +525,7 @@ type UpdateWorkflowRequest struct { Steps []WorkflowStep `json:"steps,omitempty"` Enable bool `json:"enable"` Default bool `json:"default"` + EnvName string `json:"envName"` } // WorkflowStep workflow step config @@ -543,6 +560,7 @@ type WorkflowBase struct { Description string `json:"description"` Enable bool `json:"enable"` Default bool `json:"default"` + EnvName string `json:"envName"` CreateTime time.Time `json:"createTime"` UpdateTime time.Time `json:"updateTime"` } @@ -595,9 +613,14 @@ type VelaQLViewResponse map[string]interface{} // PutApplicationEnvRequest set diff request type PutApplicationEnvRequest struct { ComponentSelector *ComponentSelector `json:"componentSelector,omitempty"` - Alias *string `json:"alias,omitempty" validate:"checkalias" optional:"true"` - Description *string `json:"description,omitempty" optional:"true"` - TargetNames *[]string `json:"targetNames"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + TargetNames []string `json:"targetNames"` +} + +// ListApplicationEnvBinding list app envBindings +type ListApplicationEnvBinding struct { + EnvBindings []*EnvBindingBase `json:"envBindings"` } // CreateApplicationEnvRequest new application env diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index ce96d9d8a..0f0a90092 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -77,10 +77,6 @@ type ApplicationUsecase interface { DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) DeletePolicy(ctx context.Context, app *model.Application, policyName string) error UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) - GetApplicationEnvBindingPolicy(ctx context.Context, app *model.Application) (*v1alpha1.EnvBindingSpec, error) - UpdateApplicationEnvBinding(ctx context.Context, app *model.Application, envName string, diff apisv1.PutApplicationEnvRequest) (*apisv1.EnvBinding, error) - CreateApplicationEnvBinding(ctx context.Context, app *model.Application, env apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) - DeleteApplicationEnvBinding(ctx context.Context, app *model.Application, envName string) error CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string, req apisv1.UpdateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) @@ -89,23 +85,27 @@ type ApplicationUsecase interface { } type applicationUsecaseImpl struct { - ds datastore.DataStore - kubeClient client.Client - apply apply.Applicator - workflowUsecase WorkflowUsecase + ds datastore.DataStore + kubeClient client.Client + apply apply.Applicator + workflowUsecase WorkflowUsecase + envBindingUsecase EnvBindingUsecase + deliveryTargetUsecase DeliveryTargetUsecase } // NewApplicationUsecase new application usecase -func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase) ApplicationUsecase { +func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase, envBindingUsecase EnvBindingUsecase, deliveryTargetUsecase DeliveryTargetUsecase) ApplicationUsecase { kubecli, err := clients.GetKubeClient() if err != nil { log.Logger.Fatalf("get kubeclient failure %s", err.Error()) } return &applicationUsecaseImpl{ - ds: ds, - workflowUsecase: workflowUsecase, - kubeClient: kubecli, - apply: apply.NewAPIApplicator(kubecli), + ds: ds, + workflowUsecase: workflowUsecase, + envBindingUsecase: envBindingUsecase, + deliveryTargetUsecase: deliveryTargetUsecase, + kubeClient: kubecli, + apply: apply.NewAPIApplicator(kubecli), } } @@ -128,8 +128,11 @@ func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptio strings.Contains(appBase.Description, listOptions.Query)) { continue } - if listOptions.TargetName != "" && !appBase.EnvBinding.ContainTarget(listOptions.TargetName) { - continue + if listOptions.TargetName != "" { + targetIsContain, _ := c.envBindingUsecase.CheckAppEnvBindingsContainTarget(ctx, &app, listOptions.TargetName) + if targetIsContain { + continue + } } list = append(list, appBase) } @@ -161,16 +164,22 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod return nil, err } components, err := c.ListComponents(ctx, app, apisv1.ListApplicationComponentOptions{}) + envBindings, err := c.envBindingUsecase.GetEnvBindings(ctx, app) if err != nil { return nil, err } var policyNames []string + var envBindingNames []string for _, p := range policys { policyNames = append(policyNames, p.Name) } + for _, e := range envBindings { + envBindingNames = append(envBindingNames, e.Name) + } var detail = &apisv1.DetailApplicationResponse{ ApplicationBase: *base, Policies: policyNames, + EnvBindings: envBindingNames, ResourceInfo: apisv1.ApplicationResourceInfo{ ComponentNum: len(components), }, @@ -236,45 +245,22 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis } } if oamApp.Spec.Workflow != nil && len(oamApp.Spec.Workflow.Steps) > 0 { - var steps []apisv1.WorkflowStep - for _, step := range oamApp.Spec.Workflow.Steps { - var propertyStr string - if step.Properties != nil { - properties, err := model.NewJSONStruct(step.Properties) - if err != nil { - log.Logger.Errorf("workflow %s step %s properties is invalid %s", application.Name, step.Name, err.Error()) - continue - } - propertyStr = properties.JSON() - } - steps = append(steps, apisv1.WorkflowStep{ - Name: step.Name, - Type: step.Type, - DependsOn: step.DependsOn, - Properties: propertyStr, - Inputs: step.Inputs, - Outputs: step.Outputs, - }) - } - _, err := c.workflowUsecase.CreateWorkflow(ctx, &application, apisv1.CreateWorkflowRequest{ - AppName: application.PrimaryKey(), - Name: application.Name, - Description: "Created automatically.", - Steps: steps, - Default: true, - }) - if err != nil { + if err := c.saveApplicationWorkflow(ctx, &application, oamApp.Spec.Workflow.Steps, application.Name); err != nil { + log.Logger.Errorf("save applictaion polocies failure,%s", err.Error()) return nil, err } } + // TODO Waiting for Spec.EnvBinding support } - // build-in create env binding policy + // build-in create env binding if len(req.EnvBinding) > 0 { - if _, err := c.createApplictionEnvBindingPolicy(ctx, &application, req.EnvBinding); err != nil { + err := c.saveApplicationEnvBinding(ctx, application, req.EnvBinding) + if err != nil { return nil, err } } + if req.Component != nil { _, err = c.AddComponent(ctx, &application, *req.Component) if err != nil { @@ -293,6 +279,70 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return base, nil } +func (c *applicationUsecaseImpl) genPolicyByEnv(ctx context.Context, app *model.Application, envName string) (v1beta1.AppPolicy, error) { + appPolicy := v1beta1.AppPolicy{} + envBinding, err := c.envBindingUsecase.GetEnvBinding(ctx, app, envName) + if err != nil { + return appPolicy, err + } + appPolicy.Name = genPolicyName(envBinding.Name) + appPolicy.Type = string(EnvBindingPolicy) + + var envBindingSpec v1alpha1.EnvBindingSpec + for _, targetName := range envBinding.TargetNames { + target, err := c.deliveryTargetUsecase.GetDeliveryTarget(ctx, targetName) + if err != nil || target == nil { + return appPolicy, bcode.ErrFoundEnvbindingDeliveryTarget + } + envBindingSpec.Envs = append(envBindingSpec.Envs, createTargetClusterEnv(envBinding.EnvBindingBase, target)) + } + properties, err := model.NewJSONStructByStruct(envBindingSpec) + appPolicy.Properties = properties.RawExtension() + return appPolicy, nil +} + +func (c *applicationUsecaseImpl) saveApplicationWorkflow(ctx context.Context, application *model.Application, workflowSteps []v1beta1.WorkflowStep, workflowName string) error { + var steps []apisv1.WorkflowStep + for _, step := range workflowSteps { + var propertyStr string + if step.Properties != nil { + properties, err := model.NewJSONStruct(step.Properties) + if err != nil { + log.Logger.Errorf("workflow %s step %s properties is invalid %s", application.Name, step.Name, err.Error()) + continue + } + propertyStr = properties.JSON() + } + steps = append(steps, apisv1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + DependsOn: step.DependsOn, + Properties: propertyStr, + Inputs: step.Inputs, + Outputs: step.Outputs, + }) + } + _, err := c.workflowUsecase.CreateWorkflow(ctx, application, apisv1.CreateWorkflowRequest{ + AppName: application.PrimaryKey(), + Name: workflowName, + Description: "Created automatically.", + Steps: steps, + Default: true, + }) + if err != nil { + return err + } + return nil +} + +func (c *applicationUsecaseImpl) saveApplicationEnvBinding(ctx context.Context, app model.Application, envBindings []*apisv1.EnvBinding) error { + err := c.envBindingUsecase.BatchCreateEnvBinding(ctx, &app, envBindings) + if err != nil { + return err + } + return nil +} + func (c *applicationUsecaseImpl) UpdateApplication(ctx context.Context, app *model.Application, req apisv1.UpdateApplicationRequest) (*apisv1.ApplicationBase, error) { app.Alias = req.Alias app.Description = req.Description @@ -354,15 +404,15 @@ func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model. envComponents := map[string]bool{} componentSelectorDefine := false if op.EnvName != "" { - envbinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) + envbindings, err := c.envBindingUsecase.GetEnvBindings(ctx, app) if err != nil && !errors.Is(err, bcode.ErrApplicationNotEnv) { log.Logger.Errorf("query app env binding policy config failure %s", err.Error()) } - if envbinding != nil { - for _, env := range envbinding.Envs { - if env.Selector != nil && env.Name == op.EnvName { + if len(envbindings) > 0 { + for _, env := range envbindings { + if env != nil && env.Name == op.EnvName { componentSelectorDefine = true - for _, componentName := range env.Selector.Components { + for _, componentName := range env.ComponentSelector.Components { envComponents[componentName] = true } } @@ -474,7 +524,6 @@ func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app if env.Selector != nil { envBind.ComponentSelector = (*model.ComponentSelector)(env.Selector) } - app.EnvBinding = append(app.EnvBinding, envBind) } } return c.ds.BatchAdd(ctx, policyModels) @@ -495,54 +544,6 @@ func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, ap return } -func (c *applicationUsecaseImpl) GetApplicationEnvBindingPolicy(ctx context.Context, app *model.Application) (*v1alpha1.EnvBindingSpec, error) { - var policy = model.ApplicationPolicy{ - AppPrimaryKey: app.PrimaryKey(), - Type: string(EnvBindingPolicy), - Name: EnvBindingPolicyDefaultName, - } - err := c.ds.Get(ctx, &policy) - if err != nil { - if errors.Is(err, datastore.ErrRecordNotExist) { - return nil, bcode.ErrApplicationNotEnv - } - return nil, err - } - var envBindingSpec v1alpha1.EnvBindingSpec - if err := json.Unmarshal([]byte(policy.Properties.JSON()), &envBindingSpec); err != nil { - return nil, err - } - return &envBindingSpec, nil -} - -// nolint -func (c *applicationUsecaseImpl) createApplictionEnvBindingPolicy(ctx context.Context, app *model.Application, envbinds apisv1.EnvBindingList) (*model.ApplicationPolicy, error) { - policy := &model.ApplicationPolicy{ - AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindingPolicyDefaultName, - Description: "build-in create", - Type: string(EnvBindingPolicy), - Creator: "", - } - var envBindingSpec v1alpha1.EnvBindingSpec - for _, envBind := range envbinds { - // TODO: check delivery target - envBindingSpec.Envs = append(envBindingSpec.Envs, createEnvBind(*envBind)) - app.EnvBinding = append(app.EnvBinding, createModelEnvBind(*envBind)) - } - properties, err := model.NewJSONStructByStruct(envBindingSpec) - if err != nil { - log.Logger.Errorf("new env binding properties failure,%s", err.Error()) - return nil, bcode.ErrInvalidProperties - } - policy.Properties = properties - if err := c.ds.Add(ctx, policy); err != nil { - log.Logger.Errorf("save env binding policy failure,%s", err.Error()) - return nil, err - } - return policy, nil -} - // DetailPolicy detail app policy // TODO: Add status data about the policy. func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) { @@ -727,6 +728,14 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo if err != nil { return nil, err } + if workflow.EnvName != "" { + envPolicy, err := c.genPolicyByEnv(ctx, appModel, workflow.EnvName) + if err != nil { + return nil, err + } + app.Spec.Policies = append(app.Spec.Policies, envPolicy) + } + } else { workflow, err = c.workflowUsecase.GetApplicationDefaultWorkflow(ctx, appModel) if err != nil && !errors.Is(err, bcode.ErrWorkflowNoDefault) { @@ -747,11 +756,13 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo if step.Properties != nil { wstep.Properties = step.Properties.RawExtension() } + steps = append(steps, wstep) } app.Spec.Workflow = &v1beta1.Workflow{ Steps: steps, } } + return app, nil } @@ -766,18 +777,6 @@ func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *a Icon: app.Icon, Labels: app.Labels, } - for _, envBind := range app.EnvBinding { - apiEnvBind := &apisv1.EnvBinding{ - Name: envBind.Name, - Alias: envBind.Alias, - Description: envBind.Description, - TargetNames: envBind.TargetNames, - } - if envBind.ComponentSelector != nil { - apiEnvBind.ComponentSelector = (*apisv1.ComponentSelector)(envBind.ComponentSelector) - } - appBase.EnvBinding = append(appBase.EnvBinding, apiEnvBind) - } return appBase } @@ -795,6 +794,7 @@ func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *mod if err != nil { return err } + // delete workflow if err := c.workflowUsecase.DeleteWorkflow(ctx, app.Name); err != nil && !errors.Is(err, bcode.ErrWorkflowNotExist) { log.Logger.Errorf("delete workflow %s failure %s", app.Name, err.Error()) @@ -814,6 +814,10 @@ func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *mod } } + if err := c.envBindingUsecase.BatchDeleteEnvBinding(ctx, app); err != nil { + log.Logger.Errorf("delete envbindings in app %s failure %s", app.Name, err.Error()) + } + return c.ds.Delete(ctx, app) } @@ -946,166 +950,6 @@ func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.Ap }, nil } -// UpdateApplicationEnvBinding update application env binding diff -func (c *applicationUsecaseImpl) UpdateApplicationEnvBinding( - ctx context.Context, - app *model.Application, - envName string, - envUpdate apisv1.PutApplicationEnvRequest) (*apisv1.EnvBinding, error) { - // update env-binding policy - envBinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) - if err != nil { - return nil, err - } - for i, env := range envBinding.Envs { - if env.Name == envName { - if envUpdate.ComponentSelector == nil { - envBinding.Envs[i].Selector = nil - } else { - envBinding.Envs[i].Selector = &v1alpha1.EnvSelector{ - Components: envUpdate.ComponentSelector.Components, - } - } - - } - } - properties, err := model.NewJSONStructByStruct(envBinding) - if err != nil { - log.Logger.Errorf("new env binding properties failure,%s", err.Error()) - return nil, bcode.ErrInvalidProperties - } - policy := &model.ApplicationPolicy{ - AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindingPolicyDefaultName, - } - if err := c.ds.Get(ctx, policy); err != nil { - return nil, err - } - policy.Properties = properties - if err := c.ds.Put(ctx, policy); err != nil { - return nil, err - } - var envBind model.EnvBinding - // update env-binding base - for i, env := range app.EnvBinding { - if env.Name == envName { - if envUpdate.Description != nil { - app.EnvBinding[i].Description = *envUpdate.Description - } - if envUpdate.Alias != nil { - app.EnvBinding[i].Alias = *envUpdate.Alias - } - if envUpdate.TargetNames != nil { - app.EnvBinding[i].TargetNames = *envUpdate.TargetNames - } - if envUpdate.ComponentSelector == nil { - app.EnvBinding[i].ComponentSelector = nil - } else { - app.EnvBinding[i].ComponentSelector = &model.ComponentSelector{ - Components: envUpdate.ComponentSelector.Components, - } - } - envBind = *app.EnvBinding[i] - } - } - if err := c.ds.Put(ctx, app); err != nil { - return nil, err - } - re := &apisv1.EnvBinding{ - Name: envBind.Name, - Alias: envBind.Alias, - Description: envBind.Description, - TargetNames: envBind.TargetNames, - } - if envBind.ComponentSelector != nil { - re.ComponentSelector = (*apisv1.ComponentSelector)(envBind.ComponentSelector) - } - return re, nil -} - -// CreateApplicationEnvBinding create application env -func (c *applicationUsecaseImpl) CreateApplicationEnvBinding(ctx context.Context, app *model.Application, envReq apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) { - for _, env := range app.EnvBinding { - if env.Name == envReq.Name { - return nil, bcode.ErrApplicationEnvExist - } - } - envBinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) - if err != nil { - if !errors.Is(err, bcode.ErrApplicationNotEnv) { - return nil, err - } - } - if envBinding == nil { - _, err := c.createApplictionEnvBindingPolicy(ctx, app, []*apisv1.EnvBinding{&envReq.EnvBinding}) - if err != nil { - return nil, err - } - } else { - app.EnvBinding = append(app.EnvBinding, createModelEnvBind(envReq.EnvBinding)) - envBinding.Envs = append(envBinding.Envs, createEnvBind(envReq.EnvBinding)) - properties, err := model.NewJSONStructByStruct(envBinding) - if err != nil { - log.Logger.Errorf("new env binding properties failure,%s", err.Error()) - return nil, bcode.ErrInvalidProperties - } - policy := &model.ApplicationPolicy{ - AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindingPolicyDefaultName, - } - if err := c.ds.Get(ctx, policy); err != nil { - return nil, err - } - policy.Properties = properties - if err := c.ds.Put(ctx, policy); err != nil { - return nil, err - } - } - if err := c.ds.Put(ctx, app); err != nil { - return nil, err - } - return &envReq.EnvBinding, nil -} - -// DeleteApplicationEnvBinding delete application env binding -func (c *applicationUsecaseImpl) DeleteApplicationEnvBinding(ctx context.Context, app *model.Application, envName string) error { - - for i, envBind := range app.EnvBinding { - if envBind.Name == envName { - app.EnvBinding = append(app.EnvBinding[0:i], app.EnvBinding[i+1:]...) - } - } - envBinding, err := c.GetApplicationEnvBindingPolicy(ctx, app) - if err != nil { - return err - } - for i, envBind := range envBinding.Envs { - if envBind.Name == envName { - envBinding.Envs = append(envBinding.Envs[0:i], envBinding.Envs[i+1:]...) - } - } - properties, err := model.NewJSONStructByStruct(envBinding) - if err != nil { - log.Logger.Errorf("new env binding properties failure,%s", err.Error()) - return bcode.ErrInvalidProperties - } - policy := &model.ApplicationPolicy{ - AppPrimaryKey: app.PrimaryKey(), - Name: EnvBindingPolicyDefaultName, - } - if err := c.ds.Get(ctx, policy); err != nil { - return err - } - policy.Properties = properties - if err := c.ds.Put(ctx, policy); err != nil { - return err - } - if err := c.ds.Put(ctx, app); err != nil { - return err - } - return nil -} - func (c *applicationUsecaseImpl) CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) { var comp = model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), @@ -1235,7 +1079,7 @@ func (c *applicationUsecaseImpl) DetailRevision(ctx context.Context, appName, re }, nil } -func createEnvBind(envBind apisv1.EnvBinding) v1alpha1.EnvConfig { +func createTargetClusterEnv(envBind apisv1.EnvBindingBase, target *model.DeliveryTarget) v1alpha1.EnvConfig { placement := v1alpha1.EnvPlacement{} var componentSelector *v1alpha1.EnvSelector if envBind.ComponentSelector != nil { @@ -1243,26 +1087,29 @@ func createEnvBind(envBind apisv1.EnvBinding) v1alpha1.EnvConfig { Components: envBind.ComponentSelector.Components, } } + if target.Cluster != nil { + placement.ClusterSelector = &common.ClusterSelector{Name: target.Cluster.ClusterName} + placement.NamespaceSelector = &v1alpha1.NamespaceSelector{Name: target.Cluster.Namespace} + } return v1alpha1.EnvConfig{ - Name: envBind.Name, + Name: genPolicyEnvName(target.Name), Placement: placement, Selector: componentSelector, } } -func createModelEnvBind(envBind apisv1.EnvBinding) *model.EnvBinding { - re := model.EnvBinding{ - Name: envBind.Name, - Description: envBind.Description, - Alias: envBind.Alias, - TargetNames: envBind.TargetNames, - } - if envBind.ComponentSelector != nil { - re.ComponentSelector = (*model.ComponentSelector)(envBind.ComponentSelector) - } - return &re -} - func converAppName(app *model.Application, envName string) string { return fmt.Sprintf("%s-%s", app.Name, envName) } + +func genPolicyName(envName string) string { + return fmt.Sprintf("%s-%s", EnvBindingPolicyDefaultName, envName) +} + +func genWorkflowName(app *model.Application, envName string) string { + return fmt.Sprintf("%s-%s", app.Name, envName) +} + +func genPolicyEnvName(targetName string) string { + return targetName +} diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 91548b388..6178cd3c3 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -37,16 +37,22 @@ import ( var _ = Describe("Test application usecase function", func() { var ( - appUsecase *applicationUsecaseImpl - workflowUsecase *workflowUsecaseImpl + appUsecase *applicationUsecaseImpl + workflowUsecase *workflowUsecaseImpl + envBindingUsecase *envBindingUsecaseImpl + deliveryTargetUsecase *deliveryTargetUsecaseImpl ) BeforeEach(func() { workflowUsecase = &workflowUsecaseImpl{ds: ds} + envBindingUsecase = &envBindingUsecaseImpl{ds: ds, workflowUsecase: workflowUsecase} + deliveryTargetUsecase = &deliveryTargetUsecaseImpl{ds: ds} appUsecase = &applicationUsecaseImpl{ - ds: ds, - workflowUsecase: workflowUsecase, - apply: apply.NewAPIApplicator(k8sClient), - kubeClient: k8sClient, + ds: ds, + workflowUsecase: workflowUsecase, + apply: apply.NewAPIApplicator(k8sClient), + kubeClient: k8sClient, + envBindingUsecase: envBindingUsecase, + deliveryTargetUsecase: deliveryTargetUsecase, } }) It("Test CreateApplication function", func() { @@ -138,7 +144,7 @@ var _ = Describe("Test application usecase function", func() { } appBase, err := appUsecase.CreateApplication(context.TODO(), req) Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(appBase.EnvBinding), 2)).Should(BeEmpty()) + Expect(cmp.Diff(appBase.Name, "test-app-sadasd4")).Should(BeEmpty()) appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") Expect(err).Should(BeNil()) @@ -146,23 +152,6 @@ var _ = Describe("Test application usecase function", func() { }) - It("Test GetApplicationEnvBindingingPolicy", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") - Expect(err).Should(BeNil()) - envBinding, err := appUsecase.GetApplicationEnvBindingPolicy(context.TODO(), appModel) - Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(envBinding.Envs), 2)).Should(BeEmpty()) - }) - - It("Test UpdateApplicationEnvBindingingDiff", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - _, err = appUsecase.UpdateApplicationEnvBinding(context.TODO(), appModel, "staging", v1.PutApplicationEnvRequest{ - ComponentSelector: &v1.ComponentSelector{Components: []string{"hello-world-server"}}, - }) - Expect(err).Should(BeNil()) - }) - It("Test ListApplications function", func() { apps, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioOptions{}) Expect(err).Should(BeNil()) @@ -216,15 +205,15 @@ var _ = Describe("Test application usecase function", func() { EnvName: "test", }) Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(components), 1)).Should(BeEmpty()) + Expect(cmp.Diff(len(components), 2)).Should(BeEmpty()) Expect(cmp.Diff(components[0].Name, "data-worker")).Should(BeEmpty()) components, err = appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{ EnvName: "staging", }) Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(components), 1)).Should(BeEmpty()) - Expect(cmp.Diff(components[0].Name, "hello-world-server")).Should(BeEmpty()) + Expect(cmp.Diff(len(components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].Name, "data-worker")).Should(BeEmpty()) }) It("Test DetailComponent function", func() { @@ -408,85 +397,6 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) }) - It("Test CreateApplicationEnvBinding function", func() { - req := v1.CreateApplicationRequest{ - Name: "not-have-env-bind", - Namespace: "test-app-namespace", - Description: "this is a test app", - Icon: "", - Labels: map[string]string{"test": "true"}, - } - _, err := appUsecase.CreateApplication(context.TODO(), req) - Expect(err).Should(BeNil()) - appModel4, err := appUsecase.GetApplication(context.TODO(), "not-have-env-bind") - Expect(err).Should(BeNil()) - By("test create first env") - env4, err := appUsecase.CreateApplicationEnvBinding(context.TODO(), appModel4, v1.CreateApplicationEnvRequest{ - EnvBinding: v1.EnvBinding{ - Name: "prod2", - Alias: "生产环境", - Description: "这是一个用户某客户的生产环境", - TargetNames: []string{"prod-target"}, - }, - }) - Expect(err).Should(BeNil()) - Expect(env4).ShouldNot(BeNil()) - - appModelNew, err := appUsecase.GetApplication(context.TODO(), "not-have-env-bind") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(appModelNew.EnvBinding), 1)).Should(BeEmpty()) - - By("test create not first env") - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - env, err := appUsecase.CreateApplicationEnvBinding(context.TODO(), appModel, v1.CreateApplicationEnvRequest{ - EnvBinding: v1.EnvBinding{ - Name: "prod2", - Alias: "生产环境", - Description: "这是一个用户某客户的生产环境", - TargetNames: []string{"prod-target"}, - }, - }) - Expect(err).Should(BeNil()) - Expect(env).ShouldNot(BeNil()) - - appModelNew, err = appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(appModelNew.EnvBinding), 4)).Should(BeEmpty()) - - spec, err := appUsecase.GetApplicationEnvBindingPolicy(context.TODO(), appModelNew) - Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(spec.Envs), 4)).Should(BeEmpty()) - }) - - It("Test UpdateApplicationEnvBinding function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - env, err := appUsecase.UpdateApplicationEnvBinding(context.TODO(), appModel, "prod2", v1.PutApplicationEnvRequest{ - ComponentSelector: &v1.ComponentSelector{ - Components: []string{}, - }, - }) - Expect(err).Should(BeNil()) - Expect(env).ShouldNot(BeNil()) - - components, err := appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{ - EnvName: "prod2", - }) - Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(components), 0)).Should(BeEmpty()) - }) - - It("Test DeleteApplicationEnvBindinging function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - err = appUsecase.DeleteApplicationEnvBinding(context.TODO(), appModel, "prod2") - Expect(err).Should(BeNil()) - }) - It("Test Deploy Application function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) diff --git a/pkg/apiserver/rest/usecase/delivery_target.go b/pkg/apiserver/rest/usecase/delivery_target.go index 8b0d470e4..fe2353332 100644 --- a/pkg/apiserver/rest/usecase/delivery_target.go +++ b/pkg/apiserver/rest/usecase/delivery_target.go @@ -37,15 +37,17 @@ type DeliveryTargetUsecase interface { ListDeliveryTargets(ctx context.Context, page, pageSize int, namespace string) (*apisv1.ListDeliveryTargetResponse, error) } -// NewDeliveryTargetUsecase new DeliveryTarget usecase -func NewDeliveryTargetUsecase(ds datastore.DataStore) DeliveryTargetUsecase { - return &deliveryTargetUsecaseImpl{ds: ds} -} - type deliveryTargetUsecaseImpl struct { ds datastore.DataStore } +// NewDeliveryTargetUsecase new DeliveryTarget usecase +func NewDeliveryTargetUsecase(ds datastore.DataStore) DeliveryTargetUsecase { + return &deliveryTargetUsecaseImpl{ + ds: ds, + } +} + func (dt *deliveryTargetUsecaseImpl) ListDeliveryTargets(ctx context.Context, page, pageSize int, namespace string) (*apisv1.ListDeliveryTargetResponse, error) { deliveryTarget := model.DeliveryTarget{} if namespace != "" { diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go new file mode 100644 index 000000000..cd34fd976 --- /dev/null +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -0,0 +1,288 @@ +/* +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" + "errors" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/oam/util" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +// EnvBindingUsecase envbinding usecase +type EnvBindingUsecase interface { + GetEnvBindings(ctx context.Context, app *model.Application) ([]*apisv1.EnvBindingBase, error) + GetEnvBinding(ctx context.Context, app *model.Application, envName string) (*apisv1.DetailEnvBindingResponse, error) + CheckAppEnvBindingsContainTarget(ctx context.Context, app *model.Application, targetName string) (bool, error) + CreateEnvBinding(ctx context.Context, app *model.Application, env apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) + BatchCreateEnvBinding(ctx context.Context, app *model.Application, env apisv1.EnvBindingList) error + UpdateEnvBinding(ctx context.Context, app *model.Application, envName string, diff apisv1.PutApplicationEnvRequest) (*apisv1.DetailEnvBindingResponse, error) + DeleteEnvBinding(ctx context.Context, app *model.Application, envName string) error + BatchDeleteEnvBinding(ctx context.Context, app *model.Application) error + DetailEnvBinding(ctx context.Context, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) +} + +type envBindingUsecaseImpl struct { + ds datastore.DataStore + workflowUsecase WorkflowUsecase +} + +// NewEnvBindingUsecase new envBinding usecase +func NewEnvBindingUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase) EnvBindingUsecase { + return &envBindingUsecaseImpl{ + ds: ds, + workflowUsecase: workflowUsecase, + } +} + +func (e *envBindingUsecaseImpl) GetEnvBindings(ctx context.Context, app *model.Application) ([]*apisv1.EnvBindingBase, error) { + var envBinding = model.EnvBinding{ + AppPrimaryKey: app.PrimaryKey(), + } + envBindings, err := e.ds.List(ctx, &envBinding, &datastore.ListOptions{}) + if err != nil { + return nil, bcode.ErrEnvBindingsNotExist + } + var list []*apisv1.EnvBindingBase + for _, ebd := range envBindings { + eb := ebd.(*model.EnvBinding) + list = append(list, convertEnvbindingModelToBase(eb)) + } + return list, nil +} + +func (e *envBindingUsecaseImpl) GetEnvBinding(ctx context.Context, app *model.Application, envName string) (*apisv1.DetailEnvBindingResponse, error) { + envBinding, err := e.getBindingByEnv(ctx, app, envName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrEnvBindingsNotExist + } + return nil, err + } + return e.DetailEnvBinding(ctx, envBinding) +} + +func (e *envBindingUsecaseImpl) CheckAppEnvBindingsContainTarget(ctx context.Context, app *model.Application, targetName string) (bool, error) { + envBindings, err := e.GetEnvBindings(ctx, app) + if err != nil { + return false, err + } + var filteredList []*apisv1.EnvBindingBase + for _, envBinding := range envBindings { + if utils.StringsContain(envBinding.TargetNames, targetName) { + filteredList = append(filteredList, envBinding) + } + } + return len(filteredList) > 0, nil +} + +func (e *envBindingUsecaseImpl) CreateEnvBinding(ctx context.Context, app *model.Application, envReq apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) { + envBinding, err := e.getBindingByEnv(ctx, app, envReq.Name) + if err != nil { + if !errors.Is(err, datastore.ErrRecordNotExist) { + return nil, err + } + } + if envBinding != nil { + return nil, bcode.ErrEnvBindingExist + } else { + envBindingModel := convertCreateReqToEnvBindingModel(app, envReq) + if err := e.ds.Add(ctx, &envBindingModel); err != nil { + return nil, err + } + err := e.createEnvWorkflow(ctx, app, &envBindingModel) + if err != nil { + return nil, err + } + } + return &envReq.EnvBinding, nil +} + +func (e *envBindingUsecaseImpl) BatchCreateEnvBinding(ctx context.Context, app *model.Application, envbindings apisv1.EnvBindingList) error { + for _, envBinding := range envbindings { + envBindingModel := convertToEnvBindingModel(app, *envBinding) + if err := e.ds.Add(ctx, envBindingModel); err != nil { + return err + } + err := e.createEnvWorkflow(ctx, app, envBindingModel) + if err != nil { + return err + } + } + return nil +} + +func (e *envBindingUsecaseImpl) getBindingByEnv(ctx context.Context, app *model.Application, envName string) (*model.EnvBinding, error) { + var envBinding = model.EnvBinding{ + AppPrimaryKey: app.PrimaryKey(), + Name: envName, + } + err := e.ds.Get(ctx, &envBinding) + if err != nil { + return nil, err + } + return &envBinding, nil +} + +func (e *envBindingUsecaseImpl) UpdateEnvBinding(ctx context.Context, app *model.Application, envName string, envUpdate apisv1.PutApplicationEnvRequest) (*apisv1.DetailEnvBindingResponse, error) { + envBinding, err := e.getBindingByEnv(ctx, app, envName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrEnvBindingNotExist + } + return nil, err + } + envBindingModel := &model.EnvBinding{ + Name: envBinding.Name, + Alias: envUpdate.Alias, + Description: envUpdate.Description, + TargetNames: envUpdate.TargetNames, + } + if envBinding.ComponentSelector != nil { + envBindingModel.ComponentSelector = envBinding.ComponentSelector + } + if err := e.ds.Put(ctx, envBindingModel); err != nil { + return nil, err + } + return e.DetailEnvBinding(ctx, envBindingModel) +} + +func (e *envBindingUsecaseImpl) DeleteEnvBinding(ctx context.Context, app *model.Application, envName string) error { + envBinding, err := e.getBindingByEnv(ctx, app, envName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrEnvBindingNotExist + } + return err + } + if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: app.PrimaryKey(), Name: envBinding.Name}); err != nil { + return err + } + return nil +} + +func (e *envBindingUsecaseImpl) BatchDeleteEnvBinding(ctx context.Context, app *model.Application) error { + envBindings, err := e.GetEnvBindings(ctx, app) + if err != nil { + return err + } + for _, envBinding := range envBindings { + if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: app.PrimaryKey(), Name: envBinding.Name}); err != nil { + return err + } + } + return nil +} + +func (e *envBindingUsecaseImpl) createEnvWorkflow(ctx context.Context, app *model.Application, env *model.EnvBinding) error { + var workflowSteps []v1beta1.WorkflowStep + for _, targetName := range env.TargetNames { + step := v1beta1.WorkflowStep{ + Name: genPolicyEnvName(targetName), + Type: "deploy2env", + Properties: util.Object2RawExtension(map[string]string{ + "policy": genPolicyName(env.Name), + "env": genPolicyEnvName(targetName), + }), + } + workflowSteps = append(workflowSteps, step) + } + var steps []apisv1.WorkflowStep + for _, step := range workflowSteps { + var propertyStr string + if step.Properties != nil { + properties, err := model.NewJSONStruct(step.Properties) + if err != nil { + log.Logger.Errorf("workflow %s step %s properties is invalid %s", app.Name, step.Name, err.Error()) + continue + } + propertyStr = properties.JSON() + } + steps = append(steps, apisv1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + DependsOn: step.DependsOn, + Properties: propertyStr, + Inputs: step.Inputs, + Outputs: step.Outputs, + }) + } + _, err := e.workflowUsecase.CreateWorkflow(ctx, app, apisv1.CreateWorkflowRequest{ + AppName: app.PrimaryKey(), + Name: genWorkflowName(app, env.Name), + Description: "Created automatically by envbinding.", + EnvName: env.Name, + Steps: steps, + Default: false, + }) + if err != nil { + return err + } + return nil +} + +func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) { + return &apisv1.DetailEnvBindingResponse{ + EnvBindingBase: *convertEnvbindingModelToBase(envBinding), + }, nil +} + +func convertCreateReqToEnvBindingModel(app *model.Application, req apisv1.CreateApplicationEnvRequest) model.EnvBinding { + envBinding := model.EnvBinding{ + AppPrimaryKey: app.Name, + Name: req.Name, + Alias: req.Alias, + Description: req.Description, + TargetNames: req.TargetNames, + } + return envBinding +} + +func convertEnvbindingModelToBase(envBinding *model.EnvBinding) *apisv1.EnvBindingBase { + ebb := &apisv1.EnvBindingBase{ + Name: envBinding.Name, + Alias: envBinding.Alias, + Description: envBinding.Description, + TargetNames: envBinding.TargetNames, + ComponentSelector: (*apisv1.ComponentSelector)(envBinding.ComponentSelector), + CreateTime: envBinding.CreateTime, + UpdateTime: envBinding.UpdateTime, + } + return ebb +} + +func convertToEnvBindingModel(app *model.Application, envBind apisv1.EnvBinding) *model.EnvBinding { + re := model.EnvBinding{ + AppPrimaryKey: app.Name, + Name: envBind.Name, + Description: envBind.Description, + Alias: envBind.Alias, + TargetNames: envBind.TargetNames, + } + if envBind.ComponentSelector != nil { + re.ComponentSelector = (*model.ComponentSelector)(envBind.ComponentSelector) + } + return &re +} diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 37dc4e27e..d14d782ba 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -110,6 +110,7 @@ func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.App Name: req.Name, Description: req.Description, Default: req.Default, + EnvName: req.EnvName, AppPrimaryKey: app.PrimaryKey(), } if err := w.ds.Add(ctx, &workflow); err != nil { @@ -138,6 +139,7 @@ func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *mode workflow.Description = req.Description // It is allowed to set multiple workflows as default, and only one takes effect. workflow.Default = req.Default + workflow.EnvName = req.EnvName if err := w.ds.Put(ctx, workflow); err != nil { return nil, err } @@ -165,6 +167,7 @@ func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *mode Name: workflow.Name, Description: workflow.Description, Default: workflow.Default, + EnvName: workflow.EnvName, CreateTime: workflow.CreateTime, UpdateTime: workflow.UpdateTime, }, @@ -178,6 +181,9 @@ func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName stri Name: workflowName, } if err := w.ds.Get(ctx, &workflow); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrWorkflowNotExist + } return nil, err } return &workflow, nil @@ -199,6 +205,7 @@ func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app * Name: wm.Name, Description: wm.Description, Default: wm.Default, + EnvName: wm.EnvName, CreateTime: wm.CreateTime, UpdateTime: wm.UpdateTime, }) diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index db8d27e22..0bc309d57 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -56,7 +56,7 @@ var ErrCreateNamespace = NewBcode(500, 10011, "auto create namespace failure") var ErrApplicationNotExist = NewBcode(404, 10012, "application name is not exist") // ErrApplicationNotEnv no env binding policy -var ErrApplicationNotEnv = NewBcode(404, 10013, "application not set env binding policy") +var ErrApplicationNotEnv = NewBcode(404, 10013, "application not set env binding") // ErrApplicationEnvExist application env is exist var ErrApplicationEnvExist = NewBcode(400, 10014, "application env is exist") diff --git a/pkg/apiserver/rest/utils/bcode/delivery_target.go b/pkg/apiserver/rest/utils/bcode/delivery_target.go index 32c019068..727cf72f7 100644 --- a/pkg/apiserver/rest/utils/bcode/delivery_target.go +++ b/pkg/apiserver/rest/utils/bcode/delivery_target.go @@ -17,7 +17,10 @@ limitations under the License. package bcode // ErrDeliveryTargetExist deliveryTarget is exist -var ErrDeliveryTargetExist = NewBcode(400, 20006, "deliveryTarget is exist") +var ErrDeliveryTargetExist = NewBcode(400, 80001, "deliveryTarget is exist") // ErrDeliveryTargetNotExist deliveryTarget is not exist -var ErrDeliveryTargetNotExist = NewBcode(404, 20007, "deliveryTarget is not exist") +var ErrDeliveryTargetNotExist = NewBcode(404, 80002, "deliveryTarget is not exist") + +// ErrDeliveryTargetInUseCantDeleted deliveryTarget being used +var ErrDeliveryTargetInUseCantDeleted = NewBcode(404, 80003, "deliveryTarget in use, can't be deleted") diff --git a/pkg/apiserver/rest/utils/bcode/envbinding.go b/pkg/apiserver/rest/utils/bcode/envbinding.go new file mode 100644 index 000000000..6e4b7a3f4 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/envbinding.go @@ -0,0 +1,32 @@ +/* +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 + +// ErrEnvbindingDeliveryTargetNotAllExist application envbinding deliveryTarget is not exist +var ErrEnvbindingDeliveryTargetNotAllExist = NewBcode(400, 90001, "application envbinding deliveryTarget is not all exist") + +// ErrFoundEnvbindingDeliveryTarget found application envbinding deliveryTarget failure +var ErrFoundEnvbindingDeliveryTarget = NewBcode(400, 90002, "found application envbinding deliveryTarget failure") + +// ErrEnvBindingNotExist application envbinding is not exist +var ErrEnvBindingNotExist = NewBcode(400, 90003, "application envbinding not exist") + +// ErrEnvBindingsNotExist application envbindings is not exist +var ErrEnvBindingsNotExist = NewBcode(400, 90004, "application envbinding snot exist") + +// ErrEnvBindingExist application envbinding is exist +var ErrEnvBindingExist = NewBcode(400, 90005, "application envbinding is exist") diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index b2d706618..a55b96b45 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -19,6 +19,8 @@ package webservice import ( "context" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" @@ -26,18 +28,19 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/model" 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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) type applicationWebService struct { applicationUsecase usecase.ApplicationUsecase + envBindingUsecase usecase.EnvBindingUsecase } // NewApplicationWebService new application manage webservice -func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase) WebService { +func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase, envBindingUsecase usecase.EnvBindingUsecase) WebService { return &applicationWebService{ applicationUsecase: applicationUsecase, + envBindingUsecase: envBindingUsecase, } } @@ -86,28 +89,6 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.DetailApplicationResponse{})) - ws.Route(ws.PUT("/{name}").To(c.updateApplication). - Doc("update one application "). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(c.appCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). - Reads(apis.UpdateApplicationRequest{}). - Returns(200, "", apis.ApplicationBase{}). - Returns(400, "", bcode.Bcode{}). - Writes(apis.ApplicationBase{})) - - ws.Route(ws.PUT("/{name}/envs/{envName}").To(c.updateApplicationEnvBinding). - Doc("set application differences in the specified environment"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(c.appCheckFilter). - Filter(c.envCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). - Param(ws.PathParameter("envName", "identifier of the application ").DataType("string")). - Reads(apis.PutApplicationEnvRequest{}). - Returns(200, "", apis.EnvBinding{}). - Returns(400, "", bcode.Bcode{}). - Writes(apis.EnvBinding{})) - ws.Route(ws.GET("/{name}/envs/{envName}/status").To(c.getApplicationStatus). Doc("get application status"). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -118,26 +99,15 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationStatusResponse{})) - ws.Route(ws.POST("/{name}/envs").To(c.createApplicationEnv). - Doc("creating an application environment "). + ws.Route(ws.PUT("/{name}").To(c.updateApplication). + Doc("update one application "). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). - Reads(apis.CreateApplicationEnvRequest{}). - Returns(200, "", apis.EnvBinding{}). + Reads(apis.UpdateApplicationRequest{}). + Returns(200, "", apis.ApplicationBase{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.EmptyResponse{})) - - ws.Route(ws.DELETE("/{name}/envs/{envName}").To(c.deleteApplicationEnv). - Doc("delete an application environment "). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(c.appCheckFilter). - Filter(c.envCheckFilter). - Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). - Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string")). - Returns(200, "", apis.EmptyResponse{}). - Returns(404, "", bcode.Bcode{}). - Writes(apis.EmptyResponse{})) + Writes(apis.ApplicationBase{})) ws.Route(ws.POST("/{name}/template").To(c.publishApplicationTemplate). Doc("create one application template"). @@ -293,36 +263,52 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(200, "", apis.DetailRevisionResponse{}). Returns(400, "", bcode.Bcode{}). Writes(apis.DetailRevisionResponse{})) + + ws.Route(ws.GET("/{name}/envs").To(c.listApplicationEnvs). + Doc("list policy for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListApplicationEnvBinding{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListApplicationEnvBinding{})) + + ws.Route(ws.POST("/{name}/envs").To(c.createApplicationEnv). + Doc("creating an application environment "). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Reads(apis.CreateApplicationEnvRequest{}). + Returns(200, "", apis.EnvBinding{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.PUT("/{name}/envs/{envName}").To(c.updateApplicationEnv). + Doc("set application differences in the specified environment"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the envBinding ").DataType("string")). + Reads(apis.PutApplicationEnvRequest{}). + Returns(200, "", apis.EnvBinding{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EnvBinding{})) + + ws.Route(ws.DELETE("/{name}/envs/{envName}").To(c.deleteApplicationEnv). + Doc("delete an application environment "). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the envBinding ").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). + Returns(404, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + return ws } -func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - app, err := c.applicationUsecase.GetApplication(req.Request.Context(), req.PathParameter("name")) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplication, app)) - chain.ProcessFilter(req, res) -} - -func (c *applicationWebService) envCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - envBinding, err := c.applicationUsecase.GetApplicationEnvBindingPolicy(req.Request.Context(), app) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - for _, env := range envBinding.Envs { - if env.Name == req.PathParameter("envName") { - req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplicationEnvBinding, env)) - chain.ProcessFilter(req, res) - return - } - } - bcode.ReturnError(req, res, bcode.ErrApplicationNotEnv) -} - func (c *applicationWebService) createApplication(req *restful.Request, res *restful.Response) { // Verify the validity of parameters var createReq apis.CreateApplicationRequest @@ -564,29 +550,6 @@ func (c *applicationWebService) updateApplicationPolicy(req *restful.Request, re } } -func (c *applicationWebService) updateApplicationEnvBinding(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - // Verify the validity of parameters - var updateReq apis.PutApplicationEnvRequest - if err := req.ReadEntity(&updateReq); err != nil { - bcode.ReturnError(req, res, err) - return - } - if err := validate.Struct(&updateReq); err != nil { - bcode.ReturnError(req, res, err) - return - } - diff, err := c.applicationUsecase.UpdateApplicationEnvBinding(req.Request.Context(), app, req.PathParameter("envName"), updateReq) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - if err := res.WriteEntity(diff); err != nil { - bcode.ReturnError(req, res, err) - return - } -} - func (c *applicationWebService) updateApplication(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters @@ -610,42 +573,6 @@ func (c *applicationWebService) updateApplication(req *restful.Request, res *res } } -func (c *applicationWebService) createApplicationEnv(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - // Verify the validity of parameters - var createReq apis.CreateApplicationEnvRequest - 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 - } - base, err := c.applicationUsecase.CreateApplicationEnvBinding(req.Request.Context(), app, createReq) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - if err := res.WriteEntity(base); err != nil { - bcode.ReturnError(req, res, err) - return - } -} - -func (c *applicationWebService) deleteApplicationEnv(req *restful.Request, res *restful.Response) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - err := c.applicationUsecase.DeleteApplicationEnvBinding(req.Request.Context(), app, req.PathParameter("envName")) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { - bcode.ReturnError(req, res, err) - return - } -} - func (c *applicationWebService) addApplicationTrait(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) var createReq apis.CreateApplicationTraitRequest @@ -749,3 +676,102 @@ func (c *applicationWebService) detailApplicationRevision(req *restful.Request, return } } + +func (c *applicationWebService) updateApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var updateReq apis.PutApplicationEnvRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + diff, err := c.envBindingUsecase.UpdateEnvBinding(req.Request.Context(), app, req.PathParameter("envName"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(diff); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplicationEnvs(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + envBindings, err := c.envBindingUsecase.GetEnvBindings(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListApplicationEnvBinding{EnvBindings: envBindings}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) createApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var createReq apis.CreateApplicationEnvRequest + 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 + } + base, err := c.envBindingUsecase.CreateEnvBinding(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) deleteApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.envBindingUsecase.DeleteEnvBinding(req.Request.Context(), app, req.PathParameter("envName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app, err := c.applicationUsecase.GetApplication(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplication, app)) + chain.ProcessFilter(req, res) +} + +func (c *applicationWebService) envCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + envBindings, err := c.envBindingUsecase.GetEnvBindings(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + for _, env := range envBindings { + if env.Name == req.PathParameter("envName") { + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplicationEnvBinding, env)) + chain.ProcessFilter(req, res) + return + } + } + bcode.ReturnError(req, res, bcode.ErrApplicationNotEnv) +} diff --git a/pkg/apiserver/rest/webservice/delivery_target.go b/pkg/apiserver/rest/webservice/delivery_target.go index 8168cbd53..8fdf6ba79 100644 --- a/pkg/apiserver/rest/webservice/delivery_target.go +++ b/pkg/apiserver/rest/webservice/delivery_target.go @@ -19,6 +19,10 @@ package webservice import ( "context" + "github.com/pkg/errors" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" @@ -31,15 +35,17 @@ import ( ) // NewDeliveryTargetWebService new deliveryTarget webservice -func NewDeliveryTargetWebService(deliveryTargetUsecase usecase.DeliveryTargetUsecase) WebService { +func NewDeliveryTargetWebService(deliveryTargetUsecase usecase.DeliveryTargetUsecase, applicationUsecase usecase.ApplicationUsecase) WebService { return &DeliveryTargetWebService{ deliveryTargetUsecase: deliveryTargetUsecase, + applicationUsecase: applicationUsecase, } } // DeliveryTargetWebService delivery target web service type DeliveryTargetWebService struct { deliveryTargetUsecase usecase.DeliveryTargetUsecase + applicationUsecase usecase.ApplicationUsecase } // GetWebService get web service @@ -169,7 +175,20 @@ func (dt *DeliveryTargetWebService) updateDeliveryTarget(req *restful.Request, r } func (dt *DeliveryTargetWebService) deleteDeliveryTarget(req *restful.Request, res *restful.Response) { - if err := dt.deliveryTargetUsecase.DeleteDeliveryTarget(req.Request.Context(), req.PathParameter("name")); err != nil { + deliveryTargetName := req.PathParameter("name") + //deliveryTarget in use, can't be deleted + applications, err := dt.applicationUsecase.ListApplications(context.TODO(), apis.ListApplicatioOptions{TargetName: deliveryTargetName}) + if err != nil { + if !errors.Is(err, datastore.ErrRecordNotExist) { + bcode.ReturnError(req, res, err) + return + } + } + if applications != nil { + bcode.ReturnError(req, res, bcode.ErrDeliveryTargetInUseCantDeleted) + return + } + if err := dt.deliveryTargetUsecase.DeleteDeliveryTarget(req.Request.Context(), deliveryTargetName); err != nil { bcode.ReturnError(req, res, err) return } @@ -185,13 +204,11 @@ func (dt *DeliveryTargetWebService) listDeliveryTargets(req *restful.Request, re bcode.ReturnError(req, res, err) return } - deliveryTargets, err := dt.deliveryTargetUsecase.ListDeliveryTargets(req.Request.Context(), page, pageSize, req.QueryParameter("namespace")) if err != nil { bcode.ReturnError(req, res, err) return } - if err := res.WriteEntity(deliveryTargets); err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 8463ca307..047b38657 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -60,15 +60,16 @@ func returns500(b *restful.RouteBuilder) { func Init(ds datastore.DataStore) { clusterUsecase := usecase.NewClusterUsecase(ds) workflowUsecase := usecase.NewWorkflowUsecase(ds) - applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase) + deliveryTargetUsecase := usecase.NewDeliveryTargetUsecase(ds) namespaceUsecase := usecase.NewNamespaceUsecase() oamApplicationUsecase := usecase.NewOAMApplicationUsecase() velaQLUsecase := usecase.NewVelaQLUsecase() definitionUsecase := usecase.NewDefinitionUsecase() addonUsecase := usecase.NewAddonUsecase(ds) - deliveryTargetUsecase := usecase.NewDeliveryTargetUsecase(ds) + envBindingUsecase := usecase.NewEnvBindingUsecase(ds, workflowUsecase) + applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase, envBindingUsecase, deliveryTargetUsecase) RegistWebService(NewClusterWebService(clusterUsecase)) - RegistWebService(NewApplicationWebService(applicationUsecase)) + RegistWebService(NewApplicationWebService(applicationUsecase, envBindingUsecase)) RegistWebService(NewNamespaceWebService(namespaceUsecase)) RegistWebService(NewDefinitionWebservice(definitionUsecase)) RegistWebService(NewAddonWebService(addonUsecase)) @@ -76,6 +77,6 @@ func Init(ds datastore.DataStore) { RegistWebService(NewOAMApplication(oamApplicationUsecase)) RegistWebService(&policyDefinitionWebservice{}) RegistWebService(NewWorkflowWebService(workflowUsecase, applicationUsecase)) - RegistWebService(NewDeliveryTargetWebService(deliveryTargetUsecase)) + RegistWebService(NewDeliveryTargetWebService(deliveryTargetUsecase, applicationUsecase)) RegistWebService(NewVelaQLWebService(velaQLUsecase)) } diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index 48387fab7..4b107b2ff 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -59,7 +59,6 @@ var _ = Describe("Test application rest api", func() { Expect(cmp.Diff(appBase.Description, req.Description)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Namespace, req.Namespace)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Labels["test"], req.Labels["test"])).Should(BeEmpty()) - Expect(cmp.Diff(appBase.EnvBinding[0].Name, "dev-env")).Should(BeEmpty()) }) It("Test delete app", func() { From 1a2c964dac2f943038d63136a74b1eef542f7b1a Mon Sep 17 00:00:00 2001 From: Somefive Date: Fri, 19 Nov 2021 15:39:54 +0800 Subject: [PATCH 47/59] Fix: enhance cluster api (#2742) --- pkg/addon/addon.go | 2 + pkg/apiserver/datastore/datastore.go | 31 ++++- pkg/apiserver/datastore/kubeapi/kubeapi.go | 126 ++++++++++++++++-- .../datastore/kubeapi/kubeapi_test.go | 59 +++++++- pkg/apiserver/datastore/mongodb/mongodb.go | 25 +++- .../datastore/mongodb/mongodb_test.go | 63 +++++++-- pkg/apiserver/model/cluster.go | 32 ++++- pkg/apiserver/rest/apis/v1/types.go | 16 ++- pkg/apiserver/rest/usecase/application.go | 8 +- pkg/apiserver/rest/usecase/cluster.go | 101 +++++++++++--- pkg/apiserver/rest/usecase/delivery_target.go | 2 +- pkg/apiserver/rest/usecase/envbinding.go | 17 ++- pkg/apiserver/rest/usecase/workflow.go | 2 +- pkg/apiserver/rest/utils/bcode/cluster.go | 3 + pkg/apiserver/rest/webservice/cluster.go | 37 +++++ .../rest/webservice/delivery_target.go | 2 +- pkg/cloudprovider/aliyun.go | 6 + pkg/cloudprovider/types.go | 3 + pkg/velaql/providers/query/collector.go | 2 +- test/e2e-apiserver-test/cluster_test.go | 28 +++- test/e2e-apiserver-test/suite_test.go | 10 +- 21 files changed, 499 insertions(+), 76 deletions(-) diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 927b7c523..ea95b2b2f 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -486,6 +486,7 @@ func Convert2AddonName(name string) string { return strings.TrimPrefix(name, addonAppPrefix) } +// RenderArgsSecret TODO add desc func RenderArgsSecret(addon *types.Addon, args map[string]string) *v1.Secret { sec := v1.Secret{ TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, @@ -499,6 +500,7 @@ func RenderArgsSecret(addon *types.Addon, args map[string]string) *v1.Secret { return &sec } +// Convert2SecName TODO add desc func Convert2SecName(name string) string { return addonSecPrefix + name } diff --git a/pkg/apiserver/datastore/datastore.go b/pkg/apiserver/datastore/datastore.go index 2dc550191..6a431de7d 100644 --- a/pkg/apiserver/datastore/datastore.go +++ b/pkg/apiserver/datastore/datastore.go @@ -86,10 +86,39 @@ func NewEntity(in Entity) (Entity, error) { return new.Interface().(Entity), nil } +// SortOrder is the order of sort +type SortOrder int + +const ( + // SortOrderAscending defines the order of ascending for sorting + SortOrderAscending = SortOrder(1) + // SortOrderDescending defines the order of descending for sorting + SortOrderDescending = SortOrder(-1) +) + +// SortOption describes the sorting parameters for list +type SortOption struct { + Key string + Order SortOrder +} + +// FuzzyQueryOption defines the fuzzy query search filter option +type FuzzyQueryOption struct { + Key string + Query string +} + +// FilterOptions filter query returned items +type FilterOptions struct { + Queries []FuzzyQueryOption +} + // ListOptions list api options type ListOptions struct { + FilterOptions Page int PageSize int + SortBy []SortOption } // DataStore datastore interface @@ -113,7 +142,7 @@ type DataStore interface { List(ctx context.Context, query Entity, options *ListOptions) ([]Entity, error) // Count entities from database, TableName() can't return zero value. - Count(ctx context.Context, entity Entity) (int64, error) + Count(ctx context.Context, entity Entity, options *FilterOptions) (int64, error) // IsExist Name() and TableName() can't return zero value. IsExist(ctx context.Context, entity Entity) (bool, error) diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi.go b/pkg/apiserver/datastore/kubeapi/kubeapi.go index 08b75f422..b696b5752 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi.go @@ -21,9 +21,11 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" "time" + "github.com/tidwall/gjson" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -220,6 +222,95 @@ func (m *kubeapi) Delete(ctx context.Context, entity datastore.Entity) error { return nil } +type bySortOptionConfigMap struct { + items []corev1.ConfigMap + objects []map[string]interface{} + sortBy []datastore.SortOption +} + +func newBySortOptionConfigMap(items []corev1.ConfigMap, sortBy []datastore.SortOption) bySortOptionConfigMap { + s := bySortOptionConfigMap{ + items: items, + objects: make([]map[string]interface{}, len(items)), + sortBy: sortBy, + } + for i, item := range items { + m := map[string]interface{}{} + data := item.BinaryData["data"] + for _, op := range sortBy { + res := gjson.Get(string(data), op.Key) + if res.Type == gjson.Number { + m[op.Key] = res.Num + } else { + m[op.Key] = res.Raw + } + } + s.objects[i] = m + } + return s +} + +func (b bySortOptionConfigMap) Len() int { + return len(b.items) +} + +func (b bySortOptionConfigMap) Swap(i, j int) { + b.items[i], b.items[j] = b.items[j], b.items[i] + b.objects[i], b.objects[j] = b.objects[j], b.objects[i] +} + +func (b bySortOptionConfigMap) Less(i, j int) bool { + for _, op := range b.sortBy { + x := b.objects[i][op.Key] + y := b.objects[j][op.Key] + _x, xok := x.(float64) + _y, yok := y.(float64) + var lt, gt bool + if xok && yok { + lt, gt = _x < _y, _x > _y + } + if !xok && !yok { + lt, gt = x.(string) < y.(string), x.(string) > y.(string) + } + if xok != yok { + lt, gt = false, false + } + if !lt && !gt { + continue + } + if op.Order == datastore.SortOrderAscending { + return lt + } + return gt + } + return true +} + +func _sortConfigMapBySortOptions(items []corev1.ConfigMap, sortOptions []datastore.SortOption) []corev1.ConfigMap { + so := newBySortOptionConfigMap(items, sortOptions) + sort.Sort(so) + return so.items +} + +func _filterConfigMapByFuzzyQueryOptions(items []corev1.ConfigMap, queries []datastore.FuzzyQueryOption) []corev1.ConfigMap { + var _items []corev1.ConfigMap + for _, item := range items { + data := string(item.BinaryData["data"]) + valid := true + for _, query := range queries { + res := gjson.Get(data, query.Key) + if res.Type != gjson.String || !strings.Contains(res.Str, query.Query) { + valid = false + break + } + } + if valid { + _items = append(_items, item) + } + } + return _items +} + // 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() == "" { @@ -241,17 +332,13 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto LabelSelector: selector, Namespace: m.namespace, } - var skip, limit int64 + var skip, limit int if op != nil && op.PageSize > 0 && op.Page > 0 { - skip = int64(op.PageSize * (op.Page - 1)) - limit = int64(op.PageSize * op.Page) + skip = op.PageSize * (op.Page - 1) + limit = op.PageSize 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 { @@ -261,12 +348,22 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto return nil, datastore.NewDBError(err) } items := configMaps.Items + if op != nil && len(op.Queries) > 0 { + items = _filterConfigMapByFuzzyQueryOptions(items, op.Queries) + } + if op != nil && len(op.SortBy) > 0 { + items = _sortConfigMapBySortOptions(items, op.SortBy) + } if op != nil && op.PageSize > 0 && op.Page > 0 { - if len(configMaps.Items) > int(limit) { - items = configMaps.Items[skip:limit] + if skip >= len(items) { + items = []corev1.ConfigMap{} } else { - items = configMaps.Items[skip:] + items = items[skip:] } + if limit >= len(items) { + limit = len(items) + } + items = items[:limit] } var list []datastore.Entity log.Logger.Debugf("query %s result count %d", selector, len(items)) @@ -284,7 +381,7 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto } // Count counts entities -func (m *kubeapi) Count(ctx context.Context, entity datastore.Entity) (int64, error) { +func (m *kubeapi) Count(ctx context.Context, entity datastore.Entity, filterOptions *datastore.FilterOptions) (int64, error) { if entity.TableName() == "" { return 0, datastore.ErrTableNameEmpty } @@ -312,6 +409,9 @@ func (m *kubeapi) Count(ctx context.Context, entity datastore.Entity) (int64, er } return 0, datastore.NewDBError(err) } - - return int64(len(configMaps.Items)), nil + items := configMaps.Items + if filterOptions != nil && len(filterOptions.Queries) > 0 { + items = _filterConfigMapByFuzzyQueryOptions(configMaps.Items, filterOptions.Queries) + } + return int64(len(items)), nil } diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go index bdb52f62f..70f8172a0 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go @@ -87,12 +87,12 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(err).Should(BeNil()) Expect(kubeStore).ToNot(BeNil()) - It("Test add funtion", func() { + It("Test add function", func() { err := kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) Expect(err).ToNot(HaveOccurred()) }) - It("Test batch add funtion", func() { + It("Test batch add function", func() { var datas = []datastore.Entity{ &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, @@ -110,7 +110,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(equal).To(BeEmpty()) }) - It("Test get funtion", func() { + It("Test get function", func() { app := &model.Application{Name: "kubevela-app"} err := kubeStore.Get(context.TODO(), app) Expect(err).Should(BeNil()) @@ -118,7 +118,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test put funtion", func() { + It("Test put function", func() { err := kubeStore.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) Expect(err).ToNot(HaveOccurred()) }) @@ -165,16 +165,61 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(diff).Should(BeEmpty()) }) + It("Test list clusters with sort and fuzzy query", func() { + clusters, err := kubeStore.List(context.TODO(), &model.Cluster{}, nil) + Expect(err).Should(Succeed()) + for _, cluster := range clusters { + Expect(kubeStore.Delete(context.TODO(), cluster)).Should(Succeed()) + } + for _, name := range []string{"first", "second", "third"} { + Expect(kubeStore.Add(context.TODO(), &model.Cluster{Name: name})).Should(Succeed()) + time.Sleep(time.Millisecond * 100) + } + entities, err := kubeStore.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderAscending}}}) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(3)) + for i, name := range []string{"first", "second", "third"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + entities, err = kubeStore.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{ + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + Page: 2, + PageSize: 2, + }) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(1)) + for i, name := range []string{"first"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + entities, err = kubeStore.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{ + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + FilterOptions: datastore.FilterOptions{ + Queries: []datastore.FuzzyQueryOption{{Key: "name", Query: "ir"}}, + }, + }) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(2)) + for i, name := range []string{"third", "first"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + }) + It("Test count function", func() { var app model.Application - count, err := kubeStore.Count(context.TODO(), &app) + count, err := kubeStore.Count(context.TODO(), &app, nil) Expect(err).ShouldNot(HaveOccurred()) Expect(count).Should(Equal(int64(4))) app.Namespace = "test-namespace" - count, err = kubeStore.Count(context.TODO(), &app) + count, err = kubeStore.Count(context.TODO(), &app, nil) Expect(err).ShouldNot(HaveOccurred()) Expect(count).Should(Equal(int64(1))) + + count, err = kubeStore.Count(context.TODO(), &model.Cluster{}, &datastore.FilterOptions{ + Queries: []datastore.FuzzyQueryOption{{Key: "name", Query: "ir"}}, + }) + Expect(err).Should(Succeed()) + Expect(count).Should(Equal(int64(2))) }) It("Test isExist function", func() { @@ -192,7 +237,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test delete funtion", func() { + It("Test delete function", func() { var app model.Application app.Name = "kubevela-app" err := kubeStore.Delete(context.TODO(), &app) diff --git a/pkg/apiserver/datastore/mongodb/mongodb.go b/pkg/apiserver/datastore/mongodb/mongodb.go index 256ea9907..d16dcb237 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb.go +++ b/pkg/apiserver/datastore/mongodb/mongodb.go @@ -26,6 +26,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/x/bsonx" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" @@ -182,6 +183,15 @@ func (m *mongodb) Delete(ctx context.Context, entity datastore.Entity) error { return nil } +func _applyFilterOptions(filter bson.D, filterOptions datastore.FilterOptions) bson.D { + if len(filterOptions.Queries) > 0 { + for _, queryOp := range filterOptions.Queries { + filter = append(filter, bson.E{Key: strings.ToLower(queryOp.Key), Value: bsonx.Regex(".*"+queryOp.Query+".*", "s")}) + } + } + return filter +} + // List list entity function func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datastore.ListOptions) ([]datastore.Entity, error) { if entity.TableName() == "" { @@ -198,11 +208,21 @@ func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datasto }) } } + if op != nil && len(op.Queries) > 0 { + filter = _applyFilterOptions(filter, op.FilterOptions) + } 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)) } + if op != nil && len(op.SortBy) > 0 { + _d := bson.D{} + for _, sortOp := range op.SortBy { + _d = append(_d, bson.E{Key: strings.ToLower(sortOp.Key), Value: int(sortOp.Order)}) + } + findOptions.SetSort(_d) + } cur, err := collection.Find(ctx, filter, &findOptions) if err != nil { return nil, datastore.NewDBError(err) @@ -230,7 +250,7 @@ func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datasto } // Count counts entities -func (m *mongodb) Count(ctx context.Context, entity datastore.Entity) (int64, error) { +func (m *mongodb) Count(ctx context.Context, entity datastore.Entity, filterOptions *datastore.FilterOptions) (int64, error) { if entity.TableName() == "" { return 0, datastore.ErrTableNameEmpty } @@ -244,6 +264,9 @@ func (m *mongodb) Count(ctx context.Context, entity datastore.Entity) (int64, er }) } } + if filterOptions != nil && len(filterOptions.Queries) > 0 { + filter = _applyFilterOptions(filter, *filterOptions) + } count, err := collection.CountDocuments(ctx, filter) if err != nil { return 0, datastore.NewDBError(err) diff --git a/pkg/apiserver/datastore/mongodb/mongodb_test.go b/pkg/apiserver/datastore/mongodb/mongodb_test.go index 3e947d028..8d49ab63c 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb_test.go +++ b/pkg/apiserver/datastore/mongodb/mongodb_test.go @@ -55,12 +55,12 @@ var _ = BeforeSuite(func(done Done) { var _ = Describe("Test mongodb datastore driver", func() { - It("Test add funtion", func() { + It("Test add function", func() { err := mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) Expect(err).ToNot(HaveOccurred()) }) - It("Test batch add funtion", func() { + It("Test batch add function", func() { var datas = []datastore.Entity{ &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, @@ -78,7 +78,7 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(equal).To(BeEmpty()) }) - It("Test get funtion", func() { + It("Test get function", func() { app := &model.Application{Name: "kubevela-app"} err := mongodbDriver.Get(context.TODO(), app) Expect(err).Should(BeNil()) @@ -86,11 +86,11 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test put funtion", func() { + It("Test put function", func() { err := mongodbDriver.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) Expect(err).ToNot(HaveOccurred()) }) - It("Test list funtion", func() { + It("Test list function", func() { var app model.Application list, err := mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) Expect(err).ShouldNot(HaveOccurred()) @@ -119,19 +119,64 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(diff).Should(BeEmpty()) }) + It("Test list clusters with sort and fuzzy query", func() { + clusters, err := mongodbDriver.List(context.TODO(), &model.Cluster{}, nil) + Expect(err).Should(Succeed()) + for _, cluster := range clusters { + Expect(mongodbDriver.Delete(context.TODO(), cluster)).Should(Succeed()) + } + for _, name := range []string{"first", "second", "third"} { + Expect(mongodbDriver.Add(context.TODO(), &model.Cluster{Name: name})).Should(Succeed()) + time.Sleep(time.Millisecond * 100) + } + entities, err := mongodbDriver.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderAscending}}}) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(3)) + for i, name := range []string{"first", "second", "third"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + entities, err = mongodbDriver.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{ + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + Page: 2, + PageSize: 2, + }) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(1)) + for i, name := range []string{"first"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + entities, err = mongodbDriver.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{ + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + FilterOptions: datastore.FilterOptions{ + Queries: []datastore.FuzzyQueryOption{{Key: "name", Query: "ir"}}, + }, + }) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(2)) + for i, name := range []string{"third", "first"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + }) + It("Test count function", func() { var app model.Application - count, err := mongodbDriver.Count(context.TODO(), &app) + count, err := mongodbDriver.Count(context.TODO(), &app, nil) Expect(err).ShouldNot(HaveOccurred()) Expect(count).Should(Equal(int64(4))) app.Namespace = "test-namespace" - count, err = mongodbDriver.Count(context.TODO(), &app) + count, err = mongodbDriver.Count(context.TODO(), &app, nil) Expect(err).ShouldNot(HaveOccurred()) Expect(count).Should(Equal(int64(1))) + + count, err = mongodbDriver.Count(context.TODO(), &model.Cluster{}, &datastore.FilterOptions{ + Queries: []datastore.FuzzyQueryOption{{Key: "name", Query: "ir"}}, + }) + Expect(err).Should(Succeed()) + Expect(count).Should(Equal(int64(2))) }) - It("Test isExist funtion", func() { + It("Test isExist function", func() { var app model.Application app.Name = "kubevela-app-3" exist, err := mongodbDriver.IsExist(context.TODO(), &app) @@ -146,7 +191,7 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test delete funtion", func() { + It("Test delete function", func() { var app model.Application app.Name = "kubevela-app" err := mongodbDriver.Delete(context.TODO(), &app) diff --git a/pkg/apiserver/model/cluster.go b/pkg/apiserver/model/cluster.go index c222e238b..3b5f858d4 100644 --- a/pkg/apiserver/model/cluster.go +++ b/pkg/apiserver/model/cluster.go @@ -16,6 +16,12 @@ limitations under the License. package model +import ( + "time" + + "github.com/oam-dev/kubevela/pkg/multicluster" +) + func init() { RegistModel(&Cluster{}) } @@ -23,9 +29,12 @@ func init() { // ProviderInfo describes the information from provider API type ProviderInfo struct { Provider string `json:"provider"` - ClusterName string `json:"name"` - ID string `json:"id"` - Zone string `json:"zone"` + ClusterID string `json:"clusterID"` + ClusterName string `json:"clusterName,omitempty"` + Zone string `json:"zone,omitempty"` + ZoneID string `json:"zoneID,omitempty"` + RegionID string `json:"regionID,omitempty"` + VpcID string `json:"vpcID,omitempty"` Labels map[string]string `json:"labels"` } @@ -36,9 +45,14 @@ const ( ClusterStatusUnhealthy = "Unhealthy" ) +var ( + // LocalClusterCreatedTime create time for local cluster, set to late date in order to ensure it is sorted to first + LocalClusterCreatedTime = time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC) +) + // Cluster describes the model of cluster in apiserver type Cluster struct { - Model + Model `json:"model"` Name string `json:"name"` Alias string `json:"alias"` Description string `json:"description"` @@ -53,6 +67,16 @@ type Cluster struct { KubeConfigSecret string `json:"kubeConfigSecret"` } +// SetCreateTime for local cluster, create time is set to a large date which ensures the order of list +func (c *Cluster) SetCreateTime(t time.Time) { + if c.Name == multicluster.ClusterLocalName { + c.CreateTime = LocalClusterCreatedTime + c.SetUpdateTime(t) + } else { + c.CreateTime = t + } +} + // TableName table name for datastore func (c *Cluster) TableName() string { return tableNamePrefix + "cluster" diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 0afde2958..d2d721454 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -114,6 +114,7 @@ type EnablingProgress struct { TotalComponents int `json:"total_components"` } +// AddonArgsResponse defines the response of addon args type AddonArgsResponse struct { Args map[string]string `json:"args"` } @@ -174,6 +175,16 @@ type ClusterResourceInfo struct { StorageClassList []string `json:"storageClassList,omitempty"` } +// CreateClusterNamespaceRequest request parameter to create namespace in cluster +type CreateClusterNamespaceRequest struct { + Namespace string `json:"namespace"` +} + +// CreateClusterNamespaceResponse response parameter for created namespace in cluster +type CreateClusterNamespaceResponse struct { + Exists bool `json:"exists"` +} + // DetailClusterResponse cluster detail information model type DetailClusterResponse struct { model.Cluster @@ -183,6 +194,7 @@ type DetailClusterResponse struct { // ListClusterResponse list cluster type ListClusterResponse struct { Clusters []ClusterBase `json:"clusters"` + Total int64 `json:"total"` } // ListCloudClusterResponse list cloud clusters @@ -193,13 +205,14 @@ type ListCloudClusterResponse struct { // CreateCloudClusterResponse return values for cloud cluster create request type CreateCloudClusterResponse struct { + Name string `json:"clusterName"` ClusterID string `json:"clusterID"` Status string `json:"status"` } // ListCloudClusterCreationResponse return the cluster names of creation process of cloud clusters type ListCloudClusterCreationResponse struct { - Creations []string `json:"creations"` + Creations []CreateCloudClusterResponse `json:"creations"` } // ClusterBase cluster base model @@ -302,6 +315,7 @@ type EnvBindingBase struct { UpdateTime time.Time `json:"updateTime"` } +// DetailEnvBindingResponse defines the response of env-binding details type DetailEnvBindingResponse struct { EnvBindingBase } diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index 0f0a90092..5b2063279 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -164,6 +164,9 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod return nil, err } components, err := c.ListComponents(ctx, app, apisv1.ListApplicationComponentOptions{}) + if err != nil { + return nil, err + } envBindings, err := c.envBindingUsecase.GetEnvBindings(ctx, app) if err != nil { return nil, err @@ -297,6 +300,9 @@ func (c *applicationUsecaseImpl) genPolicyByEnv(ctx context.Context, app *model. envBindingSpec.Envs = append(envBindingSpec.Envs, createTargetClusterEnv(envBinding.EnvBindingBase, target)) } properties, err := model.NewJSONStructByStruct(envBindingSpec) + if err != nil { + return appPolicy, err + } appPolicy.Properties = properties.RawExtension() return appPolicy, nil } @@ -1057,7 +1063,7 @@ func (c *applicationUsecaseImpl) ListRevisions(ctx context.Context, appName, env }) } } - count, err := c.ds.Count(ctx, &revision) + count, err := c.ds.Count(ctx, &revision, nil) if err != nil { return nil, err } diff --git a/pkg/apiserver/rest/usecase/cluster.go b/pkg/apiserver/rest/usecase/cluster.go index 44021a873..a0e11998c 100644 --- a/pkg/apiserver/rest/usecase/cluster.go +++ b/pkg/apiserver/rest/usecase/cluster.go @@ -27,6 +27,7 @@ import ( "github.com/oam-dev/terraform-controller/api/types" "github.com/oam-dev/terraform-controller/api/v1beta1" "github.com/pkg/errors" + v12 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -53,6 +54,8 @@ type ClusterUsecase interface { ModifyKubeCluster(context.Context, apis.CreateClusterRequest, string) (*apis.ClusterBase, error) DeleteKubeCluster(context.Context, string) (*apis.ClusterBase, error) + CreateClusterNamespace(context.Context, string, apis.CreateClusterNamespaceRequest) (*apis.CreateClusterNamespaceResponse, error) + ListCloudClusters(context.Context, string, apis.AccessKeyRequest, int, int) (*apis.ListCloudClusterResponse, error) ConnectCloudCluster(context.Context, string, apis.ConnectCloudClusterRequest) (*apis.ClusterBase, error) CreateCloudCluster(context.Context, string, apis.CreateCloudClusterRequest) (*apis.CreateCloudClusterResponse, error) @@ -135,15 +138,31 @@ func (c *clusterUsecaseImpl) preAddLocalCluster(ctx context.Context) error { } return err } + return nil } return err } + if localCluster.CreateTime.Before(model.LocalClusterCreatedTime) { + localCluster.CreateTime = model.LocalClusterCreatedTime + if err = c.ds.Put(ctx, localCluster); err != nil { + return err + } + } return nil } func (c *clusterUsecaseImpl) ListKubeClusters(ctx context.Context, query string, page int, pageSize int) (*apis.ListClusterResponse, error) { - // TODO: Fuzzy query - clusters, err := c.ds.List(ctx, &model.Cluster{}, &datastore.ListOptions{Page: page, PageSize: pageSize}) + var queries []datastore.FuzzyQueryOption + if query != "" { + queries = append(queries, datastore.FuzzyQueryOption{Key: "name", Query: query}) + } + fo := datastore.FilterOptions{Queries: queries} + clusters, err := c.ds.List(ctx, &model.Cluster{}, &datastore.ListOptions{ + Page: page, + PageSize: pageSize, + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + FilterOptions: fo, + }) if err != nil { return nil, errors.Wrapf(err, "failed to list cluster with query %s in data store", query) } @@ -156,6 +175,11 @@ func (c *clusterUsecaseImpl) ListKubeClusters(ctx context.Context, query string, resp.Clusters = append(resp.Clusters, *newClusterBaseFromCluster(cluster)) } } + total, err := c.ds.Count(ctx, &model.Cluster{}, &fo) + if err != nil { + return nil, errors.Wrapf(err, "failed to count cluster with query %s in data store", query) + } + resp.Total = total return resp, nil } @@ -207,8 +231,11 @@ func (c *clusterUsecaseImpl) createKubeCluster(ctx context.Context, req apis.Cre cluster.Provider = model.ProviderInfo{ Provider: providerCluster.Provider, ClusterName: providerCluster.Name, - ID: providerCluster.ID, + ClusterID: providerCluster.ID, Zone: providerCluster.Zone, + ZoneID: providerCluster.ZoneID, + RegionID: providerCluster.RegionID, + VpcID: providerCluster.VpcID, Labels: providerCluster.Labels, } cluster.DashboardURL = providerCluster.DashBoardURL @@ -352,6 +379,28 @@ func (c *clusterUsecaseImpl) DeleteKubeCluster(ctx context.Context, clusterName return newClusterBaseFromCluster(cluster), nil } +func (c *clusterUsecaseImpl) CreateClusterNamespace(ctx context.Context, clusterName string, req apis.CreateClusterNamespaceRequest) (*apis.CreateClusterNamespaceResponse, error) { + _, err := c.getClusterFromDataStore(ctx, clusterName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to found cluster %s in data store", clusterName) + } + ns := &v12.Namespace{} + ns.Name = req.Namespace + if err = c.k8sClient.Create(multicluster.ContextWithClusterName(ctx, clusterName), ns); err != nil { + if kerrors.IsAlreadyExists(err) { + return &apis.CreateClusterNamespaceResponse{Exists: true}, nil + } + if kerrors.IsForbidden(err) { + return nil, bcode.ErrClusterCreateNamespaceNoPermission + } + return nil, errors.Wrapf(err, "failed to create namespace %s in cluster %s", req.Namespace, clusterName) + } + return &apis.CreateClusterNamespaceResponse{Exists: false}, nil +} + func (c *clusterUsecaseImpl) setClusterStatusAndResourceInfo(ctx context.Context, cluster *model.Cluster) apis.ClusterResourceInfo { resourceInfo, err := c.getClusterResourceInfoFromK8s(ctx, cluster.Name) if err != nil { @@ -475,6 +524,25 @@ func (c *clusterUsecaseImpl) CreateCloudCluster(ctx context.Context, provider st return c.GetCloudClusterCreationStatus(ctx, provider, req.Name) } +func (c *clusterUsecaseImpl) convertTerraformConfigurationStateIntoCloudClusterCreationStatus(cfg v1beta1.Configuration) (status string, clusterID string, err error) { + status = string(cfg.Status.Apply.State) + if status == "" { + return "Initializing", "", nil + } + if cfg.DeletionTimestamp != nil { + return "Deleting", "", nil + } + if status == string(types.Available) { + cid, ok := cfg.Status.Apply.Outputs["CLUSTER_ID"] + if !ok { + status = "ClusterIDNotFound" + return status, "", bcode.ErrClusterIDNotFoundInTerraformConfiguration + } + return status, cid.Value, nil + } + return status, "", nil +} + func (c *clusterUsecaseImpl) getCloudClusterCreationStatus(ctx context.Context, provider string, cloudClusterName string) (*apis.CreateCloudClusterResponse, *v1beta1.Configuration, error) { terraformConfigurationName := cloudprovider.GetCloudClusterFullName(provider, cloudClusterName) cfg := &v1beta1.Configuration{ @@ -489,24 +557,11 @@ func (c *clusterUsecaseImpl) getCloudClusterCreationStatus(ctx context.Context, } return nil, nil, err } - status := string(cfg.Status.Apply.State) - if status == "" { - status = "Initializing" + status, clusterID, err := c.convertTerraformConfigurationStateIntoCloudClusterCreationStatus(*cfg) + if err != nil { + return nil, cfg, err } - if cfg.DeletionTimestamp != nil { - status = "Deleting" - } - if status == string(types.Available) { - cid, ok := cfg.Status.Apply.Outputs["CLUSTER_ID"] - if !ok { - return nil, nil, bcode.ErrClusterIDNotFoundInTerraformConfiguration - } - return &apis.CreateCloudClusterResponse{ - Status: status, - ClusterID: cid.Value, - }, cfg, nil - } - return &apis.CreateCloudClusterResponse{Status: status}, cfg, nil + return &apis.CreateCloudClusterResponse{Name: cloudClusterName, Status: status, ClusterID: clusterID}, cfg, nil } func (c *clusterUsecaseImpl) GetCloudClusterCreationStatus(ctx context.Context, provider string, cloudClusterName string) (*apis.CreateCloudClusterResponse, error) { @@ -519,11 +574,13 @@ func (c *clusterUsecaseImpl) ListCloudClusterCreation(ctx context.Context, provi if err := c.k8sClient.List(ctx, &cfgs, client.HasLabels{cloudprovider.CloudClusterCreatorLabelKey}, client.InNamespace(util.GetRuntimeNamespace())); err != nil { return nil, err } - var creations []string + var creations []apis.CreateCloudClusterResponse for _, cfg := range cfgs.Items { prefix := "cloud-cluster-" + provider + "-" if strings.HasPrefix(cfg.Name, prefix) { - creations = append(creations, strings.TrimPrefix(cfg.Name, prefix)) + status, clusterID, _ := c.convertTerraformConfigurationStateIntoCloudClusterCreationStatus(cfg) + name := strings.TrimPrefix(cfg.Name, prefix) + creations = append(creations, apis.CreateCloudClusterResponse{Name: name, Status: status, ClusterID: clusterID}) } } return &apis.ListCloudClusterCreationResponse{Creations: creations}, nil diff --git a/pkg/apiserver/rest/usecase/delivery_target.go b/pkg/apiserver/rest/usecase/delivery_target.go index fe2353332..a5363dc3c 100644 --- a/pkg/apiserver/rest/usecase/delivery_target.go +++ b/pkg/apiserver/rest/usecase/delivery_target.go @@ -67,7 +67,7 @@ func (dt *deliveryTargetUsecaseImpl) ListDeliveryTargets(ctx context.Context, pa resp.DeliveryTargets = append(resp.DeliveryTargets, *convertFromDeliveryTargetModel(dt)) } } - count, err := dt.ds.Count(ctx, &deliveryTarget) + count, err := dt.ds.Count(ctx, &deliveryTarget, nil) if err != nil { return nil, err } diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go index cd34fd976..84000bd48 100644 --- a/pkg/apiserver/rest/usecase/envbinding.go +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -107,15 +107,14 @@ func (e *envBindingUsecaseImpl) CreateEnvBinding(ctx context.Context, app *model } if envBinding != nil { return nil, bcode.ErrEnvBindingExist - } else { - envBindingModel := convertCreateReqToEnvBindingModel(app, envReq) - if err := e.ds.Add(ctx, &envBindingModel); err != nil { - return nil, err - } - err := e.createEnvWorkflow(ctx, app, &envBindingModel) - if err != nil { - return nil, err - } + } + envBindingModel := convertCreateReqToEnvBindingModel(app, envReq) + if err := e.ds.Add(ctx, &envBindingModel); err != nil { + return nil, err + } + err = e.createEnvWorkflow(ctx, app, &envBindingModel) + if err != nil { + return nil, err } return &envReq.EnvBinding, nil } diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index d14d782ba..7e35e0e37 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -248,7 +248,7 @@ func (w *workflowUsecaseImpl) ListWorkflowRecords(ctx context.Context, workflowN resp.Records = append(resp.Records, *convertFromRecordModel(record)) } } - count, err := w.ds.Count(ctx, &record) + count, err := w.ds.Count(ctx, &record, nil) if err != nil { return nil, err } diff --git a/pkg/apiserver/rest/utils/bcode/cluster.go b/pkg/apiserver/rest/utils/bcode/cluster.go index 5b7478cb6..52f515107 100644 --- a/pkg/apiserver/rest/utils/bcode/cluster.go +++ b/pkg/apiserver/rest/utils/bcode/cluster.go @@ -57,3 +57,6 @@ var ErrBootstrapTerraformConfiguration = NewBcode(500, 40012, "failed to bootstr // ErrInvalidAccessKeyOrSecretKey access key or secret key is invalid var ErrInvalidAccessKeyOrSecretKey = NewBcode(400, 40013, "access key or secret key is invalid") + +// ErrClusterCreateNamespaceNoPermission cluster create namespace is forbidden +var ErrClusterCreateNamespaceNoPermission = NewBcode(401, 40014, "no permission to create namespace in cluster") diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index 42dc3f3db..1f7698bb5 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -89,6 +89,15 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.ClusterBase{})) + ws.Route(ws.POST("/{clusterName}/namespaces").To(c.createNamespace). + Doc("create namespace in cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("clusterName", "name of the target cluster").DataType("string")). + Reads(apis.CreateClusterNamespaceRequest{}). + Returns(200, "", apis.CreateClusterNamespaceResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.CreateClusterNamespaceResponse{})) + ws.Route(ws.POST("/cloud-clusters/{provider}").To(c.listCloudClusters). Doc("list cloud clusters"). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -255,6 +264,34 @@ func (c *ClusterWebService) deleteKubeCluster(req *restful.Request, res *restful } } +func (c *ClusterWebService) createNamespace(req *restful.Request, res *restful.Response) { + clusterName := req.PathParameter("clusterName") + + // Verify the validity of parameters + var createReq apis.CreateClusterNamespaceRequest + 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 + resp, err := c.clusterUsecase.CreateClusterNamespace(req.Request.Context(), clusterName, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + func (c *ClusterWebService) listCloudClusters(req *restful.Request, res *restful.Response) { provider := req.PathParameter("provider") page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) diff --git a/pkg/apiserver/rest/webservice/delivery_target.go b/pkg/apiserver/rest/webservice/delivery_target.go index 8fdf6ba79..5a8843ca9 100644 --- a/pkg/apiserver/rest/webservice/delivery_target.go +++ b/pkg/apiserver/rest/webservice/delivery_target.go @@ -176,7 +176,7 @@ func (dt *DeliveryTargetWebService) updateDeliveryTarget(req *restful.Request, r func (dt *DeliveryTargetWebService) deleteDeliveryTarget(req *restful.Request, res *restful.Response) { deliveryTargetName := req.PathParameter("name") - //deliveryTarget in use, can't be deleted + // deliveryTarget in use, can't be deleted applications, err := dt.applicationUsecase.ListApplications(context.TODO(), apis.ListApplicatioOptions{TargetName: deliveryTargetName}) if err != nil { if !errors.Is(err, datastore.ErrRecordNotExist) { diff --git a/pkg/cloudprovider/aliyun.go b/pkg/cloudprovider/aliyun.go index 0f379fdce..72621c9cc 100644 --- a/pkg/cloudprovider/aliyun.go +++ b/pkg/cloudprovider/aliyun.go @@ -105,6 +105,9 @@ func (provider *AliyunCloudProvider) ListCloudClusters(pageNumber int, pageSize Name: *cluster.Name, Type: *cluster.ClusterType, Zone: *cluster.ZoneId, + ZoneID: *cluster.ZoneId, + RegionID: *cluster.RegionId, + VpcID: *cluster.VpcId, Labels: labels, Status: *cluster.State, APIServerURL: url.APIServerEndpoint, @@ -139,6 +142,9 @@ func (provider *AliyunCloudProvider) GetClusterInfo(clusterID string) (*CloudClu Name: *cluster.Name, Type: *cluster.ClusterType, Zone: *cluster.ZoneId, + ZoneID: *cluster.ZoneId, + RegionID: *cluster.RegionId, + VpcID: *cluster.VpcId, Labels: labels, Status: *cluster.State, APIServerURL: url.APIServerEndpoint, diff --git a/pkg/cloudprovider/types.go b/pkg/cloudprovider/types.go index 5898ee433..699943d7d 100644 --- a/pkg/cloudprovider/types.go +++ b/pkg/cloudprovider/types.go @@ -28,6 +28,9 @@ type CloudCluster struct { Name string `json:"name"` Type string `json:"type"` Zone string `json:"zone"` + ZoneID string `json:"zoneID"` + RegionID string `json:"regionID"` + VpcID string `json:"vpcID"` Labels map[string]string `json:"labels"` Status string `json:"status"` APIServerURL string `json:"apiServerURL"` diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go index 7a685ad5a..6d1e9079e 100644 --- a/pkg/velaql/providers/query/collector.go +++ b/pkg/velaql/providers/query/collector.go @@ -21,7 +21,6 @@ import ( "fmt" "reflect" - "github.com/oam-dev/kubevela/pkg/workflow/types" kruise "github.com/openkruise/kruise-api/apps/v1alpha1" "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" @@ -40,6 +39,7 @@ import ( "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" oamutil "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/workflow/types" ) // AppCollector collect resource created by application diff --git a/test/e2e-apiserver-test/cluster_test.go b/test/e2e-apiserver-test/cluster_test.go index 8e2dd0684..d4c8cdf58 100644 --- a/test/e2e-apiserver-test/cluster_test.go +++ b/test/e2e-apiserver-test/cluster_test.go @@ -17,14 +17,17 @@ limitations under the License. package e2e_apiserver import ( + "fmt" "io/ioutil" "net/http" "os" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/multicluster" util "github.com/oam-dev/kubevela/pkg/utils" ) @@ -68,11 +71,18 @@ var _ = Describe("Test cluster rest api", func() { Expect(clusterResp.Status).Should(Equal("Healthy")) }) - It("Test get clusters", func() { + It("Test list clusters", func() { resp, err := CreateRequest(http.MethodGet, "/clusters/?page=1&pageSize=5", nil) clusterResp := &v1.ListClusterResponse{} Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) - Expect(len(clusterResp.Clusters)).ShouldNot(Equal(0)) + Expect(len(clusterResp.Clusters) >= 2).Should(BeTrue()) + Expect(clusterResp.Clusters[0].Name).Should(Equal(multicluster.ClusterLocalName)) + Expect(clusterResp.Clusters[1].Name).Should(Equal(clusterName)) + resp, err = CreateRequest(http.MethodGet, "/clusters/?page=1&pageSize=5&query="+WorkerClusterName, nil) + clusterResp = &v1.ListClusterResponse{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(len(clusterResp.Clusters) >= 1).Should(BeTrue()) + Expect(clusterResp.Clusters[0].Name).Should(Equal(clusterName)) }) It("Test modify cluster", func() { @@ -88,6 +98,20 @@ var _ = Describe("Test cluster rest api", func() { Expect(clusterResp.Description).ShouldNot(Equal("")) }) + It("Test create ns in cluster", func() { + testNamespace := fmt.Sprintf("test-%d", time.Now().Unix()) + resp, err := CreateRequest(http.MethodPost, "/clusters/"+clusterName+"/namespaces", v1.CreateClusterNamespaceRequest{Namespace: testNamespace}) + Expect(err).Should(Succeed()) + nsResp := &v1.CreateClusterNamespaceResponse{} + Expect(DecodeResponseBody(resp, err, nsResp)).Should(Succeed()) + Expect(nsResp.Exists).Should(Equal(false)) + resp, err = CreateRequest(http.MethodPost, "/clusters/"+clusterName+"/namespaces", v1.CreateClusterNamespaceRequest{Namespace: testNamespace}) + Expect(err).Should(Succeed()) + nsResp = &v1.CreateClusterNamespaceResponse{} + Expect(DecodeResponseBody(resp, err, nsResp)).Should(Succeed()) + Expect(nsResp.Exists).Should(Equal(true)) + }) + }) PContext("Test cloud cluster rest api", func() { diff --git a/test/e2e-apiserver-test/suite_test.go b/test/e2e-apiserver-test/suite_test.go index 02a879bb7..4371d0bd1 100644 --- a/test/e2e-apiserver-test/suite_test.go +++ b/test/e2e-apiserver-test/suite_test.go @@ -23,6 +23,7 @@ import ( "testing" "time" + "github.com/google/uuid" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "sigs.k8s.io/controller-runtime/pkg/client" @@ -50,13 +51,18 @@ var _ = BeforeSuite(func() { ctx := context.Background() - server, err := arest.New(arest.Config{ + cfg := arest.Config{ BindAddr: "127.0.0.1:8000", Datastore: datastore.Config{ Type: "kubeapi", Database: "kubevela", }, - }) + } + cfg.LeaderConfig.ID = uuid.New().String() + cfg.LeaderConfig.LockName = "apiserver-lock" + cfg.LeaderConfig.Duration = time.Second * 5 + + server, err := arest.New(cfg) Expect(err).ShouldNot(HaveOccurred()) Expect(server).ShouldNot(BeNil()) go func() { From 36f5bbc9733e41aa3a2a2caf88f08920ca1cdd2f Mon Sep 17 00:00:00 2001 From: wyike Date: Fri, 19 Nov 2021 17:57:50 +0800 Subject: [PATCH 48/59] Fix: addon spell issue (#2748) Fix: small issue --- apis/types/capability.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/types/capability.go b/apis/types/capability.go index 79dd77610..553edcc42 100644 --- a/apis/types/capability.go +++ b/apis/types/capability.go @@ -212,7 +212,7 @@ type AddonMeta struct { Icon string `json:"icon"` URL string `json:"url,omitempty"` Tags []string `json:"tags,omitempty"` - DeployTo *AddonDeployTo `json:"deploy_to,omitempty"` + DeployTo *AddonDeployTo `json:"deployTo,omitempty"` Dependencies []*AddonDependency `json:"dependencies,omitempty"` } From 54eb6629595bcd6dcfdbe768e1d2db2dfb773121 Mon Sep 17 00:00:00 2001 From: qiaozp <47812250+chivalryq@users.noreply.github.com> Date: Sat, 20 Nov 2021 12:24:35 +0800 Subject: [PATCH 49/59] Feat: add definitions to addon detail API, fix addon cache, async download files (#2738) * add definition to addon detail API * change little * tmp * fix cache * fix import --- pkg/addon/addon.go | 374 +++++++++++++++---------- pkg/apiserver/rest/apis/v1/types.go | 11 +- pkg/apiserver/rest/usecase/addon.go | 199 ++++++++----- pkg/apiserver/rest/webservice/addon.go | 5 +- 4 files changed, 377 insertions(+), 212 deletions(-) diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index ea95b2b2f..860fffca0 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -8,6 +8,7 @@ import ( "path" "path/filepath" "strings" + "sync" "time" v1 "k8s.io/api/core/v1" @@ -32,7 +33,6 @@ import ( "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils" - addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/common" ) @@ -53,6 +53,26 @@ const ( DefinitionsDirName string = "definitions" ) +type ListOptions struct { + GetDetail bool + GetDefinition bool + GetResource bool + GetParameter bool + GetTemplate bool +} + +var ( + ListLevelOptions = ListOptions{} + GetLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true} + EnableLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetResource: true, GetTemplate: true, GetParameter: true} +) + +type AddonErr error + +var ( + AddonNotExist AddonErr = errors.New("addon not exist") +) + type gitHelper struct { Client *github.Client Meta *utils.Content @@ -65,33 +85,40 @@ type GitAddonSource struct { Token string `json:"token,omitempty"` } -// GetAddon get a detailed addon info from GitAddonSource -func GetAddon(name string, git *GitAddonSource) (*types.Addon, error) { - addons, err := ListAddons(true, git) +type AddonReader struct { + addon *types.Addon + h *gitHelper + item *github.RepositoryContent + errChan chan error +} + +func (r *AddonReader) SetReadContent(content *github.RepositoryContent) { + r.item = content +} + +// GetAddon get a addon info from GitAddonSource, can be used for get or enable +func GetAddon(name string, git *GitAddonSource, opt ListOptions) (*types.Addon, error) { + addon, err := getSingleAddonFromGit(git.URL, git.Path, name, git.Token, opt) if err != nil { return nil, err } - - for _, addon := range addons { - if addon.Name == name { - return addon, nil - } - } - return nil, errors.New("addon not exist") + return addon, nil } -// ListAddons list addons' info from GitAddonSource, if not detailed, result only contains types.AddonMeta -func ListAddons(detailed bool, git *GitAddonSource) ([]*types.Addon, error) { - var gitAddons []*types.Addon - gitAddons, err := getAddonsFromGit(git.URL, git.Path, git.Token, detailed) +// ListAddons list addons' info from GitAddonSource +func ListAddons(git *GitAddonSource, opt ListOptions) ([]*types.Addon, error) { + gitAddons, err := getAddonsFromGit(git.URL, git.Path, git.Token, opt) if err != nil { return nil, err } return gitAddons, nil } -func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*types.Addon, error) { +func getAddonsFromGit(baseURL, dir, token string, opt ListOptions) ([]*types.Addon, error) { var addons []*types.Addon + var err error + var wg sync.WaitGroup + errChan := make(chan error, 1) gith, err := createGitHelper(baseURL, dir, token) if err != nil { @@ -106,168 +133,233 @@ func getAddonsFromGit(baseURL, dir, token string, detailed bool) ([]*types.Addon if subItems.GetType() != "dir" { continue } - addonRes := &types.Addon{} - _, files, err := gith.readRepo(subItems.GetPath()) - if err != nil { - return nil, err - } - for _, file := range files { - var err error - - switch strings.ToLower(file.GetName()) { - case ReadmeFileName: - if !detailed { - break - } - err = readReadme(addonRes, gith, file) - case MetadataFileName: - err = readMetadata(addonRes, gith, file) - addonRes.Name = addonutil.TransAddonName(addonRes.Name) - case DefinitionsDirName: - if !detailed { - break - } - err = readDefinitions(addonRes, gith, file) - case ResourcesDirName: - if !detailed { - break - } - err = readResources(addonRes, gith, file) - case TemplateFileName: - if !detailed { - break - } - err = readTemplate(addonRes, gith, file) - } - + wg.Add(1) + go func(item *github.RepositoryContent) { + defer wg.Done() + addonRes, err := getSingleAddonFromGit(baseURL, dir, item.GetName(), token, opt) if err != nil { - return nil, err + errChan <- err + return } - } - - if detailed && addonRes.Parameters != "" { - err = genAddonAPISchema(addonRes) - if err != nil { - continue - } - } - addons = append(addons, addonRes) + addons = append(addons, addonRes) + }(subItems) + } + wg.Wait() + if len(errChan) != 0 { + return nil, <-errChan } return addons, nil } -func readTemplate(addon *types.Addon, h *gitHelper, file *github.RepositoryContent) error { - content, _, err := h.readRepo(*file.Path) +func getSingleAddonFromGit(baseURL, dir, addonName, token string, opt ListOptions) (*types.Addon, error) { + var wg sync.WaitGroup + + gith, err := createGitHelper(baseURL, path.Join(dir, addonName), token) if err != nil { - return err + return nil, err + } + _, items, err := gith.readRepo(gith.Meta.Path) + + reader := AddonReader{ + addon: &types.Addon{}, + h: gith, + errChan: make(chan error, 1), + } + for _, item := range items { + switch strings.ToLower(item.GetName()) { + case ReadmeFileName: + if !opt.GetDetail { + break + } + reader.SetReadContent(item) + wg.Add(1) + go readReadme(&wg, reader) + case MetadataFileName: + reader.SetReadContent(item) + wg.Add(1) + go readMetadata(&wg, reader) + case DefinitionsDirName: + if !opt.GetDefinition { + break + } + reader.SetReadContent(item) + wg.Add(1) + go readDefinitions(&wg, reader) + case ResourcesDirName: + if !opt.GetResource { + break + } + reader.SetReadContent(item) + wg.Add(1) + go readResources(&wg, reader) + case TemplateFileName: + if !opt.GetTemplate { + break + } + reader.SetReadContent(item) + wg.Add(1) + go readTemplate(&wg, reader) + } + } + wg.Wait() + + if opt.GetParameter && reader.addon.Parameters != "" { + err = genAddonAPISchema(reader.addon) + if err != nil { + return nil, err + } + } + return reader.addon, nil + +} + +func readTemplate(wg *sync.WaitGroup, reader AddonReader) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return } data, err := content.GetContent() if err != nil { - return err + reader.errChan <- err + return } dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) - addon.AppTemplate = &v1beta1.Application{} - _, _, err = dec.Decode([]byte(data), nil, addon.AppTemplate) + reader.addon.AppTemplate = &v1beta1.Application{} + _, _, err = dec.Decode([]byte(data), nil, reader.addon.AppTemplate) if err != nil { - return err + reader.errChan <- err + return } - return nil } -func readResources(addon *types.Addon, h *gitHelper, dir *github.RepositoryContent) error { - dirPath := strings.Split(dir.GetPath(), "/") +func readResources(wg *sync.WaitGroup, reader AddonReader) { + defer wg.Done() + dirPath := strings.Split(reader.item.GetPath(), "/") dirPath, err := cutPathUntil(dirPath, ResourcesDirName) if err != nil { - return err + reader.errChan <- err } - _, files, err := h.readRepo(*dir.Path) + _, items, err := reader.h.readRepo(*reader.item.Path) if err != nil { - return err + reader.errChan <- err + return } - for _, file := range files { - switch file.GetType() { + for _, item := range items { + switch item.GetType() { case "file": - content, _, err := h.readRepo(*file.Path) - if err != nil { - return err - } - b, err := content.GetContent() - if err != nil { - return err - } - - if file.GetName() == "parameter.cue" { - addon.Parameters = b - break - } - switch filepath.Ext(file.GetName()) { - case ".cue": - addon.CUETemplates = append(addon.CUETemplates, types.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) - default: - addon.YAMLTemplates = append(addon.YAMLTemplates, types.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) - } + reader.SetReadContent(item) + wg.Add(1) + go readResFile(wg, reader, dirPath) case "dir": - err = readResources(addon, h, file) - if err != nil { - return err - } + reader.SetReadContent(item) + wg.Add(1) + go readResources(wg, reader) + } } - return nil } -func readDefinitions(addon *types.Addon, h *gitHelper, dir *github.RepositoryContent) error { - dirPath := strings.Split(dir.GetPath(), "/") - dirPath, err := cutPathUntil(dirPath, DefinitionsDirName) +// readResFile read single resource file +func readResFile(wg *sync.WaitGroup, reader AddonReader, dirPath []string) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) if err != nil { - return err - } - _, files, err := h.readRepo(*dir.Path) - if err != nil { - return err - } - for _, file := range files { - switch file.GetType() { - case "file": - content, _, err := h.readRepo(*file.Path) - if err != nil { - return err - } - b, err := content.GetContent() - if err != nil { - return err - } - addon.Definitions = append(addon.Definitions, types.AddonElementFile{Data: b, Name: file.GetName(), Path: dirPath}) - case "dir": - err = readDefinitions(addon, h, file) - if err != nil { - return err - } - } - } - return nil -} - -func readMetadata(addon *types.Addon, h *gitHelper, file *github.RepositoryContent) error { - content, _, err := h.readRepo(*file.Path) - if err != nil { - return err + reader.errChan <- err + return } b, err := content.GetContent() if err != nil { - return err + reader.errChan <- err + return + } + + if reader.item.GetName() == "parameter.cue" { + reader.addon.Parameters = b + return + } + switch filepath.Ext(reader.item.GetName()) { + case ".cue": + reader.addon.CUETemplates = append(reader.addon.CUETemplates, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath}) + default: + reader.addon.YAMLTemplates = append(reader.addon.YAMLTemplates, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath}) } - return yaml.Unmarshal([]byte(b), &addon.AddonMeta) } -func readReadme(addon *types.Addon, h *gitHelper, file *github.RepositoryContent) error { - content, _, err := h.readRepo(*file.Path) +func readDefinitions(wg *sync.WaitGroup, reader AddonReader) { + defer wg.Done() + dirPath := strings.Split(reader.item.GetPath(), "/") + dirPath, err := cutPathUntil(dirPath, DefinitionsDirName) if err != nil { - return err + reader.errChan <- err + return } - addon.Detail, err = content.GetContent() - return err + _, items, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + for _, item := range items { + switch item.GetType() { + case "file": + reader.SetReadContent(item) + wg.Add(1) + go readDefFile(wg, reader, dirPath) + case "dir": + reader.SetReadContent(item) + wg.Add(1) + go readDefinitions(wg, reader) + } + } +} + +// readDefFile read single definition file +func readDefFile(wg *sync.WaitGroup, reader AddonReader, dirPath []string) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + b, err := content.GetContent() + if err != nil { + reader.errChan <- err + return + } + reader.addon.Definitions = append(reader.addon.Definitions, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath}) +} + +func readMetadata(wg *sync.WaitGroup, reader AddonReader) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + b, err := content.GetContent() + if err != nil { + reader.errChan <- err + return + } + err = yaml.Unmarshal([]byte(b), &reader.addon.AddonMeta) + if err != nil { + reader.errChan <- err + return + } + return +} + +func readReadme(wg *sync.WaitGroup, reader AddonReader) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + reader.addon.Detail, err = content.GetContent() + return } func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index d2d721454..dee81ea03 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -97,7 +97,16 @@ type DetailAddonResponse struct { UISchema []*utils.UIParameter `json:"uiSchema"` // More details about the addon, e.g. README - Detail string `json:"detail,omitempty"` + Detail string `json:"detail,omitempty"` + Definitions []*AddonDefinition `json:"definitions"` +} + +// AddonDefinition is definition an addon can provide +type AddonDefinition struct { + Name string `json:"name,omitempty"` + // can be component/trait...definition + DefType string `json:"type,omitempty"` + Description string `json:"description,omitempty"` } // AddonStatusResponse defines the format of addon status response diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 86871cf7c..82bec2f05 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -12,6 +12,7 @@ import ( errors2 "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" "sigs.k8s.io/controller-runtime/pkg/client" common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" @@ -35,7 +36,7 @@ type AddonUsecase interface { DeleteAddonRegistry(ctx context.Context, name string) error UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) - ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) + ListAddons(ctx context.Context, registry, query string) ([]*apis.DetailAddonResponse, error) StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error) GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error @@ -43,13 +44,28 @@ type AddonUsecase interface { } // AddonImpl2AddonRes convert types.Addon to the type apiserver need -func AddonImpl2AddonRes(impl *types.Addon) *apis.DetailAddonResponse { - return &apis.DetailAddonResponse{ - AddonMeta: impl.AddonMeta, - APISchema: impl.APISchema, - UISchema: impl.UISchema, - Detail: impl.Detail, +func AddonImpl2AddonRes(impl *types.Addon) (*apis.DetailAddonResponse, error) { + var defs []*apis.AddonDefinition + for _, def := range impl.Definitions { + obj := &unstructured.Unstructured{} + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + _, _, err := dec.Decode([]byte(def.Data), nil, obj) + if err != nil { + return nil, errors.New(fmt.Sprintf("convert %s file content to definition fail", def.Name)) + } + defs = append(defs, &apis.AddonDefinition{ + obj.GetName(), + obj.GetKind(), + obj.GetAnnotations()["definition.oam.dev/description"], + }) } + return &apis.DetailAddonResponse{ + AddonMeta: impl.AddonMeta, + APISchema: impl.APISchema, + UISchema: impl.UISchema, + Detail: impl.Detail, + Definitions: defs, + }, nil } // NewAddonUsecase returns a addon usecase @@ -75,16 +91,47 @@ type addonUsecaseImpl struct { // GetAddon will get addon information func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) { - addonDetails, err := u.ListAddons(ctx, true, registry, "") + var addon *types.Addon + var err error + var exist bool + + if registry == "" { + registries, err := u.ListAddonRegistries(ctx) + if err != nil { + return nil, err + } + for _, r := range registries { + if addon, exist = u.tryGetAddonFromCache(r.Name, name); !exist { + addon, err = pkgaddon.GetAddon(name, r.Git, pkgaddon.GetLevelOptions) + } + if err != nil && !errors.Is(err, pkgaddon.AddonNotExist) { + return nil, err + } + if addon != nil { + break + } + } + } else { + if addon, exist = u.tryGetAddonFromCache(registry, name); !exist { + addonRegistry, err := u.GetAddonRegistry(ctx, registry) + if err != nil { + return nil, err + } + addon, err = pkgaddon.GetAddon(name, addonRegistry.Git, pkgaddon.GetLevelOptions) + if err != nil && !errors.Is(err, pkgaddon.AddonNotExist) { + return nil, err + } + } + } + + if addon == nil { + return nil, bcode.ErrAddonNotExist + } + a, err := AddonImpl2AddonRes(addon) if err != nil { return nil, err } - for _, a := range addonDetails { - if a.Name == name { - return a, nil - } - } - return nil, bcode.ErrAddonNotExist + return a, nil } func (u *addonUsecaseImpl) StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error) { @@ -130,67 +177,66 @@ func (u *addonUsecaseImpl) StatusAddon(ctx context.Context, name string) (*apis. } } -// getCacheKeyWithListOptions will get right cache key for given method registry and detailed, to split different -func getCacheKeyWithListOptions(registry string, detailed bool, query string) string { - var d string - if detailed { - d = "detailed" - } - return fmt.Sprintf("%s/%s/%s", registry, d, query) -} - -func (u *addonUsecaseImpl) ListAddons(ctx context.Context, detailed bool, registry, query string) ([]*apis.DetailAddonResponse, error) { +func (u *addonUsecaseImpl) ListAddons(ctx context.Context, registry, query string) ([]*apis.DetailAddonResponse, error) { var addons []*types.Addon var listAddons []*types.Addon - cacheKey := getCacheKeyWithListOptions(registry, detailed, query) - if u.isRegistryCacheUpToDate(cacheKey) { - addons = u.getRegistryCache(cacheKey) - } else { - rs, err := u.ListAddonRegistries(ctx) - if err != nil { - return nil, err - } + rs, err := u.ListAddonRegistries(ctx) + if err != nil { + return nil, err + } - for _, r := range rs { - if registry != "" && r.Name != registry { - continue - } - listAddons, err = pkgaddon.ListAddons(detailed, r.Git) + for _, r := range rs { + if registry != "" && r.Name != registry { + continue + } + if u.isRegistryCacheUpToDate(r.Name) { + listAddons = u.getRegistryCache(r.Name) + } else { + listAddons, err = pkgaddon.ListAddons(r.Git, pkgaddon.GetLevelOptions) if err != nil { log.Logger.Errorf("fail to get addons from registry %s", r.Name) continue } - addons = mergeAddons(addons, listAddons) - } - - if query != "" { - var filtered []*types.Addon - for i, addon := range addons { - if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) { - filtered = append(filtered, addons[i]) + // if list addons, details will be retrieved later + go func() { + addonDetails, err := pkgaddon.ListAddons(r.Git, pkgaddon.EnableLevelOptions) + if err != nil { + return } - } - addons = filtered + u.putRegistryCache(r.Name, addonDetails) + }() } - sort.Slice(addons, func(i, j int) bool { - return addons[i].Name < addons[j].Name - }) - - if detailed { - for _, addon := range addons { - // render default ui schema - addon.UISchema = renderDefaultUISchema(addon.APISchema) - } - } - - u.putRegistryCache(cacheKey, addons) + addons = mergeAddons(addons, listAddons) } - var addonRes []*apis.DetailAddonResponse + if query != "" { + var filtered []*types.Addon + for i, addon := range addons { + if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) { + filtered = append(filtered, addons[i]) + } + } + addons = filtered + } + sort.Slice(addons, func(i, j int) bool { + return addons[i].Name < addons[j].Name + }) + + for _, addon := range addons { + // render default ui schema + addon.UISchema = renderDefaultUISchema(addon.APISchema) + } + + var addonReses []*apis.DetailAddonResponse for _, a := range addons { - addonRes = append(addonRes, AddonImpl2AddonRes(a)) + addonRes, err := AddonImpl2AddonRes(a) + if err != nil { + log.Logger.Errorf("err while converting AddonImpl to DetailAddonResponse: %v", err) + continue + } + addonReses = append(addonReses, addonRes) } - return addonRes, nil + return addonReses, nil } func (u *addonUsecaseImpl) DeleteAddonRegistry(ctx context.Context, name string) error { @@ -262,19 +308,36 @@ func (u *addonUsecaseImpl) ListAddonRegistries(ctx context.Context) ([]*apis.Add return list, nil } +func (u *addonUsecaseImpl) tryGetAddonFromCache(registry, addonName string) (*types.Addon, bool) { + if u.isRegistryCacheUpToDate(registry) { + addons := u.getRegistryCache(registry) + for _, a := range addons { + if a.Name == addonName { + return a, true + } + } + } + return nil, false +} + func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error { + var addon *types.Addon + var err error registries, err := u.ListAddonRegistries(ctx) if err != nil { return err } for _, r := range registries { - addon, err := pkgaddon.GetAddon(name, r.Git) - - if err != nil && errors.Is(err, bcode.ErrAddonNotExist) { - continue - } else if err != nil { + var exist bool + if addon, exist = u.tryGetAddonFromCache(r.Name, name); !exist { + addon, err = pkgaddon.GetAddon(name, r.Git, pkgaddon.EnableLevelOptions) + } + if err != nil && !errors.Is(err, pkgaddon.AddonNotExist) { return bcode.WrapGithubRateLimitErr(err) } + if addon == nil { + continue + } app, defs, err := pkgaddon.RenderApplication(addon, args.Args) if err != nil { @@ -322,7 +385,7 @@ func (u *addonUsecaseImpl) getRegistryCache(name string) []*types.Addon { } func (u *addonUsecaseImpl) putRegistryCache(name string, addons []*types.Addon) { - u.addonRegistryCache[name] = restutils.NewMemoryCache(addons, time.Minute*3) + u.addonRegistryCache[name] = restutils.NewMemoryCache(addons, time.Minute*10) } func (u *addonUsecaseImpl) isRegistryCacheUpToDate(name string) bool { diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index e41dfda32..b92f421ff 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -65,6 +65,7 @@ func (s *addonWebService) GetWebService() *restful.WebService { Returns(200, "", apis.DetailAddonResponse{}). Returns(400, "", bcode.Bcode{}). Param(ws.PathParameter("name", "addon name to query detail").DataType("string").Required(true)). + Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")). Writes(apis.DetailAddonResponse{})) // GET status @@ -99,7 +100,7 @@ func (s *addonWebService) GetWebService() *restful.WebService { } func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response) { - detailAddons, err := s.addonUsecase.ListAddons(req.Request.Context(), false, req.QueryParameter("registry"), req.QueryParameter("query")) + detailAddons, err := s.addonUsecase.ListAddons(req.Request.Context(), req.QueryParameter("registry"), req.QueryParameter("query")) if err != nil { bcode.ReturnError(req, res, err) return @@ -120,7 +121,7 @@ func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response func (s *addonWebService) detailAddon(req *restful.Request, res *restful.Response) { name := req.PathParameter("name") - addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name, "") + addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name, req.QueryParameter("registry")) if err != nil { bcode.ReturnError(req, res, err) return From ff405cd62a245fee2dba0f715bdb8065214eb5f0 Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Sat, 20 Nov 2021 13:05:52 +0800 Subject: [PATCH 50/59] Feat: add workflow record actions (#2733) * Feat: add application revision actions * refactor workflow record and application revision * generate doc * fix rebase * fix rebase * delete comment * fix comment * delete suspend status * use apply instead of update * find latest comlete revision if the revision is not specified * delete name * fix primary key --- docs/apidoc/swagger.json | 534 ++++++++++++++---- pkg/apiserver/model/workflow.go | 30 +- pkg/apiserver/rest/apis/v1/types.go | 11 +- pkg/apiserver/rest/usecase/application.go | 25 +- .../rest/usecase/application_test.go | 44 +- .../rest/usecase/testdata/ui-schema.yaml | 122 ++-- pkg/apiserver/rest/usecase/workflow.go | 313 +++++++--- pkg/apiserver/rest/usecase/workflow_test.go | 240 ++++++-- pkg/apiserver/rest/webservice/workflow.go | 76 +++ .../application_controller_test.go | 5 +- pkg/oam/labels.go | 3 + pkg/velaql/providers/query/collector.go | 3 +- pkg/workflow/recorder/recorder.go | 4 +- pkg/workflow/types/types.go | 2 - pkg/workflow/workflow.go | 8 +- 15 files changed, 1099 insertions(+), 321 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index c4cc5281b..8283de3a9 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -898,6 +898,42 @@ } }, "/api/v1/applications/{name}/envs": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list policy for application", + "operationId": "listApplicationEnvs", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListApplicationEnvBinding" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, "post": { "consumes": [ "application/xml", @@ -957,7 +993,7 @@ "application" ], "summary": "set application differences in the specified environment", - "operationId": "updateApplicationEnvBinding", + "operationId": "updateApplicationEnv", "parameters": [ { "type": "string", @@ -968,7 +1004,7 @@ }, { "type": "string", - "description": "identifier of the application ", + "description": "identifier of the envBinding ", "name": "envName", "in": "path", "required": true @@ -1019,7 +1055,7 @@ }, { "type": "string", - "description": "identifier of the application envbinding", + "description": "identifier of the envBinding ", "name": "envName", "in": "path", "required": true @@ -1946,6 +1982,52 @@ } } }, + "/api/v1/clusters/{clusterName}/namespaces": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create namespace in cluster", + "operationId": "createNamespace", + "parameters": [ + { + "type": "string", + "description": "name of the target cluster", + "name": "clusterName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateClusterNamespaceRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateClusterNamespaceResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/definitions": { "get": { "consumes": [ @@ -1964,9 +2046,9 @@ "parameters": [ { "enum": [ - "workflowstep", "component", - "trait" + "trait", + "workflowstep" ], "type": "string", "description": "query the definition type", @@ -2652,6 +2734,135 @@ } } }, + "/api/v1/workflows/{name}/records/{record}/resume": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "workflow" + ], + "summary": "resume suspend workflow record", + "operationId": "resumeWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/workflows/{name}/records/{record}/rollback": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "workflow" + ], + "summary": "rollback suspend application record", + "operationId": "rollbackWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the rollback revision", + "name": "rollbackVersion", + "in": "query" + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/workflows/{name}/records/{record}/terminate": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "workflow" + ], + "summary": "terminate suspend workflow record", + "operationId": "terminateWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/v1/namespaces/{namespace}/applications/{appname}": { "get": { "consumes": [ @@ -2809,6 +3020,9 @@ "name", "type", "zone", + "zoneID", + "regionID", + "vpcID", "labels", "status", "apiServerURL", @@ -2836,22 +3050,31 @@ "provider": { "type": "string" }, + "regionID": { + "type": "string" + }, "status": { "type": "string" }, "type": { "type": "string" }, + "vpcID": { + "type": "string" + }, "zone": { "type": "string" + }, + "zoneID": { + "type": "string" } } }, "common.AppRolloutStatus": { "required": [ "rollingState", - "batchRollingState", "currentBatch", + "batchRollingState", "upgradedReplicas", "upgradedReadyReplicas", "lastTargetAppRevision" @@ -3427,8 +3650,8 @@ }, "model.ApplicationRevision": { "required": [ - "createTime", "updateTime", + "createTime", "appPrimaryKey", "version", "status", @@ -3484,12 +3707,18 @@ "required": [ "alias", "description", - "type" + "type", + "createTime", + "updateTime" ], "properties": { "alias": { "type": "string" }, + "createTime": { + "type": "string", + "format": "date-time" + }, "description": { "type": "string" }, @@ -3498,13 +3727,16 @@ }, "type": { "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" } } }, "model.Cluster": { "required": [ - "createTime", - "updateTime", + "model", "name", "alias", "description", @@ -3525,10 +3757,6 @@ "apiServerURL": { "type": "string" }, - "createTime": { - "type": "string", - "format": "date-time" - }, "dashboardURL": { "type": "string" }, @@ -3550,6 +3778,9 @@ "type": "string" } }, + "model": { + "$ref": "#/definitions/model.Model" + }, "name": { "type": "string" }, @@ -3561,10 +3792,6 @@ }, "status": { "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" } } }, @@ -3590,13 +3817,14 @@ "model.ProviderInfo": { "required": [ "provider", - "name", - "id", - "zone", + "clusterID", "labels" ], "properties": { - "id": { + "clusterID": { + "type": "string" + }, + "clusterName": { "type": "string" }, "labels": { @@ -3605,14 +3833,20 @@ "type": "string" } }, - "name": { - "type": "string" - }, "provider": { "type": "string" }, + "regionID": { + "type": "string" + }, + "vpcID": { + "type": "string" + }, "zone": { "type": "string" + }, + "zoneID": { + "type": "string" } } }, @@ -3855,9 +4089,16 @@ }, "v1.AddonStatusResponse": { "required": [ - "phase" + "phase", + "args" ], "properties": { + "args": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "enabling_progress": { "$ref": "#/definitions/v1.EnablingProgress" }, @@ -3887,12 +4128,6 @@ "description": { "type": "string" }, - "envBinding": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.EnvBinding" - } - }, "icon": { "type": "string" }, @@ -3938,13 +4173,14 @@ }, "v1.ApplicationDeployResponse": { "required": [ - "createTime", + "name", "version", + "envName", + "createTime", "status", "reason", "deployUser", "note", - "envName", "triggerType" ], "properties": { @@ -3958,6 +4194,9 @@ "envName": { "type": "string" }, + "name": { + "type": "string" + }, "note": { "type": "string" }, @@ -4033,6 +4272,7 @@ "v1.ApplicationRevisionBase": { "required": [ "createTime", + "name", "version", "status", "reason", @@ -4052,6 +4292,9 @@ "envName": { "type": "string" }, + "name": { + "type": "string" + }, "note": { "type": "string" }, @@ -4141,12 +4384,18 @@ "required": [ "name", "type", - "properties" + "properties", + "createTime", + "updateTime" ], "properties": { "alias": { "type": "string" }, + "createTime": { + "type": "string", + "format": "date-time" + }, "description": { "type": "string" }, @@ -4158,6 +4407,10 @@ }, "type": { "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" } } }, @@ -4420,8 +4673,8 @@ }, "v1.CreateApplicationEnvRequest": { "required": [ - "targetNames", - "name" + "name", + "targetNames" ], "properties": { "alias": { @@ -4564,6 +4817,7 @@ }, "v1.CreateCloudClusterResponse": { "required": [ + "clusterName", "clusterID", "status" ], @@ -4571,11 +4825,34 @@ "clusterID": { "type": "string" }, + "clusterName": { + "type": "string" + }, "status": { "type": "string" } } }, + "v1.CreateClusterNamespaceRequest": { + "required": [ + "namespace" + ], + "properties": { + "namespace": { + "type": "string" + } + } + }, + "v1.CreateClusterNamespaceResponse": { + "required": [ + "exists" + ], + "properties": { + "exists": { + "type": "boolean" + } + } + }, "v1.CreateClusterRequest": { "required": [ "name", @@ -4721,7 +4998,8 @@ "required": [ "appName", "name", - "default" + "default", + "envName" ], "properties": { "alias": { @@ -4736,6 +5014,9 @@ "description": { "type": "string" }, + "envName": { + "type": "string" + }, "name": { "type": "string" }, @@ -4857,14 +5138,15 @@ }, "v1.DetailApplicationResponse": { "required": [ - "name", + "alias", "namespace", "description", "createTime", "updateTime", "icon", - "alias", + "name", "policies", + "envBindings", "status", "resourceInfo", "workflowStatus" @@ -4880,10 +5162,10 @@ "description": { "type": "string" }, - "envBinding": { + "envBindings": { "type": "array", "items": { - "$ref": "#/definitions/v1.EnvBinding" + "type": "string" } }, "icon": { @@ -4927,20 +5209,19 @@ }, "v1.DetailClusterResponse": { "required": [ - "createTime", - "alias", - "icon", - "status", - "reason", "provider", - "dashboardURL", - "updateTime", - "name", - "labels", - "description", "apiServerURL", + "model", + "name", + "description", + "icon", + "dashboardURL", "kubeConfig", "kubeConfigSecret", + "alias", + "labels", + "status", + "reason", "resourceInfo" ], "properties": { @@ -4950,10 +5231,6 @@ "apiServerURL": { "type": "string" }, - "createTime": { - "type": "string", - "format": "date-time" - }, "dashboardURL": { "type": "string" }, @@ -4975,6 +5252,9 @@ "type": "string" } }, + "model": { + "$ref": "#/definitions/model.Model" + }, "name": { "type": "string" }, @@ -4989,22 +5269,18 @@ }, "status": { "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" } } }, "v1.DetailComponentResponse": { "required": [ - "appPrimaryKey", - "creator", - "name", - "type", - "createTime", "updateTime", - "alias" + "type", + "creator", + "alias", + "createTime", + "appPrimaryKey", + "name" ], "properties": { "alias": { @@ -5099,10 +5375,10 @@ }, "v1.DetailDeliveryTargetResponse": { "required": [ + "createTime", "updateTime", "name", - "namespace", - "createTime" + "namespace" ], "properties": { "alias": { @@ -5135,13 +5411,13 @@ }, "v1.DetailPolicyResponse": { "required": [ - "createTime", - "updateTime", "name", "type", "description", "creator", - "properties" + "properties", + "createTime", + "updateTime" ], "properties": { "createTime": { @@ -5171,17 +5447,17 @@ }, "v1.DetailRevisionResponse": { "required": [ - "triggerType", - "workflowName", - "createTime", - "appPrimaryKey", "reason", "deployUser", + "triggerType", + "workflowName", + "envName", + "createTime", + "version", "note", "updateTime", - "version", - "status", - "envName" + "appPrimaryKey", + "status" ], "properties": { "appPrimaryKey": { @@ -5226,10 +5502,9 @@ }, "v1.DetailWorkflowRecordResponse": { "required": [ - "terminated", "name", "namespace", - "suspend", + "status", "deployTime", "deployUser", "note", @@ -5256,18 +5531,15 @@ "type": "string", "format": "date-time" }, + "status": { + "type": "string" + }, "steps": { "type": "array", "items": { "$ref": "#/definitions/common.WorkflowStepStatus" } }, - "suspend": { - "type": "boolean" - }, - "terminated": { - "type": "boolean" - }, "triggerType": { "type": "string" } @@ -5275,13 +5547,14 @@ }, "v1.DetailWorkflowResponse": { "required": [ - "default", - "createTime", - "updateTime", "name", "alias", "description", "enable", + "default", + "envName", + "createTime", + "updateTime", "workflowRecord" ], "properties": { @@ -5301,6 +5574,9 @@ "enable": { "type": "boolean" }, + "envName": { + "type": "string" + }, "name": { "type": "string" }, @@ -5372,6 +5648,42 @@ } } }, + "v1.EnvBindingBase": { + "required": [ + "name", + "targetNames", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "targetNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, "v1.ListAddonRegistryResponse": { "required": [ "registrys" @@ -5398,6 +5710,19 @@ } } }, + "v1.ListApplicationEnvBinding": { + "required": [ + "envBindings" + ], + "properties": { + "envBindings": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBindingBase" + } + } + } + }, "v1.ListApplicationPolicy": { "required": [ "policies" @@ -5432,7 +5757,7 @@ "creations": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/v1.CreateCloudClusterResponse" } } } @@ -5457,7 +5782,8 @@ }, "v1.ListClusterResponse": { "required": [ - "clusters" + "clusters", + "total" ], "properties": { "clusters": { @@ -5465,6 +5791,10 @@ "items": { "$ref": "#/definitions/v1.ClusterBase" } + }, + "total": { + "type": "integer", + "format": "int64" } } }, @@ -5813,7 +6143,8 @@ "v1.UpdateWorkflowRequest": { "required": [ "enable", - "default" + "default", + "envName" ], "properties": { "alias": { @@ -5828,6 +6159,9 @@ "enable": { "type": "boolean" }, + "envName": { + "type": "string" + }, "steps": { "type": "array", "items": { @@ -5846,6 +6180,7 @@ "description", "enable", "default", + "envName", "createTime", "updateTime" ], @@ -5866,6 +6201,9 @@ "enable": { "type": "boolean" }, + "envName": { + "type": "string" + }, "name": { "type": "string" }, @@ -5879,8 +6217,7 @@ "required": [ "name", "namespace", - "suspend", - "terminated" + "status" ], "properties": { "name": { @@ -5893,17 +6230,14 @@ "type": "string", "format": "date-time" }, + "status": { + "type": "string" + }, "steps": { "type": "array", "items": { "$ref": "#/definitions/common.WorkflowStepStatus" } - }, - "suspend": { - "type": "boolean" - }, - "terminated": { - "type": "boolean" } } }, diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index b7785dfbc..2827682b5 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -83,15 +83,15 @@ func (w *Workflow) Index() map[string]string { // WorkflowRecord is the workflow record database model type WorkflowRecord struct { Model - WorkflowPrimaryKey string `json:"workflowPrimaryKey"` - AppPrimaryKey string `json:"appPrimaryKey"` - // name is `appName-version`, which is the same as the primary key of deploy event - Name string `json:"name"` - Namespace string `json:"namespace"` - StartTime time.Time `json:"startTime,omitempty"` - Suspend bool `json:"suspend"` - Terminated bool `json:"terminated"` - Steps []common.WorkflowStepStatus `json:"steps,omitempty"` + WorkflowPrimaryKey string `json:"workflowPrimaryKey"` + AppPrimaryKey string `json:"appPrimaryKey"` + RevisionPrimaryKey string `json:"revisionPrimaryKey"` + Name string `json:"name"` + Namespace string `json:"namespace"` + StartTime time.Time `json:"startTime,omitempty"` + Finished string `json:"finished"` + Steps []common.WorkflowStepStatus `json:"steps,omitempty"` + Status string `json:"status"` } // TableName return custom table name @@ -110,8 +110,20 @@ func (w *WorkflowRecord) Index() map[string]string { if w.Name != "" { index["name"] = w.Name } + if w.Namespace != "" { + index["namespace"] = w.Namespace + } if w.WorkflowPrimaryKey != "" { index["workflowPrimaryKey"] = w.WorkflowPrimaryKey } + if w.AppPrimaryKey != "" { + index["appPrimaryKey"] = w.AppPrimaryKey + } + if w.RevisionPrimaryKey != "" { + index["revisionPrimaryKey"] = w.RevisionPrimaryKey + } + if w.Finished != "" { + index["finished"] = w.Finished + } return index } diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index dee81ea03..7919a53d9 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -606,12 +606,11 @@ type DetailWorkflowRecordResponse struct { // WorkflowRecord workflow record type WorkflowRecord struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - StartTime time.Time `json:"startTime,omitempty"` - Suspend bool `json:"suspend"` - Terminated bool `json:"terminated"` - Steps []common.WorkflowStepStatus `json:"steps,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace"` + StartTime time.Time `json:"startTime,omitempty"` + Status string `json:"status"` + Steps []common.WorkflowStepStatus `json:"steps,omitempty"` } // ApplicationDeployRequest the application deploy or update event request diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index 5b2063279..d55405839 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -81,7 +81,7 @@ type ApplicationUsecase interface { DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string, req apisv1.UpdateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) ListRevisions(ctx context.Context, appName, envName, status string, page, pageSize int) (*apisv1.ListRevisionsResponse, error) - DetailRevision(ctx context.Context, appName, revisionName string) (*apisv1.DetailRevisionResponse, error) + DetailRevision(ctx context.Context, appName, revisionVersion string) (*apisv1.DetailRevisionResponse, error) } type applicationUsecaseImpl struct { @@ -570,6 +570,7 @@ func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Ap // 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, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) { + // TODO: rollback to handle all the error case // step1: Render oam application version := utils.GenerateVersion("") oamApp, err := c.renderOAMApplication(ctx, app, req.WorkflowName, version) @@ -613,7 +614,11 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat if err := c.ds.Add(ctx, appRevision); err != nil { return nil, err } - // step3: check and create namespace + // step3: create workflow record + if err := c.workflowUsecase.CreateWorkflowRecord(ctx, oamApp); err != nil { + return nil, err + } + // step4: check and create namespace var namespace corev1.Namespace if err := c.kubeClient.Get(ctx, types.NamespacedName{Name: oamApp.Namespace}, &namespace); apierrors.IsNotFound(err) { namespace.Name = oamApp.Namespace @@ -622,7 +627,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat return nil, bcode.ErrCreateNamespace } } - // step4: apply to controller cluster + // step5: apply to controller cluster err = c.apply.Apply(ctx, oamApp) if err != nil { appRevision.Status = model.RevisionStatusFail @@ -638,7 +643,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat log.Logger.Warnf("update deploy event failure %s", err.Error()) } - // step5: update deploy event status + // step6: update deploy event status return &apisv1.ApplicationDeployResponse{ ApplicationRevisionBase: apisv1.ApplicationRevisionBase{ Version: appRevision.Version, @@ -663,6 +668,8 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo Labels: appModel.Labels, Annotations: map[string]string{ oam.AnnotationDeployVersion: version, + // publish version is the identifier of workflow record + oam.AnnotationPublishVersion: utils.GenerateVersion(reqWorkflowName), }, }, } @@ -1040,7 +1047,11 @@ func (c *applicationUsecaseImpl) ListRevisions(ctx context.Context, appName, env revision.Status = status } - revisions, err := c.ds.List(ctx, &revision, &datastore.ListOptions{Page: page, PageSize: pageSize}) + revisions, err := c.ds.List(ctx, &revision, &datastore.ListOptions{ + Page: page, + PageSize: pageSize, + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + }) if err != nil { return nil, err } @@ -1072,10 +1083,10 @@ func (c *applicationUsecaseImpl) ListRevisions(ctx context.Context, appName, env return resp, nil } -func (c *applicationUsecaseImpl) DetailRevision(ctx context.Context, appName, revisionName string) (*apisv1.DetailRevisionResponse, error) { +func (c *applicationUsecaseImpl) DetailRevision(ctx context.Context, appName, revisionVersion string) (*apisv1.DetailRevisionResponse, error) { var revision = model.ApplicationRevision{ AppPrimaryKey: appName, - Version: revisionName, + Version: revisionVersion, } if err := c.ds.Get(ctx, &revision); err != nil { return nil, err diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 6178cd3c3..f0e5b94b8 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -26,12 +26,17 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/apiserver/model" v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/utils/apply" ) @@ -459,7 +464,7 @@ var _ = Describe("Test application usecase function", func() { Expect(revisions.Total).Should(Equal(int64(0))) }) - It("Test DetailRevisions function", func() { + It("Test DetailRevision function", func() { err := workflowUsecase.createTestApplicationRevision(context.TODO(), &model.ApplicationRevision{ AppPrimaryKey: "test-app", Version: "123", @@ -472,3 +477,40 @@ var _ = Describe("Test application usecase function", func() { Expect(revision.DeployUser).Should(Equal("test-user")) }) }) + +func createTestSuspendApp(ctx context.Context, appName, revisionVersion, wfName, recordName string, kubeClient client.Client) (*v1beta1.Application, error) { + testapp := &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: "default", + Annotations: map[string]string{ + oam.AnnotationDeployVersion: revisionVersion, + oam.AnnotationWorkflowName: wfName, + oam.AnnotationPublishVersion: recordName, + }, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "test-component", + Type: "worker", + Properties: &runtime.RawExtension{Raw: []byte(`{"test":"test"}`)}, + Traits: []common.ApplicationTrait{}, + Scopes: map[string]string{}, + }}, + }, + Status: common.AppStatus{ + Workflow: &common.WorkflowStatus{ + Suspend: true, + }, + }, + } + + if err := kubeClient.Create(ctx, testapp); err != nil { + return nil, err + } + if err := kubeClient.Status().Patch(ctx, testapp, client.Merge); err != nil { + return nil, err + } + + return testapp, nil +} diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index bc313008f..05ce5bba3 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -137,41 +137,6 @@ label: ReadinessProbe检测 sort: 13 subParameters: - - description: How often, in seconds, to execute the probe. - jsonKey: periodSeconds - label: PeriodSeconds - sort: 100 - uiType: Number - validate: - defaultValue: 10 - required: true - - description: Minimum consecutive successes for the probe to be considered successful - after having failed. - jsonKey: successThreshold - label: SuccessThreshold - sort: 100 - uiType: Number - validate: - defaultValue: 1 - required: true - - description: Instructions for assessing container health by probing a TCP socket. - Either this attribute or the exec attribute or the httpGet attribute MUST be - specified. This attribute is mutually exclusive with both the exec attribute - and the httpGet attribute. - jsonKey: tcpSocket - label: TcpSocket - sort: 100 - subParameters: - - description: The TCP socket within the container that should be probed to assess - container health. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - uiType: KV - validate: {} - description: Number of seconds after which the probe times out. jsonKey: timeoutSeconds label: TimeoutSeconds @@ -265,6 +230,41 @@ validate: defaultValue: 0 required: true + - description: How often, in seconds, to execute the probe. + jsonKey: periodSeconds + label: PeriodSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 10 + required: true + - description: Minimum consecutive successes for the probe to be considered successful + after having failed. + jsonKey: successThreshold + label: SuccessThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} uiType: Group validate: {} - description: Instructions for assessing whether the container is alive. @@ -272,26 +272,6 @@ label: LivenessProbe检测 sort: 15 subParameters: - - description: Instructions for assessing container health by executing a command. - Either this attribute or the httpGet attribute or the tcpSocket attribute MUST - be specified. This attribute is mutually exclusive with both the httpGet attribute - and the tcpSocket attribute. - jsonKey: exec - label: Exec - sort: 100 - subParameters: - - description: A command to be executed inside the container to assess its health. - Each space delimited token of the command is a separate array element. Commands - exiting 0 are considered to be successful probes, whilst all other exit codes - are considered failures. - jsonKey: command - label: Command - sort: 100 - uiType: Strings - validate: - required: true - uiType: KV - validate: {} - description: Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe). jsonKey: failureThreshold @@ -400,6 +380,26 @@ validate: defaultValue: 1 required: true + - description: Instructions for assessing container health by executing a command. + Either this attribute or the httpGet attribute or the tcpSocket attribute MUST + be specified. This attribute is mutually exclusive with both the httpGet attribute + and the tcpSocket attribute. + jsonKey: exec + label: Exec + sort: 100 + subParameters: + - description: A command to be executed inside the container to assess its health. + Each space delimited token of the command is a separate array element. Commands + exiting 0 are considered to be successful probes, whilst all other exit codes + are considered failures. + jsonKey: command + label: Command + sort: 100 + uiType: Strings + validate: + required: true + uiType: KV + validate: {} uiType: Group validate: {} - description: Specify image pull policy for your service @@ -416,12 +416,6 @@ value: Always - label: 永不更新 value: Never -- description: Specify image pull secrets for your service - jsonKey: imagePullSecrets - label: ImagePullSecrets - sort: 100 - uiType: Strings - validate: {} - description: If addRevisionLabel is true, the appRevision label will be added to the underlying pods jsonKey: addRevisionLabel @@ -431,3 +425,9 @@ validate: defaultValue: false required: true +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + sort: 100 + uiType: Strings + validate: {} diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 7e35e0e37..6f0682a26 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -20,12 +20,14 @@ import ( "context" "errors" "fmt" - "strings" + "strconv" + "helm.sh/helm/v3/pkg/time" appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/apiserver/clients" @@ -33,10 +35,11 @@ import ( "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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/pkg/workflow/recorder" + "github.com/oam-dev/kubevela/pkg/utils/apply" ) const ( @@ -51,10 +54,14 @@ type WorkflowUsecase interface { GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) DeleteWorkflow(ctx context.Context, workflowName string) error CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + CreateWorkflowRecord(ctx context.Context, app *v1beta1.Application) error UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) ListWorkflowRecords(ctx context.Context, workflowName string, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) DetailWorkflowRecord(ctx context.Context, workflowName, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) SyncWorkflowRecord(ctx context.Context) error + ResumeRecord(ctx context.Context, appModel *model.Application, recordName string) error + TerminateRecord(ctx context.Context, appModel *model.Application, recordName string) error + RollbackRecord(ctx context.Context, appModel *model.Application, recordName, revisionName string) error } // NewWorkflowUsecase new workflow usecase @@ -66,12 +73,14 @@ func NewWorkflowUsecase(ds datastore.DataStore) WorkflowUsecase { return &workflowUsecaseImpl{ ds: ds, kubeClient: kubecli, + apply: apply.NewAPIApplicator(kubecli), } } type workflowUsecaseImpl struct { ds datastore.DataStore kubeClient client.Client + apply apply.Applicator } // DeleteWorkflow delete application workflow @@ -268,10 +277,9 @@ func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflow return nil, err } - version := strings.TrimPrefix(recordName, fmt.Sprintf("%s-", record.AppPrimaryKey)) var revision = model.ApplicationRevision{ AppPrimaryKey: record.AppPrimaryKey, - Version: version, + Version: record.RevisionPrimaryKey, } err = w.ds.Get(ctx, &revision) if err != nil { @@ -288,115 +296,256 @@ func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflow } func (w *workflowUsecaseImpl) SyncWorkflowRecord(ctx context.Context) error { - crList := &appsv1.ControllerRevisionList{} - matchLabels := metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{ - { - Key: labelControllerRevisionSync, - Operator: metav1.LabelSelectorOpDoesNotExist, - }, - { - Key: recorder.LabelRecordVersion, - Operator: metav1.LabelSelectorOpExists, - }, - }, + var record = model.WorkflowRecord{ + Finished: "false", } - selector, err := metav1.LabelSelectorAsSelector(&matchLabels) + // list all unfinished workflow records + records, err := w.ds.List(ctx, &record, &datastore.ListOptions{}) if err != nil { return err } - if err := w.kubeClient.List(ctx, crList, &client.ListOptions{ - LabelSelector: selector, - }); err != nil { - return err - } - for i, cr := range crList.Items { - app, err := util.RawExtension2Application(cr.Data) + for _, item := range records { + app := &v1beta1.Application{} + index := item.Index() + appPrimaryKey := index["appPrimaryKey"] + namespace := index["namespace"] + recordName := index["name"] + + if err := w.kubeClient.Get(ctx, types.NamespacedName{ + Name: appPrimaryKey, + Namespace: namespace, + }, app); err != nil { + klog.ErrorS(err, "failed to get app", "app name", appPrimaryKey) + return err + } + + // try to sync the status from the running application + if app.Annotations != nil && app.Annotations[oam.AnnotationPublishVersion] == recordName { + if err := w.syncWorkflowStatus(ctx, app, recordName); err != nil { + klog.ErrorS(err, "failed to sync workflow status", "app name", appPrimaryKey, "workflow record name", recordName) + } + continue + } + + // try to sync the status from the controller revision + cr := &appsv1.ControllerRevision{} + if err := w.kubeClient.Get(ctx, types.NamespacedName{ + Name: fmt.Sprintf("record-%s-%s", appPrimaryKey, recordName), + Namespace: namespace, + }, cr); err != nil { + klog.ErrorS(err, "failed to get controller revision", "app name", appPrimaryKey, "workflow record name", recordName) + continue + } + appInRevision, err := util.RawExtension2Application(cr.Data) if err != nil { - klog.ErrorS(err, "failed to get app data", "controller revision name", cr.Name) + klog.ErrorS(err, "failed to get app data in controller revision", "controller revision name", cr.Name, "app name", appPrimaryKey, "workflow record name", recordName) + continue + } + if err := w.syncWorkflowStatus(ctx, appInRevision, recordName); err != nil { + klog.ErrorS(err, "failed to sync workflow status", "app name", appPrimaryKey, "workflow record version", recordName) continue } - if app.Annotations == nil { - klog.ErrorS(err, "empty application annotation", "controller revision name", cr.Name) - continue + } + + return nil +} + +func (w *workflowUsecaseImpl) syncWorkflowStatus(ctx context.Context, app *v1beta1.Application, recordName string) error { + var record = &model.WorkflowRecord{ + AppPrimaryKey: app.Name, + Name: recordName, + } + if err := w.ds.Get(ctx, record); err != nil { + return err + } + var revision = &model.ApplicationRevision{ + AppPrimaryKey: app.Name, + Version: record.RevisionPrimaryKey, + } + if err := w.ds.Get(ctx, revision); err != nil { + return err + } + + if app.Status.Workflow != nil { + status := app.Status.Workflow + summaryStatus := model.RevisionStatusRunning + if status.Finished { + summaryStatus = model.RevisionStatusComplete + } + if status.Terminated { + summaryStatus = model.RevisionStatusTerminated } - if _, ok := app.Annotations[oam.AnnotationWorkflowName]; !ok { - klog.ErrorS(err, "missing application workflow name", "controller revision name", cr.Name) - continue - } - revisionName, ok := app.Annotations[oam.AnnotationDeployVersion] - if !ok { - klog.ErrorS(err, "failed to get application revision name", "controller revision name", cr.Name) - continue + record.Status = summaryStatus + record.Steps = status.Steps + record.Finished = strconv.FormatBool(status.Finished) + + if err := w.ds.Put(ctx, record); err != nil { + return err } - if err := w.createWorkflowRecord(ctx, app, strings.TrimPrefix(cr.Name, "record-")); err != nil && !errors.Is(err, datastore.ErrRecordExist) { - klog.ErrorS(err, "failed to create workflow record", "controller revision name", cr.Name) - continue - } - - err = w.updateRecordApplicationRevisionStatus(ctx, app.Name, revisionName, app.Status.Workflow.Terminated) - if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { - klog.ErrorS(err, "failed to update deploy event status", "controller revision name", cr.Name) - continue - } - - crList.Items[i].Labels[labelControllerRevisionSync] = "true" - if err := w.kubeClient.Update(ctx, &crList.Items[i]); err != nil { - klog.ErrorS(err, "failed to update annotation", "controller revision name", cr.Name) - continue + revision.Status = summaryStatus + if err := w.ds.Put(ctx, revision); err != nil { + return err } } return nil } -func (w *workflowUsecaseImpl) updateRecordApplicationRevisionStatus(ctx context.Context, appPrimaryKey, version string, terminated bool) error { - var applicationRevision = &model.ApplicationRevision{ - AppPrimaryKey: appPrimaryKey, - Version: version, +func (w *workflowUsecaseImpl) CreateWorkflowRecord(ctx context.Context, app *v1beta1.Application) error { + if app.Annotations == nil { + return fmt.Errorf("empty annotations in application") } - if err := w.ds.Get(ctx, applicationRevision); err != nil { - return err + if app.Annotations[oam.AnnotationWorkflowName] == "" { + return fmt.Errorf("failed to get workflow name from application") } - if terminated { - applicationRevision.Status = model.RevisionStatusTerminated - } else { - applicationRevision.Status = model.RevisionStatusComplete + if app.Annotations[oam.AnnotationPublishVersion] == "" { + return fmt.Errorf("failed to get record version from application") } - - if err := w.ds.Put(ctx, applicationRevision); err != nil { - return err + if app.Annotations[oam.AnnotationDeployVersion] == "" { + return fmt.Errorf("failed to get deploy version from application") } - return nil -} - -func (w *workflowUsecaseImpl) createWorkflowRecord(ctx context.Context, app *v1beta1.Application, revisionName string) error { - status := app.Status.Workflow - return w.ds.Add(ctx, &model.WorkflowRecord{ WorkflowPrimaryKey: app.Annotations[oam.AnnotationWorkflowName], AppPrimaryKey: app.Name, - Name: strings.TrimPrefix(revisionName, "record-"), + RevisionPrimaryKey: app.Annotations[oam.AnnotationDeployVersion], + Name: app.Annotations[oam.AnnotationPublishVersion], Namespace: app.Namespace, - StartTime: status.StartTime.Time, - Suspend: status.Suspend, - Terminated: status.Terminated, - Steps: status.Steps, + Finished: "false", + StartTime: time.Now().Time, + Status: model.RevisionStatusInit, }) } +func (w *workflowUsecaseImpl) ResumeRecord(ctx context.Context, appModel *model.Application, recordName string) error { + oamApp, err := w.checkRecordRunning(ctx, appModel) + if err != nil { + return err + } + + oamApp.Status.Workflow.Suspend = false + if err := w.kubeClient.Status().Patch(ctx, oamApp, client.Merge); err != nil { + return err + } + if err := w.syncWorkflowStatus(ctx, oamApp, recordName); err != nil { + return err + } + + return nil +} + +func (w *workflowUsecaseImpl) TerminateRecord(ctx context.Context, appModel *model.Application, recordName string) error { + oamApp, err := w.checkRecordRunning(ctx, appModel) + if err != nil { + return err + } + + oamApp.Status.Workflow.Terminated = true + if err := w.kubeClient.Status().Patch(ctx, oamApp, client.Merge); err != nil { + return err + } + if err := w.syncWorkflowStatus(ctx, oamApp, recordName); err != nil { + return err + } + + return nil +} + +func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *model.Application, recordName, revisionVersion string) error { + if revisionVersion == "" { + // find the latest complete revision version + var revision = model.ApplicationRevision{ + AppPrimaryKey: appModel.Name, + Status: model.RevisionStatusComplete, + } + + revisions, err := w.ds.List(ctx, &revision, &datastore.ListOptions{ + Page: 0, + PageSize: 1, + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + }) + if err != nil { + return err + } + if len(revisions) == 0 { + fmt.Errorf("there is no complete revision, please specify a revision version") + } + revisionVersion = revisions[0].Index()["version"] + } + + oamApp, err := w.checkRecordRunning(ctx, appModel) + if err != nil { + return err + } + + var record = &model.WorkflowRecord{ + AppPrimaryKey: appModel.Name, + Name: recordName, + } + if err := w.ds.Get(ctx, record); err != nil { + return err + } + var rollbackRevision = model.ApplicationRevision{ + AppPrimaryKey: appModel.Name, + Version: revisionVersion, + } + if err := w.ds.Get(ctx, &rollbackRevision); err != nil { + return err + } + + rollBackApp := &v1beta1.Application{} + if err := yaml.Unmarshal([]byte(rollbackRevision.ApplyAppConfig), rollBackApp); err != nil { + return err + } + // replace the application spec + oamApp.Spec.Components = rollBackApp.Spec.Components + oamApp.Spec.Policies = rollBackApp.Spec.Policies + if oamApp.Annotations == nil { + oamApp.Annotations = make(map[string]string) + } + newRecordName := utils.GenerateVersion(record.WorkflowPrimaryKey) + oamApp.Annotations[oam.AnnotationDeployVersion] = revisionVersion + oamApp.Annotations[oam.AnnotationPublishVersion] = newRecordName + + // create a new workflow record + if err := w.CreateWorkflowRecord(ctx, oamApp); err != nil { + return err + } + + if err := w.apply.Apply(ctx, oamApp); err != nil { + // rollback error case + if err := w.ds.Delete(ctx, &model.WorkflowRecord{Name: newRecordName}); err != nil { + klog.Error(err, "failed to delete record", newRecordName) + } + return err + } + + return nil +} + +func (w *workflowUsecaseImpl) checkRecordRunning(ctx context.Context, appModel *model.Application) (*v1beta1.Application, error) { + oamApp := &v1beta1.Application{} + if err := w.kubeClient.Get(ctx, types.NamespacedName{Name: appModel.Name, Namespace: appModel.Namespace}, oamApp); err != nil { + return nil, err + } + if oamApp.Status.Workflow != nil && !oamApp.Status.Workflow.Suspend && !oamApp.Status.Workflow.Terminated && !oamApp.Status.Workflow.Finished { + return nil, fmt.Errorf("workflow is still running, can not operate a running workflow") + } + + oamApp.SetGroupVersionKind(v1beta1.ApplicationKindVersionKind) + return oamApp, nil +} + func convertFromRecordModel(record *model.WorkflowRecord) *apisv1.WorkflowRecord { return &apisv1.WorkflowRecord{ - Name: record.Name, - Namespace: record.Namespace, - StartTime: record.StartTime, - Suspend: record.Suspend, - Terminated: record.Terminated, - Steps: record.Steps, + Name: record.Name, + Namespace: record.Namespace, + StartTime: record.StartTime, + Status: record.Status, + Steps: record.Steps, } } diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 37f35d7e6..7f7305487 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -27,12 +27,14 @@ import ( appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" "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/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/utils/apply" ) var _ = Describe("Test workflow usecase functions", func() { @@ -40,7 +42,7 @@ var _ = Describe("Test workflow usecase functions", func() { workflowUsecase *workflowUsecaseImpl ) BeforeEach(func() { - workflowUsecase = &workflowUsecaseImpl{ds: ds, kubeClient: k8sClient} + workflowUsecase = &workflowUsecaseImpl{ds: ds, kubeClient: k8sClient, apply: apply.NewAPIApplicator(k8sClient)} }) It("Test CreateWorkflow function", func() { req := apisv1.CreateWorkflowRequest{ @@ -75,35 +77,40 @@ var _ = Describe("Test workflow usecase functions", func() { }) It("Test ListWorkflowRecords function", func() { - By("create some controller revisions to test list workflow records") + By("create some workflow records to test list workflow records") raw, err := yaml.YAMLToJSON([]byte(yamlStr)) Expect(err).Should(BeNil()) app := &v1beta1.Application{} err = json.Unmarshal(raw, app) Expect(err).Should(BeNil()) + app.Annotations[oam.AnnotationWorkflowName] = "list-workflow-name" for i := 0; i < 3; i++ { - err := workflowUsecase.createWorkflowRecord(context.TODO(), app, fmt.Sprintf("record-test-%v", i)) + app.Annotations[oam.AnnotationPublishVersion] = fmt.Sprintf("list-workflow-name-%d", i) + err := workflowUsecase.CreateWorkflowRecord(context.TODO(), app) Expect(err).Should(BeNil()) } - resp, err := workflowUsecase.ListWorkflowRecords(context.TODO(), "test-workflow-name", 0, 10) + resp, err := workflowUsecase.ListWorkflowRecords(context.TODO(), "list-workflow-name", 0, 10) Expect(err).Should(BeNil()) Expect(resp.Total).Should(Equal(int64(3))) }) It("Test DetailWorkflowRecord function", func() { - By("create one controller revision to test detail workflow record") + By("create one workflow record to test detail workflow record") raw, err := yaml.YAMLToJSON([]byte(yamlStr)) Expect(err).Should(BeNil()) app := &v1beta1.Application{} err = json.Unmarshal(raw, app) Expect(err).Should(BeNil()) - err = workflowUsecase.createWorkflowRecord(context.TODO(), app, "record-test-123") + app.Annotations[oam.AnnotationWorkflowName] = "test-workflow-name" + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-name-123" + app.Annotations[oam.AnnotationDeployVersion] = "1234" + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) Expect(err).Should(BeNil()) - var deployEvent = &model.ApplicationRevision{ + var revision = &model.ApplicationRevision{ AppPrimaryKey: "test", - Version: "123", + Version: "1234", Status: model.RevisionStatusInit, DeployUser: "test-user", Note: "test-commit", @@ -111,57 +118,212 @@ var _ = Describe("Test workflow usecase functions", func() { WorkflowName: "test-workflow-name", } - err = workflowUsecase.createTestApplicationRevision(context.TODO(), deployEvent) + err = workflowUsecase.createTestApplicationRevision(context.TODO(), revision) Expect(err).Should(BeNil()) - detail, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-123") + detail, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-workflow-name-123") Expect(err).Should(BeNil()) - Expect(detail.WorkflowRecord.Name).Should(Equal("test-123")) + Expect(detail.WorkflowRecord.Name).Should(Equal("test-workflow-name-123")) Expect(detail.DeployUser).Should(Equal("test-user")) }) It("Test SyncWorkflowRecord function", func() { - By("create one controller revision to test sync workflow record") - ctx := context.Background() + By("create one workflow record to test sync status from application") raw, err := yaml.YAMLToJSON([]byte(yamlStr)) Expect(err).Should(BeNil()) + app := &v1beta1.Application{} + err = json.Unmarshal(raw, app) + Expect(err).Should(BeNil()) + app.Status.Workflow.Finished = false + app.Annotations[oam.AnnotationWorkflowName] = "test-workflow-name" + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-name-233" + app.Annotations[oam.AnnotationDeployVersion] = "4321" + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + Expect(err).Should(BeNil()) + + By("create one revision to test sync workflow record") + var revision = &model.ApplicationRevision{ + AppPrimaryKey: "test", + Version: "4321", + Status: model.RevisionStatusInit, + DeployUser: "test-user", + WorkflowName: "test-workflow-name", + } + err = workflowUsecase.createTestApplicationRevision(context.TODO(), revision) + Expect(err).Should(BeNil()) + + By("create the application to sync") + ctx := context.Background() + app.Status.Workflow.Finished = true + err = workflowUsecase.kubeClient.Create(ctx, app) + Expect(err).Should(BeNil()) + err = workflowUsecase.kubeClient.Status().Patch(ctx, app, client.Merge) + + err = workflowUsecase.SyncWorkflowRecord(ctx) + Expect(err).Should(BeNil()) + + By("check the record") + record, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-workflow-name-233") + Expect(err).Should(BeNil()) + Expect(record.Status).Should(Equal(model.RevisionStatusComplete)) + + By("check the application revision") + err = workflowUsecase.ds.Get(ctx, revision) + Expect(err).Should(BeNil()) + Expect(revision.Status).Should(Equal(model.RevisionStatusComplete)) + + By("create another workflow record to test sync status from controller revision") + app.Status.Workflow.Finished = false + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-name-111" + app.Annotations[oam.AnnotationDeployVersion] = "1111" + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + Expect(err).Should(BeNil()) + + By("create another revision to test sync workflow record") + var anotherRevision = &model.ApplicationRevision{ + AppPrimaryKey: "test", + Version: "1111", + Status: model.RevisionStatusInit, + DeployUser: "test-user", + WorkflowName: "test-workflow-name", + } + err = workflowUsecase.createTestApplicationRevision(context.TODO(), anotherRevision) + Expect(err).Should(BeNil()) + + By("create one controller revision to test sync workflow record") + Expect(err).Should(BeNil()) cr := &appsv1.ControllerRevision{ ObjectMeta: metav1.ObjectMeta{ - Name: "record-test-1234", + Name: "record-test-test-workflow-name-111", Namespace: "default", - Labels: map[string]string{"vela.io/wf-revision": "1234"}, + Labels: map[string]string{"vela.io/wf-revision": "test-workflow-name-111"}, }, Data: runtime.RawExtension{Raw: raw}, } err = workflowUsecase.kubeClient.Create(ctx, cr) Expect(err).Should(BeNil()) - By("create one deploy event to test sync workflow record") - var deployEvent = &model.ApplicationRevision{ - AppPrimaryKey: "test", - Version: "1234", - Status: model.RevisionStatusInit, - DeployUser: "test-user", - WorkflowName: "test-workflow-name", - } - - err = workflowUsecase.createTestApplicationRevision(context.TODO(), deployEvent) - Expect(err).Should(BeNil()) - err = workflowUsecase.SyncWorkflowRecord(ctx) Expect(err).Should(BeNil()) By("check the record") - app := &v1beta1.Application{} - err = json.Unmarshal(raw, app) + anotherRecord, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-workflow-name-111") Expect(err).Should(BeNil()) - err = workflowUsecase.createWorkflowRecord(context.TODO(), app, "test-1234") - Expect(err).Should(Equal(datastore.ErrRecordExist)) + Expect(anotherRecord.Status).Should(Equal(model.RevisionStatusComplete)) - By("check the deploy event") - err = workflowUsecase.ds.Get(ctx, deployEvent) + By("check the application revision") + err = workflowUsecase.ds.Get(ctx, anotherRevision) Expect(err).Should(BeNil()) - Expect(deployEvent.Status).Should(Equal(model.RevisionStatusComplete)) + Expect(anotherRevision.Status).Should(Equal(model.RevisionStatusComplete)) + }) + + It("Test ResumeRecord function", func() { + ctx := context.TODO() + app, err := createTestSuspendApp(ctx, "resume-app", "revision-resume1", "workflow-resume", "workflow-resume-1", workflowUsecase.kubeClient) + Expect(err).Should(BeNil()) + + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + Expect(err).Should(BeNil()) + + err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ + AppPrimaryKey: "resume-app", + + Version: "revision-resume1", + Status: model.RevisionStatusRunning, + }) + Expect(err).Should(BeNil()) + + err = workflowUsecase.ResumeRecord(ctx, &model.Application{ + Name: "resume-app", + Namespace: "default", + }, "workflow-resume-1") + Expect(err).Should(BeNil()) + + record, err := workflowUsecase.DetailWorkflowRecord(ctx, "workflow-resume", "workflow-resume-1") + Expect(err).Should(BeNil()) + Expect(record.Status).Should(Equal(model.RevisionStatusRunning)) + }) + + It("Test TerminateRecord function", func() { + ctx := context.TODO() + app, err := createTestSuspendApp(ctx, "terminate-app", "revision-terminate1", "workflow-terminate", "workflow-terminate-1", workflowUsecase.kubeClient) + Expect(err).Should(BeNil()) + + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + Expect(err).Should(BeNil()) + + err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ + AppPrimaryKey: "terminate-app", + Version: "revision-terminate1", + Status: model.RevisionStatusRunning, + }) + Expect(err).Should(BeNil()) + + err = workflowUsecase.TerminateRecord(ctx, &model.Application{ + Name: "terminate-app", + Namespace: "default", + }, "workflow-terminate-1") + Expect(err).Should(BeNil()) + + record, err := workflowUsecase.DetailWorkflowRecord(ctx, "workflow-terminate", "workflow-terminate-1") + Expect(err).Should(BeNil()) + Expect(record.Status).Should(Equal(model.RevisionStatusTerminated)) + }) + + It("Test RollbackRecord function", func() { + ctx := context.TODO() + app, err := createTestSuspendApp(ctx, "rollback-app", "revision-rollback1", "workflow-rollback", "workflow-rollback-1", workflowUsecase.kubeClient) + Expect(err).Should(BeNil()) + + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + Expect(err).Should(BeNil()) + + err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ + AppPrimaryKey: "rollback-app", + Version: "revision-rollback1", + Status: model.RevisionStatusRunning, + }) + Expect(err).Should(BeNil()) + err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ + AppPrimaryKey: "rollback-app", + Version: "revision-rollback0", + ApplyAppConfig: `{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"annotations":{"app.oam.dev/workflowName":"workflow-rollback","app.oam.dev/deployVersion":"revision-rollback1","vela.io/publish-version":"workflow-rollback1"},"name":"first-vela-app","namespace":"default"},"spec":{"components":[{"name":"express-server","properties":{"image":"crccheck/hello-world","port":8000},"traits":[{"properties":{"domain":"testsvc.example.com","http":{"/":8000}},"type":"ingress-1-20"}],"type":"webservice"}]}}`, + Status: model.RevisionStatusComplete, + }) + Expect(err).Should(BeNil()) + + err = workflowUsecase.RollbackRecord(ctx, &model.Application{ + Name: "rollback-app", + Namespace: "default", + }, "workflow-rollback-1", "revision-rollback0") + Expect(err).Should(BeNil()) + + recordsNum, err := workflowUsecase.ds.Count(ctx, &model.WorkflowRecord{ + AppPrimaryKey: "rollback-app", + WorkflowPrimaryKey: "workflow-rollback", + RevisionPrimaryKey: "revision-rollback0", + }, nil) + Expect(err).Should(BeNil()) + Expect(recordsNum).Should(Equal(int64(1))) + + By("rollback application without revision version") + app.Annotations[oam.AnnotationPublishVersion] = "workflow-rollback-2" + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + Expect(err).Should(BeNil()) + + err = workflowUsecase.RollbackRecord(ctx, &model.Application{ + Name: "rollback-app", + Namespace: "default", + }, "workflow-rollback-2", "") + Expect(err).Should(BeNil()) + + recordsNum, err = workflowUsecase.ds.Count(ctx, &model.WorkflowRecord{ + AppPrimaryKey: "rollback-app", + WorkflowPrimaryKey: "workflow-rollback", + RevisionPrimaryKey: "revision-rollback0", + }, nil) + Expect(err).Should(BeNil()) + Expect(recordsNum).Should(Equal(int64(2))) }) }) @@ -171,6 +333,7 @@ metadata: annotations: app.oam.dev/workflowName: test-workflow-name app.oam.dev/deployVersion: "1234" + vela.io/publish-version: "test-workflow-name-111" name: test namespace: default spec: @@ -202,10 +365,11 @@ status: phase: succeeded type: apply-component suspend: false - terminated: false` + terminated: false + finished: true` -func (w *workflowUsecaseImpl) createTestApplicationRevision(ctx context.Context, deployEvent *model.ApplicationRevision) error { - if err := w.ds.Add(ctx, deployEvent); err != nil { +func (w *workflowUsecaseImpl) createTestApplicationRevision(ctx context.Context, revision *model.ApplicationRevision) error { + if err := w.ds.Add(ctx, revision); err != nil { return err } return nil diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 8ccbecd83..be92b120b 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -112,6 +112,36 @@ func (w *workflowWebService) GetWebService() *restful.WebService { Returns(200, "", apis.DetailWorkflowRecordResponse{}). Writes(apis.DetailWorkflowRecordResponse{}).Do(returns200, returns500)) + ws.Route(ws.GET("/{name}/records/{record}/resume").To(w.resumeWorkflowRecord). + Doc("resume suspend workflow record"). + Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(w.applicationCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) + + ws.Route(ws.GET("/{name}/records/{record}/terminate").To(w.terminateWorkflowRecord). + Doc("terminate suspend workflow record"). + Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(w.applicationCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) + + ws.Route(ws.GET("/{name}/records/{record}/rollback").To(w.rollbackWorkflowRecord). + Doc("rollback suspend application record"). + Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Param(ws.QueryParameter("rollbackVersion", "identifier of the rollback revision").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(w.applicationCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) return ws } @@ -125,6 +155,22 @@ func (w *workflowWebService) workflowCheckFilter(req *restful.Request, res *rest chain.ProcessFilter(req, res) } +func (w *workflowWebService) applicationCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + workflow, err := w.workflowUsecase.GetWorkflow(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + app, err := w.applicationUsecase.GetApplication(req.Request.Context(), workflow.AppPrimaryKey) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplication, app)) + chain.ProcessFilter(req, res) +} + func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res *restful.Response) { if req.QueryParameter("appName") == "" { bcode.ReturnError(req, res, bcode.ErrMustQueryByApp) @@ -260,3 +306,33 @@ func (w *workflowWebService) detailWorkflowRecord(req *restful.Request, res *res return } } + +func (w *workflowWebService) resumeWorkflowRecord(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := w.workflowUsecase.ResumeRecord(req.Request.Context(), app, req.PathParameter("record")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + return +} + +func (w *workflowWebService) terminateWorkflowRecord(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := w.workflowUsecase.TerminateRecord(req.Request.Context(), app, req.PathParameter("record")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + return +} + +func (w *workflowWebService) rollbackWorkflowRecord(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := w.workflowUsecase.RollbackRecord(req.Request.Context(), app, req.PathParameter("record"), req.QueryParameter("rollbackVersion")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + return +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go index 85d3ef3ad..b457f9e77 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go @@ -55,7 +55,6 @@ import ( "github.com/oam-dev/kubevela/pkg/oam/testutil" "github.com/oam-dev/kubevela/pkg/oam/util" common2 "github.com/oam-dev/kubevela/pkg/utils/common" - wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" ) // TODO: Refactor the tests to not copy and paste duplicated code 10 times @@ -2089,7 +2088,7 @@ var _ = Describe("Test Application Controller", func() { app := appwithNoTrait.DeepCopy() app.Name = "vela-test-app-trace" app.SetNamespace(ns.Name) - app.Annotations = map[string]string{wfTypes.AnnotationPublishVersion: "v134"} + app.Annotations = map[string]string{oam.AnnotationPublishVersion: "v134"} Expect(k8sClient.Create(ctx, ns)).Should(BeNil()) Expect(k8sClient.Create(ctx, app)).Should(BeNil()) @@ -2116,7 +2115,7 @@ var _ = Describe("Test Application Controller", func() { web.Spec.Replicas = pointer.Int32(0) Expect(k8sClient.Update(ctx, web)).Should(BeNil()) - checkApp.Annotations[wfTypes.AnnotationPublishVersion] = "v135" + checkApp.Annotations[oam.AnnotationPublishVersion] = "v135" Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil()) testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey}) checkApp = &v1beta1.Application{} diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index 348da8f0f..45333220f 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -132,6 +132,9 @@ const ( // AnnotationDeployVersion know the version number of the deployment. AnnotationDeployVersion = "app.oam.dev/deployVersion" + // AnnotationPublishVersion is annotation that record the application workflow version. + AnnotationPublishVersion = "vela.io/publish-version" + // AnnotationWorkflowName specifies the workflow name for execution. AnnotationWorkflowName = "app.oam.dev/workflowName" diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go index 6d1e9079e..c2cafc7e1 100644 --- a/pkg/velaql/providers/query/collector.go +++ b/pkg/velaql/providers/query/collector.go @@ -39,7 +39,6 @@ import ( "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" oamutil "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/pkg/workflow/types" ) // AppCollector collect resource created by application @@ -77,7 +76,7 @@ func (c *AppCollector) CollectLatestResourceFromApp() ([]AppResources, error) { if app.Status.LatestRevision != nil { revision = app.Status.LatestRevision.Revision } - publishVersion := app.GetAnnotations()[types.AnnotationPublishVersion] + publishVersion := app.GetAnnotations()[oam.AnnotationPublishVersion] appRevName := fmt.Sprintf("%s-v%d", app.Name, revision) comps := make(map[string][]Resource, len(app.Spec.Components)) diff --git a/pkg/workflow/recorder/recorder.go b/pkg/workflow/recorder/recorder.go index bbfd0e713..0cc0f1c37 100644 --- a/pkg/workflow/recorder/recorder.go +++ b/pkg/workflow/recorder/recorder.go @@ -66,9 +66,7 @@ func (r *recorder) Save(version string, data []byte) Store { if version == "" { wfStatus := r.source.Status.Workflow if wfStatus != nil { - if !strings.Contains(wfStatus.AppRevision, ":") { - version = wfStatus.AppRevision - } + version = strings.ReplaceAll(wfStatus.AppRevision, ":", "-") } } diff --git a/pkg/workflow/types/types.go b/pkg/workflow/types/types.go index be9aaf35f..ce069ee64 100644 --- a/pkg/workflow/types/types.go +++ b/pkg/workflow/types/types.go @@ -77,6 +77,4 @@ type Action interface { const ( // ContextKeyMetadata is key that refer to application metadata. ContextKeyMetadata = "metadata__" - // AnnotationPublishVersion is annotation that record the application workflow version. - AnnotationPublishVersion = "vela.io/publish-version" ) diff --git a/pkg/workflow/workflow.go b/pkg/workflow/workflow.go index fa83b2f7c..618fd70a1 100644 --- a/pkg/workflow/workflow.go +++ b/pkg/workflow/workflow.go @@ -132,12 +132,6 @@ func (w *workflow) ExecuteSteps(ctx context.Context, appRev *oamcore.Application // Trace record the workflow execute history. func (w *workflow) Trace() error { - // add annotation for apiserver sync - if w.app.Annotations == nil { - w.app.Annotations = make(map[string]string) - } - w.app.Annotations[oam.AnnotationWorkflowName] = w.app.Name - data, err := json.Marshal(w.app) if err != nil { return err @@ -339,7 +333,7 @@ func (e *engine) needStop() bool { func computeAppRevisionHash(rev string, app *oamcore.Application) (string, error) { version := "" if annos := app.Annotations; annos != nil { - version = annos[wfTypes.AnnotationPublishVersion] + version = annos[oam.AnnotationPublishVersion] } if version == "" { specHash, err := utils.ComputeSpecHash(app.Spec) From adcb7bd65e6119b0b04007f58a134f36cd754b03 Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Sat, 20 Nov 2021 19:50:36 +0800 Subject: [PATCH 51/59] Feat: support update component and query app statistics info (#2746) * Feat: change swagger config * Feat: support update component and query app statistics info * Fix: fix workflow list bug * Fix: fix test bug * Fix: fix e2e test bug * Feat: change workflow api * Fix: fix app deploy e2e test bug * Fix: change e2e test * Fix: fix workflow bug * Fix: fix deploy bug * Fix: fix selector bug * Feat: support recycle env * Fix: debug e2e * Fix: fix e2e case bug Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 1228 ++++++++++------- pkg/apiserver/model/workflow.go | 10 +- pkg/apiserver/rest/apis/v1/types.go | 67 +- pkg/apiserver/rest/usecase/application.go | 302 ++-- .../rest/usecase/application_test.go | 94 +- pkg/apiserver/rest/usecase/delivery_target.go | 3 + pkg/apiserver/rest/usecase/envbinding.go | 88 +- .../rest/usecase/testdata/ui-schema.yaml | 184 +-- pkg/apiserver/rest/usecase/workflow.go | 235 ++-- pkg/apiserver/rest/usecase/workflow_test.go | 231 +++- pkg/apiserver/rest/utils/bcode/application.go | 9 + pkg/apiserver/rest/utils/bcode/workflow.go | 6 + pkg/apiserver/rest/webservice/application.go | 239 +++- .../rest/webservice/delivery_target.go | 2 +- pkg/apiserver/rest/webservice/webservice.go | 3 +- pkg/apiserver/rest/webservice/workflow.go | 161 +-- pkg/oam/labels.go | 8 +- test/e2e-apiserver-test/application_test.go | 115 +- test/e2e-apiserver-test/suite_test.go | 2 +- 19 files changed, 1793 insertions(+), 1194 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 8283de3a9..93dc02006 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -228,6 +228,12 @@ "name": "name", "in": "path", "required": true + }, + { + "type": "string", + "description": "filter addons from given registry", + "name": "registry", + "in": "query" } ], "responses": { @@ -834,7 +840,7 @@ "tags": [ "application" ], - "summary": "detail component for application ", + "summary": "detail component for application ", "operationId": "detailComponent", "parameters": [ { @@ -857,6 +863,50 @@ } } } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update component config", + "operationId": "updateComponent", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateApplicationComponentRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ComponentBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } } }, "/api/v1/applications/{name}/deploy": { @@ -1448,6 +1498,44 @@ } } }, + "/api/v1/applications/{name}/statistics": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail one application ", + "operationId": "applicationStatistics", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationStatisticsResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/applications/{name}/template": { "post": { "consumes": [ @@ -1494,6 +1582,485 @@ } } }, + "/api/v1/applications/{name}/workflows": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list application workflow", + "operationId": "listApplicationWorkflows", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + } + ], + "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": [ + "application" + ], + "summary": "create application workflow", + "operationId": "createApplicationWorkflow", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateWorkflowRequest" + } + }, + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "400": { + "description": "create failure", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail application workflow", + "operationId": "detailWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workfloc.", + "name": "workflowName", + "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": [ + "application" + ], + "summary": "update application workflow config", + "operationId": "updateWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "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" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "deletet workflow", + "operationId": "deleteWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "query application workflow execution record", + "operationId": "listWorkflowRecords", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "query the page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "query the page size number", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "query application workflow execution record detail", + "operationId": "detailWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/resume": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "resume suspend workflow record", + "operationId": "resumeWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/rollback": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "rollback suspend application record", + "operationId": "rollbackWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the rollback revision", + "name": "rollbackVersion", + "in": "query" + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/terminate": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "terminate suspend workflow record", + "operationId": "terminateWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/clusters": { "get": { "consumes": [ @@ -2441,428 +3008,6 @@ } } }, - "/api/v1/workflows": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "workflow" - ], - "summary": "list application workflow", - "operationId": "listApplicationWorkflows", - "parameters": [ - { - "type": "string", - "description": "identifier of the application.", - "name": "appName", - "in": "query", - "required": true - }, - { - "type": "boolean", - "description": "query based on enable status", - "name": "enable", - "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": [ - "workflow" - ], - "summary": "create application workflow", - "operationId": "createApplicationWorkflow", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateWorkflowRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "400": { - "description": "create failure", - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/api/v1/workflows/{name}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "workflow" - ], - "summary": "detail application workflow", - "operationId": "detailWorkflow", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow.", - "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": [ - "workflow" - ], - "summary": "update application workflow config", - "operationId": "updateWorkflow", - "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" - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "workflow" - ], - "summary": "deletet workflow", - "operationId": "deleteWorkflow", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - } - ], - "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": [ - "workflow" - ], - "summary": "query application workflow execution record", - "operationId": "listWorkflowRecords", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "query the page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "query the page size number", - "name": "pageSize", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/api/v1/workflows/{name}/records/{record}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "workflow" - ], - "summary": "query application workflow execution record detail", - "operationId": "detailWorkflowRecord", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the workflow record", - "name": "record", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/api/v1/workflows/{name}/records/{record}/resume": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "workflow" - ], - "summary": "resume suspend workflow record", - "operationId": "resumeWorkflowRecord", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the workflow record", - "name": "record", - "in": "path", - "required": true - } - ], - "responses": { - "200": {}, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/workflows/{name}/records/{record}/rollback": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "workflow" - ], - "summary": "rollback suspend application record", - "operationId": "rollbackWorkflowRecord", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the workflow record", - "name": "record", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the rollback revision", - "name": "rollbackVersion", - "in": "query" - } - ], - "responses": { - "200": {}, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, - "/api/v1/workflows/{name}/records/{record}/terminate": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "workflow" - ], - "summary": "terminate suspend workflow record", - "operationId": "terminateWorkflowRecord", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the workflow record", - "name": "record", - "in": "path", - "required": true - } - ], - "responses": { - "200": {}, - "400": { - "schema": { - "$ref": "#/definitions/bcode.Bcode" - } - } - } - } - }, "/v1/namespaces/{namespace}/applications/{appname}": { "get": { "consumes": [ @@ -3072,11 +3217,11 @@ }, "common.AppRolloutStatus": { "required": [ - "rollingState", - "currentBatch", - "batchRollingState", - "upgradedReplicas", "upgradedReadyReplicas", + "rollingState", + "batchRollingState", + "currentBatch", + "upgradedReplicas", "lastTargetAppRevision" ], "properties": { @@ -3566,8 +3711,8 @@ }, "model.ApplicationComponent": { "required": [ - "createTime", "updateTime", + "createTime", "appPrimaryKey", "creator", "name", @@ -3650,8 +3795,8 @@ }, "model.ApplicationRevision": { "required": [ - "updateTime", "createTime", + "updateTime", "appPrimaryKey", "version", "status", @@ -3885,7 +4030,7 @@ "$ref": "#/definitions/types.AddonDependency" } }, - "deploy_to": { + "deployTo": { "$ref": "#/definitions/types.AddonDeployTo" }, "description": { @@ -4074,6 +4219,19 @@ } } }, + "v1.AddonDefinition": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, "v1.AddonRegistryMeta": { "required": [ "name" @@ -4173,15 +4331,14 @@ }, "v1.ApplicationDeployResponse": { "required": [ - "name", - "version", "envName", + "triggerType", "createTime", + "version", "status", "reason", "deployUser", - "note", - "triggerType" + "note" ], "properties": { "createTime": { @@ -4194,9 +4351,6 @@ "envName": { "type": "string" }, - "name": { - "type": "string" - }, "note": { "type": "string" }, @@ -4243,7 +4397,7 @@ "properties": { "componentNum": { "type": "integer", - "format": "int32" + "format": "int64" } } }, @@ -4272,7 +4426,6 @@ "v1.ApplicationRevisionBase": { "required": [ "createTime", - "name", "version", "status", "reason", @@ -4292,9 +4445,6 @@ "envName": { "type": "string" }, - "name": { - "type": "string" - }, "note": { "type": "string" }, @@ -4312,6 +4462,32 @@ } } }, + "v1.ApplicationStatisticsResponse": { + "required": [ + "envCount", + "deliveryTargetCount", + "revisonCount", + "workflowCount" + ], + "properties": { + "deliveryTargetCount": { + "type": "integer", + "format": "int64" + }, + "envCount": { + "type": "integer", + "format": "int64" + }, + "revisonCount": { + "type": "integer", + "format": "int64" + }, + "workflowCount": { + "type": "integer", + "format": "int64" + } + } + }, "v1.ApplicationStatusResponse": { "required": [ "envName", @@ -4673,8 +4849,8 @@ }, "v1.CreateApplicationEnvRequest": { "required": [ - "name", - "targetNames" + "targetNames", + "name" ], "properties": { "alias": { @@ -5057,6 +5233,10 @@ "alias": { "type": "string" }, + "appNum": { + "type": "integer", + "format": "int64" + }, "cluster": { "$ref": "#/definitions/v1.ClusterTarget" }, @@ -5084,21 +5264,28 @@ }, "v1.DetailAddonResponse": { "required": [ - "description", - "icon", "name", "version", + "description", + "icon", "schema", - "uiSchema" + "uiSchema", + "definitions" ], "properties": { + "definitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonDefinition" + } + }, "dependencies": { "type": "array", "items": { "$ref": "#/definitions/types.AddonDependency" } }, - "deploy_to": { + "deployTo": { "$ref": "#/definitions/types.AddonDeployTo" }, "description": { @@ -5138,18 +5325,17 @@ }, "v1.DetailApplicationResponse": { "required": [ - "alias", - "namespace", "description", "createTime", "updateTime", "icon", "name", + "alias", + "namespace", "policies", "envBindings", "status", - "resourceInfo", - "workflowStatus" + "resourceInfo" ], "properties": { "alias": { @@ -5198,30 +5384,24 @@ "updateTime": { "type": "string", "format": "date-time" - }, - "workflowStatus": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowStepStatus" - } } } }, "v1.DetailClusterResponse": { "required": [ - "provider", "apiServerURL", - "model", "name", - "description", + "labels", + "reason", "icon", + "status", + "provider", "dashboardURL", "kubeConfig", - "kubeConfigSecret", + "model", "alias", - "labels", - "status", - "reason", + "description", + "kubeConfigSecret", "resourceInfo" ], "properties": { @@ -5274,13 +5454,13 @@ }, "v1.DetailComponentResponse": { "required": [ - "updateTime", - "type", - "creator", - "alias", "createTime", + "type", + "updateTime", + "name", "appPrimaryKey", - "name" + "creator", + "alias" ], "properties": { "alias": { @@ -5375,15 +5555,19 @@ }, "v1.DetailDeliveryTargetResponse": { "required": [ - "createTime", - "updateTime", "name", - "namespace" + "updateTime", + "namespace", + "createTime" ], "properties": { "alias": { "type": "string" }, + "appNum": { + "type": "integer", + "format": "int64" + }, "cluster": { "$ref": "#/definitions/v1.ClusterTarget" }, @@ -5411,13 +5595,13 @@ }, "v1.DetailPolicyResponse": { "required": [ - "name", - "type", - "description", "creator", "properties", "createTime", - "updateTime" + "updateTime", + "name", + "type", + "description" ], "properties": { "createTime": { @@ -5447,17 +5631,17 @@ }, "v1.DetailRevisionResponse": { "required": [ + "note", + "updateTime", + "appPrimaryKey", + "version", "reason", - "deployUser", "triggerType", "workflowName", "envName", "createTime", - "version", - "note", - "updateTime", - "appPrimaryKey", - "status" + "status", + "deployUser" ], "properties": { "appPrimaryKey": { @@ -5548,14 +5732,13 @@ "v1.DetailWorkflowResponse": { "required": [ "name", + "enable", + "envName", "alias", "description", - "enable", "default", - "envName", "createTime", - "updateTime", - "workflowRecord" + "updateTime" ], "properties": { "alias": { @@ -5589,9 +5772,6 @@ "updateTime": { "type": "string", "format": "date-time" - }, - "workflowRecord": { - "$ref": "#/definitions/v1.WorkflowRecord" } } }, @@ -5653,12 +5833,16 @@ "name", "targetNames", "createTime", - "updateTime" + "updateTime", + "appDeployName" ], "properties": { "alias": { "type": "string" }, + "appDeployName": { + "type": "string" + }, "componentSelector": { "$ref": "#/definitions/v1.ComponentSelector" }, @@ -5666,6 +5850,12 @@ "type": "string", "format": "date-time" }, + "deliveryTargets": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.DeliveryTargetBase" + } + }, "description": { "type": "string" }, @@ -6071,6 +6261,37 @@ } } }, + "v1.UpdateApplicationComponentRequest": { + "properties": { + "alias": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/v1.UpdateApplicationComponentRequest.labels" + }, + "properties": { + "type": "string" + } + } + }, + "v1.UpdateApplicationComponentRequest.labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "v1.UpdateApplicationRequest": { "properties": { "alias": { @@ -6207,6 +6428,12 @@ "name": { "type": "string" }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + }, "updateTime": { "type": "string", "format": "date-time" @@ -6282,25 +6509,6 @@ } } }, - "v1.WorkflowStepStatus": { - "required": [ - "name", - "status", - "takeTime" - ], - "properties": { - "name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "takeTime": { - "type": "integer", - "format": "integer" - } - } - }, "v1alpha1.CanaryMetric": { "required": [ "name" diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index 2827682b5..a7cd4bf26 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -17,6 +17,7 @@ limitations under the License. package model import ( + "fmt" "strconv" "time" @@ -51,6 +52,7 @@ type WorkflowStep struct { OrderIndex int `json:"orderIndex"` Inputs common.StepInputs `json:"inputs,omitempty"` Outputs common.StepOutputs `json:"outputs,omitempty"` + DependsOn []string `json:"dependsOn"` Properties *JSONStruct `json:"properties,omitempty"` } @@ -61,7 +63,7 @@ func (w *Workflow) TableName() string { // PrimaryKey return custom primary key func (w *Workflow) PrimaryKey() string { - return w.Name + return fmt.Sprintf("%s-%s", w.AppPrimaryKey, w.Name) } // Index return custom primary key @@ -83,7 +85,7 @@ func (w *Workflow) Index() map[string]string { // WorkflowRecord is the workflow record database model type WorkflowRecord struct { Model - WorkflowPrimaryKey string `json:"workflowPrimaryKey"` + WorkflowName string `json:"workflowName"` AppPrimaryKey string `json:"appPrimaryKey"` RevisionPrimaryKey string `json:"revisionPrimaryKey"` Name string `json:"name"` @@ -113,8 +115,8 @@ func (w *WorkflowRecord) Index() map[string]string { if w.Namespace != "" { index["namespace"] = w.Namespace } - if w.WorkflowPrimaryKey != "" { - index["workflowPrimaryKey"] = w.WorkflowPrimaryKey + if w.WorkflowName != "" { + index["workflowPrimaryKey"] = w.WorkflowName } if w.AppPrimaryKey != "" { index["appPrimaryKey"] = w.AppPrimaryKey diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 7919a53d9..1b2cee373 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -39,6 +39,8 @@ var ( CtxKeyDeliveryTarget = "delivery-target" // CtxKeyApplicationEnvBinding request context key of env binding CtxKeyApplicationEnvBinding = "envbinding-policy" + // CtxKeyApplicationComponent request context key of component + CtxKeyApplicationComponent = "component" ) // AddonPhase defines the phase of an addon @@ -283,6 +285,14 @@ type ApplicationStatusResponse struct { Status *common.AppStatus `json:"status"` } +// ApplicationStatisticsResponse application statistics response body +type ApplicationStatisticsResponse struct { + EnvCount int64 `json:"envCount"` + DeliveryTargetCount int64 `json:"deliveryTargetCount"` + RevisonCount int64 `json:"revisonCount"` + WorkflowCount int64 `json:"workflowCount"` +} + // CreateApplicationRequest create application request body type CreateApplicationRequest struct { Name string `json:"name" validate:"checkname"` @@ -315,13 +325,15 @@ type EnvBinding struct { // EnvBindingBase application env binding type EnvBindingBase struct { - Name string `json:"name" validate:"checkname"` - Alias string `json:"alias" validate:"checkalias" optional:"true"` - Description string `json:"description,omitempty" optional:"true"` - TargetNames []string `json:"targetNames"` - ComponentSelector *ComponentSelector `json:"componentSelector" optional:"true"` - CreateTime time.Time `json:"createTime"` - UpdateTime time.Time `json:"updateTime"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + TargetNames []string `json:"targetNames"` + Targets []DeliveryTargetBase `json:"deliveryTargets,omitempty"` + ComponentSelector *ComponentSelector `json:"componentSelector" optional:"true"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + AppDeployName string `json:"appDeployName"` } // DetailEnvBindingResponse defines the response of env-binding details @@ -344,11 +356,10 @@ type ComponentSelector struct { // DetailApplicationResponse application detail type DetailApplicationResponse struct { ApplicationBase - Policies []string `json:"policies"` - EnvBindings []string `json:"envBindings"` - Status string `json:"status"` - ResourceInfo ApplicationResourceInfo `json:"resourceInfo"` - WorkflowStatus []WorkflowStepStatus `json:"workflowStatus"` + Policies []string `json:"policies"` + EnvBindings []string `json:"envBindings"` + Status string `json:"status"` + ResourceInfo ApplicationResourceInfo `json:"resourceInfo"` } // WorkflowStepStatus workflow step status model @@ -360,7 +371,7 @@ type WorkflowStepStatus struct { // ApplicationResourceInfo application-level resource consumption statistics type ApplicationResourceInfo struct { - ComponentNum int `json:"componentNum"` + ComponentNum int64 `json:"componentNum"` // Others, such as: Memory、CPU、GPU、Storage } @@ -398,6 +409,16 @@ type CreateComponentRequest struct { Traits []*CreateApplicationTraitRequest `json:"traits,omitempty" optional:"true"` } +// UpdateApplicationComponentRequest update component request body +type UpdateApplicationComponentRequest struct { + Alias *string `json:"alias" validate:"checkalias" optional:"true"` + Description *string `json:"description" optional:"true"` + Icon *string `json:"icon" optional:"true"` + Labels *map[string]string `json:"labels,omitempty"` + Properties *string `json:"properties,omitempty"` + DependsOn *[]string `json:"dependsOn" optional:"true"` +} + // DetailComponentResponse detail component response body type DetailComponentResponse struct { model.ApplicationComponent @@ -567,8 +588,6 @@ type WorkflowStep struct { // DetailWorkflowResponse detail workflow response type DetailWorkflowResponse struct { WorkflowBase - Steps []WorkflowStep `json:"steps,omitempty"` - LastRecord *WorkflowRecord `json:"workflowRecord"` } // ListWorkflowResponse list application workflows @@ -578,14 +597,15 @@ type ListWorkflowResponse struct { // WorkflowBase workflow base model type WorkflowBase struct { - Name string `json:"name"` - Alias string `json:"alias"` - Description string `json:"description"` - Enable bool `json:"enable"` - Default bool `json:"default"` - EnvName string `json:"envName"` - CreateTime time.Time `json:"createTime"` - UpdateTime time.Time `json:"updateTime"` + Name string `json:"name"` + Alias string `json:"alias"` + Description string `json:"description"` + Enable bool `json:"enable"` + Default bool `json:"default"` + EnvName string `json:"envName"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + Steps []WorkflowStep `json:"steps,omitempty"` } // ListWorkflowRecordsResponse list workflow execution record @@ -722,6 +742,7 @@ type DeliveryTargetBase struct { Variable map[string]interface{} `json:"variable,omitempty"` CreateTime time.Time `json:"createTime"` UpdateTime time.Time `json:"updateTime"` + AppNum int64 `json:"appNum,omitempty"` } // ApplicationRevisionBase application revision base spec diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index d55405839..b6644edfc 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -18,7 +18,6 @@ package usecase import ( "context" - "encoding/json" "errors" "fmt" "sort" @@ -28,6 +27,8 @@ import ( 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/selection" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" @@ -68,10 +69,12 @@ type ApplicationUsecase interface { UpdateApplication(context.Context, *model.Application, apisv1.UpdateApplicationRequest) (*apisv1.ApplicationBase, error) DeleteApplication(ctx context.Context, app *model.Application) error Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) + GetApplicationComponent(ctx context.Context, app *model.Application, componentName string) (*model.ApplicationComponent, error) ListComponents(ctx context.Context, app *model.Application, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentBase, error) AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) DetailComponent(ctx context.Context, app *model.Application, componentName string) (*apisv1.DetailComponentResponse, error) DeleteComponent(ctx context.Context, app *model.Application, componentName string) error + UpdateComponent(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.UpdateApplicationComponentRequest) (*apisv1.ComponentBase, error) ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) AddPolicy(ctx context.Context, app *model.Application, policy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) @@ -81,7 +84,8 @@ type ApplicationUsecase interface { DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string, req apisv1.UpdateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) ListRevisions(ctx context.Context, appName, envName, status string, page, pageSize int) (*apisv1.ListRevisionsResponse, error) - DetailRevision(ctx context.Context, appName, revisionVersion string) (*apisv1.DetailRevisionResponse, error) + DetailRevision(ctx context.Context, appName, revisionName string) (*apisv1.DetailRevisionResponse, error) + Statistics(ctx context.Context, app *model.Application) (*apisv1.ApplicationStatisticsResponse, error) } type applicationUsecaseImpl struct { @@ -163,7 +167,7 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod if err != nil { return nil, err } - components, err := c.ListComponents(ctx, app, apisv1.ListApplicationComponentOptions{}) + componentNum, err := c.ds.Count(ctx, &model.ApplicationComponent{AppPrimaryKey: app.PrimaryKey()}, &datastore.FilterOptions{}) if err != nil { return nil, err } @@ -184,9 +188,8 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod Policies: policyNames, EnvBindings: envBindingNames, ResourceInfo: apisv1.ApplicationResourceInfo{ - ComponentNum: len(components), + ComponentNum: componentNum, }, - WorkflowStatus: []apisv1.WorkflowStepStatus{}, } return detail, nil } @@ -194,7 +197,7 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod // GetApplicationStatus get application status from controller cluster func (c *applicationUsecaseImpl) GetApplicationStatus(ctx context.Context, appmodel *model.Application, envName string) (*common.AppStatus, error) { var app v1beta1.Application - err := c.kubeClient.Get(ctx, types.NamespacedName{Namespace: appmodel.Namespace, Name: converAppName(appmodel, envName)}, &app) + err := c.kubeClient.Get(ctx, types.NamespacedName{Namespace: appmodel.Namespace, Name: converAppName(appmodel.Name, envName)}, &app) if err != nil { if apierrors.IsNotFound(err) { return nil, nil @@ -204,6 +207,27 @@ func (c *applicationUsecaseImpl) GetApplicationStatus(ctx context.Context, appmo return &app.Status, nil } +// GetApplicationCR get application cr in cluster +func (c *applicationUsecaseImpl) GetApplicationCR(ctx context.Context, appmodel *model.Application) (*v1beta1.ApplicationList, error) { + var apps v1beta1.ApplicationList + selector := labels.NewSelector() + re, err := labels.NewRequirement(oam.AnnotationAppName, selection.Equals, []string{appmodel.Name}) + if err != nil { + return nil, err + } + selector = selector.Add(*re) + err = c.kubeClient.List(ctx, &apps, &client.ListOptions{ + LabelSelector: selector, + }) + if err != nil { + if apierrors.IsNotFound(err) { + return &apps, nil + } + return nil, err + } + return &apps, nil +} + // PublishApplicationTemplate publish app template func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) { //TODO: @@ -247,13 +271,6 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return nil, err } } - if oamApp.Spec.Workflow != nil && len(oamApp.Spec.Workflow.Steps) > 0 { - if err := c.saveApplicationWorkflow(ctx, &application, oamApp.Spec.Workflow.Steps, application.Name); err != nil { - log.Logger.Errorf("save applictaion polocies failure,%s", err.Error()) - return nil, err - } - } - // TODO Waiting for Spec.EnvBinding support } // build-in create env binding @@ -301,46 +318,12 @@ func (c *applicationUsecaseImpl) genPolicyByEnv(ctx context.Context, app *model. } properties, err := model.NewJSONStructByStruct(envBindingSpec) if err != nil { - return appPolicy, err + return appPolicy, bcode.ErrInvalidProperties } appPolicy.Properties = properties.RawExtension() return appPolicy, nil } -func (c *applicationUsecaseImpl) saveApplicationWorkflow(ctx context.Context, application *model.Application, workflowSteps []v1beta1.WorkflowStep, workflowName string) error { - var steps []apisv1.WorkflowStep - for _, step := range workflowSteps { - var propertyStr string - if step.Properties != nil { - properties, err := model.NewJSONStruct(step.Properties) - if err != nil { - log.Logger.Errorf("workflow %s step %s properties is invalid %s", application.Name, step.Name, err.Error()) - continue - } - propertyStr = properties.JSON() - } - steps = append(steps, apisv1.WorkflowStep{ - Name: step.Name, - Type: step.Type, - DependsOn: step.DependsOn, - Properties: propertyStr, - Inputs: step.Inputs, - Outputs: step.Outputs, - }) - } - _, err := c.workflowUsecase.CreateWorkflow(ctx, application, apisv1.CreateWorkflowRequest{ - AppName: application.PrimaryKey(), - Name: workflowName, - Description: "Created automatically.", - Steps: steps, - Default: true, - }) - if err != nil { - return err - } - return nil -} - func (c *applicationUsecaseImpl) saveApplicationEnvBinding(ctx context.Context, app model.Application, envBindings []*apisv1.EnvBinding) error { err := c.envBindingUsecase.BatchCreateEnvBinding(ctx, &app, envBindings) if err != nil { @@ -495,7 +478,6 @@ func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.Applicati func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app *model.Application, policys []v1beta1.AppPolicy) error { var policyModels []datastore.Entity - var envbindingPolicy *model.ApplicationPolicy for _, policy := range policys { properties, err := model.NewJSONStruct(policy.Properties) if err != nil { @@ -510,26 +492,6 @@ func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app } if policy.Type != string(EnvBindingPolicy) { policyModels = append(policyModels, appPolicy) - } else { - envbindingPolicy = appPolicy - } - } - // If multiple configurations are configured, enable only the last one. - if envbindingPolicy != nil { - envbindingPolicy.Name = EnvBindingPolicyDefaultName - policyModels = append(policyModels, envbindingPolicy) - var envBindingSpec v1alpha1.EnvBindingSpec - if err := json.Unmarshal([]byte(envbindingPolicy.Properties.JSON()), &envBindingSpec); err != nil { - return fmt.Errorf("unmarshal env binding policy failure %w", err) - } - for _, env := range envBindingSpec.Envs { - envBind := &model.EnvBinding{ - Name: env.Name, - Description: "", - } - if env.Selector != nil { - envBind.ComponentSelector = (*model.ComponentSelector)(env.Selector) - } } } return c.ds.BatchAdd(ctx, policyModels) @@ -578,26 +540,30 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat return nil, err } configByte, _ := yaml.Marshal(oamApp) + + workflow, err := c.workflowUsecase.GetWorkflow(ctx, app, oamApp.Annotations[oam.AnnotationWorkflowName]) + if err != nil { + return nil, err + } + // step2: check and create deploy event if !req.Force { var lastVersion = model.ApplicationRevision{ AppPrimaryKey: app.PrimaryKey(), + EnvName: workflow.EnvName, } - list, err := c.ds.List(ctx, &lastVersion, &datastore.ListOptions{PageSize: 1, Page: 1}) + list, err := c.ds.List(ctx, &lastVersion, &datastore.ListOptions{ + PageSize: 1, Page: 1, SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}}) if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { log.Logger.Errorf("query app latest revision failure %s", err.Error()) return nil, bcode.ErrDeployConflict } if len(list) > 0 && list[0].(*model.ApplicationRevision).Status != model.RevisionStatusComplete { + log.Logger.Warnf("last app revision can not complete %s/%s", list[0].(*model.ApplicationRevision).AppPrimaryKey, list[0].(*model.ApplicationRevision).Version) return nil, bcode.ErrDeployConflict } } - workflow, err := c.workflowUsecase.GetWorkflow(ctx, oamApp.Annotations[oam.AnnotationWorkflowName]) - if err != nil { - return nil, err - } - var appRevision = &model.ApplicationRevision{ AppPrimaryKey: app.PrimaryKey(), Version: version, @@ -615,7 +581,7 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat return nil, err } // step3: create workflow record - if err := c.workflowUsecase.CreateWorkflowRecord(ctx, oamApp); err != nil { + if err := c.workflowUsecase.CreateWorkflowRecord(ctx, app, oamApp, workflow); err != nil { return nil, err } // step4: check and create namespace @@ -657,19 +623,46 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat } func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appModel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) { + // Priority 1 uses the requested workflow as release . + // Priority 2 uses the default workflow as release . + var workflow *model.Workflow + var err error + if reqWorkflowName != "" { + workflow, err = c.workflowUsecase.GetWorkflow(ctx, appModel, reqWorkflowName) + if err != nil { + return nil, err + } + } else { + workflow, err = c.workflowUsecase.GetApplicationDefaultWorkflow(ctx, appModel) + if err != nil && !errors.Is(err, bcode.ErrWorkflowNoDefault) { + return nil, err + } + } + if workflow == nil || workflow.EnvName == "" { + return nil, bcode.ErrWorkflowNotExist + } + + labels := make(map[string]string) + for key, value := range appModel.Labels { + labels[key] = value + } + labels[oam.AnnotationAppName] = appModel.Name + var app = &v1beta1.Application{ TypeMeta: metav1.TypeMeta{ Kind: "Application", APIVersion: "core.oam.dev/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ - Name: appModel.Name, + Name: converAppName(appModel.Name, workflow.EnvName), Namespace: appModel.Namespace, - Labels: appModel.Labels, + Labels: labels, Annotations: map[string]string{ oam.AnnotationDeployVersion: version, // publish version is the identifier of workflow record oam.AnnotationPublishVersion: utils.GenerateVersion(reqWorkflowName), + oam.AnnotationAppName: appModel.Name, + oam.AnnotationAppAlias: appModel.Alias, }, }, } @@ -705,7 +698,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo traits = append(traits, aTrait) } bc := common.ApplicationComponent{ - Name: component.Name, + Name: converComponentName(component.Name, workflow.EnvName), Type: component.Type, ExternalRevision: component.ExternalRevision, DependsOn: component.DependsOn, @@ -732,48 +725,29 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo } app.Spec.Policies = append(app.Spec.Policies, apolicy) } - - // Priority 1 uses the requested workflow as release . - // Priority 2 uses the default workflow as release . - var workflow *model.Workflow - if reqWorkflowName != "" { - workflow, err = c.workflowUsecase.GetWorkflow(ctx, reqWorkflowName) + if workflow.EnvName != "" { + envPolicy, err := c.genPolicyByEnv(ctx, appModel, workflow.EnvName) if err != nil { return nil, err } - if workflow.EnvName != "" { - envPolicy, err := c.genPolicyByEnv(ctx, appModel, workflow.EnvName) - if err != nil { - return nil, err - } - app.Spec.Policies = append(app.Spec.Policies, envPolicy) - } - - } else { - workflow, err = c.workflowUsecase.GetApplicationDefaultWorkflow(ctx, appModel) - if err != nil && !errors.Is(err, bcode.ErrWorkflowNoDefault) { - return nil, err - } + app.Spec.Policies = append(app.Spec.Policies, envPolicy) } - - if workflow != nil { - app.Annotations[oam.AnnotationWorkflowName] = workflow.Name - var steps []v1beta1.WorkflowStep - for _, step := range workflow.Steps { - var wstep = v1beta1.WorkflowStep{ - Name: step.Name, - Type: step.Type, - Inputs: step.Inputs, - Outputs: step.Outputs, - } - if step.Properties != nil { - wstep.Properties = step.Properties.RawExtension() - } - steps = append(steps, wstep) + app.Annotations[oam.AnnotationWorkflowName] = workflow.Name + var steps []v1beta1.WorkflowStep + for _, step := range workflow.Steps { + var wstep = v1beta1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + Inputs: step.Inputs, + Outputs: step.Outputs, } - app.Spec.Workflow = &v1beta1.Workflow{ - Steps: steps, + if step.Properties != nil { + wstep.Properties = step.Properties.RawExtension() } + steps = append(steps, wstep) + } + app.Spec.Workflow = &v1beta1.Workflow{ + Steps: steps, } return app, nil @@ -796,7 +770,13 @@ func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *a // DeleteApplication delete application func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *model.Application) error { // TODO: check app can be deleted - + crs, err := c.GetApplicationCR(ctx, app) + if err != nil { + return err + } + if len(crs.Items) > 0 { + return bcode.ErrApplicationRefusedDelete + } // query all components to deleted components, err := c.ListComponents(ctx, app, apisv1.ListApplicationComponentOptions{}) if err != nil { @@ -809,7 +789,7 @@ func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *mod } // delete workflow - if err := c.workflowUsecase.DeleteWorkflow(ctx, app.Name); err != nil && !errors.Is(err, bcode.ErrWorkflowNotExist) { + if err := c.workflowUsecase.DeleteWorkflowByApp(ctx, app); err != nil && !errors.Is(err, bcode.ErrWorkflowNotExist) { log.Logger.Errorf("delete workflow %s failure %s", app.Name, err.Error()) } @@ -834,6 +814,50 @@ func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *mod return c.ds.Delete(ctx, app) } +func (c *applicationUsecaseImpl) GetApplicationComponent(ctx context.Context, app *model.Application, componentName string) (*model.ApplicationComponent, error) { + var component = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: componentName, + } + err := c.ds.Get(ctx, &component) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationComponetNotExist + } + return nil, err + } + return &component, nil +} + +func (c *applicationUsecaseImpl) UpdateComponent(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.UpdateApplicationComponentRequest) (*apisv1.ComponentBase, error) { + if req.Alias != nil { + component.Alias = *req.Alias + } + if req.Description != nil { + component.Description = *req.Description + } + if req.DependsOn != nil { + component.DependsOn = *req.DependsOn + } + if req.Icon != nil { + component.Icon = *req.Icon + } + if req.Labels != nil { + component.Labels = *req.Labels + } + if req.Properties != nil { + properties, err := model.NewJSONStructByString(*req.Properties) + if err != nil { + return nil, bcode.ErrInvalidProperties + } + component.Properties = properties + } + if err := c.ds.Put(ctx, component); err != nil { + return nil, err + } + return converComponentModelToBase(component), nil +} + func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) { componentModel := model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), @@ -859,6 +883,13 @@ func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Ap log.Logger.Warnf("add component for app %s failure %s", app.PrimaryKey(), err.Error()) return nil, err } + return converComponentModelToBase(&componentModel), nil +} + +func converComponentModelToBase(componentModel *model.ApplicationComponent) *apisv1.ComponentBase { + if componentModel == nil { + return nil + } return &apisv1.ComponentBase{ Name: componentModel.Name, Description: componentModel.Description, @@ -869,7 +900,7 @@ func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Ap Creator: componentModel.Creator, CreateTime: componentModel.CreateTime, UpdateTime: componentModel.UpdateTime, - }, nil + } } func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.Application, componentName string) error { @@ -1096,6 +1127,29 @@ func (c *applicationUsecaseImpl) DetailRevision(ctx context.Context, appName, re }, nil } +func (c *applicationUsecaseImpl) Statistics(ctx context.Context, app *model.Application) (*apisv1.ApplicationStatisticsResponse, error) { + var targetMap = make(map[string]int) + envbinding, err := c.envBindingUsecase.GetEnvBindings(ctx, app) + if err != nil { + log.Logger.Errorf("query app envbinding failure %s", err.Error()) + } + for _, env := range envbinding { + for _, target := range env.TargetNames { + targetMap[target]++ + } + } + count, err := c.ds.Count(ctx, &model.ApplicationRevision{AppPrimaryKey: app.PrimaryKey()}, &datastore.FilterOptions{}) + if err != nil { + return nil, err + } + return &apisv1.ApplicationStatisticsResponse{ + EnvCount: int64(len(envbinding)), + DeliveryTargetCount: int64(len(targetMap)), + RevisonCount: count, + WorkflowCount: c.workflowUsecase.CountWorkflow(ctx, app), + }, nil +} + func createTargetClusterEnv(envBind apisv1.EnvBindingBase, target *model.DeliveryTarget) v1alpha1.EnvConfig { placement := v1alpha1.EnvPlacement{} var componentSelector *v1alpha1.EnvSelector @@ -1115,18 +1169,18 @@ func createTargetClusterEnv(envBind apisv1.EnvBindingBase, target *model.Deliver } } -func converAppName(app *model.Application, envName string) string { - return fmt.Sprintf("%s-%s", app.Name, envName) +func converAppName(appModelName, envName string) string { + return fmt.Sprintf("%s-%s", appModelName, envName) +} + +func converComponentName(componentModelName, envName string) string { + return fmt.Sprintf("%s-%s", componentModelName, envName) } func genPolicyName(envName string) string { return fmt.Sprintf("%s-%s", EnvBindingPolicyDefaultName, envName) } -func genWorkflowName(app *model.Application, envName string) string { - return fmt.Sprintf("%s-%s", app.Name, envName) -} - func genPolicyEnvName(targetName string) string { return targetName } diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index f0e5b94b8..3d3d38d5e 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -28,7 +28,6 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" @@ -158,9 +157,8 @@ var _ = Describe("Test application usecase function", func() { }) It("Test ListApplications function", func() { - apps, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioOptions{}) + _, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioOptions{}) Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(apps), 3)).Should(BeEmpty()) }) It("Test DetailApplication function", func() { @@ -170,29 +168,8 @@ var _ = Describe("Test application usecase function", func() { detail, err := appUsecase.DetailApplication(context.TODO(), appModel) Expect(err).Should(BeNil()) - Expect(cmp.Diff(detail.ResourceInfo.ComponentNum, 2)).Should(BeEmpty()) - Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) - }) - - It("Test GetWorkflow function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - - _, err = workflowUsecase.GetWorkflow(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - }) - - It("Test ListPolicies function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - - policies, err := appUsecase.ListPolicies(context.TODO(), appModel) - Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(policies), 1)).Should(BeEmpty()) - Expect(cmp.Diff(policies[0].Type, "env-binding")).Should(BeEmpty()) - Expect((*policies[0].Properties)["envs"]).ShouldNot(BeEmpty()) + Expect(cmp.Diff(detail.ResourceInfo.ComponentNum, int64(2))).Should(BeEmpty()) + Expect(cmp.Diff(len(detail.Policies), 0)).Should(BeEmpty()) }) It("Test ListComponents function", func() { @@ -233,17 +210,6 @@ var _ = Describe("Test application usecase function", func() { Expect(cmp.Diff(strings.Contains((*detail.Properties)["image"].(string), "crccheck/hello-world"), true)).Should(BeEmpty()) }) - It("Test DetailPolicy function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - - detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, EnvBindingPolicyDefaultName) - Expect(err).Should(BeNil()) - Expect(cmp.Diff(detail.Type, "env-binding")).Should(BeEmpty()) - Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) - }) - It("Test AddComponent function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) @@ -279,23 +245,34 @@ var _ = Describe("Test application usecase function", func() { Name: EnvBindingPolicyDefaultName, Description: "this is a test2 policy", Type: "env-binding", - Properties: ``, - }) - Expect(cmp.Equal(err, bcode.ErrApplicationPolicyExist, cmpopts.EquateErrors())).Should(BeTrue()) - _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ - Name: "env-binding-2", - Description: "this is a test2 policy", - Type: "env-binding", Properties: `{"envs":{ "name": "test", "placement":{"namespaceSelector":{ "name": "TEST_NAMESPACE"}}, "selector":{ "components": ["data-worker"]}}}`, }) Expect(err).Should(BeNil()) + + _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ + Name: EnvBindingPolicyDefaultName, + Description: "this is a test2 policy", + Type: "env-binding", + Properties: ``, + }) + Expect(cmp.Equal(err, bcode.ErrApplicationPolicyExist, cmpopts.EquateErrors())).Should(BeTrue()) + }) + + It("Test ListPolicies function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + policies, err := appUsecase.ListPolicies(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(policies), 1)).Should(BeEmpty()) }) It("Test DetailPolicy function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, "env-binding-2") + detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, EnvBindingPolicyDefaultName) Expect(err).Should(BeNil()) Expect(detail.Properties).ShouldNot(BeNil()) Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) @@ -305,7 +282,7 @@ var _ = Describe("Test application usecase function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - base, err := appUsecase.UpdatePolicy(context.TODO(), appModel, "env-binding-2", v1.UpdatePolicyRequest{ + base, err := appUsecase.UpdatePolicy(context.TODO(), appModel, EnvBindingPolicyDefaultName, v1.UpdatePolicyRequest{ Type: "env-binding", Properties: `{"envs":{}}`, }) @@ -317,7 +294,7 @@ var _ = Describe("Test application usecase function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - err = appUsecase.DeletePolicy(context.TODO(), appModel, "env-binding-2") + err = appUsecase.DeletePolicy(context.TODO(), appModel, EnvBindingPolicyDefaultName) Expect(err).Should(BeNil()) }) @@ -402,24 +379,6 @@ var _ = Describe("Test application usecase function", func() { Expect(err).Should(BeNil()) }) - It("Test Deploy Application function", func() { - appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") - Expect(err).Should(BeNil()) - Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) - res, err := appUsecase.Deploy(context.TODO(), appModel, v1.ApplicationDeployRequest{ - Note: "unit test deploy", - TriggerType: "api", - }) - Expect(err).Should(BeNil()) - Expect(cmp.Diff(res.Status, model.RevisionStatusRunning)).Should(BeEmpty()) - - var oam v1beta1.Application - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: appModel.Name, Namespace: appModel.Namespace}, &oam) - Expect(err).Should(BeNil()) - Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty()) - Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty()) - }) - It("Test DeleteApplication function", func() { appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") Expect(err).Should(BeNil()) @@ -478,15 +437,16 @@ var _ = Describe("Test application usecase function", func() { }) }) -func createTestSuspendApp(ctx context.Context, appName, revisionVersion, wfName, recordName string, kubeClient client.Client) (*v1beta1.Application, error) { +func createTestSuspendApp(ctx context.Context, appName, envName, revisionVersion, wfName, recordName string, kubeClient client.Client) (*v1beta1.Application, error) { testapp := &v1beta1.Application{ ObjectMeta: metav1.ObjectMeta{ - Name: appName, + Name: converAppName(appName, envName), Namespace: "default", Annotations: map[string]string{ oam.AnnotationDeployVersion: revisionVersion, oam.AnnotationWorkflowName: wfName, oam.AnnotationPublishVersion: recordName, + oam.AnnotationAppName: appName, }, }, Spec: v1beta1.ApplicationSpec{ diff --git a/pkg/apiserver/rest/usecase/delivery_target.go b/pkg/apiserver/rest/usecase/delivery_target.go index a5363dc3c..cbb25dd59 100644 --- a/pkg/apiserver/rest/usecase/delivery_target.go +++ b/pkg/apiserver/rest/usecase/delivery_target.go @@ -154,6 +154,8 @@ func convertCreateReqToDeliveryTargetModel(req apisv1.CreateDeliveryTargetReques } func convertFromDeliveryTargetModel(deliveryTarget *model.DeliveryTarget) *apisv1.DeliveryTargetBase { + var appNum int64 = 0 + // TODO: query app num in target return &apisv1.DeliveryTargetBase{ Name: deliveryTarget.Name, Namespace: deliveryTarget.Namespace, @@ -163,5 +165,6 @@ func convertFromDeliveryTargetModel(deliveryTarget *model.DeliveryTarget) *apisv Variable: deliveryTarget.Variable, CreateTime: deliveryTarget.CreateTime, UpdateTime: deliveryTarget.UpdateTime, + AppNum: appNum, } } diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go index 84000bd48..f046212c0 100644 --- a/pkg/apiserver/rest/usecase/envbinding.go +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -19,16 +19,21 @@ package usecase import ( "context" "errors" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" "github.com/oam-dev/kubevela/pkg/oam/util" - - "github.com/oam-dev/kubevela/pkg/apiserver/datastore" - "github.com/oam-dev/kubevela/pkg/apiserver/model" - apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" ) // EnvBindingUsecase envbinding usecase @@ -41,19 +46,26 @@ type EnvBindingUsecase interface { UpdateEnvBinding(ctx context.Context, app *model.Application, envName string, diff apisv1.PutApplicationEnvRequest) (*apisv1.DetailEnvBindingResponse, error) DeleteEnvBinding(ctx context.Context, app *model.Application, envName string) error BatchDeleteEnvBinding(ctx context.Context, app *model.Application) error - DetailEnvBinding(ctx context.Context, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) + DetailEnvBinding(ctx context.Context, app *model.Application, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) + ApplicationEnvRecycle(ctx context.Context, appModel *model.Application, envBinding *model.EnvBinding) error } type envBindingUsecaseImpl struct { ds datastore.DataStore workflowUsecase WorkflowUsecase + kubeClient client.Client } // NewEnvBindingUsecase new envBinding usecase func NewEnvBindingUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase) EnvBindingUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } return &envBindingUsecaseImpl{ ds: ds, workflowUsecase: workflowUsecase, + kubeClient: kubecli, } } @@ -65,10 +77,17 @@ func (e *envBindingUsecaseImpl) GetEnvBindings(ctx context.Context, app *model.A if err != nil { return nil, bcode.ErrEnvBindingsNotExist } + deliveryTarget := model.DeliveryTarget{ + Namespace: app.Namespace, + } + deliveryTargets, err := e.ds.List(ctx, &deliveryTarget, &datastore.ListOptions{}) + if err != nil { + return nil, err + } var list []*apisv1.EnvBindingBase for _, ebd := range envBindings { eb := ebd.(*model.EnvBinding) - list = append(list, convertEnvbindingModelToBase(eb)) + list = append(list, convertEnvbindingModelToBase(app, eb, deliveryTargets)) } return list, nil } @@ -81,7 +100,7 @@ func (e *envBindingUsecaseImpl) GetEnvBinding(ctx context.Context, app *model.Ap } return nil, err } - return e.DetailEnvBinding(ctx, envBinding) + return e.DetailEnvBinding(ctx, app, envBinding) } func (e *envBindingUsecaseImpl) CheckAppEnvBindingsContainTarget(ctx context.Context, app *model.Application, targetName string) (bool, error) { @@ -165,18 +184,23 @@ func (e *envBindingUsecaseImpl) UpdateEnvBinding(ctx context.Context, app *model if err := e.ds.Put(ctx, envBindingModel); err != nil { return nil, err } - return e.DetailEnvBinding(ctx, envBindingModel) + return e.DetailEnvBinding(ctx, app, envBindingModel) } -func (e *envBindingUsecaseImpl) DeleteEnvBinding(ctx context.Context, app *model.Application, envName string) error { - envBinding, err := e.getBindingByEnv(ctx, app, envName) +func (e *envBindingUsecaseImpl) DeleteEnvBinding(ctx context.Context, appModel *model.Application, envName string) error { + envBinding, err := e.getBindingByEnv(ctx, appModel, envName) if err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { return bcode.ErrEnvBindingNotExist } return err } - if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: app.PrimaryKey(), Name: envBinding.Name}); err != nil { + var app v1beta1.Application + err = e.kubeClient.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: converAppName(appModel.Name, envBinding.Name)}, &app) + if err != nil && !apierrors.IsNotFound(err) { + return bcode.ErrApplicationRefusedDelete + } + if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: appModel.PrimaryKey(), Name: envBinding.Name}); err != nil { return err } return nil @@ -230,7 +254,8 @@ func (e *envBindingUsecaseImpl) createEnvWorkflow(ctx context.Context, app *mode } _, err := e.workflowUsecase.CreateWorkflow(ctx, app, apisv1.CreateWorkflowRequest{ AppName: app.PrimaryKey(), - Name: genWorkflowName(app, env.Name), + Name: env.Name, + Alias: fmt.Sprintf("%s env workflow", env.Alias), Description: "Created automatically by envbinding.", EnvName: env.Name, Steps: steps, @@ -242,12 +267,31 @@ func (e *envBindingUsecaseImpl) createEnvWorkflow(ctx context.Context, app *mode return nil } -func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) { +func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, app *model.Application, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) { + deliveryTarget := model.DeliveryTarget{ + Namespace: app.Namespace, + } + deliveryTargets, err := e.ds.List(ctx, &deliveryTarget, &datastore.ListOptions{}) + if err != nil { + return nil, err + } return &apisv1.DetailEnvBindingResponse{ - EnvBindingBase: *convertEnvbindingModelToBase(envBinding), + EnvBindingBase: *convertEnvbindingModelToBase(app, envBinding, deliveryTargets), }, nil } +func (e *envBindingUsecaseImpl) ApplicationEnvRecycle(ctx context.Context, appModel *model.Application, envBinding *model.EnvBinding) error { + var app v1beta1.Application + err := e.kubeClient.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: converAppName(appModel.Name, envBinding.Name)}, &app) + if err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + return e.kubeClient.Delete(ctx, &app) +} + func convertCreateReqToEnvBindingModel(app *model.Application, req apisv1.CreateApplicationEnvRequest) model.EnvBinding { envBinding := model.EnvBinding{ AppPrimaryKey: app.Name, @@ -259,15 +303,29 @@ func convertCreateReqToEnvBindingModel(app *model.Application, req apisv1.Create return envBinding } -func convertEnvbindingModelToBase(envBinding *model.EnvBinding) *apisv1.EnvBindingBase { +func convertEnvbindingModelToBase(app *model.Application, envBinding *model.EnvBinding, deliveryTargets []datastore.Entity) *apisv1.EnvBindingBase { + var dtMap = make(map[string]*model.DeliveryTarget, len(deliveryTargets)) + for _, dte := range deliveryTargets { + dt := dte.(*model.DeliveryTarget) + dtMap[dt.Name] = dt + } + var targets []apisv1.DeliveryTargetBase + for _, targetName := range envBinding.TargetNames { + dt := dtMap[targetName] + if dt != nil { + targets = append(targets, *convertFromDeliveryTargetModel(dt)) + } + } ebb := &apisv1.EnvBindingBase{ Name: envBinding.Name, Alias: envBinding.Alias, Description: envBinding.Description, TargetNames: envBinding.TargetNames, + Targets: targets, ComponentSelector: (*apisv1.ComponentSelector)(envBinding.ComponentSelector), CreateTime: envBinding.CreateTime, UpdateTime: envBinding.UpdateTime, + AppDeployName: converAppName(app.Name, envBinding.Name), } return ebb } diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index 05ce5bba3..650b814e0 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -99,20 +99,6 @@ label: 持久化存储 sort: 11 subParameters: - - description: "" - jsonKey: mountPath - label: MountPath - sort: 100 - uiType: Input - validate: - required: true - - description: "" - jsonKey: name - label: Name - sort: 100 - uiType: Input - validate: - required: true - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' jsonKey: type label: Type @@ -129,6 +115,20 @@ - label: EmptyDir value: emptyDir required: true + - description: "" + jsonKey: mountPath + label: MountPath + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true uiType: Structs validate: {} - description: Instructions for assessing whether the container is in a suitable state @@ -137,6 +137,24 @@ label: ReadinessProbe检测 sort: 13 subParameters: + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} - description: Number of seconds after which the probe times out. jsonKey: timeoutSeconds label: TimeoutSeconds @@ -182,6 +200,14 @@ label: HttpGet sort: 100 subParameters: + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true - description: "" jsonKey: httpHeaders label: HttpHeaders @@ -211,14 +237,6 @@ uiType: Input validate: required: true - - description: The TCP socket within the container to which the HTTP GET request - should be directed. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true uiType: KV validate: {} - description: Number of seconds after the container is started before the first @@ -247,24 +265,6 @@ validate: defaultValue: 1 required: true - - description: Instructions for assessing container health by probing a TCP socket. - Either this attribute or the exec attribute or the httpGet attribute MUST be - specified. This attribute is mutually exclusive with both the exec attribute - and the httpGet attribute. - jsonKey: tcpSocket - label: TcpSocket - sort: 100 - subParameters: - - description: The TCP socket within the container that should be probed to assess - container health. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - uiType: KV - validate: {} uiType: Group validate: {} - description: Instructions for assessing whether the container is alive. @@ -272,6 +272,52 @@ label: LivenessProbe检测 sort: 15 subParameters: + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} + - description: Number of seconds after which the probe times out. + jsonKey: timeoutSeconds + label: TimeoutSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: Instructions for assessing container health by executing a command. + Either this attribute or the httpGet attribute or the tcpSocket attribute MUST + be specified. This attribute is mutually exclusive with both the httpGet attribute + and the tcpSocket attribute. + jsonKey: exec + label: Exec + sort: 100 + subParameters: + - description: A command to be executed inside the container to assess its health. + Each space delimited token of the command is a separate array element. Commands + exiting 0 are considered to be successful probes, whilst all other exit codes + are considered failures. + jsonKey: command + label: Command + sort: 100 + uiType: Strings + validate: + required: true + uiType: KV + validate: {} - description: Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe). jsonKey: failureThreshold @@ -354,52 +400,6 @@ validate: defaultValue: 1 required: true - - description: Instructions for assessing container health by probing a TCP socket. - Either this attribute or the exec attribute or the httpGet attribute MUST be - specified. This attribute is mutually exclusive with both the exec attribute - and the httpGet attribute. - jsonKey: tcpSocket - label: TcpSocket - sort: 100 - subParameters: - - description: The TCP socket within the container that should be probed to assess - container health. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - uiType: KV - validate: {} - - description: Number of seconds after which the probe times out. - jsonKey: timeoutSeconds - label: TimeoutSeconds - sort: 100 - uiType: Number - validate: - defaultValue: 1 - required: true - - description: Instructions for assessing container health by executing a command. - Either this attribute or the httpGet attribute or the tcpSocket attribute MUST - be specified. This attribute is mutually exclusive with both the httpGet attribute - and the tcpSocket attribute. - jsonKey: exec - label: Exec - sort: 100 - subParameters: - - description: A command to be executed inside the container to assess its health. - Each space delimited token of the command is a separate array element. Commands - exiting 0 are considered to be successful probes, whilst all other exit codes - are considered failures. - jsonKey: command - label: Command - sort: 100 - uiType: Strings - validate: - required: true - uiType: KV - validate: {} uiType: Group validate: {} - description: Specify image pull policy for your service @@ -416,6 +416,12 @@ value: Always - label: 永不更新 value: Never +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + sort: 100 + uiType: Strings + validate: {} - description: If addRevisionLabel is true, the appRevision label will be added to the underlying pods jsonKey: addRevisionLabel @@ -425,9 +431,3 @@ validate: defaultValue: false required: true -- description: Specify image pull secrets for your service - jsonKey: imagePullSecrets - label: ImagePullSecrets - sort: 100 - uiType: Strings - validate: {} diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 6f0682a26..1a6b189a9 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -48,20 +48,22 @@ const ( // WorkflowUsecase workflow manage api type WorkflowUsecase interface { - ListApplicationWorkflow(ctx context.Context, app *model.Application, enable *bool) ([]*apisv1.WorkflowBase, error) - GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) + ListApplicationWorkflow(ctx context.Context, app *model.Application) ([]*apisv1.WorkflowBase, error) + GetWorkflow(ctx context.Context, app *model.Application, workflowName string) (*model.Workflow, error) DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) - DeleteWorkflow(ctx context.Context, workflowName string) error + DeleteWorkflow(ctx context.Context, app *model.Application, workflowName string) error + DeleteWorkflowByApp(ctx context.Context, app *model.Application) error CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) - CreateWorkflowRecord(ctx context.Context, app *v1beta1.Application) error UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) - ListWorkflowRecords(ctx context.Context, workflowName string, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) - DetailWorkflowRecord(ctx context.Context, workflowName, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) + CreateWorkflowRecord(ctx context.Context, appModel *model.Application, app *v1beta1.Application, workflow *model.Workflow) error + ListWorkflowRecords(ctx context.Context, workflow *model.Workflow, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) + DetailWorkflowRecord(ctx context.Context, workflow *model.Workflow, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) SyncWorkflowRecord(ctx context.Context) error - ResumeRecord(ctx context.Context, appModel *model.Application, recordName string) error - TerminateRecord(ctx context.Context, appModel *model.Application, recordName string) error - RollbackRecord(ctx context.Context, appModel *model.Application, recordName, revisionName string) error + ResumeRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName string) error + TerminateRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName string) error + RollbackRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName, revisionName string) error + CountWorkflow(ctx context.Context, app *model.Application) int64 } // NewWorkflowUsecase new workflow usecase @@ -84,9 +86,10 @@ type workflowUsecaseImpl struct { } // DeleteWorkflow delete application workflow -func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, workflowName string) error { +func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, app *model.Application, workflowName string) error { var workflow = &model.Workflow{ - Name: workflowName, + Name: workflowName, + AppPrimaryKey: app.PrimaryKey(), } if err := w.ds.Delete(ctx, workflow); err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { @@ -97,7 +100,30 @@ func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, workflowName s return nil } +func (w *workflowUsecaseImpl) DeleteWorkflowByApp(ctx context.Context, app *model.Application) error { + var workflow = &model.Workflow{ + AppPrimaryKey: app.PrimaryKey(), + } + + workflows, err := w.ds.List(ctx, workflow, &datastore.ListOptions{}) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil + } + return err + } + for i := range workflows { + if err := w.ds.Delete(ctx, workflows[i]); err != nil { + log.Logger.Errorf("delete workflow %s failure %s", workflows[i].PrimaryKey(), err.Error()) + } + } + return nil +} + func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { + if req.EnvName == "" { + return nil, bcode.ErrWorkflowNoEnv + } var steps []model.WorkflowStep for _, step := range req.Steps { properties, err := model.NewJSONStructByString(step.Properties) @@ -106,11 +132,13 @@ func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.App return nil, bcode.ErrInvalidProperties } steps = append(steps, model.WorkflowStep{ - Name: step.Name, - Type: step.Type, - Inputs: step.Inputs, - Outputs: step.Outputs, - Properties: properties, + Name: step.Name, + Type: step.Type, + Inputs: step.Inputs, + Outputs: step.Outputs, + Description: step.Description, + DependsOn: step.DependsOn, + Properties: properties, }) } // It is allowed to set multiple workflows as default, and only one takes effect. @@ -155,39 +183,46 @@ func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *mode return w.DetailWorkflow(ctx, workflow) } -// DetailWorkflow detail workflow -func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) { +func converWorkflowBase(workflow *model.Workflow) apisv1.WorkflowBase { var steps []apisv1.WorkflowStep for _, step := range workflow.Steps { apiStep := apisv1.WorkflowStep{ - Name: step.Name, - Type: step.Type, - Inputs: step.Inputs, - Outputs: step.Outputs, - Properties: step.Properties.JSON(), + Name: step.Name, + Type: step.Type, + Description: step.Description, + Inputs: step.Inputs, + Outputs: step.Outputs, + Properties: step.Properties.JSON(), + DependsOn: step.DependsOn, } if step.Properties != nil { apiStep.Properties = step.Properties.JSON() } steps = append(steps, apiStep) } + return apisv1.WorkflowBase{ + Name: workflow.Name, + Description: workflow.Description, + Default: workflow.Default, + EnvName: workflow.EnvName, + CreateTime: workflow.CreateTime, + UpdateTime: workflow.UpdateTime, + Steps: steps, + } +} + +// DetailWorkflow detail workflow +func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) { return &apisv1.DetailWorkflowResponse{ - WorkflowBase: apisv1.WorkflowBase{ - Name: workflow.Name, - Description: workflow.Description, - Default: workflow.Default, - EnvName: workflow.EnvName, - CreateTime: workflow.CreateTime, - UpdateTime: workflow.UpdateTime, - }, - Steps: steps, + WorkflowBase: converWorkflowBase(workflow), }, nil } // GetWorkflow get workflow model -func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName string) (*model.Workflow, error) { +func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, app *model.Application, workflowName string) (*model.Workflow, error) { var workflow = model.Workflow{ - Name: workflowName, + Name: workflowName, + AppPrimaryKey: app.PrimaryKey(), } if err := w.ds.Get(ctx, &workflow); err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { @@ -199,7 +234,7 @@ func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, workflowName stri } // ListApplicationWorkflow list application workflows -func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app *model.Application, enable *bool) ([]*apisv1.WorkflowBase, error) { +func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app *model.Application) ([]*apisv1.WorkflowBase, error) { var workflow = model.Workflow{ AppPrimaryKey: app.PrimaryKey(), } @@ -210,14 +245,8 @@ func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app * var list []*apisv1.WorkflowBase for _, workflow := range workflows { wm := workflow.(*model.Workflow) - list = append(list, &apisv1.WorkflowBase{ - Name: wm.Name, - Description: wm.Description, - Default: wm.Default, - EnvName: wm.EnvName, - CreateTime: wm.CreateTime, - UpdateTime: wm.UpdateTime, - }) + base := converWorkflowBase(wm) + list = append(list, &base) } return list, nil } @@ -239,9 +268,10 @@ func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, } // ListWorkflowRecords list workflow record -func (w *workflowUsecaseImpl) ListWorkflowRecords(ctx context.Context, workflowName string, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) { +func (w *workflowUsecaseImpl) ListWorkflowRecords(ctx context.Context, workflow *model.Workflow, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) { var record = model.WorkflowRecord{ - WorkflowPrimaryKey: workflowName, + AppPrimaryKey: workflow.AppPrimaryKey, + WorkflowName: workflow.Name, } records, err := w.ds.List(ctx, &record, &datastore.ListOptions{Page: page, PageSize: pageSize}) if err != nil { @@ -267,13 +297,17 @@ func (w *workflowUsecaseImpl) ListWorkflowRecords(ctx context.Context, workflowN } // DetailWorkflowRecord get workflow record detail with name -func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflowName, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) { +func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflow *model.Workflow, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) { var record = model.WorkflowRecord{ - WorkflowPrimaryKey: workflowName, - Name: recordName, + AppPrimaryKey: workflow.AppPrimaryKey, + WorkflowName: workflow.Name, + Name: recordName, } err := w.ds.Get(ctx, &record) if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrWorkflowRecordNotExist + } return nil, err } @@ -283,6 +317,9 @@ func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflow } err = w.ds.Get(ctx, &revision) if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationRevisionNotExist + } return nil, err } @@ -307,23 +344,27 @@ func (w *workflowUsecaseImpl) SyncWorkflowRecord(ctx context.Context) error { for _, item := range records { app := &v1beta1.Application{} - index := item.Index() - appPrimaryKey := index["appPrimaryKey"] - namespace := index["namespace"] - recordName := index["name"] - + record := item.(*model.WorkflowRecord) + workflow := &model.Workflow{ + Name: record.WorkflowName, + AppPrimaryKey: record.AppPrimaryKey, + } + if err := w.ds.Get(ctx, workflow); err != nil { + log.Logger.Errorf("get workflow %s/%s failure %s", record.AppPrimaryKey, record.WorkflowName, err.Error()) + continue + } if err := w.kubeClient.Get(ctx, types.NamespacedName{ - Name: appPrimaryKey, - Namespace: namespace, + Name: converAppName(record.AppPrimaryKey, workflow.EnvName), + Namespace: record.Namespace, }, app); err != nil { - klog.ErrorS(err, "failed to get app", "app name", appPrimaryKey) + klog.ErrorS(err, "failed to get app", "app name", record.AppPrimaryKey) return err } // try to sync the status from the running application - if app.Annotations != nil && app.Annotations[oam.AnnotationPublishVersion] == recordName { - if err := w.syncWorkflowStatus(ctx, app, recordName); err != nil { - klog.ErrorS(err, "failed to sync workflow status", "app name", appPrimaryKey, "workflow record name", recordName) + if app.Annotations != nil && app.Annotations[oam.AnnotationPublishVersion] == record.Name { + if err := w.syncWorkflowStatus(ctx, app, record.Name); err != nil { + klog.ErrorS(err, "failed to sync workflow status", "app name", record.AppPrimaryKey, "workflow record name", record.Name) } continue } @@ -331,19 +372,19 @@ func (w *workflowUsecaseImpl) SyncWorkflowRecord(ctx context.Context) error { // try to sync the status from the controller revision cr := &appsv1.ControllerRevision{} if err := w.kubeClient.Get(ctx, types.NamespacedName{ - Name: fmt.Sprintf("record-%s-%s", appPrimaryKey, recordName), - Namespace: namespace, + Name: fmt.Sprintf("record-%s-%s", record.AppPrimaryKey, record.Name), + Namespace: record.Namespace, }, cr); err != nil { - klog.ErrorS(err, "failed to get controller revision", "app name", appPrimaryKey, "workflow record name", recordName) + klog.ErrorS(err, "failed to get controller revision", "app name", record.AppPrimaryKey, "workflow record name", record.Name) continue } appInRevision, err := util.RawExtension2Application(cr.Data) if err != nil { - klog.ErrorS(err, "failed to get app data in controller revision", "controller revision name", cr.Name, "app name", appPrimaryKey, "workflow record name", recordName) + klog.ErrorS(err, "failed to get app data in controller revision", "controller revision name", cr.Name, "app name", record.AppPrimaryKey, "workflow record name", record.Name) continue } - if err := w.syncWorkflowStatus(ctx, appInRevision, recordName); err != nil { - klog.ErrorS(err, "failed to sync workflow status", "app name", appPrimaryKey, "workflow record version", recordName) + if err := w.syncWorkflowStatus(ctx, appInRevision, record.Name); err != nil { + klog.ErrorS(err, "failed to sync workflow status", "app name", record.AppPrimaryKey, "workflow record version", record.Name) continue } @@ -353,18 +394,26 @@ func (w *workflowUsecaseImpl) SyncWorkflowRecord(ctx context.Context) error { } func (w *workflowUsecaseImpl) syncWorkflowStatus(ctx context.Context, app *v1beta1.Application, recordName string) error { + var record = &model.WorkflowRecord{ - AppPrimaryKey: app.Name, + AppPrimaryKey: app.Annotations[oam.AnnotationAppName], Name: recordName, } if err := w.ds.Get(ctx, record); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrWorkflowRecordNotExist + } return err } var revision = &model.ApplicationRevision{ - AppPrimaryKey: app.Name, + AppPrimaryKey: app.Annotations[oam.AnnotationAppName], Version: record.RevisionPrimaryKey, } + if err := w.ds.Get(ctx, revision); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrApplicationRevisionNotExist + } return err } @@ -395,13 +444,10 @@ func (w *workflowUsecaseImpl) syncWorkflowStatus(ctx context.Context, app *v1bet return nil } -func (w *workflowUsecaseImpl) CreateWorkflowRecord(ctx context.Context, app *v1beta1.Application) error { +func (w *workflowUsecaseImpl) CreateWorkflowRecord(ctx context.Context, appModel *model.Application, app *v1beta1.Application, workflow *model.Workflow) error { if app.Annotations == nil { return fmt.Errorf("empty annotations in application") } - if app.Annotations[oam.AnnotationWorkflowName] == "" { - return fmt.Errorf("failed to get workflow name from application") - } if app.Annotations[oam.AnnotationPublishVersion] == "" { return fmt.Errorf("failed to get record version from application") } @@ -410,19 +456,26 @@ func (w *workflowUsecaseImpl) CreateWorkflowRecord(ctx context.Context, app *v1b } return w.ds.Add(ctx, &model.WorkflowRecord{ - WorkflowPrimaryKey: app.Annotations[oam.AnnotationWorkflowName], - AppPrimaryKey: app.Name, + WorkflowName: workflow.Name, + AppPrimaryKey: appModel.PrimaryKey(), RevisionPrimaryKey: app.Annotations[oam.AnnotationDeployVersion], Name: app.Annotations[oam.AnnotationPublishVersion], - Namespace: app.Namespace, + Namespace: appModel.Namespace, Finished: "false", StartTime: time.Now().Time, Status: model.RevisionStatusInit, }) } +func (w *workflowUsecaseImpl) CountWorkflow(ctx context.Context, app *model.Application) int64 { + count, err := w.ds.Count(ctx, &model.Workflow{AppPrimaryKey: app.PrimaryKey()}, &datastore.FilterOptions{}) + if err != nil { + log.Logger.Errorf("count app %s workflow failure %s", app.PrimaryKey(), err.Error()) + } + return count +} -func (w *workflowUsecaseImpl) ResumeRecord(ctx context.Context, appModel *model.Application, recordName string) error { - oamApp, err := w.checkRecordRunning(ctx, appModel) +func (w *workflowUsecaseImpl) ResumeRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName string) error { + oamApp, err := w.checkRecordRunning(ctx, appModel, workflow.EnvName) if err != nil { return err } @@ -438,8 +491,8 @@ func (w *workflowUsecaseImpl) ResumeRecord(ctx context.Context, appModel *model. return nil } -func (w *workflowUsecaseImpl) TerminateRecord(ctx context.Context, appModel *model.Application, recordName string) error { - oamApp, err := w.checkRecordRunning(ctx, appModel) +func (w *workflowUsecaseImpl) TerminateRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName string) error { + oamApp, err := w.checkRecordRunning(ctx, appModel, workflow.EnvName) if err != nil { return err } @@ -455,7 +508,7 @@ func (w *workflowUsecaseImpl) TerminateRecord(ctx context.Context, appModel *mod return nil } -func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *model.Application, recordName, revisionVersion string) error { +func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName, revisionVersion string) error { if revisionVersion == "" { // find the latest complete revision version var revision = model.ApplicationRevision{ @@ -472,23 +525,24 @@ func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *mode return err } if len(revisions) == 0 { - fmt.Errorf("there is no complete revision, please specify a revision version") + return bcode.ErrApplicationNoReadyRevision } revisionVersion = revisions[0].Index()["version"] } - oamApp, err := w.checkRecordRunning(ctx, appModel) - if err != nil { - return err - } - var record = &model.WorkflowRecord{ - AppPrimaryKey: appModel.Name, + AppPrimaryKey: appModel.PrimaryKey(), Name: recordName, } if err := w.ds.Get(ctx, record); err != nil { return err } + + oamApp, err := w.checkRecordRunning(ctx, appModel, workflow.EnvName) + if err != nil { + return err + } + var rollbackRevision = model.ApplicationRevision{ AppPrimaryKey: appModel.Name, Version: revisionVersion, @@ -507,12 +561,11 @@ func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *mode if oamApp.Annotations == nil { oamApp.Annotations = make(map[string]string) } - newRecordName := utils.GenerateVersion(record.WorkflowPrimaryKey) + newRecordName := utils.GenerateVersion(record.WorkflowName) oamApp.Annotations[oam.AnnotationDeployVersion] = revisionVersion oamApp.Annotations[oam.AnnotationPublishVersion] = newRecordName - // create a new workflow record - if err := w.CreateWorkflowRecord(ctx, oamApp); err != nil { + if err := w.CreateWorkflowRecord(ctx, appModel, oamApp, workflow); err != nil { return err } @@ -527,9 +580,9 @@ func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *mode return nil } -func (w *workflowUsecaseImpl) checkRecordRunning(ctx context.Context, appModel *model.Application) (*v1beta1.Application, error) { +func (w *workflowUsecaseImpl) checkRecordRunning(ctx context.Context, appModel *model.Application, envName string) (*v1beta1.Application, error) { oamApp := &v1beta1.Application{} - if err := w.kubeClient.Get(ctx, types.NamespacedName{Name: appModel.Name, Namespace: appModel.Namespace}, oamApp); err != nil { + if err := w.kubeClient.Get(ctx, types.NamespacedName{Name: converAppName(appModel.Name, envName), Namespace: appModel.Namespace}, oamApp); err != nil { return nil, err } if oamApp.Status.Workflow != nil && !oamApp.Status.Workflow.Suspend && !oamApp.Status.Workflow.Terminated && !oamApp.Status.Workflow.Finished { diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 7f7305487..799b0bab4 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -37,20 +37,42 @@ import ( "github.com/oam-dev/kubevela/pkg/utils/apply" ) +var appName = "app-workflow" var _ = Describe("Test workflow usecase functions", func() { var ( workflowUsecase *workflowUsecaseImpl + appUsecase *applicationUsecaseImpl ) BeforeEach(func() { workflowUsecase = &workflowUsecaseImpl{ds: ds, kubeClient: k8sClient, apply: apply.NewAPIApplicator(k8sClient)} + appUsecase = &applicationUsecaseImpl{ds: ds, kubeClient: k8sClient, apply: apply.NewAPIApplicator(k8sClient), envBindingUsecase: &envBindingUsecaseImpl{ + ds: ds, + workflowUsecase: workflowUsecase, + }} }) It("Test CreateWorkflow function", func() { + reqApp := apisv1.CreateApplicationRequest{ + Name: appName, + Namespace: "default", + Description: "this is a test app", + EnvBinding: []*apisv1.EnvBinding{{ + Name: "dev", + Description: "dev env", + TargetNames: []string{"dev-target"}, + }}, + } + _, err := appUsecase.CreateApplication(context.TODO(), reqApp) + Expect(err).Should(BeNil()) + req := apisv1.CreateWorkflowRequest{ Name: "test-workflow-1", Description: "this is a workflow", + EnvName: "dev", } + base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ - Name: "test-app", + Name: appName, + Namespace: "default", }, req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) @@ -58,10 +80,12 @@ var _ = Describe("Test workflow usecase functions", func() { req = apisv1.CreateWorkflowRequest{ Name: "test-workflow-2", Description: "this is test workflow", + EnvName: "dev", Default: true, } base, err = workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ - Name: "test-app", + Name: appName, + Namespace: "default", }, req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) @@ -69,7 +93,8 @@ var _ = Describe("Test workflow usecase functions", func() { It("Test GetApplicationDefaultWorkflow function", func() { workflow, err := workflowUsecase.GetApplicationDefaultWorkflow(context.TODO(), &model.Application{ - Name: "test-app", + Name: appName, + Namespace: "default", }) Expect(err).Should(BeNil()) Expect(workflow).ShouldNot(BeNil()) @@ -83,14 +108,22 @@ var _ = Describe("Test workflow usecase functions", func() { app := &v1beta1.Application{} err = json.Unmarshal(raw, app) Expect(err).Should(BeNil()) - app.Annotations[oam.AnnotationWorkflowName] = "list-workflow-name" + app.Annotations[oam.AnnotationWorkflowName] = "test-workflow-2" + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, "test-workflow-2") + Expect(err).Should(BeNil()) for i := 0; i < 3; i++ { app.Annotations[oam.AnnotationPublishVersion] = fmt.Sprintf("list-workflow-name-%d", i) - err := workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) Expect(err).Should(BeNil()) } - resp, err := workflowUsecase.ListWorkflowRecords(context.TODO(), "list-workflow-name", 0, 10) + resp, err := workflowUsecase.ListWorkflowRecords(context.TODO(), workflow, 0, 10) Expect(err).Should(BeNil()) Expect(resp.Total).Should(Equal(int64(3))) }) @@ -102,28 +135,35 @@ var _ = Describe("Test workflow usecase functions", func() { app := &v1beta1.Application{} err = json.Unmarshal(raw, app) Expect(err).Should(BeNil()) - app.Annotations[oam.AnnotationWorkflowName] = "test-workflow-name" - app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-name-123" + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-2-123" app.Annotations[oam.AnnotationDeployVersion] = "1234" - err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, "test-workflow-2") + Expect(err).Should(BeNil()) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) Expect(err).Should(BeNil()) var revision = &model.ApplicationRevision{ - AppPrimaryKey: "test", + AppPrimaryKey: appName, Version: "1234", Status: model.RevisionStatusInit, DeployUser: "test-user", Note: "test-commit", TriggerType: "API", - WorkflowName: "test-workflow-name", + WorkflowName: "test-workflow-2", } err = workflowUsecase.createTestApplicationRevision(context.TODO(), revision) Expect(err).Should(BeNil()) - detail, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-workflow-name-123") + detail, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), workflow, "test-workflow-2-123") Expect(err).Should(BeNil()) - Expect(detail.WorkflowRecord.Name).Should(Equal("test-workflow-name-123")) + Expect(detail.WorkflowRecord.Name).Should(Equal("test-workflow-2-123")) Expect(detail.DeployUser).Should(Equal("test-user")) }) @@ -135,19 +175,27 @@ var _ = Describe("Test workflow usecase functions", func() { err = json.Unmarshal(raw, app) Expect(err).Should(BeNil()) app.Status.Workflow.Finished = false - app.Annotations[oam.AnnotationWorkflowName] = "test-workflow-name" - app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-name-233" + app.Annotations[oam.AnnotationWorkflowName] = "test-workflow-2" + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-2-233" app.Annotations[oam.AnnotationDeployVersion] = "4321" - err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, "test-workflow-2") + Expect(err).Should(BeNil()) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) Expect(err).Should(BeNil()) By("create one revision to test sync workflow record") var revision = &model.ApplicationRevision{ - AppPrimaryKey: "test", + AppPrimaryKey: appName, Version: "4321", Status: model.RevisionStatusInit, DeployUser: "test-user", - WorkflowName: "test-workflow-name", + WorkflowName: "test-workflow-2", } err = workflowUsecase.createTestApplicationRevision(context.TODO(), revision) Expect(err).Should(BeNil()) @@ -158,12 +206,17 @@ var _ = Describe("Test workflow usecase functions", func() { err = workflowUsecase.kubeClient.Create(ctx, app) Expect(err).Should(BeNil()) err = workflowUsecase.kubeClient.Status().Patch(ctx, app, client.Merge) - + Expect(err).Should(BeNil()) err = workflowUsecase.SyncWorkflowRecord(ctx) Expect(err).Should(BeNil()) + workflow, err = workflowUsecase.GetWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, "test-workflow-2") + Expect(err).Should(BeNil()) By("check the record") - record, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-workflow-name-233") + record, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), workflow, "test-workflow-2-233") Expect(err).Should(BeNil()) Expect(record.Status).Should(Equal(model.RevisionStatusComplete)) @@ -174,18 +227,21 @@ var _ = Describe("Test workflow usecase functions", func() { By("create another workflow record to test sync status from controller revision") app.Status.Workflow.Finished = false - app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-name-111" + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-2-111" app.Annotations[oam.AnnotationDeployVersion] = "1111" - err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) Expect(err).Should(BeNil()) By("create another revision to test sync workflow record") var anotherRevision = &model.ApplicationRevision{ - AppPrimaryKey: "test", + AppPrimaryKey: appName, Version: "1111", Status: model.RevisionStatusInit, DeployUser: "test-user", - WorkflowName: "test-workflow-name", + WorkflowName: "test-workflow-2", } err = workflowUsecase.createTestApplicationRevision(context.TODO(), anotherRevision) Expect(err).Should(BeNil()) @@ -194,9 +250,9 @@ var _ = Describe("Test workflow usecase functions", func() { Expect(err).Should(BeNil()) cr := &appsv1.ControllerRevision{ ObjectMeta: metav1.ObjectMeta{ - Name: "record-test-test-workflow-name-111", + Name: "record-" + appName + "-test-workflow-2-111", Namespace: "default", - Labels: map[string]string{"vela.io/wf-revision": "test-workflow-name-111"}, + Labels: map[string]string{"vela.io/wf-revision": "test-workflow-2-111"}, }, Data: runtime.RawExtension{Raw: raw}, } @@ -207,7 +263,7 @@ var _ = Describe("Test workflow usecase functions", func() { Expect(err).Should(BeNil()) By("check the record") - anotherRecord, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), "test-workflow-name", "test-workflow-name-111") + anotherRecord, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), workflow, "test-workflow-2-111") Expect(err).Should(BeNil()) Expect(anotherRecord.Status).Should(Equal(model.RevisionStatusComplete)) @@ -219,88 +275,141 @@ var _ = Describe("Test workflow usecase functions", func() { It("Test ResumeRecord function", func() { ctx := context.TODO() - app, err := createTestSuspendApp(ctx, "resume-app", "revision-resume1", "workflow-resume", "workflow-resume-1", workflowUsecase.kubeClient) + + ResumeWorkflow := "resume-workflow" + req := apisv1.CreateWorkflowRequest{ + Name: ResumeWorkflow, + Description: "this is a workflow", + EnvName: "resume", + } + + base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + app, err := createTestSuspendApp(ctx, appName, "resume", "revision-resume1", ResumeWorkflow, "workflow-resume-1", workflowUsecase.kubeClient) Expect(err).Should(BeNil()) - err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, &model.Workflow{Name: ResumeWorkflow}) Expect(err).Should(BeNil()) err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ - AppPrimaryKey: "resume-app", - - Version: "revision-resume1", - Status: model.RevisionStatusRunning, + AppPrimaryKey: appName, + Version: "revision-resume1", + Status: model.RevisionStatusRunning, }) Expect(err).Should(BeNil()) err = workflowUsecase.ResumeRecord(ctx, &model.Application{ - Name: "resume-app", + Name: appName, Namespace: "default", - }, "workflow-resume-1") + }, &model.Workflow{Name: ResumeWorkflow, EnvName: "resume"}, "workflow-resume-1") Expect(err).Should(BeNil()) - record, err := workflowUsecase.DetailWorkflowRecord(ctx, "workflow-resume", "workflow-resume-1") + record, err := workflowUsecase.DetailWorkflowRecord(ctx, &model.Workflow{Name: ResumeWorkflow, AppPrimaryKey: appName}, "workflow-resume-1") Expect(err).Should(BeNil()) Expect(record.Status).Should(Equal(model.RevisionStatusRunning)) }) It("Test TerminateRecord function", func() { ctx := context.TODO() - app, err := createTestSuspendApp(ctx, "terminate-app", "revision-terminate1", "workflow-terminate", "workflow-terminate-1", workflowUsecase.kubeClient) + + workflowName := "terminate-workflow" + req := apisv1.CreateWorkflowRequest{ + Name: workflowName, + Description: "this is a workflow", + EnvName: "terminate", + } + workflow := &model.Workflow{Name: workflowName, EnvName: "terminate"} + base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + app, err := createTestSuspendApp(ctx, appName, "terminate", "revision-terminate1", workflow.Name, "test-workflow-2-1", workflowUsecase.kubeClient) Expect(err).Should(BeNil()) - err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) Expect(err).Should(BeNil()) err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ - AppPrimaryKey: "terminate-app", + AppPrimaryKey: appName, Version: "revision-terminate1", Status: model.RevisionStatusRunning, }) Expect(err).Should(BeNil()) err = workflowUsecase.TerminateRecord(ctx, &model.Application{ - Name: "terminate-app", + Name: appName, Namespace: "default", - }, "workflow-terminate-1") + }, workflow, "test-workflow-2-1") Expect(err).Should(BeNil()) - record, err := workflowUsecase.DetailWorkflowRecord(ctx, "workflow-terminate", "workflow-terminate-1") + record, err := workflowUsecase.DetailWorkflowRecord(ctx, workflow, "test-workflow-2-1") Expect(err).Should(BeNil()) Expect(record.Status).Should(Equal(model.RevisionStatusTerminated)) }) It("Test RollbackRecord function", func() { ctx := context.TODO() - app, err := createTestSuspendApp(ctx, "rollback-app", "revision-rollback1", "workflow-rollback", "workflow-rollback-1", workflowUsecase.kubeClient) + + workflowName := "rollback-workflow" + req := apisv1.CreateWorkflowRequest{ + Name: workflowName, + Description: "this is a workflow", + EnvName: "rollback", + } + workflow := &model.Workflow{Name: workflowName, EnvName: "rollback"} + base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + app, err := createTestSuspendApp(ctx, appName, "rollback", "revision-rollback1", workflow.Name, "test-workflow-2-2", workflowUsecase.kubeClient) Expect(err).Should(BeNil()) - err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) Expect(err).Should(BeNil()) err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ - AppPrimaryKey: "rollback-app", + AppPrimaryKey: appName, Version: "revision-rollback1", Status: model.RevisionStatusRunning, }) Expect(err).Should(BeNil()) err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ - AppPrimaryKey: "rollback-app", + AppPrimaryKey: appName, Version: "revision-rollback0", - ApplyAppConfig: `{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"annotations":{"app.oam.dev/workflowName":"workflow-rollback","app.oam.dev/deployVersion":"revision-rollback1","vela.io/publish-version":"workflow-rollback1"},"name":"first-vela-app","namespace":"default"},"spec":{"components":[{"name":"express-server","properties":{"image":"crccheck/hello-world","port":8000},"traits":[{"properties":{"domain":"testsvc.example.com","http":{"/":8000}},"type":"ingress-1-20"}],"type":"webservice"}]}}`, + ApplyAppConfig: `{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"annotations":{"app.oam.dev/workflowName":"test-workflow-2-2","app.oam.dev/deployVersion":"revision-rollback1","vela.io/publish-version":"workflow-rollback1"},"name":"first-vela-app","namespace":"default"},"spec":{"components":[{"name":"express-server","properties":{"image":"crccheck/hello-world","port":8000},"traits":[{"properties":{"domain":"testsvc.example.com","http":{"/":8000}},"type":"ingress-1-20"}],"type":"webservice"}]}}`, Status: model.RevisionStatusComplete, }) Expect(err).Should(BeNil()) err = workflowUsecase.RollbackRecord(ctx, &model.Application{ - Name: "rollback-app", + Name: appName, Namespace: "default", - }, "workflow-rollback-1", "revision-rollback0") + }, workflow, "test-workflow-2-2", "revision-rollback0") Expect(err).Should(BeNil()) recordsNum, err := workflowUsecase.ds.Count(ctx, &model.WorkflowRecord{ - AppPrimaryKey: "rollback-app", - WorkflowPrimaryKey: "workflow-rollback", + AppPrimaryKey: appName, + WorkflowName: workflow.Name, RevisionPrimaryKey: "revision-rollback0", }, nil) Expect(err).Should(BeNil()) @@ -308,18 +417,21 @@ var _ = Describe("Test workflow usecase functions", func() { By("rollback application without revision version") app.Annotations[oam.AnnotationPublishVersion] = "workflow-rollback-2" - err = workflowUsecase.CreateWorkflowRecord(context.TODO(), app) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) Expect(err).Should(BeNil()) err = workflowUsecase.RollbackRecord(ctx, &model.Application{ - Name: "rollback-app", + Name: appName, Namespace: "default", - }, "workflow-rollback-2", "") + }, workflow, "workflow-rollback-2", "") Expect(err).Should(BeNil()) recordsNum, err = workflowUsecase.ds.Count(ctx, &model.WorkflowRecord{ - AppPrimaryKey: "rollback-app", - WorkflowPrimaryKey: "workflow-rollback", + AppPrimaryKey: appName, + WorkflowName: workflow.Name, RevisionPrimaryKey: "revision-rollback0", }, nil) Expect(err).Should(BeNil()) @@ -331,10 +443,11 @@ var yamlStr = `apiVersion: core.oam.dev/v1beta1 kind: Application metadata: annotations: - app.oam.dev/workflowName: test-workflow-name + app.oam.dev/workflowName: test-workflow-2 app.oam.dev/deployVersion: "1234" - vela.io/publish-version: "test-workflow-name-111" - name: test + app.oam.dev/publishVersion: "test-workflow-name-111" + app.oam.dev/appName: "app-workflow" + name: app-workflow-dev namespace: default spec: components: diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index 0bc309d57..73075b356 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -66,3 +66,12 @@ var ErrTraitNotExist = NewBcode(400, 10015, "trait is not exist") // ErrTraitAlreadyExist trait is already exist var ErrTraitAlreadyExist = NewBcode(400, 10016, "trait is already exist") + +// ErrApplicationNoReadyRevision application not have ready revision +var ErrApplicationNoReadyRevision = NewBcode(400, 10017, "application not have ready revision") + +// ErrApplicationRevisionNotExist application revision is not exist +var ErrApplicationRevisionNotExist = NewBcode(404, 10018, "application revision is not exist") + +// ErrApplicationRefusedDelete The application cannot be deleted because it has been deployed +var ErrApplicationRefusedDelete = NewBcode(400, 10019, "The application cannot be deleted because it has been deployed") diff --git a/pkg/apiserver/rest/utils/bcode/workflow.go b/pkg/apiserver/rest/utils/bcode/workflow.go index 9b4059cf3..7296bac83 100644 --- a/pkg/apiserver/rest/utils/bcode/workflow.go +++ b/pkg/apiserver/rest/utils/bcode/workflow.go @@ -27,3 +27,9 @@ var ErrWorkflowNoDefault = NewBcode(404, 20004, "application default workflow is // ErrMustQueryByApp you can only query the Workflow list based on applications. var ErrMustQueryByApp = NewBcode(404, 20005, "you can only query the Workflow list based on applications.") + +// ErrWorkflowNoEnv workflow have not env +var ErrWorkflowNoEnv = NewBcode(400, 20006, "workflow must set env name") + +// ErrWorkflowRecordNotExist workflow record is not exist +var ErrWorkflowRecordNotExist = NewBcode(404, 20007, "workflow record is not exist") diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index a55b96b45..94e0e9772 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -5,7 +5,7 @@ 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 + http://wwc.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, @@ -32,13 +32,18 @@ import ( ) type applicationWebService struct { + workflowWebService applicationUsecase usecase.ApplicationUsecase envBindingUsecase usecase.EnvBindingUsecase } // NewApplicationWebService new application manage webservice -func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase, envBindingUsecase usecase.EnvBindingUsecase) WebService { +func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase, envBindingUsecase usecase.EnvBindingUsecase, workflowUsecase usecase.WorkflowUsecase) WebService { return &applicationWebService{ + workflowWebService: workflowWebService{ + workflowUsecase: workflowUsecase, + applicationUsecase: applicationUsecase, + }, applicationUsecase: applicationUsecase, envBindingUsecase: envBindingUsecase, } @@ -89,15 +94,23 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.DetailApplicationResponse{})) - ws.Route(ws.GET("/{name}/envs/{envName}/status").To(c.getApplicationStatus). - Doc("get application status"). + ws.Route(ws.PUT("/{name}").To(c.updateApplication). + Doc("update one application "). Metadata(restfulspec.KeyOpenAPITags, tags). Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). - Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string")). - Returns(200, "", apis.ApplicationStatusResponse{}). + Reads(apis.UpdateApplicationRequest{}). + Returns(200, "", apis.ApplicationBase{}). Returns(400, "", bcode.Bcode{}). - Writes(apis.ApplicationStatusResponse{})) + Writes(apis.ApplicationBase{})) + ws.Route(ws.GET("/{name}/statistics").To(c.applicationStatistics). + Doc("detail one application "). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Returns(200, "", apis.ApplicationStatisticsResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationStatisticsResponse{})) ws.Route(ws.PUT("/{name}").To(c.updateApplication). Doc("update one application "). @@ -149,7 +162,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Writes(apis.ComponentBase{})) ws.Route(ws.GET("/{name}/components/{componentName}").To(c.detailComponent). - Doc("detail component for application "). + Doc("detail component for application "). Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -157,6 +170,17 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.DetailComponentResponse{})) + ws.Route(ws.PUT("/{name}/components/{componentName}").To(c.updateComponent). + Doc("update component config"). + Filter(c.appCheckFilter). + Filter(c.componentCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdateApplicationComponentRequest{}). + Returns(200, "", apis.ComponentBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ComponentBase{})) + ws.Route(ws.GET("/{name}/policies").To(c.listApplicationPolicies). Doc("list policy for application"). Filter(c.appCheckFilter). @@ -210,6 +234,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { ws.Route(ws.POST("/{name}/components/{compName}/traits").To(c.addApplicationTrait). Doc("add trait for a component"). Filter(c.appCheckFilter). + Filter(c.componentCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). @@ -221,6 +246,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { ws.Route(ws.PUT("/{name}/components/{compName}/traits/{traitType}").To(c.updateApplicationTrait). Doc("update trait from a component"). Filter(c.appCheckFilter). + Filter(c.componentCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Param(ws.PathParameter("traitType", "identifier of the type of trait").DataType("string")). @@ -233,6 +259,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { ws.Route(ws.DELETE("/{name}/components/{compName}/traits/{traitType}").To(c.deleteApplicationTrait). Doc("delete trait from a component"). Filter(c.appCheckFilter). + Filter(c.componentCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). Param(ws.PathParameter("traitType", "identifier of the type of trait").DataType("string")). @@ -306,6 +333,138 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(404, "", bcode.Bcode{}). Writes(apis.EmptyResponse{})) + ws.Route(ws.GET("/{name}/envs/{envName}/status").To(c.getApplicationStatus). + Doc("get application status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string")). + Returns(200, "", apis.ApplicationStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationStatusResponse{})) + + ws.Route(ws.POST("/{name}/envs/{envName}/recycle").To(c.recycleApplicationEnv). + Doc("get application status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string").Required(true)). + Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string").Required(true)). + Returns(200, "", apis.EmptyResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/{name}/workflows").To(c.listApplicationWorkflows). + Doc("list application workflow"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListWorkflowResponse{}). + Writes(apis.ListWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.POST("/{name}/workflows").To(c.createApplicationWorkflow). + Doc("create application workflow"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateWorkflowRequest{}). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Returns(200, "create success", apis.DetailWorkflowResponse{}). + Returns(400, "create failure", bcode.Bcode{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}").To(c.detailWorkflow). + Doc("detail application workflow"). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workfloc.").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.workflowCheckFilter). + Returns(200, "create success", apis.DetailWorkflowResponse{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.PUT("/{name}/workflows/{workflowName}").To(c.updateWorkflow). + Doc("update application workflow config"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Reads(apis.UpdateWorkflowRequest{}). + Returns(200, "", apis.DetailWorkflowResponse{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.DELETE("/{name}/workflows/{workflowName}").To(c.deleteWorkflow). + Doc("deletet workflow"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records").To(c.listWorkflowRecords). + Doc("query application workflow execution record"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Param(ws.QueryParameter("page", "query the page number").DataType("integer")). + Param(ws.QueryParameter("pageSize", "query the page size number").DataType("integer")). + Returns(200, "", apis.ListWorkflowRecordsResponse{}). + Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}").To(c.detailWorkflowRecord). + Doc("query application workflow execution record detail"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Returns(200, "", apis.DetailWorkflowRecordResponse{}). + Writes(apis.DetailWorkflowRecordResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/resume").To(c.resumeWorkflowRecord). + Doc("resume suspend workflow record"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/terminate").To(c.terminateWorkflowRecord). + Doc("terminate suspend workflow record"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/rollback").To(c.rollbackWorkflowRecord). + Doc("rollback suspend application record"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Param(ws.QueryParameter("rollbackVersion", "identifier of the rollback revision").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) + return ws } @@ -465,6 +624,30 @@ func (c *applicationWebService) detailComponent(req *restful.Request, res *restf } } +func (c *applicationWebService) updateComponent(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + component := req.Request.Context().Value(&apis.CtxKeyApplicationComponent).(*model.ApplicationComponent) + // Verify the validity of parameters + var updateReq apis.UpdateApplicationComponentRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + base, err := c.applicationUsecase.UpdateComponent(req.Request.Context(), app, component, updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + func (c *applicationWebService) createApplicationPolicy(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Verify the validity of parameters @@ -641,7 +824,7 @@ func (c *applicationWebService) getApplicationStatus(req *restful.Request, res * return } - if err := res.WriteEntity(apis.ApplicationStatusResponse{Status: status}); err != nil { + if err := res.WriteEntity(apis.ApplicationStatusResponse{Status: status, EnvName: req.PathParameter("envName")}); err != nil { bcode.ReturnError(req, res, err) return } @@ -759,6 +942,17 @@ func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restfu chain.ProcessFilter(req, res) } +func (c *applicationWebService) componentCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + component, err := c.applicationUsecase.GetApplicationComponent(req.Request.Context(), app, req.PathParameter("compName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplicationComponent, component)) + chain.ProcessFilter(req, res) +} + func (c *applicationWebService) envCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) envBindings, err := c.envBindingUsecase.GetEnvBindings(req.Request.Context(), app) @@ -775,3 +969,30 @@ func (c *applicationWebService) envCheckFilter(req *restful.Request, res *restfu } bcode.ReturnError(req, res, bcode.ErrApplicationNotEnv) } + +func (c *applicationWebService) applicationStatistics(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.Statistics(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) recycleApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + env := req.Request.Context().Value(&apis.CtxKeyApplicationEnvBinding).(*model.EnvBinding) + err := c.envBindingUsecase.ApplicationEnvRecycle(req.Request.Context(), app, env) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/delivery_target.go b/pkg/apiserver/rest/webservice/delivery_target.go index 5a8843ca9..814e1ace6 100644 --- a/pkg/apiserver/rest/webservice/delivery_target.go +++ b/pkg/apiserver/rest/webservice/delivery_target.go @@ -177,7 +177,7 @@ func (dt *DeliveryTargetWebService) updateDeliveryTarget(req *restful.Request, r func (dt *DeliveryTargetWebService) deleteDeliveryTarget(req *restful.Request, res *restful.Response) { deliveryTargetName := req.PathParameter("name") // deliveryTarget in use, can't be deleted - applications, err := dt.applicationUsecase.ListApplications(context.TODO(), apis.ListApplicatioOptions{TargetName: deliveryTargetName}) + applications, err := dt.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioOptions{TargetName: deliveryTargetName}) if err != nil { if !errors.Is(err, datastore.ErrRecordNotExist) { bcode.ReturnError(req, res, err) diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 047b38657..907e41f2e 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -69,14 +69,13 @@ func Init(ds datastore.DataStore) { envBindingUsecase := usecase.NewEnvBindingUsecase(ds, workflowUsecase) applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase, envBindingUsecase, deliveryTargetUsecase) RegistWebService(NewClusterWebService(clusterUsecase)) - RegistWebService(NewApplicationWebService(applicationUsecase, envBindingUsecase)) + RegistWebService(NewApplicationWebService(applicationUsecase, envBindingUsecase, workflowUsecase)) RegistWebService(NewNamespaceWebService(namespaceUsecase)) RegistWebService(NewDefinitionWebservice(definitionUsecase)) RegistWebService(NewAddonWebService(addonUsecase)) RegistWebService(NewAddonRegistryWebService(addonUsecase)) RegistWebService(NewOAMApplication(oamApplicationUsecase)) RegistWebService(&policyDefinitionWebservice{}) - RegistWebService(NewWorkflowWebService(workflowUsecase, applicationUsecase)) RegistWebService(NewDeliveryTargetWebService(deliveryTargetUsecase, applicationUsecase)) RegistWebService(NewVelaQLWebService(velaQLUsecase)) } diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index be92b120b..d09e427ec 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -18,9 +18,7 @@ package webservice import ( "context" - "strconv" - restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" "github.com/oam-dev/kubevela/pkg/apiserver/log" @@ -31,122 +29,14 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) -// NewWorkflowWebService new workflow webservice -func NewWorkflowWebService(workflowUsecase usecase.WorkflowUsecase, applicationUsecase usecase.ApplicationUsecase) WebService { - return &workflowWebService{ - workflowUsecase: workflowUsecase, - applicationUsecase: applicationUsecase, - } -} - type workflowWebService struct { workflowUsecase usecase.WorkflowUsecase applicationUsecase usecase.ApplicationUsecase } -func (w *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{"workflow"} - - ws.Route(ws.GET("/").To(w.listApplicationWorkflows). - Doc("list application workflow"). - Param(ws.QueryParameter("appName", "identifier of the application.").DataType("string").Required(true)). - Param(ws.QueryParameter("enable", "query based on enable status").DataType("boolean")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Returns(200, "", apis.ListWorkflowResponse{}). - Writes(apis.ListWorkflowResponse{}).Do(returns200, returns500)) - - ws.Route(ws.POST("/").To(w.createApplicationWorkflow). - Doc("create application workflow"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateWorkflowRequest{}). - Returns(200, "create success", apis.DetailWorkflowResponse{}). - Returns(400, "create failure", bcode.Bcode{}). - Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - - ws.Route(ws.GET("/{name}").To(w.detailWorkflow). - Doc("detail application workflow"). - Param(ws.PathParameter("name", "identifier of the workflow.").DataType("string")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(w.workflowCheckFilter). - Returns(200, "create success", apis.DetailWorkflowResponse{}). - Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - - ws.Route(ws.PUT("/{name}").To(w.updateWorkflow). - Doc("update application workflow config"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(w.workflowCheckFilter). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Reads(apis.UpdateWorkflowRequest{}). - Returns(200, "", apis.DetailWorkflowResponse{}). - Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - - ws.Route(ws.DELETE("/{name}").To(w.deleteWorkflow). - Doc("deletet workflow"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(w.workflowCheckFilter). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Returns(200, "", apis.EmptyResponse{}). - Writes(apis.EmptyResponse{}).Do(returns200, returns500)) - - ws.Route(ws.GET("/{name}/records").To(w.listWorkflowRecords). - Doc("query application workflow execution record"). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(w.workflowCheckFilter). - Param(ws.QueryParameter("page", "query the page number").DataType("integer")). - Param(ws.QueryParameter("pageSize", "query the page size number").DataType("integer")). - Returns(200, "", apis.ListWorkflowRecordsResponse{}). - Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) - - ws.Route(ws.GET("/{name}/records/{record}").To(w.detailWorkflowRecord). - Doc("query application workflow execution record detail"). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Returns(200, "", apis.DetailWorkflowRecordResponse{}). - Writes(apis.DetailWorkflowRecordResponse{}).Do(returns200, returns500)) - - ws.Route(ws.GET("/{name}/records/{record}/resume").To(w.resumeWorkflowRecord). - Doc("resume suspend workflow record"). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(w.applicationCheckFilter). - Returns(200, "", nil). - Returns(400, "", bcode.Bcode{}). - Writes(apis.DetailWorkflowRecordResponse{})) - - ws.Route(ws.GET("/{name}/records/{record}/terminate").To(w.terminateWorkflowRecord). - Doc("terminate suspend workflow record"). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(w.applicationCheckFilter). - Returns(200, "", nil). - Returns(400, "", bcode.Bcode{}). - Writes(apis.DetailWorkflowRecordResponse{})) - - ws.Route(ws.GET("/{name}/records/{record}/rollback").To(w.rollbackWorkflowRecord). - Doc("rollback suspend application record"). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). - Param(ws.QueryParameter("rollbackVersion", "identifier of the rollback revision").DataType("string")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Filter(w.applicationCheckFilter). - Returns(200, "", nil). - Returns(400, "", bcode.Bcode{}). - Writes(apis.DetailWorkflowRecordResponse{})) - return ws -} - func (w *workflowWebService) workflowCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - workflow, err := w.workflowUsecase.GetWorkflow(req.Request.Context(), req.PathParameter("name")) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + workflow, err := w.workflowUsecase.GetWorkflow(req.Request.Context(), app, req.PathParameter("workflowName")) if err != nil { bcode.ReturnError(req, res, err) return @@ -156,37 +46,19 @@ func (w *workflowWebService) workflowCheckFilter(req *restful.Request, res *rest } func (w *workflowWebService) applicationCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - workflow, err := w.workflowUsecase.GetWorkflow(req.Request.Context(), req.PathParameter("name")) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + workflow, err := w.workflowUsecase.GetWorkflow(req.Request.Context(), app, req.PathParameter("workflowName")) if err != nil { bcode.ReturnError(req, res, err) return } - - app, err := w.applicationUsecase.GetApplication(req.Request.Context(), workflow.AppPrimaryKey) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplication, app)) + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyWorkflow, workflow)) chain.ProcessFilter(req, res) } func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res *restful.Response) { - if req.QueryParameter("appName") == "" { - bcode.ReturnError(req, res, bcode.ErrMustQueryByApp) - return - } - app, err := w.applicationUsecase.GetApplication(req.Request.Context(), req.QueryParameter("appName")) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - var enableQuery *bool - enable, err := strconv.ParseBool(req.QueryParameter("enable")) - if err == nil { - enableQuery = &enable - } - workflows, err := w.workflowUsecase.ListApplicationWorkflow(req.Request.Context(), app, enableQuery) + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + workflows, err := w.workflowUsecase.ListApplicationWorkflow(req.Request.Context(), app) if err != nil { bcode.ReturnError(req, res, err) return @@ -265,7 +137,8 @@ func (w *workflowWebService) updateWorkflow(req *restful.Request, res *restful.R } func (w *workflowWebService) deleteWorkflow(req *restful.Request, res *restful.Response) { - if err := w.workflowUsecase.DeleteWorkflow(req.Request.Context(), req.PathParameter("name")); err != nil { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + if err := w.workflowUsecase.DeleteWorkflow(req.Request.Context(), app, req.PathParameter("workflowName")); err != nil { bcode.ReturnError(req, res, err) return } @@ -281,8 +154,8 @@ func (w *workflowWebService) listWorkflowRecords(req *restful.Request, res *rest bcode.ReturnError(req, res, err) return } - - records, err := w.workflowUsecase.ListWorkflowRecords(req.Request.Context(), req.PathParameter("name"), page, pageSize) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + records, err := w.workflowUsecase.ListWorkflowRecords(req.Request.Context(), workflow, page, pageSize) if err != nil { bcode.ReturnError(req, res, err) return @@ -295,7 +168,8 @@ func (w *workflowWebService) listWorkflowRecords(req *restful.Request, res *rest } func (w *workflowWebService) detailWorkflowRecord(req *restful.Request, res *restful.Response) { - record, err := w.workflowUsecase.DetailWorkflowRecord(req.Request.Context(), req.PathParameter("name"), req.PathParameter("record")) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + record, err := w.workflowUsecase.DetailWorkflowRecord(req.Request.Context(), workflow, req.PathParameter("record")) if err != nil { bcode.ReturnError(req, res, err) return @@ -309,7 +183,8 @@ func (w *workflowWebService) detailWorkflowRecord(req *restful.Request, res *res func (w *workflowWebService) resumeWorkflowRecord(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - err := w.workflowUsecase.ResumeRecord(req.Request.Context(), app, req.PathParameter("record")) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + err := w.workflowUsecase.ResumeRecord(req.Request.Context(), app, workflow, req.PathParameter("record")) if err != nil { bcode.ReturnError(req, res, err) return @@ -319,7 +194,8 @@ func (w *workflowWebService) resumeWorkflowRecord(req *restful.Request, res *res func (w *workflowWebService) terminateWorkflowRecord(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - err := w.workflowUsecase.TerminateRecord(req.Request.Context(), app, req.PathParameter("record")) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + err := w.workflowUsecase.TerminateRecord(req.Request.Context(), app, workflow, req.PathParameter("record")) if err != nil { bcode.ReturnError(req, res, err) return @@ -329,7 +205,8 @@ func (w *workflowWebService) terminateWorkflowRecord(req *restful.Request, res * func (w *workflowWebService) rollbackWorkflowRecord(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - err := w.workflowUsecase.RollbackRecord(req.Request.Context(), app, req.PathParameter("record"), req.QueryParameter("rollbackVersion")) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + err := w.workflowUsecase.RollbackRecord(req.Request.Context(), app, workflow, req.PathParameter("record"), req.QueryParameter("rollbackVersion")) if err != nil { bcode.ReturnError(req, res, err) return diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index 45333220f..23e887c53 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -133,11 +133,17 @@ const ( AnnotationDeployVersion = "app.oam.dev/deployVersion" // AnnotationPublishVersion is annotation that record the application workflow version. - AnnotationPublishVersion = "vela.io/publish-version" + AnnotationPublishVersion = "app.oam.dev/publishVersion" // AnnotationWorkflowName specifies the workflow name for execution. AnnotationWorkflowName = "app.oam.dev/workflowName" + // AnnotationAppName specifies the name for application in db. + AnnotationAppName = "app.oam.dev/appName" + + // AnnotationAppAlias specifies the alias for application in db. + AnnotationAppAlias = "app.oam.dev/appAlias" + // AnnotationWorkloadGVK indicates the managed workload's GVK by trait AnnotationWorkloadGVK = "trait.oam.dev/workload-gvk" diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index 4b107b2ff..62192b71e 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -33,12 +33,15 @@ import ( apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" ) +var appName = "app-e2e" +var appProject = "test-app-project" + 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-namespace", + Name: appName, + Namespace: appProject, Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, @@ -63,7 +66,7 @@ var _ = Describe("Test application rest api", func() { It("Test delete app", func() { defer GinkgoRecover() - req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd", nil) + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/"+appName, nil) Expect(err).ShouldNot(HaveOccurred()) res, err := http.DefaultClient.Do(req) Expect(err).ShouldNot(HaveOccurred()) @@ -76,8 +79,8 @@ var _ = Describe("Test application rest api", func() { bs, err := ioutil.ReadFile("./testdata/example-app.yaml") Expect(err).Should(Succeed()) var req = apisv1.CreateApplicationRequest{ - Name: "test-app-sadasd", - Namespace: "test-app-namespace", + Name: appName, + Namespace: appProject, Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, @@ -102,7 +105,7 @@ var _ = Describe("Test application rest api", func() { It("Test list components", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/" + appName + "/components") Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -114,37 +117,9 @@ var _ = Describe("Test application rest api", func() { Expect(cmp.Diff(len(components.Components), 2)).Should(BeEmpty()) }) - It("Test list policies", func() { - defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies") - 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 policies apisv1.ListApplicationPolicy - err = json.NewDecoder(res.Body).Decode(&policies) - Expect(err).ShouldNot(HaveOccurred()) - Expect(cmp.Diff(len(policies.Policies), 1)).Should(BeEmpty()) - }) - - It("Test get workflow", func() { - // defer GinkgoRecover() - // res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies") - // 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 policies apisv1.ListApplicationPolicy - // err = json.NewDecoder(res.Body).Decode(&policies) - // Expect(err).ShouldNot(HaveOccurred()) - // Expect(cmp.Diff(len(policies.Policies), 1)).Should(BeEmpty()) - }) - It("Test detail application", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/" + appName) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -153,19 +128,53 @@ var _ = Describe("Test application rest api", func() { var detail apisv1.DetailApplicationResponse err = json.NewDecoder(res.Body).Decode(&detail) Expect(err).ShouldNot(HaveOccurred()) - Expect(cmp.Diff(len(detail.Policies), 1)).Should(BeEmpty()) + Expect(cmp.Diff(len(detail.Policies), 0)).Should(BeEmpty()) }) It("Test deploy application", func() { defer GinkgoRecover() - var req = apisv1.ApplicationDeployRequest{ - Note: "test apply", - TriggerType: "web", - Force: false, + var targetName = "dev-default" + var envName = "dev" + var namespace = "default" + // create target + var createTarget = apisv1.CreateDeliveryTargetRequest{ + Name: targetName, + Namespace: appProject, + Cluster: &apisv1.ClusterTarget{ + ClusterName: "local", + Namespace: namespace, + }, } - bodyByte, err := json.Marshal(req) + bodyByte, err := json.Marshal(createTarget) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/deploy", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/deliveryTargets", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + + // create env + var createEnvReq = apisv1.CreateApplicationEnvRequest{ + EnvBinding: apisv1.EnvBinding{ + Name: envName, + TargetNames: []string{targetName}, + }, + } + bodyByte, err = json.Marshal(createEnvReq) + Expect(err).ShouldNot(HaveOccurred()) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/envs", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + + // deploy app + var req = apisv1.ApplicationDeployRequest{ + Note: "test apply", + TriggerType: "web", + WorkflowName: "dev", + Force: false, + } + bodyByte, err = json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/deploy", "application/json", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -177,7 +186,7 @@ var _ = Describe("Test application rest api", func() { Expect(cmp.Diff(response.Status, model.RevisionStatusRunning)).Should(BeEmpty()) var oam v1beta1.Application - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: "test-app-sadasd", Namespace: "test-app-namespace"}, &oam) + err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: appName + "-" + envName, Namespace: appProject}, &oam) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty()) Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty()) @@ -195,7 +204,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/components", "application/json", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -209,7 +218,7 @@ var _ = Describe("Test application rest api", func() { It("Test detail component", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/" + appName + "/components/test2") Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -229,7 +238,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2/traits", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/components/test2/traits", "application/json", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -249,7 +258,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req2) Expect(err).ShouldNot(HaveOccurred()) - req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2/traits/ingress", bytes.NewBuffer(bodyByte)) + req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/"+appName+"/components/test2/traits/ingress", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) @@ -266,7 +275,7 @@ var _ = Describe("Test application rest api", func() { It("Test delete trait", func() { defer GinkgoRecover() - req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/components/test2/traits/ingress", nil) + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/"+appName+"/components/test2/traits/ingress", nil) Expect(err).ShouldNot(HaveOccurred()) res, err := http.DefaultClient.Do(req) Expect(err).ShouldNot(HaveOccurred()) @@ -283,7 +292,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) - res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte)) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/policies", "application/json", bytes.NewBuffer(bodyByte)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 400)).Should(BeEmpty()) @@ -295,7 +304,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte2, err := json.Marshal(req2) Expect(err).ShouldNot(HaveOccurred()) - res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies", "application/json", bytes.NewBuffer(bodyByte2)) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/policies", "application/json", bytes.NewBuffer(bodyByte2)) Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -310,7 +319,7 @@ var _ = Describe("Test application rest api", func() { It("Test detail application policy", func() { defer GinkgoRecover() - res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2") + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/" + appName + "/policies/test2") Expect(err).ShouldNot(HaveOccurred()) Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) @@ -330,7 +339,7 @@ var _ = Describe("Test application rest api", func() { } bodyByte2, err := json.Marshal(req2) Expect(err).ShouldNot(HaveOccurred()) - req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2", bytes.NewBuffer(bodyByte2)) + req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/"+appName+"/policies/test2", bytes.NewBuffer(bodyByte2)) Expect(err).ShouldNot(HaveOccurred()) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) @@ -348,7 +357,7 @@ var _ = Describe("Test application rest api", func() { It("Test delete application policy", func() { defer GinkgoRecover() - req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/test-app-sadasd/policies/test2", nil) + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/"+appName+"/policies/test2", nil) Expect(err).ShouldNot(HaveOccurred()) res, err := http.DefaultClient.Do(req) Expect(err).ShouldNot(HaveOccurred()) diff --git a/test/e2e-apiserver-test/suite_test.go b/test/e2e-apiserver-test/suite_test.go index 4371d0bd1..e0364e87b 100644 --- a/test/e2e-apiserver-test/suite_test.go +++ b/test/e2e-apiserver-test/suite_test.go @@ -60,7 +60,7 @@ var _ = BeforeSuite(func() { } cfg.LeaderConfig.ID = uuid.New().String() cfg.LeaderConfig.LockName = "apiserver-lock" - cfg.LeaderConfig.Duration = time.Second * 5 + cfg.LeaderConfig.Duration = time.Second * 10 server, err := arest.New(cfg) Expect(err).ShouldNot(HaveOccurred()) From 8181b4d266622e0eb7e67e00b635f866504e412e Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Sun, 21 Nov 2021 10:15:15 +0800 Subject: [PATCH 52/59] Fix: fix the envbinding can not be deleted bug (#2756) * Fix: fix the envbinding can not be deleted bug * Fix: fix target must be interface or implement error Co-authored-by: barnettZQG --- docs/apidoc/swagger.json | 135 ++++++++++++------ pkg/addon/addon.go | 2 - pkg/addon/error.go | 3 +- pkg/apiserver/rest/usecase/application.go | 9 +- pkg/apiserver/rest/usecase/envbinding.go | 12 +- pkg/apiserver/rest/usecase/workflow.go | 4 - pkg/apiserver/rest/utils/bcode/application.go | 3 + pkg/apiserver/rest/utils/bcode/envbinding.go | 2 +- pkg/apiserver/rest/webservice/application.go | 12 +- pkg/apiserver/rest/webservice/workflow.go | 26 ++-- 10 files changed, 122 insertions(+), 86 deletions(-) diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 93dc02006..e12eeacc9 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -1125,6 +1125,51 @@ } } }, + "/api/v1/applications/{name}/envs/{envName}/recycle": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "get application status", + "operationId": "recycleApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application envbinding", + "name": "envName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/applications/{name}/envs/{envName}/status": { "get": { "consumes": [ @@ -2613,9 +2658,9 @@ "parameters": [ { "enum": [ + "workflowstep", "component", - "trait", - "workflowstep" + "trait" ], "type": "string", "description": "query the definition type", @@ -3217,10 +3262,10 @@ }, "common.AppRolloutStatus": { "required": [ - "upgradedReadyReplicas", "rollingState", - "batchRollingState", "currentBatch", + "upgradedReadyReplicas", + "batchRollingState", "upgradedReplicas", "lastTargetAppRevision" ], @@ -3711,8 +3756,8 @@ }, "model.ApplicationComponent": { "required": [ - "updateTime", "createTime", + "updateTime", "appPrimaryKey", "creator", "name", @@ -4331,14 +4376,14 @@ }, "v1.ApplicationDeployResponse": { "required": [ + "note", "envName", "triggerType", "createTime", "version", "status", "reason", - "deployUser", - "note" + "deployUser" ], "properties": { "createTime": { @@ -4849,8 +4894,8 @@ }, "v1.CreateApplicationEnvRequest": { "required": [ - "targetNames", - "name" + "name", + "targetNames" ], "properties": { "alias": { @@ -5264,10 +5309,10 @@ }, "v1.DetailAddonResponse": { "required": [ + "icon", "name", "version", "description", - "icon", "schema", "uiSchema", "definitions" @@ -5325,13 +5370,13 @@ }, "v1.DetailApplicationResponse": { "required": [ - "description", - "createTime", "updateTime", "icon", "name", "alias", "namespace", + "description", + "createTime", "policies", "envBindings", "status", @@ -5389,19 +5434,19 @@ }, "v1.DetailClusterResponse": { "required": [ - "apiServerURL", - "name", - "labels", - "reason", - "icon", "status", - "provider", + "reason", "dashboardURL", + "name", + "description", + "icon", + "labels", "kubeConfig", + "kubeConfigSecret", "model", "alias", - "description", - "kubeConfigSecret", + "provider", + "apiServerURL", "resourceInfo" ], "properties": { @@ -5454,13 +5499,13 @@ }, "v1.DetailComponentResponse": { "required": [ + "alias", + "creator", + "updateTime", + "appPrimaryKey", "createTime", "type", - "updateTime", - "name", - "appPrimaryKey", - "creator", - "alias" + "name" ], "properties": { "alias": { @@ -5555,10 +5600,10 @@ }, "v1.DetailDeliveryTargetResponse": { "required": [ - "name", - "updateTime", "namespace", - "createTime" + "createTime", + "updateTime", + "name" ], "properties": { "alias": { @@ -5595,13 +5640,13 @@ }, "v1.DetailPolicyResponse": { "required": [ - "creator", "properties", "createTime", "updateTime", "name", "type", - "description" + "description", + "creator" ], "properties": { "createTime": { @@ -5631,17 +5676,17 @@ }, "v1.DetailRevisionResponse": { "required": [ - "note", - "updateTime", - "appPrimaryKey", - "version", - "reason", "triggerType", "workflowName", + "version", + "appPrimaryKey", + "status", + "reason", + "deployUser", + "note", "envName", "createTime", - "status", - "deployUser" + "updateTime" ], "properties": { "appPrimaryKey": { @@ -5686,9 +5731,9 @@ }, "v1.DetailWorkflowRecordResponse": { "required": [ - "name", "namespace", "status", + "name", "deployTime", "deployUser", "note", @@ -5731,14 +5776,14 @@ }, "v1.DetailWorkflowResponse": { "required": [ - "name", - "enable", - "envName", "alias", "description", - "default", + "envName", "createTime", - "updateTime" + "updateTime", + "name", + "enable", + "default" ], "properties": { "alias": { @@ -6120,10 +6165,10 @@ }, "v1.NamespaceDetailResponse": { "required": [ - "name", "description", "createTime", - "updateTime" + "updateTime", + "name" ], "properties": { "createTime": { diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 860fffca0..879e91b6c 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -348,7 +348,6 @@ func readMetadata(wg *sync.WaitGroup, reader AddonReader) { reader.errChan <- err return } - return } func readReadme(wg *sync.WaitGroup, reader AddonReader) { @@ -359,7 +358,6 @@ func readReadme(wg *sync.WaitGroup, reader AddonReader) { return } reader.addon.Detail, err = content.GetContent() - return } func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { diff --git a/pkg/addon/error.go b/pkg/addon/error.go index 244345b78..c66d40d48 100644 --- a/pkg/addon/error.go +++ b/pkg/addon/error.go @@ -20,7 +20,8 @@ var ( // WrapErrRateLimit return ErrRateLimit if is the situation, or return error directly func WrapErrRateLimit(err error) error { - if errors.As(err, &github.RateLimitError{}) { + var rateLimit *github.RateLimitError + if errors.As(err, &rateLimit) { return ErrRateLimit } return err diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index b6644edfc..b50e94b87 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -208,16 +208,17 @@ func (c *applicationUsecaseImpl) GetApplicationStatus(ctx context.Context, appmo } // GetApplicationCR get application cr in cluster -func (c *applicationUsecaseImpl) GetApplicationCR(ctx context.Context, appmodel *model.Application) (*v1beta1.ApplicationList, error) { +func (c *applicationUsecaseImpl) GetApplicationCR(ctx context.Context, appModel *model.Application) (*v1beta1.ApplicationList, error) { var apps v1beta1.ApplicationList selector := labels.NewSelector() - re, err := labels.NewRequirement(oam.AnnotationAppName, selection.Equals, []string{appmodel.Name}) + re, err := labels.NewRequirement(oam.AnnotationAppName, selection.Equals, []string{appModel.Name}) if err != nil { return nil, err } selector = selector.Add(*re) err = c.kubeClient.List(ctx, &apps, &client.ListOptions{ LabelSelector: selector, + Namespace: appModel.Namespace, }) if err != nil { if apierrors.IsNotFound(err) { @@ -314,7 +315,7 @@ func (c *applicationUsecaseImpl) genPolicyByEnv(ctx context.Context, app *model. if err != nil || target == nil { return appPolicy, bcode.ErrFoundEnvbindingDeliveryTarget } - envBindingSpec.Envs = append(envBindingSpec.Envs, createTargetClusterEnv(envBinding.EnvBindingBase, target)) + envBindingSpec.Envs = append(envBindingSpec.Envs, createTargetClusterEnv(envBinding, target)) } properties, err := model.NewJSONStructByStruct(envBindingSpec) if err != nil { @@ -1150,7 +1151,7 @@ func (c *applicationUsecaseImpl) Statistics(ctx context.Context, app *model.Appl }, nil } -func createTargetClusterEnv(envBind apisv1.EnvBindingBase, target *model.DeliveryTarget) v1alpha1.EnvConfig { +func createTargetClusterEnv(envBind *model.EnvBinding, target *model.DeliveryTarget) v1alpha1.EnvConfig { placement := v1alpha1.EnvPlacement{} var componentSelector *v1alpha1.EnvSelector if envBind.ComponentSelector != nil { diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go index f046212c0..06dc0f12a 100644 --- a/pkg/apiserver/rest/usecase/envbinding.go +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -39,7 +39,7 @@ import ( // EnvBindingUsecase envbinding usecase type EnvBindingUsecase interface { GetEnvBindings(ctx context.Context, app *model.Application) ([]*apisv1.EnvBindingBase, error) - GetEnvBinding(ctx context.Context, app *model.Application, envName string) (*apisv1.DetailEnvBindingResponse, error) + GetEnvBinding(ctx context.Context, app *model.Application, envName string) (*model.EnvBinding, error) CheckAppEnvBindingsContainTarget(ctx context.Context, app *model.Application, targetName string) (bool, error) CreateEnvBinding(ctx context.Context, app *model.Application, env apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) BatchCreateEnvBinding(ctx context.Context, app *model.Application, env apisv1.EnvBindingList) error @@ -92,7 +92,7 @@ func (e *envBindingUsecaseImpl) GetEnvBindings(ctx context.Context, app *model.A return list, nil } -func (e *envBindingUsecaseImpl) GetEnvBinding(ctx context.Context, app *model.Application, envName string) (*apisv1.DetailEnvBindingResponse, error) { +func (e *envBindingUsecaseImpl) GetEnvBinding(ctx context.Context, app *model.Application, envName string) (*model.EnvBinding, error) { envBinding, err := e.getBindingByEnv(ctx, app, envName) if err != nil { if errors.Is(err, datastore.ErrRecordNotExist) { @@ -100,7 +100,7 @@ func (e *envBindingUsecaseImpl) GetEnvBinding(ctx context.Context, app *model.Ap } return nil, err } - return e.DetailEnvBinding(ctx, app, envBinding) + return envBinding, nil } func (e *envBindingUsecaseImpl) CheckAppEnvBindingsContainTarget(ctx context.Context, app *model.Application, targetName string) (bool, error) { @@ -196,9 +196,9 @@ func (e *envBindingUsecaseImpl) DeleteEnvBinding(ctx context.Context, appModel * return err } var app v1beta1.Application - err = e.kubeClient.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: converAppName(appModel.Name, envBinding.Name)}, &app) - if err != nil && !apierrors.IsNotFound(err) { - return bcode.ErrApplicationRefusedDelete + err = e.kubeClient.Get(ctx, types.NamespacedName{Namespace: appModel.Namespace, Name: converAppName(appModel.Name, envBinding.Name)}, &app) + if err == nil || !apierrors.IsNotFound(err) { + return bcode.ErrApplicationEnvRefusedDelete } if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: appModel.PrimaryKey(), Name: envBinding.Name}); err != nil { return err diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 1a6b189a9..84ed9b920 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -42,10 +42,6 @@ import ( "github.com/oam-dev/kubevela/pkg/utils/apply" ) -const ( - labelControllerRevisionSync = "apiserver.oam.dev/cr-sync" -) - // WorkflowUsecase workflow manage api type WorkflowUsecase interface { ListApplicationWorkflow(ctx context.Context, app *model.Application) ([]*apisv1.WorkflowBase, error) diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index 73075b356..21e9743d9 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -75,3 +75,6 @@ var ErrApplicationRevisionNotExist = NewBcode(404, 10018, "application revision // ErrApplicationRefusedDelete The application cannot be deleted because it has been deployed var ErrApplicationRefusedDelete = NewBcode(400, 10019, "The application cannot be deleted because it has been deployed") + +// ErrApplicationEnvRefusedDelete The application env cannot be deleted because it has been deployed +var ErrApplicationEnvRefusedDelete = NewBcode(400, 10020, "The application envbinding cannot be deleted because it has been deployed") diff --git a/pkg/apiserver/rest/utils/bcode/envbinding.go b/pkg/apiserver/rest/utils/bcode/envbinding.go index 6e4b7a3f4..0f40030ce 100644 --- a/pkg/apiserver/rest/utils/bcode/envbinding.go +++ b/pkg/apiserver/rest/utils/bcode/envbinding.go @@ -26,7 +26,7 @@ var ErrFoundEnvbindingDeliveryTarget = NewBcode(400, 90002, "found application e var ErrEnvBindingNotExist = NewBcode(400, 90003, "application envbinding not exist") // ErrEnvBindingsNotExist application envbindings is not exist -var ErrEnvBindingsNotExist = NewBcode(400, 90004, "application envbinding snot exist") +var ErrEnvBindingsNotExist = NewBcode(400, 90004, "application envbinding is not exist") // ErrEnvBindingExist application envbinding is exist var ErrEnvBindingExist = NewBcode(400, 90005, "application envbinding is exist") diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 94e0e9772..b665a5baf 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -955,19 +955,13 @@ func (c *applicationWebService) componentCheckFilter(req *restful.Request, res * func (c *applicationWebService) envCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - envBindings, err := c.envBindingUsecase.GetEnvBindings(req.Request.Context(), app) + envBinding, err := c.envBindingUsecase.GetEnvBinding(req.Request.Context(), app, req.PathParameter("envName")) if err != nil { bcode.ReturnError(req, res, err) return } - for _, env := range envBindings { - if env.Name == req.PathParameter("envName") { - req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplicationEnvBinding, env)) - chain.ProcessFilter(req, res) - return - } - } - bcode.ReturnError(req, res, bcode.ErrApplicationNotEnv) + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplicationEnvBinding, envBinding)) + chain.ProcessFilter(req, res) } func (c *applicationWebService) applicationStatistics(req *restful.Request, res *restful.Response) { diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index d09e427ec..7f2f638f3 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -45,17 +45,6 @@ func (w *workflowWebService) workflowCheckFilter(req *restful.Request, res *rest chain.ProcessFilter(req, res) } -func (w *workflowWebService) applicationCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { - app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) - workflow, err := w.workflowUsecase.GetWorkflow(req.Request.Context(), app, req.PathParameter("workflowName")) - if err != nil { - bcode.ReturnError(req, res, err) - return - } - req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyWorkflow, workflow)) - chain.ProcessFilter(req, res) -} - func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res *restful.Response) { app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) workflows, err := w.workflowUsecase.ListApplicationWorkflow(req.Request.Context(), app) @@ -189,7 +178,10 @@ func (w *workflowWebService) resumeWorkflowRecord(req *restful.Request, res *res bcode.ReturnError(req, res, err) return } - return + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } } func (w *workflowWebService) terminateWorkflowRecord(req *restful.Request, res *restful.Response) { @@ -200,7 +192,10 @@ func (w *workflowWebService) terminateWorkflowRecord(req *restful.Request, res * bcode.ReturnError(req, res, err) return } - return + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } } func (w *workflowWebService) rollbackWorkflowRecord(req *restful.Request, res *restful.Response) { @@ -211,5 +206,8 @@ func (w *workflowWebService) rollbackWorkflowRecord(req *restful.Request, res *r bcode.ReturnError(req, res, err) return } - return + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } } From 32103f53fc9830ee6450e7de5cb50979f743e15e Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Sun, 21 Nov 2021 10:45:03 +0800 Subject: [PATCH 53/59] Feat: add list application records api (#2757) * Feat: add list application records api * remove workflow checker * Update envbinding.go Co-authored-by: barnettZQG <576501057@qq.com> --- pkg/apiserver/model/workflow.go | 3 + pkg/apiserver/rest/usecase/application.go | 43 ++++- .../rest/usecase/application_test.go | 31 +++- pkg/apiserver/rest/usecase/envbinding.go | 6 +- .../rest/usecase/testdata/ui-schema.yaml | 152 +++++++++--------- pkg/apiserver/rest/usecase/workflow.go | 6 +- pkg/apiserver/rest/webservice/application.go | 20 +++ 7 files changed, 175 insertions(+), 86 deletions(-) diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index a7cd4bf26..ecdc8ef2a 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -127,5 +127,8 @@ func (w *WorkflowRecord) Index() map[string]string { if w.Finished != "" { index["finished"] = w.Finished } + if w.Status != "" { + index["status"] = w.Status + } return index } diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index b50e94b87..6b795d865 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -86,6 +86,7 @@ type ApplicationUsecase interface { ListRevisions(ctx context.Context, appName, envName, status string, page, pageSize int) (*apisv1.ListRevisionsResponse, error) DetailRevision(ctx context.Context, appName, revisionName string) (*apisv1.DetailRevisionResponse, error) Statistics(ctx context.Context, app *model.Application) (*apisv1.ApplicationStatisticsResponse, error) + ListRecords(ctx context.Context, appName string) (*apisv1.ListWorkflowRecordsResponse, error) } type applicationUsecaseImpl struct { @@ -197,7 +198,7 @@ func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *mod // GetApplicationStatus get application status from controller cluster func (c *applicationUsecaseImpl) GetApplicationStatus(ctx context.Context, appmodel *model.Application, envName string) (*common.AppStatus, error) { var app v1beta1.Application - err := c.kubeClient.Get(ctx, types.NamespacedName{Namespace: appmodel.Namespace, Name: converAppName(appmodel.Name, envName)}, &app) + err := c.kubeClient.Get(ctx, types.NamespacedName{Namespace: appmodel.Namespace, Name: convertAppName(appmodel.Name, envName)}, &app) if err != nil { if apierrors.IsNotFound(err) { return nil, nil @@ -383,6 +384,42 @@ func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, a return c.ds.BatchAdd(ctx, componentModels) } +// ListRecords list application record +func (c *applicationUsecaseImpl) ListRecords(ctx context.Context, appName string) (*apisv1.ListWorkflowRecordsResponse, error) { + var record = model.WorkflowRecord{ + AppPrimaryKey: appName, + Status: model.RevisionStatusRunning, + } + records, err := c.ds.List(ctx, &record, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + if len(records) == 0 { + record.Status = model.RevisionStatusComplete + records, err = c.ds.List(ctx, &record, &datastore.ListOptions{ + Page: 1, + PageSize: 1, + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + }) + if err != nil { + return nil, err + } + } + + resp := &apisv1.ListWorkflowRecordsResponse{ + Records: []apisv1.WorkflowRecord{}, + } + for _, raw := range records { + record, ok := raw.(*model.WorkflowRecord) + if ok { + resp.Records = append(resp.Records, *convertFromRecordModel(record)) + } + } + resp.Total = int64(len(records)) + + return resp, nil +} + func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.Application, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentBase, error) { var component = model.ApplicationComponent{ AppPrimaryKey: app.PrimaryKey(), @@ -655,7 +692,7 @@ func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appMo APIVersion: "core.oam.dev/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ - Name: converAppName(appModel.Name, workflow.EnvName), + Name: convertAppName(appModel.Name, workflow.EnvName), Namespace: appModel.Namespace, Labels: labels, Annotations: map[string]string{ @@ -1170,7 +1207,7 @@ func createTargetClusterEnv(envBind *model.EnvBinding, target *model.DeliveryTar } } -func converAppName(appModelName, envName string) string { +func convertAppName(appModelName, envName string) string { return fmt.Sprintf("%s-%s", appModelName, envName) } diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 3d3d38d5e..39e9d6393 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -435,12 +435,41 @@ var _ = Describe("Test application usecase function", func() { Expect(revision.Version).Should(Equal("123")) Expect(revision.DeployUser).Should(Equal("test-user")) }) + + It("Test ListRecords function", func() { + By("no running records in application") + ctx := context.TODO() + for i := 0; i < 2; i++ { + appUsecase.ds.Add(ctx, &model.WorkflowRecord{ + AppPrimaryKey: "app-records", + Name: fmt.Sprintf("list-%d", i), + Status: model.RevisionStatusComplete, + }) + } + + resp, err := appUsecase.ListRecords(context.TODO(), "app-records") + Expect(err).Should(BeNil()) + Expect(resp.Total).Should(Equal(int64(1))) + + By("3 running records in application") + for i := 0; i < 3; i++ { + appUsecase.ds.Add(ctx, &model.WorkflowRecord{ + AppPrimaryKey: "app-records", + Name: fmt.Sprintf("list-running-%d", i), + Status: model.RevisionStatusRunning, + }) + } + + resp, err = appUsecase.ListRecords(context.TODO(), "app-records") + Expect(err).Should(BeNil()) + Expect(resp.Total).Should(Equal(int64(3))) + }) }) func createTestSuspendApp(ctx context.Context, appName, envName, revisionVersion, wfName, recordName string, kubeClient client.Client) (*v1beta1.Application, error) { testapp := &v1beta1.Application{ ObjectMeta: metav1.ObjectMeta{ - Name: converAppName(appName, envName), + Name: convertAppName(appName, envName), Namespace: "default", Annotations: map[string]string{ oam.AnnotationDeployVersion: revisionVersion, diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go index 06dc0f12a..73c354d12 100644 --- a/pkg/apiserver/rest/usecase/envbinding.go +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -196,7 +196,7 @@ func (e *envBindingUsecaseImpl) DeleteEnvBinding(ctx context.Context, appModel * return err } var app v1beta1.Application - err = e.kubeClient.Get(ctx, types.NamespacedName{Namespace: appModel.Namespace, Name: converAppName(appModel.Name, envBinding.Name)}, &app) + err = e.kubeClient.Get(ctx, types.NamespacedName{Namespace: appModel.Namespace, Name: convertAppName(appModel.Name, envBinding.Name)}, &app) if err == nil || !apierrors.IsNotFound(err) { return bcode.ErrApplicationEnvRefusedDelete } @@ -282,7 +282,7 @@ func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, app *model func (e *envBindingUsecaseImpl) ApplicationEnvRecycle(ctx context.Context, appModel *model.Application, envBinding *model.EnvBinding) error { var app v1beta1.Application - err := e.kubeClient.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: converAppName(appModel.Name, envBinding.Name)}, &app) + err := e.kubeClient.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: convertAppName(appModel.Name, envBinding.Name)}, &app) if err != nil { if apierrors.IsNotFound(err) { return nil @@ -325,7 +325,7 @@ func convertEnvbindingModelToBase(app *model.Application, envBinding *model.EnvB ComponentSelector: (*apisv1.ComponentSelector)(envBinding.ComponentSelector), CreateTime: envBinding.CreateTime, UpdateTime: envBinding.UpdateTime, - AppDeployName: converAppName(app.Name, envBinding.Name), + AppDeployName: convertAppName(app.Name, envBinding.Name), } return ebb } diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index 650b814e0..8be6fab45 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -99,6 +99,20 @@ label: 持久化存储 sort: 11 subParameters: + - description: "" + jsonKey: mountPath + label: MountPath + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' jsonKey: type label: Type @@ -115,20 +129,6 @@ - label: EmptyDir value: emptyDir required: true - - description: "" - jsonKey: mountPath - label: MountPath - sort: 100 - uiType: Input - validate: - required: true - - description: "" - jsonKey: name - label: Name - sort: 100 - uiType: Input - validate: - required: true uiType: Structs validate: {} - description: Instructions for assessing whether the container is in a suitable state @@ -137,6 +137,32 @@ label: ReadinessProbe检测 sort: 13 subParameters: + - description: Number of seconds after the container is started before the first + probe is initiated. + jsonKey: initialDelaySeconds + label: InitialDelaySeconds + sort: 100 + uiType: Number + validate: + defaultValue: 0 + required: true + - description: How often, in seconds, to execute the probe. + jsonKey: periodSeconds + label: PeriodSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 10 + required: true + - description: Minimum consecutive successes for the probe to be considered successful + after having failed. + jsonKey: successThreshold + label: SuccessThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true - description: Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute @@ -200,29 +226,21 @@ label: HttpGet sort: 100 subParameters: - - description: The TCP socket within the container to which the HTTP GET request - should be directed. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - description: "" jsonKey: httpHeaders label: HttpHeaders sort: 100 subParameters: - description: "" - jsonKey: name - label: Name + jsonKey: value + label: Value sort: 100 uiType: Input validate: required: true - description: "" - jsonKey: value - label: Value + jsonKey: name + label: Name sort: 100 uiType: Input validate: @@ -237,34 +255,16 @@ uiType: Input validate: required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true uiType: KV validate: {} - - description: Number of seconds after the container is started before the first - probe is initiated. - jsonKey: initialDelaySeconds - label: InitialDelaySeconds - sort: 100 - uiType: Number - validate: - defaultValue: 0 - required: true - - description: How often, in seconds, to execute the probe. - jsonKey: periodSeconds - label: PeriodSeconds - sort: 100 - uiType: Number - validate: - defaultValue: 10 - required: true - - description: Minimum consecutive successes for the probe to be considered successful - after having failed. - jsonKey: successThreshold - label: SuccessThreshold - sort: 100 - uiType: Number - validate: - defaultValue: 1 - required: true uiType: Group validate: {} - description: Instructions for assessing whether the container is alive. @@ -272,24 +272,6 @@ label: LivenessProbe检测 sort: 15 subParameters: - - description: Instructions for assessing container health by probing a TCP socket. - Either this attribute or the exec attribute or the httpGet attribute MUST be - specified. This attribute is mutually exclusive with both the exec attribute - and the httpGet attribute. - jsonKey: tcpSocket - label: TcpSocket - sort: 100 - subParameters: - - description: The TCP socket within the container that should be probed to assess - container health. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - uiType: KV - validate: {} - description: Number of seconds after which the probe times out. jsonKey: timeoutSeconds label: TimeoutSeconds @@ -400,6 +382,24 @@ validate: defaultValue: 1 required: true + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} uiType: Group validate: {} - description: Specify image pull policy for your service @@ -416,12 +416,6 @@ value: Always - label: 永不更新 value: Never -- description: Specify image pull secrets for your service - jsonKey: imagePullSecrets - label: ImagePullSecrets - sort: 100 - uiType: Strings - validate: {} - description: If addRevisionLabel is true, the appRevision label will be added to the underlying pods jsonKey: addRevisionLabel @@ -431,3 +425,9 @@ validate: defaultValue: false required: true +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + sort: 100 + uiType: Strings + validate: {} diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 84ed9b920..67825df20 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -350,7 +350,7 @@ func (w *workflowUsecaseImpl) SyncWorkflowRecord(ctx context.Context) error { continue } if err := w.kubeClient.Get(ctx, types.NamespacedName{ - Name: converAppName(record.AppPrimaryKey, workflow.EnvName), + Name: convertAppName(record.AppPrimaryKey, workflow.EnvName), Namespace: record.Namespace, }, app); err != nil { klog.ErrorS(err, "failed to get app", "app name", record.AppPrimaryKey) @@ -513,7 +513,7 @@ func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *mode } revisions, err := w.ds.List(ctx, &revision, &datastore.ListOptions{ - Page: 0, + Page: 1, PageSize: 1, SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, }) @@ -578,7 +578,7 @@ func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *mode func (w *workflowUsecaseImpl) checkRecordRunning(ctx context.Context, appModel *model.Application, envName string) (*v1beta1.Application, error) { oamApp := &v1beta1.Application{} - if err := w.kubeClient.Get(ctx, types.NamespacedName{Name: converAppName(appModel.Name, envName), Namespace: appModel.Namespace}, oamApp); err != nil { + if err := w.kubeClient.Get(ctx, types.NamespacedName{Name: convertAppName(appModel.Name, envName), Namespace: appModel.Namespace}, oamApp); err != nil { return nil, err } if oamApp.Status.Workflow != nil && !oamApp.Status.Workflow.Suspend && !oamApp.Status.Workflow.Terminated && !oamApp.Status.Workflow.Finished { diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index b665a5baf..6aa04315a 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -465,6 +465,14 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(400, "", bcode.Bcode{}). Writes(apis.DetailWorkflowRecordResponse{})) + ws.Route(ws.GET("/{name}/records").To(c.listApplicationRecords). + Doc("list application records"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListWorkflowRecordsResponse{})) return ws } @@ -990,3 +998,15 @@ func (c *applicationWebService) recycleApplicationEnv(req *restful.Request, res return } } + +func (c *applicationWebService) listApplicationRecords(req *restful.Request, res *restful.Response) { + records, err := c.applicationUsecase.ListRecords(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(records); err != nil { + bcode.ReturnError(req, res, err) + return + } +} From 99ffe80eb4f08b013a98af49286d01643b88fbee Mon Sep 17 00:00:00 2001 From: yangsoon Date: Sun, 21 Nov 2021 14:56:01 +0800 Subject: [PATCH 54/59] Feat: component-pod-view support filter resource by cluster name and cluster namespace (#2754) --- .../velaql-views/component-pod-view.yaml | 35 +++-- docs/examples/velaql-views/usage.md | 12 +- pkg/stdlib/pkgs/query.cue | 6 +- pkg/velaql/providers/query/collector.go | 15 +- pkg/velaql/providers/query/handler.go | 17 +- .../testdata/component-pod-view.yaml | 146 ++++++++++-------- test/e2e-apiserver-test/velaql_test.go | 27 ++-- 7 files changed, 155 insertions(+), 103 deletions(-) diff --git a/charts/vela-core/templates/velaql-views/component-pod-view.yaml b/charts/vela-core/templates/velaql-views/component-pod-view.yaml index 6b2de40d3..e16ec99b8 100644 --- a/charts/vela-core/templates/velaql-views/component-pod-view.yaml +++ b/charts/vela-core/templates/velaql-views/component-pod-view.yaml @@ -11,26 +11,35 @@ data: ) parameter: { - name: string - namespace: string - componentName: string - cluster: *"" | string + appName: string + appNs: string + name: string + cluster?: string + clusterNs?: string } appList: ql.#ListResourcesInApp & { app: { - name: parameter.name - namespace: parameter.namespace - components: [parameter.componentName] - cluster: parameter.cluster + name: parameter.appName + namespace: parameter.appNs + components: [parameter.name] + filter: { + if parameter.cluster != _|_ { + cluster: parameter.cluster + } + if parameter.clusterNs != _|_ { + clusterNamespace: parameter.clusterNs + } + } } } if appList.err == _|_ { - appRev: appList.list[0].revision + appRev: appList.list[0].revision appPublishVersion: appList.list[0].publishVersion - resources: appList.list[0].components[0].resources - collectedPods: op.#Steps & { + appDeployVersion: appList.list[0].deployVersion + resources: appList.list[0].components[0].resources + collectedPods: op.#Steps & { for i, resource in resources { "\(i)": ql.#CollectPods & { value: resource.object @@ -49,7 +58,9 @@ data: clusterName: pod.cluster revision: appRev publishVersion: appPublishVersion + deployVersion: appDeployVersion podName: pod.obj.metadata.name + podNs: pod.obj.metadata.namespace status: pod.obj.status.phase // refer to https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase if status != "Pending" && status != "Unknown" { @@ -66,5 +77,3 @@ data: error: appList.err } } - - diff --git a/docs/examples/velaql-views/usage.md b/docs/examples/velaql-views/usage.md index dca9d764f..c39b69559 100644 --- a/docs/examples/velaql-views/usage.md +++ b/docs/examples/velaql-views/usage.md @@ -24,10 +24,11 @@ List the pods created by specified component ``` parameter: { - name: string // application name - namespace: string // application namespace - componentName: string // component name - cluster?: string // cluster name(Optional) + appName: string // application name + appNs: string // application namespace + name: string // component name + cluster?: string // cluster name(Optional) + clusterNs?: string // cluster namespace(Optional) } ``` @@ -45,6 +46,7 @@ status: { revision: string publishVersion: string podName: string + podNs: string status: string podIP: string hostIP: string @@ -61,7 +63,7 @@ status: { #### demo ```sql -component-pod-view{name=demo,namespace=default,cluster=prod,componentName=web}.status +component-pod-view{appName=demo,appNs=default,cluster=prod,clusterNs=default,name=web}.status ``` ### pod-view diff --git a/pkg/stdlib/pkgs/query.cue b/pkg/stdlib/pkgs/query.cue index 1e8112f0e..4f2c708c9 100644 --- a/pkg/stdlib/pkgs/query.cue +++ b/pkg/stdlib/pkgs/query.cue @@ -5,7 +5,11 @@ name: string namespace: string components?: [...string] - cluster?: string + filter?: { + cluster?: string + clusterNamespace?: string + } + clusterNamespace?: string enableHistoryQuery?: bool } ... diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go index c2cafc7e1..b52a32b19 100644 --- a/pkg/velaql/providers/query/collector.go +++ b/pkg/velaql/providers/query/collector.go @@ -34,6 +34,7 @@ import ( "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/dispatch" "github.com/oam-dev/kubevela/pkg/multicluster" @@ -77,11 +78,12 @@ func (c *AppCollector) CollectLatestResourceFromApp() ([]AppResources, error) { revision = app.Status.LatestRevision.Revision } publishVersion := app.GetAnnotations()[oam.AnnotationPublishVersion] + deployVersion := app.GetAnnotations()[oam.AnnotationDeployVersion] appRevName := fmt.Sprintf("%s-v%d", app.Name, revision) comps := make(map[string][]Resource, len(app.Spec.Components)) for _, rsrcRef := range app.Status.AppliedResources { - if c.opt.Cluster != "" && c.opt.Cluster != rsrcRef.Cluster { + if !isTargetResource(c.opt.Filter, rsrcRef) { continue } compName, obj, err := getObjectCreatedByComponent(c.k8sClient, rsrcRef.ObjectReference, rsrcRef.Cluster, appRevName) @@ -106,6 +108,7 @@ func (c *AppCollector) CollectLatestResourceFromApp() ([]AppResources, error) { Metadata: app.ObjectMeta, Components: compResList, PublishVersion: publishVersion, + DeployVersion: deployVersion, }}, nil } @@ -413,3 +416,13 @@ func getEventFieldSelector(obj *unstructured.Unstructured) fields.Selector { field["involvedObject.uid"] = string(obj.GetUID()) return field.AsSelector() } + +func isTargetResource(opt ClusterFilter, resource common.ClusterObjectReference) bool { + if opt.Cluster == "" && opt.ClusterNamespace == "" { + return true + } + if opt.Cluster == resource.Cluster && opt.ClusterNamespace == resource.ObjectReference.Namespace { + return true + } + return false +} diff --git a/pkg/velaql/providers/query/handler.go b/pkg/velaql/providers/query/handler.go index 3ac094795..e41fa1a09 100644 --- a/pkg/velaql/providers/query/handler.go +++ b/pkg/velaql/providers/query/handler.go @@ -45,6 +45,7 @@ type provider struct { type AppResources struct { Revision int64 `json:"revision"` PublishVersion string `json:"publishVersion"` + DeployVersion string `json:"deployVersion"` Metadata metav1.ObjectMeta `json:"metadata"` Components []Component `json:"components"` } @@ -63,11 +64,17 @@ type Resource struct { // Option is the query option type Option struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Components []string `json:"components,omitempty"` - Cluster string `json:"cluster,omitempty"` - EnableHistoryQuery bool `json:"enableHistoryQuery,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Components []string `json:"components,omitempty"` + Filter ClusterFilter `json:"filter,omitempty"` + EnableHistoryQuery bool `json:"enableHistoryQuery,omitempty"` +} + +// ClusterFilter filter resource created by component +type ClusterFilter struct { + Cluster string `json:"cluster,omitempty"` + ClusterNamespace string `json:"clusterNamespace,omitempty"` } // ListResourcesInApp lists CRs created by Application diff --git a/test/e2e-apiserver-test/testdata/component-pod-view.yaml b/test/e2e-apiserver-test/testdata/component-pod-view.yaml index fc8b431f0..290547d2d 100644 --- a/test/e2e-apiserver-test/testdata/component-pod-view.yaml +++ b/test/e2e-apiserver-test/testdata/component-pod-view.yaml @@ -1,81 +1,91 @@ apiVersion: v1 kind: ConfigMap metadata: - name: test-component-pod-view - namespace: vela-system + name: test-component-pod-view + namespace: vela-system data: - template: | - import ( - "vela/ql" - "vela/op" - ) + template: | + import ( + "vela/ql" + "vela/op" + ) - parameter: { - name: string - namespace: string - componentName: string - } - - application: ql.#ListResourcesInApp & { - app: { - name: parameter.name - namespace: parameter.namespace - components: [parameter.componentName] - } - } - - app: application.list[0] - resources: app.components[0].resources - - podsMap: op.#Steps & { - for i, resource in resources { - "\(i)": ql.#CollectPods & { - value: resource.object - cluster: resource.cluster + parameter: { + appName: string + appNs: string + name: string + cluster?: string + clusterNs?: string } - } - } - podsWithCluster: [ for i, pods in podsMap for podObj in pods.list { - cluster: pods.cluster - obj: podObj - }] - - podStatus: op.#Steps & { - for i, pod in podsWithCluster { - "\(i)": op.#Steps & { - name: pod.obj.metadata.name - containers: {for container in pod.obj.status.containerStatuses { - "\(container.name)": { - image: container.image - state: container.state - } - }} - events: ql.#SearchEvents & { - value: pod.obj - cluster: pod.cluster - } - metrics: ql.#Read & { - cluster: pod.cluster - value: { - apiVersion: "metrics.k8s.io/v1beta1" - kind: "PodMetrics" - metadata: { - name: pod.obj.metadata.name - namespace: pod.obj.metadata.namespace + application: ql.#ListResourcesInApp & { + app: { + name: parameter.appName + namespace: parameter.appNs + components: [parameter.name] + filter: { + if parameter.cluster != _|_ { + cluster: parameter.cluster + } + if parameter.clusterNs != _|_ { + clusterNamespace: parameter.clusterNs } } } } - } - } - status: { - podList: [ for podInfo in podStatus { - name: podInfo.name - containers: [ for containerName, container in podInfo.containers { - containerName + app: application.list[0] + resources: app.components[0].resources + + podsMap: op.#Steps & { + for i, resource in resources { + "\(i)": ql.#CollectPods & { + value: resource.object + cluster: resource.cluster + } + } + } + + podsWithCluster: [ for i, pods in podsMap for podObj in pods.list { + cluster: pods.cluster + obj: podObj }] - events: podInfo.events.list - }] - } + + podStatus: op.#Steps & { + for i, pod in podsWithCluster { + "\(i)": op.#Steps & { + name: pod.obj.metadata.name + containers: {for container in pod.obj.status.containerStatuses { + "\(container.name)": { + image: container.image + state: container.state + } + }} + events: ql.#SearchEvents & { + value: pod.obj + cluster: pod.cluster + } + metrics: ql.#Read & { + cluster: pod.cluster + value: { + apiVersion: "metrics.k8s.io/v1beta1" + kind: "PodMetrics" + metadata: { + name: pod.obj.metadata.name + namespace: pod.obj.metadata.namespace + } + } + } + } + } + } + + status: { + podList: [ for podInfo in podStatus { + name: podInfo.name + containers: [ for containerName, container in podInfo.containers { + containerName + }] + events: podInfo.events.list + }] + } diff --git a/test/e2e-apiserver-test/velaql_test.go b/test/e2e-apiserver-test/velaql_test.go index 964bbcfa7..8bb475e48 100644 --- a/test/e2e-apiserver-test/velaql_test.go +++ b/test/e2e-apiserver-test/velaql_test.go @@ -83,8 +83,8 @@ var _ = Describe("Test velaQL rest api", func() { if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, oldApp); err != nil { return err } - if oldApp.Status.Phase != common2.ApplicationRunning { - return errors.New("application is not ready") + if len(oldApp.Status.AppliedResources) != 2 { + return errors.Errorf("expect the applied resources number is %d, but get %d", 2, len(oldApp.Status.AppliedResources)) } return nil }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) @@ -124,14 +124,14 @@ var _ = Describe("Test velaQL rest api", func() { if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, oldApp); err != nil { return err } - if oldApp.Status.Phase != common2.ApplicationRunning { - return errors.New("application is not ready") + if len(oldApp.Status.AppliedResources) != 2 { + return errors.Errorf("expect the applied resources number is %d, but get %d", 2, len(oldApp.Status.AppliedResources)) } return nil }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) queryRes, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "test-component-pod-view", appName, namespace, component1Name, "status"), + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{appName=%s,appNs=%s,name=%s}.%s", "test-component-pod-view", appName, namespace, component1Name, "status"), ) Expect(err).Should(BeNil()) Expect(queryRes.StatusCode).Should(Equal(200)) @@ -145,7 +145,7 @@ var _ = Describe("Test velaQL rest api", func() { Eventually(func() error { queryRes1, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "test-component-pod-view", appName, namespace, component2Name, "status"), + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{appName=%s,appNs=%s,name=%s}.%s", "test-component-pod-view", appName, namespace, component2Name, "status"), ) if err != nil { return err @@ -195,8 +195,15 @@ var _ = Describe("Test velaQL rest api", func() { if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(oldApp), newApp); err != nil { return err } - if newApp.Status.Phase != common2.ApplicationRunning { - return errors.New("application is not ready") + appliedCronJob := false + for _, resource := range newApp.Status.AppliedResources { + if resource.ObjectReference.Kind == "CronJob" { + appliedCronJob = true + break + } + } + if !appliedCronJob { + return errors.New("fail to apply cronjob") } return nil }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) @@ -208,7 +215,7 @@ var _ = Describe("Test velaQL rest api", func() { Eventually(func() error { queryRes, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "test-component-pod-view", appName, namespace, component2Name, "status"), + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{appName=%s,appNs=%s,name=%s}.%s", "test-component-pod-view", appName, namespace, component2Name, "status"), ) if err != nil { return err @@ -259,7 +266,7 @@ var _ = Describe("Test velaQL rest api", func() { Eventually(func() error { queryRes, err := http.Get( - fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s,componentName=%s}.%s", "component-pod-view", appWithHelm.Name, namespace, "podinfo", "status"), + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{appName=%s,appNs=%s,name=%s}.%s", "component-pod-view", appWithHelm.Name, namespace, "podinfo", "status"), ) if err != nil { return err From 4799cbf6ccd496cc1719811837dc567f66df38a5 Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Sun, 21 Nov 2021 16:43:10 +0800 Subject: [PATCH 55/59] Feat: workflow support update (#2760) * Feat: workflow support update * Fix: fix recycle bug Co-authored-by: barnettZQG --- pkg/apiserver/rest/usecase/envbinding.go | 4 +- .../rest/usecase/testdata/ui-schema.yaml | 142 +++++++++--------- pkg/apiserver/rest/usecase/workflow.go | 42 ++++-- pkg/apiserver/rest/usecase/workflow_test.go | 24 ++- pkg/apiserver/rest/webservice/application.go | 2 +- pkg/apiserver/rest/webservice/workflow.go | 10 +- 6 files changed, 124 insertions(+), 100 deletions(-) diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go index 73c354d12..f2fdbecea 100644 --- a/pkg/apiserver/rest/usecase/envbinding.go +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -252,7 +252,7 @@ func (e *envBindingUsecaseImpl) createEnvWorkflow(ctx context.Context, app *mode Outputs: step.Outputs, }) } - _, err := e.workflowUsecase.CreateWorkflow(ctx, app, apisv1.CreateWorkflowRequest{ + _, err := e.workflowUsecase.CreateOrUpdateWorkflow(ctx, app, apisv1.CreateWorkflowRequest{ AppName: app.PrimaryKey(), Name: env.Name, Alias: fmt.Sprintf("%s env workflow", env.Alias), @@ -282,7 +282,7 @@ func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, app *model func (e *envBindingUsecaseImpl) ApplicationEnvRecycle(ctx context.Context, appModel *model.Application, envBinding *model.EnvBinding) error { var app v1beta1.Application - err := e.kubeClient.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: convertAppName(appModel.Name, envBinding.Name)}, &app) + err := e.kubeClient.Get(ctx, types.NamespacedName{Namespace: appModel.Namespace, Name: convertAppName(appModel.Name, envBinding.Name)}, &app) if err != nil { if apierrors.IsNotFound(err) { return nil diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index 8be6fab45..60e7eeace 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -40,19 +40,6 @@ - valueFrom label: Add By Secret subParameters: - - description: Environment variable name - jsonKey: name - label: Name - sort: 100 - uiType: Input - validate: - required: true - - description: The value of the environment variable - jsonKey: value - label: Value - sort: 100 - uiType: Input - validate: {} - description: Specifies a source the value of this var should come from disable: false jsonKey: valueFrom @@ -84,6 +71,19 @@ required: true uiType: InnerGroup validate: {} + - description: Environment variable name + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + - description: The value of the environment variable + jsonKey: value + label: Value + sort: 100 + uiType: Input + validate: {} uiType: Structs validate: {} - description: Which port do you want customer traffic sent to @@ -99,20 +99,6 @@ label: 持久化存储 sort: 11 subParameters: - - description: "" - jsonKey: mountPath - label: MountPath - sort: 100 - uiType: Input - validate: - required: true - - description: "" - jsonKey: name - label: Name - sort: 100 - uiType: Input - validate: - required: true - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' jsonKey: type label: Type @@ -129,6 +115,20 @@ - label: EmptyDir value: emptyDir required: true + - description: "" + jsonKey: mountPath + label: MountPath + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true uiType: Structs validate: {} - description: Instructions for assessing whether the container is in a suitable state @@ -232,15 +232,15 @@ sort: 100 subParameters: - description: "" - jsonKey: value - label: Value + jsonKey: name + label: Name sort: 100 uiType: Input validate: required: true - description: "" - jsonKey: name - label: Name + jsonKey: value + label: Value sort: 100 uiType: Input validate: @@ -272,6 +272,24 @@ label: LivenessProbe检测 sort: 15 subParameters: + - description: Instructions for assessing container health by probing a TCP socket. + Either this attribute or the exec attribute or the httpGet attribute MUST be + specified. This attribute is mutually exclusive with both the exec attribute + and the httpGet attribute. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} - description: Number of seconds after which the probe times out. jsonKey: timeoutSeconds label: TimeoutSeconds @@ -317,6 +335,22 @@ label: HttpGet sort: 100 subParameters: + - description: The endpoint, relative to the port, to which the HTTP GET request + should be directed. + jsonKey: path + label: Path + sort: 100 + uiType: Input + validate: + required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true - description: "" jsonKey: httpHeaders label: HttpHeaders @@ -338,22 +372,6 @@ required: true uiType: Structs validate: {} - - description: The endpoint, relative to the port, to which the HTTP GET request - should be directed. - jsonKey: path - label: Path - sort: 100 - uiType: Input - validate: - required: true - - description: The TCP socket within the container to which the HTTP GET request - should be directed. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true uiType: KV validate: {} - description: Number of seconds after the container is started before the first @@ -382,24 +400,6 @@ validate: defaultValue: 1 required: true - - description: Instructions for assessing container health by probing a TCP socket. - Either this attribute or the exec attribute or the httpGet attribute MUST be - specified. This attribute is mutually exclusive with both the exec attribute - and the httpGet attribute. - jsonKey: tcpSocket - label: TcpSocket - sort: 100 - subParameters: - - description: The TCP socket within the container that should be probed to assess - container health. - jsonKey: port - label: Port - sort: 100 - uiType: Number - validate: - required: true - uiType: KV - validate: {} uiType: Group validate: {} - description: Specify image pull policy for your service @@ -416,6 +416,12 @@ value: Always - label: 永不更新 value: Never +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + sort: 100 + uiType: Strings + validate: {} - description: If addRevisionLabel is true, the appRevision label will be added to the underlying pods jsonKey: addRevisionLabel @@ -425,9 +431,3 @@ validate: defaultValue: false required: true -- description: Specify image pull secrets for your service - jsonKey: imagePullSecrets - label: ImagePullSecrets - sort: 100 - uiType: Strings - validate: {} diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go index 67825df20..2d2336cf3 100644 --- a/pkg/apiserver/rest/usecase/workflow.go +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -50,7 +50,7 @@ type WorkflowUsecase interface { GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) DeleteWorkflow(ctx context.Context, app *model.Application, workflowName string) error DeleteWorkflowByApp(ctx context.Context, app *model.Application) error - CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + CreateOrUpdateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) CreateWorkflowRecord(ctx context.Context, appModel *model.Application, app *v1beta1.Application, workflow *model.Workflow) error ListWorkflowRecords(ctx context.Context, workflow *model.Workflow, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) @@ -116,10 +116,14 @@ func (w *workflowUsecaseImpl) DeleteWorkflowByApp(ctx context.Context, app *mode return nil } -func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { +func (w *workflowUsecaseImpl) CreateOrUpdateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { if req.EnvName == "" { return nil, bcode.ErrWorkflowNoEnv } + workflow, err := w.GetWorkflow(ctx, app, req.Name) + if err != nil && errors.Is(err, datastore.ErrRecordNotExist) { + return nil, err + } var steps []model.WorkflowStep for _, step := range req.Steps { properties, err := model.NewJSONStructByString(step.Properties) @@ -137,19 +141,29 @@ func (w *workflowUsecaseImpl) CreateWorkflow(ctx context.Context, app *model.App Properties: properties, }) } - // It is allowed to set multiple workflows as default, and only one takes effect. - var workflow = model.Workflow{ - Steps: steps, - Name: req.Name, - Description: req.Description, - Default: req.Default, - EnvName: req.EnvName, - AppPrimaryKey: app.PrimaryKey(), + if workflow != nil { + workflow.Steps = steps + workflow.Alias = req.Alias + workflow.Description = req.Description + workflow.Default = req.Default + if err := w.ds.Put(ctx, workflow); err != nil { + return nil, err + } + } else { + // It is allowed to set multiple workflows as default, and only one takes effect. + workflow = &model.Workflow{ + Steps: steps, + Name: req.Name, + Description: req.Description, + Default: req.Default, + EnvName: req.EnvName, + AppPrimaryKey: app.PrimaryKey(), + } + if err := w.ds.Add(ctx, workflow); err != nil { + return nil, err + } } - if err := w.ds.Add(ctx, &workflow); err != nil { - return nil, err - } - return w.DetailWorkflow(ctx, &workflow) + return w.DetailWorkflow(ctx, workflow) } func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go index 799b0bab4..076c06043 100644 --- a/pkg/apiserver/rest/usecase/workflow_test.go +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -70,20 +70,34 @@ var _ = Describe("Test workflow usecase functions", func() { EnvName: "dev", } - base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + base, err := workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ Name: appName, Namespace: "default", }, req) Expect(err).Should(BeNil()) Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + req2 := apisv1.CreateWorkflowRequest{ + Name: "test-workflow-1", + Description: "change description", + EnvName: "dev2", + } + + base, err = workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req2) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req2.Description)).Should(BeEmpty()) + Expect(cmp.Diff(base.EnvName, req2.EnvName)).ShouldNot(BeEmpty()) + req = apisv1.CreateWorkflowRequest{ Name: "test-workflow-2", Description: "this is test workflow", EnvName: "dev", Default: true, } - base, err = workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + base, err = workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ Name: appName, Namespace: "default", }, req) @@ -283,7 +297,7 @@ var _ = Describe("Test workflow usecase functions", func() { EnvName: "resume", } - base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + base, err := workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ Name: appName, Namespace: "default", }, req) @@ -327,7 +341,7 @@ var _ = Describe("Test workflow usecase functions", func() { EnvName: "terminate", } workflow := &model.Workflow{Name: workflowName, EnvName: "terminate"} - base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + base, err := workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ Name: appName, Namespace: "default", }, req) @@ -371,7 +385,7 @@ var _ = Describe("Test workflow usecase functions", func() { EnvName: "rollback", } workflow := &model.Workflow{Name: workflowName, EnvName: "rollback"} - base, err := workflowUsecase.CreateWorkflow(context.TODO(), &model.Application{ + base, err := workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ Name: appName, Namespace: "default", }, req) diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 6aa04315a..cc3ab60cf 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -363,7 +363,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Returns(200, "", apis.ListWorkflowResponse{}). Writes(apis.ListWorkflowResponse{}).Do(returns200, returns500)) - ws.Route(ws.POST("/{name}/workflows").To(c.createApplicationWorkflow). + ws.Route(ws.POST("/{name}/workflows").To(c.createOrUpdateApplicationWorkflow). Doc("create application workflow"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateWorkflowRequest{}). diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 7f2f638f3..60f48b56a 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -58,7 +58,7 @@ func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res } } -func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res *restful.Response) { +func (w *workflowWebService) createOrUpdateApplicationWorkflow(req *restful.Request, res *restful.Response) { // Verify the validity of parameters var createReq apis.CreateWorkflowRequest if err := req.ReadEntity(&createReq); err != nil { @@ -69,13 +69,9 @@ func (w *workflowWebService) createApplicationWorkflow(req *restful.Request, res bcode.ReturnError(req, res, err) return } - app, err := w.applicationUsecase.GetApplication(req.Request.Context(), createReq.AppName) - if err != nil { - bcode.ReturnError(req, res, err) - return - } + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) // Call the usecase layer code - workflowDetail, err := w.workflowUsecase.CreateWorkflow(req.Request.Context(), app, createReq) + workflowDetail, err := w.workflowUsecase.CreateOrUpdateWorkflow(req.Request.Context(), app, createReq) if err != nil { log.Logger.Errorf("create application failure %s", err.Error()) bcode.ReturnError(req, res, err) From 51b6e8b459e3b7e3bf2b7dbb8987a10a535bcbb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=99=93=E5=85=B5?= <596908030@qq.com> Date: Sun, 21 Nov 2021 17:27:35 +0800 Subject: [PATCH 56/59] =?UTF-8?q?Fix:=20fix=20envbinding=20related=20workf?= =?UTF-8?q?low=20logic=EF=BC=8Cadd=20unit=20test=20(#2758)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: fix envbinding related workflow logic,add unit test * Fix: bcode field * Update envbinding.go Co-authored-by: zhuxiaobing Co-authored-by: barnettZQG <576501057@qq.com> --- .../rest/usecase/application_test.go | 19 ++- pkg/apiserver/rest/usecase/envbinding.go | 129 +++++++++++------ pkg/apiserver/rest/usecase/envbinding_test.go | 137 ++++++++++++++++++ pkg/apiserver/rest/utils/bcode/envbinding.go | 3 + 4 files changed, 244 insertions(+), 44 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/envbinding_test.go diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index 39e9d6393..5cbdbe44a 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -48,7 +48,7 @@ var _ = Describe("Test application usecase function", func() { ) BeforeEach(func() { workflowUsecase = &workflowUsecaseImpl{ds: ds} - envBindingUsecase = &envBindingUsecaseImpl{ds: ds, workflowUsecase: workflowUsecase} + envBindingUsecase = &envBindingUsecaseImpl{ds: ds, workflowUsecase: workflowUsecase, kubeClient: k8sClient} deliveryTargetUsecase = &deliveryTargetUsecaseImpl{ds: ds} appUsecase = &applicationUsecaseImpl{ ds: ds, @@ -436,6 +436,23 @@ var _ = Describe("Test application usecase function", func() { Expect(revision.DeployUser).Should(Equal("test-user")) }) + It("Test ApplicationEnvRecycle function", func() { + req := v1.CreateApplicationRequest{ + Name: "app-env-recycle" + "-dev", + Namespace: "test-app-namespace", + Description: "this is a test app with env", + } + base, err := appUsecase.CreateApplication(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) + + err = envBindingUsecase.ApplicationEnvRecycle(context.TODO(), &model.Application{ + Name: "app-env-recycle", + Namespace: "test-app-namespace", + }, &model.EnvBinding{Name: "dev"}) + Expect(err).Should(BeNil()) + }) + It("Test ListRecords function", func() { By("no running records in application") ctx := context.TODO() diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go index f2fdbecea..1ff122f2d 100644 --- a/pkg/apiserver/rest/usecase/envbinding.go +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -172,19 +172,16 @@ func (e *envBindingUsecaseImpl) UpdateEnvBinding(ctx context.Context, app *model } return nil, err } - envBindingModel := &model.EnvBinding{ - Name: envBinding.Name, - Alias: envUpdate.Alias, - Description: envUpdate.Description, - TargetNames: envUpdate.TargetNames, - } - if envBinding.ComponentSelector != nil { - envBindingModel.ComponentSelector = envBinding.ComponentSelector - } - if err := e.ds.Put(ctx, envBindingModel); err != nil { + convertUpdateReqToEnvBindingModel(envBinding, envUpdate) + //update env + if err := e.ds.Put(ctx, envBinding); err != nil { return nil, err } - return e.DetailEnvBinding(ctx, app, envBindingModel) + //update env workflow + if err := e.updateEnvWorkflow(ctx, app, envBinding); err != nil { + return nil, bcode.ErrEnvBindingUpdateWorkflow + } + return e.DetailEnvBinding(ctx, app, envBinding) } func (e *envBindingUsecaseImpl) DeleteEnvBinding(ctx context.Context, appModel *model.Application, envName string) error { @@ -203,6 +200,10 @@ func (e *envBindingUsecaseImpl) DeleteEnvBinding(ctx context.Context, appModel * if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: appModel.PrimaryKey(), Name: envBinding.Name}); err != nil { return err } + //delete env workflow + if err := e.deleteEnvWorkflow(ctx, appModel, envBinding.Name); err != nil { + return err + } return nil } @@ -212,46 +213,21 @@ func (e *envBindingUsecaseImpl) BatchDeleteEnvBinding(ctx context.Context, app * return err } for _, envBinding := range envBindings { + //delete env if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: app.PrimaryKey(), Name: envBinding.Name}); err != nil { return err } + //delete env workflow + err := e.deleteEnvWorkflow(ctx, app, envBinding.Name) + if err != nil { + return err + } } return nil } func (e *envBindingUsecaseImpl) createEnvWorkflow(ctx context.Context, app *model.Application, env *model.EnvBinding) error { - var workflowSteps []v1beta1.WorkflowStep - for _, targetName := range env.TargetNames { - step := v1beta1.WorkflowStep{ - Name: genPolicyEnvName(targetName), - Type: "deploy2env", - Properties: util.Object2RawExtension(map[string]string{ - "policy": genPolicyName(env.Name), - "env": genPolicyEnvName(targetName), - }), - } - workflowSteps = append(workflowSteps, step) - } - var steps []apisv1.WorkflowStep - for _, step := range workflowSteps { - var propertyStr string - if step.Properties != nil { - properties, err := model.NewJSONStruct(step.Properties) - if err != nil { - log.Logger.Errorf("workflow %s step %s properties is invalid %s", app.Name, step.Name, err.Error()) - continue - } - propertyStr = properties.JSON() - } - steps = append(steps, apisv1.WorkflowStep{ - Name: step.Name, - Type: step.Type, - DependsOn: step.DependsOn, - Properties: propertyStr, - Inputs: step.Inputs, - Outputs: step.Outputs, - }) - } + steps := genEnvWorkflowSteps(env, app) _, err := e.workflowUsecase.CreateOrUpdateWorkflow(ctx, app, apisv1.CreateWorkflowRequest{ AppName: app.PrimaryKey(), Name: env.Name, @@ -267,6 +243,27 @@ func (e *envBindingUsecaseImpl) createEnvWorkflow(ctx context.Context, app *mode return nil } +func (e *envBindingUsecaseImpl) updateEnvWorkflow(ctx context.Context, app *model.Application, env *model.EnvBinding) error { + steps := genEnvWorkflowSteps(env, app) + workflow, err := e.workflowUsecase.GetWorkflow(ctx, app, env.Name) + if err != nil { + return err + } + _, err = e.workflowUsecase.UpdateWorkflow(ctx, workflow, apisv1.UpdateWorkflowRequest{ + Steps: steps, + Description: workflow.Description, + EnvName: workflow.EnvName, + }) + if err != nil { + return err + } + return nil +} + +func (e *envBindingUsecaseImpl) deleteEnvWorkflow(ctx context.Context, app *model.Application, workflowName string) error { + return e.workflowUsecase.DeleteWorkflow(ctx, app, workflowName) +} + func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, app *model.Application, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) { deliveryTarget := model.DeliveryTarget{ Namespace: app.Namespace, @@ -330,6 +327,16 @@ func convertEnvbindingModelToBase(app *model.Application, envBinding *model.EnvB return ebb } +func convertUpdateReqToEnvBindingModel(envBinding *model.EnvBinding, envUpdate apisv1.PutApplicationEnvRequest) *model.EnvBinding { + envBinding.Alias = envUpdate.Alias + envBinding.Description = envUpdate.Description + envBinding.TargetNames = envUpdate.TargetNames + if envUpdate.ComponentSelector != nil { + envBinding.ComponentSelector = (*model.ComponentSelector)(envUpdate.ComponentSelector) + } + return envBinding +} + func convertToEnvBindingModel(app *model.Application, envBind apisv1.EnvBinding) *model.EnvBinding { re := model.EnvBinding{ AppPrimaryKey: app.Name, @@ -343,3 +350,39 @@ func convertToEnvBindingModel(app *model.Application, envBind apisv1.EnvBinding) } return &re } + +func genEnvWorkflowSteps(env *model.EnvBinding, app *model.Application) []apisv1.WorkflowStep { + var workflowSteps []v1beta1.WorkflowStep + for _, targetName := range env.TargetNames { + step := v1beta1.WorkflowStep{ + Name: genPolicyEnvName(targetName), + Type: "deploy2env", + Properties: util.Object2RawExtension(map[string]string{ + "policy": genPolicyName(env.Name), + "env": genPolicyEnvName(targetName), + }), + } + workflowSteps = append(workflowSteps, step) + } + var steps []apisv1.WorkflowStep + for _, step := range workflowSteps { + var propertyStr string + if step.Properties != nil { + properties, err := model.NewJSONStruct(step.Properties) + if err != nil { + log.Logger.Errorf("workflow %s step %s properties is invalid %s", app.Name, step.Name, err.Error()) + continue + } + propertyStr = properties.JSON() + } + steps = append(steps, apisv1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + DependsOn: step.DependsOn, + Properties: propertyStr, + Inputs: step.Inputs, + Outputs: step.Outputs, + }) + } + return steps +} diff --git a/pkg/apiserver/rest/usecase/envbinding_test.go b/pkg/apiserver/rest/usecase/envbinding_test.go new file mode 100644 index 000000000..0e8c1c29a --- /dev/null +++ b/pkg/apiserver/rest/usecase/envbinding_test.go @@ -0,0 +1,137 @@ +/* +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" + "github.com/google/go-cmp/cmp" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test envBindingUsecase functions", func() { + var ( + envBindingUsecase *envBindingUsecaseImpl + workflowUsecase *workflowUsecaseImpl + envBindingDemo1 apisv1.EnvBinding + envBindingDemo2 apisv1.EnvBinding + testApp *model.Application + ) + BeforeEach(func() { + testApp = &model.Application{ + Name: "test-app-env", + Namespace: "default", + } + workflowUsecase = &workflowUsecaseImpl{ds: ds, kubeClient: k8sClient} + envBindingUsecase = &envBindingUsecaseImpl{ds: ds, workflowUsecase: workflowUsecase, kubeClient: k8sClient} + envBindingDemo1 = apisv1.EnvBinding{ + Name: "dev", + Alias: "dev alias", + TargetNames: []string{"dev-target"}, + } + envBindingDemo2 = apisv1.EnvBinding{ + Name: "prod", + Alias: "prod alias", + TargetNames: []string{"prod-target"}, + } + }) + + It("Test Create Application Env function", func() { + By("create two envbinding") + req := apisv1.CreateApplicationEnvRequest{EnvBinding: envBindingDemo1} + base, err := envBindingUsecase.CreateEnvBinding(context.TODO(), testApp, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + req = apisv1.CreateApplicationEnvRequest{EnvBinding: envBindingDemo2} + base, err = envBindingUsecase.CreateEnvBinding(context.TODO(), testApp, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + By("auto create two workflow") + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), testApp, "dev") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(workflow.Steps[0].Name, "dev-target")).Should(BeEmpty()) + + workflow, err = workflowUsecase.GetWorkflow(context.TODO(), testApp, "prod") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(workflow.Steps[0].Name, "prod-target")).Should(BeEmpty()) + }) + + It("Test GetApplication Envs function", func() { + envBindings, err := envBindingUsecase.GetEnvBindings(context.TODO(), testApp) + Expect(err).Should(BeNil()) + Expect(envBindings).ShouldNot(BeNil()) + Expect(cmp.Diff(len(envBindings), 2)).Should(BeEmpty()) + }) + + It("Test GetApplication Env function", func() { + envBinding, err := envBindingUsecase.GetEnvBinding(context.TODO(), testApp, "dev") + Expect(err).Should(BeNil()) + Expect(envBinding).ShouldNot(BeNil()) + Expect(cmp.Diff(envBinding.Name, "dev")).Should(BeEmpty()) + }) + + It("Test CheckAppEnvBindingsContainTarget function", func() { + isContain, err := envBindingUsecase.CheckAppEnvBindingsContainTarget(context.TODO(), testApp, "dev-target") + Expect(err).Should(BeNil()) + Expect(isContain).ShouldNot(BeNil()) + Expect(cmp.Diff(isContain, true)).Should(BeEmpty()) + }) + + It("Test Application UpdateEnv function", func() { + envBinding, err := envBindingUsecase.UpdateEnvBinding(context.TODO(), testApp, "prod", apisv1.PutApplicationEnvRequest{ + TargetNames: []string{"prod-target-new1"}, + }) + + Expect(envBinding).ShouldNot(BeNil()) + Expect(cmp.Diff(envBinding.TargetNames[0], "prod-target-new1")).Should(BeEmpty()) + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), testApp, "prod") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(workflow.Steps[0].Name, "prod-target-new1")).Should(BeEmpty()) + }) + + It("Test Application DeleteEnv function", func() { + err := envBindingUsecase.DeleteEnvBinding(context.TODO(), testApp, "dev") + Expect(err).Should(BeNil()) + _, err = workflowUsecase.GetWorkflow(context.TODO(), testApp, "dev") + Expect(err).ShouldNot(BeNil()) + err = envBindingUsecase.DeleteEnvBinding(context.TODO(), testApp, "prod") + Expect(err).Should(BeNil()) + _, err = workflowUsecase.GetWorkflow(context.TODO(), testApp, "prod") + Expect(err).ShouldNot(BeNil()) + }) + + It("Test Application BatchCreateEnv function", func() { + err := envBindingUsecase.BatchCreateEnvBinding(context.TODO(), testApp, apisv1.EnvBindingList{&envBindingDemo1, &envBindingDemo2}) + Expect(err).Should(BeNil()) + envBindings, err := envBindingUsecase.GetEnvBindings(context.TODO(), testApp) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(envBindings), 2)).Should(BeEmpty()) + }) + + It("Test BatchDeleteEnvBinding function", func() { + err := envBindingUsecase.BatchDeleteEnvBinding(context.TODO(), testApp) + Expect(err).Should(BeNil()) + envBindings, err := envBindingUsecase.GetEnvBindings(context.TODO(), testApp) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(envBindings), 0)).Should(BeEmpty()) + }) + +}) diff --git a/pkg/apiserver/rest/utils/bcode/envbinding.go b/pkg/apiserver/rest/utils/bcode/envbinding.go index 0f40030ce..6d4ad0ad1 100644 --- a/pkg/apiserver/rest/utils/bcode/envbinding.go +++ b/pkg/apiserver/rest/utils/bcode/envbinding.go @@ -30,3 +30,6 @@ var ErrEnvBindingsNotExist = NewBcode(400, 90004, "application envbinding is not // ErrEnvBindingExist application envbinding is exist var ErrEnvBindingExist = NewBcode(400, 90005, "application envbinding is exist") + +// ErrEnvBindingUpdateWorkflow application envbinding update workflow error +var ErrEnvBindingUpdateWorkflow = NewBcode(400, 90006, "application envbinding update workflow error") From 1ea26865aaaa7d702a590e9a0e1a12aa32722c0e Mon Sep 17 00:00:00 2001 From: qiaozp <47812250+chivalryq@users.noreply.github.com> Date: Sun, 21 Nov 2021 18:09:19 +0800 Subject: [PATCH 57/59] Fix: lint apiserver code, fix panic (#2755) * lint code * fix error judge try * fix multicluster enable panic * add err log * fix can not get parameter * debug * try ci * debug * debug * debug * debugo Co-authored-by: barnettZQG <576501057@qq.com> --- pkg/addon/addon.go | 48 ++++++++++++++++++-------- pkg/addon/error.go | 4 +-- pkg/apiserver/rest/usecase/addon.go | 32 ++++++++--------- test/e2e-apiserver-test/addon_test.go | 49 ++++++++++++--------------- 4 files changed, 71 insertions(+), 62 deletions(-) diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 879e91b6c..19dd83f02 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -53,6 +53,7 @@ const ( DefinitionsDirName string = "definitions" ) +// ListOptions contains flags mark what files should be read in an addon directory type ListOptions struct { GetDetail bool GetDefinition bool @@ -62,17 +63,22 @@ type ListOptions struct { } var ( - ListLevelOptions = ListOptions{} - GetLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true} + // GetLevelOptions used when get or list addons + GetLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true} + + // EnableLevelOptions used when enable addon EnableLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetResource: true, GetTemplate: true, GetParameter: true} ) -type AddonErr error +// aError is internal error type of addon +type aError error var ( - AddonNotExist AddonErr = errors.New("addon not exist") + // ErrNotExist means addon not exists + ErrNotExist aError = errors.New("addon not exist") ) +// gitHelper helps get addon's file by git type gitHelper struct { Client *github.Client Meta *utils.Content @@ -85,14 +91,16 @@ type GitAddonSource struct { Token string `json:"token,omitempty"` } -type AddonReader struct { +// asyncReader helps async read files of addon +type asyncReader struct { addon *types.Addon h *gitHelper item *github.RepositoryContent errChan chan error } -func (r *AddonReader) SetReadContent(content *github.RepositoryContent) { +// SetReadContent set which file to read +func (r *asyncReader) SetReadContent(content *github.RepositoryContent) { r.item = content } @@ -159,8 +167,11 @@ func getSingleAddonFromGit(baseURL, dir, addonName, token string, opt ListOption return nil, err } _, items, err := gith.readRepo(gith.Meta.Path) + if err != nil { + return nil, err + } - reader := AddonReader{ + reader := asyncReader{ addon: &types.Addon{}, h: gith, errChan: make(chan error, 1), @@ -186,7 +197,7 @@ func getSingleAddonFromGit(baseURL, dir, addonName, token string, opt ListOption wg.Add(1) go readDefinitions(&wg, reader) case ResourcesDirName: - if !opt.GetResource { + if !opt.GetResource && !opt.GetParameter { break } reader.SetReadContent(item) @@ -213,7 +224,7 @@ func getSingleAddonFromGit(baseURL, dir, addonName, token string, opt ListOption } -func readTemplate(wg *sync.WaitGroup, reader AddonReader) { +func readTemplate(wg *sync.WaitGroup, reader asyncReader) { defer wg.Done() content, _, err := reader.h.readRepo(*reader.item.Path) if err != nil { @@ -234,7 +245,7 @@ func readTemplate(wg *sync.WaitGroup, reader AddonReader) { } } -func readResources(wg *sync.WaitGroup, reader AddonReader) { +func readResources(wg *sync.WaitGroup, reader asyncReader) { defer wg.Done() dirPath := strings.Split(reader.item.GetPath(), "/") dirPath, err := cutPathUntil(dirPath, ResourcesDirName) @@ -263,7 +274,7 @@ func readResources(wg *sync.WaitGroup, reader AddonReader) { } // readResFile read single resource file -func readResFile(wg *sync.WaitGroup, reader AddonReader, dirPath []string) { +func readResFile(wg *sync.WaitGroup, reader asyncReader, dirPath []string) { defer wg.Done() content, _, err := reader.h.readRepo(*reader.item.Path) if err != nil { @@ -288,7 +299,7 @@ func readResFile(wg *sync.WaitGroup, reader AddonReader, dirPath []string) { } } -func readDefinitions(wg *sync.WaitGroup, reader AddonReader) { +func readDefinitions(wg *sync.WaitGroup, reader asyncReader) { defer wg.Done() dirPath := strings.Split(reader.item.GetPath(), "/") dirPath, err := cutPathUntil(dirPath, DefinitionsDirName) @@ -316,7 +327,7 @@ func readDefinitions(wg *sync.WaitGroup, reader AddonReader) { } // readDefFile read single definition file -func readDefFile(wg *sync.WaitGroup, reader AddonReader, dirPath []string) { +func readDefFile(wg *sync.WaitGroup, reader asyncReader, dirPath []string) { defer wg.Done() content, _, err := reader.h.readRepo(*reader.item.Path) if err != nil { @@ -331,7 +342,7 @@ func readDefFile(wg *sync.WaitGroup, reader AddonReader, dirPath []string) { reader.addon.Definitions = append(reader.addon.Definitions, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath}) } -func readMetadata(wg *sync.WaitGroup, reader AddonReader) { +func readMetadata(wg *sync.WaitGroup, reader asyncReader) { defer wg.Done() content, _, err := reader.h.readRepo(*reader.item.Path) if err != nil { @@ -350,7 +361,7 @@ func readMetadata(wg *sync.WaitGroup, reader AddonReader) { } } -func readReadme(wg *sync.WaitGroup, reader AddonReader) { +func readReadme(wg *sync.WaitGroup, reader asyncReader) { defer wg.Done() content, _, err := reader.h.readRepo(*reader.item.Path) if err != nil { @@ -358,6 +369,10 @@ func readReadme(wg *sync.WaitGroup, reader AddonReader) { return } reader.addon.Detail, err = content.GetContent() + if err != nil { + reader.errChan <- err + return + } } func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { @@ -476,6 +491,9 @@ func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.App } defObjs = append(defObjs, obj) } + if app.Spec.Workflow == nil { + app.Spec.Workflow = &v1beta1.Workflow{Steps: make([]v1beta1.WorkflowStep, 0)} + } app.Spec.Workflow.Steps = append(app.Spec.Workflow.Steps, v1beta1.WorkflowStep{ Name: "deploy-all", diff --git a/pkg/addon/error.go b/pkg/addon/error.go index c66d40d48..1a0c6f98c 100644 --- a/pkg/addon/error.go +++ b/pkg/addon/error.go @@ -20,8 +20,8 @@ var ( // WrapErrRateLimit return ErrRateLimit if is the situation, or return error directly func WrapErrRateLimit(err error) error { - var rateLimit *github.RateLimitError - if errors.As(err, &rateLimit) { + errRate := &github.RateLimitError{} + if errors.As(err, &errRate) { return ErrRateLimit } return err diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 82bec2f05..5a1cb1e9d 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -51,12 +51,12 @@ func AddonImpl2AddonRes(impl *types.Addon) (*apis.DetailAddonResponse, error) { dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) _, _, err := dec.Decode([]byte(def.Data), nil, obj) if err != nil { - return nil, errors.New(fmt.Sprintf("convert %s file content to definition fail", def.Name)) + return nil, fmt.Errorf("convert %s file content to definition fail", def.Name) } defs = append(defs, &apis.AddonDefinition{ - obj.GetName(), - obj.GetKind(), - obj.GetAnnotations()["definition.oam.dev/description"], + Name: obj.GetName(), + DefType: obj.GetKind(), + Description: obj.GetAnnotations()["definition.oam.dev/description"], }) } return &apis.DetailAddonResponse{ @@ -104,23 +104,21 @@ func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string, registry s if addon, exist = u.tryGetAddonFromCache(r.Name, name); !exist { addon, err = pkgaddon.GetAddon(name, r.Git, pkgaddon.GetLevelOptions) } - if err != nil && !errors.Is(err, pkgaddon.AddonNotExist) { + if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) { return nil, err } if addon != nil { break } } - } else { - if addon, exist = u.tryGetAddonFromCache(registry, name); !exist { - addonRegistry, err := u.GetAddonRegistry(ctx, registry) - if err != nil { - return nil, err - } - addon, err = pkgaddon.GetAddon(name, addonRegistry.Git, pkgaddon.GetLevelOptions) - if err != nil && !errors.Is(err, pkgaddon.AddonNotExist) { - return nil, err - } + } else if addon, exist = u.tryGetAddonFromCache(registry, name); !exist { + addonRegistry, err := u.GetAddonRegistry(ctx, registry) + if err != nil { + return nil, err + } + addon, err = pkgaddon.GetAddon(name, addonRegistry.Git, pkgaddon.GetLevelOptions) + if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) { + return nil, err } } @@ -194,7 +192,7 @@ func (u *addonUsecaseImpl) ListAddons(ctx context.Context, registry, query strin } else { listAddons, err = pkgaddon.ListAddons(r.Git, pkgaddon.GetLevelOptions) if err != nil { - log.Logger.Errorf("fail to get addons from registry %s", r.Name) + log.Logger.Errorf("fail to get addons from registry %s, %v", r.Name, err) continue } // if list addons, details will be retrieved later @@ -332,7 +330,7 @@ func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args ap if addon, exist = u.tryGetAddonFromCache(r.Name, name); !exist { addon, err = pkgaddon.GetAddon(name, r.Git, pkgaddon.EnableLevelOptions) } - if err != nil && !errors.Is(err, pkgaddon.AddonNotExist) { + if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) { return bcode.WrapGithubRateLimitErr(err) } if addon == nil { diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go index 9e78252c0..7df0ea650 100644 --- a/test/e2e-apiserver-test/addon_test.go +++ b/test/e2e-apiserver-test/addon_test.go @@ -1,23 +1,22 @@ -package e2e_apiserver +package e2e_apiserver_test import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "os" "time" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/addon" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" - "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/pkg/utils/common" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) const baseURL = "http://127.0.0.1:8000" @@ -52,8 +51,8 @@ var _ = Describe("Test addon rest api", func() { By("add registry") createRes := post("/api/v1/addon_registries", createReq) Expect(createRes).ShouldNot(BeNil()) - Expect(createRes.StatusCode).Should(Equal(200)) Expect(createRes.Body).ShouldNot(BeNil()) + Expect(createRes.StatusCode).Should(Equal(200)) defer createRes.Body.Close() @@ -77,17 +76,6 @@ var _ = Describe("Test addon rest api", func() { }) It("should enable and disable an addon", func() { - // todo(qiaozp) we should remove this namespace creation. This should be solved with a application template. - ns := v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "flux-system", - }, - } - args := common.Args{} - k8sClient, err := args.GetClient() - Expect(err).Should(BeNil()) - Expect(k8sClient.Create(context.Background(), &ns)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - defer GinkgoRecover() req := apis.EnableAddonRequest{ Args: map[string]string{ @@ -103,25 +91,30 @@ var _ = Describe("Test addon rest api", func() { defer res.Body.Close() var statusRes apis.AddonStatusResponse - err = json.NewDecoder(res.Body).Decode(&statusRes) + err := json.NewDecoder(res.Body).Decode(&statusRes) Expect(err).Should(BeNil()) Expect(statusRes.Phase).Should(Equal(apis.AddonPhaseEnabling)) // Wait for addon enabled - period := 20 * time.Second - timeout := 5 * time.Minute - err = wait.PollImmediate(period, timeout, func() (done bool, err error) { + period := 10 * time.Second + timeout := 2 * time.Minute + Eventually(func() error { res = get("/api/v1/addons/" + testAddon + "/status") err = json.NewDecoder(res.Body).Decode(&statusRes) Expect(err).Should(BeNil()) if statusRes.Phase == apis.AddonPhaseEnabled { - return true, nil + return nil } - return false, nil - }) - Expect(err).Should(BeNil()) + var app v1beta1.Application + err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "addon-example", Namespace: "vela-system"}, &app) + Expect(err).Should(BeNil()) + data, err := json.Marshal(app) + Expect(err).Should(BeNil()) + fmt.Println(data) + return errors.New("not ready") + }, timeout, period).Should(BeNil()) res = post("/api/v1/addons/"+testAddon+"/disable", req) Expect(res).ShouldNot(BeNil()) From 4ad27e9bcdab2440b4140abe04b4c2d97a860006 Mon Sep 17 00:00:00 2001 From: wyike Date: Sun, 21 Nov 2021 21:03:41 +0800 Subject: [PATCH 58/59] Fix: (#2761) 1. load component in arrary, so apply them in order addon's needNamespace will be apply firstly 2. apply application in controle plane will be first workflowStep 3. bigger application reconcile timeout context get avoid of time out --- apis/types/capability.go | 17 +++++----- .../defwithtemplate/deploy2runtime.yaml | 4 +-- .../defwithtemplate/deploy2runtime.yaml | 4 +-- pkg/addon/addon.go | 25 ++++++++++++++- .../application/application_controller.go | 3 +- pkg/stdlib/op.cue | 4 ++- pkg/stdlib/pkgs/oam.cue | 6 ++++ pkg/workflow/providers/oam/apply.go | 32 ++++++++++++++++--- .../definitions/internal/deploy2runtime.cue | 4 +-- 9 files changed, 77 insertions(+), 22 deletions(-) diff --git a/apis/types/capability.go b/apis/types/capability.go index 553edcc42..cecb4d633 100644 --- a/apis/types/capability.go +++ b/apis/types/capability.go @@ -206,14 +206,15 @@ type Addon struct { // AddonMeta defines the format for a single addon type AddonMeta struct { - Name string `json:"name" validate:"required"` - Version string `json:"version"` - Description string `json:"description"` - Icon string `json:"icon"` - URL string `json:"url,omitempty"` - Tags []string `json:"tags,omitempty"` - DeployTo *AddonDeployTo `json:"deployTo,omitempty"` - Dependencies []*AddonDependency `json:"dependencies,omitempty"` + Name string `json:"name" validate:"required"` + Version string `json:"version"` + Description string `json:"description"` + Icon string `json:"icon"` + URL string `json:"url,omitempty"` + Tags []string `json:"tags,omitempty"` + DeployTo *AddonDeployTo `json:"deployTo,omitempty"` + Dependencies []*AddonDependency `json:"dependencies,omitempty"` + NeedNamespace []string `json:"needNamespace,omitempty"` } // AddonDeployTo defines where the addon to deploy to diff --git a/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml b/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml index 5b007b43d..b0bbabae2 100644 --- a/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml +++ b/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml @@ -32,10 +32,10 @@ spec: "\(cluster_)-\(name)": op.#ApplyComponent & { value: c cluster: cluster_ - } @step(3) + } } } - } + } @step(3) } parameter: { // +usage=Declare the runtime clusters to apply, if empty, all runtime clusters will be used diff --git a/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml b/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml index 5b007b43d..b0bbabae2 100644 --- a/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml +++ b/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml @@ -32,10 +32,10 @@ spec: "\(cluster_)-\(name)": op.#ApplyComponent & { value: c cluster: cluster_ - } @step(3) + } } } - } + } @step(3) } parameter: { // +usage=Declare the runtime clusters to apply, if empty, all runtime clusters will be used diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go index 19dd83f02..c21abe4d3 100644 --- a/pkg/addon/addon.go +++ b/pkg/addon/addon.go @@ -464,6 +464,17 @@ func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.App } app.Name = Convert2AppName(addon.Name) app.Labels = util.MergeMapOverrideWithDst(app.Labels, map[string]string{oam.LabelAddonName: addon.Name}) + if app.Spec.Workflow == nil { + app.Spec.Workflow = &v1beta1.Workflow{} + } + for _, namespace := range addon.NeedNamespace { + comp := common2.ApplicationComponent{ + Type: "raw", + Name: fmt.Sprintf("%s-namespace", namespace), + Properties: util.Object2RawExtension(renderNamespace(namespace)), + } + app.Spec.Components = append(app.Spec.Components, comp) + } for _, tmpl := range addon.YAMLTemplates { comp, err := renderRawComponent(tmpl) @@ -496,7 +507,11 @@ func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.App } app.Spec.Workflow.Steps = append(app.Spec.Workflow.Steps, v1beta1.WorkflowStep{ - Name: "deploy-all", + Name: "deploy-control-plane", + Type: "apply-application", + }, + v1beta1.WorkflowStep{ + Name: "deploy-runtime", Type: "deploy2runtime", }) } else { @@ -529,6 +544,14 @@ func renderObject(elem types.AddonElementFile) (*unstructured.Unstructured, erro return obj, nil } +func renderNamespace(namespace string) *unstructured.Unstructured { + u := &unstructured.Unstructured{} + u.SetAPIVersion("v1") + u.SetKind("Namespace") + u.SetName(namespace) + return u +} + // renderRawComponent will return a component in raw type from string func renderRawComponent(elem types.AddonElementFile) (*common2.ApplicationComponent, error) { baseRawComponent := common2.ApplicationComponent{ diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go index 00e6893cd..ee8c424f8 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go @@ -41,7 +41,6 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" velatypes "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/appfile" - common2 "github.com/oam-dev/kubevela/pkg/controller/common" core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/assemble" "github.com/oam-dev/kubevela/pkg/cue/packages" @@ -88,7 +87,7 @@ type Reconciler struct { // Reconcile process app event // nolint:gocyclo func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - ctx, cancel := common2.NewReconcileContext(ctx) + ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() klog.InfoS("Reconcile application", "application", klog.KRef(req.Namespace, req.Name)) diff --git a/pkg/stdlib/op.cue b/pkg/stdlib/op.cue index b3b4f39a4..a9a1f7b42 100644 --- a/pkg/stdlib/op.cue +++ b/pkg/stdlib/op.cue @@ -23,7 +23,7 @@ import ( #Delete: kube.#Delete #ApplyApplication: #Steps & { - load: oam.#LoadComponets @step(1) + load: oam.#LoadComponetsInOrder @step(1) components: #Steps & { for name, c in load.value { "\(name)": oam.#ApplyComponent & { @@ -133,6 +133,8 @@ import ( #Load: oam.#LoadComponets +#LoadInOrder: oam.#LoadComponetsInOrder + #Steps: { #do: "steps" ... diff --git a/pkg/stdlib/pkgs/oam.cue b/pkg/stdlib/pkgs/oam.cue index 51e9d15f2..32455aea7 100644 --- a/pkg/stdlib/pkgs/oam.cue +++ b/pkg/stdlib/pkgs/oam.cue @@ -30,3 +30,9 @@ value?: {...} ... } + +#LoadComponetsInOrder: { + #provider: "oam" + #do: "load-comps-in-order" + ... +} diff --git a/pkg/workflow/providers/oam/apply.go b/pkg/workflow/providers/oam/apply.go index be767abb2..da1e06e6a 100644 --- a/pkg/workflow/providers/oam/apply.go +++ b/pkg/workflow/providers/oam/apply.go @@ -18,6 +18,7 @@ package oam import ( "encoding/json" + "strings" "github.com/oam-dev/kubevela/pkg/cue/model/sets" @@ -157,6 +158,28 @@ func (p *provider) LoadComponent(ctx wfContext.Context, v *value.Value, act wfTy return nil } +// LoadComponentInOrder load component describe info in application output will be a list with order defined in application. +func (p *provider) LoadComponentInOrder(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + app := &v1beta1.Application{} + // if specify `app`, use specified application otherwise use default application fron provider + appSettings, err := v.LookupValue("app") + if err != nil { + if strings.Contains(err.Error(), "not exist") { + app = p.app + } else { + return err + } + } else { + if err := appSettings.UnmarshalTo(app); err != nil { + return err + } + } + if err := v.FillObject(app.Spec.Components, "value"); err != nil { + return err + } + return nil +} + // LoadPolicies load policy describe info in application. func (p *provider) LoadPolicies(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { for _, po := range p.app.Spec.Policies { @@ -175,9 +198,10 @@ func Install(p providers.Providers, app *v1beta1.Application, apply ComponentApp app: app.DeepCopy(), } p.Register(ProviderName, map[string]providers.Handler{ - "component-render": prd.RenderComponent, - "component-apply": prd.ApplyComponent, - "load": prd.LoadComponent, - "load-policies": prd.LoadPolicies, + "component-render": prd.RenderComponent, + "component-apply": prd.ApplyComponent, + "load": prd.LoadComponent, + "load-policies": prd.LoadPolicies, + "load-comps-in-order": prd.LoadComponentInOrder, }) } diff --git a/vela-templates/definitions/internal/deploy2runtime.cue b/vela-templates/definitions/internal/deploy2runtime.cue index b7862e878..504d11580 100644 --- a/vela-templates/definitions/internal/deploy2runtime.cue +++ b/vela-templates/definitions/internal/deploy2runtime.cue @@ -26,10 +26,10 @@ template: { "\(cluster_)-\(name)": op.#ApplyComponent & { value: c cluster: cluster_ - } @step(3) + } } } - } + } @step(3) } parameter: { From 47a565d00d0bde2f49b1a615defd1089a2bae9c2 Mon Sep 17 00:00:00 2001 From: Tianxin Dong Date: Mon, 22 Nov 2021 10:58:23 +0800 Subject: [PATCH 59/59] Fix: controllerrevision can not be updated (#2764) --- pkg/workflow/recorder/recorder.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/recorder/recorder.go b/pkg/workflow/recorder/recorder.go index 0cc0f1c37..a2ea343cf 100644 --- a/pkg/workflow/recorder/recorder.go +++ b/pkg/workflow/recorder/recorder.go @@ -66,7 +66,9 @@ func (r *recorder) Save(version string, data []byte) Store { if version == "" { wfStatus := r.source.Status.Workflow if wfStatus != nil { - version = strings.ReplaceAll(wfStatus.AppRevision, ":", "-") + if !strings.Contains(wfStatus.AppRevision, ":") { + version = wfStatus.AppRevision + } } } @@ -89,7 +91,16 @@ func (r *recorder) Save(version string, data []byte) Store { } if err := r.cli.Create(context.Background(), rv); err != nil { if kerrors.IsAlreadyExists(err) { - r.err = r.cli.Update(context.Background(), rv) + // ControllerRevision implements an immutable snapshot of state data + // Once a ControllerRevision has been successfully created, it can not be updated. + // So we need to delete the old one and create a new one. + r.err = r.cli.Delete(context.Background(), &apps.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: rv.Name, + Namespace: rv.Namespace, + }, + }) + r.err = r.cli.Create(context.Background(), rv) } else { r.err = errors.WithMessagef(err, "save record %s/%s", rv.Namespace, rv.Name) }