diff --git a/pkg/apiserver/rest/apis/core.go b/pkg/apiserver/rest/apis/core.go new file mode 100644 index 000000000..8caaa807a --- /dev/null +++ b/pkg/apiserver/rest/apis/core.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 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"` +} diff --git a/pkg/apiserver/rest/rest_server.go b/pkg/apiserver/rest/rest_server.go index ca5d8c8f1..fc3477f24 100644 --- a/pkg/apiserver/rest/rest_server.go +++ b/pkg/apiserver/rest/rest_server.go @@ -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 } diff --git a/pkg/apiserver/rest/services/application.go b/pkg/apiserver/rest/services/application.go new file mode 100644 index 000000000..084e64733 --- /dev/null +++ b/pkg/apiserver/rest/services/application.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 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//applications/ +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{}{}) +} diff --git a/pkg/apiserver/rest/services/application_test.go b/pkg/apiserver/rest/services/application_test.go new file mode 100644 index 000000000..e84ef632f --- /dev/null +++ b/pkg/apiserver/rest/services/application_test.go @@ -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) + } +} diff --git a/pkg/apiserver/rest/services/catalog_test.go b/pkg/apiserver/rest/services/catalog_test.go index 7a83fb06f..e2dcd1809 100644 --- a/pkg/apiserver/rest/services/catalog_test.go +++ b/pkg/apiserver/rest/services/catalog_test.go @@ -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)