From da5282a15d34f849020ae09d0fcc5203150e725c Mon Sep 17 00:00:00 2001 From: Charlie Chiang Date: Wed, 6 Jul 2022 15:41:00 +0800 Subject: [PATCH] Feat: show or list revisions in `vela def get` and `vela show` (#4307) * Feat: support --version when getting definitions Signed-off-by: Charlie Chiang * Test: add tests Signed-off-by: Charlie Chiang * Test: add tests for vela show Signed-off-by: Charlie Chiang * Style: format imports Signed-off-by: Charlie Chiang * Test: fix tests Signed-off-by: Charlie Chiang * Test: fix tests Signed-off-by: Charlie Chiang * Test: fix tests Signed-off-by: Charlie Chiang --- pkg/definition/definition.go | 122 +++++++++++++++++- pkg/definition/definition_test.go | 81 +++++++++++- .../testdata/custom-trait-affinity-v1.yaml | 59 +++++++++ .../default-component-webservice-v1.yaml | 62 +++++++++ .../default-component-webservice-v2.yaml | 62 +++++++++ .../testdata/default-trait-affinity-v1.yaml | 64 +++++++++ references/cli/def.go | 76 ++++++++++- references/cli/def_test.go | 42 ++++++ references/cli/show.go | 38 +++++- references/plugins/cluster.go | 58 +++++++++ references/plugins/cluster_test.go | 54 ++++++++ 11 files changed, 705 insertions(+), 13 deletions(-) create mode 100644 pkg/definition/testdata/custom-trait-affinity-v1.yaml create mode 100644 pkg/definition/testdata/default-component-webservice-v1.yaml create mode 100644 pkg/definition/testdata/default-component-webservice-v2.yaml create mode 100644 pkg/definition/testdata/default-trait-affinity-v1.yaml diff --git a/pkg/definition/definition.go b/pkg/definition/definition.go index 15a04f9dc..b1fed2783 100644 --- a/pkg/definition/definition.go +++ b/pkg/definition/definition.go @@ -23,8 +23,6 @@ import ( "fmt" "strings" - "github.com/oam-dev/kubevela/pkg/utils/filters" - "cuelang.org/go/cue" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/format" @@ -33,15 +31,19 @@ import ( "cuelang.org/go/tools/fix" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" velacue "github.com/oam-dev/kubevela/pkg/cue" "github.com/oam-dev/kubevela/pkg/cue/model/sets" "github.com/oam-dev/kubevela/pkg/cue/model/value" "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/utils/filters" ) const ( @@ -65,6 +67,24 @@ var ( "scope": v1beta1.ScopeDefinitionKind, "workflow-step": v1beta1.WorkflowStepDefinitionKind, } + // StringToDefinitionType converts user input to DefinitionType used in DefinitionRevisions + StringToDefinitionType = map[string]common.DefinitionType{ + // component + "component": common.ComponentType, + // trait + "trait": common.TraitType, + // policy + "policy": common.PolicyType, + // workflow-step + "workflow-step": common.WorkflowStepType, + } + // DefinitionKindToNameLabel records DefinitionRevision types and labels to search its name + DefinitionKindToNameLabel = map[common.DefinitionType]string{ + common.ComponentType: oam.LabelComponentDefinitionName, + common.TraitType: oam.LabelTraitDefinitionName, + common.PolicyType: oam.LabelPolicyDefinitionName, + common.WorkflowStepType: oam.LabelWorkflowStepDefinitionName, + } ) // Definition the general struct for handling all kinds of definitions like ComponentDefinition or TraitDefinition @@ -425,6 +445,104 @@ func SearchDefinition(c client.Client, definitionType, namespace string, additio return definitions, nil } +// SearchDefinitionRevisions finds DefinitionRevisions. +// Use defName to filter DefinitionRevisions using the name of the underlying Definition. +// Empty defName will keep everything. +// Use defType to only keep DefinitionRevisions of the specified DefinitionType. +// Empty defType will search every possible type. +// Use rev to only keep the revision you want. rev=0 will keep every revision. +func SearchDefinitionRevisions(ctx context.Context, c client.Client, namespace string, + defName string, defType common.DefinitionType, rev int64) ([]v1beta1.DefinitionRevision, error) { + var nameLabels []string + + if defName == "" { + // defName="" means we don't care about the underlying definition names. + // So, no need to add name labels, just use anything to let the loop run once. + nameLabels = append(nameLabels, "") + } else { + // Since different definitions have different labels for its name, we need to + // find the corresponding label for definition names, to match names later. + // Empty defType will give all possible name labels of DefinitionRevisions, + // so that we can search for DefinitionRevisions of all Definition types. + for k, v := range DefinitionKindToNameLabel { + if defType != "" && defType != k { + continue + } + nameLabels = append(nameLabels, v) + } + } + + var defRev []v1beta1.DefinitionRevision + + // Search DefinitionRevisions using each possible label + for _, l := range nameLabels { + var listOptions []client.ListOption + if namespace != "" { + listOptions = append(listOptions, client.InNamespace(namespace)) + } + // Using name label to find DefinitionRevisions with specified name. + if defName != "" { + listOptions = append(listOptions, client.MatchingLabels{ + l: defName, + }) + } + + objs := v1beta1.DefinitionRevisionList{} + objs.SetGroupVersionKind(schema.GroupVersionKind{ + Group: v1beta1.Group, + Version: v1beta1.Version, + Kind: v1beta1.DefinitionRevisionKind, + }) + + // Search for DefinitionRevisions + if err := c.List(ctx, &objs, listOptions...); err != nil { + return nil, errors.Wrapf(err, "failed to list DefinitionRevisions of %s", defName) + } + + for _, dr := range objs.Items { + // Keep only the specified type + if defType != "" && defType != dr.Spec.DefinitionType { + continue + } + // Only give the revision that the user wants + if rev != 0 && rev != dr.Spec.Revision { + continue + } + defRev = append(defRev, dr) + } + } + + return defRev, nil +} + +// GetDefinitionFromDefinitionRevision will extract the underlying Definition from a DefinitionRevision. +func GetDefinitionFromDefinitionRevision(rev *v1beta1.DefinitionRevision) (*Definition, error) { + var def *Definition + var u map[string]interface{} + var err error + + switch rev.Spec.DefinitionType { + case common.ComponentType: + u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&rev.Spec.ComponentDefinition) + case common.TraitType: + u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&rev.Spec.TraitDefinition) + case common.PolicyType: + u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&rev.Spec.PolicyDefinition) + case common.WorkflowStepType: + u, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&rev.Spec.WorkflowStepDefinition) + default: + return nil, fmt.Errorf("unsupported definition type: %s", rev.Spec.DefinitionType) + } + + if err != nil { + return nil, err + } + + def = &Definition{Unstructured: unstructured.Unstructured{Object: u}} + + return def, nil +} + // GetDefinitionDefaultSpec returns the default spec of Definition with given kind. This may be implemented with cue in the future. func GetDefinitionDefaultSpec(kind string) map[string]interface{} { switch kind { diff --git a/pkg/definition/definition_test.go b/pkg/definition/definition_test.go index 974263167..42c72206a 100644 --- a/pkg/definition/definition_test.go +++ b/pkg/definition/definition_test.go @@ -19,19 +19,22 @@ package definition import ( "context" "encoding/json" + "io/ioutil" + "path/filepath" "strings" "testing" - "github.com/oam-dev/kubevela/pkg/utils/filters" - + "github.com/stretchr/testify/assert" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/yaml" + common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" addonutils "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/pkg/utils/filters" ) func TestDefinitionBasicFunctions(t *testing.T) { @@ -143,3 +146,75 @@ func TestDefinitionBasicFunctions(t *testing.T) { t.Fatalf("failed to search definition with addon filter applied: %s", "too many results returned") } } + +func TestDefinitionRevisionSearch(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(common.Scheme).Build() + + var err error + + // Load test DefinitionRevisions files into client + testFiles, err := ioutil.ReadDir("testdata") + assert.NoError(t, err, "read testdata failed") + for _, file := range testFiles { + if !strings.HasSuffix(file.Name(), ".yaml") { + continue + } + content, err := ioutil.ReadFile(filepath.Join("testdata", file.Name())) + assert.NoError(t, err) + def := &v1beta1.DefinitionRevision{} + err = yaml.Unmarshal(content, def) + assert.NoError(t, err) + err = c.Create(context.TODO(), def) + assert.NoError(t, err, "cannot create "+file.Name()) + } + + var defrevs []v1beta1.DefinitionRevision + + // Read with no conditions, should at least have 4 defrevs + defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "", "", "", 0) + assert.NoError(t, err) + assert.Equal(t, true, len(defrevs) >= 4) + + // Restrict namespace + defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "rev-test-custom-ns", "", "", 0) + assert.NoError(t, err) + assert.Equal(t, 1, len(defrevs)) + + // Restrict type + defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "rev-test-ns", "", common2.ComponentType, 0) + assert.NoError(t, err) + assert.Equal(t, 2, len(defrevs)) + + // Restrict revision + defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "rev-test-ns", "", "", 1) + assert.NoError(t, err) + assert.Equal(t, 2, len(defrevs)) + + // Restrict name + defrevs, err = SearchDefinitionRevisions(context.TODO(), c, "rev-test-ns", "webservice", "", 1) + assert.NoError(t, err) + assert.Equal(t, 1, len(defrevs)) + + // Test GetDefinitionFromDefinitionRevision + defrev := defrevs[0] + + // Simulate ComponentDefinition + defrev.Spec.DefinitionType = common2.ComponentType + _, err = GetDefinitionFromDefinitionRevision(&defrev) + assert.NoError(t, err) + + // Simulate TraitDefinition + defrev.Spec.DefinitionType = common2.TraitType + _, err = GetDefinitionFromDefinitionRevision(&defrev) + assert.NoError(t, err) + + // Simulate PolicyDefinition + defrev.Spec.DefinitionType = common2.PolicyType + _, err = GetDefinitionFromDefinitionRevision(&defrev) + assert.NoError(t, err) + + // Simulate WorkflowStepDefinition + defrev.Spec.DefinitionType = common2.WorkflowStepType + _, err = GetDefinitionFromDefinitionRevision(&defrev) + assert.NoError(t, err) +} diff --git a/pkg/definition/testdata/custom-trait-affinity-v1.yaml b/pkg/definition/testdata/custom-trait-affinity-v1.yaml new file mode 100644 index 000000000..701113b4c --- /dev/null +++ b/pkg/definition/testdata/custom-trait-affinity-v1.yaml @@ -0,0 +1,59 @@ +apiVersion: core.oam.dev/v1beta1 +kind: DefinitionRevision +metadata: + labels: + app.kubernetes.io/managed-by: Helm + custom.definition.oam.dev/ui-hidden: 'true' + trait.oam.dev/name: affinity + name: affinity-v1 + namespace: rev-test-custom-ns +spec: + componentDefinition: + metadata: {} + spec: + workload: + definition: + apiVersion: '' + kind: '' + status: {} + definitionType: Trait + policyDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} + revision: 1 + revisionHash: 9db54dd8da7e4bd5 + traitDefinition: + apiVersion: core.oam.dev/v1beta1 + kind: TraitDefinition + metadata: + annotations: + definition.oam.dev/description: >- + affinity specify affinity and tolerationon K8s pod for your workload + which follows the pod spec in path 'spec.template'. + name: affinity + namespace: rev-test-custom-ns + spec: + appliesToWorkloads: + - '*' + definitionRef: + name: '' + podDisruptive: true + schematic: + cue: + template: | + parameter: { + // +usage=Specify the number of workload + replicas: *1 | int + } + // +patchStrategy=retainKeys + patch: spec: replicas: parameter.replicas + status: {} + workflowStepDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} diff --git a/pkg/definition/testdata/default-component-webservice-v1.yaml b/pkg/definition/testdata/default-component-webservice-v1.yaml new file mode 100644 index 000000000..1449bfe02 --- /dev/null +++ b/pkg/definition/testdata/default-component-webservice-v1.yaml @@ -0,0 +1,62 @@ +apiVersion: core.oam.dev/v1beta1 +kind: DefinitionRevision +metadata: + labels: + app.kubernetes.io/managed-by: Helm + componentdefinition.oam.dev/name: webservice + name: webservice-v1 + namespace: rev-test-ns +spec: + componentDefinition: + apiVersion: core.oam.dev/v1beta1 + kind: ComponentDefinition + metadata: + annotations: + definition.oam.dev/description: >- + Describes long-running, scalable, containerized services that have a + stable network endpoint to receive external network traffic from + customers. + name: webservice + namespace: rev-test-ns + spec: + schematic: + cue: + template: | + output: parameter.objects[0] + outputs: { + for i, v in parameter.objects { + if i > 0 { + "objects-\(i)": v + } + } + } + parameter: { + objects: [...{}] + } + workload: + definition: + apiVersion: "" + kind: "" + type: autodetects.core.oam.dev + status: {} + definitionType: Component + policyDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} + revision: 1 + revisionHash: dfa072dac5088ed8 + traitDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} + workflowStepDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} diff --git a/pkg/definition/testdata/default-component-webservice-v2.yaml b/pkg/definition/testdata/default-component-webservice-v2.yaml new file mode 100644 index 000000000..47f76e8bf --- /dev/null +++ b/pkg/definition/testdata/default-component-webservice-v2.yaml @@ -0,0 +1,62 @@ +apiVersion: core.oam.dev/v1beta1 +kind: DefinitionRevision +metadata: + labels: + app.kubernetes.io/managed-by: Helm + componentdefinition.oam.dev/name: webservice + name: webservice-v2 + namespace: rev-test-ns +spec: + componentDefinition: + apiVersion: core.oam.dev/v1beta1 + kind: ComponentDefinition + metadata: + annotations: + definition.oam.dev/description: >- + Describes long-running, scalable, containerized services that have a + stable network endpoint to receive external network traffic from + customers. + name: webservice + namespace: rev-test-ns + spec: + schematic: + cue: + template: | + output: parameter.objects[0] + outputs: { + for i, v in parameter.objects { + if i > 0 { + "objects-\(i)": v + } + } + } + parameter: { + objects: [...{}] + } + workload: + definition: + apiVersion: "" + kind: "" + type: autodetects.core.oam.dev + status: {} + definitionType: Component + policyDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} + revision: 2 + revisionHash: c0a0c0591d0bc6 + traitDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} + workflowStepDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} diff --git a/pkg/definition/testdata/default-trait-affinity-v1.yaml b/pkg/definition/testdata/default-trait-affinity-v1.yaml new file mode 100644 index 000000000..958749d1a --- /dev/null +++ b/pkg/definition/testdata/default-trait-affinity-v1.yaml @@ -0,0 +1,64 @@ +apiVersion: core.oam.dev/v1beta1 +kind: DefinitionRevision +metadata: + labels: + app.kubernetes.io/managed-by: Helm + custom.definition.oam.dev/ui-hidden: 'true' + trait.oam.dev/name: affinity + name: affinity-v1 + namespace: rev-test-ns +spec: + componentDefinition: + metadata: {} + spec: + workload: + definition: + apiVersion: '' + kind: '' + status: {} + definitionType: Trait + policyDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} + revision: 1 + revisionHash: 9db54dd8d5314bd5 + traitDefinition: + apiVersion: core.oam.dev/v1beta1 + kind: TraitDefinition + metadata: + annotations: + definition.oam.dev/description: >- + affinity specify affinity and tolerationon K8s pod for your workload + which follows the pod spec in path 'spec.template'. + meta.helm.sh/release-name: kubevela + meta.helm.sh/release-namespace: rev-test-ns + labels: + app.kubernetes.io/managed-by: Helm + custom.definition.oam.dev/ui-hidden: 'true' + name: affinity + namespace: rev-test-ns + spec: + appliesToWorkloads: + - '*' + definitionRef: + name: '' + podDisruptive: true + schematic: + cue: + template: | + parameter: { + // +usage=Specify the number of workload + replicas: *1 | int + } + // +patchStrategy=retainKeys + patch: spec: replicas: parameter.replicas + status: {} + workflowStepDefinition: + metadata: {} + spec: + definitionRef: + name: '' + status: {} diff --git a/references/cli/def.go b/references/cli/def.go index 32b6a93bf..776de0279 100644 --- a/references/cli/def.go +++ b/references/cli/def.go @@ -26,7 +26,9 @@ import ( "os/exec" "path" "path/filepath" + "reflect" "regexp" + "strconv" "strings" "time" @@ -409,8 +411,39 @@ func getSingleDefinition(cmd *cobra.Command, definitionName string, client clien return &pkgdef.Definition{Unstructured: definitions[0]}, nil } +// getDefRevs will search for DefinitionRevisions with specified conditions. +// Check SearchDefinitionRevisions for details. +func getDefRevs(ctx context.Context, client client.Client, ns, defTypeStr, defName string, rev int64) ([]v1beta1.DefinitionRevision, error) { + defType, ok := pkgdef.StringToDefinitionType[defTypeStr] + // Empty definition type is intentionally allowed, to allow the user to match all definition types + if defTypeStr != "" && !ok { + return nil, fmt.Errorf("%s is not a valid type. Valid types are %v", defTypeStr, reflect.ValueOf(pkgdef.StringToDefinitionType).MapKeys()) + } + + return pkgdef.SearchDefinitionRevisions(ctx, client, ns, defName, defType, rev) +} + +// printDefRevs will print DefinitionRevisions +func printDefRevs(ctx context.Context, cmd *cobra.Command, client client.Client, ns, defTypeStr, defName string) error { + revs, err := getDefRevs(ctx, client, ns, defTypeStr, defName, 0) + if err != nil { + return err + } + + table := newUITable() + table.AddRow("NAME", "REVISION", "TYPE", "HASH") + for _, rev := range revs { + table.AddRow(defName, rev.Spec.Revision, rev.Spec.DefinitionType, rev.Spec.RevisionHash) + } + cmd.Println(table) + + return nil +} + // NewDefinitionGetCommand create the `vela def get` command to get definition from k8s func NewDefinitionGetCommand(c common.Args) *cobra.Command { + var listRevisions bool + var targetRevision string cmd := &cobra.Command{ Use: "get NAME", Short: "Get definition", @@ -433,10 +466,45 @@ func NewDefinitionGetCommand(c common.Args) *cobra.Command { if err != nil { return errors.Wrapf(err, "failed to get k8s client") } - def, err := getSingleDefinition(cmd, args[0], k8sClient, definitionType, namespace) - if err != nil { - return err + + if listRevisions { + return printDefRevs(context.Background(), cmd, k8sClient, namespace, definitionType, args[0]) } + + var def *pkgdef.Definition + + // Get history Definition from DefinitionRevisions + if targetRevision != "" { + // "v1", "1", both need to work + targetRevision = strings.TrimPrefix(targetRevision, "v") + ver, err := strconv.Atoi(targetRevision) + if err != nil { + return fmt.Errorf("invalid version: %w", err) + } + + // Get the user-specified revision. + revs, err := getDefRevs(context.Background(), k8sClient, namespace, definitionType, args[0], int64(ver)) + if err != nil { + return err + } + if len(revs) == 0 { + return fmt.Errorf("no %s with revision %s found in namespace %s", args[0], targetRevision, namespace) + } + + // Now we have at least one DefinitionRevision (typically it will only be one). + // They all fit user's conditions. We will use the first one. + // Extract Definition from DefinitionRevision that we just got. + def, err = pkgdef.GetDefinitionFromDefinitionRevision(&revs[0]) + if err != nil { + return err + } + } else { + def, err = getSingleDefinition(cmd, args[0], k8sClient, definitionType, namespace) + if err != nil { + return err + } + } + cueString, err := def.ToCUEString() if err != nil { return errors.Wrapf(err, "failed to get cue format definition") @@ -449,6 +517,8 @@ func NewDefinitionGetCommand(c common.Args) *cobra.Command { } cmd.Flags().StringP(FlagType, "t", "", "Specify which definition type to get. If empty, all types will be searched. Valid types: "+strings.Join(pkgdef.ValidDefinitionTypes(), ", ")) cmd.Flags().StringP(Namespace, "n", "", "Specify which namespace to get. If empty, all namespaces will be searched.") + cmd.Flags().BoolVarP(&listRevisions, "revisions", "", false, "List revisions of the specified definition.") + cmd.Flags().StringVarP(&targetRevision, "revision", "r", "", "Get the specified version of a definition.") return cmd } diff --git a/references/cli/def_test.go b/references/cli/def_test.go index ec1db3a84..fe9630d3a 100644 --- a/references/cli/def_test.go +++ b/references/cli/def_test.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "io" + "io/ioutil" "os" "path/filepath" "strings" @@ -32,6 +33,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/yaml" common3 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" @@ -413,6 +415,46 @@ func TestNewDefinitionGetCommand(t *testing.T) { if err := cmd.Execute(); err == nil { t.Fatalf("expect found no trait error, but not found") } + + // Load test DefinitionRevisions files into client + dir := filepath.Join("..", "..", "pkg", "definition", "testdata") + testFiles, err := ioutil.ReadDir(dir) + assert.NoError(t, err, "read testdata failed") + for _, file := range testFiles { + if !strings.HasSuffix(file.Name(), ".yaml") { + continue + } + content, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) + assert.NoError(t, err) + def := &v1beta1.DefinitionRevision{} + err = yaml.Unmarshal(content, def) + assert.NoError(t, err) + client, err := c.GetClient() + assert.NoError(t, err) + err = client.Create(context.TODO(), def) + assert.NoError(t, err, "cannot create "+file.Name()) + } + + // test get revision list + cmd = NewDefinitionGetCommand(c) + initCommand(cmd) + cmd.SetArgs([]string{"webservice", "--revisions", "--namespace=rev-test-ns"}) + err = cmd.Execute() + assert.NoError(t, err) + + // test get a non-existent revision + cmd = NewDefinitionGetCommand(c) + initCommand(cmd) + cmd.SetArgs([]string{"webservice", "--revision=3"}) + err = cmd.Execute() + assert.NotNil(t, err, "should have not found error") + + // test get a revision + cmd = NewDefinitionGetCommand(c) + initCommand(cmd) + cmd.SetArgs([]string{"webservice", "--revision=1", "--namespace=rev-test-ns"}) + err = cmd.Execute() + assert.NoError(t, err) } func TestNewDefinitionGenDocCommand(t *testing.T) { diff --git a/references/cli/show.go b/references/cli/show.go index 68799dc3c..1c3c10d93 100644 --- a/references/cli/show.go +++ b/references/cli/show.go @@ -26,6 +26,8 @@ import ( "os/signal" "path/filepath" "runtime" + "strconv" + "strings" "syscall" "time" @@ -61,6 +63,7 @@ var webSite bool // NewCapabilityShowCommand shows the reference doc for a component type or trait func NewCapabilityShowCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command { + var revision string cmd := &cobra.Command{ Use: "show", Short: "Show the reference doc for a component, trait or workflow.", @@ -76,10 +79,24 @@ func NewCapabilityShowCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra if err != nil { return err } + + if revision == "" { + if webSite { + return startReferenceDocsSite(ctx, namespace, c, ioStreams, capabilityName) + } + return ShowReferenceConsole(ctx, c, ioStreams, capabilityName, namespace, 0) + } + + // v1, 1, both need to work + version := strings.TrimPrefix(revision, "v") + ver, err := strconv.Atoi(version) + if err != nil { + return fmt.Errorf("invalid revision: %w", err) + } if webSite { return startReferenceDocsSite(ctx, namespace, c, ioStreams, capabilityName) } - return ShowReferenceConsole(ctx, c, ioStreams, capabilityName, namespace) + return ShowReferenceConsole(ctx, c, ioStreams, capabilityName, namespace, int64(ver)) }, Annotations: map[string]string{ types.TagCommandType: types.TypeStart, @@ -87,6 +104,7 @@ func NewCapabilityShowCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra } cmd.Flags().BoolVarP(&webSite, "web", "", false, " start web doc site") + cmd.Flags().StringVarP(&revision, "revision", "r", "", "Get the specified revision of a definition. Use def get to list revisions.") addNamespaceAndEnvArg(cmd) cmd.SetOut(ioStreams.Out) @@ -384,7 +402,7 @@ func getDefinitions(capabilities []types.Capability) ([]string, []string, []stri } // ShowReferenceConsole will show capability reference in console -func ShowReferenceConsole(ctx context.Context, c common.Args, ioStreams cmdutil.IOStreams, capabilityName string, ns string) error { +func ShowReferenceConsole(ctx context.Context, c common.Args, ioStreams cmdutil.IOStreams, capabilityName string, ns string, rev int64) error { config, err := c.GetConfig() if err != nil { return err @@ -393,9 +411,19 @@ func ShowReferenceConsole(ctx context.Context, c common.Args, ioStreams cmdutil. if err != nil { return err } - capability, err := plugins.GetCapabilityByName(ctx, c, capabilityName, ns, pd) - if err != nil { - return err + + var capability *types.Capability + + if rev == 0 { + capability, err = plugins.GetCapabilityByName(ctx, c, capabilityName, ns, pd) + if err != nil { + return err + } + } else { + capability, err = plugins.GetCapabilityFromDefinitionRevision(ctx, c, pd, ns, capabilityName, rev) + if err != nil { + return err + } } cli, err := c.GetClient() diff --git a/references/plugins/cluster.go b/references/plugins/cluster.go index d22643f00..210816d1b 100644 --- a/references/plugins/cluster.go +++ b/references/plugins/cluster.go @@ -24,6 +24,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/oam-dev/kubevela/pkg/definition" + "github.com/pkg/errors" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" @@ -453,6 +455,62 @@ func GetCapabilityByName(ctx context.Context, c common.Args, capabilityName stri return nil, fmt.Errorf("could not find %s in namespace %s, or %s", capabilityName, ns, types.DefaultKubeVelaNS) } +// GetCapabilityFromDefinitionRevision gets capabilities from the underlying Definition in DefinitionRevisions +func GetCapabilityFromDefinitionRevision(ctx context.Context, c common.Args, pd *packages.PackageDiscover, ns, defName string, r int64) (*types.Capability, error) { + k8sClient, err := c.GetClient() + if err != nil { + return nil, err + } + + revs, err := definition.SearchDefinitionRevisions(ctx, k8sClient, ns, defName, "", r) + if err != nil { + return nil, err + } + // `ns` defaults to `default` in `vela show`, if user doesn't specify anything, + // which often is not the desired behavior. + // So we need to search again in the vela-system namespace, if no revisions found. + // This behavior is consistent with the code above in GetCapabilityByName(), which also does double-search. + if len(revs) == 0 && ns == "default" { + revs, err = definition.SearchDefinitionRevisions(ctx, k8sClient, types.DefaultKubeVelaNS, defName, "", r) + if err != nil { + return nil, err + } + } + if len(revs) == 0 { + return nil, fmt.Errorf("no %s with revision %d found in namespace %s or %s", defName, r, ns, types.DefaultKubeVelaNS) + } + + rev := revs[0] + + switch rev.Spec.DefinitionType { + case commontypes.ComponentType: + var refName string + componentDef := rev.Spec.ComponentDefinition + // if workload type of ComponentDefinition is unclear, + // set the DefinitionReference's Name to AutoDetectWorkloadDefinition + if componentDef.Spec.Workload.Type == types.AutoDetectWorkloadDefinition { + refName = types.AutoDetectWorkloadDefinition + } else { + dm, err := c.GetDiscoveryMapper() + if err != nil { + return nil, err + } + ref, err := util.ConvertWorkloadGVK2Definition(dm, componentDef.Spec.Workload.Definition) + if err != nil { + return nil, err + } + refName = ref.Name + } + return GetCapabilityByComponentDefinitionObject(componentDef, refName) + case commontypes.TraitType: + return GetCapabilityByTraitDefinitionObject(rev.Spec.TraitDefinition) + case commontypes.WorkflowStepType: + return GetCapabilityByWorkflowStepDefinitionObject(rev.Spec.WorkflowStepDefinition, pd) + default: + return nil, fmt.Errorf("unsupported type %s", rev.Spec.DefinitionType) + } +} + // GetCapabilityByComponentDefinitionObject gets capability by ComponentDefinition object func GetCapabilityByComponentDefinitionObject(componentDef v1beta1.ComponentDefinition, referenceName string) (*types.Capability, error) { capability, err := HandleDefinition(componentDef.Name, referenceName, componentDef.Annotations, componentDef.Labels, diff --git a/references/plugins/cluster_test.go b/references/plugins/cluster_test.go index 8f968f17b..25d9fe03a 100644 --- a/references/plugins/cluster_test.go +++ b/references/plugins/cluster_test.go @@ -19,7 +19,10 @@ package plugins import ( "context" "encoding/json" + "io/ioutil" "os" + "path/filepath" + "strings" "cuelang.org/go/cue" "github.com/google/go-cmp/cmp" @@ -344,3 +347,54 @@ var _ = Describe("test GetNamespacedCapabilitiesFromCluster", func() { }) }) + +var _ = Describe("test GetCapabilityFromDefinitionRevision", func() { + var ( + ctx context.Context + c common.Args + ) + + BeforeEach(func() { + c = common.Args{} + c.SetClient(k8sClient) + ctx = context.Background() + + By("create namespace") + Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "rev-test-custom-ns"}})).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "rev-test-ns"}})).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + // Load test DefinitionRevisions files into client + dir := filepath.Join("..", "..", "pkg", "definition", "testdata") + testFiles, err := ioutil.ReadDir(dir) + Expect(err).Should(Succeed()) + for _, file := range testFiles { + if !strings.HasSuffix(file.Name(), ".yaml") { + continue + } + content, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) + Expect(err).Should(Succeed()) + def := &corev1beta1.DefinitionRevision{} + err = yaml.Unmarshal(content, def) + Expect(err).Should(Succeed()) + client, err := c.GetClient() + Expect(err).Should(Succeed()) + err = client.Create(context.TODO(), def) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + } + }) + + It("non-existent defrev", func() { + _, err := GetCapabilityFromDefinitionRevision(ctx, c, nil, "rev-test-custom-ns", "not-a-name", 0) + Expect(err).ShouldNot(Succeed()) + }) + + It("component type", func() { + _, err := GetCapabilityFromDefinitionRevision(ctx, c, nil, "rev-test-ns", "webservice", 0) + Expect(err).Should(Succeed()) + }) + + It("trait type", func() { + _, err := GetCapabilityFromDefinitionRevision(ctx, c, nil, "rev-test-custom-ns", "affinity", 0) + Expect(err).Should(Succeed()) + }) +})