-
Notifications
You must be signed in to change notification settings - Fork 18.9k
Expand file tree
/
Copy pathutils.go
More file actions
154 lines (136 loc) · 4.23 KB
/
utils.go
File metadata and controls
154 lines (136 loc) · 4.23 KB
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
package client
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
cerrdefs "github.com/containerd/errdefs"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type emptyIDError string
func (e emptyIDError) InvalidParameter() {}
func (e emptyIDError) Error() string {
return "invalid " + string(e) + " name or ID: value is empty"
}
// trimID trims the given object-ID / name, returning an error if it's empty.
func trimID(objType, id string) (string, error) {
id = strings.TrimSpace(id)
if id == "" {
return "", emptyIDError(objType)
}
return id, nil
}
// parseAPIVersion checks v to be a well-formed ("<major>.<minor>")
// API version. It returns an error if the value is empty or does not
// have the correct format, but does not validate if the API version is
// within the supported range ([MinAPIVersion] <= v <= [MaxAPIVersion]).
//
// It returns version after normalizing, or an error if validation failed.
func parseAPIVersion(version string) (string, error) {
if strings.TrimPrefix(strings.TrimSpace(version), "v") == "" {
return "", cerrdefs.ErrInvalidArgument.WithMessage("value is empty")
}
major, minor, err := parseMajorMinor(version)
if err != nil {
return "", err
}
return fmt.Sprintf("%d.%d", major, minor), nil
}
// parseMajorMinor is a helper for parseAPIVersion.
func parseMajorMinor(v string) (major, minor int, _ error) {
if strings.HasPrefix(v, "v") {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("must be formatted <major>.<minor>")
}
if strings.TrimSpace(v) == "" {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("value is empty")
}
majVer, minVer, ok := strings.Cut(v, ".")
if !ok {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("must be formatted <major>.<minor>")
}
major, err := strconv.Atoi(majVer)
if err != nil {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid major version: must be formatted <major>.<minor>")
}
minor, err = strconv.Atoi(minVer)
if err != nil {
return 0, 0, cerrdefs.ErrInvalidArgument.WithMessage("invalid minor version: must be formatted <major>.<minor>")
}
return major, minor, nil
}
// encodePlatforms marshals the given platform(s) to JSON format, to
// be used for query-parameters for filtering / selecting platforms.
func encodePlatforms(platform ...ocispec.Platform) ([]string, error) {
if len(platform) == 0 {
return []string{}, nil
}
if len(platform) == 1 {
p, err := encodePlatform(&platform[0])
if err != nil {
return nil, err
}
return []string{p}, nil
}
seen := make(map[string]struct{}, len(platform))
out := make([]string, 0, len(platform))
for i := range platform {
p, err := encodePlatform(&platform[i])
if err != nil {
return nil, err
}
if _, ok := seen[p]; !ok {
out = append(out, p)
seen[p] = struct{}{}
}
}
return out, nil
}
// encodePlatform marshals the given platform to JSON format, to
// be used for query-parameters for filtering / selecting platforms. It
// is used as a helper for encodePlatforms,
func encodePlatform(platform *ocispec.Platform) (string, error) {
p, err := json.Marshal(platform)
if err != nil {
return "", fmt.Errorf("%w: invalid platform: %v", cerrdefs.ErrInvalidArgument, err)
}
return string(p), nil
}
func decodeWithRaw[T any](resp *http.Response, out *T) (raw json.RawMessage, _ error) {
if resp == nil || resp.Body == nil {
return nil, errors.New("empty response")
}
defer ensureReaderClosed(resp)
var buf bytes.Buffer
tr := io.TeeReader(resp.Body, &buf)
err := json.NewDecoder(tr).Decode(out)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// newCancelReadCloser wraps rc so it's automatically closed when ctx is canceled.
// Close is idempotent and returns the first error from rc.Close.
func newCancelReadCloser(ctx context.Context, rc io.ReadCloser) io.ReadCloser {
crc := &cancelReadCloser{
rc: rc,
close: sync.OnceValue(rc.Close),
}
crc.stop = context.AfterFunc(ctx, func() { _ = crc.close() })
return crc
}
type cancelReadCloser struct {
rc io.ReadCloser
close func() error
stop func() bool
}
func (c *cancelReadCloser) Read(p []byte) (int, error) { return c.rc.Read(p) }
func (c *cancelReadCloser) Close() error {
c.stop() // unregister AfterFunc
return c.close()
}