Skip to content

Commit 73b4eaa

Browse files
committed
feat: implement a dump command
It allows to dump all packages in a set either to JSON or via Go template to be transformed into any other text. This was used to build automatically extension catalog in siderolabs/extensions#793 Signed-off-by: Andrey Smirnov <[email protected]>
1 parent 42e5c02 commit 73b4eaa

File tree

5 files changed

+203
-26
lines changed

5 files changed

+203
-26
lines changed

cmd/bldr/cmd/dump.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package cmd
6+
7+
import (
8+
"log"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"text/template"
13+
14+
"github.com/Masterminds/sprig/v3"
15+
"github.com/spf13/cobra"
16+
"github.com/stretchr/testify/assert/yaml"
17+
18+
"github.com/siderolabs/bldr/internal/pkg/solver"
19+
)
20+
21+
var dumpCmdFlags struct {
22+
templatePath string
23+
buildArgs []string
24+
}
25+
26+
// dumpCmd represents the graph command.
27+
var dumpCmd = &cobra.Command{
28+
Use: "dump",
29+
Short: "Dump information about all packages.",
30+
Long: `This command outputs the set of all packages as JSON
31+
output, or runs a Go template against a set of all packages
32+
and prints the template output.
33+
`,
34+
Args: cobra.NoArgs,
35+
Run: func(_ *cobra.Command, _ []string) {
36+
context := options.GetVariables().Copy()
37+
38+
for _, buildArg := range dumpCmdFlags.buildArgs {
39+
name, value, _ := strings.Cut(buildArg, "=")
40+
41+
context["BUILD_ARG_"+name] = value
42+
}
43+
44+
loader := solver.FilesystemPackageLoader{
45+
Root: pkgRoot,
46+
Context: context,
47+
}
48+
49+
packages, err := solver.NewPackages(&loader)
50+
if err != nil {
51+
log.Fatal(err)
52+
}
53+
54+
var packageSet solver.PackageSet
55+
56+
if options.Target != "" {
57+
var graph *solver.PackageGraph
58+
59+
graph, err = packages.Resolve(options.Target)
60+
if err != nil {
61+
log.Fatal(err)
62+
}
63+
64+
packageSet = graph.ToSet()
65+
} else {
66+
packageSet = packages.ToSet()
67+
}
68+
69+
packageSet = packageSet.Sorted()
70+
71+
if dumpCmdFlags.templatePath == "" {
72+
if err = packageSet.DumpJSON(os.Stdout); err != nil {
73+
log.Fatal(err)
74+
}
75+
76+
return
77+
}
78+
79+
funcs := sprig.HermeticTxtFuncMap()
80+
funcs["mustFromYAML"] = mustFromYAML
81+
82+
tmpl, err := template.New(filepath.Base(dumpCmdFlags.templatePath)).
83+
Funcs(funcs).
84+
ParseFiles(dumpCmdFlags.templatePath)
85+
if err != nil {
86+
log.Fatal(err)
87+
}
88+
89+
if err := packageSet.Template(os.Stdout, tmpl); err != nil {
90+
log.Fatal(err)
91+
}
92+
},
93+
}
94+
95+
func mustFromYAML(v string) (any, error) {
96+
var output any
97+
98+
err := yaml.Unmarshal([]byte(v), &output)
99+
100+
return output, err
101+
}
102+
103+
func init() {
104+
dumpCmd.Flags().StringVarP(&options.Target, "target", "t", "", "Target image to dump, if not set - dump all stages")
105+
dumpCmd.Flags().StringSliceVar(&dumpCmdFlags.buildArgs, "build-arg", nil, "Build arguments to pass similar to docker buildx")
106+
dumpCmd.Flags().StringVarP(&dumpCmdFlags.templatePath, "template", "", "", "Path to Go template file to use for output formatting")
107+
rootCmd.AddCommand(dumpCmd)
108+
}

internal/pkg/solver/filesystem_loader.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@ import (
2323
// FilesystemPackageLoader loads packages by walking file system tree.
2424
type FilesystemPackageLoader struct {
2525
*log.Logger
26-
Context types.Variables
27-
pathContexts map[string]types.Variables
28-
multiErr *multierror.Error
29-
pkgFile *v1alpha2.Pkgfile
30-
Root string
31-
absRootPath string
32-
pkgFilePaths []string
33-
varFilePaths []string
34-
pkgs []*v1alpha2.Pkg
26+
Context types.Variables
27+
pathContexts map[string]types.Variables
28+
multiErr *multierror.Error
29+
pkgFile *v1alpha2.Pkgfile
30+
Root string
31+
absRootPath string
32+
pkgFilePaths []string
33+
varFilePaths []string
34+
templateFilePaths []string
35+
pkgs []*v1alpha2.Pkg
3536
}
3637

3738
func (fspl *FilesystemPackageLoader) walkFunc() filepath.WalkFunc {
@@ -50,11 +51,13 @@ func (fspl *FilesystemPackageLoader) walkFunc() filepath.WalkFunc {
5051
return nil
5152
}
5253

53-
switch info.Name() {
54-
case constants.PkgYaml:
54+
switch {
55+
case info.Name() == constants.PkgYaml:
5556
fspl.pkgFilePaths = append(fspl.pkgFilePaths, path)
56-
case constants.VarsYaml:
57+
case info.Name() == constants.VarsYaml:
5758
fspl.varFilePaths = append(fspl.varFilePaths, path)
59+
case strings.HasSuffix(info.Name(), constants.TemplateExt):
60+
fspl.templateFilePaths = append(fspl.templateFilePaths, path)
5861
}
5962

6063
return nil
@@ -115,6 +118,17 @@ func (fspl *FilesystemPackageLoader) Load() (*LoadResult, error) {
115118
fspl.Printf("loaded pkg %q from %q", pkg.Name, path)
116119
fspl.pkgs = append(fspl.pkgs, pkg)
117120
}
121+
122+
for _, path := range fspl.templateFilePaths {
123+
var pkg *v1alpha2.Pkg
124+
125+
if pkg, err = fspl.attachTemplate(path); err != nil {
126+
fspl.Printf("error attaching template %q: %s", path, err)
127+
fspl.multiErr = multierror.Append(fspl.multiErr, fmt.Errorf("error attaching template %q: %w", path, err))
128+
} else {
129+
fspl.Printf("attached template %q to %q", path, pkg.Name)
130+
}
131+
}
118132
}
119133

120134
return &LoadResult{
@@ -197,6 +211,38 @@ func (fspl *FilesystemPackageLoader) loadPkg(path string) (*v1alpha2.Pkg, error)
197211
return v1alpha2.NewPkg(filepath.Dir(basePath), path, contents, context)
198212
}
199213

214+
func (fspl *FilesystemPackageLoader) attachTemplate(path string) (*v1alpha2.Pkg, error) {
215+
// find the closest pkgs in relative path
216+
var (
217+
closestPkg *v1alpha2.Pkg
218+
shortestRel string
219+
)
220+
221+
for _, pkg := range fspl.pkgs {
222+
rel, err := filepath.Rel(pkg.BaseDir, filepath.Dir(path))
223+
if err != nil || strings.HasPrefix(rel, "..") {
224+
continue
225+
}
226+
227+
if shortestRel == "" || len(rel) < len(shortestRel) {
228+
closestPkg = pkg
229+
shortestRel = rel
230+
}
231+
}
232+
233+
if closestPkg == nil {
234+
return nil, fmt.Errorf("no suitable package found for template %q", path)
235+
}
236+
237+
content, err := os.ReadFile(path)
238+
if err != nil {
239+
return nil, err
240+
}
241+
242+
// attach the template to the closest package
243+
return closestPkg, closestPkg.AttachTemplatedFile(filepath.Join(shortestRel, filepath.Base(path)), content)
244+
}
245+
200246
func (fspl *FilesystemPackageLoader) loadPkgfile() error {
201247
f, err := os.Open(filepath.Join(fspl.Root, constants.Pkgfile))
202248
if err != nil {

internal/pkg/solver/set.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
package solver
66

77
import (
8+
"cmp"
9+
"encoding/json"
810
"io"
11+
"slices"
12+
"text/template"
913

1014
"github.com/emicklei/dot"
1115
)
@@ -23,3 +27,23 @@ func (set PackageSet) DumpDot(w io.Writer) {
2327

2428
g.Write(w)
2529
}
30+
31+
// Sorted returns a new set which is sorted by name package set.
32+
func (set PackageSet) Sorted() PackageSet {
33+
return slices.SortedFunc(slices.Values(set), func(a, b *PackageNode) int {
34+
return cmp.Compare(a.Name, b.Name)
35+
})
36+
}
37+
38+
// DumpJSON dumps the package set as JSON.
39+
func (set PackageSet) DumpJSON(w io.Writer) error {
40+
encoder := json.NewEncoder(w)
41+
encoder.SetIndent("", " ")
42+
43+
return encoder.Encode(set)
44+
}
45+
46+
// Template dumps the package set as a Go template.
47+
func (set PackageSet) Template(w io.Writer, tmpl *template.Template) error {
48+
return tmpl.Execute(w, set)
49+
}

internal/pkg/types/v1alpha2/pkg.go

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,17 @@ import (
1919

2020
// Pkg represents build instructions for a single package.
2121
type Pkg struct {
22-
templatedFiles []TemplatedFile
23-
24-
Context types.Variables `yaml:"-"`
25-
Name string `yaml:"name,omitempty"`
26-
Shell Shell `yaml:"shell,omitempty"`
27-
BaseDir string `yaml:"-"`
28-
FileName string `yaml:"-"`
29-
Install Install `yaml:"install,omitempty"`
30-
Dependencies Dependencies `yaml:"dependencies,omitempty"`
31-
Steps Steps `yaml:"steps,omitempty"`
32-
Finalize []Finalize `yaml:"finalize,omitempty"`
33-
Variant Variant `yaml:"variant,omitempty"`
22+
TemplatedFiles []TemplatedFile `yaml:"-"`
23+
Context types.Variables `yaml:"-"`
24+
Name string `yaml:"name,omitempty"`
25+
Shell Shell `yaml:"shell,omitempty"`
26+
BaseDir string `yaml:"-"`
27+
FileName string `yaml:"-"`
28+
Install Install `yaml:"install,omitempty"`
29+
Dependencies Dependencies `yaml:"dependencies,omitempty"`
30+
Steps Steps `yaml:"steps,omitempty"`
31+
Finalize []Finalize `yaml:"finalize,omitempty"`
32+
Variant Variant `yaml:"variant,omitempty"`
3433
}
3534

3635
// NewPkg loads Pkg structure from file.

internal/pkg/types/v1alpha2/templates.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func (p *Pkg) AttachTemplatedFile(path string, content []byte) error {
3535
return fmt.Errorf("failed to template file %s: %w", path, err)
3636
}
3737

38-
p.templatedFiles = append(p.templatedFiles, TemplatedFile{
38+
p.TemplatedFiles = append(p.TemplatedFiles, TemplatedFile{
3939
Path: strings.TrimSuffix(path, constants.TemplateExt),
4040
Content: buf.Bytes(),
4141
})
@@ -45,5 +45,5 @@ func (p *Pkg) AttachTemplatedFile(path string, content []byte) error {
4545

4646
// GetTemplatedFiles return a list of templated files in the package.
4747
func (p *Pkg) GetTemplatedFiles() []TemplatedFile {
48-
return p.templatedFiles
48+
return p.TemplatedFiles
4949
}

0 commit comments

Comments
 (0)