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 <yiyun.pro>
This commit is contained in:
barnettZQG
2021-10-23 00:29:43 +08:00
committed by Hongchao Deng
co-authored by lnx01
parent b85dda35f3
commit f0b91ef8d6
14 changed files with 805 additions and 129 deletions
+137 -108
View File
@@ -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": {
+15 -1
View File
@@ -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
+16
View File
@@ -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)
+22 -9
View File
@@ -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
+85 -5
View File
@@ -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
}
+34 -1
View File
@@ -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() {
+72
View File
@@ -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
}
@@ -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())
})
})
+255
View File
@@ -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
+41
View File
@@ -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
}
@@ -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
}
}
+2 -1
View File
@@ -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{})
+4 -1
View File
@@ -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() {
@@ -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())
})
})