From 7944b4b2b0a591c7d3a6d4b2add713dce25eb2d7 Mon Sep 17 00:00:00 2001 From: Nalin Dahyabhai Date: Mon, 23 Jun 2025 15:11:06 -0400 Subject: [PATCH 1/2] Use containers/common's formats package instead of our own Use the containers/common "formats" package, which started off as a copy of our own and is still mostly the same, instead of our own. Signed-off-by: Nalin Dahyabhai --- cmd/buildah/containers.go | 2 +- cmd/buildah/images.go | 2 +- go.mod | 2 +- pkg/formats/doc.go | 2 + pkg/formats/formats.go | 154 +--------------- pkg/formats/formats_test.go | 45 ----- pkg/formats/templates.go | 71 +------- .../containers/common/pkg/formats/formats.go | 166 ++++++++++++++++++ .../common/pkg/formats/templates.go | 78 ++++++++ vendor/modules.txt | 1 + 10 files changed, 263 insertions(+), 260 deletions(-) create mode 100644 pkg/formats/doc.go delete mode 100644 pkg/formats/formats_test.go create mode 100644 vendor/github.com/containers/common/pkg/formats/formats.go create mode 100644 vendor/github.com/containers/common/pkg/formats/templates.go diff --git a/cmd/buildah/containers.go b/cmd/buildah/containers.go index bcf61d599..06fdda2e0 100644 --- a/cmd/buildah/containers.go +++ b/cmd/buildah/containers.go @@ -8,8 +8,8 @@ import ( "github.com/containers/buildah" "github.com/containers/buildah/define" - "github.com/containers/buildah/pkg/formats" "github.com/containers/buildah/util" + "github.com/containers/common/pkg/formats" "github.com/containers/storage" "github.com/spf13/cobra" ) diff --git a/cmd/buildah/images.go b/cmd/buildah/images.go index b254d4615..9ba911060 100644 --- a/cmd/buildah/images.go +++ b/cmd/buildah/images.go @@ -10,9 +10,9 @@ import ( "time" buildahcli "github.com/containers/buildah/pkg/cli" - "github.com/containers/buildah/pkg/formats" "github.com/containers/buildah/pkg/parse" "github.com/containers/common/libimage" + "github.com/containers/common/pkg/formats" "github.com/docker/go-units" "github.com/spf13/cobra" ) diff --git a/go.mod b/go.mod index 256302737..f57b9546f 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,6 @@ require ( golang.org/x/sync v0.15.0 golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 - sigs.k8s.io/yaml v1.4.0 tags.cncf.io/container-device-interface v1.0.1 ) @@ -156,5 +155,6 @@ require ( google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog v1.0.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) diff --git a/pkg/formats/doc.go b/pkg/formats/doc.go new file mode 100644 index 000000000..16638498b --- /dev/null +++ b/pkg/formats/doc.go @@ -0,0 +1,2 @@ +// The formats package is an API compatibility wrapper which merely calls into github.com/containers/common/pkg/formats. Newer code should instead call that package directly. +package formats diff --git a/pkg/formats/formats.go b/pkg/formats/formats.go index 9a786578c..4bcb6743e 100644 --- a/pkg/formats/formats.go +++ b/pkg/formats/formats.go @@ -1,166 +1,30 @@ package formats import ( - "bytes" - "encoding/json" - "fmt" - "io" - "os" - "strings" - "text/tabwriter" - "text/template" - - "golang.org/x/term" - "sigs.k8s.io/yaml" + "github.com/containers/common/pkg/formats" ) const ( // JSONString const to save on duplicate variable names - JSONString = "json" + JSONString = formats.JSONString // IDString const to save on duplicates for Go templates - IDString = "{{.ID}}" - - parsingErrorStr = "Template parsing error" + IDString = formats.IDString ) // Writer interface for outputs -type Writer interface { - Out() error -} +type Writer = formats.Writer // JSONStructArray for JSON output -type JSONStructArray struct { - Output []any -} +type JSONStructArray = formats.JSONStructArray // StdoutTemplateArray for Go template output -type StdoutTemplateArray struct { - Output []any - Template string - Fields map[string]string -} +type StdoutTemplateArray = formats.StdoutTemplateArray // JSONStruct for JSON output -type JSONStruct struct { - Output any -} +type JSONStruct = formats.JSONStruct // StdoutTemplate for Go template output -type StdoutTemplate struct { - Output any - Template string - Fields map[string]string -} +type StdoutTemplate = formats.StdoutTemplate // YAMLStruct for YAML output -type YAMLStruct struct { - Output any -} - -func setJSONFormatEncoder(isTerminal bool, w io.Writer) *json.Encoder { - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - if isTerminal { - enc.SetEscapeHTML(false) - } - return enc -} - -// Out method for JSON Arrays -func (j JSONStructArray) Out() error { - buf := bytes.NewBuffer(nil) - enc := setJSONFormatEncoder(term.IsTerminal(int(os.Stdout.Fd())), buf) - if err := enc.Encode(j.Output); err != nil { - return err - } - data := buf.Bytes() - - // JSON returns a byte array with a literal null [110 117 108 108] in it - // if it is passed empty data. We used bytes.Compare to see if that is - // the case. - if diff := bytes.Compare(data, []byte("null")); diff == 0 { - data = []byte("[]") - } - - // If the we did get NULL back, we should spit out {} which is - // at least valid JSON for the consumer. - fmt.Printf("%s", data) - humanNewLine() - return nil -} - -// Out method for Go templates -func (t StdoutTemplateArray) Out() error { - w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) - if strings.HasPrefix(t.Template, "table") { - // replace any spaces with tabs in template so that tabwriter can align it - t.Template = strings.ReplaceAll(strings.TrimSpace(t.Template[5:]), " ", "\t") - headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template) - if err != nil { - return fmt.Errorf("%v: %w", parsingErrorStr, err) - } - err = headerTmpl.Execute(w, t.Fields) - if err != nil { - return err - } - fmt.Fprintln(w, "") - } - t.Template = strings.ReplaceAll(t.Template, " ", "\t") - tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template) - if err != nil { - return fmt.Errorf("%v: %w", parsingErrorStr, err) - } - for _, raw := range t.Output { - basicTmpl := tmpl.Funcs(basicFunctions) - if err := basicTmpl.Execute(w, raw); err != nil { - return fmt.Errorf("%v: %w", parsingErrorStr, err) - } - fmt.Fprintln(w, "") - } - return w.Flush() -} - -// Out method for JSON struct -func (j JSONStruct) Out() error { - data, err := json.MarshalIndent(j.Output, "", " ") - if err != nil { - return err - } - fmt.Printf("%s", data) - humanNewLine() - return nil -} - -// Out method for Go templates -func (t StdoutTemplate) Out() error { - tmpl, err := template.New("image").Parse(t.Template) - if err != nil { - return fmt.Errorf("template parsing error: %w", err) - } - err = tmpl.Execute(os.Stdout, t.Output) - if err != nil { - return err - } - humanNewLine() - return nil -} - -// Out method for YAML -func (y YAMLStruct) Out() error { - var buf []byte - var err error - buf, err = yaml.Marshal(y.Output) - if err != nil { - return err - } - fmt.Printf("%s", string(buf)) - humanNewLine() - return nil -} - -// humanNewLine prints a new line at the end of the output only if stdout is the terminal -func humanNewLine() { - if term.IsTerminal(int(os.Stdout.Fd())) { - fmt.Println() - } -} +type YAMLStruct = formats.YAMLStruct diff --git a/pkg/formats/formats_test.go b/pkg/formats/formats_test.go deleted file mode 100644 index 9aaccfe68..000000000 --- a/pkg/formats/formats_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package formats - -import ( - "bytes" - "strings" - "testing" -) - -type ImageData struct { - Author string `json:"Author"` -} - -func TestSetJSONFormatEncoder(t *testing.T) { - t.Parallel() - tt := []struct { - name string - imageData *ImageData - expected string - isTerminal bool - }{ - { - name: "HTML tags are not escaped", - imageData: &ImageData{Author: "dave "}, - expected: `"Author": "dave "`, - isTerminal: true, - }, - { - name: "HTML tags are escaped", - imageData: &ImageData{Author: "dave "}, - expected: `"Author": "dave \u003cdave@corp.io\u003e"`, - isTerminal: false, - }, - } - - for _, tc := range tt { - buf := bytes.NewBuffer(nil) - enc := setJSONFormatEncoder(tc.isTerminal, buf) - if err := enc.Encode(tc.imageData); err != nil { - t.Errorf("test %#v failed encoding: %s", tc.name, err) - } - if !strings.Contains(buf.String(), tc.expected) { - t.Errorf("test %#v expected output to contain %#v. Output:\n%v\n", tc.name, tc.expected, buf.String()) - } - } -} diff --git a/pkg/formats/templates.go b/pkg/formats/templates.go index 809597a95..ec75511ef 100644 --- a/pkg/formats/templates.go +++ b/pkg/formats/templates.go @@ -1,82 +1,19 @@ package formats import ( - "bytes" - "encoding/json" - "strings" "text/template" + + "github.com/containers/common/pkg/formats" ) -// basicFunctions are the set of initial -// functions provided to every template. -var basicFunctions = template.FuncMap{ - "json": func(v any) string { - buf := &bytes.Buffer{} - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) - _ = enc.Encode(v) - // Remove the trailing new line added by the encoder - return strings.TrimSpace(buf.String()) - }, - "split": strings.Split, - "join": strings.Join, - // strings.Title is deprecated since go 1.18 - // However for our use case it is still fine. The recommended replacement - // is adding about 400kb binary size so lets keep using this for now. - //nolint:staticcheck - "title": strings.Title, - "lower": strings.ToLower, - "upper": strings.ToUpper, - "pad": padWithSpace, - "truncate": truncateWithLength, -} - -// HeaderFunctions are used to created headers of a table. -// This is a replacement of basicFunctions for header generation -// because we want the header to remain intact. -// Some functions like `split` are irrelevant so not added. -var headerFunctions = template.FuncMap{ - "json": func(v string) string { - return v - }, - "title": func(v string) string { - return v - }, - "lower": func(v string) string { - return v - }, - "upper": func(v string) string { - return v - }, - "truncate": func(v string, _ int) string { - return v - }, -} - // Parse creates a new anonymous template with the basic functions // and parses the given format. func Parse(format string) (*template.Template, error) { - return NewParse("", format) + return formats.Parse(format) } // NewParse creates a new tagged template with the basic functions // and parses the given format. func NewParse(tag, format string) (*template.Template, error) { - return template.New(tag).Funcs(basicFunctions).Parse(format) -} - -// padWithSpace adds whitespace to the input if the input is non-empty -func padWithSpace(source string, prefix, suffix int) string { - if source == "" { - return source - } - return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix) -} - -// truncateWithLength truncates the source string up to the length provided by the input -func truncateWithLength(source string, length int) string { - if len(source) < length { - return source - } - return source[:length] + return formats.NewParse(tag, format) } diff --git a/vendor/github.com/containers/common/pkg/formats/formats.go b/vendor/github.com/containers/common/pkg/formats/formats.go new file mode 100644 index 000000000..12e9c8f8c --- /dev/null +++ b/vendor/github.com/containers/common/pkg/formats/formats.go @@ -0,0 +1,166 @@ +package formats + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "text/template" + + terminal "golang.org/x/term" + "sigs.k8s.io/yaml" +) + +const ( + // JSONString const to save on duplicate variable names + JSONString = "json" + // IDString const to save on duplicates for Go templates + IDString = "{{.ID}}" + + parsingErrorStr = "Template parsing error" +) + +// Writer interface for outputs +type Writer interface { + Out() error +} + +// JSONStructArray for JSON output +type JSONStructArray struct { + Output []any +} + +// StdoutTemplateArray for Go template output +type StdoutTemplateArray struct { + Output []any + Template string + Fields map[string]string +} + +// JSONStruct for JSON output +type JSONStruct struct { + Output any +} + +// StdoutTemplate for Go template output +type StdoutTemplate struct { + Output any + Template string + Fields map[string]string +} + +// YAMLStruct for YAML output +type YAMLStruct struct { + Output any +} + +func setJSONFormatEncoder(isTerminal bool, w io.Writer) *json.Encoder { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + if isTerminal { + enc.SetEscapeHTML(false) + } + return enc +} + +// Out method for JSON Arrays +func (j JSONStructArray) Out() error { + buf := bytes.NewBuffer(nil) + enc := setJSONFormatEncoder(terminal.IsTerminal(int(os.Stdout.Fd())), buf) + if err := enc.Encode(j.Output); err != nil { + return err + } + data := buf.Bytes() + + // JSON returns a byte array with a literal null [110 117 108 108] in it + // if it is passed empty data. We used bytes.Compare to see if that is + // the case. + if diff := bytes.Compare(data, []byte("null")); diff == 0 { + data = []byte("[]") + } + + // If the we did get NULL back, we should spit out {} which is + // at least valid JSON for the consumer. + fmt.Printf("%s", data) //nolint:forbidigo + humanNewLine() + return nil +} + +// Out method for Go templates +func (t StdoutTemplateArray) Out() error { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + if strings.HasPrefix(t.Template, "table") { + // replace any spaces with tabs in template so that tabwriter can align it + t.Template = strings.ReplaceAll(strings.TrimSpace(t.Template[5:]), " ", "\t") + headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template) + if err != nil { + return fmt.Errorf("%s: %w", parsingErrorStr, err) + } + err = headerTmpl.Execute(w, t.Fields) + if err != nil { + return err + } + fmt.Fprintln(w, "") + } + t.Template = strings.ReplaceAll(t.Template, " ", "\t") + tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template) + if err != nil { + return fmt.Errorf("%s: %w", parsingErrorStr, err) + } + for _, raw := range t.Output { + basicTmpl := tmpl.Funcs(basicFunctions) + if err := basicTmpl.Execute(w, raw); err != nil { + return fmt.Errorf("%s: %w", parsingErrorStr, err) + } + fmt.Fprintln(w, "") + } + return w.Flush() +} + +// Out method for JSON struct +func (j JSONStruct) Out() error { + data, err := json.MarshalIndent(j.Output, "", " ") + if err != nil { + return err + } + fmt.Printf("%s", data) //nolint:forbidigo + humanNewLine() + return nil +} + +// Out method for Go templates +func (t StdoutTemplate) Out() error { + tmpl, err := template.New("image").Parse(t.Template) + if err != nil { + return fmt.Errorf("template parsing error: %w", err) + } + err = tmpl.Execute(os.Stdout, t.Output) + if err != nil { + return err + } + humanNewLine() + return nil +} + +// Out method for YAML +func (y YAMLStruct) Out() error { + var buf []byte + var err error + buf, err = yaml.Marshal(y.Output) + if err != nil { + return err + } + fmt.Printf("%s", string(buf)) //nolint:forbidigo + humanNewLine() + return nil +} + +// humanNewLine prints a new line at the end of the output only if stdout is the terminal +func humanNewLine() { + if terminal.IsTerminal(int(os.Stdout.Fd())) { + fmt.Println() //nolint:forbidigo + } +} diff --git a/vendor/github.com/containers/common/pkg/formats/templates.go b/vendor/github.com/containers/common/pkg/formats/templates.go new file mode 100644 index 000000000..78b5d9863 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/formats/templates.go @@ -0,0 +1,78 @@ +package formats + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" +) + +// basicFunctions are the set of initial +// functions provided to every template. +var basicFunctions = template.FuncMap{ + "json": func(v any) string { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + _ = enc.Encode(v) + // Remove the trailing new line added by the encoder + return strings.TrimSpace(buf.String()) + }, + "split": strings.Split, + "join": strings.Join, + "title": strings.Title, //nolint:staticcheck + "lower": strings.ToLower, + "upper": strings.ToUpper, + "pad": padWithSpace, + "truncate": truncateWithLength, +} + +// HeaderFunctions are used to created headers of a table. +// This is a replacement of basicFunctions for header generation +// because we want the header to remain intact. +// Some functions like `split` are irrelevant so not added. +var headerFunctions = template.FuncMap{ + "json": func(v string) string { + return v + }, + "title": func(v string) string { + return v + }, + "lower": func(v string) string { + return v + }, + "upper": func(v string) string { + return v + }, + "truncate": func(v string, _ int) string { + return v + }, +} + +// Parse creates a new anonymous template with the basic functions +// and parses the given format. +func Parse(format string) (*template.Template, error) { + return NewParse("", format) +} + +// NewParse creates a new tagged template with the basic functions +// and parses the given format. +func NewParse(tag, format string) (*template.Template, error) { + return template.New(tag).Funcs(basicFunctions).Parse(format) +} + +// padWithSpace adds whitespace to the input if the input is non-empty +func padWithSpace(source string, prefix, suffix int) string { + if source == "" { + return source + } + return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix) +} + +// truncateWithLength truncates the source string up to the length provided by the input +func truncateWithLength(source string, length int) string { + if len(source) < length { + return source + } + return source[:length] +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4adfe65f7..3ab9267b8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -131,6 +131,7 @@ github.com/containers/common/pkg/completion github.com/containers/common/pkg/config github.com/containers/common/pkg/download github.com/containers/common/pkg/filters +github.com/containers/common/pkg/formats github.com/containers/common/pkg/hooks github.com/containers/common/pkg/hooks/0.1.0 github.com/containers/common/pkg/hooks/1.0.0 From 52bbc61e1f0f0c511b9203c1f96110ebab59f165 Mon Sep 17 00:00:00 2001 From: Nalin Dahyabhai Date: Mon, 23 Jun 2025 15:46:42 -0400 Subject: [PATCH 2/2] info,inspect: use the "formats" package to get some builtins Use the "formats" package to format `info` and `inspect` output, so that template users will be able to use whatever functions are provided with `images` and `containers` output, including "json", "lower", and "upper". Signed-off-by: Nalin Dahyabhai --- cmd/buildah/info.go | 4 ++-- cmd/buildah/inspect.go | 4 ++-- tests/inspect.bats | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/buildah/info.go b/cmd/buildah/info.go index af3c872c5..15a287aeb 100644 --- a/cmd/buildah/info.go +++ b/cmd/buildah/info.go @@ -6,10 +6,10 @@ import ( "os" "regexp" "runtime" - "text/template" "github.com/containers/buildah" "github.com/containers/buildah/define" + "github.com/containers/common/pkg/formats" "github.com/spf13/cobra" "golang.org/x/term" ) @@ -71,7 +71,7 @@ func infoCmd(c *cobra.Command, iopts infoResults) error { } else if !matched { return fmt.Errorf("invalid format provided: %s", format) } - t, err := template.New("format").Parse(format) + t, err := formats.NewParse("info", format) if err != nil { return fmt.Errorf("template parsing error: %w", err) } diff --git a/cmd/buildah/inspect.go b/cmd/buildah/inspect.go index b2f2334e6..4cd20513a 100644 --- a/cmd/buildah/inspect.go +++ b/cmd/buildah/inspect.go @@ -6,11 +6,11 @@ import ( "fmt" "os" "regexp" - "text/template" "github.com/containers/buildah" buildahcli "github.com/containers/buildah/pkg/cli" "github.com/containers/buildah/pkg/parse" + "github.com/containers/common/pkg/formats" "github.com/spf13/cobra" "golang.org/x/term" ) @@ -113,7 +113,7 @@ func inspectCmd(c *cobra.Command, args []string, iopts inspectResults) error { } else if !matched { return fmt.Errorf("invalid format provided: %s", format) } - t, err := template.New("format").Parse(format) + t, err := formats.NewParse("inspect", format) if err != nil { return fmt.Errorf("template parsing error: %w", err) } diff --git a/tests/inspect.bats b/tests/inspect.bats index 4685e6a99..a61bf6b4f 100644 --- a/tests/inspect.bats +++ b/tests/inspect.bats @@ -28,8 +28,8 @@ load helpers run_buildah inspect --type image --format '{{.OCIv1.Config}}' alpine-image inspect_after_commit=$output - # ...except that at some point in November 2019 buildah-inspect started - # including version. Strip it out, + # ...except that in #2510/#3036/#3829 we started adding a label with + # buildah's version. Strip it out for comparison. run_buildah --version local -a output_fields=($output) buildah_version=${output_fields[2]}