diff --git a/internal/model1/fields.go b/internal/model1/fields.go index 9242d54f3b..3ebd4b7327 100644 --- a/internal/model1/fields.go +++ b/internal/model1/fields.go @@ -3,18 +3,22 @@ package model1 -import "reflect" +import ( + "reflect" +) // Fields represents a collection of row fields. type Fields []string // Customize returns a subset of fields. -func (f Fields) Customize(cols []int, out Fields) { +func (f Fields) Customize(cols []int, out Fields, extractionInfoBag ExtractionInfoBag) { for i, c := range cols { + if c < 0 { - out[i] = NAValue + out[i] = getValueOfInvalidColumn(f, i, extractionInfoBag) continue } + if c < len(f) { out[i] = f[c] } @@ -39,3 +43,19 @@ func (f Fields) Clone() Fields { return cp } + +func getValueOfInvalidColumn(f Fields, i int, extractionInfoBag ExtractionInfoBag) string { + + extractionInfo, ok := extractionInfoBag[i] + + // If the extractionInfo is existed in extractionInfoBag, + // meaning this column has to retrieve the actual value from other field. + // For example: `LABELS[kubernetes.io/hostname]` needs to extract the value from column `LABELS` + if ok { + idxInFields := extractionInfo.IdxInFields + escapedKey := escapeDots(extractionInfo.Key) + return extractValueFromField(escapedKey, f[idxInFields]) + } + + return NAValue +} diff --git a/internal/model1/header.go b/internal/model1/header.go index d4f6d4c850..266f419966 100644 --- a/internal/model1/header.go +++ b/internal/model1/header.go @@ -5,12 +5,23 @@ package model1 import ( "reflect" + "regexp" "github.com/rs/zerolog/log" ) const ageCol = "AGE" +// ExtractionInfo stores data for a field to extract value from another field +type ExtractionInfo struct { + IdxInFields int + HeaderName string + Key string +} + +// ExtractionInfoBag store ExtractionInfo by using the index of the column +type ExtractionInfoBag map[int]ExtractionInfo + // HeaderColumn represent a table header. type HeaderColumn struct { Name string @@ -64,18 +75,52 @@ func (h Header) Labelize(cols []int, labelCol int, rr *RowEvents) Header { } // MapIndices returns a collection of mapped column indices based of the requested columns. -func (h Header) MapIndices(cols []string, wide bool) []int { +// And the extraction information used to extract a value for a field from another field. +func (h Header) MapIndices(cols []string, wide bool) ([]int, ExtractionInfoBag) { ii := make([]int, 0, len(cols)) cc := make(map[int]struct{}, len(cols)) + + eib := make(ExtractionInfoBag) + regex, _ := regexp.Compile(`^(?.*)\[(?.*)\]$`) + for _, col := range cols { idx, ok := h.IndexOf(col, true) - if !ok { - log.Warn().Msgf("Column %q not found on resource", col) - } + ii, cc[idx] = append(ii, idx), struct{}{} + + // Continue to next iteration if the column is found + if ok { + continue + } + + log.Warn().Msgf("Column %q not found on resource", col) + + // Continue to next iteration ff the column doesn't match the regex + if !regex.MatchString(col) { + log.Warn().Msgf("Column %q doesn't match regex pattern", col) + continue + } + + // Check if the column matches the pattern + // For example: `LABELS[beta.kubernetes.io/os]` will match + matches := regex.FindStringSubmatch(col) + headerName := matches[1] + key := matches[2] + + // now only support LABELS + if headerName != "LABELS" { + log.Warn().Msgf("Custom Column %q is not supported", col) + continue + } + + log.Warn().Msgf("Custom column %q is extracting value with header name: %q and key: %q", col, headerName, key) + currentIdx := len(ii) - 1 + idxInFields, _ := h.IndexOf(headerName, true) + eib[currentIdx] = ExtractionInfo{idxInFields, headerName, key} } + if !wide { - return ii + return ii, eib } for i := range h { @@ -84,7 +129,7 @@ func (h Header) MapIndices(cols []string, wide bool) []int { } ii = append(ii, i) } - return ii + return ii, eib } // Customize builds a header from custom col definitions. diff --git a/internal/model1/header_test.go b/internal/model1/header_test.go index 3d1d62f321..908a62a21f 100644 --- a/internal/model1/header_test.go +++ b/internal/model1/header_test.go @@ -16,34 +16,49 @@ func TestHeaderMapIndices(t *testing.T) { cols []string wide bool e []int + eib model1.ExtractionInfoBag }{ "all": { h1: makeHeader(), cols: []string{"A", "B", "C"}, e: []int{0, 1, 2}, + eib: model1.ExtractionInfoBag{}, }, "reverse": { h1: makeHeader(), cols: []string{"C", "B", "A"}, e: []int{2, 1, 0}, + eib: model1.ExtractionInfoBag{}, }, "missing": { h1: makeHeader(), cols: []string{"Duh", "B", "A"}, e: []int{-1, 1, 0}, + eib: model1.ExtractionInfoBag{}, }, "skip": { h1: makeHeader(), cols: []string{"C", "A"}, e: []int{2, 0}, + eib: model1.ExtractionInfoBag{}, + }, + "labels": { + h1: makeHeader(), + cols: []string{"A", "LABELS[kubernetes.io/hostname]", "B", "LABELS[topology.kubernetes.io/region]"}, + e: []int{0, -1, 1, -1}, + eib: model1.ExtractionInfoBag{ + 1: model1.ExtractionInfo{3, "LABELS", "kubernetes.io/hostname"}, + 3: model1.ExtractionInfo{3, "LABELS", "topology.kubernetes.io/region"}, + }, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - ii := u.h1.MapIndices(u.cols, u.wide) + ii, eib := u.h1.MapIndices(u.cols, u.wide) assert.Equal(t, u.e, ii) + assert.Equal(t, u.eib, eib) }) } } @@ -264,11 +279,11 @@ func TestHeaderColumns(t *testing.T) { }, "regular": { h: makeHeader(), - e: []string{"A", "C"}, + e: []string{"A", "C", "LABELS"}, }, "wide": { h: makeHeader(), - e: []string{"A", "B", "C"}, + e: []string{"A", "B", "C", "LABELS"}, wide: true, }, } @@ -314,5 +329,6 @@ func makeHeader() model1.Header { model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B", Wide: true}, model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "LABELS"}, } } diff --git a/internal/model1/helpers.go b/internal/model1/helpers.go index fb0ac597a7..e16f205cdf 100644 --- a/internal/model1/helpers.go +++ b/internal/model1/helpers.go @@ -6,6 +6,7 @@ package model1 import ( "fmt" "math" + "regexp" "sort" "strings" @@ -164,3 +165,23 @@ func lessNumber(s1, s2 string) bool { return sortorder.NaturalLess(v1, v2) } + +// Escape dots from the string +// For example: `kubernetes.io/hostname` needs to be escaped to `kubernetes\.io/hostname` +func escapeDots(s string) string { + return strings.ReplaceAll(s, ".", "\\.") +} + +func extractValueFromField(key string, field string) string { + // Extract the value by using regex + pattern := fmt.Sprintf(`%s=([^ ]+)`, key) + regex := regexp.MustCompile(pattern) + + // Find the value in the field that store original values + matches := regex.FindStringSubmatch(field) + if len(matches) > 1 { + return matches[1] + } + + return "" +} diff --git a/internal/model1/helpers_test.go b/internal/model1/helpers_test.go index 3e6a04272f..84cb30c320 100644 --- a/internal/model1/helpers_test.go +++ b/internal/model1/helpers_test.go @@ -87,3 +87,29 @@ func BenchmarkDurationToSecond(b *testing.B) { durationToSeconds(t) } } + +func TestEscapeDots(t *testing.T) { + var s string + + s = escapeDots("kubernetes.io/hostname") + assert.Equal(t, "kubernetes\\.io/hostname", s) + + s = escapeDots("kubernetes-io/hostname") + assert.Equal(t, "kubernetes-io/hostname", s) +} + +func TestExtractValueFromFields(t *testing.T) { + k := escapeDots("kubernetes.io/hostname") + f := "kubernetes.io/arch=amd64 kubernetes.io/hostname=a-b-c-d kubernetes.io/os=linux" + + var s string + + s = extractValueFromField(k, f) + assert.Equal(t, "a-b-c-d", s) + + s = extractValueFromField(k, "kubernetes.io/hostname=e-f-g-h "+f) + assert.Equal(t, "e-f-g-h", s) + + s = extractValueFromField("random-key", f) + assert.Equal(t, "", s) +} diff --git a/internal/model1/row.go b/internal/model1/row.go index bda6a86065..711f96df48 100644 --- a/internal/model1/row.go +++ b/internal/model1/row.go @@ -29,9 +29,9 @@ func (r Row) Labelize(cols []int, labelCol int, labels []string) Row { } // Customize returns a row subset based on given col indices. -func (r Row) Customize(cols []int) Row { +func (r Row) Customize(cols []int, extractionInfoBag ExtractionInfoBag) Row { out := NewRow(len(cols)) - r.Fields.Customize(cols, out.Fields) + r.Fields.Customize(cols, out.Fields, extractionInfoBag) out.ID = r.ID return out diff --git a/internal/model1/row_event.go b/internal/model1/row_event.go index c475f38d38..eb2807cbae 100644 --- a/internal/model1/row_event.go +++ b/internal/model1/row_event.go @@ -47,7 +47,7 @@ func (r RowEvent) Clone() RowEvent { } // Customize returns a new subset based on the given column indices. -func (r RowEvent) Customize(cols []int) RowEvent { +func (r RowEvent) Customize(cols []int, extractionInfoBag ExtractionInfoBag) RowEvent { delta := r.Deltas if !r.Deltas.IsBlank() { delta = make(DeltaRow, len(cols)) @@ -57,7 +57,7 @@ func (r RowEvent) Customize(cols []int) RowEvent { return RowEvent{ Kind: r.Kind, Deltas: delta, - Row: r.Row.Customize(cols), + Row: r.Row.Customize(cols, extractionInfoBag), } } @@ -158,10 +158,10 @@ func (r *RowEvents) Labelize(cols []int, labelCol int, labels []string) *RowEven } // Customize returns custom row events based on columns layout. -func (r *RowEvents) Customize(cols []int) *RowEvents { +func (r *RowEvents) Customize(cols []int, extractionInfoBag ExtractionInfoBag) *RowEvents { ee := make([]RowEvent, 0, len(cols)) for _, re := range r.events { - ee = append(ee, re.Customize(cols)) + ee = append(ee, re.Customize(cols, extractionInfoBag)) } return NewRowEventsWithEvts(ee...) diff --git a/internal/model1/row_event_test.go b/internal/model1/row_event_test.go index 9ca9322980..e79e5d8f28 100644 --- a/internal/model1/row_event_test.go +++ b/internal/model1/row_event_test.go @@ -15,6 +15,7 @@ func TestRowEventCustomize(t *testing.T) { uu := map[string]struct { re1, e model1.RowEvent cols []int + eib model1.ExtractionInfoBag }{ "empty": { re1: model1.RowEvent{ @@ -101,7 +102,7 @@ func TestRowEventCustomize(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re1.Customize(u.cols)) + assert.Equal(t, u.e, u.re1.Customize(u.cols, u.eib)) }) } } @@ -297,6 +298,7 @@ func TestRowEventsCustomize(t *testing.T) { uu := map[string]struct { re, e *model1.RowEvents cols []int + eib model1.ExtractionInfoBag }{ "same": { re: model1.NewRowEventsWithEvts( @@ -355,7 +357,7 @@ func TestRowEventsCustomize(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re.Customize(u.cols)) + assert.Equal(t, u.e, u.re.Customize(u.cols, u.eib)) }) } } diff --git a/internal/model1/row_test.go b/internal/model1/row_test.go index d206083165..9369cadc69 100644 --- a/internal/model1/row_test.go +++ b/internal/model1/row_test.go @@ -16,10 +16,11 @@ import ( func BenchmarkRowCustomize(b *testing.B) { row := model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}} cols := []int{0, 1, 2} + eib := model1.ExtractionInfoBag{} b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - _ = row.Customize(cols) + _ = row.Customize(cols, eib) } } @@ -28,6 +29,7 @@ func TestFieldCustomize(t *testing.T) { fields model1.Fields cols []int e model1.Fields + eib model1.ExtractionInfoBag }{ "empty": { fields: model1.Fields{}, @@ -49,13 +51,22 @@ func TestFieldCustomize(t *testing.T) { cols: []int{10, 0}, e: model1.Fields{"", "f1"}, }, + "labels": { + fields: model1.Fields{"f1", "f2", "f3", "kubernetes.io/os=linux kubernetes.io/hostname=node1"}, + cols: []int{0, -1, -1}, + e: model1.Fields{"f1", "node1", "linux"}, + eib: model1.ExtractionInfoBag{ + 1: model1.ExtractionInfo{3, "LABELS", "kubernetes.io/hostname"}, + 2: model1.ExtractionInfo{3, "LABELS", "kubernetes.io/os"}, + }, + }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { ff := make(model1.Fields, len(u.cols)) - u.fields.Customize(u.cols, ff) + u.fields.Customize(u.cols, ff, u.eib) assert.Equal(t, u.e, ff) }) } @@ -74,6 +85,7 @@ func TestRowlabelize(t *testing.T) { row model1.Row cols []int e model1.Row + eib model1.ExtractionInfoBag }{ "empty": { row: model1.Row{}, @@ -95,7 +107,7 @@ func TestRowlabelize(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - row := u.row.Customize(u.cols) + row := u.row.Customize(u.cols, u.eib) assert.Equal(t, u.e, row) }) } @@ -106,6 +118,7 @@ func TestRowCustomize(t *testing.T) { row model1.Row cols []int e model1.Row + eib model1.ExtractionInfoBag }{ "empty": { row: model1.Row{}, @@ -127,7 +140,7 @@ func TestRowCustomize(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - row := u.row.Customize(u.cols) + row := u.row.Customize(u.cols, u.eib) assert.Equal(t, u.e, row) }) } diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go index 702a86edc4..da345a95b6 100644 --- a/internal/model1/table_data.go +++ b/internal/model1/table_data.go @@ -340,8 +340,8 @@ func (t *TableData) Customize(vs *config.ViewSetting, sc SortColumn, manual, wid namespace: t.namespace, header: t.header.Customize(cols, wide), } - ids := t.header.MapIndices(cols, wide) - cdata.rowEvents = t.rowEvents.Customize(ids) + ids, extractionInfoBag := t.header.MapIndices(cols, wide) + cdata.rowEvents = t.rowEvents.Customize(ids, extractionInfoBag) if manual || vs == nil { return &cdata, sc }