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()) + }) +})