diff --git a/e2e/apiserver/apiserver_test.go b/e2e/apiserver/apiserver_test.go
index 9aedb4047..d4f4abe3c 100644
--- a/e2e/apiserver/apiserver_test.go
+++ b/e2e/apiserver/apiserver_test.go
@@ -7,12 +7,13 @@ import (
"net/http"
"strings"
- "github.com/oam-dev/kubevela/e2e"
- "github.com/oam-dev/kubevela/pkg/server/apis"
- "github.com/oam-dev/kubevela/pkg/server/util"
-
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
+
+ "github.com/oam-dev/kubevela/e2e"
+ "github.com/oam-dev/kubevela/pkg/appfile/api"
+ "github.com/oam-dev/kubevela/pkg/server/apis"
+ "github.com/oam-dev/kubevela/pkg/server/util"
)
var (
@@ -30,20 +31,27 @@ var (
Namespace: "env-e2e-world-modified",
}
- workloadType = "webservice"
- workloadName = "app-e2e-api-hello"
+ workloadType = "webservice"
+ applicationName = "app-e2e-api-hello"
+ svcName = "svc-e2e-api-hello"
- workloadRunBodyWithoutImageFlag = apis.WorkloadRunBody{
- EnvName: envHelloMeta.EnvName,
- WorkloadName: workloadName,
- WorkloadType: workloadType,
- Flags: []apis.CommonFlag{{Name: "port", Value: "80"}},
+ applicationCreationBodyWithoutImageFlag = api.AppFile{
+ Name: applicationName,
+ Services: map[string]api.Service{
+ svcName: map[string]interface{}{},
+ },
}
- workloadRunBody = apis.WorkloadRunBody{
- EnvName: envHelloMeta.EnvName,
- WorkloadName: workloadName,
- WorkloadType: workloadType,
- Flags: []apis.CommonFlag{{Name: "image", Value: "nginx:1.9.4"}, {Name: "port", Value: "80"}},
+
+ applicationCreationBody = api.AppFile{
+ Name: applicationName,
+ Services: map[string]api.Service{
+ svcName: map[string]interface{}{
+ "type": workloadType,
+ "image": "wordpress:php7.4-apache",
+ "port": "80",
+ "cpu": "1",
+ },
+ },
}
)
@@ -165,10 +173,11 @@ var _ = ginkgo.Describe("API", func() {
})
ginkgo.Context("Workloads", func() {
- ginkgo.It("run workload", func() {
- data, err := json.Marshal(&workloadRunBody)
+ ginkgo.It("create an application", func() {
+ data, err := json.Marshal(&applicationCreationBody)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
- resp, err := http.Post(util.URL("/workloads/"), "application/json", strings.NewReader(string(data)))
+ url := fmt.Sprintf("/envs/%s/apps/", envHelloMeta.EnvName)
+ resp, err := http.Post(util.URL(url), "application/json", strings.NewReader(string(data)))
gomega.Expect(err).NotTo(gomega.HaveOccurred())
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
@@ -177,14 +186,16 @@ var _ = ginkgo.Describe("API", func() {
err = json.Unmarshal(result, &r)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(http.StatusOK).Should(gomega.Equal(r.Code), string(result))
- output := fmt.Sprintf("App %s deployed", workloadName)
+ output := fmt.Sprintf("application %s is successfully created", applicationName)
gomega.Expect(r.Data.(string)).To(gomega.ContainSubstring(output))
})
- ginkgo.It("run workload without compulsory flag", func() {
- data, err := json.Marshal(&workloadRunBodyWithoutImageFlag)
+ ginkgo.It("create an application without compulsory flag of a service", func() {
+ data, err := json.Marshal(&applicationCreationBodyWithoutImageFlag)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
- resp, err := http.Post(util.URL("/workloads/"), "application/json", strings.NewReader(string(data)))
+ url := fmt.Sprintf("/envs/%s/apps/", envHelloMeta.EnvName)
+ resp, err := http.Post(util.URL(url), "application/json", strings.NewReader(string(data)))
+ // TODO(zzxwill) revise the check process if we need to work on https://github.com/oam-dev/kubevela/discussions/933
gomega.Expect(err).NotTo(gomega.HaveOccurred())
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
@@ -192,8 +203,8 @@ var _ = ginkgo.Describe("API", func() {
var r apis.Response
err = json.Unmarshal(result, &r)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
- gomega.Expect(http.StatusInternalServerError).Should(gomega.Equal(r.Code))
- output := "required flag(s) \"image\" not set"
+ gomega.Expect(http.StatusOK).Should(gomega.Equal(r.Code), string(result))
+ output := fmt.Sprintf("application %s is successfully created", applicationName)
gomega.Expect(r.Data.(string)).To(gomega.ContainSubstring(output))
})
@@ -215,7 +226,7 @@ var _ = ginkgo.Describe("API", func() {
})
ginkgo.It("should delete an application", func() {
- req, err := http.NewRequest("DELETE", util.URL("/envs/"+envHelloMeta.EnvName+"/apps/"+workloadRunBody.WorkloadName), nil)
+ req, err := http.NewRequest("DELETE", util.URL("/envs/"+envHelloMeta.EnvName+"/apps/"+applicationName), nil)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
resp, err := http.DefaultClient.Do(req)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
diff --git a/pkg/commands/export.go b/pkg/commands/export.go
index 7a67dd188..dfd623af3 100644
--- a/pkg/commands/export.go
+++ b/pkg/commands/export.go
@@ -5,6 +5,7 @@ import (
"github.com/oam-dev/kubevela/apis/types"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
+ "github.com/oam-dev/kubevela/pkg/serverlib"
)
// NewExportCommand will create command for exporting deploy manifests from an AppFile
@@ -18,7 +19,7 @@ func NewExportCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
types.TagCommandType: types.TypeStart,
},
RunE: func(cmd *cobra.Command, args []string) error {
- o := &AppfileOptions{
+ o := &serverlib.AppfileOptions{
IO: ioStream,
Env: &types.EnvMeta{},
}
@@ -26,7 +27,7 @@ func NewExportCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
if err != nil {
return err
}
- _, data, err := o.export(filePath, true)
+ _, data, err := o.Export(filePath, true)
if err != nil {
return err
}
diff --git a/pkg/commands/up.go b/pkg/commands/up.go
index 342c91fcc..4ed4ca68a 100644
--- a/pkg/commands/up.go
+++ b/pkg/commands/up.go
@@ -1,29 +1,12 @@
package commands
import (
- "bytes"
- "context"
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/pkg/errors"
"github.com/spf13/cobra"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- k8sjson "k8s.io/apimachinery/pkg/runtime/serializer/json"
- apitypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
- "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
- "github.com/oam-dev/kubevela/pkg/appfile"
- "github.com/oam-dev/kubevela/pkg/appfile/api"
- "github.com/oam-dev/kubevela/pkg/appfile/template"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
- "github.com/oam-dev/kubevela/pkg/oam"
- "github.com/oam-dev/kubevela/pkg/utils/common"
+ "github.com/oam-dev/kubevela/pkg/serverlib"
)
var (
@@ -53,7 +36,7 @@ func NewUpCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
return err
}
- o := &AppfileOptions{
+ o := &serverlib.AppfileOptions{
Kubecli: kubecli,
IO: ioStream,
Env: velaEnv,
@@ -70,174 +53,3 @@ func NewUpCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
cmd.Flags().StringP(appFilePath, "f", "", "specify file path for appfile")
return cmd
}
-
-// AppfileOptions is some configuration that modify options for an Appfile
-type AppfileOptions struct {
- Kubecli client.Client
- IO cmdutil.IOStreams
- Env *types.EnvMeta
-}
-
-func saveRemoteAppfile(url string) (string, error) {
- body, err := common.HTTPGet(context.Background(), url)
- if err != nil {
- return "", err
- }
- ext := filepath.Ext(url)
- dest := "Appfile"
- if ext == ".json" {
- dest = "vela.json"
- } else if ext == ".yaml" || ext == ".yml" {
- dest = "vela.yaml"
- }
- //nolint:gosec
- return dest, ioutil.WriteFile(dest, body, 0644)
-}
-
-type buildResult struct {
- appFile *api.AppFile
- application *v1alpha2.Application
- scopes []oam.Object
-}
-
-func (o *AppfileOptions) export(filePath string, quiet bool) (*buildResult, []byte, error) {
- var app *api.AppFile
- var err error
- if !quiet {
- o.IO.Info("Parsing vela appfile ...")
- }
- if filePath != "" {
- if strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "http://") {
- filePath, err = saveRemoteAppfile(filePath)
- if err != nil {
- return nil, nil, err
- }
- }
- app, err = api.LoadFromFile(filePath)
- } else {
- app, err = api.Load()
- }
- if err != nil {
- return nil, nil, err
- }
-
- if !quiet {
- o.IO.Info("Load Template ...")
- }
-
- tm, err := template.Load()
- if err != nil {
- return nil, nil, err
- }
-
- appHandler := appfile.NewApplication(app, tm)
-
- // new
- retApplication, scopes, err := appHandler.BuildOAMApplication(o.Env, o.IO, appHandler.Tm, quiet)
- if err != nil {
- return nil, nil, err
- }
-
- var w bytes.Buffer
-
- enc := k8sjson.NewYAMLSerializer(k8sjson.DefaultMetaFactory, nil, nil)
- err = enc.Encode(retApplication, &w)
- if err != nil {
- return nil, nil, fmt.Errorf("yaml encode application failed: %w", err)
- }
- w.WriteByte('\n')
-
- for _, scope := range scopes {
- w.WriteString("---\n")
- err = enc.Encode(scope, &w)
- if err != nil {
- return nil, nil, fmt.Errorf("yaml encode scope (%s) failed: %w", scope.GetName(), err)
- }
- w.WriteByte('\n')
- }
-
- result := &buildResult{
- appFile: app,
- application: retApplication,
- scopes: scopes,
- }
- return result, w.Bytes(), nil
-}
-
-// Run starts an application according to Appfile
-func (o *AppfileOptions) Run(filePath string) error {
- result, data, err := o.export(filePath, false)
- if err != nil {
- return err
- }
- deployFilePath := ".vela/deploy.yaml"
- o.IO.Infof("Writing deploy config to (%s)\n", deployFilePath)
- if err := os.MkdirAll(filepath.Dir(deployFilePath), 0700); err != nil {
- return err
- }
-
- if err := ioutil.WriteFile(deployFilePath, data, 0600); err != nil {
- return errors.Wrap(err, "write deploy config manifests failed")
- }
-
- if err := o.saveToAppDir(result.appFile); err != nil {
- return errors.Wrap(err, "save to app dir failed")
- }
-
- o.IO.Infof("\nApplying application ...\n")
- return o.ApplyApp(result.application, result.scopes)
-}
-
-func (o *AppfileOptions) saveToAppDir(f *api.AppFile) error {
- app := &api.Application{AppFile: f}
- return appfile.Save(app, o.Env.Name)
-}
-
-// ApplyApp applys config resources for the app.
-// It differs by create and update:
-// - for create, it displays app status along with information of url, metrics, ssh, logging.
-// - for update, it rolls out a canary deployment and prints its information. User can verify the canary deployment.
-// This will wait for user approval. If approved, it continues upgrading the whole; otherwise, it would rollback.
-func (o *AppfileOptions) ApplyApp(app *v1alpha2.Application, scopes []oam.Object) error {
- key := apitypes.NamespacedName{
- Namespace: app.Namespace,
- Name: app.Name,
- }
- o.IO.Infof("Checking if app has been deployed...\n")
- var tmpApp v1alpha2.Application
- err := o.Kubecli.Get(context.TODO(), key, &tmpApp)
- switch {
- case apierrors.IsNotFound(err):
- o.IO.Infof("App has not been deployed, creating a new deployment...\n")
- case err == nil:
- o.IO.Infof("App exists, updating existing deployment...\n")
- default:
- return err
- }
- if err := o.apply(app, scopes); err != nil {
- return err
- }
- o.IO.Infof(o.Info(app))
- return nil
-}
-
-func (o *AppfileOptions) apply(app *v1alpha2.Application, scopes []oam.Object) error {
- if err := appfile.Run(context.TODO(), o.Kubecli, app, scopes); err != nil {
- return err
- }
- return nil
-}
-
-// Info shows the status of each service in the Appfile
-func (o *AppfileOptions) Info(app *v1alpha2.Application) string {
- appName := app.Name
- var appUpMessage = "✅ App has been deployed 🚀🚀🚀\n" +
- fmt.Sprintf(" Port forward: vela port-forward %s\n", appName) +
- fmt.Sprintf(" SSH: vela exec %s\n", appName) +
- fmt.Sprintf(" Logging: vela logs %s\n", appName) +
- fmt.Sprintf(" App status: vela status %s\n", appName)
- for _, comp := range app.Spec.Components {
- appUpMessage += fmt.Sprintf(" Service status: vela status %s --svc %s\n", appName, comp.Name)
- }
- return appUpMessage
-}
diff --git a/pkg/commands/up_test.go b/pkg/commands/up_test.go
index 518a93819..b7970b34a 100644
--- a/pkg/commands/up_test.go
+++ b/pkg/commands/up_test.go
@@ -13,6 +13,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/commands/util"
+ "github.com/oam-dev/kubevela/pkg/serverlib"
)
func TestUp(t *testing.T) {
@@ -23,7 +24,7 @@ func TestUp(t *testing.T) {
Namespace: "env-up",
Issuer: "up",
}
- o := AppfileOptions{
+ o := serverlib.AppfileOptions{
Kubecli: client,
IO: ioStream,
Env: &env,
diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/appconfig_suit_test.go b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/appconfig_suit_test.go
index 5570ef5fc..a3928c0ea 100644
--- a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/appconfig_suit_test.go
+++ b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/appconfig_suit_test.go
@@ -22,7 +22,7 @@ import (
var _ = Describe("CRD without definition can run in an ApplicationConfiguration", func() {
ctx := context.Background()
- It("run workload and trait without CRD", func() {
+ It("create an application without CRD", func() {
By("Creating CRD foo.crdtest1.com")
// Create a crd for appconfig dependency test
diff --git a/pkg/server/apis/types.go b/pkg/server/apis/types.go
index 0baa1cd92..a9e8e8e6a 100644
--- a/pkg/server/apis/types.go
+++ b/pkg/server/apis/types.go
@@ -34,17 +34,6 @@ type CommonFlag struct {
Value string `json:"value"`
}
-// WorkloadRunBody used for restful API arguments for run workload for dashboard restful API server
-type WorkloadRunBody struct {
- EnvName string `json:"envName"`
- WorkloadType string `json:"workloadType"`
- WorkloadName string `json:"workloadName"`
- AppName string `json:"appName,omitempty"`
- Flags []CommonFlag `json:"flags"`
- Staging bool `json:"staging,omitempty"`
- Traits []TraitBody `json:"traits,omitempty"`
-}
-
// WorkloadMeta store workload metadata for dashboard restful API server
type WorkloadMeta struct {
Name string `json:"name"`
diff --git a/pkg/server/appHandlers.go b/pkg/server/appHandlers.go
index 39f4c49ae..cb8e4b7e4 100644
--- a/pkg/server/appHandlers.go
+++ b/pkg/server/appHandlers.go
@@ -1,11 +1,17 @@
package server
import (
+ "fmt"
+ "os"
+
+ "github.com/gin-gonic/gin"
+
+ "github.com/oam-dev/kubevela/pkg/appfile/api"
+ cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/server/util"
"github.com/oam-dev/kubevela/pkg/serverlib"
"github.com/oam-dev/kubevela/pkg/utils/env"
-
- "github.com/gin-gonic/gin"
+ env2 "github.com/oam-dev/kubevela/pkg/utils/env"
)
// UpdateApps is placeholder for updating applications
@@ -57,7 +63,7 @@ func (s *APIServer) ListApps(c *gin.Context) {
util.AssembleResponse(c, applicationMetaList, nil)
}
-// DeleteApps deletes an application by the namespacedname in the gin.Context
+// DeleteApps deletes an application by the namespaced name in the gin.Context
func (s *APIServer) DeleteApps(c *gin.Context) {
envName := c.Param("envName")
envMeta, err := env.GetEnvByName(envName)
@@ -75,3 +81,43 @@ func (s *APIServer) DeleteApps(c *gin.Context) {
message, err := o.DeleteApp()
util.AssembleResponse(c, message, err)
}
+
+// CreateApplication creates an application
+// @tags applications
+// @ID CreateApplication
+// @Summary creates an application
+// @Param envName path string true "environment name"
+// @Param body body appfile.AppFile true "application parameters"
+// @Success 200 {object} apis.Response{code=int,data=string}
+// @Failure 500 {object} apis.Response{code=int,data=string}
+// @Router /envs/{envName}/apps [post]
+func (s *APIServer) CreateApplication(c *gin.Context) {
+ var body api.AppFile
+ if err := c.ShouldBindJSON(&body); err != nil {
+ util.HandleError(c, util.InvalidArgument, "the application creation request body is invalid")
+ return
+ }
+ env, err := env2.GetEnvByName(c.Param("envName"))
+ if err != nil {
+ util.HandleError(c, util.StatusInternalServerError, err.Error())
+ return
+ }
+ ioStream := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
+ o := &serverlib.AppfileOptions{
+ Kubecli: s.KubeClient,
+ IO: ioStream,
+ Env: env,
+ }
+ buildResult, data, err := o.ExportFromAppFile(&body, false)
+ if err != nil {
+ util.HandleError(c, util.StatusInternalServerError, err.Error())
+ return
+ }
+ err = o.BaseAppFileRun(buildResult, data)
+ if err != nil {
+ util.HandleError(c, util.StatusInternalServerError, err.Error())
+ return
+ }
+ msg := fmt.Sprintf("application %s is successfully created", body.Name)
+ util.AssembleResponse(c, msg, nil)
+}
diff --git a/pkg/server/docs/docs.go b/pkg/server/docs/docs.go
index dfbc68182..5918ff17c 100644
--- a/pkg/server/docs/docs.go
+++ b/pkg/server/docs/docs.go
@@ -518,6 +518,75 @@ var doc = `{
}
}
}
+ },
+ "post": {
+ "tags": [
+ "applications"
+ ],
+ "summary": "creates an application",
+ "operationId": "CreateApplication",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "environment name",
+ "name": "envName",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "application parameters",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/appfile.AppFile"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/apis.Response"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/apis.Response"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
}
}
},
@@ -622,6 +691,39 @@ var doc = `{
}
}
},
+ "appfile.AppFile": {
+ "type": "object",
+ "properties": {
+ "createTime": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "secrets": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "services": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/appfile.Service"
+ }
+ },
+ "updateTime": {
+ "type": "string"
+ }
+ }
+ },
+ "appfile.Service": {
+ "type": "object",
+ "additionalProperties": true
+ },
+ "config.Store": {
+ "type": "object"
+ },
"v1alpha2.ApplicationConfiguration": {
"type": "object",
"properties": {
diff --git a/pkg/server/docs/swagger.json b/pkg/server/docs/swagger.json
index 9394c4a51..1e7b19390 100644
--- a/pkg/server/docs/swagger.json
+++ b/pkg/server/docs/swagger.json
@@ -503,6 +503,75 @@
}
}
}
+ },
+ "post": {
+ "tags": [
+ "applications"
+ ],
+ "summary": "creates an application",
+ "operationId": "CreateApplication",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "environment name",
+ "name": "envName",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "application parameters",
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/appfile.AppFile"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/apis.Response"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/apis.Response"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "data": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
}
}
},
@@ -607,6 +676,39 @@
}
}
},
+ "appfile.AppFile": {
+ "type": "object",
+ "properties": {
+ "createTime": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "secrets": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "services": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/appfile.Service"
+ }
+ },
+ "updateTime": {
+ "type": "string"
+ }
+ }
+ },
+ "appfile.Service": {
+ "type": "object",
+ "additionalProperties": true
+ },
+ "config.Store": {
+ "type": "object"
+ },
"v1alpha2.ApplicationConfiguration": {
"type": "object",
"properties": {
diff --git a/pkg/server/docs/swagger.yaml b/pkg/server/docs/swagger.yaml
index 350a76760..ed025fc7e 100644
--- a/pkg/server/docs/swagger.yaml
+++ b/pkg/server/docs/swagger.yaml
@@ -66,6 +66,28 @@ definitions:
code:
type: integer
type: object
+ appfile.AppFile:
+ properties:
+ createTime:
+ type: string
+ name:
+ type: string
+ secrets:
+ additionalProperties:
+ type: string
+ type: object
+ services:
+ additionalProperties:
+ $ref: '#/definitions/appfile.Service'
+ type: object
+ updateTime:
+ type: string
+ type: object
+ appfile.Service:
+ additionalProperties: true
+ type: object
+ config.Store:
+ type: object
v1alpha2.ApplicationConfiguration:
properties:
spec:
@@ -731,4 +753,44 @@ paths:
summary: list all applications
tags:
- applications
+ post:
+ operationId: CreateApplication
+ parameters:
+ - description: environment name
+ in: path
+ name: envName
+ required: true
+ type: string
+ - description: application parameters
+ in: body
+ name: body
+ required: true
+ schema:
+ $ref: '#/definitions/appfile.AppFile'
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/apis.Response'
+ - properties:
+ code:
+ type: integer
+ data:
+ type: string
+ type: object
+ "500":
+ description: Internal Server Error
+ schema:
+ allOf:
+ - $ref: '#/definitions/apis.Response'
+ - properties:
+ code:
+ type: integer
+ data:
+ type: string
+ type: object
+ summary: creates an application
+ tags:
+ - applications
swagger: "2.0"
diff --git a/pkg/server/route.go b/pkg/server/route.go
index 1f9413549..1e93f5e89 100644
--- a/pkg/server/route.go
+++ b/pkg/server/route.go
@@ -80,6 +80,7 @@ func (s *APIServer) setupRoute(staticPath string) http.Handler {
apps.GET("/", s.ListApps)
apps.GET("", s.ListApps)
apps.DELETE("/:appName", s.DeleteApps)
+ apps.POST("/", s.CreateApplication)
// component related operation
components := apps.Group("/:appName/components")
@@ -101,7 +102,6 @@ func (s *APIServer) setupRoute(staticPath string) http.Handler {
// workload related api
workload := api.Group(util.WorkloadDefinitionPath)
{
- workload.POST("/", s.CreateWorkload)
workload.GET("/:workloadName", s.GetWorkload)
workload.PUT("/:workloadName", s.UpdateWorkload)
workload.GET("/", s.ListWorkload)
diff --git a/pkg/server/workloadHandler.go b/pkg/server/workloadHandler.go
index 31e105c2f..e94563bcd 100644
--- a/pkg/server/workloadHandler.go
+++ b/pkg/server/workloadHandler.go
@@ -1,65 +1,14 @@
package server
import (
- "os"
-
"github.com/gin-gonic/gin"
- "github.com/spf13/pflag"
"github.com/oam-dev/kubevela/apis/types"
- cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/plugins"
"github.com/oam-dev/kubevela/pkg/server/apis"
"github.com/oam-dev/kubevela/pkg/server/util"
- "github.com/oam-dev/kubevela/pkg/serverlib"
- env2 "github.com/oam-dev/kubevela/pkg/utils/env"
)
-// CreateWorkload creates a workload
-func (s *APIServer) CreateWorkload(c *gin.Context) {
- var body apis.WorkloadRunBody
- if err := c.ShouldBindJSON(&body); err != nil {
- util.HandleError(c, util.InvalidArgument, "the workload run request body is invalid")
- return
- }
- fs := pflag.NewFlagSet("workload", pflag.ContinueOnError)
- for _, f := range body.Flags {
- fs.String(f.Name, f.Value, "")
- }
- envName := body.EnvName
-
- appObj, err := serverlib.BaseComplete(envName, body.WorkloadName, body.AppName, fs, body.WorkloadType)
- if err != nil {
- util.HandleError(c, util.StatusInternalServerError, err.Error())
- return
- }
- env, err := env2.GetEnvByName(envName)
- if err != nil {
- util.HandleError(c, util.StatusInternalServerError, err.Error())
- return
- }
- io := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
- msg, err := serverlib.BaseRun(body.Staging, appObj, s.KubeClient, env, io)
- if err != nil {
- util.HandleError(c, util.StatusInternalServerError, err.Error())
- return
- }
- if len(body.Traits) == 0 {
- util.AssembleResponse(c, msg, err)
- return
- }
- for _, t := range body.Traits {
- t.AppName = body.AppName
- t.ComponentName = body.WorkloadName
- msg, err = s.DoAttachTrait(c, t)
- if err != nil {
- util.HandleError(c, util.StatusInternalServerError, err.Error())
- return
- }
- }
- util.AssembleResponse(c, msg, err)
-}
-
// UpdateWorkload updates a workload
func (s *APIServer) UpdateWorkload(c *gin.Context) {
}
diff --git a/pkg/serverlib/application.go b/pkg/serverlib/application.go
index 808f5b328..21f665610 100644
--- a/pkg/serverlib/application.go
+++ b/pkg/serverlib/application.go
@@ -1,23 +1,34 @@
package serverlib
import (
+ "bytes"
"context"
"fmt"
+ "io/ioutil"
"os"
+ "path/filepath"
"sort"
+ "strings"
"time"
"github.com/AlecAivazis/survey/v2"
+ "github.com/pkg/errors"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime/serializer/json"
+ apitypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
+ "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
corev1alpha2 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/appfile/api"
+ "github.com/oam-dev/kubevela/pkg/appfile/template"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
+ "github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/server/apis"
+ "github.com/oam-dev/kubevela/pkg/utils/common"
)
// nolint:golint
@@ -31,6 +42,20 @@ const (
type componentMetaList []apis.ComponentMeta
type applicationMetaList []apis.ApplicationMeta
+// AppfileOptions is some configuration that modify options for an Appfile
+type AppfileOptions struct {
+ Kubecli client.Client
+ IO cmdutil.IOStreams
+ Env *types.EnvMeta
+}
+
+// BuildResult is the export struct from AppFile yaml or AppFile object
+type BuildResult struct {
+ appFile *api.AppFile
+ application *v1alpha2.Application
+ scopes []oam.Object
+}
+
func (comps componentMetaList) Len() int {
return len(comps)
}
@@ -298,3 +323,172 @@ func GetServicesWhenDescribingApplication(cmd *cobra.Command, app *api.Applicati
}
return targetServices, nil
}
+
+func saveRemoteAppfile(url string) (string, error) {
+ body, err := common.HTTPGet(context.Background(), url)
+ if err != nil {
+ return "", err
+ }
+ ext := filepath.Ext(url)
+ dest := "Appfile"
+ if ext == ".json" {
+ dest = "vela.json"
+ } else if ext == ".yaml" || ext == ".yml" {
+ dest = "vela.yaml"
+ }
+ //nolint:gosec
+ return dest, ioutil.WriteFile(dest, body, 0644)
+}
+
+// ExportFromAppFile exports Application from appfile object
+func (o *AppfileOptions) ExportFromAppFile(app *api.AppFile, quiet bool) (*BuildResult, []byte, error) {
+ tm, err := template.Load()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ appHandler := appfile.NewApplication(app, tm)
+
+ // new
+ retApplication, scopes, err := appHandler.BuildOAMApplication(o.Env, o.IO, appHandler.Tm, quiet)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var w bytes.Buffer
+
+ options := json.SerializerOptions{Yaml: true, Pretty: false, Strict: false}
+ enc := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, options)
+ err = enc.Encode(retApplication, &w)
+ if err != nil {
+ return nil, nil, fmt.Errorf("yaml encode application failed: %w", err)
+ }
+ w.WriteByte('\n')
+
+ for _, scope := range scopes {
+ w.WriteString("---\n")
+ err = enc.Encode(scope, &w)
+ if err != nil {
+ return nil, nil, fmt.Errorf("yaml encode scope (%s) failed: %w", scope.GetName(), err)
+ }
+ w.WriteByte('\n')
+ }
+
+ result := &BuildResult{
+ appFile: app,
+ application: retApplication,
+ scopes: scopes,
+ }
+ return result, w.Bytes(), nil
+}
+
+// Export export Application object from the path of Appfile
+func (o *AppfileOptions) Export(filePath string, quiet bool) (*BuildResult, []byte, error) {
+ var app *api.AppFile
+ var err error
+ if !quiet {
+ o.IO.Info("Parsing vela appfile ...")
+ }
+ if filePath != "" {
+ if strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "http://") {
+ filePath, err = saveRemoteAppfile(filePath)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+ app, err = api.LoadFromFile(filePath)
+ } else {
+ app, err = api.Load()
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if !quiet {
+ o.IO.Info("Load Template ...")
+ }
+ return o.ExportFromAppFile(app, quiet)
+}
+
+// Run starts an application according to Appfile
+func (o *AppfileOptions) Run(filePath string) error {
+ result, data, err := o.Export(filePath, false)
+ if err != nil {
+ return err
+ }
+ return o.BaseAppFileRun(result, data)
+}
+
+// BaseAppFileRun starts an application according to Appfile
+func (o *AppfileOptions) BaseAppFileRun(result *BuildResult, data []byte) error {
+ deployFilePath := ".vela/deploy.yaml"
+ o.IO.Infof("Writing deploy config to (%s)\n", deployFilePath)
+ if err := os.MkdirAll(filepath.Dir(deployFilePath), 0700); err != nil {
+ return err
+ }
+
+ if err := ioutil.WriteFile(deployFilePath, data, 0600); err != nil {
+ return errors.Wrap(err, "write deploy config manifests failed")
+ }
+
+ if err := o.saveToAppDir(result.appFile); err != nil {
+ return errors.Wrap(err, "save to app dir failed")
+ }
+
+ o.IO.Infof("\nApplying application ...\n")
+ return o.ApplyApp(result.application, result.scopes)
+}
+
+func (o *AppfileOptions) saveToAppDir(f *api.AppFile) error {
+ app := &api.Application{AppFile: f}
+ return appfile.Save(app, o.Env.Name)
+}
+
+// ApplyApp applys config resources for the app.
+// It differs by create and update:
+// - for create, it displays app status along with information of url, metrics, ssh, logging.
+// - for update, it rolls out a canary deployment and prints its information. User can verify the canary deployment.
+// This will wait for user approval. If approved, it continues upgrading the whole; otherwise, it would rollback.
+func (o *AppfileOptions) ApplyApp(app *v1alpha2.Application, scopes []oam.Object) error {
+ key := apitypes.NamespacedName{
+ Namespace: app.Namespace,
+ Name: app.Name,
+ }
+ o.IO.Infof("Checking if app has been deployed...\n")
+ var tmpApp v1alpha2.Application
+ err := o.Kubecli.Get(context.TODO(), key, &tmpApp)
+ switch {
+ case apierrors.IsNotFound(err):
+ o.IO.Infof("App has not been deployed, creating a new deployment...\n")
+ case err == nil:
+ o.IO.Infof("App exists, updating existing deployment...\n")
+ default:
+ return err
+ }
+ if err := o.apply(app, scopes); err != nil {
+ return err
+ }
+ o.IO.Infof(o.Info(app))
+ return nil
+}
+
+func (o *AppfileOptions) apply(app *v1alpha2.Application, scopes []oam.Object) error {
+ if err := appfile.Run(context.TODO(), o.Kubecli, app, scopes); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Info shows the status of each service in the Appfile
+func (o *AppfileOptions) Info(app *v1alpha2.Application) string {
+ appName := app.Name
+ var appUpMessage = "✅ App has been deployed 🚀🚀🚀\n" +
+ fmt.Sprintf(" Port forward: vela port-forward %s\n", appName) +
+ fmt.Sprintf(" SSH: vela exec %s\n", appName) +
+ fmt.Sprintf(" Logging: vela logs %s\n", appName) +
+ fmt.Sprintf(" App status: vela status %s\n", appName)
+ for _, comp := range app.Spec.Components {
+ appUpMessage += fmt.Sprintf(" Service status: vela status %s --svc %s\n", appName, comp.Name)
+ }
+ return appUpMessage
+}