mirror of
https://github.com/kubevela/kubevela.git
synced 2026-07-29 02:11:44 +00:00
* feat(cue/upgrade): auto-remediate legacy CUE syntax at render time
Transparently rewrite CUE templates that use deprecated list arithmetic
(+, *) and conflicting field names (error) so that older definitions
continue to work with CUE ≥ v0.14 (KubeVela ≥ 1.11).
- CUEUpgradeFunc registry with ID, CUE/KubeVela version guards, precheck,
and upgrade function fields
- upgradeListConcatenation: rewrites list1+list2 → list.Concat([list1,list2])
and list*n → list.Repeat(list, n); adds "list" import as needed
- collectAddChain + extractListConcatArgs: flatten left-associative + chains
and existing list.Concat([...]) leaves into a single flat call, so both
fresh chains (a+b+c+d) and partially-upgraded chains produce one
list.Concat([a,b,c,d]) with no nesting across repeated passes
- upgradeErrorFieldLabel: rewrites unquoted `error` field labels to "error"
to avoid conflict with the CUE 0.14 built-in; precheck uses a tighter
\berror\s*: regex to avoid false positives on identifiers like errorMessage
- EnsureCueVersionCompatibility: single entry point used at render time;
LRU cache with TTL eviction, Prometheus metrics, feature flag
- ParseVersion: regex anchored to reject garbage suffixes (e.g. "1.11foo")
while accepting pre-release+build metadata (e.g. "v1.13.0-alpha.1+dev")
- template.go: call EnsureCueVersionCompatibility for every template area
(main, health, custom status, status detail) with correct DefinitionKind
derived from which definition pointer is non-nil
- validate.go: upgrade policy templates before compiling in
validateNoRequiredParameters
- `vela def upgrade FILE [-o OUTPUT]`: upgrades a single .cue file
- `vela def upgrade FILE --validate [--quiet]`: exit 1 if upgrade needed
- `vela def compat definitions` / `vela def compat applications`: scan
cluster definitions/apps for compat issues; output as table or YAML
- Cyclomatic complexity kept below threshold by extracting scanDefinitions,
scanDefRevisions, buildDefCompatReport, scanApplications, scanAppRevision
as standalone functions with options structs
- revisionNum() helper for numeric vN comparison (avoids lexicographic bugs)
- mergeImports() dedup helper shared by ToCUEString and formatCUEString
- ANSI escape sequences replaced with fatih/color for portability
- goconst: "yaml" → outputFormatYAML named constant throughout
- Component, trait, and policy definition validating handlers: removed
spurious obj.Name argument from fmt.Sprintf in warning messages
- FromCUEString: only prepend importString to the stored template when
imports are non-empty; empty importString ("\n") was causing a leading
newline that made yaml.v3 use |2 block scalar on every generated YAML
- gen_sdk testdata: removed unused imports (vela/op, encoding/base64) from
one_of.cue that were exposed by our importString+templateString change
- e2e test: fix flaky trait-order assertion using ContainElements instead
of index-based equality
Upgraded all built-in .cue files that used deprecated list arithmetic:
- vela-templates/definitions/internal/component/cron-task.cue
- vela-templates/definitions/internal/trait/command.cue
- vela-templates/definitions/internal/trait/container-ports.cue
- vela-templates/definitions/internal/trait/env.cue
- vela-templates/definitions/internal/trait/init-container.cue
Removed unused stdlib imports that caused `def gen-api` to fail:
- vela-templates/definitions/internal/workflowstep/apply-deployment.cue
- vela-templates/definitions/internal/workflowstep/apply-terraform-provider.cue
- vela-templates/definitions/internal/workflowstep/build-push-image.cue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>
* fix(cue/upgrade): address PR review comments
- sync.atomic.Pointer for compatCache to fix data race on reinit
- SummaryVec → HistogramVec for both duration metrics (aggregatable
across HA replicas); buckets tuned to sub-millisecond upgrade path
and millisecond render path respectively
- errorFieldLabelRe: extend to match optional (?) and required (!)
field constraint markers before the colon
- cue-compatibility-cache-size: clamp negative values to 0 (disabled)
with warning log; document 0=disabled in flag help; cache put is
no-op when capacity <= 0
- webhook: replace RequiresUpgrade+EnsureCueVersionCompatibility double
parse with single EnsureCueVersionCompatibility call; use string
comparison to detect upgrade and emit warning
- def compat: log warning when ApplicationRevision fetch fails instead
of silently skipping (partial results are preserved)
- e2e: only delete definitions in DeferCleanup if this test created
them (avoid deleting pre-existing shared resources)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>
* fix(cue/upgrade): address further PR review comments
- EnsureCueVersionCompatibility: return (string, bool) where bool
indicates semantic upgrades were applied (len(applied)>0), not
string inequality — prevents false-positive warnings from
formatting-only normalisation; update all call sites
- webhook handlers (component, trait, policy): switch from
RequiresUpgrade+EnsureCueVersionCompatibility double-call to single
EnsureCueVersionCompatibility call using wasUpgraded bool; remove
now-unused strings imports
- cache: skip eviction goroutine when capacity==0 (disabled); set
compatCacheCancel=nil on disabled path to avoid stale cancel on
next InitCompatibilityCache call
- e2e: replace boolean ownership tracking with createAndTrack helper
that checks pre-existence via Get before Create, eliminating both
the ambiguous-create leak and the boilerplate booleans
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>
* fix: address reviewer comments — cache determinism, e2e ownership race
- cache: store normalised string in compatEntry.upgraded even when no
semantic fixes were applied, so cache-hit and cache-miss paths return
identical output (fixes non-deterministic behaviour flagged in review)
- upgrade: return entry.upgraded on the requiresUpgrade=false cache-hit
path instead of the raw input cueStr
- e2e: replace GET-then-CREATE ownership inference with atomic CREATE-
first pattern; err==nil means we created it (register DeferCleanup),
IsAlreadyExists means it pre-existed (skip cleanup), eliminating the
GET/CREATE race window that could misattribute ownership
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>
---------
Signed-off-by: Brian Kane <briankane1@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
520 lines
15 KiB
Go
520 lines
15 KiB
Go
/*
|
|
Copyright 2024 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"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
oamcommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/pkg/utils/util"
|
|
)
|
|
|
|
// ── parseHints ────────────────────────────────────────────────────────────────
|
|
|
|
func TestParseHints(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reasons []string
|
|
wantHints []CompatibilityHint
|
|
}{
|
|
{
|
|
name: "empty",
|
|
reasons: nil,
|
|
wantHints: nil,
|
|
},
|
|
{
|
|
name: "single hint",
|
|
reasons: []string{"[v1.11] contains deprecated list operators"},
|
|
wantHints: []CompatibilityHint{
|
|
{Introduced: "v1.11", Reason: "contains deprecated list operators"},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple hints",
|
|
reasons: []string{
|
|
"[v1.11] contains deprecated list operators",
|
|
"[v1.11] contains field named 'error'",
|
|
},
|
|
wantHints: []CompatibilityHint{
|
|
{Introduced: "v1.11", Reason: "contains deprecated list operators"},
|
|
{Introduced: "v1.11", Reason: "contains field named 'error'"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := parseHints(tt.reasons)
|
|
if len(got) != len(tt.wantHints) {
|
|
t.Fatalf("parseHints() len = %d, want %d", len(got), len(tt.wantHints))
|
|
}
|
|
for i, h := range got {
|
|
if h != tt.wantHints[i] {
|
|
t.Errorf("hint[%d] = %+v, want %+v", i, h, tt.wantHints[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── parseSelectorMap ──────────────────────────────────────────────────────────
|
|
|
|
func TestParseSelectorMap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want map[string]string
|
|
wantError bool
|
|
}{
|
|
{name: "empty", input: "", want: nil},
|
|
{
|
|
name: "single pair",
|
|
input: "app=foo",
|
|
want: map[string]string{"app": "foo"},
|
|
},
|
|
{
|
|
name: "multiple pairs",
|
|
input: "app=foo,env=prod",
|
|
want: map[string]string{"app": "foo", "env": "prod"},
|
|
},
|
|
{
|
|
name: "value with equals sign",
|
|
input: "key=a=b",
|
|
want: map[string]string{"key": "a=b"},
|
|
},
|
|
{
|
|
name: "missing value separator",
|
|
input: "badkey",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "empty key",
|
|
input: "=value",
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := parseSelectorMap(tt.input)
|
|
if tt.wantError {
|
|
if err == nil {
|
|
t.Errorf("parseSelectorMap(%q) expected error, got nil", tt.input)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("parseSelectorMap(%q) unexpected error: %v", tt.input, err)
|
|
}
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("parseSelectorMap(%q) len = %d, want %d", tt.input, len(got), len(tt.want))
|
|
}
|
|
for k, v := range tt.want {
|
|
if got[k] != v {
|
|
t.Errorf("parseSelectorMap(%q)[%q] = %q, want %q", tt.input, k, got[k], v)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── matchesSelector ───────────────────────────────────────────────────────────
|
|
|
|
func TestMatchesSelector(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
meta map[string]string
|
|
sel map[string]string
|
|
want bool
|
|
}{
|
|
{name: "nil selector matches anything", meta: map[string]string{"a": "b"}, sel: nil, want: true},
|
|
{name: "empty selector matches anything", meta: map[string]string{"a": "b"}, sel: map[string]string{}, want: true},
|
|
{name: "exact match", meta: map[string]string{"app": "foo"}, sel: map[string]string{"app": "foo"}, want: true},
|
|
{name: "subset match", meta: map[string]string{"app": "foo", "env": "prod"}, sel: map[string]string{"app": "foo"}, want: true},
|
|
{name: "value mismatch", meta: map[string]string{"app": "foo"}, sel: map[string]string{"app": "bar"}, want: false},
|
|
{name: "missing key", meta: map[string]string{"app": "foo"}, sel: map[string]string{"env": "prod"}, want: false},
|
|
{name: "nil meta", meta: nil, sel: map[string]string{"app": "foo"}, want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := matchesSelector(tt.meta, tt.sel); got != tt.want {
|
|
t.Errorf("matchesSelector() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── extractCUETemplate ────────────────────────────────────────────────────────
|
|
|
|
func TestExtractCUETemplate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
obj map[string]any
|
|
wantTmpl string
|
|
wantOk bool
|
|
}{
|
|
{
|
|
name: "missing spec",
|
|
obj: map[string]any{},
|
|
wantOk: false,
|
|
},
|
|
{
|
|
name: "missing schematic",
|
|
obj: map[string]any{"spec": map[string]any{}},
|
|
wantOk: false,
|
|
},
|
|
{
|
|
name: "missing cue",
|
|
obj: map[string]any{"spec": map[string]any{
|
|
"schematic": map[string]any{},
|
|
}},
|
|
wantOk: false,
|
|
},
|
|
{
|
|
name: "template present",
|
|
obj: map[string]any{"spec": map[string]any{
|
|
"schematic": map[string]any{
|
|
"cue": map[string]any{
|
|
"template": "output: {}",
|
|
},
|
|
},
|
|
}},
|
|
wantTmpl: "output: {}",
|
|
wantOk: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, ok := extractCUETemplate(tt.obj)
|
|
if ok != tt.wantOk {
|
|
t.Errorf("extractCUETemplate() ok = %v, want %v", ok, tt.wantOk)
|
|
}
|
|
if got != tt.wantTmpl {
|
|
t.Errorf("extractCUETemplate() = %q, want %q", got, tt.wantTmpl)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── defNameFromRevision ───────────────────────────────────────────────────────
|
|
|
|
func TestDefNameFromRevision(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
drName string
|
|
drType oamcommon.DefinitionType
|
|
labels map[string]string
|
|
wantName string
|
|
}{
|
|
{
|
|
name: "name from label",
|
|
drName: "worker-v3",
|
|
drType: oamcommon.ComponentType,
|
|
labels: map[string]string{"componentdefinition.oam.dev/name": "worker"},
|
|
wantName: "worker",
|
|
},
|
|
{
|
|
name: "fallback strip -vN",
|
|
drName: "my-trait-v12",
|
|
drType: oamcommon.TraitType,
|
|
labels: map[string]string{},
|
|
wantName: "my-trait",
|
|
},
|
|
{
|
|
name: "fallback no -vN suffix",
|
|
drName: "standalone",
|
|
drType: oamcommon.ComponentType,
|
|
labels: map[string]string{},
|
|
wantName: "standalone",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dr := v1beta1.DefinitionRevision{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: tt.drName,
|
|
Labels: tt.labels,
|
|
},
|
|
}
|
|
dr.Spec.DefinitionType = tt.drType
|
|
got := defNameFromRevision(dr)
|
|
if got != tt.wantName {
|
|
t.Errorf("defNameFromRevision() = %q, want %q", got, tt.wantName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── extractDefRevTemplate ─────────────────────────────────────────────────────
|
|
|
|
func TestExtractDefRevTemplate(t *testing.T) {
|
|
tmpl := "output: {}"
|
|
|
|
tests := []struct {
|
|
name string
|
|
dr v1beta1.DefinitionRevision
|
|
want string
|
|
}{
|
|
{
|
|
name: "component",
|
|
dr: v1beta1.DefinitionRevision{Spec: v1beta1.DefinitionRevisionSpec{
|
|
DefinitionType: oamcommon.ComponentType,
|
|
ComponentDefinition: v1beta1.ComponentDefinition{Spec: v1beta1.ComponentDefinitionSpec{
|
|
Schematic: &oamcommon.Schematic{CUE: &oamcommon.CUE{Template: tmpl}},
|
|
}},
|
|
}},
|
|
want: tmpl,
|
|
},
|
|
{
|
|
name: "trait",
|
|
dr: v1beta1.DefinitionRevision{Spec: v1beta1.DefinitionRevisionSpec{
|
|
DefinitionType: oamcommon.TraitType,
|
|
TraitDefinition: v1beta1.TraitDefinition{Spec: v1beta1.TraitDefinitionSpec{
|
|
Schematic: &oamcommon.Schematic{CUE: &oamcommon.CUE{Template: tmpl}},
|
|
}},
|
|
}},
|
|
want: tmpl,
|
|
},
|
|
{
|
|
name: "no schematic",
|
|
dr: v1beta1.DefinitionRevision{Spec: v1beta1.DefinitionRevisionSpec{
|
|
DefinitionType: oamcommon.ComponentType,
|
|
}},
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := extractDefRevTemplate(tt.dr)
|
|
if got != tt.want {
|
|
t.Errorf("extractDefRevTemplate() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── printDefCompatReport ──────────────────────────────────────────────────────
|
|
|
|
func TestPrintDefCompatReport(t *testing.T) {
|
|
report := DefCompatReport{
|
|
Total: 1,
|
|
Definitions: []DefCompatEntry{
|
|
{
|
|
Name: "worker",
|
|
Kind: "ComponentDefinition",
|
|
Namespace: "vela-system",
|
|
Issues: []DefIssueEntry{
|
|
{
|
|
Introduced: "v1.11",
|
|
Reason: "contains deprecated list operators",
|
|
Revisions: []string{"v1", "v2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("table output", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
if err := printDefCompatReport(streams, report, "table"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "worker") {
|
|
t.Error("expected definition name 'worker' in output")
|
|
}
|
|
if !strings.Contains(out, "v1.11") {
|
|
t.Error("expected introduced version 'v1.11' in output")
|
|
}
|
|
if !strings.Contains(out, "v1, v2") {
|
|
t.Error("expected revisions 'v1, v2' in output")
|
|
}
|
|
})
|
|
|
|
t.Run("empty table output", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
empty := DefCompatReport{Total: 0, Definitions: []DefCompatEntry{}}
|
|
if err := printDefCompatReport(streams, empty, "table"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "compatible") {
|
|
t.Error("expected 'compatible' message for empty report")
|
|
}
|
|
})
|
|
|
|
t.Run("json output", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
if err := printDefCompatReport(streams, report, "json"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
var decoded DefCompatReport
|
|
if err := json.Unmarshal(buf.Bytes(), &decoded); err != nil {
|
|
t.Fatalf("json output not valid: %v", err)
|
|
}
|
|
if decoded.Total != 1 {
|
|
t.Errorf("json total = %d, want 1", decoded.Total)
|
|
}
|
|
if decoded.Definitions[0].Issues[0].Introduced != "v1.11" {
|
|
t.Errorf("json introduced = %q, want v1.11", decoded.Definitions[0].Issues[0].Introduced)
|
|
}
|
|
})
|
|
|
|
t.Run("yaml output", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
if err := printDefCompatReport(streams, report, "yaml"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "introduced: v1.11") {
|
|
t.Errorf("yaml output missing introduced field, got:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "- v1") {
|
|
t.Errorf("yaml output missing revision v1, got:\n%s", out)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ── printAppCompatReport ──────────────────────────────────────────────────────
|
|
|
|
func TestPrintAppCompatReport(t *testing.T) {
|
|
report := AppCompatReport{
|
|
Total: 1,
|
|
Applications: []AppCompatEntry{
|
|
{
|
|
Application: "my-app",
|
|
Namespace: "default",
|
|
Revision: "my-app-v1",
|
|
Issues: []AppIssueEntry{
|
|
{
|
|
Introduced: "v1.11",
|
|
Reason: "contains deprecated list operators",
|
|
Components: []string{"worker"},
|
|
Traits: []string{"scaler"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("table output", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
if err := printAppCompatReport(streams, report, "table"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "my-app") {
|
|
t.Error("expected app name in output")
|
|
}
|
|
if !strings.Contains(out, "my-app-v1") {
|
|
t.Error("expected revision in output")
|
|
}
|
|
if !strings.Contains(out, "components: worker") {
|
|
t.Error("expected components in output")
|
|
}
|
|
if !strings.Contains(out, "traits: scaler") {
|
|
t.Error("expected traits in output")
|
|
}
|
|
})
|
|
|
|
t.Run("empty table output", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
empty := AppCompatReport{Total: 0, Applications: []AppCompatEntry{}}
|
|
if err := printAppCompatReport(streams, empty, "table"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "compatible") {
|
|
t.Error("expected 'compatible' message for empty report")
|
|
}
|
|
})
|
|
|
|
t.Run("json output", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
if err := printAppCompatReport(streams, report, "json"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
var decoded AppCompatReport
|
|
if err := json.Unmarshal(buf.Bytes(), &decoded); err != nil {
|
|
t.Fatalf("json output not valid: %v", err)
|
|
}
|
|
if decoded.Total != 1 {
|
|
t.Errorf("json total = %d, want 1", decoded.Total)
|
|
}
|
|
issue := decoded.Applications[0].Issues[0]
|
|
if issue.Introduced != "v1.11" {
|
|
t.Errorf("json introduced = %q, want v1.11", issue.Introduced)
|
|
}
|
|
if len(issue.Components) != 1 || issue.Components[0] != "worker" {
|
|
t.Errorf("json components = %v, want [worker]", issue.Components)
|
|
}
|
|
})
|
|
|
|
t.Run("yaml output", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
if err := printAppCompatReport(streams, report, "yaml"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "application: my-app") {
|
|
t.Errorf("yaml output missing application field, got:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "components:") {
|
|
t.Errorf("yaml output missing components field, got:\n%s", out)
|
|
}
|
|
})
|
|
|
|
t.Run("omitempty hides empty kind fields", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
streams := util.IOStreams{Out: &buf}
|
|
noTraits := AppCompatReport{
|
|
Total: 1,
|
|
Applications: []AppCompatEntry{{
|
|
Application: "app",
|
|
Namespace: "default",
|
|
Revision: "app-v1",
|
|
Issues: []AppIssueEntry{{
|
|
Introduced: "v1.11",
|
|
Reason: "some issue",
|
|
Components: []string{"worker"},
|
|
}},
|
|
}},
|
|
}
|
|
if err := printAppCompatReport(streams, noTraits, "json"); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if strings.Contains(buf.String(), "traits") {
|
|
t.Error("expected traits to be omitted when empty")
|
|
}
|
|
})
|
|
}
|