From 9ea02adf2f0cd5005decee6ef0eb659179f337bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A9=E5=85=83?= Date: Wed, 12 Aug 2020 21:23:14 +0800 Subject: [PATCH] implement appfile oriented OAM Application run --- api/types/capability.go | 6 + api/types/types.go | 118 +------- dashboard/README.md | 2 +- pkg/application/app.go | 269 ++++++++++++++++++ .../application/app_test.go | 24 +- pkg/application/modify.go | 69 +++++ pkg/application/run.go | 58 ++++ .../apps => application/testdata}/myapp.yaml | 0 pkg/cmd/capability.go | 12 + pkg/cmd/delete.go | 9 +- pkg/cmd/env.go | 19 +- pkg/cmd/env_test.go | 3 + pkg/cmd/ls.go | 4 +- pkg/cmd/{app_show.go => show.go} | 7 +- pkg/cmd/status.go | 3 +- pkg/cmd/{trait_bind.go => trait.go} | 154 ++++------ pkg/cmd/{trait_bind_test.go => trait_test.go} | 0 pkg/cmd/util/helpers.go | 12 +- pkg/cmd/workload_run.go | 104 +++---- pkg/plugins/cluster.go | 14 + pkg/plugins/local.go | 13 + pkg/utils/system/system.go | 34 +-- 22 files changed, 599 insertions(+), 335 deletions(-) create mode 100644 pkg/application/app.go rename api/types/types_test.go => pkg/application/app_test.go (89%) create mode 100644 pkg/application/modify.go create mode 100644 pkg/application/run.go rename pkg/{cue/testdata/apps => application/testdata}/myapp.yaml (100%) rename pkg/cmd/{app_show.go => show.go} (93%) rename pkg/cmd/{trait_bind.go => trait.go} (52%) rename pkg/cmd/{trait_bind_test.go => trait_test.go} (100%) diff --git a/api/types/capability.go b/api/types/capability.go index c8360d82d..fecb27b12 100644 --- a/api/types/capability.go +++ b/api/types/capability.go @@ -32,6 +32,11 @@ type Source struct { ChartName string `json:"chartName,omitempty"` } +type CrdInfo struct { + ApiVersion string `json:"apiVersion"` + Kind string `json:"kind"` +} + // Capability defines the content of a capability type Capability struct { Name string `json:"name"` @@ -47,6 +52,7 @@ type Capability struct { // Plugin Source Source *Source `json:"source,omitempty"` Install *Installation `json:"install,omitempty"` + CrdInfo *CrdInfo `json:"crdInfo,omitempty"` } type Chart struct { diff --git a/api/types/types.go b/api/types/types.go index fdc771022..b9c2c034d 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -1,11 +1,5 @@ package types -import ( - "errors" - "fmt" - "sort" -) - const ( DefaultOAMNS = "oam-system" DefaultOAMReleaseName = "core-runtime" @@ -14,116 +8,22 @@ const ( DefaultOAMRepoUrl = "https://charts.crossplane.io/master" DefaultOAMVersion = ">0.0.0-0" - DefaultEnvName = "default" + DefaultEnvName = "default" + DefaultAppNamespace = "default" ) const ( - Traits = "traits" - Scopes = "scopes" + AnnApiVersion = "oam.appengine.info/apiVersion" + AnnKind = "oam.appengine.info/kind" + + // ComponentWorkloadDefLabel indicate which workloaddefinition generate from + ComponentWorkloadDefLabel = "vela.oam.dev/workloadDef" + TraitDefLabel = "vela.oam.dev/traitDef" ) -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:"appScopes"` -} - -func (app *Application) Valid() error { - if app.Name == "" { - return errors.New("name is required") - } - if len(app.Components) == 0 { - return errors.New("at least one component is required") - } - for name, comp := range app.Components { - lenth := len(comp) - if traits, ok := comp[Traits]; ok { - lenth-- - trs, ok := traits.(map[string]interface{}) - if !ok { - return fmt.Errorf("format of traits in %s must be map", name) - } - for traitName, tr := range trs { - _, ok := tr.(map[string]interface{}) - if !ok { - return fmt.Errorf("trait %s in %s must be map", traitName, name) - } - } - } - 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) - } - } - 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 { - components = append(components, name) - } - sort.Strings(components) - return components -} - -func (app *Application) GetWorkload(componentName string) (string, map[string]interface{}, error) { - comp, ok := app.Components[componentName] - if !ok { - return "", nil, fmt.Errorf("%s not exist", componentName) - } - for tp, workload := range comp { - if NotWorkload(tp) { - continue - } - return tp, workload.(map[string]interface{}), nil - } - return "", nil, fmt.Errorf("workload not exist in %s", componentName) -} - -func (app *Application) GetTraits(componentName 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 map[string]interface{}{}, nil - } - // assume it's valid, use Valid() to check - traits := t.(map[string]interface{}) - return traits, nil -} - type EnvMeta struct { Namespace string `json:"namespace"` + Name string `json:"name"` } const ( diff --git a/dashboard/README.md b/dashboard/README.md index e3aa7255b..83b2b1e37 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -1 +1 @@ -# RudrX Dashboard +# Vela Dashboard diff --git a/pkg/application/app.go b/pkg/application/app.go new file mode 100644 index 000000000..813e4c8ef --- /dev/null +++ b/pkg/application/app.go @@ -0,0 +1,269 @@ +package application + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + + "k8s.io/apimachinery/pkg/runtime" + + mycue "github.com/cloud-native-application/rudrx/pkg/cue" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/cloud-native-application/rudrx/pkg/plugins" + + "github.com/cloud-native-application/rudrx/api/types" + + "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" + + "github.com/cloud-native-application/rudrx/pkg/utils/system" + "github.com/ghodss/yaml" +) + +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:"appScopes"` +} + +func Load(envName, appName string) (*Application, error) { + appDir, err := system.GetApplicationDir(envName) + if err != nil { + return nil, fmt.Errorf("get app dir from env %s err %v", envName, err) + } + app := &Application{Name: appName} + data, err := ioutil.ReadFile(filepath.Join(appDir, appName+".yaml")) + if err != nil { + if os.IsNotExist(err) { + return app, nil + } + return nil, err + } + err = yaml.Unmarshal(data, app) + if err != nil { + return nil, err + } + return app, nil +} + +func (app *Application) Save(envName, appName string) error { + appDir, err := system.GetApplicationDir(envName) + if err != nil { + return fmt.Errorf("get app dir from env %s err %v", envName, err) + } + out, err := yaml.Marshal(app) + if err != nil { + return err + } + return ioutil.WriteFile(filepath.Join(appDir, appName+".yaml"), out, 0644) +} + +func (app *Application) Validate() error { + if app.Name == "" { + return errors.New("name is required") + } + if len(app.Components) == 0 { + return errors.New("at least one component is required") + } + for name, comp := range app.Components { + lenth := len(comp) + if traits, ok := comp[Traits]; ok { + lenth-- + trs, ok := traits.(map[string]interface{}) + if !ok { + return fmt.Errorf("format of traits in %s must be map", name) + } + for traitName, tr := range trs { + _, ok := tr.(map[string]interface{}) + if !ok { + return fmt.Errorf("trait %s in %s must be map", traitName, name) + } + } + } + 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) + } + } + 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 { + components = append(components, name) + } + sort.Strings(components) + return components +} + +func (app *Application) GetWorkload(componentName string) (string, map[string]interface{}, error) { + comp, ok := app.Components[componentName] + if !ok { + return "", nil, fmt.Errorf("%s not exist", componentName) + } + for tp, workload := range comp { + if NotWorkload(tp) { + continue + } + return tp, workload.(map[string]interface{}), nil + } + return "", nil, fmt.Errorf("workload not exist in %s", componentName) +} + +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 + trs := t.(map[string]interface{}) + traits := make(map[string]map[string]interface{}) + for k, v := range trs { + traits[k] = v.(map[string]interface{}) + } + return traits, nil +} + +func (app *Application) GetTraitsByType(componentName, traitType string) (map[string]interface{}, error) { + traits, err := app.GetTraits(componentName) + if err != nil { + return nil, err + } + for t, tt := range traits { + if t == traitType { + return tt, nil + } + } + return make(map[string]interface{}), nil +} + +func (app *Application) GetWorkloadObject(componentName string) (*unstructured.Unstructured, error) { + workloadType, workloadData, err := app.GetWorkload(componentName) + if err != nil { + return nil, err + } + return EvalToObject(workloadType, workloadData) +} + +func EvalToObject(capName string, data map[string]interface{}) (*unstructured.Unstructured, error) { + cap, err := plugins.LoadCapabilityByName(capName) + if err != nil { + return nil, err + } + jsondata, err := mycue.Eval(cap.DefinitionPath, capName, data) + if err != nil { + return nil, err + } + var obj = make(map[string]interface{}) + if err = json.Unmarshal([]byte(jsondata), &obj); err != nil { + return nil, err + } + u := &unstructured.Unstructured{Object: obj} + if cap.CrdInfo != nil { + u.SetAPIVersion(cap.CrdInfo.ApiVersion) + u.SetKind(cap.CrdInfo.Kind) + } + return u, nil +} + +func (app *Application) GetComponentTraits(componentName string) ([]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 := EvalToObject(traitType, traitData) + if err != nil { + return nil, err + } + //TODO(wonderflow): handle trait data input/output here + obj.SetAnnotations(map[string]string{types.TraitDefLabel: traitType}) + traits = append(traits, v1alpha2.ComponentTrait{Trait: runtime.RawExtension{Object: obj}}) + } + return traits, nil +} + +//TODO(wonderflow) add scope support here +func (app *Application) OAM(env *types.EnvMeta) ([]v1alpha2.Component, v1alpha2.ApplicationConfiguration, error) { + var appConfig v1alpha2.ApplicationConfiguration + if err := app.Validate(); err != nil { + return nil, appConfig, err + } + appConfig.Name = app.Name + appConfig.Namespace = env.Namespace + + var components []v1alpha2.Component + + for name := range app.Components { + // fulfill component + var component v1alpha2.Component + component.Name = name + component.Namespace = env.Namespace + obj, err := app.GetWorkloadObject(name) + if err != nil { + return nil, v1alpha2.ApplicationConfiguration{}, err + } + labels := component.Labels + if labels == nil { + labels = map[string]string{types.ComponentWorkloadDefLabel: name} + } else { + labels[types.ComponentWorkloadDefLabel] = name + } + component.Labels = labels + component.Spec.Workload.Object = obj + components = append(components, component) + + var appConfigComp v1alpha2.ApplicationConfigurationComponent + appConfigComp.ComponentName = name + //TODO(wonderflow): handle component data input/output here + compTraits, err := app.GetComponentTraits(name) + if err != nil { + return nil, v1alpha2.ApplicationConfiguration{}, err + } + appConfigComp.Traits = compTraits + appConfig.Spec.Components = append(appConfig.Spec.Components, appConfigComp) + } + return components, appConfig, nil +} diff --git a/api/types/types_test.go b/pkg/application/app_test.go similarity index 89% rename from api/types/types_test.go rename to pkg/application/app_test.go index f07ea6140..11c3d507f 100644 --- a/api/types/types_test.go +++ b/pkg/application/app_test.go @@ -1,4 +1,4 @@ -package types +package application import ( "errors" @@ -73,7 +73,7 @@ components: WantWorkload string ExpWorklaod map[string]interface{} ExpWorkloadType string - ExpTraits map[string]interface{} + ExpTraits map[string]map[string]interface{} }{ "normal case backend": { raw: yaml1, @@ -84,7 +84,7 @@ components: "image": "back:v1", }, ExpWorkloadType: "cloneset", - ExpTraits: map[string]interface{}{}, + ExpTraits: map[string]map[string]interface{}{}, }, "normal case frontend": { raw: yaml1, @@ -94,18 +94,18 @@ components: ExpWorklaod: map[string]interface{}{ "image": "inanimate/echo-server", "env": map[string]interface{}{ - "PORT": 8080, + "PORT": float64(8080), }, }, ExpWorkloadType: "deployment", - ExpTraits: map[string]interface{}{ - "autoscaling": map[string]interface{}{ - "max": 10, - "min": 1, + ExpTraits: map[string]map[string]interface{}{ + "autoscaling": { + "max": float64(10), + "min": float64(1), }, - "rollout": map[string]interface{}{ + "rollout": { "strategy": "canary", - "step": 5, + "step": float64(5), }, }, }, @@ -144,7 +144,7 @@ components: var app Application err := yaml.Unmarshal([]byte(c.raw), &app) assert.NoError(t, err, caseName) - err = app.Valid() + err = app.Validate() if c.InValid { assert.Equal(t, c.InvalidReason, err) continue @@ -157,6 +157,6 @@ components: assert.Equal(t, c.ExpWorkloadType, workloadType, caseName) traits, err := app.GetTraits(c.WantWorkload) assert.NoError(t, err, caseName) - assert.Equal(t, c.ExpTraits, traits) + assert.Equal(t, c.ExpTraits, traits, caseName) } } diff --git a/pkg/application/modify.go b/pkg/application/modify.go new file mode 100644 index 000000000..c9aac7781 --- /dev/null +++ b/pkg/application/modify.go @@ -0,0 +1,69 @@ +package application + +import ( + "errors" + "strings" +) + +func (app *Application) SetWorkload(workloadName, 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{}) + } + workloadData["name"] = strings.ToLower(workloadName) + if app.Components == nil { + app.Components = make(map[string]map[string]interface{}) + } + app.Components[workloadName] = map[string]interface{}{ + workloadType: workloadData, + } + return app.Validate() +} + +func (app *Application) SetTrait(workloadName, traitType string, traitData map[string]interface{}) error { + if app == nil { + return errors.New("app is nil pointer") + } + if traitData == nil { + traitData = make(map[string]interface{}) + } + traitData["name"] = strings.ToLower(traitType) + if app.Components == nil { + app.Components = make(map[string]map[string]interface{}) + } + comp := app.Components[workloadName] + if comp == nil { + comp = make(map[string]interface{}) + } + traits, err := app.GetTraits(workloadName) + if err != nil { + return err + } + traits[traitType] = traitData + comp[Traits] = traits + app.Components[workloadName] = comp + return app.Validate() +} + +func (app *Application) RemoveTrait(workloadName, 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{}) + } + comp := app.Components[workloadName] + if comp == nil { + comp = make(map[string]interface{}) + } + traits, err := app.GetTraits(workloadName) + if err != nil { + return err + } + delete(traits, traitType) + comp[Traits] = traits + app.Components[workloadName] = comp + return app.Validate() +} diff --git a/pkg/application/run.go b/pkg/application/run.go new file mode 100644 index 000000000..aebd610d0 --- /dev/null +++ b/pkg/application/run.go @@ -0,0 +1,58 @@ +package application + +import ( + "context" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" + + "github.com/cloud-native-application/rudrx/api/types" + ctypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (app *Application) Run(ctx context.Context, client client.Client, env *types.EnvMeta) error { + components, appconfig, err := app.OAM(env) + if err != nil { + return err + } + for _, cmp := range components { + if err = CreateOrUpdateComponent(ctx, client, cmp); err != nil { + return err + } + } + return CreateOrUpdateAppConfig(ctx, client, appconfig) +} + +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} + var exist = true + if err := client.Get(ctx, key, &getc); err != nil { + if !apierrors.IsAlreadyExists(err) { + return err + } + exist = false + } + if !exist { + return client.Create(ctx, &comp) + } + return client.Update(ctx, &comp) +} + +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 + if err := client.Get(ctx, key, &geta); err != nil { + if !apierrors.IsAlreadyExists(err) { + return err + } + exist = false + } + if !exist { + return client.Create(ctx, &appConfig) + } + return client.Update(ctx, &appConfig) +} diff --git a/pkg/cue/testdata/apps/myapp.yaml b/pkg/application/testdata/myapp.yaml similarity index 100% rename from pkg/cue/testdata/apps/myapp.yaml rename to pkg/application/testdata/myapp.yaml diff --git a/pkg/cmd/capability.go b/pkg/cmd/capability.go index 6764244fa..5bc116b10 100644 --- a/pkg/cmd/capability.go +++ b/pkg/cmd/capability.go @@ -325,6 +325,12 @@ func InstallCapability(client client.Client, centerName, capabilityName string, return err } } + if apiVerion, kind := cmdutil.GetApiVersionKindFromWorkload(wd); apiVerion != "" && kind != "" { + tp.CrdInfo = &types.CrdInfo{ + ApiVersion: apiVerion, + Kind: kind, + } + } if err = client.Create(context.Background(), &wd); err != nil && !apierrors.IsAlreadyExists(err) { return err } @@ -345,6 +351,12 @@ func InstallCapability(client client.Client, centerName, capabilityName string, return err } } + if apiVerion, kind := cmdutil.GetApiVersionKindFromTrait(td); apiVerion != "" && kind != "" { + tp.CrdInfo = &types.CrdInfo{ + ApiVersion: apiVerion, + Kind: kind, + } + } if err = client.Create(context.Background(), &td); err != nil && !apierrors.IsAlreadyExists(err) { return err } diff --git a/pkg/cmd/delete.go b/pkg/cmd/delete.go index a74dc01e3..0e7168662 100644 --- a/pkg/cmd/delete.go +++ b/pkg/cmd/delete.go @@ -27,16 +27,15 @@ func newDeleteOptions(ioStreams cmdutil.IOStreams) *deleteOptions { func newDeleteCommand() *cobra.Command { return &cobra.Command{ - Use: "app:delete [APPLICATION_NAME]", + Use: "app:delete ", + Aliases: []string{"delete"}, DisableFlagsInUseLine: true, Short: "Delete OAM Applications", Long: "Delete OAM Applications", Annotations: map[string]string{ types.TagCommandType: types.TypeApp, }, - Example: ` - vela delete frontend -`} + Example: "vela delete frontend"} } // NewDeleteCommand init new command @@ -64,7 +63,7 @@ func NewDeleteCommand(c types.Args, ioStreams cmdutil.IOStreams, args []string) func (o *deleteOptions) Complete(cmd *cobra.Command, args []string) error { if len(args) < 1 { - return errors.New("must specify name for workload") + return errors.New("must specify name for the app") } namespace := o.Env.Namespace diff --git a/pkg/cmd/env.go b/pkg/cmd/env.go index 41a8a8e54..1a29cd883 100644 --- a/pkg/cmd/env.go +++ b/pkg/cmd/env.go @@ -59,7 +59,7 @@ func NewEnvInitCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command var envArgs types.EnvMeta ctx := context.Background() cmd := &cobra.Command{ - Use: "env:init", + Use: "env:init ", DisableFlagsInUseLine: true, Short: "Create environments", Long: "Create environment and switch to it", @@ -149,10 +149,10 @@ func ListEnvs(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) e curEnv = types.DefaultEnvName } for _, f := range files { - if f.IsDir() { + if !f.IsDir() { continue } - data, err := ioutil.ReadFile(filepath.Join(envDir, f.Name())) + data, err := ioutil.ReadFile(filepath.Join(envDir, f.Name(), system.EnvConfigName)) if err != nil { continue } @@ -186,7 +186,7 @@ func DeleteEnv(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) if err != nil { return err } - if err = os.Remove(filepath.Join(envdir, envname)); err != nil { + if err = os.RemoveAll(filepath.Join(envdir, envname)); err != nil { return err } ioStreams.Info(envname + " deleted") @@ -198,6 +198,7 @@ func CreateOrUpdateEnv(ctx context.Context, c client.Client, envArgs *types.EnvM return fmt.Errorf("you must specify env name for vela env:init command") } envname := args[0] + envArgs.Name = envname data, err := json.Marshal(envArgs) if err != nil { return err @@ -206,7 +207,9 @@ func CreateOrUpdateEnv(ctx context.Context, c client.Client, envArgs *types.EnvM if err != nil { return err } - if err = ioutil.WriteFile(filepath.Join(envdir, envname), data, 0644); err != nil { + subEnvDir := filepath.Join(envdir, envname) + system.StatAndCreate(subEnvDir) + if err = ioutil.WriteFile(filepath.Join(subEnvDir, system.EnvConfigName), data, 0644); err != nil { return err } curEnvPath, err := system.GetCurrentEnvPath() @@ -271,11 +274,7 @@ func GetEnv() (*types.EnvMeta, error) { } func getEnvByName(name string) (*types.EnvMeta, error) { - envdir, err := system.GetEnvDir() - if err != nil { - return nil, err - } - data, err := ioutil.ReadFile(filepath.Join(envdir, name)) + data, err := ioutil.ReadFile(filepath.Join(system.GetEnvDirByName(name), system.EnvConfigName)) if err != nil { return nil, err } diff --git a/pkg/cmd/env_test.go b/pkg/cmd/env_test.go index 045d8f843..3c4a85441 100644 --- a/pkg/cmd/env_test.go +++ b/pkg/cmd/env_test.go @@ -37,11 +37,13 @@ func TestENV(t *testing.T) { assert.NoError(t, err) assert.Equal(t, &types.EnvMeta{ Namespace: "default", + Name: "default", }, gotEnv) ioStream := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} exp := &types.EnvMeta{ Namespace: "test1", + Name: "default", } client := test.NewMockClient() // Create env1 @@ -81,6 +83,7 @@ func TestENV(t *testing.T) { assert.NoError(t, err) assert.Equal(t, &types.EnvMeta{ Namespace: "default", + Name: "default", }, gotEnv) // delete env diff --git a/pkg/cmd/ls.go b/pkg/cmd/ls.go index 52451208c..9c089bfb3 100644 --- a/pkg/cmd/ls.go +++ b/pkg/cmd/ls.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" "fmt" + "strings" + corev1alpha2 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" "github.com/gosuri/uitable" - "strings" "github.com/cloud-native-application/rudrx/api/types" cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" @@ -26,6 +27,7 @@ func NewAppsCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { ctx := context.Background() cmd := &cobra.Command{ Use: "app:ls", + Aliases: []string{"ls"}, DisableFlagsInUseLine: true, Short: "List applications", Long: "List applications with workloads, traits, status and created time", diff --git a/pkg/cmd/app_show.go b/pkg/cmd/show.go similarity index 93% rename from pkg/cmd/app_show.go rename to pkg/cmd/show.go index bc6eb19ff..f9c5dea81 100644 --- a/pkg/cmd/app_show.go +++ b/pkg/cmd/show.go @@ -18,14 +18,15 @@ import ( func NewAppShowCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { ctx := context.Background() cmd := &cobra.Command{ - Use: "app:show", + Use: "app:show ", + Aliases: []string{"show"}, Short: "get detail spec of your app", Long: "get detail spec of your app, including its workload and trait", Example: `vela app:show `, RunE: func(cmd *cobra.Command, args []string) error { argsLength := len(args) if argsLength == 0 { - ioStreams.Errorf("Hint: please specify an application") + ioStreams.Errorf("Hint: please specify the application name") os.Exit(1) } appName := args[0] @@ -79,7 +80,7 @@ func showApplication(ctx context.Context, c client.Client, cmd *cobra.Command, e } if component.Labels == nil { return fmt.Errorf("Can't get workloadDef, please check component %s label \"%s\" is correct.", - componentName, ComponentWorkloadDefLabel) + componentName, types.ComponentWorkloadDefLabel) } traitDefinitions := cmdutil.ListTraitDefinitionsByApplicationConfiguration(application) diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index 5d31a516a..9ce0f210b 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -24,7 +24,8 @@ type ApplicationStatusMeta struct { func NewAppStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { ctx := context.Background() cmd := &cobra.Command{ - Use: "app:status", + Use: "app:status ", + Aliases: []string{"status"}, Short: "get status of an application", Long: "get status of an application, including its workload and trait", Example: `vela app:status `, diff --git a/pkg/cmd/trait_bind.go b/pkg/cmd/trait.go similarity index 52% rename from pkg/cmd/trait_bind.go rename to pkg/cmd/trait.go index 695483487..a6487ec5c 100644 --- a/pkg/cmd/trait_bind.go +++ b/pkg/cmd/trait.go @@ -2,38 +2,33 @@ package cmd import ( "context" - "encoding/json" "errors" - "fmt" "strconv" - "strings" - mycue "github.com/cloud-native-application/rudrx/pkg/cue" + "github.com/cloud-native-application/rudrx/pkg/application" "cuelang.org/go/cue" - "k8s.io/apimachinery/pkg/runtime" - "github.com/cloud-native-application/rudrx/pkg/plugins" "github.com/cloud-native-application/rudrx/api/types" cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" - "github.com/crossplane/crossplane-runtime/pkg/fieldpath" - corev1alpha2 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" ) type commandOptions struct { Template types.Capability - Component corev1alpha2.Component - AppConfig corev1alpha2.ApplicationConfiguration Client client.Client TraitAlias string Detach bool Env *types.EnvMeta + + workloadName string + appName string + staging bool + app *application.Application cmdutil.IOStreams } @@ -56,14 +51,14 @@ func AddTraitCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil. DisableFlagsInUseLine: true, Short: "Attach " + name + " trait to an app", Long: "Attach " + name + " trait to an app", - Example: `vela scale frontend --max=5`, + Example: "vela " + name + " frontend", RunE: func(cmd *cobra.Command, args []string) error { newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema}) if err != nil { return err } o.Client = newClient - if err := o.Complete(cmd, args, ctx); err != nil { + if err := o.AddOrUpdateTrait(cmd, args); err != nil { return err } return o.Run(cmd, ctx) @@ -76,6 +71,8 @@ func AddTraitCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil. for _, v := range tmp.Parameters { types.SetFlagBy(pluginCmd, v) } + pluginCmd.Flags().StringP(App, "a", "", "create or add into an existing application group") + pluginCmd.Flags().BoolP(Staging, "s", false, "only save changes locally without real update application") o.Template = tmp parentCmd.AddCommand(pluginCmd) @@ -83,35 +80,32 @@ func AddTraitCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil. return nil } -func (o *commandOptions) Complete(cmd *cobra.Command, args []string, ctx context.Context) error { - argsLength := len(args) - var appName string - - c := o.Client - - namespace := o.Env.Namespace - - if argsLength < 1 { +func (o *commandOptions) Prepare(cmd *cobra.Command, args []string) error { + if len(args) < 1 { return errors.New("please specify the name of the app") } + o.workloadName = args[0] + if app := cmd.Flag(App).Value.String(); app != "" { + o.appName = app + } else { + o.appName = o.workloadName + } + return nil +} - // Get AppConfig - // TODO(wonderflow): appName is Component Name here, check if it's has appset with a different name - appName = args[0] - if err := c.Get(ctx, client.ObjectKey{Namespace: o.Env.Namespace, Name: appName}, &o.AppConfig); err != nil { +func (o *commandOptions) AddOrUpdateTrait(cmd *cobra.Command, args []string) error { + if err := o.Prepare(cmd, args); err != nil { return err } - - // Get component - var component corev1alpha2.Component - if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appName}, &component); err != nil { + app, err := application.Load(o.Env.Name, o.appName) + if err != nil { + return err + } + var traitType = o.Template.Name + traitData, err := app.GetTraitsByType(o.workloadName, traitType) + if err != nil { return err } - - var traitData = make(map[string]interface{}) - - var tp = o.Template.Name - for _, v := range o.Template.Parameters { flagSet := cmd.Flag(v.Name) switch v.Type { @@ -128,43 +122,11 @@ func (o *commandOptions) Complete(cmd *cobra.Command, args []string, ctx context traitData[v.Name] = d } } - - jsondata, err := mycue.Eval(o.Template.DefinitionPath, tp, traitData) - if err != nil { + if err = app.SetTrait(o.workloadName, traitType, traitData); err != nil { return err } - var obj = make(map[string]interface{}) - if err = json.Unmarshal([]byte(jsondata), &obj); err != nil { - return err - } - - pvd := fieldpath.Pave(obj) - // metadata.name needs to be in lower case. - pvd.SetString("metadata.name", strings.ToLower(fmt.Sprintf("%s-%s-trait", appName, o.Template.Name))) - curObj := &unstructured.Unstructured{Object: pvd.UnstructuredContent()} - var updated bool - for ic, c := range o.AppConfig.Spec.Components { - if c.ComponentName != appName { - continue - } - for it, t := range c.Traits { - g, v, k := GetGVKFromRawExtension(t.Trait) - - // TODO(wonderflow): we should get GVK from DefinitionPath instead of assuming template object contains - gvk := curObj.GroupVersionKind() - if gvk.Group == g && gvk.Version == v && gvk.Kind == k { - updated = true - c.Traits[it] = corev1alpha2.ComponentTrait{Trait: runtime.RawExtension{Object: curObj}} - break - } - } - if !updated { - c.Traits = append(c.Traits, corev1alpha2.ComponentTrait{Trait: runtime.RawExtension{Object: curObj}}) - } - o.AppConfig.Spec.Components[ic] = c - break - } - return nil + o.app = app + return app.Save(o.Env.Name, o.appName) } func AddTraitDetachCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil.IOStreams) error { @@ -182,14 +144,14 @@ func AddTraitDetachCommands(parentCmd *cobra.Command, c types.Args, ioStreams cm DisableFlagsInUseLine: true, Short: "Detach " + name + " trait from an app", Long: "Detach " + name + " trait from an app", - Example: `vela scale:detach frontend`, + Example: "vela " + name + ":detach frontend", RunE: func(cmd *cobra.Command, args []string) error { newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema}) if err != nil { return err } o.Client = newClient - if err := o.DetachTrait(cmd, args, ctx); err != nil { + if err := o.DetachTrait(cmd, args); err != nil { return err } return o.Run(cmd, ctx) @@ -206,48 +168,36 @@ func AddTraitDetachCommands(parentCmd *cobra.Command, c types.Args, ioStreams cm return nil } -func (o *commandOptions) DetachTrait(cmd *cobra.Command, args []string, ctx context.Context) error { - argsLength := len(args) - if argsLength < 1 { - return errors.New("please specify the name of the app") - } - c := o.Client - namespace := o.Env.Namespace - - var appName = args[0] - if err := c.Get(ctx, client.ObjectKey{Namespace: o.Env.Namespace, Name: appName}, &o.AppConfig); err != nil { +func (o *commandOptions) DetachTrait(cmd *cobra.Command, args []string) error { + if err := o.Prepare(cmd, args); err != nil { return err } - - apiVersion, kind, err := cmdutil.GetTraitApiVersionKind(ctx, c, namespace, o.TraitAlias) + app, err := application.Load(o.Env.Name, o.appName) if err != nil { return err } - for i, com := range o.AppConfig.Spec.Components { - if com.ComponentName != appName { - continue - } - var traits []corev1alpha2.ComponentTrait - for _, tr := range com.Traits { - a, k := tr.Trait.Object.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind() - if a == apiVersion && k == kind { - continue - } - traits = append(traits, tr) - } - o.AppConfig.Spec.Components[i].Traits = traits + var traitType = o.Template.Name + if err = app.RemoveTrait(o.workloadName, traitType); err != nil { + return err } - return nil + return app.Save(o.Env.Name, o.appName) } func (o *commandOptions) Run(cmd *cobra.Command, ctx context.Context) error { if o.Detach { - o.Info("Detaching trait from app", o.Component.Name) + o.Infof("Detaching %s from app %s\n", o.Template.Name, o.workloadName) } else { - o.Info("Applying trait for app", o.Component.Name) + o.Infof("Adding %s for app %s \n", o.Template.Name, o.workloadName) } - c := o.Client - err := c.Update(ctx, &o.AppConfig) + staging, err := strconv.ParseBool(cmd.Flag(Staging).Value.String()) + if err != nil { + return err + } + if staging { + o.Info("Staging saved") + return nil + } + err = o.app.Run(ctx, o.Client, o.Env) if err != nil { return err } diff --git a/pkg/cmd/trait_bind_test.go b/pkg/cmd/trait_test.go similarity index 100% rename from pkg/cmd/trait_bind_test.go rename to pkg/cmd/trait_test.go diff --git a/pkg/cmd/util/helpers.go b/pkg/cmd/util/helpers.go index fe52d1293..1549181c5 100644 --- a/pkg/cmd/util/helpers.go +++ b/pkg/cmd/util/helpers.go @@ -107,11 +107,19 @@ func GetTraitApiVersionKind(ctx context.Context, c client.Client, namespace stri if err != nil { return "", "", err } - apiVersion := t.Annotations["oam.appengine.info/apiVersion"] - kind := t.Annotations["oam.appengine.info/kind"] + apiVersion := t.Annotations[types.AnnApiVersion] + kind := t.Annotations[types.AnnKind] return apiVersion, kind, nil } +func GetApiVersionKindFromTrait(td corev1alpha2.TraitDefinition) (string, string) { + return td.Annotations[types.AnnApiVersion], td.Annotations[types.AnnKind] +} + +func GetApiVersionKindFromWorkload(td corev1alpha2.WorkloadDefinition) (string, string) { + return td.Annotations[types.AnnApiVersion], td.Annotations[types.AnnKind] +} + func GetWorkloadNameAliasKind(ctx context.Context, c client.Client, namespace string, workloadName string) (string, string, string) { var name, alias, kind string diff --git a/pkg/cmd/workload_run.go b/pkg/cmd/workload_run.go index 27765d625..dcaca0042 100644 --- a/pkg/cmd/workload_run.go +++ b/pkg/cmd/workload_run.go @@ -2,52 +2,39 @@ package cmd import ( "context" - "encoding/json" "errors" "fmt" - "io/ioutil" - "path/filepath" "strconv" - "strings" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/ghodss/yaml" - - "cuelang.org/go/cue" - - "github.com/cloud-native-application/rudrx/pkg/utils/system" "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" - - cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" - mycue "github.com/cloud-native-application/rudrx/pkg/cue" - - corev1alpha2 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" ) -// ComponentWorkloadDefLabel indicate which workloaddefinition generate from -const ComponentWorkloadDefLabel = "vela.oam.dev/workloadDef" +const Staging = "staging" +const App = "app" type runOptions struct { Template types.Capability Env *types.EnvMeta workloadName string client client.Client - app *types.Application - cmdutil.IOStreams + app *application.Application + appName string + staging bool + util.IOStreams } -func newRunOptions(ioStreams cmdutil.IOStreams) *runOptions { +func newRunOptions(ioStreams util.IOStreams) *runOptions { return &runOptions{IOStreams: ioStreams} } -func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil.IOStreams) error { +func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams util.IOStreams) error { templates, err := plugins.LoadInstalledCapabilityWithType(types.TypeWorkload) if err != nil { return err @@ -62,7 +49,7 @@ func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdut DisableFlagsInUseLine: true, Short: "Run " + name + " workloads", Long: "Run " + name + " workloads", - Example: `vela deployment:run frontend -i nginx:latest`, + Example: "vela " + name + ":run frontend", RunE: func(cmd *cobra.Command, args []string) error { newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema}) if err != nil { @@ -82,6 +69,8 @@ func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdut for _, v := range tmp.Parameters { types.SetFlagBy(pluginCmd, v) } + pluginCmd.Flags().StringP(App, "a", "", "create or add into an existing application group") + pluginCmd.Flags().BoolP(Staging, "s", false, "only save changes locally without real update application") o.Template = tmp parentCmd.AddCommand(pluginCmd) @@ -90,21 +79,25 @@ func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdut } func (o *runOptions) Complete(cmd *cobra.Command, args []string, ctx context.Context) error { - argsLength := len(args) - if argsLength < 1 { return errors.New("must specify name for workload") } - - workloadName := args[0] - // TODO(wonderflow): load application from file - var app = &types.Application{Name: workloadName} + 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 + } if app.Components == nil { app.Components = make(map[string]map[string]interface{}) } - tp, workloadData, err := app.GetWorkload(workloadName) + tp, workloadData, err := app.GetWorkload(o.workloadName) if err != nil { // Not exist tp = o.Template.Name @@ -127,52 +120,25 @@ func (o *runOptions) Complete(cmd *cobra.Command, args []string, ctx context.Con workloadData[v.Name] = d } } - workloadData["name"] = strings.ToLower(workloadName) - app.Components[workloadName] = map[string]interface{}{ - tp: workloadData, - } - o.workloadName = workloadName - o.app = app - appDir, _ := system.GetApplicationDir() - out, err := yaml.Marshal(app) - if err != nil { + if err = app.SetWorkload(o.workloadName, tp, workloadData); err != nil { return err } - return ioutil.WriteFile(filepath.Join(appDir, workloadName), out, 0644) + o.app = app + return app.Save(o.Env.Name, o.appName) } func (o *runOptions) Run(cmd *cobra.Command) error { - var component corev1alpha2.Component - var appconfig corev1alpha2.ApplicationConfiguration - tp, data, _ := o.app.GetWorkload(o.workloadName) - jsondata, err := mycue.Eval(o.Template.DefinitionPath, tp, data) + staging, err := strconv.ParseBool(cmd.Flag(Staging).Value.String()) if err != nil { return err } - var obj = make(map[string]interface{}) - if err = json.Unmarshal([]byte(jsondata), &obj); err != nil { - return err + if staging { + o.Info("Staging saved") + return nil } - - component.Spec.Workload.Object = &unstructured.Unstructured{Object: obj} - component.Name = o.workloadName - component.Namespace = o.Env.Namespace - component.Labels = map[string]string{ComponentWorkloadDefLabel: o.workloadName} - - appconfig.Name = o.workloadName - appconfig.Namespace = o.Env.Namespace - appconfig.Spec.Components = append(appconfig.Spec.Components, corev1alpha2.ApplicationConfigurationComponent{ComponentName: o.workloadName}) - - //TODO(wonderflow): we should also support update here - - o.Infof("Creating AppConfig %s\n", appconfig.Name) - err = o.client.Create(context.Background(), &component) - if err != nil { - return fmt.Errorf("create component err: %s", err) - } - err = o.client.Create(context.Background(), &appconfig) - if err != nil { - return fmt.Errorf("create appconfig err %s", 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") return nil diff --git a/pkg/plugins/cluster.go b/pkg/plugins/cluster.go index 79ce44e90..836d9c059 100644 --- a/pkg/plugins/cluster.go +++ b/pkg/plugins/cluster.go @@ -7,6 +7,8 @@ import ( "io/ioutil" "path/filepath" + cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" + "github.com/cloud-native-application/rudrx/pkg/utils/system" "k8s.io/apimachinery/pkg/labels" @@ -47,6 +49,12 @@ func GetWorkloadsFromCluster(ctx context.Context, namespace string, c client.Cli fmt.Printf("[WARN]handle template %s: %v\n", wd.Name, err) continue } + if apiVerion, kind := cmdutil.GetApiVersionKindFromWorkload(wd); apiVerion != "" && kind != "" { + tmp.CrdInfo = &types.CrdInfo{ + ApiVersion: apiVerion, + Kind: kind, + } + } templates = append(templates, tmp) } return templates, nil @@ -66,6 +74,12 @@ func GetTraitsFromCluster(ctx context.Context, namespace string, c client.Client fmt.Printf("[WARN]handle template %s: %v\n", td.Name, err) continue } + if apiVerion, kind := cmdutil.GetApiVersionKindFromTrait(td); apiVerion != "" && kind != "" { + tmp.CrdInfo = &types.CrdInfo{ + ApiVersion: apiVerion, + Kind: kind, + } + } templates = append(templates, tmp) } return templates, nil diff --git a/pkg/plugins/local.go b/pkg/plugins/local.go index 50b09a18b..b78642cba 100644 --- a/pkg/plugins/local.go +++ b/pkg/plugins/local.go @@ -13,6 +13,19 @@ import ( "github.com/cloud-native-application/rudrx/pkg/utils/system" ) +func LoadCapabilityByName(name string) (types.Capability, error) { + caps, err := LoadAllInstalledCapability() + if err != nil { + return types.Capability{}, err + } + for _, c := range caps { + if c.Name == name { + return c, nil + } + } + return types.Capability{}, fmt.Errorf("%s not found", name) +} + func LoadAllInstalledCapability() ([]types.Capability, error) { workloads, err := LoadInstalledCapabilityWithType(types.TypeWorkload) if err != nil { diff --git a/pkg/utils/system/system.go b/pkg/utils/system/system.go index 6ec6d5b5f..81ed01f2e 100644 --- a/pkg/utils/system/system.go +++ b/pkg/utils/system/system.go @@ -39,14 +39,6 @@ func GetRepoConfig() (string, error) { return filepath.Join(home, "config.yaml"), nil } -func GetApplicationDir() (string, error) { - home, err := GetVelaHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, "applications"), nil -} - func GetCapabilityDir() (string, error) { home, err := GetVelaHomeDir() if err != nil { @@ -75,9 +67,6 @@ func InitDirs() error { if err := InitCapabilityDir(); err != nil { return err } - if err := InitApplicationDir(); err != nil { - return err - } if err := InitCapCenterDir(); err != nil { return err } @@ -100,22 +89,22 @@ func InitCapabilityDir() error { return StatAndCreate(dir) } -func InitApplicationDir() error { - dir, err := GetApplicationDir() - if err != nil { - return err - } - return StatAndCreate(dir) +func GetApplicationDir(envName string) (string, error) { + appDir := filepath.Join(GetEnvDirByName(envName), "applications") + return appDir, StatAndCreate(appDir) } +const EnvConfigName = "config.json" + func InitDefaultEnv() error { envDir, err := GetEnvDir() if err != nil { return err } - StatAndCreate(envDir) - data, _ := json.Marshal(&types.EnvMeta{Namespace: types.DefaultEnvName}) - if err = ioutil.WriteFile(filepath.Join(envDir, types.DefaultEnvName), data, 0644); err != nil { + defaultEnvDir := filepath.Join(envDir, types.DefaultEnvName) + StatAndCreate(defaultEnvDir) + data, _ := json.Marshal(&types.EnvMeta{Namespace: types.DefaultAppNamespace, Name: types.DefaultEnvName}) + if err = ioutil.WriteFile(filepath.Join(defaultEnvDir, EnvConfigName), data, 0644); err != nil { return err } curEnvPath, err := GetCurrentEnvPath() @@ -134,3 +123,8 @@ func StatAndCreate(dir string) error { } return nil } + +func GetEnvDirByName(name string) string { + envdir, _ := GetEnvDir() + return filepath.Join(envdir, name) +}