-
Notifications
You must be signed in to change notification settings - Fork 10
/
common.go
346 lines (307 loc) · 11.2 KB
/
common.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023-Present The UDS Authors
// Package bundle contains functions for interacting with, managing and deploying UDS packages
package bundle
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/defenseunicorns/uds-cli/src/config"
"github.com/defenseunicorns/uds-cli/src/pkg/bundler/fetcher"
"github.com/defenseunicorns/uds-cli/src/pkg/utils"
"github.com/defenseunicorns/uds-cli/src/types"
"github.com/defenseunicorns/zarf/src/pkg/cluster"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/pkg/oci"
zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/pkg/utils/helpers"
"github.com/defenseunicorns/zarf/src/pkg/zoci"
zarfTypes "github.com/defenseunicorns/zarf/src/types"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// Bundle handles bundler operations
type Bundle struct {
// cfg is the Bundle's configuration options
cfg *types.BundleConfig
// bundle is the bundle's metadata read into memory
bundle types.UDSBundle
// tmp is the temporary directory used by the Bundle cleaned up with ClearPaths()
tmp string
}
// New creates a new Bundle
func New(cfg *types.BundleConfig) (*Bundle, error) {
message.Debugf("bundler.New(%s)", message.JSONValue(cfg))
if cfg == nil {
return nil, errors.New("bundler.New() called with nil config")
}
var (
bundle = &Bundle{
cfg: cfg,
}
)
tmp, err := zarfUtils.MakeTempDir(config.CommonOptions.TempDirectory)
if err != nil {
return nil, fmt.Errorf("bundler unable to create temp directory: %w", err)
}
bundle.tmp = tmp
return bundle, nil
}
// NewOrDie creates a new Bundle or dies
func NewOrDie(cfg *types.BundleConfig) *Bundle {
var (
err error
bundle *Bundle
)
if bundle, err = New(cfg); err != nil {
message.Fatalf(err, "bundle unable to setup, bad config: %s", err.Error())
}
return bundle
}
// ClearPaths closes any files and clears out the paths used by Bundle
func (b *Bundle) ClearPaths() {
_ = os.RemoveAll(b.tmp)
}
// ValidateBundleResources validates the bundle's metadata and package references
func (b *Bundle) ValidateBundleResources(spinner *message.Spinner) error {
bundle := &b.bundle
if bundle.Metadata.Architecture == "" {
// ValidateBundle was erroneously called before CalculateBuildInfo
if err := b.CalculateBuildInfo(); err != nil {
return err
}
if bundle.Metadata.Architecture == "" {
return errors.New("unable to determine architecture")
}
}
if bundle.Metadata.Version == "" {
return fmt.Errorf("%s is missing required field: metadata.version", config.BundleYAML)
}
if bundle.Metadata.Name == "" {
return fmt.Errorf("%s is missing required field: metadata.name", config.BundleYAML)
}
if len(bundle.Packages) == 0 {
return fmt.Errorf("%s is missing required list: packages", config.BundleYAML)
}
if err := validateBundleVars(bundle.Packages); err != nil {
return fmt.Errorf("error validating bundle vars: %s", err)
}
// validate access to packages as well as components referenced in the package
for idx, pkg := range bundle.Packages {
// if package path is set, make it relative to source directory
if pkg.Path != "" {
pkg.Path = filepath.Join(b.cfg.CreateOpts.SourceDirectory, pkg.Path)
}
spinner.Updatef("Validating Bundle Package: %s", pkg.Name)
if pkg.Name == "" {
return fmt.Errorf("%s is missing required field: name", pkg)
}
if pkg.Repository == "" && pkg.Path == "" {
return fmt.Errorf("zarf pkg %s must have either a repository or path field", pkg.Name)
}
if pkg.Repository != "" && pkg.Path != "" {
return fmt.Errorf("zarf pkg %s cannot have both a repository and a path", pkg.Name)
}
if pkg.Ref == "" {
return fmt.Errorf("%s .packages[%s] is missing required field: ref", config.BundleYAML, pkg.Repository)
}
var zarfYAML zarfTypes.ZarfPackage
var url string
// if using a remote repository
// todo: refactor these hash checks using the fetcher
if pkg.Repository != "" {
url = fmt.Sprintf("%s:%s", pkg.Repository, pkg.Ref)
if strings.Contains(pkg.Ref, "@sha256:") {
url = fmt.Sprintf("%s:%s", pkg.Repository, pkg.Ref)
}
platform := ocispec.Platform{
Architecture: config.GetArch(),
OS: oci.MultiOS,
}
remote, err := zoci.NewRemote(url, platform)
if err != nil {
return err
}
if err := remote.Repo().Reference.ValidateReferenceAsDigest(); err != nil {
manifestDesc, err := remote.ResolveRoot(context.TODO())
if err != nil {
return err
}
// todo: don't do this here, a "validate" fn shouldn't be modifying the bundle
bundle.Packages[idx].Ref = pkg.Ref + "@sha256:" + manifestDesc.Digest.Encoded()
}
} else {
// atm we don't support outputting a bundle with local pkgs outputting to OCI
if utils.IsRegistryURL(b.cfg.CreateOpts.Output) {
return fmt.Errorf("detected local Zarf package: %s, outputting to an OCI registry is not supported when using local Zarf packages", pkg.Name)
}
path := getPkgPath(pkg, bundle.Metadata.Architecture)
bundle.Packages[idx].Path = path
}
// grab the Zarf pkg metadata
f, err := fetcher.NewPkgFetcher(pkg, fetcher.Config{
PkgIter: idx, Bundle: bundle,
})
if err != nil {
return err
}
// For local pkgs, this will throw an error if the zarf package name in the bundle doesn't match the actual zarf package name
zarfYAML, err = f.GetPkgMetadata()
if err != nil {
return err
}
message.Debug("Validating package:", message.JSONValue(pkg))
// todo: need to packager.ValidatePackageSignature (or come up with a bundle-level signature scheme)
publicKeyPath := filepath.Join(b.tmp, config.PublicKeyFile)
if pkg.PublicKey != "" {
if err := os.WriteFile(publicKeyPath, []byte(pkg.PublicKey), helpers.ReadWriteUser); err != nil {
return err
}
defer os.Remove(publicKeyPath)
}
if len(pkg.OptionalComponents) > 0 {
// validate the optional components exist in the package and support the bundle's target architecture
for _, component := range pkg.OptionalComponents {
c := helpers.Find(zarfYAML.Components, func(c zarfTypes.ZarfComponent) bool {
return c.Name == component
})
// make sure the component exists
if c.Name == "" {
return fmt.Errorf("%s .packages[%s].components[%s] does not exist in upstream: %s", config.BundleYAML, pkg.Repository, component, url)
}
// make sure the component supports the bundle's target architecture
if c.Only.Cluster.Architecture != "" && c.Only.Cluster.Architecture != bundle.Metadata.Architecture {
return fmt.Errorf("%s .packages[%s].components[%s] does not support architecture: %s", config.BundleYAML, pkg.Repository, component, bundle.Metadata.Architecture)
}
}
}
err = validateOverrides(pkg, zarfYAML)
if err != nil {
return err
}
}
return nil
}
func getPkgPath(pkg types.Package, arch string) string {
var fullPkgName string
var path string
if strings.HasSuffix(pkg.Path, ".tar.zst") {
// use the provided pkg tarball
path = pkg.Path
} else if pkg.Name == "init" {
// Zarf init pkgs have a specific naming convention
fullPkgName = fmt.Sprintf("zarf-%s-%s-%s.tar.zst", pkg.Name, arch, pkg.Ref)
path = filepath.Join(pkg.Path, fullPkgName)
} else {
// infer the name of the local Zarf pkg
fullPkgName = fmt.Sprintf("zarf-package-%s-%s-%s.tar.zst", pkg.Name, arch, pkg.Ref)
path = filepath.Join(pkg.Path, fullPkgName)
}
return path
}
// CalculateBuildInfo calculates the build info for the bundle
func (b *Bundle) CalculateBuildInfo() error {
now := time.Now()
b.bundle.Build.User = os.Getenv("USER")
hostname, err := os.Hostname()
if err != nil {
return err
}
b.bundle.Build.Terminal = hostname
// --architecture flag > metadata.arch > build.arch > runtime.GOARCH (default)
b.bundle.Build.Architecture = config.GetArch(b.bundle.Metadata.Architecture, b.bundle.Build.Architecture)
b.bundle.Metadata.Architecture = b.bundle.Build.Architecture
b.bundle.Build.Timestamp = now.Format(time.RFC1123Z)
b.bundle.Build.Version = config.CLIVersion
return nil
}
// ValidateBundleSignature validates the bundle signature
func ValidateBundleSignature(bundleYAMLPath, signaturePath, publicKeyPath string) error {
if helpers.InvalidPath(bundleYAMLPath) {
return fmt.Errorf("path for %s at %s does not exist", config.BundleYAML, bundleYAMLPath)
}
// The package is not signed, and no public key was provided
if signaturePath == "" && publicKeyPath == "" {
return nil
}
// The package is not signed, but a public key was provided
if helpers.InvalidPath(signaturePath) && !helpers.InvalidPath(publicKeyPath) {
return fmt.Errorf("package is not signed, but a public key was provided")
}
// The package is signed, but no public key was provided
if !helpers.InvalidPath(signaturePath) && helpers.InvalidPath(publicKeyPath) {
return fmt.Errorf("package is signed, but no public key was provided")
}
// The package is signed, and a public key was provided
return zarfUtils.CosignVerifyBlob(bundleYAMLPath, signaturePath, publicKeyPath)
}
// GetDeployedPackageNames returns the names of the packages that have been deployed
func GetDeployedPackageNames() []string {
var deployedPackageNames []string
c, _ := cluster.NewCluster()
if c != nil {
deployedPackages, _ := c.GetDeployedZarfPackages()
for _, pkg := range deployedPackages {
deployedPackageNames = append(deployedPackageNames, pkg.Name)
}
}
return deployedPackageNames
}
// validateOverrides ensures that the overrides have matching components and charts in the zarf package
func validateOverrides(pkg types.Package, zarfYAML zarfTypes.ZarfPackage) error {
for componentName, chartsValues := range pkg.Overrides {
var foundComponent *zarfTypes.ZarfComponent
for _, component := range zarfYAML.Components {
if component.Name == componentName {
componentCopy := component // Create a copy of the component
foundComponent = &componentCopy
break
}
}
if foundComponent == nil {
return fmt.Errorf("invalid override: package %q does not contain the component %q", pkg.Name, componentName)
}
for chartName := range chartsValues {
var foundChart *zarfTypes.ZarfChart
for _, chart := range foundComponent.Charts {
if chart.Name == chartName {
chartCopy := chart // Create a copy of the chart
foundChart = &chartCopy
break
}
}
if foundChart == nil {
return fmt.Errorf("invalid override: package %q does not contain the chart %q", pkg.Name, chartName)
}
}
}
return nil
}
// validateBundleVars ensures imports and exports between Zarf pkgs match up
func validateBundleVars(packages []types.Package) error {
exports := make(map[string]string)
for i, pkg := range packages {
if i == 0 && pkg.Imports != nil {
return fmt.Errorf("first package in bundle cannot have imports")
}
// capture exported vars from all Zarf pkgs
if pkg.Exports != nil {
for _, v := range pkg.Exports {
exports[v.Name] = pkg.Name // save off pkg.Name to check when importing
}
}
// ensure imports have a matching export
if pkg.Imports != nil {
for _, v := range pkg.Imports {
if _, ok := exports[v.Name]; ok && v.Package == exports[v.Name] {
continue
}
return fmt.Errorf("import var %s does not have a matching export", v.Name)
}
}
}
return nil
}