/
remote.go
336 lines (304 loc) · 9.92 KB
/
remote.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
// 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 (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/defenseunicorns/pkg/helpers"
"github.com/defenseunicorns/pkg/oci"
"github.com/defenseunicorns/uds-cli/src/config"
"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"
zarfUtils "github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/pkg/zoci"
"github.com/mholt/archiver/v4"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
ocistore "oras.land/oras-go/v2/content/oci"
)
const (
// GHCRPackagesPath is the default package path
GHCRPackagesPath = "oci://ghcr.io/defenseunicorns/packages/"
// GHCRUDSBundlePath is the default path for uds bundles
GHCRUDSBundlePath = GHCRPackagesPath + "uds/bundles/"
// GHCRDeliveryBundlePath is the default path for delivery bundles
GHCRDeliveryBundlePath = GHCRPackagesPath + "delivery/"
)
type ociProvider struct {
src string
dst string
*oci.OrasRemote
rootManifest *oci.Manifest
}
func (op *ociProvider) getBundleManifest() (*oci.Manifest, error) {
if op.rootManifest != nil {
return op.rootManifest, nil
}
return nil, fmt.Errorf("bundle root manifest not loaded")
}
// LoadBundleMetadata loads a remote bundle's metadata
func (op *ociProvider) LoadBundleMetadata() (types.PathMap, error) {
ctx := context.TODO()
if err := helpers.CreateDirectory(filepath.Join(op.dst, config.BlobsDir), 0700); err != nil {
return nil, err
}
layers, err := op.PullPaths(ctx, filepath.Join(op.dst, config.BlobsDir), config.BundleAlwaysPull)
if err != nil {
return nil, err
}
loaded := make(types.PathMap)
for _, layer := range layers {
rel := layer.Annotations[ocispec.AnnotationTitle]
abs := filepath.Join(op.dst, config.BlobsDir, rel)
absSha := filepath.Join(op.dst, config.BlobsDir, layer.Digest.Encoded())
if err := os.Rename(abs, absSha); err != nil {
return nil, err
}
loaded[rel] = absSha
}
return loaded, nil
}
// CreateBundleSBOM creates a bundle-level SBOM from the underlying Zarf packages, if the Zarf package contains an SBOM
func (op *ociProvider) CreateBundleSBOM(extractSBOM bool) error {
ctx := context.TODO()
SBOMArtifactPathMap := make(types.PathMap)
root, err := op.FetchRoot(ctx)
if err != nil {
return err
}
// make tmp dir for pkg SBOM extraction
err = os.Mkdir(filepath.Join(op.dst, config.BundleSBOM), 0700)
if err != nil {
return err
}
containsSBOMs := false
// iterate through Zarf image manifests and find the Zarf pkg's sboms.tar
for _, layer := range root.Layers {
if layer.Annotations[ocispec.AnnotationTitle] == config.BundleYAML {
continue
}
zarfManifest, err := op.OrasRemote.FetchManifest(ctx, layer)
if err != nil {
return err
}
// grab descriptor for sboms.tar
sbomDesc := zarfManifest.Locate(config.SBOMsTar)
if oci.IsEmptyDescriptor(sbomDesc) {
message.Warnf("%s not found in Zarf pkg", config.SBOMsTar)
continue
}
// grab sboms.tar and extract
sbomBytes, err := op.OrasRemote.FetchLayer(ctx, sbomDesc)
if err != nil {
return err
}
extractor := utils.SBOMExtractor(op.dst, SBOMArtifactPathMap)
err = archiver.Tar{}.Extract(context.TODO(), bytes.NewReader(sbomBytes), nil, extractor)
if err != nil {
return err
}
containsSBOMs = true
}
if extractSBOM {
if !containsSBOMs {
message.Warnf("Cannot extract, no SBOMs found in bundle")
return nil
}
currentDir, err := os.Getwd()
if err != nil {
return err
}
err = utils.MoveExtractedSBOMs(op.dst, currentDir)
if err != nil {
return err
}
} else {
err = utils.CreateSBOMArtifact(SBOMArtifactPathMap)
if err != nil {
return err
}
}
return nil
}
// LoadBundle loads a bundle from a remote source
func (op *ociProvider) LoadBundle(opts types.BundlePullOptions, _ int) (*types.UDSBundle, types.PathMap, error) {
ctx := context.TODO()
var bundle types.UDSBundle
// pull the bundle's metadata + sig
loaded, err := op.LoadBundleMetadata()
if err != nil {
return nil, nil, err
}
if err := zarfUtils.ReadYaml(loaded[config.BundleYAML], &bundle); err != nil {
return nil, nil, err
}
// validate the sig (if present) before pulling the whole bundle
if err := ValidateBundleSignature(loaded[config.BundleYAML], loaded[config.BundleYAMLSignature], opts.PublicKeyPath); err != nil {
return nil, nil, err
}
var layersToPull []ocispec.Descriptor
estimatedBytes := int64(0)
// get the bundle's root manifest
rootManifest, err := op.getBundleManifest()
if err != nil {
return nil, nil, err
}
// grab root manifest config
layersToPull = append(layersToPull, rootManifest.Config)
for _, pkg := range bundle.Packages {
// grab sha of zarf image manifest and pull it down
sha := strings.Split(pkg.Ref, "@sha256:")[1] // this is where we use the SHA appended to the Zarf pkg inside the bundle
manifestDesc := rootManifest.Locate(sha)
if err != nil {
return nil, nil, err
}
manifestBytes, err := op.FetchLayer(ctx, manifestDesc)
if err != nil {
return nil, nil, err
}
// unmarshal the zarf image manifest and add it to the layers to pull
var manifest oci.Manifest
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
return nil, nil, err
}
layersToPull = append(layersToPull, manifestDesc)
progressBar := message.NewProgressBar(int64(len(manifest.Layers)), fmt.Sprintf("Verifying layers in Zarf package: %s", pkg.Name))
// go through the layers in the zarf image manifest and check if they exist in the remote
for _, layer := range manifest.Layers {
ok, err := op.Repo().Blobs().Exists(ctx, layer)
progressBar.Add(1)
estimatedBytes += layer.Size
if err != nil {
return nil, nil, err
}
// if the layer exists in the remote, add it to the layers to pull
if ok {
layersToPull = append(layersToPull, layer)
}
}
progressBar.Successf("Verified %s package", pkg.Name)
}
store, err := ocistore.NewWithContext(ctx, op.dst)
if err != nil {
return nil, nil, err
}
// grab the bundle root manifest and add it to the layers to pull
rootDesc, err := op.ResolveRoot(ctx)
if err != nil {
return nil, nil, err
}
layersToPull = append(layersToPull, rootDesc)
// create copy options for oras.Copy()
copyOpts := utils.CreateCopyOpts(layersToPull, config.CommonOptions.OCIConcurrency)
// Create a thread to update a progress bar as we save the package to disk
doneSaving := make(chan error)
go zarfUtils.RenderProgressBarForLocalDirWrite(op.dst, estimatedBytes, doneSaving, fmt.Sprintf("Pulling bundle: %s", bundle.Metadata.Name), fmt.Sprintf("Successfully pulled bundle: %s", bundle.Metadata.Name))
// note that in this case oras.Copy() copies using the bundle root manifest, not the packages directly
_, err = oras.Copy(ctx, op.Repo(), op.Repo().Reference.String(), store, op.Repo().Reference.String(), copyOpts)
doneSaving <- err
<-doneSaving
if err != nil {
return nil, nil, err
}
for _, layer := range layersToPull {
sha := layer.Digest.Encoded()
loaded[sha] = filepath.Join(op.dst, config.BlobsDir, sha)
}
return &bundle, loaded, nil
}
func (op *ociProvider) PublishBundle(_ types.UDSBundle, _ *oci.OrasRemote) error {
// todo: implement moving bundles from one registry to another
return fmt.Errorf("moving bundles in between remote registries not yet supported")
}
// Returns the validated source path based on the provided oci source path
func getOCIValidatedSource(source string) (string, error) {
ctx := context.TODO()
originalSource := source
platform := ocispec.Platform{
Architecture: config.GetArch(),
OS: oci.MultiOS,
}
// Check provided repository path
sourceWithOCI := utils.EnsureOCIPrefix(source)
remote, err := zoci.NewRemote(sourceWithOCI, platform)
if err == nil {
source = sourceWithOCI
_, err = remote.ResolveRoot(ctx)
}
// if root didn't resolve, expand the path
if err != nil {
// Check in ghcr uds bundle path
source = GHCRUDSBundlePath + originalSource
remote, err = zoci.NewRemote(source, platform)
if err == nil {
_, err = remote.ResolveRoot(ctx)
}
if err != nil {
message.Debugf("%s: not found", source)
// Check in delivery bundle path
source = GHCRDeliveryBundlePath + originalSource
remote, err = zoci.NewRemote(source, platform)
if err == nil {
_, err = remote.ResolveRoot(ctx)
}
if err != nil {
message.Debugf("%s: not found", source)
// Check in packages bundle path
source = GHCRPackagesPath + originalSource
remote, err = zoci.NewRemote(source, platform)
if err == nil {
_, err = remote.ResolveRoot(ctx)
}
if err != nil {
errMsg := fmt.Sprintf("%s: not found", originalSource)
message.Debug(errMsg)
return "", errors.New(errMsg)
}
}
}
}
message.Debugf("%s: found", source)
return source, nil
}
// ValidateArch validates that the passed in arch matches the cluster arch
func ValidateArch(arch string) error {
// compare bundle arch and cluster arch
var clusterArchs []string
c, err := cluster.NewCluster()
if err != nil {
message.Debugf("error creating cluster object: %s", err)
}
if c != nil {
clusterArchs, err = c.GetArchitectures()
if err == nil {
return err
}
// check if bundle arch is in clusterArchs
if !slices.Contains(clusterArchs, arch) {
return fmt.Errorf("arch %s does not match cluster arch, %s", arch, clusterArchs)
}
}
return nil
}
// CheckOCISourcePath checks that provided oci source path is valid, and updates it if it's missing the full path
func CheckOCISourcePath(source string) (string, error) {
validTarballPath := utils.IsValidTarballPath(source)
var err error
if !validTarballPath {
source, err = getOCIValidatedSource(source)
if err != nil {
return "", err
}
}
return source, nil
}