1 package pack
2
3 import (
4 "context"
5 "fmt"
6 "io/fs"
7 "path/filepath"
8 "strings"
9
10 "sigs.k8s.io/kustomize/kyaml/filesys"
11
12 "edge-infra.dev/pkg/f8n/warehouse/lift"
13 "edge-infra.dev/pkg/f8n/warehouse/lift/cmd/internal"
14 "edge-infra.dev/pkg/f8n/warehouse/lift/pack/types"
15 "edge-infra.dev/pkg/f8n/warehouse/oci/layout"
16 "edge-infra.dev/pkg/f8n/warehouse/oci/name"
17 ociremote "edge-infra.dev/pkg/f8n/warehouse/oci/remote"
18 "edge-infra.dev/pkg/f8n/warehouse/pallet/resolve"
19 "edge-infra.dev/pkg/lib/cli/rags"
20 "edge-infra.dev/pkg/lib/cli/sink"
21 )
22
23 func New(cfg lift.Config) *sink.Command {
24 var (
25 packer = internal.NewPacker(cfg)
26 push bool
27 )
28
29 cmd := &sink.Command{
30 Use: "pack [flags] <package paths>...",
31 Short: "build and publish Warehouse packages",
32 Flags: []*rags.Rag{
33 {
34 Name: "push",
35 Usage: "push built pallets to configured Warehouse registry",
36 Value: &rags.Bool{Var: &push},
37 Short: "p",
38 },
39 },
40 Extensions: []sink.Extension{packer},
41 Exec: func(_ context.Context, r sink.Run) error {
42 if len(r.Args()) == 0 {
43 return fmt.Errorf("error: at least one argument is required")
44 }
45
46 if push && cfg.Repo == "" {
47 return fmt.Errorf("error: oci repo is required for pushing\n" +
48 "set one via the --warehouse-oci-repo flag or setting the " +
49 "WAREHOUSE_OCI_REPO environment variable")
50 }
51
52 l, err := layout.New(cfg.Cache)
53 if err != nil {
54 return err
55 }
56
57 log := r.Log
58 for _, a := range r.Args() {
59 pkgPaths, err := resolvePalletPaths(a, packer.FS)
60 if err != nil {
61 return err
62 }
63 for _, path := range pkgPaths {
64 artifact, err := packer.Pack(path)
65 if err != nil {
66 return err
67 }
68 d, err := artifact.Digest()
69 if err != nil {
70 return err
71 }
72 l := log.WithValues(
73 "sha256", d.Hex[:9], "tag", packer.Tags,
74 )
75
76
77
78
79 plts, err := resolve.Resolve(artifact)
80 if err != nil {
81 return err
82 }
83 if len(plts) > 1 {
84 deps := make([]string, 0, len(plts))
85 for k, v := range plts {
86 if v.Name() == artifact.Name() {
87 continue
88 }
89 d, err := v.Digest()
90 if err != nil {
91 return fmt.Errorf("failed to get digest for %s: %w", k, err)
92 }
93 deps = append(deps, fmt.Sprintf("%s@%s", k, d.String()[:16]))
94 }
95 l = l.WithValues("deps", deps)
96 }
97 l.Info("built " + artifact.Name())
98 }
99 }
100
101
102
103 for _, p := range packer.Artifacts() {
104 for _, t := range packer.Tags {
105 ref, err := name.Tag(fmt.Sprintf("%s:%s", p.Name(), t))
106 if err != nil {
107 return fmt.Errorf("failed to parse tag for %s: %w", p.Name(), err)
108 }
109 if err := l.Append(ref, p); err != nil {
110 return fmt.Errorf("failed to write pallet to cache: %v", err)
111 }
112 }
113 if push {
114 d, err := p.Digest()
115 if err != nil {
116 return fmt.Errorf("failed to parse digest for %s: %w", p.Name(), err)
117 }
118 ref, err := name.Digest(fmt.Sprintf("%s/%s@%s", cfg.Repo, p.Name(), d))
119 if err != nil {
120 return fmt.Errorf("failed to create reference for %s: %w", p.Name(), err)
121 }
122
123 refs := ociremote.Map{ref: p}
124 for _, t := range packer.Tags {
125 tag, err := name.Tag(fmt.Sprintf("%s/%s:%s", cfg.Repo, p.Name(), t))
126 if err != nil {
127 return fmt.Errorf("failed to create tag for %s: %w", p.Name(), err)
128 }
129 refs[tag] = p
130 }
131 log.Info("pushing", "pkg", p.Name(), "dst", ref)
132 if err := ociremote.MultiWrite(refs); err != nil {
133 return fmt.Errorf("error: failed to push %s: %w", ref, err)
134 }
135 }
136 }
137
138
139 return l.Sort()
140 },
141 }
142
143 return cmd
144 }
145
146 func resolvePalletPaths(path string, fsys filesys.FileSystem) ([]string, error) {
147 var pkgPaths []string
148 switch {
149
150 case isWarehouseFile(path):
151 return []string{filepath.Dir(path)}, nil
152
153 case strings.HasSuffix(path, "/..."):
154 path = strings.TrimSuffix(path, "/...")
155 if err := fsys.Walk(path, func(wPath string, info fs.FileInfo, err error) error {
156 var buildPath string
157 switch {
158 case err != nil:
159 return fmt.Errorf("failed to search for Pallet files (%s): %w",
160 types.Files, err)
161 case info.IsDir():
162 if info.Name() == "testdata" || info.Name() == "test" {
163 return fs.SkipDir
164 }
165 return nil
166 case isWarehouseFile(info.Name()):
167 buildPath = filepath.Dir(wPath)
168 }
169 if buildPath != "" {
170 pkgPaths = append(pkgPaths, buildPath)
171 }
172 return nil
173 }); err != nil {
174 return nil, err
175 }
176
177 if len(pkgPaths) == 0 {
178 return nil, fmt.Errorf("no packages found for path %s", path)
179 }
180 return pkgPaths, nil
181 default:
182 return []string{path}, nil
183 }
184 }
185
186 func isWarehouseFile(path string) bool {
187 _, fileName := filepath.Split(path)
188 for _, file := range types.Files {
189 if fileName == file {
190 return true
191 }
192 }
193 return false
194 }
195
View as plain text