diff --git a/references/cli/def.go b/references/cli/def.go index b77815251..df2af9209 100644 --- a/references/cli/def.go +++ b/references/cli/def.go @@ -56,7 +56,7 @@ import ( addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/common" "github.com/oam-dev/kubevela/pkg/utils/filters" - cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" + "github.com/oam-dev/kubevela/pkg/utils/util" ) const ( @@ -67,7 +67,7 @@ const ( ) // DefinitionCommandGroup create the command group for `vela def` command to manage definitions -func DefinitionCommandGroup(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command { +func DefinitionCommandGroup(c common.Args, order string, ioStreams util.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "def", Short: "Manage Definitions", @@ -82,7 +82,7 @@ func DefinitionCommandGroup(c common.Args, order string, ioStreams cmdutil.IOStr NewDefinitionListCommand(c), NewDefinitionEditCommand(c), NewDefinitionRenderCommand(c), - NewDefinitionApplyCommand(c), + NewDefinitionApplyCommand(c, ioStreams), NewDefinitionDelCommand(c), NewDefinitionInitCommand(c), NewDefinitionValidateCommand(c), @@ -518,8 +518,8 @@ func NewDefinitionGetCommand(c common.Args) *cobra.Command { } // NewDefinitionGenDocCommand create the `vela def doc-gen` command to generate documentation of definitions -func NewDefinitionGenDocCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command { - var path, location, i18nPath string +func NewDefinitionGenDocCommand(c common.Args, ioStreams util.IOStreams) *cobra.Command { + var docPath, location, i18nPath string cmd := &cobra.Command{ Use: "doc-gen NAME", Short: "Generate documentation for definitions", @@ -539,11 +539,11 @@ func NewDefinitionGenDocCommand(c common.Args, ioStreams cmdutil.IOStreams) *cob if err != nil { return errors.Wrapf(err, "failed to get `%s`", Namespace) } - return ShowReferenceMarkdown(context.Background(), c, ioStreams, args[0], path, location, i18nPath, namespace, 0) + return ShowReferenceMarkdown(context.Background(), c, ioStreams, args[0], docPath, location, i18nPath, namespace, 0) }, } - cmd.Flags().StringVarP(&path, "path", "p", "", "Specify the path for of the doc generated from definition.") + cmd.Flags().StringVarP(&docPath, "path", "p", "", "Specify the path for of the doc generated from definition.") cmd.Flags().StringVarP(&location, "location", "l", "", "specify the location for of the doc generated from definition, now supported options 'zh', 'en'. ") cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Specify which namespace the definition locates.") cmd.Flags().StringVarP(&i18nPath, "i18n", "", "https://kubevela.io/reference-i18n.json", "specify the location for of the doc generated from definition, now supported options 'zh', 'en'. ") @@ -843,13 +843,15 @@ func NewDefinitionRenderCommand(c common.Args) *cobra.Command { } // NewDefinitionApplyCommand create the `vela def apply` command to help user apply local definitions to k8s -func NewDefinitionApplyCommand(c common.Args) *cobra.Command { +func NewDefinitionApplyCommand(c common.Args, streams util.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "apply DEFINITION.cue", Short: "Apply X-Definition.", Long: "Apply X-Definition from local storage to kubernetes cluster. It will apply file to vela-system namespace by default.", Example: "# Command below will apply the local my-webservice.cue file to kubernetes vela-system namespace\n" + "> vela def apply my-webservice.cue\n" + + "# Apply the local directory including all files(YAML and CUE definition) to kubernetes vela-system namespace\n" + + "> vela def apply def/\n" + "# Command below will apply the ./defs/my-trait.cue file to kubernetes default namespace\n" + "> vela def apply ./defs/my-trait.cue --namespace default" + "# Command below will convert the ./defs/my-trait.cue file to kubernetes CRD object and print it without applying it to kubernetes\n" + @@ -869,85 +871,100 @@ func NewDefinitionApplyCommand(c common.Args) *cobra.Command { if err != nil { return errors.Wrapf(err, "failed to get `%s`", Namespace) } - config, err := c.GetConfig() - if err != nil { - return err + if len(args) < 1 { + return errors.New("you must specify the definition path, directory or URL") } - k8sClient, err := c.GetClient() - if err != nil { - return errors.Wrapf(err, "failed to get k8s client") - } - defpath := args[0] - defBytes, err := utils.ReadRemoteOrLocalPath(defpath, false) - if err != nil { - return errors.Wrapf(err, "failed to get from %s", defpath) - } - def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}} - - switch { - case strings.HasSuffix(defpath, ".yaml") || strings.HasSuffix(defpath, ".yml"): - // In this case, it's not in cue format, it's a yaml - if err = def.FromYAML(defBytes); err != nil { - return errors.Wrapf(err, "failed to parse YAML to definition") - } - if dryRun { - return errors.New("dry-run will render CUE to YAML, while the input is already in yaml") - } - // YAML won't validate or format CUE schematic - op, err := utils.CreateOrUpdate(ctx, k8sClient, &def) - if err != nil { - return err - } - cmd.Printf("%s %s in namespace %s %s.\n", def.GetKind(), def.GetName(), def.GetNamespace(), op) - return nil - default: - if err := def.FromCUEString(string(defBytes), config); err != nil { - return errors.Wrapf(err, "failed to parse CUE for definition") - } - def.SetNamespace(namespace) - } - - if dryRun { - s, err := prettyYAMLMarshal(def.Object) - if err != nil { - return errors.Wrapf(err, "failed to marshal CRD into YAML") - } - cmd.Print(s) - return nil - } - - oldDef := pkgdef.Definition{Unstructured: unstructured.Unstructured{}} - oldDef.SetGroupVersionKind(def.GroupVersionKind()) - err = k8sClient.Get(ctx, types2.NamespacedName{ - Namespace: def.GetNamespace(), - Name: def.GetName(), - }, &oldDef) - if err != nil { - if errors2.IsNotFound(err) { - kind := def.GetKind() - if err = k8sClient.Create(ctx, &def); err != nil { - return errors.Wrapf(err, "failed to create new definition in kubernetes") - } - cmd.Printf("%s %s created in namespace %s.\n", kind, def.GetName(), def.GetNamespace()) - return nil - } - return errors.Wrapf(err, "failed to check existence of target definition in kubernetes") - } - if err := oldDef.FromCUEString(string(defBytes), config); err != nil { - return errors.Wrapf(err, "failed to merge with existing definition") - } - if err = k8sClient.Update(ctx, &oldDef); err != nil { - return errors.Wrapf(err, "failed to update existing definition in kubernetes") - } - cmd.Printf("%s %s in namespace %s updated.\n", oldDef.GetKind(), oldDef.GetName(), oldDef.GetNamespace()) - return nil + return defApplyAll(ctx, c, streams, namespace, args[0], dryRun) }, } + cmd.Flags().BoolP(FlagDryRun, "", false, "only build definition from CUE into CRB object without applying it to kubernetes clusters") cmd.Flags().StringP(Namespace, "n", types.DefaultKubeVelaNS, "Specify which namespace the definition locates.") return cmd } +func defApplyAll(ctx context.Context, c common.Args, io util.IOStreams, namespace, path string, dryRun bool) error { + files, err := utils.LoadDataFromPath(ctx, path, utils.IsJSONYAMLorCUEFile) + if err != nil { + return errors.Wrapf(err, "failed to get from %s", path) + } + for _, f := range files { + result, err := defApplyOne(ctx, c, namespace, f.Path, f.Data, dryRun) + if err != nil { + return err + } + io.Infonln(result) + } + return nil +} + +func defApplyOne(ctx context.Context, c common.Args, namespace, defpath string, defBytes []byte, dryRun bool) (string, error) { + config, err := c.GetConfig() + if err != nil { + return "", err + } + k8sClient, err := c.GetClient() + if err != nil { + return "", errors.Wrapf(err, "failed to get k8s client") + } + + def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}} + + switch { + case strings.HasSuffix(defpath, ".yaml") || strings.HasSuffix(defpath, ".yml"): + // In this case, it's not in cue format, it's a yaml + if err = def.FromYAML(defBytes); err != nil { + return "", errors.Wrapf(err, "failed to parse YAML to definition") + } + if dryRun { + return "", errors.New("dry-run will render CUE to YAML, while the input is already in yaml") + } + // YAML won't validate or format CUE schematic + op, err := utils.CreateOrUpdate(ctx, k8sClient, &def) + if err != nil { + return "", err + } + return fmt.Sprintf("%s %s in namespace %s %s.\n", def.GetKind(), def.GetName(), def.GetNamespace(), op), nil + default: + if err := def.FromCUEString(string(defBytes), config); err != nil { + return "", errors.Wrapf(err, "failed to parse CUE for definition") + } + def.SetNamespace(namespace) + } + + if dryRun { + s, err := prettyYAMLMarshal(def.Object) + if err != nil { + return "", errors.Wrapf(err, "failed to marshal CRD into YAML") + } + return s, nil + } + + oldDef := pkgdef.Definition{Unstructured: unstructured.Unstructured{}} + oldDef.SetGroupVersionKind(def.GroupVersionKind()) + err = k8sClient.Get(ctx, types2.NamespacedName{ + Namespace: def.GetNamespace(), + Name: def.GetName(), + }, &oldDef) + if err != nil { + if errors2.IsNotFound(err) { + kind := def.GetKind() + if err = k8sClient.Create(ctx, &def); err != nil { + return "", errors.Wrapf(err, "failed to create new definition in kubernetes") + } + return fmt.Sprintf("%s %s created in namespace %s.\n", kind, def.GetName(), def.GetNamespace()), nil + } + return "", errors.Wrapf(err, "failed to check existence of target definition in kubernetes") + } + if err := oldDef.FromCUEString(string(defBytes), config); err != nil { + return "", errors.Wrapf(err, "failed to merge with existing definition") + } + if err = k8sClient.Update(ctx, &oldDef); err != nil { + return "", errors.Wrapf(err, "failed to update existing definition in kubernetes") + } + return fmt.Sprintf("%s %s in namespace %s updated.\n", oldDef.GetKind(), oldDef.GetName(), oldDef.GetNamespace()), nil +} + // NewDefinitionDelCommand create the `vela def del` command to help user delete existing definitions conveniently func NewDefinitionDelCommand(c common.Args) *cobra.Command { cmd := &cobra.Command{ diff --git a/references/cli/def_test.go b/references/cli/def_test.go index 9c3442924..04038d3c7 100644 --- a/references/cli/def_test.go +++ b/references/cli/def_test.go @@ -17,6 +17,7 @@ limitations under the License. package cli import ( + "bytes" "context" "fmt" "io" @@ -40,7 +41,7 @@ import ( pkgdef "github.com/oam-dev/kubevela/pkg/definition" addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" common2 "github.com/oam-dev/kubevela/pkg/utils/common" - cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" + "github.com/oam-dev/kubevela/pkg/utils/util" ) const ( @@ -180,7 +181,7 @@ func removeDir(dirname string, t *testing.T) { } func TestNewDefinitionCommandGroup(t *testing.T) { - cmd := DefinitionCommandGroup(common2.Args{}, "", cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) + cmd := DefinitionCommandGroup(common2.Args{}, "", util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) initCommand(cmd) cmd.SetArgs([]string{"-h"}) if err := cmd.Execute(); err != nil { @@ -460,7 +461,7 @@ func TestNewDefinitionGetCommand(t *testing.T) { func TestNewDefinitionGenDocCommand(t *testing.T) { c := initArgs() - cmd := NewDefinitionGenDocCommand(c, cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) + cmd := NewDefinitionGenDocCommand(c, util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) assert.NotNil(t, cmd.Execute()) cmd.SetArgs([]string{"alibaba-xxxxxxx"}) @@ -559,8 +560,9 @@ func TestNewDefinitionRenderCommand(t *testing.T) { func TestNewDefinitionApplyCommand(t *testing.T) { c := initArgs() + ioStreams := util.IOStreams{In: os.Stdin, Out: bytes.NewBuffer(nil), ErrOut: bytes.NewBuffer(nil)} // dry-run test - cmd := NewDefinitionApplyCommand(c) + cmd := NewDefinitionApplyCommand(c, ioStreams) initCommand(cmd) _, traitFilename := createLocalTrait(t) defer removeFile(traitFilename, t) @@ -569,7 +571,7 @@ func TestNewDefinitionApplyCommand(t *testing.T) { t.Fatalf("unexpeced error when executing apply command: %v", err) } // normal test and reapply - cmd = NewDefinitionApplyCommand(c) + cmd = NewDefinitionApplyCommand(c, ioStreams) initCommand(cmd) cmd.SetArgs([]string{traitFilename}) for i := 0; i < 2; i++ { diff --git a/references/cli/defapply_test.go b/references/cli/defapply_test.go new file mode 100644 index 000000000..ef970dd9f --- /dev/null +++ b/references/cli/defapply_test.go @@ -0,0 +1,60 @@ +/* + Copyright 2022 The KubeVela Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package cli + +import ( + "bytes" + "context" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/pkg/utils/util" +) + +var _ = Describe("Test def apply cli", func() { + + When("test vela def apply", func() { + + It("should not have err and applied all def files", func() { + + buffer := bytes.NewBuffer(nil) + ioStreams := util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer} + + ctx := context.Background() + c := common.Args{} + c.SetConfig(cfg) + c.SetClient(k8sClient) + err := defApplyAll(ctx, c, ioStreams, types.DefaultKubeVelaNS, "./test-data/defapply", false) + Expect(err).Should(BeNil()) + + By("check component definition in YAML exist") + cml := v1beta1.ComponentDefinition{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "testdefyaml", Namespace: types.DefaultKubeVelaNS}, &cml)).Should(BeNil()) + + By("check trait definition in CUE exist") + traitd := v1beta1.TraitDefinition{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "testdefcue", Namespace: types.DefaultKubeVelaNS}, &traitd)).Should(BeNil()) + + }) + }) +}) diff --git a/references/cli/cli_suite_test.go b/references/cli/suite_test.go similarity index 100% rename from references/cli/cli_suite_test.go rename to references/cli/suite_test.go diff --git a/references/cli/test-data/defapply/testdef.cue b/references/cli/test-data/defapply/testdef.cue new file mode 100644 index 000000000..7ecd9bdae --- /dev/null +++ b/references/cli/test-data/defapply/testdef.cue @@ -0,0 +1,25 @@ +testdefcue: { + type: "trait" + annotations: {} + labels: { + "ui-hidden": "true" + } + description: "Add host aliases on K8s pod for your workload which follows the pod spec in path 'spec.template'." + attributes: { + podDisruptive: false + appliesToWorkloads: ["*"] + } +} +template: { + patch: { + // +patchKey=ip + spec: template: spec: hostAliases: parameter.hostAliases + } + parameter: { + // +usage=Specify the hostAliases to add + hostAliases: [...{ + ip: string + hostnames: [...string] + }] + } +} diff --git a/references/cli/test-data/defapply/testdef.yaml b/references/cli/test-data/defapply/testdef.yaml new file mode 100644 index 000000000..f1d922fe6 --- /dev/null +++ b/references/cli/test-data/defapply/testdef.yaml @@ -0,0 +1,20 @@ +apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + annotations: + definition.oam.dev/description: xxx + creationTimestamp: null + labels: + type: terraform + name: testdefyaml + namespace: vela-system +spec: + schematic: + terraform: + configuration: https://github.com/kubevela-contrib/terraform-modules.git + path: alibaba/vswitch + type: remote + workload: + definition: + apiVersion: terraform.core.oam.dev/v1beta2 + kind: Configuration \ No newline at end of file