-
Notifications
You must be signed in to change notification settings - Fork 95
/
Copy pathpull.go
183 lines (149 loc) · 5.47 KB
/
pull.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
package oci
import (
"context"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/internal/util"
"github.com/replicatedhq/troubleshoot/pkg/version"
"k8s.io/klog/v2"
"oras.land/oras-go/pkg/auth"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
"oras.land/oras-go/pkg/registry"
)
const (
HelmCredentialsFileBasename = ".config/helm/registry/config.json"
)
var (
ErrNoRelease = errors.New("no release found")
)
func PullPreflightFromOCI(uri string) ([]byte, error) {
return pullFromOCI(context.Background(), uri, "replicated.preflight.spec", "replicated-preflight")
}
func PullSupportBundleFromOCI(uri string) ([]byte, error) {
return pullFromOCI(context.Background(), uri, "replicated.supportbundle.spec", "replicated-supportbundle")
}
// PullSpecsFromOCI pulls both the preflight and support bundle specs from the given URI
//
// The URI is expected to be the same as the one used to install your KOTS application
// Example oci://registry.replicated.com/app-slug/unstable will endup pulling
// preflights from "registry.replicated.com/app-slug/unstable/replicated-preflight:latest"
// and support bundles from "registry.replicated.com/app-slug/unstable/replicated-supportbundle:latest"
// Both images have their own media types created when publishing KOTS OCI image.
// NOTE: This only works with replicated registries for now and for KOTS applications only
func PullSpecsFromOCI(ctx context.Context, uri string) ([]string, error) {
// TODOs (API is opinionated, but we should be able to support these):
// - Pulling from generic OCI registries (not just replicated)
// - Pulling from registries that require authentication
// - Passing in a complete URI including tags and image name
rawSpecs := []string{}
// First try to pull the preflight spec
rawPreflight, err := pullFromOCI(ctx, uri, "replicated.preflight.spec", "replicated-preflight")
if err != nil {
// Ignore "not found" error and continue fetching the support bundle spec
if !errors.Is(err, ErrNoRelease) {
return nil, err
}
} else {
rawSpecs = append(rawSpecs, string(rawPreflight))
}
// Then try to pull the support bundle spec
rawSupportBundle, err := pullFromOCI(ctx, uri, "replicated.supportbundle.spec", "replicated-supportbundle")
// If we had found a preflight spec, do not return an error
if err != nil && len(rawSpecs) == 0 {
return nil, err
}
rawSpecs = append(rawSpecs, string(rawSupportBundle))
return rawSpecs, nil
}
func pullFromOCI(ctx context.Context, uri string, mediaType string, imageName string) ([]byte, error) {
// helm credentials
helmCredentialsFile := filepath.Join(util.HomeDir(), HelmCredentialsFileBasename)
dockerauthClient, err := dockerauth.NewClientWithDockerFallback(helmCredentialsFile)
if err != nil {
return nil, errors.Wrap(err, "failed to create auth client")
}
authClient := dockerauthClient
headers := http.Header{}
headers.Set("User-Agent", version.GetUserAgent())
opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
resolver, err := authClient.ResolverWithOpts(opts...)
if err != nil {
return nil, errors.Wrap(err, "failed to create resolver")
}
memoryStore := content.NewMemory()
allowedMediaTypes := []string{
mediaType,
}
var descriptors, layers []ocispec.Descriptor
registryStore := content.Registry{Resolver: resolver}
parsedRef, err := parseURI(uri, imageName)
if err != nil {
return nil, err
}
klog.V(1).Infof("Pulling spec from %q OCI uri", parsedRef)
manifest, err := oras.Copy(ctx, registryStore, parsedRef, memoryStore, "",
oras.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(allowedMediaTypes),
oras.WithLayerDescriptors(func(l []ocispec.Descriptor) {
layers = l
}))
if err != nil {
if strings.Contains(err.Error(), "not found") {
return nil, ErrNoRelease
}
return nil, errors.Wrap(err, "failed to copy")
}
descriptors = append(descriptors, manifest)
descriptors = append(descriptors, layers...)
// expect 2 descriptors
if len(descriptors) != 2 {
return nil, fmt.Errorf("expected 2 descriptor, got %d", len(descriptors))
}
var matchingDescriptor *ocispec.Descriptor
for _, descriptor := range descriptors {
d := descriptor
switch d.MediaType {
case mediaType:
matchingDescriptor = &d
}
}
if matchingDescriptor == nil {
return nil, fmt.Errorf("no descriptor found with media type: %s", mediaType)
}
_, matchingSpec, ok := memoryStore.Get(*matchingDescriptor)
if !ok {
return nil, fmt.Errorf("failed to get matching descriptor")
}
return matchingSpec, nil
}
func parseURI(in, imageName string) (string, error) {
u, err := url.Parse(in)
if err != nil {
return "", err
}
// Always check the scheme. If more schemes need to be supported
// we need to compare u.Scheme against a list of supported schemes.
// url.Parse(raw) will not return an error if a scheme is not present.
if u.Scheme != "oci" {
return "", fmt.Errorf("%q is an invalid OCI registry scheme", u.Scheme)
}
// remove unnecessary bits (oci://, tags)
uriParts := strings.Split(u.EscapedPath(), ":")
tag := "latest"
if len(uriParts) > 1 {
tag = uriParts[1]
}
uri := fmt.Sprintf("%s%s/%s:%s", u.Host, uriParts[0], imageName, tag) // <host>:<port>/path/<imageName>:tag
parsedRef, err := registry.ParseReference(uri)
if err != nil {
return "", errors.Wrap(err, "failed to parse OCI uri reference")
}
return parsedRef.String(), nil
}