diff --git a/e2e/application/application_test.go b/e2e/application/application_test.go index 4448a8d0c..e0ebd2248 100644 --- a/e2e/application/application_test.go +++ b/e2e/application/application_test.go @@ -24,6 +24,6 @@ var _ = ginkgo.Describe("Application", func() { e2e.TraitManualScalerAttachContext("vela attach trait", traitAlias, applicationName) //e2e.ApplicationListContext("app ls", applicationName, traitAlias) e2e.ApplicationShowContext("app show", applicationName, workloadType) - e2e.ApplicationStatusContext("app status", applicationName, workloadType) + e2e.ApplicationStatusContext("comp status", applicationName, workloadType) e2e.WorkloadDeleteContext("delete", applicationName) }) diff --git a/e2e/commonContext.go b/e2e/commonContext.go index ac8262560..a5c76b921 100644 --- a/e2e/commonContext.go +++ b/e2e/commonContext.go @@ -177,13 +177,11 @@ var ( ApplicationStatusContext = func(context string, applicationName string, workloadType string) bool { return ginkgo.Context(context, func() { ginkgo.It("should get status for the application", func() { - cli := fmt.Sprintf("vela app status %s", applicationName) + cli := fmt.Sprintf("vela comp status %s", applicationName) output, err := Exec(cli) gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(output).To(gomega.ContainSubstring(applicationName)) // TODO(zzxwill) need to check workloadType after app status is refined - //gomega.Expect(output).To(gomega.ContainSubstring(workloadType)) - gomega.Expect(output).To(gomega.ContainSubstring("Workload")) }) }) } diff --git a/pkg/application/app.go b/pkg/application/app.go index 193ad44ab..4c2f2433d 100644 --- a/pkg/application/app.go +++ b/pkg/application/app.go @@ -12,6 +12,10 @@ import ( "strings" "time" + "github.com/crossplane/oam-kubernetes-runtime/pkg/oam" + + "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" + "cuelang.org/go/cue" "k8s.io/apimachinery/pkg/runtime" @@ -39,7 +43,7 @@ type Application struct { // 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"` + Scopes map[string]map[string]interface{} `json:"globalScopes"` CreateTime time.Time `json:"createTime,omitempty"` UpdateTime time.Time `json:"updateTime,omitempty"` } @@ -166,6 +170,7 @@ func (app *Application) Validate() error { 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) @@ -340,17 +345,25 @@ func (app *Application) GetComponentTraits(componentName string) ([]v1alpha2.Com return traits, 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, error) { +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, err + return nil, appConfig, nil, err } appConfig.Name = app.Name appConfig.Namespace = env.Namespace - var components []v1alpha2.Component + 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 @@ -358,7 +371,7 @@ func (app *Application) OAM(env *types.EnvMeta) ([]v1alpha2.Component, v1alpha2. component.Namespace = env.Namespace obj, workloadType, err := app.GetWorkloadObject(name) if err != nil { - return nil, v1alpha2.ApplicationConfiguration{}, err + return nil, v1alpha2.ApplicationConfiguration{}, nil, err } anns := component.Annotations if anns == nil { @@ -372,13 +385,22 @@ func (app *Application) OAM(env *types.EnvMeta) ([]v1alpha2.Component, v1alpha2. 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) if err != nil { - return nil, v1alpha2.ApplicationConfiguration{}, err + return nil, v1alpha2.ApplicationConfiguration{}, nil, err } appConfigComp.Traits = compTraits appConfig.Spec.Components = append(appConfig.Spec.Components, appConfigComp) } - return components, appConfig, nil + + return components, appConfig, []oam.Object{&health}, nil } diff --git a/pkg/application/run.go b/pkg/application/run.go index 56be79a64..5b0598820 100644 --- a/pkg/application/run.go +++ b/pkg/application/run.go @@ -3,6 +3,8 @@ package application import ( "context" + "github.com/crossplane/oam-kubernetes-runtime/pkg/oam" + apierrors "k8s.io/apimachinery/pkg/api/errors" "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" @@ -13,7 +15,7 @@ import ( ) func (app *Application) Run(ctx context.Context, client client.Client, env *types.EnvMeta) error { - components, appconfig, err := app.OAM(env) + components, appconfig, scopes, err := app.OAM(env) if err != nil { return err } @@ -22,6 +24,9 @@ func (app *Application) Run(ctx context.Context, client client.Client, env *type return err } } + if err = CreateScopes(ctx, client, scopes); err != nil { + return err + } return CreateOrUpdateAppConfig(ctx, client, appconfig) } @@ -58,3 +63,20 @@ func CreateOrUpdateAppConfig(ctx context.Context, client client.Client, appConfi appConfig.ResourceVersion = geta.ResourceVersion return client.Update(ctx, &appConfig) } + +func CreateScopes(ctx context.Context, client client.Client, scopes []oam.Object) error { + for _, obj := range scopes { + key := ctypes.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()} + err := client.Get(ctx, key, obj) + if err == nil { + return nil + } + if !apierrors.IsNotFound(err) { + return err + } + if err = client.Create(ctx, obj); err != nil { + return err + } + } + return nil +} diff --git a/pkg/application/testdata/myapp.yaml b/pkg/application/testdata/myapp.yaml index 0fa5e0a1f..7dccdd39f 100644 --- a/pkg/application/testdata/myapp.yaml +++ b/pkg/application/testdata/myapp.yaml @@ -20,11 +20,15 @@ components: container_port: 8080 scopes: - public-scope + - myapp-default-health secrets: secret-foo: key1: 'pass-word' -appScopes: - public-scope: - networkPolicy: public - private-scope: - networkPolicy: private \ No newline at end of file +globalScopes: + network: + public-scope: + networkPolicy: public + private-scope: + networkPolicy: private + health: + myapp-default-health: {} \ No newline at end of file diff --git a/pkg/builtin/built_test.go b/pkg/builtin/built_test.go deleted file mode 100644 index 3b23b40ac..000000000 --- a/pkg/builtin/built_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package builtin - -import ( - "testing" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/ghodss/yaml" - "github.com/stretchr/testify/assert" - - "github.com/cloud-native-application/rudrx/pkg/builtin/traitdefinition" - "github.com/cloud-native-application/rudrx/pkg/builtin/workloaddefinition" -) - -func TestValidYaml(t *testing.T) { - cases := map[string]string{ - "scale": traitdefinition.ManualScaler, - "rollout": traitdefinition.SimpleRollout, - "containerized": workloaddefinition.ContainerizedWorkload, - "deployment": workloaddefinition.Deployment, - } - for name, val := range cases { - data := unstructured.Unstructured{} - assert.NoError(t, yaml.Unmarshal([]byte(val), &data), name) - } -} diff --git a/pkg/builtin/traitdefinition/built_test.go b/pkg/builtin/traitdefinition/built_test.go new file mode 100644 index 000000000..a0d5974fb --- /dev/null +++ b/pkg/builtin/traitdefinition/built_test.go @@ -0,0 +1,21 @@ +package traitdefinition + +import ( + "testing" + + "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" + + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidTraitDefinition(t *testing.T) { + cases := map[string]string{ + "scale": ManualScaler, + "rollout": SimpleRollout, + } + for name, val := range cases { + data := v1alpha2.TraitDefinition{} + assert.NoError(t, yaml.Unmarshal([]byte(val), &data), name) + } +} diff --git a/pkg/builtin/workloaddefinition/built_test.go b/pkg/builtin/workloaddefinition/built_test.go new file mode 100644 index 000000000..6c3abc616 --- /dev/null +++ b/pkg/builtin/workloaddefinition/built_test.go @@ -0,0 +1,21 @@ +package workloaddefinition + +import ( + "testing" + + "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" + + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" +) + +func TestValidWorkloadDefinition(t *testing.T) { + cases := map[string]string{ + "containerized": ContainerizedWorkload, + "deployment": Deployment, + } + for name, val := range cases { + data := v1alpha2.WorkloadDefinition{} + assert.NoError(t, yaml.Unmarshal([]byte(val), &data), name) + } +} diff --git a/pkg/cmd/app.go b/pkg/cmd/app.go index 00d271ecd..aa6fc8744 100644 --- a/pkg/cmd/app.go +++ b/pkg/cmd/app.go @@ -20,7 +20,6 @@ func NewAppsCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { cmd.AddCommand(NewAppListCommand(c, ioStreams), NewDeleteCommand(c, ioStreams), - NewAppStatusCommand(c, ioStreams), NewAppShowCommand(ioStreams), NewRunCommand(c, ioStreams)) return cmd diff --git a/pkg/cmd/comp.go b/pkg/cmd/comp.go index cd5872c14..a472ea38b 100644 --- a/pkg/cmd/comp.go +++ b/pkg/cmd/comp.go @@ -43,6 +43,7 @@ func AddCompCommands(c types.Args, ioStreams util.IOStreams) *cobra.Command { NewCompListCommand(c, ioStreams), NewCompRunCommands(c, ioStreams), NewCompShowCommand(ioStreams), + NewCompStatusCommand(c, ioStreams), NewCompDeleteCommand(c, ioStreams), ) return compCommands @@ -132,9 +133,7 @@ func (o *runOptions) Complete(cmd *cobra.Command, args []string, ctx context.Con if err = flags.Parse(args); err != nil { return err } - - var flagSet = cmd.Flags() - app, err := oam.BaseComplete(envName, workloadName, appGroup, flagSet, workloadType) + app, err := oam.BaseComplete(envName, workloadName, appGroup, flags, workloadType) if err != nil { return err } diff --git a/pkg/cmd/delete.go b/pkg/cmd/delete.go index a9a426865..f8bed80b3 100644 --- a/pkg/cmd/delete.go +++ b/pkg/cmd/delete.go @@ -77,8 +77,9 @@ func (o *deleteOptions) DeleteApp() error { } return fmt.Errorf("delete appconfig err %s", err) } - for _, comp := range appConfig.Status.Workloads { + for _, comp := range appConfig.Spec.Components { var c corev1alpha2.Component + //TODO(wonderflow): what if we use componentRevision here? c.Name = comp.ComponentName c.Namespace = o.Env.Namespace err = o.client.Delete(ctx, &c) @@ -90,6 +91,15 @@ func (o *deleteOptions) DeleteApp() error { if err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf("delete appconfig err %s", err) } + + var healthscope corev1alpha2.HealthScope + healthscope.Name = application.FormatDefaultHealthScopeName(o.appName) + healthscope.Namespace = o.Env.Namespace + err = o.client.Delete(ctx, &healthscope) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("delete health scope %s err %v", healthscope.Name, err) + } + o.Info("DELETE SUCCEED") return nil } diff --git a/pkg/cmd/ls.go b/pkg/cmd/ls.go index b69e78016..e2150675f 100644 --- a/pkg/cmd/ls.go +++ b/pkg/cmd/ls.go @@ -116,7 +116,7 @@ func mergeStagingComponents(deployed []oam.ComponentMeta, env *types.EnvMeta, io } var all []oam.ComponentMeta for _, app := range apps { - comps, appConfig, err := app.OAM(env) + comps, appConfig, _, err := app.OAM(env) if err != nil { ioStreams.Errorf("convert app %s err %v\n", app.Name, err) continue diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index e0ce4db7d..a5891a539 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -3,18 +3,20 @@ package cmd import ( "context" "os" + "time" + + "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" + + "github.com/cloud-native-application/rudrx/pkg/application" "github.com/cloud-native-application/rudrx/api/types" cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" - "github.com/cloud-native-application/rudrx/pkg/oam" - - "github.com/ghodss/yaml" "github.com/spf13/cobra" "sigs.k8s.io/controller-runtime/pkg/client" ) -func NewAppStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { +func NewCompStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { ctx := context.Background() cmd := &cobra.Command{ Use: "status ", @@ -27,18 +29,18 @@ func NewAppStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Comma ioStreams.Errorf("Hint: please specify an application") os.Exit(1) } - appName := args[0] + compName := args[0] env, err := GetEnv(cmd) if err != nil { ioStreams.Errorf("Error: failed to get Env: %s", err) return err } - namespace := env.Namespace newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema}) if err != nil { return err } - return printApplicationStatus(ctx, newClient, ioStreams, appName, namespace) + appName, _ := cmd.Flags().GetString(App) + return printComponentStatus(ctx, newClient, ioStreams, compName, appName, env) }, Annotations: map[string]string{ types.TagCommandType: types.TypeApp, @@ -48,15 +50,33 @@ func NewAppStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Comma return cmd } -func printApplicationStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOStreams, appName string, namespace string) error { - application, err := oam.RetrieveApplicationStatusByName(ctx, c, appName, namespace) +func printComponentStatus(ctx context.Context, c client.Client, ioStreams cmdutil.IOStreams, compName, appName string, env *types.EnvMeta) error { + ioStreams.Infof("Showing status of Component %s deployed in Environment %s\n", compName, env.Name) + var app *application.Application + var err error + if appName != "" { + app, err = application.Load(env.Name, appName) + } else { + app, err = application.MatchAppByComp(env.Name, compName) + } if err != nil { return err } - out, err := yaml.Marshal(application) - if err != nil { + + var health v1alpha2.HealthScope + if err = c.Get(ctx, client.ObjectKey{Namespace: env.Namespace, Name: application.FormatDefaultHealthScopeName(app.Name)}, &health); err != nil { return err } - ioStreams.Info(string(out)) + ioStreams.Info("Component Status:") + //TODO(wonderflow): add more information from health scope + ioStreams.Infof("\n %s \n\n", health.Status.Health) + + var appConfig v1alpha2.ApplicationConfiguration + if err = c.Get(ctx, client.ObjectKey{Namespace: env.Namespace, Name: app.Name}, &appConfig); err != nil { + return err + } + ioStreams.Infof("Last Deployment:\n\n") + ioStreams.Infof("\tCreated at:\t%v\n", appConfig.CreationTimestamp) + ioStreams.Infof("\tUpdated at:\t%v\n", app.UpdateTime.Format(time.RFC3339)) return nil } diff --git a/pkg/cmd/system.go b/pkg/cmd/system.go index 2116c218f..55628aac5 100644 --- a/pkg/cmd/system.go +++ b/pkg/cmd/system.go @@ -189,51 +189,55 @@ func GetOAMReleaseVersion() (string, error) { func GenNativeResourceDefinition(c client.Client) error { var capabilities []string + ctx := context.Background() for name, manifest := range workloadResource { - workloadDefinition, err := NewWorkloadDefinition(manifest) - if err != nil { - continue - } - err = c.Get(context.Background(), client.ObjectKey{Name: name}, &workloadDefinition) - if kubeerrors.IsNotFound(err) { - if err := c.Create(context.Background(), &workloadDefinition); err != nil { + wd := NewWorkloadDefinition(manifest) + capabilities = append(capabilities, name) + nwd := &oamv1.WorkloadDefinition{} + err := c.Get(ctx, client.ObjectKey{Name: name}, nwd) + if err != nil && kubeerrors.IsNotFound(err) { + if err := c.Create(context.Background(), &wd); err != nil { return fmt.Errorf("create workload definition %s hit an issue: %v", name, err) } - } else if err != nil { - return fmt.Errorf("get workload definition hit an issue: %v", err) + continue + } + wd.ResourceVersion = nwd.ResourceVersion + if err := c.Update(ctx, &wd); err != nil { + return fmt.Errorf("update workload definition %s err %v", wd.Name, err) } - capabilities = append(capabilities, name) } for name, manifest := range traitResource { - traitDefinition, err := NewTraitDefinition(manifest) - if err != nil { - fmt.Printf("creating local definition %s err %v", name, err) + td := NewTraitDefinition(manifest) + capabilities = append(capabilities, name) + ntd := &oamv1.TraitDefinition{} + err := c.Get(context.Background(), client.ObjectKey{Name: name}, ntd) + if err != nil && kubeerrors.IsNotFound(err) { + if err := c.Create(context.Background(), &td); err != nil { + return fmt.Errorf("create trait definition %s hit an issue: %v", name, err) + } continue } - err = c.Get(context.Background(), client.ObjectKey{Name: name}, &traitDefinition) - if kubeerrors.IsNotFound(err) { - if err := c.Create(context.Background(), &traitDefinition); err != nil { - return fmt.Errorf("create workload definition %s hit an issue: %v", name, err) - } - } else if err != nil { - return fmt.Errorf("get workload definition hit an issue: %v", err) + td.ResourceVersion = ntd.ResourceVersion + if err := c.Update(ctx, &td); err != nil { + return fmt.Errorf("update trait definition %s err %v", td.Name, err) } - capabilities = append(capabilities, name) } fmt.Printf("Successful applied %d kinds of Workloads and Traits: %s.", len(capabilities), strings.Join(capabilities, ",")) return nil } -func NewWorkloadDefinition(manifest string) (oamv1.WorkloadDefinition, error) { +func NewWorkloadDefinition(manifest string) oamv1.WorkloadDefinition { var workloadDefinition oamv1.WorkloadDefinition - err := yaml.Unmarshal([]byte(manifest), &workloadDefinition) - return workloadDefinition, err + // We have tests to make sure built-in resource can always unmarshal succeed + _ = yaml.Unmarshal([]byte(manifest), &workloadDefinition) + return workloadDefinition } -func NewTraitDefinition(manifest string) (oamv1.TraitDefinition, error) { +func NewTraitDefinition(manifest string) oamv1.TraitDefinition { var traitDefinition oamv1.TraitDefinition - err := yaml.Unmarshal([]byte(manifest), &traitDefinition) - return traitDefinition, err + // We have tests to make sure built-in resource can always unmarshal succeed + _ = yaml.Unmarshal([]byte(manifest), &traitDefinition) + return traitDefinition } diff --git a/pkg/oam/workload.go b/pkg/oam/workload.go index 52eae776a..c7bbd3891 100644 --- a/pkg/oam/workload.go +++ b/pkg/oam/workload.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "strings" "github.com/cloud-native-application/rudrx/pkg/plugins" @@ -65,23 +66,44 @@ func BaseComplete(envName string, workloadName string, appGroup string, flagSet } for _, v := range template.Parameters { - flagValue, _ := flagSet.GetString(v.Name) // Cli can check required flag before make a request to backend, but API itself could not, so validate flags here - if v.Required && v.Name != "name" && flagValue == "" { - return app, fmt.Errorf("required flag(s) \"%s\" not set", v.Name) + flag := flagSet.Lookup(v.Name) + if v.Name == "name" { + continue + } + if flag == nil || flag.Value.String() == "" { + if v.Required { + return nil, fmt.Errorf("required flag(s) \"%s\" not set", v.Name) + } + continue } switch v.Type { case cue.IntKind: - d, _ := strconv.ParseInt(flagValue, 10, 64) - workloadData[v.Name] = d + workloadData[v.Name], err = flagSet.GetInt64(v.Name) case cue.StringKind: - workloadData[v.Name] = flagValue + workloadData[v.Name], err = flagSet.GetString(v.Name) case cue.BoolKind: - d, _ := strconv.ParseBool(flagValue) - workloadData[v.Name] = d + workloadData[v.Name], err = flagSet.GetBool(v.Name) case cue.NumberKind, cue.FloatKind: - d, _ := strconv.ParseFloat(flagValue, 64) - workloadData[v.Name] = d + workloadData[v.Name], err = flagSet.GetFloat64(v.Name) + } + if err != nil { + if strings.Contains(err.Error(), "of flag of type string") { + data, _ := flagSet.GetString(v.Name) + switch v.Type { + case cue.IntKind: + workloadData[v.Name], err = strconv.ParseInt(data, 10, 64) + case cue.BoolKind: + workloadData[v.Name], err = strconv.ParseBool(data) + case cue.NumberKind, cue.FloatKind: + workloadData[v.Name], err = strconv.ParseFloat(data, 64) + } + if err != nil { + return nil, fmt.Errorf("get flag(s) \"%s\" err %v", v.Name, err) + } + continue + } + return nil, fmt.Errorf("get flag(s) \"%s\" err %v", v.Name, err) } } if err = app.SetWorkload(workloadName, tp, workloadData); err != nil {