Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions service/values_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ const (
saveFilePerm = 0o644
)

// TypeNull is the Type-tag string the flattener emits for `key: ~` /
// `key: null` leaves. Exported so UI-layer code can compare against it
// (detecting nullified leaves on file load, serializing nullified keys
// back to `~`, rendering the "null" pill label) without re-declaring the
// literal in every consumer package.
const TypeNull = "null"

const (
typeString = "string"
typeBool = "bool"
typeNumber = "number"
typeNull = "null"
typeUnknown = "unknown"
typeMap = "map"
typeList = "list"
Expand Down Expand Up @@ -817,7 +823,7 @@ func flattenValues(source string, vals map[string]any) *domain.ValuesFile {
// pass that literal string through to the YAML encoder, replacing
// the original `null` / `~` with the four-character "<nil>".
// Empty-string Value with Type=null round-trips through
// convertValue's typeNull case back to a real nil.
// convertValue's TypeNull case back to a real nil.
if typedVal == nil {
value = ""
}
Expand Down Expand Up @@ -882,7 +888,7 @@ func inferType(v any) string {
case int, int64, float64:
return typeNumber
case nil:
return typeNull
return TypeNull
default:
return typeUnknown
}
Expand Down
8 changes: 8 additions & 0 deletions service/values_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ func (e FlatValueEntry) IsFocusable() bool {
return !e.IsSection() && !e.IsComment()
}

// IsNullLeaf reports whether this entry is an explicit-null scalar (the
// flattener emits Type="null" for `key: ~` or `key: null` in the source).
// Used by the UI layer to detect overlay-side nullifications without
// stringly-comparing the package-private type tag.
func (e FlatValueEntry) IsNullLeaf() bool {
return e.Kind == EntryKindLeaf && e.Type == TypeNull
}

// IsEmptyContainer reports whether this entry is a container key with no
// children — flattenValues emits "{}" for an empty mapping and "[]" for an
// empty sequence, distinguishable from a populated-container section header
Expand Down
6 changes: 3 additions & 3 deletions service/yaml_serialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,20 +312,20 @@ func PatchNodeTree(root *yaml.Node, entries []OverrideEntry, indent int, docs Do

// scalarsEquivalent reports whether the tree's effective scalar text and the
// override entry's value mean the same thing for round-trip purposes. For
// typeNull the three YAML representations (`null`, `~`, blank `""`) all encode
// TypeNull the three YAML representations (`null`, `~`, blank `""`) all encode
// the same value, so treating them as equal here keeps PatchNodeTree's unedited
// branch from rewriting (and re-styling) a null leaf the user didn't touch.
// All other types fall back to literal string equality.
func scalarsEquivalent(treeValue, entryValue, entryType string) bool {
if entryType == typeNull && isYAMLNullLiteral(treeValue) && isYAMLNullLiteral(entryValue) {
if entryType == TypeNull && isYAMLNullLiteral(treeValue) && isYAMLNullLiteral(entryValue) {
return true
}

return treeValue == entryValue
}

func isYAMLNullLiteral(s string) bool {
return s == "" || s == "~" || s == typeNull
return s == "" || s == "~" || s == TypeNull
}

// applyDocFoots writes each (leafKey -> footText) pair as the FootComment of
Expand Down
4 changes: 2 additions & 2 deletions service/yaml_serialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,8 @@ keep: x

// Mimic flattenValues' nil handling — Value="" for both null entries.
entries := []OverrideEntry{
{Key: "usePasswordFiles", Value: "", Type: typeNull},
{Key: "explicitNull", Value: "", Type: typeNull},
{Key: "usePasswordFiles", Value: "", Type: TypeNull},
{Key: "explicitNull", Value: "", Type: TypeNull},
{Key: keepKey, Value: "x", Type: typeString},
}

Expand Down
8 changes: 4 additions & 4 deletions service/yaml_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ func descendSequence(current *yaml.Node, seg, next keySegment) (*yaml.Node, erro

for len(current.Content) <= seg.index {
current.Content = append(current.Content, &yaml.Node{
Kind: yaml.ScalarNode, Tag: yamlTagNull, Value: typeNull,
Kind: yaml.ScalarNode, Tag: yamlTagNull, Value: TypeNull,
})
}

Expand Down Expand Up @@ -711,7 +711,7 @@ func setLeaf(current *yaml.Node, seg keySegment, value any) error {

for len(current.Content) <= seg.index {
current.Content = append(current.Content, &yaml.Node{
Kind: yaml.ScalarNode, Tag: yamlTagNull, Value: typeNull,
Kind: yaml.ScalarNode, Tag: yamlTagNull, Value: TypeNull,
})
}

Expand Down Expand Up @@ -791,8 +791,8 @@ func convertValue(value, typ string) any {
return b
}

case typeNull:
if value == typeNull || value == "~" || value == "" {
case TypeNull:
if value == TypeNull || value == "~" || value == "" {
return nil
}
}
Expand Down
36 changes: 36 additions & 0 deletions ui/page/values_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ func (vc *ValuesController) Callbacks() ValuesPageCallbacks {
OnUnlockCell: vc.onUnlockCell,
OnAnchorRename: vc.onAnchorRename,
OnAnchorDelete: vc.onAnchorDelete,
OnCellNullify: vc.onCellNullify,
}
}

Expand Down Expand Up @@ -629,9 +630,15 @@ func (vc *ValuesController) populateColumnOverrides(colIdx int) {
return
}

// Fresh load: NullifiedKeys from a previous file are no longer
// applicable. The disk-authoritative pass below re-seeds the map
// from any `key: ~` entries it finds.
col.NullifiedKeys = nil

// Build lookup from custom values (flat key match).
customMap := make(map[string]string, len(col.CustomValues.Entries))
commentMap := make(map[string]string, len(col.CustomValues.Entries))
nullKeys := make(map[string]bool)

for _, e := range col.CustomValues.Entries {
// Include scalar-leaf entries unconditionally — even when Value is
Expand All @@ -648,6 +655,16 @@ func (vc *ValuesController) populateColumnOverrides(colIdx int) {
if e.Comment != "" {
commentMap[e.Key] = e.Comment
}

// Record explicit-null leaves so the UI can render them as the
// "null" pill instead of plain "~" / empty editor text. An
// overlay that nulls a key the chart declares as a mapping
// (`section: ~`) is flattened as a leaf with Type="null", so
// this catches both leaf-level and section-level nullifications
// in a single pass.
if e.IsNullLeaf() {
nullKeys[e.Key] = true
}
}

col.EnsureEditors(len(vc.State.Entries))
Expand Down Expand Up @@ -694,6 +711,25 @@ func (vc *ValuesController) populateColumnOverrides(colIdx int) {
}

col.RebuildOverrideFlags()

// Seed NullifiedKeys for every key the overlay marked as explicit
// null on disk, and force their override flag so the row tint
// reflects the user's intent. The flag wouldn't survive
// RebuildOverrideFlags on its own — null leaves carry empty editor
// text, which RebuildOverrideFlags treats as "no override".
if len(nullKeys) > 0 {
for i, entry := range vc.State.Entries {
if i >= len(col.OverrideEditors) {
break
}

if nullKeys[entry.Key] {
col.MarkNullified(entry.Key)
col.MarkOverride(i, true)
}
}
}

col.DrainPendingChanges = true
col.ValuesModified = false
}
Expand Down
Loading
Loading