diff --git a/DESIGN.md b/DESIGN.md index e4763efc5..478ff585b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -141,10 +141,11 @@ services: express-server: type: webservice # workload type build: - image: oamdev/testapp:v1 docker: file: Dockerfile context: . + + image: oamdev/testapp:v1 cmd: ["node", "server.js"] ports: - 8080:80 diff --git a/charts/vela-core/templates/definitions/metricstraits.yaml b/charts/vela-core/templates/definitions/metricstraits.yaml index cf1e094ff..f3fa94688 100644 --- a/charts/vela-core/templates/definitions/metricstraits.yaml +++ b/charts/vela-core/templates/definitions/metricstraits.yaml @@ -49,4 +49,5 @@ spec: } } parameter: #metrics + output: {} diff --git a/design/appfile-design.md b/design/appfile-design.md index 1ae880f4b..c01bdccd9 100644 --- a/design/appfile-design.md +++ b/design/appfile-design.md @@ -24,8 +24,11 @@ Here's an example to deploy a NodeJS express service: ```yaml services: express-server: + # this image will be used in both build and deploy config + image: oamdev/testapp:v1 + build: - image: oamdev/testapp:v1 + # Here more runtime specific build templates will be supported, like NodeJS, Go, Python, Ruby. docker: file: Dockerfile context: . @@ -93,6 +96,8 @@ spec: env: [...string] files: [...string] + + image: string } output: { @@ -112,7 +117,7 @@ spec: spec: { containers: [{ name: context.name - image: context.image + image: parameter.image command: parameter.cmd }] } @@ -217,7 +222,7 @@ output: { ... containers: [{ name: context.name - image: context.image + image: parameter.image command: parameter.cmd }] } @@ -231,7 +236,6 @@ Here is the takeout: ```yaml context: name: express-server - image: oamdev/testapp:v1 ``` You can check the definition of `context` block via `vela template context`. diff --git a/e2e/trait/trait_test.go b/e2e/trait/trait_test.go index 9688be9ea..5db3d942f 100644 --- a/e2e/trait/trait_test.go +++ b/e2e/trait/trait_test.go @@ -30,7 +30,7 @@ var _ = ginkgo.Describe("Trait", func() { cli := fmt.Sprintf("vela %s %s", traitAlias, applicationNotExistedName) output, err := e2e.Exec(cli) gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(output).To(gomega.ContainSubstring("Error: " + applicationNotExistedName + " not exist")) + gomega.Expect(output).To(gomega.ContainSubstring("component name (" + applicationNotExistedName + ") doesn't exist")) }) }) diff --git a/examples/testapp/vela.yml b/examples/testapp/vela.yml index d18a84980..0df5d2b67 100644 --- a/examples/testapp/vela.yml +++ b/examples/testapp/vela.yml @@ -3,13 +3,18 @@ name: testapp services: express-server: + # this image will be used in both build and deploy config + image: oamdev/testapp:v1 + build: - image: oamdev/testapp:v1 + # Here more runtime specific build templates will be supported, like NodeJS, Go, Python, Ruby. docker: file: Dockerfile context: . - push: # without any setting, by default push image directly - local: kind + + # Uncomment the following to push to local kind cluster + # push: + # local: kind # type: webservice (default) | task diff --git a/hack/appfile/cue-templates/webservice.cue b/hack/appfile/cue-templates/webservice.cue index 2e99a218b..3c83564c5 100644 --- a/hack/appfile/cue-templates/webservice.cue +++ b/hack/appfile/cue-templates/webservice.cue @@ -8,6 +8,8 @@ parameter: #webservice env: [...string] files: [...string] + + image: string } output: { @@ -27,7 +29,7 @@ output: { spec: { containers: [{ name: context.name - image: context.image + image: parameter.image command: parameter.cmd }] } diff --git a/hack/appfile/deploy/webservice.yaml b/hack/appfile/deploy/webservice.yaml index a62f9b9bf..f122d2682 100644 --- a/hack/appfile/deploy/webservice.yaml +++ b/hack/appfile/deploy/webservice.yaml @@ -17,6 +17,8 @@ spec: env: [...string] files: [...string] + + image: string } output: { @@ -36,7 +38,7 @@ spec: spec: { containers: [{ name: context.name - image: context.image + image: parameter.image command: parameter.cmd }] } diff --git a/pkg/appfile/appfile.go b/pkg/appfile/appfile.go index f2b69cecf..ac93cb58b 100644 --- a/pkg/appfile/appfile.go +++ b/pkg/appfile/appfile.go @@ -13,6 +13,10 @@ import ( cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" ) +var ( + ErrImageNotDefined = errors.New("image not defined") +) + const DefaultAppfilePath = "./vela.yml" type AppFile struct { @@ -49,24 +53,19 @@ func LoadFromFile(filename string) (*AppFile, error) { } // BuildOAM renders Appfile into AppConfig, Components. It also builds images for services if defined. -func (app *AppFile) BuildOAM(ns string, io cmdutil.IOStreams) ( +func (app *AppFile) BuildOAM(ns string, io cmdutil.IOStreams, tm template.Manager) ( []*v1alpha2.Component, *v1alpha2.ApplicationConfiguration, error) { - return app.buildOAM(ns, io, true) + return app.buildOAM(ns, io, true, tm) } // RenderOAM renders Appfile into AppConfig, Components. -func (app *AppFile) RenderOAM(ns string, io cmdutil.IOStreams) ( +func (app *AppFile) RenderOAM(ns string, io cmdutil.IOStreams, tm template.Manager) ( []*v1alpha2.Component, *v1alpha2.ApplicationConfiguration, error) { - return app.buildOAM(ns, io, false) + return app.buildOAM(ns, io, false, tm) } -func (app *AppFile) buildOAM(ns string, io cmdutil.IOStreams, buildImage bool) ( +func (app *AppFile) buildOAM(ns string, io cmdutil.IOStreams, buildImage bool, tm template.Manager) ( []*v1alpha2.Component, *v1alpha2.ApplicationConfiguration, error) { - io.Info("Loading templates ...") - tm, err := template.Load() - if err != nil { - return nil, nil, err - } appConfig := &v1alpha2.ApplicationConfiguration{ ObjectMeta: metav1.ObjectMeta{ @@ -78,26 +77,22 @@ func (app *AppFile) buildOAM(ns string, io cmdutil.IOStreams, buildImage bool) ( var comps []*v1alpha2.Component for sname, svc := range app.GetServices() { - build := svc.GetBuild() var image string - if build != nil { - image = build.Image + v, ok := svc["image"] + if ok { + image = v.(string) + } else { + return nil, nil, ErrImageNotDefined + } + if b := svc.GetBuild(); b != nil { if buildImage { io.Infof("\nBuilding service (%s)...\n", sname) - if err := build.BuildImage(io); err != nil { + if err := b.BuildImage(io, image); err != nil { return nil, nil, err } } } - if image == "" { - v, ok := svc["image"] - if ok { - image = v.(string) - } else { - return nil, nil, errors.New("no image is defined") - } - } io.Infof("\nRendering component configs for service (%s)...\n", sname) acComp, comp, err := svc.RenderService(tm, sname, ns, image) diff --git a/pkg/appfile/appfile_test.go b/pkg/appfile/appfile_test.go new file mode 100644 index 000000000..2abd364ae --- /dev/null +++ b/pkg/appfile/appfile_test.go @@ -0,0 +1,349 @@ +package appfile + +import ( + "os" + "testing" + + "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/oam-dev/kubevela/api/types" + "github.com/oam-dev/kubevela/pkg/appfile/template" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" +) + +func TestRenderOAM(t *testing.T) { + yamlOneService := `name: myapp +services: + express-server: + image: oamdev/testapp:v1 + cmd: ["node", "server.js"] + route: + domain: example.com + http: + "/": 8080 +` + yamlTwoServices := yamlOneService + ` + mongodb: + type: backend + image: bitnami/mongodb:3.6.20 + cmd: ["mongodb"] +` + yamlNoImage := `name: myapp +services: + bad-server: + cmd: ["node", "server.js"] +` + + templateWebservice := `parameter: #webservice +#webservice: { + cmd: [...string] + image: string +} + +output: { + apiVersion: "test.oam.dev/v1" + kind: "WebService" + metadata: { + name: context.name + } + spec: { + image: parameter.image + command: parameter.cmd + } +} +` + templateBackend := `parameter: #backend +#backend: { + cmd: [...string] + image: string +} + +output: { + apiVersion: "test.oam.dev/v1" + kind: "Worker" + metadata: { + name: context.name + } + spec: { + image: parameter.image + command: parameter.cmd + } +}` + templateRoute := `parameter: #route +#route: { + domain: string + http: [string]: int +} + +// trait template can have multiple outputs and they are all traits +outputs: service: { + apiVersion: "v1" + kind: "Service" + metadata: + name: context.name + spec: { + selector: + app: context.name + ports: [ + for k, v in parameter.http { + port: v + targetPort: v + } + ] + } +} + +outputs: ingress: { + apiVersion: "networking.k8s.io/v1beta1" + kind: "Ingress" + spec: { + rules: [{ + host: parameter.domain + http: { + paths: [ + for k, v in parameter.http { + path: k + backend: { + serviceName: context.name + servicePort: v + } + } + ] + } + }] + } +} +` + ac1 := &v1alpha2.ApplicationConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myapp", + Namespace: "default", + }, + Spec: v1alpha2.ApplicationConfigurationSpec{ + Components: []v1alpha2.ApplicationConfigurationComponent{{ + ComponentName: "express-server", + Traits: []v1alpha2.ComponentTrait{{ + Trait: runtime.RawExtension{ + Object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "express-server", + }, + "spec": map[string]interface{}{ + "selector": map[string]interface{}{ + "app": "express-server", + }, + "ports": []interface{}{ + map[string]interface{}{ + "port": float64(8080), + "targetPort": float64(8080), + }, + }, + }, + }, + }, + }, + }, { + Trait: runtime.RawExtension{ + Object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "networking.k8s.io/v1beta1", + "kind": "Ingress", + "spec": map[string]interface{}{ + "rules": []interface{}{ + map[string]interface{}{ + "http": map[string]interface{}{ + "paths": []interface{}{ + map[string]interface{}{ + "path": "/", + "backend": map[string]interface{}{ + "serviceName": "express-server", + "servicePort": float64(8080), + }, + }, + }, + }, + "host": "example.com", + }, + }, + }, + }, + }, + }, + }}, + }}, + }, + } + ac2 := ac1.DeepCopy() + ac2.Spec.Components = append(ac2.Spec.Components, v1alpha2.ApplicationConfigurationComponent{ + ComponentName: "mongodb", + Traits: []v1alpha2.ComponentTrait{}, + }) + + comp1 := &v1alpha2.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "express-server", + Namespace: "default", + }, + Spec: v1alpha2.ComponentSpec{ + Workload: runtime.RawExtension{ + Object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "test.oam.dev/v1", + "kind": "WebService", + "metadata": map[string]interface{}{"name": "express-server"}, + "spec": map[string]interface{}{ + "image": "oamdev/testapp:v1", + "command": []interface{}{"node", "server.js"}, + }, + }, + }, + }, + }, + } + + comp2 := &v1alpha2.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mongodb", + Namespace: "default", + }, + Spec: v1alpha2.ComponentSpec{ + Workload: runtime.RawExtension{ + Object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "test.oam.dev/v1", + "kind": "Worker", + "metadata": map[string]interface{}{"name": "mongodb"}, + "spec": map[string]interface{}{ + "image": "bitnami/mongodb:3.6.20", + "command": []interface{}{"mongodb"}, + }, + }, + }, + }, + }, + } + + type args struct { + appfileData string + workloadTemplates map[string]string + traitTemplates map[string]string + } + type want struct { + components []*v1alpha2.Component + appConfig *v1alpha2.ApplicationConfiguration + err error + } + cases := map[string]struct { + args args + want want + }{ + "one service should generate one component and one appconfig": { + args: args{ + appfileData: yamlOneService, + workloadTemplates: map[string]string{ + "webservice": templateWebservice, + }, + traitTemplates: map[string]string{ + "route": templateRoute, + }, + }, + want: want{ + appConfig: ac1, + components: []*v1alpha2.Component{comp1}, + }, + }, + "two services should generate two components and one appconfig": { + args: args{ + appfileData: yamlTwoServices, + workloadTemplates: map[string]string{ + "webservice": templateWebservice, + "backend": templateBackend, + }, + traitTemplates: map[string]string{ + "route": templateRoute, + }, + }, + want: want{ + appConfig: ac2, + components: []*v1alpha2.Component{comp1, comp2}, + }, + }, + "no image should fail": { + args: args{ + appfileData: yamlNoImage, + }, + want: want{ + err: ErrImageNotDefined, + }, + }, + } + + io := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + for caseName, c := range cases { + t.Run(caseName, func(t *testing.T) { + app := NewAppFile() + err := yaml.Unmarshal([]byte(c.args.appfileData), app) + if err != nil { + t.Fatal(err) + } + tm := template.NewFakeTemplateManager() + for k, v := range c.args.traitTemplates { + tm.Templates[k] = &template.Template{ + Captype: types.TypeTrait, + Raw: v, + } + } + for k, v := range c.args.workloadTemplates { + tm.Templates[k] = &template.Template{ + Captype: types.TypeWorkload, + Raw: v, + } + } + + comps, ac, err := app.RenderOAM("default", io, tm) + if err != nil { + assert.Equal(t, c.want.err, err) + return + } + + assert.Equal(t, ac.ObjectMeta, c.want.appConfig.ObjectMeta) + + for _, cp1 := range c.want.appConfig.Spec.Components { + found := false + for _, cp2 := range ac.Spec.Components { + if cp1.ComponentName != cp2.ComponentName { + continue + } + assert.Equal(t, cp1, cp2) + found = true + break + } + if !found { + t.Errorf("ac component (%s) not found", cp1.ComponentName) + } + } + for _, cp1 := range c.want.components { + found := false + for _, cp2 := range comps { + if cp1.Name != cp2.Name { + continue + } + assert.Equal(t, cp1, cp2) + found = true + break + } + if !found { + t.Errorf("component (%s) not found", cp1.Name) + } + } + }) + } +} diff --git a/pkg/appfile/build.go b/pkg/appfile/build.go index 02939c744..8bc94a78e 100644 --- a/pkg/appfile/build.go +++ b/pkg/appfile/build.go @@ -7,7 +7,6 @@ import ( ) type Build struct { - Image string `json:"image,omitempty"` Push Push `json:"push,omitempty"` Docker Docker `json:"docker,omitempty"` } @@ -22,27 +21,28 @@ type Push struct { Registry string `json:"registry,omitempty"` } -func (b *Build) BuildImage(io cmdutil.IOStreams) error { - cmd := exec.Command("docker", "build", "-t", b.Image, "-f", b.Docker.File, b.Docker.Context) +func (b *Build) BuildImage(io cmdutil.IOStreams, image string) error { + cmd := exec.Command("docker", "build", "-t", image, "-f", b.Docker.File, b.Docker.Context) out, err := cmd.CombinedOutput() io.Infof("%s\n", out) if err != nil { return err } - return b.pushImage(io) + return b.pushImage(io, image) } -func (b *Build) pushImage(io cmdutil.IOStreams) error { - io.Infof("pushing image (%s)...\n", b.Image) +func (b *Build) pushImage(io cmdutil.IOStreams, image string) error { + io.Infof("pushing image (%s)...\n", image) switch { case b.Push.Local == "kind": - cmd := exec.Command("kind", "load", "docker-image", b.Image) + cmd := exec.Command("kind", "load", "docker-image", image) out, err := cmd.CombinedOutput() io.Infof("%s\n", out) return err } - cmd := exec.Command("docker", "push", b.Image) + + cmd := exec.Command("docker", "push", image) out, err := cmd.CombinedOutput() io.Infof("%s\n", out) return err diff --git a/pkg/appfile/service.go b/pkg/appfile/service.go index f8dec6ba9..fea5f33ec 100644 --- a/pkg/appfile/service.go +++ b/pkg/appfile/service.go @@ -183,10 +183,11 @@ func renderOneOutput(appValue *cue.Struct) (*unstructured.Unstructured, error) { if err != nil { return nil, fmt.Errorf("FieldByName('output'): %w", err) } + final := outputField.Value data, err := cueJson.Marshal(final) if err != nil { - return nil, fmt.Errorf("marshal final value err %v", err) + return nil, fmt.Errorf("marshal final value failed: %v", err) } obj := make(map[string]interface{}) diff --git a/pkg/appfile/template/fake.go b/pkg/appfile/template/fake.go new file mode 100644 index 000000000..7eb059a3c --- /dev/null +++ b/pkg/appfile/template/fake.go @@ -0,0 +1,11 @@ +package template + +type FakeTemplateManager struct { + *manager +} + +func NewFakeTemplateManager() *FakeTemplateManager { + return &FakeTemplateManager{ + manager: newManager(), + } +} diff --git a/pkg/appfile/template/manager.go b/pkg/appfile/template/manager.go index 7f6bafeb4..77575866a 100644 --- a/pkg/appfile/template/manager.go +++ b/pkg/appfile/template/manager.go @@ -7,7 +7,6 @@ import ( type Manager interface { IsTrait(key string) bool - IsWorkload(key string) bool LoadTemplate(key string) string } @@ -18,49 +17,41 @@ func Load() (Manager, error) { } m := newManager() for _, cap := range caps { - t := &template{} - t.captype = cap.Type - t.raw = cap.CueTemplate - m.templates[cap.Name] = t + t := &Template{} + t.Captype = cap.Type + t.Raw = cap.CueTemplate + m.Templates[cap.Name] = t } return m, nil } +type Template struct { + Captype types.CapType + Raw string +} + type manager struct { - templates map[string]*template + Templates map[string]*Template } func newManager() *manager { return &manager{ - templates: make(map[string]*template), + Templates: make(map[string]*Template), } } -type template struct { - captype types.CapType - raw string -} - func (m *manager) IsTrait(key string) bool { - t, ok := m.templates[key] + t, ok := m.Templates[key] if !ok { return false } - return t.captype == types.TypeTrait -} - -func (m *manager) IsWorkload(key string) bool { - t, ok := m.templates[key] - if !ok { - return false - } - return t.captype == types.TypeWorkload + return t.Captype == types.TypeTrait } func (m *manager) LoadTemplate(key string) string { - t, ok := m.templates[key] + t, ok := m.Templates[key] if !ok { return "" } - return t.raw + return t.Raw } diff --git a/pkg/application/app.go b/pkg/application/app.go index 6e55d4da0..fffc69d11 100644 --- a/pkg/application/app.go +++ b/pkg/application/app.go @@ -11,48 +11,49 @@ import ( "strings" "time" - "cuelang.org/go/cue" "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" "github.com/crossplane/oam-kubernetes-runtime/pkg/oam" "github.com/ghodss/yaml" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" "github.com/oam-dev/kubevela/api/types" - mycue "github.com/oam-dev/kubevela/pkg/cue" - "github.com/oam-dev/kubevela/pkg/plugins" + "github.com/oam-dev/kubevela/pkg/appfile" + "github.com/oam-dev/kubevela/pkg/appfile/template" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" "github.com/oam-dev/kubevela/pkg/utils/system" ) -const ( - Traits = "traits" - Scopes = "scopes" -) - type Application struct { - Name string `json:"name"` - // key of map is component name - Components map[string]map[string]interface{} `json:"components"` - Secrets map[string]map[string]interface{} `json:"secrets"` - Scopes map[string]map[string]interface{} `json:"globalScopes"` - CreateTime time.Time `json:"createTime,omitempty"` - UpdateTime time.Time `json:"updateTime,omitempty"` + *appfile.AppFile `json:",inline"` + tm template.Manager +} + +func newApplication(f *appfile.AppFile, tm template.Manager) *Application { + if f == nil { + f = appfile.NewAppFile() + } + return &Application{AppFile: f, tm: tm} } func LoadFromFile(fileName string) (*Application, error) { - var app = &Application{} - data, err := ioutil.ReadFile(fileName) + tm, err := template.Load() + if err != nil { + return nil, err + } + _, err = ioutil.ReadFile(fileName) if err != nil { if os.IsNotExist(err) { - return app, nil + return newApplication(nil, tm), nil } return nil, err } - err = yaml.Unmarshal(data, app) + + f, err := appfile.LoadFromFile(fileName) if err != nil { return nil, err } + app := newApplication(f, tm) return app, app.Validate() } @@ -130,87 +131,54 @@ func (app *Application) Save(envName string) error { } func (app *Application) Validate() error { - if app == nil { - return errors.New("app is nil") - } if app.Name == "" { - return errors.New("please provide an existed App name") + return errors.New("name is required") } - if len(app.Components) == 0 { - return errors.New("at least one component is required") + if len(app.Services) == 0 { + return errors.New("at least one service is required") } - for name, comp := range app.Components { - lenth := len(comp) - if traits, ok := comp[Traits]; ok { - lenth-- - switch trs := traits.(type) { - case map[string]map[string]interface{}: - case map[string]interface{}: - for traitName, tr := range trs { - _, ok := tr.(map[string]interface{}) - if !ok { - return fmt.Errorf("trait %s in '%s' must be map", traitName, name) - } + for name, svc := range app.Services { + for traitName, traitData := range svc.GetConfig() { + if app.tm.IsTrait(traitName) { + if _, ok := traitData.(map[string]interface{}); !ok { + return fmt.Errorf("trait %s in '%s' must be map", traitName, name) } - default: - return fmt.Errorf("format of traits in '%s' must be nested map instead of %v", name, reflect.TypeOf(traits)) } } - if scopes, ok := comp[Scopes]; ok { - lenth-- - _, ok := scopes.([]string) - if !ok { - return fmt.Errorf("format of scopes in '%s' must be string array", name) - } - //TODO(wonderflow) check scope exist - } - if lenth != 1 { - return fmt.Errorf("you must have only one workload in component '%s'", name) - } - for workloadType, workload := range comp { - if NotWorkload(workloadType) { - continue - } - _, ok := workload.(map[string]interface{}) - if !ok { - return fmt.Errorf("format of workload in %s must be map", name) - } - //TODO(wonderflow) check workload type exists - //TODO(wonderflow) check arguments of workload is valid - } } - //TODO(wonderflow) check scope types return nil } -func NotWorkload(tp string) bool { - if tp == Scopes || tp == Traits { - return true - } - return false -} - func (app *Application) GetComponents() []string { var components []string - for name := range app.Components { + for name := range app.Services { components = append(components, name) } sort.Strings(components) return components } -func (app *Application) GetWorkload(componentName string) (string, map[string]interface{}) { - comp, ok := app.Components[componentName] +func (app *Application) GetServiceConfig(componentName string) (string, map[string]interface{}) { + svc, ok := app.Services[componentName] if !ok { return "", make(map[string]interface{}) } - for tp, workload := range comp { - if NotWorkload(tp) { + return svc.GetType(), svc.GetConfig() +} + +func (app *Application) GetWorkload(componentName string) (string, map[string]interface{}) { + svcType, config := app.GetServiceConfig(componentName) + if svcType == "" { + return "", make(map[string]interface{}) + } + workloadData := make(map[string]interface{}) + for k, v := range config { + if app.tm.IsTrait(k) { continue } - return tp, workload.(map[string]interface{}) + workloadData[k] = v } - return "", make(map[string]interface{}) + return svcType, workloadData } func (app *Application) GetTraitNames(componentName string) ([]string, error) { @@ -226,177 +194,76 @@ func (app *Application) GetTraitNames(componentName string) ([]string, error) { } func (app *Application) GetTraits(componentName string) (map[string]map[string]interface{}, error) { - comp, ok := app.Components[componentName] - if !ok { - return nil, fmt.Errorf("%s not exist", componentName) - } - t, ok := comp[Traits] - if !ok { - return make(map[string]map[string]interface{}), nil - } - // assume it's valid, use Validate() to check - switch trs := t.(type) { - case map[string]interface{}: - traits := make(map[string]map[string]interface{}) - for k, v := range trs { - traits[k] = v.(map[string]interface{}) + _, config := app.GetServiceConfig(componentName) + traitsData := make(map[string]map[string]interface{}) + for k, v := range config { + if !app.tm.IsTrait(k) { + continue } - return traits, nil - case map[string]map[string]interface{}: - return trs, nil + newV, ok := v.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%s is trait, but with invalid format %s, should be map[string]interface{}", k, reflect.TypeOf(v)) + } + traitsData[k] = newV } - return nil, fmt.Errorf("invalid traits data format in %s, expect nested map but got %v", componentName, reflect.TypeOf(t)) + return traitsData, nil } func (app *Application) GetTraitsByType(componentName, traitType string) (map[string]interface{}, error) { - traits, err := app.GetTraits(componentName) - if err != nil { - return nil, err + service, ok := app.Services[componentName] + if !ok { + return nil, fmt.Errorf("component name (%s) doesn't exist", componentName) } - for t, tt := range traits { - if t == traitType { - return tt, nil - } + t, ok := service[traitType] + if !ok { + return make(map[string]interface{}), nil } - return make(map[string]interface{}), nil -} - -func (app *Application) GetWorkloadObject(componentName string) (*unstructured.Unstructured, string, error) { - workloadType, workloadData := app.GetWorkload(componentName) - if workloadType == "" { - return nil, workloadType, errors.New(componentName + " workload not exist") - } - obj, err := InstantiateTemplateToCR(workloadType, workloadData) - if err != nil { - return nil, "", err - } - return obj, workloadType, nil -} - -// ConvertDataByType will fix int become float after yaml.unmarshal -func ConvertDataByType(val interface{}, tp cue.Kind) interface{} { - switch tp { - case cue.FloatKind: - switch rv := val.(type) { - case int64: - return float64(rv) - case int: - return float64(rv) - } - case cue.IntKind: - switch rv := val.(type) { - case float64: - return int64(rv) - } - } - return val -} - -// instantiate the template with the given value to generate the CR -func InstantiateTemplateToCR(capName string, data map[string]interface{}) (*unstructured.Unstructured, error) { - cap, err := plugins.LoadCapabilityByName(capName) - if err != nil { - return nil, err - } - for _, v := range cap.Parameters { - val, ok := data[v.Name] - if ok { - data[v.Name] = ConvertDataByType(val, v.Type) - } - } - cr, err := mycue.Eval(cap.DefinitionPath, data) - if err != nil { - return nil, err - } - if cap.CrdInfo != nil { - cr.SetAPIVersion(cap.CrdInfo.APIVersion) - cr.SetKind(cap.CrdInfo.Kind) - } - return cr, nil -} - -func (app *Application) GetComponentTraits(componentName string, env *types.EnvMeta) ([]v1alpha2.ComponentTrait, error) { - var traits []v1alpha2.ComponentTrait - rawTraits, err := app.GetTraits(componentName) - if err != nil { - return nil, err - } - for traitType, traitData := range rawTraits { - obj, err := InstantiateTemplateToCR(traitType, traitData) - if err != nil { - return nil, err - } - //TODO(wonderflow): handle trait data input/output here - obj.SetLabels(map[string]string{oam.TraitTypeLabel: traitType}) - traits = append(traits, v1alpha2.ComponentTrait{Trait: runtime.RawExtension{Object: obj}}) - } - return traits, nil -} - -func (app *Application) VelaCoreInjection(obj *unstructured.Unstructured, env *types.EnvMeta, traitType string) { - switch traitType { - case "route": - - } - + return t.(map[string]interface{}), nil } func FormatDefaultHealthScopeName(appName string) string { return appName + "-default-health" } -//TODO(wonderflow) add scope support here -func (app *Application) OAM(env *types.EnvMeta) ([]v1alpha2.Component, v1alpha2.ApplicationConfiguration, []oam.Object, error) { - var appConfig v1alpha2.ApplicationConfiguration - if err := app.Validate(); err != nil { - return nil, appConfig, nil, err +// TODO(wonderflow) add scope support here +func (app *Application) OAM(env *types.EnvMeta, io cmdutil.IOStreams) ([]*v1alpha2.Component, *v1alpha2.ApplicationConfiguration, []oam.Object, error) { + comps, appConfig, err := app.RenderOAM(env.Namespace, io, app.tm) + if err != nil { + return nil, nil, nil, err } - appConfig.Name = app.Name - appConfig.Namespace = env.Namespace + addWorkloadTypeLabel(comps, app.Services) + health := addHealthScope(appConfig) + return comps, appConfig, []oam.Object{health}, nil +} - var health v1alpha2.HealthScope - health.Name = FormatDefaultHealthScopeName(app.Name) - health.Namespace = env.Namespace - health.Spec.WorkloadReferences = make([]v1alpha1.TypedReference, 0) - - var components []v1alpha2.Component - for name := range app.Components { - // fulfill component - var component v1alpha2.Component - component.Name = name - component.Namespace = env.Namespace - obj, workloadType, err := app.GetWorkloadObject(name) - if err != nil { - return nil, v1alpha2.ApplicationConfiguration{}, nil, err - } - labels := obj.GetLabels() +func addWorkloadTypeLabel(comps []*v1alpha2.Component, services map[string]appfile.Service) { + for _, comp := range comps { + workloadType := services[comp.Name].GetType() + workloadObject := comp.Spec.Workload.Object.(*unstructured.Unstructured) + labels := workloadObject.GetLabels() if labels == nil { labels = map[string]string{oam.WorkloadTypeLabel: workloadType} } else { labels[oam.WorkloadTypeLabel] = workloadType } - obj.SetLabels(labels) - component.Spec.Workload.Object = obj - components = append(components, component) - - var appConfigComp v1alpha2.ApplicationConfigurationComponent - appConfigComp.ComponentName = name - - //TODO(wonderflow): Temporarily we add health scope here, should change to use scope framework - appConfigComp.Scopes = append(appConfigComp.Scopes, v1alpha2.ComponentScope{ScopeReference: v1alpha1.TypedReference{ - APIVersion: v1alpha2.SchemeGroupVersion.String(), - Kind: v1alpha2.HealthScopeKind, - Name: health.Name, - }}) - - //TODO(wonderflow): handle component data input/output here - compTraits, err := app.GetComponentTraits(name, env) - if err != nil { - return nil, v1alpha2.ApplicationConfiguration{}, nil, err - } - appConfigComp.Traits = compTraits - appConfig.Spec.Components = append(appConfig.Spec.Components, appConfigComp) + workloadObject.SetLabels(labels) } +} - return components, appConfig, []oam.Object{&health}, nil +func addHealthScope(appConfig *v1alpha2.ApplicationConfiguration) *v1alpha2.HealthScope { + health := &v1alpha2.HealthScope{} + health.Name = FormatDefaultHealthScopeName(appConfig.Name) + health.Namespace = appConfig.Namespace + health.Spec.WorkloadReferences = make([]v1alpha1.TypedReference, 0) + for i := range appConfig.Spec.Components { + // TODO(wonderflow): Temporarily we add health scope here, should change to use scope framework + appConfig.Spec.Components[i].Scopes = append(appConfig.Spec.Components[i].Scopes, v1alpha2.ComponentScope{ + ScopeReference: v1alpha1.TypedReference{ + APIVersion: v1alpha2.SchemeGroupVersion.String(), + Kind: v1alpha2.HealthScopeKind, + Name: health.Name, + }, + }) + } + return health } diff --git a/pkg/application/app_test.go b/pkg/application/app_test.go index 5b95bdc71..b5e33dcd5 100644 --- a/pkg/application/app_test.go +++ b/pkg/application/app_test.go @@ -7,62 +7,41 @@ import ( "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" + + "github.com/oam-dev/kubevela/api/types" + "github.com/oam-dev/kubevela/pkg/appfile/template" ) func TestApplication(t *testing.T) { - yaml1 := `name: myapp -components: + yamlNormal := `name: myapp +services: frontend: - deployment: - image: inanimate/echo-server - env: - PORT: 8080 - traits: - autoscaling: - max: 10 - min: 1 - rollout: - strategy: canary - step: 5 + image: inanimate/echo-server + env: + PORT: 8080 + autoscaling: + max: 10 + min: 1 + rollout: + strategy: canary + step: 5 backend: - cloneset: - image: "back:v1" + type: cloneset + image: "back:v1" ` - yaml2 := `name: myapp` - yaml3 := `components: + yamlNoService := `name: myapp` + yamlNoName := `services: frontend: - deployment: - image: inanimate/echo-server - env: - PORT: 8080` - yaml4 := `name: myapp -components: + image: inanimate/echo-server + env: + PORT: 8080` + yamlTraitNotMap := `name: myapp +services: frontend: - deployment: - image: inanimate/echo-server - scopes: - public-scope: true -appScopes: - public-scope: - networkPolicy: public` - yaml5 := `name: myapp -components: - frontend: - traits: - rollout: - strategy: canary - step: 5 - backend: - cloneset: - image: "back:v1" -` - yaml6 := `name: myapp -components: - frontend: - deployment: - image: inanimate/echo-server - traits: - autoscaling: 10` + image: inanimate/echo-server + env: + PORT: 8080 + autoscaling: 10` cases := map[string]struct { raw string @@ -71,33 +50,33 @@ components: ExpName string ExpComponents []string WantWorkload string - ExpWorklaod map[string]interface{} + ExpWorkload map[string]interface{} ExpWorkloadType string ExpTraits map[string]map[string]interface{} }{ "normal case backend": { - raw: yaml1, + raw: yamlNormal, ExpName: "myapp", ExpComponents: []string{"backend", "frontend"}, WantWorkload: "backend", - ExpWorklaod: map[string]interface{}{ + ExpWorkload: map[string]interface{}{ "image": "back:v1", }, ExpWorkloadType: "cloneset", ExpTraits: map[string]map[string]interface{}{}, }, "normal case frontend": { - raw: yaml1, + raw: yamlNormal, ExpName: "myapp", ExpComponents: []string{"backend", "frontend"}, WantWorkload: "frontend", - ExpWorklaod: map[string]interface{}{ + ExpWorkload: map[string]interface{}{ "image": "inanimate/echo-server", "env": map[string]interface{}{ "PORT": float64(8080), }, }, - ExpWorkloadType: "deployment", + ExpWorkloadType: "webservice", ExpTraits: map[string]map[string]interface{}{ "autoscaling": { "max": float64(10), @@ -110,38 +89,36 @@ components: }, }, "no component": { - raw: yaml2, + raw: yamlNoService, ExpName: "myapp", InValid: true, - InvalidReason: errors.New("at least one component is required"), + InvalidReason: errors.New("at least one service is required"), }, "no name": { - raw: yaml3, + raw: yamlNoName, ExpName: "", InValid: true, - InvalidReason: errors.New("please provide an existed App name"), - }, - "scopes not array": { - raw: yaml4, - ExpName: "myapp", - InValid: true, - InvalidReason: fmt.Errorf("format of scopes in 'frontend' must be string array"), - }, - "workload not exist": { - raw: yaml5, - ExpName: "myapp", - InValid: true, - InvalidReason: fmt.Errorf("you must have only one workload in component 'frontend'"), + InvalidReason: errors.New("name is required"), }, "trait must be map": { - raw: yaml6, + raw: yamlTraitNotMap, + ExpTraits: map[string]map[string]interface{}{ + "autoscaling": {}, + }, ExpName: "myapp", InValid: true, InvalidReason: fmt.Errorf("trait autoscaling in 'frontend' must be map"), }, } + for caseName, c := range cases { - var app Application + tm := template.NewFakeTemplateManager() + for k := range c.ExpTraits { + tm.Templates[k] = &template.Template{ + Captype: types.TypeTrait, + } + } + app := newApplication(nil, tm) err := yaml.Unmarshal([]byte(c.raw), &app) assert.NoError(t, err, caseName) err = app.Validate() @@ -152,7 +129,7 @@ components: assert.Equal(t, c.ExpName, app.Name, caseName) assert.Equal(t, c.ExpComponents, app.GetComponents(), caseName) workloadType, workload := app.GetWorkload(c.WantWorkload) - assert.Equal(t, c.ExpWorklaod, workload, caseName) + assert.Equal(t, c.ExpWorkload, workload, caseName) assert.Equal(t, c.ExpWorkloadType, workloadType, caseName) traits, err := app.GetTraits(c.WantWorkload) assert.NoError(t, err, caseName) diff --git a/pkg/application/modify.go b/pkg/application/modify.go index 8d2737611..cdaa31ff9 100644 --- a/pkg/application/modify.go +++ b/pkg/application/modify.go @@ -3,22 +3,25 @@ package application import ( "errors" "strings" + + "github.com/oam-dev/kubevela/pkg/appfile" ) func (app *Application) SetWorkload(componentName, workloadType string, workloadData map[string]interface{}) error { if app == nil { return errors.New("app is nil pointer") } - if workloadData == nil { - workloadData = make(map[string]interface{}) + + s, ok := app.Services[componentName] + if !ok { + s = appfile.Service{} } - workloadData["name"] = strings.ToLower(componentName) - if app.Components == nil { - app.Components = make(map[string]map[string]interface{}) - } - app.Components[componentName] = map[string]interface{}{ - workloadType: workloadData, + s["type"] = workloadType + s["name"] = strings.ToLower(componentName) + for k, v := range workloadData { + s[k] = v } + app.Services[componentName] = s return app.Validate() } @@ -29,20 +32,22 @@ func (app *Application) SetTrait(componentName, traitType string, traitData map[ if traitData == nil { traitData = make(map[string]interface{}) } - if app.Components == nil { - app.Components = make(map[string]map[string]interface{}) + + s, ok := app.Services[componentName] + if !ok { + s = appfile.Service{} } - comp := app.Components[componentName] - if comp == nil { - comp = make(map[string]interface{}) + + t, ok := s[traitType] + if !ok { + t = make(map[string]interface{}) } - traits, err := app.GetTraits(componentName) - if err != nil { - return err + tm := t.(map[string]interface{}) + for k, v := range traitData { + tm[k] = v } - traits[traitType] = traitData - comp[Traits] = traits - app.Components[componentName] = comp + s[traitType] = t + app.Services[componentName] = s return app.Validate() } @@ -50,30 +55,20 @@ func (app *Application) RemoveTrait(componentName, traitType string) error { if app == nil { return errors.New("app is nil pointer") } - if app.Components == nil { - app.Components = make(map[string]map[string]interface{}) + + s, ok := app.Services[componentName] + if !ok { + return nil } - comp := app.Components[componentName] - if comp == nil { - comp = make(map[string]interface{}) - } - traits, err := app.GetTraits(componentName) - if err != nil { - return err - } - delete(traits, traitType) - comp[Traits] = traits - app.Components[componentName] = comp - return app.Validate() + delete(s, traitType) + return nil } func (app *Application) RemoveComponent(componentName string) error { if app == nil { return errors.New("app is nil pointer") } - if app.Components == nil { - app.Components = make(map[string]map[string]interface{}) - } - delete(app.Components, componentName) - return app.Validate() + + delete(app.Services, componentName) + return nil } diff --git a/pkg/application/run.go b/pkg/application/run.go index 47f9f6fe9..ff4e0b63d 100644 --- a/pkg/application/run.go +++ b/pkg/application/run.go @@ -10,10 +10,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/api/types" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" ) -func (app *Application) Run(ctx context.Context, client client.Client, env *types.EnvMeta) error { - components, appconfig, scopes, err := app.OAM(env) +func (app *Application) Run(ctx context.Context, client client.Client, env *types.EnvMeta, io cmdutil.IOStreams) error { + components, appconfig, scopes, err := app.OAM(env, io) if err != nil { return err } @@ -28,20 +29,20 @@ func (app *Application) Run(ctx context.Context, client client.Client, env *type return CreateOrUpdateAppConfig(ctx, client, appconfig) } -func CreateOrUpdateComponent(ctx context.Context, client client.Client, comp v1alpha2.Component) error { +func CreateOrUpdateComponent(ctx context.Context, client client.Client, comp *v1alpha2.Component) error { var getc v1alpha2.Component key := ctypes.NamespacedName{Name: comp.Name, Namespace: comp.Namespace} if err := client.Get(ctx, key, &getc); err != nil { if !apierrors.IsNotFound(err) { return err } - return client.Create(ctx, &comp) + return client.Create(ctx, comp) } comp.ResourceVersion = getc.ResourceVersion - return client.Update(ctx, &comp) + return client.Update(ctx, comp) } -func CreateOrUpdateAppConfig(ctx context.Context, client client.Client, appConfig v1alpha2.ApplicationConfiguration) error { +func CreateOrUpdateAppConfig(ctx context.Context, client client.Client, appConfig *v1alpha2.ApplicationConfiguration) error { var geta v1alpha2.ApplicationConfiguration key := ctypes.NamespacedName{Name: appConfig.Name, Namespace: appConfig.Namespace} var exist = true @@ -52,10 +53,10 @@ func CreateOrUpdateAppConfig(ctx context.Context, client client.Client, appConfi exist = false } if !exist { - return client.Create(ctx, &appConfig) + return client.Create(ctx, appConfig) } appConfig.ResourceVersion = geta.ResourceVersion - return client.Update(ctx, &appConfig) + return client.Update(ctx, appConfig) } func CreateScopes(ctx context.Context, client client.Client, scopes []oam.Object) error { diff --git a/pkg/application/testdata/myapp.yaml b/pkg/application/testdata/myapp.yaml deleted file mode 100644 index 8fbb5ee45..000000000 --- a/pkg/application/testdata/myapp.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: myapp -components: - frontend: - deployment: - image: inanimate/echo-server - env: - - PORT: "8080" - traits: - scale: - replica: 2 - maxUnavailbe: 1 - rollout: - strategy: canary - step: 5 - expose: - service: - type: LoadBalancer - ports: - http: - service_port: 80 - container_port: 8080 - scopes: - - public-scope - - myapp-default-health -secrets: - secret-foo: - key1: 'pass-word' -globalScopes: - network: - public-scope: - networkPolicy: public - private-scope: - networkPolicy: private - health: - myapp-default-health: {} \ No newline at end of file diff --git a/pkg/commands/comp.go b/pkg/commands/comp.go index d7a74661d..49768dcf8 100644 --- a/pkg/commands/comp.go +++ b/pkg/commands/comp.go @@ -6,6 +6,7 @@ import ( "github.com/oam-dev/kubevela/api/types" "github.com/oam-dev/kubevela/pkg/commands/util" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/plugins" @@ -78,7 +79,7 @@ func NewCompDeployCommands(c types.Args, ioStreams util.IOStreams) *cobra.Comman if err := o.Complete(cmd, args); err != nil { return err } - return o.Run(cmd) + return o.Run(cmd, ioStreams) }, Annotations: map[string]string{ types.TagCommandType: types.TypeApp, @@ -162,12 +163,12 @@ func (o *runOptions) Complete(cmd *cobra.Command, args []string) error { return err } -func (o *runOptions) Run(cmd *cobra.Command) error { +func (o *runOptions) Run(cmd *cobra.Command, io cmdutil.IOStreams) error { staging, err := cmd.Flags().GetBool(Staging) if err != nil { return err } - msg, err := oam.BaseRun(staging, o.App, o.KubeClient, o.Env) + msg, err := oam.BaseRun(staging, o.App, o.KubeClient, o.Env, io) if err != nil { return err } diff --git a/pkg/commands/delete.go b/pkg/commands/delete.go index 42d6db50c..791b74ae9 100644 --- a/pkg/commands/delete.go +++ b/pkg/commands/delete.go @@ -97,7 +97,7 @@ func NewCompDeleteCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Comm } ioStreams.Infof("Deleting Component '%s' from Application '%s'\n", o.CompName, o.AppName) - message, err := o.DeleteComponent() + message, err := o.DeleteComponent(ioStreams) if err != nil { return err } diff --git a/pkg/commands/init.go b/pkg/commands/init.go index 013c13b28..9e3fea88f 100644 --- a/pkg/commands/init.go +++ b/pkg/commands/init.go @@ -66,7 +66,7 @@ func NewInitCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { if err = o.Traits(); err != nil { return err } - _, err = oam.BaseRun(false, o.app, o.client, o.Env) + _, err = oam.BaseRun(false, o.app, o.client, o.Env, ioStreams) if err != nil { return err } diff --git a/pkg/commands/ls.go b/pkg/commands/ls.go index af1d9661a..96a9f8937 100644 --- a/pkg/commands/ls.go +++ b/pkg/commands/ls.go @@ -117,7 +117,7 @@ func mergeStagingComponents(deployed []apis.ComponentMeta, env *types.EnvMeta, i } var all []apis.ComponentMeta for _, app := range apps { - comps, appConfig, _, err := app.OAM(env) + comps, appConfig, _, err := app.OAM(env, ioStreams) if err != nil { ioStreams.Errorf("convert app %s err %v\n", app.Name, err) continue diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 0fde83ba6..c740599c1 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -46,7 +46,7 @@ func NewRunCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { if err = o.LoadApp(cmd, args); err != nil { return err } - return o.Run() + return o.Run(ioStreams) }, } cmd.Flags().StringP("file", "f", "", "launch application from provided appfile") @@ -77,9 +77,9 @@ func (o *appRunOptions) LoadApp(cmd *cobra.Command, args []string) error { return nil } -func (o *appRunOptions) Run() error { +func (o *appRunOptions) Run(io cmdutil.IOStreams) error { o.Infof("Launching App Bundle \"%s\"\n", o.appName) - if err := o.app.Run(context.Background(), o.client, o.Env); err != nil { + if err := o.app.Run(context.Background(), o.client, o.Env, io); err != nil { return err } o.Info("SUCCEED") diff --git a/pkg/commands/show.go b/pkg/commands/show.go index 715b96640..873599f7f 100644 --- a/pkg/commands/show.go +++ b/pkg/commands/show.go @@ -73,7 +73,7 @@ func showApplication(cmd *cobra.Command, env *types.EnvMeta, appName string) err table.AddRow(" Name", "Type", "Traits") - for compName := range app.Components { + for compName := range app.Services { wtype, _ := app.GetWorkload(compName) var outPutTraits []string traits, _ := app.GetTraits(compName) @@ -133,7 +133,7 @@ func showComponent(cmd *cobra.Command, env *types.EnvMeta, compName, appName str return err } - for cname := range app.Components { + for cname := range app.Services { if cname != compName { continue } diff --git a/pkg/commands/trait.go b/pkg/commands/trait.go index c9bfb3cfe..3e30a8587 100644 --- a/pkg/commands/trait.go +++ b/pkg/commands/trait.go @@ -66,7 +66,7 @@ func AddTraitCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil. return err } } - return o.Run(ctx, cmd) + return o.Run(ctx, cmd, ioStreams) }, Annotations: map[string]string{ types.TagCommandType: types.TypeTraits, @@ -126,7 +126,7 @@ func (o *commandOptions) DetachTrait(cmd *cobra.Command, args []string) error { return o.app.Save(o.Env.Name) } -func (o *commandOptions) Run(ctx context.Context, cmd *cobra.Command) error { +func (o *commandOptions) Run(ctx context.Context, cmd *cobra.Command, io cmdutil.IOStreams) error { if o.Detach { o.Infof("Detaching %s from app %s\n", o.traitType, o.workloadName) } else { @@ -136,7 +136,7 @@ func (o *commandOptions) Run(ctx context.Context, cmd *cobra.Command) error { if err != nil { return err } - msg, err := oam.TraitOperationRun(ctx, o.Client, o.Env, o.app, staging) + msg, err := oam.TraitOperationRun(ctx, o.Client, o.Env, o.app, staging, io) if err != nil { return err } diff --git a/pkg/commands/up.go b/pkg/commands/up.go index 525cd03aa..724c68f5b 100644 --- a/pkg/commands/up.go +++ b/pkg/commands/up.go @@ -6,7 +6,6 @@ import ( "fmt" "io/ioutil" "os" - "os/exec" "path/filepath" "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" @@ -20,6 +19,8 @@ import ( "github.com/oam-dev/kubevela/api/types" "github.com/oam-dev/kubevela/pkg/appfile" + "github.com/oam-dev/kubevela/pkg/appfile/template" + "github.com/oam-dev/kubevela/pkg/application" cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" ) @@ -72,7 +73,13 @@ func (o *appfileOptions) Run() error { return err } - comps, appConfig, err := app.BuildOAM(o.Env.Namespace, o.IO) + o.IO.Info("Loading templates ...") + tm, err := template.Load() + if err != nil { + return err + } + + comps, appConfig, err := app.BuildOAM(o.Env.Namespace, o.IO, tm) if err != nil { return err } @@ -114,7 +121,7 @@ func (o *appfileOptions) Run() error { } o.IO.Infof("\nApplying deploy configs ...\n") - return o.ApplyAppConfig(appConfig) + return o.ApplyAppConfig(appConfig, comps) } // Apply deploy config resources for the app. @@ -122,7 +129,7 @@ func (o *appfileOptions) Run() error { // - 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) ApplyAppConfig(ac *v1alpha2.ApplicationConfiguration) error { +func (o *appfileOptions) ApplyAppConfig(ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) error { key := apitypes.NamespacedName{ Namespace: ac.Namespace, Name: ac.Name, @@ -138,21 +145,27 @@ func (o *appfileOptions) ApplyAppConfig(ac *v1alpha2.ApplicationConfiguration) e default: return err } - return o.apply(ac) -} - -func (o *appfileOptions) apply(ac *v1alpha2.ApplicationConfiguration) error { - cmd := exec.Command("kubectl", "apply", "-f", ".vela/deploy.yaml") - out, err := cmd.CombinedOutput() - o.IO.Infof("deploying======\n%s\n", out) - if err != nil { + if err := o.apply(ac, comps); err != nil { return err } - o.IO.Infof("app has been deployed %s%s%s\n", emojiRocket, emojiRocket, emojiRocket) - o.IO.Infof("\tURL: http://%s/\n", o.Env.Domain) - o.IO.Infof("\tPort forward: vela port-forward %s \n", ac.Name) - o.IO.Infof("\tSSH: vela exec %s\n", ac.Name) - o.IO.Infof("\tLogging: vela log %s\n", ac.Name) - o.IO.Infof("\tMetric: TODO\n") + o.info(ac.Name) return nil } + +func (o *appfileOptions) apply(ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) error { + for _, comp := range comps { + if err := application.CreateOrUpdateComponent(context.TODO(), o.Kubecli, comp); err != nil { + return err + } + } + return application.CreateOrUpdateAppConfig(context.TODO(), o.Kubecli, ac) +} + +func (o *appfileOptions) info(name string) { + o.IO.Infof("app has been deployed %s%s%s\n", emojiRocket, emojiRocket, emojiRocket) + o.IO.Infof("\tURL: http://%s/\n", o.Env.Domain) + o.IO.Infof("\tPort forward: vela listen %s \n", name) + o.IO.Infof("\tSSH: vela exec %s\n", name) + o.IO.Infof("\tLogging: vela logs %s\n", name) + o.IO.Infof("\tMetric: TODO\n") +} diff --git a/pkg/cue/base_template.go b/pkg/cue/base_template.go index 499fddafd..030308d7c 100644 --- a/pkg/cue/base_template.go +++ b/pkg/cue/base_template.go @@ -4,6 +4,5 @@ const BaseTemplate = ` context: { name: string - image: string } ` diff --git a/pkg/oam/application.go b/pkg/oam/application.go index 43021b55d..a56f57eef 100644 --- a/pkg/oam/application.go +++ b/pkg/oam/application.go @@ -191,7 +191,7 @@ func (o *DeleteOptions) DeleteApp() (string, error) { return fmt.Sprintf("delete apps succeed %s from %s", o.AppName, o.Env.Name), nil } -func (o *DeleteOptions) DeleteComponent() (string, error) { +func (o *DeleteOptions) DeleteComponent(io cmdutil.IOStreams) (string, error) { var app *application.Application var err error if o.AppName != "" { @@ -217,7 +217,7 @@ func (o *DeleteOptions) DeleteComponent() (string, error) { // Remove component from appConfig in k8s cluster ctx := context.Background() - if err := app.Run(ctx, o.Client, o.Env); err != nil { + if err := app.Run(ctx, o.Client, o.Env, io); err != nil { return "", err } diff --git a/pkg/oam/trait.go b/pkg/oam/trait.go index c495c7260..68d01514c 100644 --- a/pkg/oam/trait.go +++ b/pkg/oam/trait.go @@ -3,6 +3,7 @@ package oam import ( "context" "fmt" + "os" "strconv" "strings" @@ -14,6 +15,7 @@ import ( "github.com/oam-dev/kubevela/api/types" "github.com/oam-dev/kubevela/pkg/application" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" "github.com/oam-dev/kubevela/pkg/plugins" "github.com/oam-dev/kubevela/pkg/server/apis" ) @@ -225,14 +227,16 @@ func AttachTrait(c *gin.Context, body apis.TraitBody) (string, error) { return "", err } kubeClient := c.MustGet("KubeClient") - return TraitOperationRun(c, kubeClient.(client.Client), env, appObj, staging) + io := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + return TraitOperationRun(c, kubeClient.(client.Client), env, appObj, staging, io) } -func TraitOperationRun(ctx context.Context, c client.Client, env *types.EnvMeta, appObj *application.Application, staging bool) (string, error) { +func TraitOperationRun(ctx context.Context, c client.Client, env *types.EnvMeta, appObj *application.Application, + staging bool, io cmdutil.IOStreams) (string, error) { if staging { return "Staging saved", nil } - err := appObj.Run(ctx, c, env) + err := appObj.Run(ctx, c, env, io) if err != nil { return "", err } @@ -270,5 +274,6 @@ func DetachTrait(c *gin.Context, envName string, traitType string, componentName return "", err } kubeClient := c.MustGet("KubeClient") - return TraitOperationRun(c, kubeClient.(client.Client), env, appObj, staging) + io := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + return TraitOperationRun(c, kubeClient.(client.Client), env, appObj, staging, io) } diff --git a/pkg/oam/workload.go b/pkg/oam/workload.go index b6c7a4cac..56eeb6dc6 100644 --- a/pkg/oam/workload.go +++ b/pkg/oam/workload.go @@ -9,6 +9,7 @@ import ( "github.com/oam-dev/kubevela/api/types" "github.com/oam-dev/kubevela/pkg/application" "github.com/oam-dev/kubevela/pkg/commands/util" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" "github.com/oam-dev/kubevela/pkg/plugins" "cuelang.org/go/cue" @@ -35,13 +36,10 @@ func LoadIfExist(envName string, workloadName string, appGroup string) (*applica } app, err := application.Load(envName, appName) if err != nil { - return app, err + return nil, err } app.Name = appName - if app.Components == nil { - app.Components = make(map[string]map[string]interface{}) - } return app, nil } @@ -110,13 +108,13 @@ func BaseComplete(envName string, workloadName string, appName string, flagSet * return app, app.Save(envName) } -func BaseRun(staging bool, App *application.Application, kubeClient client.Client, Env *types.EnvMeta) (string, error) { +func BaseRun(staging bool, app *application.Application, kubeClient client.Client, Env *types.EnvMeta, io cmdutil.IOStreams) (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 { + msg = fmt.Sprintf("Creating App %s\n", app.Name) + if err := app.Run(context.Background(), kubeClient, Env, io); err != nil { err = fmt.Errorf("create app err: %s", err) return "", err } diff --git a/pkg/plugins/local.go b/pkg/plugins/local.go index 10c594758..bd59e3b0e 100644 --- a/pkg/plugins/local.go +++ b/pkg/plugins/local.go @@ -129,7 +129,7 @@ func SinkTemp2Local(templates []types.Capability, dir string) int { fmt.Printf("sync %s err: %v\n", tmp.Name, err) continue } - err = ioutil.WriteFile(filepath.Join(subDir, tmp.Name), data, 0644) + err = ioutil.WriteFile(filepath.Join(subDir, tmp.Name), data, 0o644) if err != nil { fmt.Printf("sync %s err: %v\n", tmp.Name, err) continue diff --git a/pkg/server/handler/componentHandlers.go b/pkg/server/handler/componentHandlers.go index 265c16bb3..55605a653 100644 --- a/pkg/server/handler/componentHandlers.go +++ b/pkg/server/handler/componentHandlers.go @@ -1,7 +1,10 @@ package handler import ( + "os" + "github.com/gin-gonic/gin" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/server/util" "sigs.k8s.io/controller-runtime/pkg/client" @@ -43,6 +46,8 @@ func DeleteComponent(c *gin.Context) { Env: envMeta, AppName: appName, CompName: componentName} - message, err := o.DeleteComponent() + + message, err := o.DeleteComponent( + cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) util.AssembleResponse(c, message, err) } diff --git a/pkg/server/handler/workloadHandler.go b/pkg/server/handler/workloadHandler.go index e475f6d4b..24cb859f4 100644 --- a/pkg/server/handler/workloadHandler.go +++ b/pkg/server/handler/workloadHandler.go @@ -1,11 +1,14 @@ package handler import ( + "os" + "github.com/gin-gonic/gin" "github.com/spf13/pflag" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/api/types" + cmdutil "github.com/oam-dev/kubevela/pkg/commands/util" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/plugins" "github.com/oam-dev/kubevela/pkg/server/apis" @@ -36,7 +39,8 @@ func CreateWorkload(c *gin.Context) { util.HandleError(c, util.StatusInternalServerError, err.Error()) return } - msg, err := oam.BaseRun(body.Staging, appObj, kubeClient.(client.Client), env) + io := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + msg, err := oam.BaseRun(body.Staging, appObj, kubeClient.(client.Client), env, io) if err != nil { util.HandleError(c, util.StatusInternalServerError, err.Error()) return