add create or update app apiserver (#2087)

* add create or update app

* add apiserver test
This commit is contained in:
Jianbo Sun
2021-08-16 20:28:03 +08:00
committed by GitHub
parent d3f264273f
commit 309e71a60f
5 changed files with 250 additions and 6 deletions
+29
View File
@@ -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 apis
import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
)
// ApplicationRequest represents application request for APIServer
type ApplicationRequest struct {
Components []common.ApplicationComponent `json:"components"`
Policies []v1beta1.AppPolicy `json:"policies,omitempty"`
Workflow *v1beta1.Workflow `json:"workflow,omitempty"`
}
+14 -5
View File
@@ -91,13 +91,22 @@ func (s *restServer) Run(ctx context.Context) error {
func (s *restServer) registerServices() error {
/* ************************************************************** */
/* ************* Open API Route Group ***************** */
/* ************************************************************** */
openapi := s.server.Group("/v1")
// catalog
catalogService := services.NewCatalogService(s.k8sClient)
s.server.GET("/catalogs", catalogService.ListCatalogs)
s.server.POST("/catalogs", catalogService.AddCatalog)
s.server.PUT("/catalogs", catalogService.UpdateCatalog)
s.server.GET("/catalogs/:catalogName", catalogService.GetCatalog)
s.server.DELETE("/catalogs/:catalogName", catalogService.DelCatalog)
openapi.GET("/catalogs", catalogService.ListCatalogs)
openapi.POST("/catalogs", catalogService.AddCatalog)
openapi.PUT("/catalogs", catalogService.UpdateCatalog)
openapi.GET("/catalogs/:catalogName", catalogService.GetCatalog)
openapi.DELETE("/catalogs/:catalogName", catalogService.DelCatalog)
// application
applicationService := services.NewApplicationService(s.k8sClient)
openapi.POST("/namespaces/:namespace/applications/:appname", applicationService.CreateOrUpdateApplication)
return nil
}
@@ -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 services
import (
"context"
"net/http"
"github.com/labstack/echo/v4"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/apis"
)
// ApplicationService serves as Application Open API for request
type ApplicationService struct {
k8sClient client.Client
}
// NewApplicationService create an application service
func NewApplicationService(kc client.Client) *ApplicationService {
return &ApplicationService{
k8sClient: kc,
}
}
// CreateOrUpdateApplication will create or update application
// POST /v1/namespaces/<namespace>/applications/<appname>
func (s *ApplicationService) CreateOrUpdateApplication(c echo.Context) error {
namespace := c.Param("namespace")
name := c.Param("appname")
appReq := new(apis.ApplicationRequest)
if err := c.Bind(appReq); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body: " + err.Error()})
}
ctx := context.TODO()
var app, existApp v1beta1.Application
app.Namespace = namespace
app.Name = name
app.Spec.Components = appReq.Components
app.Spec.Policies = appReq.Policies
app.Spec.Workflow = appReq.Workflow
err := s.k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, &existApp)
if err != nil {
if !apierrors.IsNotFound(err) {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "fail to get application: " + err.Error()})
}
err = s.k8sClient.Create(ctx, &app)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "fail to create application: " + err.Error()})
}
return c.JSON(http.StatusOK, struct{}{})
}
existApp.Spec = app.Spec
err = s.k8sClient.Update(ctx, &existApp)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "fail to update application: " + err.Error()})
}
return c.JSON(http.StatusOK, struct{}{})
}
@@ -0,0 +1,130 @@
/*
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 services
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"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/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/apis"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
func TestApplicationCreateOrUpdate(t *testing.T) {
cw := fake.NewClientBuilder().WithScheme(common.Scheme).Build()
appSvc := NewApplicationService(cw)
appComp1 := common2.ApplicationComponent{
Name: "mycomp",
Type: "webservice",
Properties: runtime.RawExtension{Raw: []byte(`{"image":"nginx:v1"}`)},
}
appComp2 := common2.ApplicationComponent{
Name: "mycomp2",
Type: "webservice",
Properties: runtime.RawExtension{Raw: []byte(`{"image":"nginx:v2"}`)},
}
tests := map[string]struct {
appReq *apis.ApplicationRequest
rawReq []byte
name string
namespace string
expHttpCode int
expErr string
expApp *v1beta1.Application
}{
"normal create with only component": {
appReq: &apis.ApplicationRequest{
Components: []common2.ApplicationComponent{appComp1},
},
expHttpCode: 200,
name: "myapp",
namespace: "mynamespace",
expApp: &v1beta1.Application{
Spec: v1beta1.ApplicationSpec{
Components: []common2.ApplicationComponent{appComp1},
},
},
},
"create with bind error": {
rawReq: []byte("XXXX"),
expHttpCode: 400,
name: "myapp",
namespace: "mynamespace",
expErr: "invalid request body: code=400",
},
"normal update with component and trait": {
appReq: &apis.ApplicationRequest{
Components: []common2.ApplicationComponent{appComp1, appComp2},
},
expHttpCode: 200,
name: "myapp",
namespace: "mynamespace",
expApp: &v1beta1.Application{
Spec: v1beta1.ApplicationSpec{
Components: []common2.ApplicationComponent{appComp1, appComp2},
},
},
},
}
for casename, c := range tests {
var err error
if c.appReq != nil {
c.rawReq, err = json.Marshal(c.appReq)
assert.NoError(t, err, casename)
}
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(c.rawReq))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
echoCtx := echo.New().NewContext(req, rec)
echoCtx.SetParamNames("namespace", "appname")
echoCtx.SetParamValues(c.namespace, c.name)
err = appSvc.CreateOrUpdateApplication(echoCtx)
assert.NoError(t, err, casename)
// check response
assert.Equal(t, c.expHttpCode, rec.Code, casename)
gotResp := map[string]string{}
err = json.Unmarshal(rec.Body.Bytes(), &gotResp)
assert.NoError(t, err, casename)
if c.expErr != "" {
assert.True(t, strings.Contains(gotResp["error"], c.expErr), casename)
}
if len(c.expErr) > 0 {
continue
}
var appObj v1beta1.Application
err = cw.Get(context.TODO(), client.ObjectKey{Namespace: c.namespace, Name: c.name}, &appObj)
assert.NoError(t, err, casename)
assert.Equal(t, c.expApp.Spec, appObj.Spec, casename)
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ var _ = Describe("Test Catalog Service", func() {
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)
c.SetPath("/catalogs/:catalogName")
c.SetPath("/v1/catalogs/:catalogName")
c.SetParamNames("catalogName")
c.SetParamValues(cr.Name)