diff --git a/e2e/apiserver/apiserver_test.go b/e2e/apiserver/apiserver_test.go index 1054ffa11..6b394ef15 100644 --- a/e2e/apiserver/apiserver_test.go +++ b/e2e/apiserver/apiserver_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" "github.com/cloud-native-application/rudrx/e2e" @@ -19,15 +20,33 @@ import ( "github.com/onsi/ginkgo" ) -var envHelloMeta = types.EnvMeta{ - Name: "env-e2e-hello", - Namespace: "env-e2e-hello", -} +var ( + envHelloMeta = types.EnvMeta{ + Name: "env-e2e-hello", + Namespace: "env-e2e-hello", + } -var envWorldMeta = types.EnvMeta{ - Name: "env-e2e-world", - Namespace: "env-e2e-world", -} + envWorldMeta = types.EnvMeta{ + Name: "env-e2e-world", + Namespace: "env-e2e-world", + } + + workloadType = "containerized" + workloadName = "app-e2e-api-hello" + + workloadRunBodyWithoutImageFlag = apis.WorkloadRunBody{ + EnvName: envHelloMeta.Name, + WorkloadName: workloadName, + WorkloadType: workloadType, + Flags: []apis.WorkloadFlag{{Name: "port", Value: "80"}}, + } + workloadRunBody = apis.WorkloadRunBody{ + EnvName: envHelloMeta.Name, + WorkloadName: workloadName, + WorkloadType: workloadType, + Flags: []apis.WorkloadFlag{{Name: "image", Value: "nginx:1.9.4"}, {Name: "port", Value: "80"}}, + } +) var notExistedEnvMeta = types.EnvMeta{ Name: "env-e2e-api-NOT-EXISTED-JUST-FOR-TEST", @@ -119,3 +138,38 @@ var _ = ginkgo.Describe("API Env", func() { }) }) }) + +var _ = ginkgo.Describe("API Workload", func() { + + ginkgo.Context("Post /workloads/", func() { + ginkgo.It("run workload", func() { + data, err := json.Marshal(&workloadRunBody) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + resp, err := http.Post(util.URL("/workloads/"), "application/json", strings.NewReader(string(data))) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + defer resp.Body.Close() + result, err := ioutil.ReadAll(resp.Body) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + var r apis.Response + err = json.Unmarshal(result, &r) + gomega.Expect(http.StatusOK).Should(gomega.Equal(r.Code)) + output := fmt.Sprintf("Creating App %s\nSUCCEED", workloadName) + gomega.Expect(r.Data.(string)).To(gomega.ContainSubstring(output)) + }) + + ginkgo.It("run workload without compulsory flag", func() { + data, err := json.Marshal(&workloadRunBodyWithoutImageFlag) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + resp, err := http.Post(util.URL("/workloads/"), "application/json", strings.NewReader(string(data))) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + defer resp.Body.Close() + result, err := ioutil.ReadAll(resp.Body) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + var r apis.Response + err = json.Unmarshal(result, &r) + gomega.Expect(http.StatusOK).Should(gomega.Equal(r.Code)) + output := fmt.Sprintf("required flag(s) \"image\" not set") + gomega.Expect(r.Data.(string)).To(gomega.ContainSubstring(output)) + }) + }) +}) diff --git a/e2e/application/application_test.go b/e2e/application/application_test.go index 66d204182..c2cf48e44 100644 --- a/e2e/application/application_test.go +++ b/e2e/application/application_test.go @@ -9,6 +9,7 @@ import ( var ( envName = "env-application" + workloadType = "containerized" applicationName = "app-basic" traitAlias = "scale" ) @@ -17,10 +18,12 @@ var _ = ginkgo.Describe("Application", func() { e2e.EnvInitContext("env init", envName) e2e.EnvShowContext("env show", envName) e2e.EnvSwitchContext("env switch", envName) - e2e.WorkloadRunContext("run", fmt.Sprintf("vela containerized:run %s -p 80 --image nginx:1.9.4", applicationName)) + e2e.WorkloadRunContext("run", fmt.Sprintf("vela %s:run %s -p 80 --image nginx:1.9.4", + workloadType, applicationName)) e2e.ApplicationListContext("app ls", applicationName, "") e2e.TraitManualScalerAttachContext("vela attach trait", traitAlias, applicationName) //e2e.ApplicationListContext("app ls", applicationName, traitAlias) - e2e.ApplicationStatusContext("app status", applicationName) + e2e.ApplicationShowContext("app show", applicationName, workloadType) + e2e.ApplicationStatusContext("app status", applicationName, workloadType) e2e.WorkloadDeleteContext("delete", applicationName) }) diff --git a/e2e/commonContext.go b/e2e/commonContext.go index 97d9ab31c..13243f99d 100644 --- a/e2e/commonContext.go +++ b/e2e/commonContext.go @@ -134,6 +134,7 @@ var ( }) }) } + WorkloadDeleteContext = func(context string, applicationName string) bool { return ginkgo.Context(context, func() { ginkgo.It("should print successful deletion information", func() { @@ -173,18 +174,33 @@ var ( }) } - ApplicationStatusContext = func(context string, applicationName string) bool { + ApplicationStatusContext = func(context string, applicationName string, workloadType string) bool { return ginkgo.Context(context, func() { ginkgo.It("should get status for the application", func() { cli := fmt.Sprintf("vela app:status %s", applicationName) output, err := Exec(cli) gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(output).To(gomega.ContainSubstring(applicationName)) + // TODO(zzxwill) need to check workloadType after app:status is refined + //gomega.Expect(output).To(gomega.ContainSubstring(workloadType)) gomega.Expect(output).To(gomega.ContainSubstring("Workload")) }) }) } + ApplicationShowContext = func(context string, applicationName string, workloadType string) bool { + return ginkgo.Context(context, func() { + ginkgo.It("should show app information", func() { + cli := fmt.Sprintf("vela app:show %s", applicationName) + output, err := Exec(cli) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + // TODO(zzxwill) need to check workloadType after app:show is refined + //gomega.Expect(output).To(gomega.ContainSubstring(workloadType)) + gomega.Expect(output).To(gomega.ContainSubstring(applicationName)) + }) + }) + } + // APIServer APIEnvInitContext = func(context string, envMeta types.EnvMeta) bool { return ginkgo.Context("Post /envs/", func() { diff --git a/e2e/workload/workload_test.go b/e2e/workload/workload_test.go index 80b1fb090..25418a891 100644 --- a/e2e/workload/workload_test.go +++ b/e2e/workload/workload_test.go @@ -5,11 +5,13 @@ import ( "github.com/cloud-native-application/rudrx/e2e" "github.com/onsi/ginkgo" + "github.com/onsi/gomega" ) var ( - envName = "env-workload" - applicationName = "app-testworkloadrun-basic" + envName = "env-workload" + applicationName = "app-workload-basic" + notEnoughFlagsApplicationName = "app-workload-basic" ) var _ = ginkgo.Describe("Workload", func() { @@ -17,5 +19,15 @@ var _ = ginkgo.Describe("Workload", func() { e2e.EnvInitContext("env init", envName) e2e.EnvSwitchContext("env switch", envName) e2e.WorkloadRunContext("run", fmt.Sprintf("vela containerized:run %s -p 80 --image nginx:1.9.4", applicationName)) + + ginkgo.Context("run without enough flags", func() { + ginkgo.It("should throw error message: some flags are NOT set", func() { + cli := fmt.Sprintf("vela containerized:run %s -p 80", notEnoughFlagsApplicationName) + output, err := e2e.Exec(cli) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(output).To(gomega.ContainSubstring("required flag(s) \"image\" not set")) + }) + }) + e2e.WorkloadDeleteContext("delete", applicationName) }) diff --git a/go.mod b/go.mod index aaba9ec09..5ac3fe705 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/satori/go.uuid v1.2.0 github.com/spf13/cobra v1.0.0 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.6.1 go.uber.org/zap v1.10.0 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 diff --git a/pkg/cmd/workload_run.go b/pkg/cmd/workload_run.go index c60c51bfb..4ddef693e 100644 --- a/pkg/cmd/workload_run.go +++ b/pkg/cmd/workload_run.go @@ -3,15 +3,13 @@ package cmd import ( "context" "errors" - "fmt" - "strconv" + + "github.com/cloud-native-application/rudrx/pkg/oam" "github.com/cloud-native-application/rudrx/api/types" - "github.com/cloud-native-application/rudrx/pkg/application" "github.com/cloud-native-application/rudrx/pkg/cmd/util" "github.com/cloud-native-application/rudrx/pkg/plugins" - "cuelang.org/go/cue" "github.com/spf13/cobra" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -19,16 +17,7 @@ import ( const Staging = "staging" const App = "app" -type runOptions struct { - Template types.Capability - Env *types.EnvMeta - workloadName string - client client.Client - app *application.Application - appName string - staging bool - util.IOStreams -} +type runOptions oam.RunOptions func newRunOptions(ioStreams util.IOStreams) *runOptions { return &runOptions{IOStreams: ioStreams} @@ -53,11 +42,12 @@ func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams util. Example: "vela " + name + ":run frontend", RunE: func(cmd *cobra.Command, args []string) error { o := newRunOptions(ioStreams) + o.WorkloadType = name newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema}) if err != nil { return err } - o.client = newClient + o.KubeClient = newClient o.Env, err = GetEnv(cmd) if err != nil { return err @@ -89,48 +79,15 @@ func (o *runOptions) Complete(cmd *cobra.Command, args []string, ctx context.Con if argsLength < 1 { return errors.New("must specify name for workload") } - o.workloadName = args[0] - if app := cmd.Flag(App).Value.String(); app != "" { - o.appName = app - } else { - o.appName = o.workloadName - } - app, err := application.Load(o.Env.Name, o.appName) - if err != nil { - return err - } - app.Name = o.appName + workloadName := args[0] + template := o.Template + appGroup := cmd.Flag(App).Value.String() - if app.Components == nil { - app.Components = make(map[string]map[string]interface{}) - } - tp, workloadData := app.GetWorkload(o.workloadName) - if tp == "" { - // Not exist - tp = o.Template.Name - } - - for _, v := range o.Template.Parameters { - flagSet := cmd.Flag(v.Name) - switch v.Type { - case cue.IntKind: - d, _ := strconv.ParseInt(flagSet.Value.String(), 10, 64) - workloadData[v.Name] = d - case cue.StringKind: - workloadData[v.Name] = flagSet.Value.String() - case cue.BoolKind: - d, _ := strconv.ParseBool(flagSet.Value.String()) - workloadData[v.Name] = d - case cue.NumberKind, cue.FloatKind: - d, _ := strconv.ParseFloat(flagSet.Value.String(), 64) - workloadData[v.Name] = d - } - } - if err = app.SetWorkload(o.workloadName, tp, workloadData); err != nil { - return err - } - o.app = app - return app.Save(o.Env.Name, o.appName) + envName := o.Env.Name + var flagSet = cmd.Flags() + app, err := oam.BaseComplete(envName, workloadName, appGroup, flagSet, template) + o.App = app + return err } func (o *runOptions) Run(cmd *cobra.Command) error { @@ -138,14 +95,10 @@ func (o *runOptions) Run(cmd *cobra.Command) error { if err != nil { return err } - if staging { - o.Info("Staging saved") - return nil + msg, err := oam.BaseRun(staging, o.App, o.KubeClient, o.Env) + if err != nil { + return err } - o.Infof("Creating App %s\n", o.app.Name) - if err := o.app.Run(context.Background(), o.client, o.Env); err != nil { - return fmt.Errorf("create app err: %s", err) - } - o.Info("SUCCEED") + o.Info(msg) return nil } diff --git a/pkg/oam/env.go b/pkg/oam/env.go index 5b7cfa7f9..67fc2e3d4 100644 --- a/pkg/oam/env.go +++ b/pkg/oam/env.go @@ -3,7 +3,6 @@ package oam import ( "context" "encoding/json" - "errors" "fmt" "io/ioutil" "os" @@ -74,7 +73,7 @@ func ListEnvs(envName string) ([]*types.EnvMeta, error) { env, err := GetEnvByName(envName) if err != nil { if os.IsNotExist(err) { - err = errors.New(fmt.Sprintf("env %s not exist", envName)) + err = fmt.Errorf("env %s not exist", envName) } return envList, err } diff --git a/pkg/oam/trait.go b/pkg/oam/trait.go index b74b28cf2..ea1c181d6 100644 --- a/pkg/oam/trait.go +++ b/pkg/oam/trait.go @@ -3,7 +3,6 @@ package oam import ( "context" "encoding/json" - "errors" "fmt" "io/ioutil" "os" @@ -80,5 +79,5 @@ func GetTraitDefinitionByKind(ctx context.Context, c client.Client, traitKind st return t, nil } } - return traitDefinition, errors.New(fmt.Sprintf("Could not find TraitDefinition by kind %s", traitKind)) + return traitDefinition, fmt.Errorf("could not find TraitDefinition by kind %s", traitKind) } diff --git a/pkg/oam/workload.go b/pkg/oam/workload.go new file mode 100644 index 000000000..faf5fd4bf --- /dev/null +++ b/pkg/oam/workload.go @@ -0,0 +1,90 @@ +package oam + +import ( + "context" + "fmt" + "strconv" + + "github.com/spf13/pflag" + + "github.com/cloud-native-application/rudrx/api/types" + "github.com/cloud-native-application/rudrx/pkg/cmd/util" + "sigs.k8s.io/controller-runtime/pkg/client" + + "cuelang.org/go/cue" + "github.com/cloud-native-application/rudrx/pkg/application" +) + +type RunOptions struct { + Template types.Capability + Env *types.EnvMeta + WorkloadType string + WorkloadName string + KubeClient client.Client + App *application.Application + AppName string + Staging bool + util.IOStreams +} + +func BaseComplete(envName string, workloadName string, appGroup string, flagSet *pflag.FlagSet, template types.Capability) (*application.Application, error) { + var appName string + if appGroup != "" { + appName = appGroup + } else { + appName = workloadName + } + app, err := application.Load(envName, appName) + if err != nil { + return app, err + } + app.Name = appName + + if app.Components == nil { + app.Components = make(map[string]map[string]interface{}) + } + tp, workloadData := app.GetWorkload(workloadName) + if tp == "" { + // Not exist + tp = template.Name + } + + for _, v := range template.Parameters { + flagValue, _ := flagSet.GetString(v.Name) + // Cli can check required flag before make a request to backend, but API itself could not, so validate flags here + if v.Required && v.Name != "name" && flagValue == "" { + return app, fmt.Errorf("required flag(s) \"%s\" not set", v.Name) + } + switch v.Type { + case cue.IntKind: + d, _ := strconv.ParseInt(flagValue, 10, 64) + workloadData[v.Name] = d + case cue.StringKind: + workloadData[v.Name] = flagValue + case cue.BoolKind: + d, _ := strconv.ParseBool(flagValue) + workloadData[v.Name] = d + case cue.NumberKind, cue.FloatKind: + d, _ := strconv.ParseFloat(flagValue, 64) + workloadData[v.Name] = d + } + } + if err = app.SetWorkload(workloadName, tp, workloadData); err != nil { + return app, err + } + return app, app.Save(envName, appName) +} + +func BaseRun(staging bool, App *application.Application, kubeClient client.Client, Env *types.EnvMeta) (string, error) { + if staging { + return "Staging saved", nil + } + var msg string + msg = fmt.Sprintf("Creating App %s\n", App.Name) + if err := App.Run(context.Background(), kubeClient, Env); err != nil { + err = fmt.Errorf("create app err: %s", err) + return "", err + } + msg += "SUCCEED" + return msg, nil +} diff --git a/pkg/server/apis/types.go b/pkg/server/apis/types.go index 1ecedbcdc..85c0574ac 100644 --- a/pkg/server/apis/types.go +++ b/pkg/server/apis/types.go @@ -1,6 +1,8 @@ package apis -import "k8s.io/apimachinery/pkg/runtime" +import ( + "k8s.io/apimachinery/pkg/runtime" +) type Environment struct { EnvironmentName string `json:"environmentName" binding:"required,min=1,max=32"` @@ -18,3 +20,15 @@ type Response struct { Code int `json:"code"` Data interface{} `json:"data"` } +type WorkloadFlag struct { + Name string `json:"name"` + Value string `json:"value"` +} +type WorkloadRunBody struct { + EnvName string `json:"env_name"` + WorkloadType string `json:"workload_type"` + WorkloadName string `json:"workload_name"` + AppGroup string `json:"app_group"` + Flags []WorkloadFlag `json:"flags"` + Staging bool `json:"staging"` +} diff --git a/pkg/server/handler/workloadHandler.go b/pkg/server/handler/workloadHandler.go index f407af66e..a614697d1 100644 --- a/pkg/server/handler/workloadHandler.go +++ b/pkg/server/handler/workloadHandler.go @@ -1,9 +1,49 @@ package handler -import "github.com/gin-gonic/gin" +import ( + "github.com/cloud-native-application/rudrx/api/types" + "github.com/cloud-native-application/rudrx/pkg/oam" + "github.com/cloud-native-application/rudrx/pkg/plugins" + "github.com/spf13/pflag" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/cloud-native-application/rudrx/pkg/server/apis" + "github.com/cloud-native-application/rudrx/pkg/server/util" + "github.com/gin-gonic/gin" +) // Workload related handlers func CreateWorkload(c *gin.Context) { + kubeClient := c.MustGet("KubeClient") + 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, "") + } + evnName := body.EnvName + var template types.Capability + + template, err := plugins.LoadCapabilityByName(body.WorkloadType) + appObj, err := oam.BaseComplete(evnName, body.WorkloadName, body.AppGroup, fs, template) + if err != nil { + util.HandleError(c, util.StatusInternalServerError, err.Error()) + return + } + env, err := oam.GetEnvByName(evnName) + if err != nil { + util.HandleError(c, util.StatusInternalServerError, err.Error()) + return + } + msg, err := oam.BaseRun(body.Staging, appObj, kubeClient.(client.Client), env) + if err != nil { + util.HandleError(c, util.StatusInternalServerError, err.Error()) + return + } + util.AssembleResponse(c, msg, err) } func UpdateWorkload(c *gin.Context) {