Merge pull request #6242 from nalind/common-formats

Use containers/common's formats package instead of our own
This commit is contained in:
openshift-merge-bot[bot] 2025-06-25 19:53:42 +00:00 committed by GitHub
commit 9f6205610c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 269 additions and 266 deletions

View File

@ -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"
)

View File

@ -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"
)

View File

@ -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)
}

View File

@ -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)
}

2
go.mod
View File

@ -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
)

2
pkg/formats/doc.go Normal file
View File

@ -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

View File

@ -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

View File

@ -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 <dave@corp.io>"},
expected: `"Author": "dave <dave@corp.io>"`,
isTerminal: true,
},
{
name: "HTML tags are escaped",
imageData: &ImageData{Author: "dave <dave@corp.io>"},
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())
}
}
}

View File

@ -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)
}

View File

@ -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]}

View File

@ -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
}
}

View File

@ -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]
}

1
vendor/modules.txt vendored
View File

@ -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