/
opts.go
327 lines (287 loc) · 10.1 KB
/
opts.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
package engine
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"os"
"strconv"
"unicode"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/opencontainers/go-digest"
"google.golang.org/grpc/metadata"
"github.com/dagger/dagger/telemetry"
)
const (
EngineVersionMetaKey = "x-dagger-engine"
clientMetadataMetaKey = "x-dagger-client-metadata"
localImportOptsMetaKey = "x-dagger-local-import-opts"
localExportOptsMetaKey = "x-dagger-local-export-opts"
// local dir import (set by buildkit, can't change)
localDirImportDirNameMetaKey = "dir-name"
localDirImportIncludePatternsMetaKey = "include-patterns"
localDirImportExcludePatternsMetaKey = "exclude-patterns"
localDirImportFollowPathsMetaKey = "followpaths"
)
type ClientMetadata struct {
// ClientID is unique to every session created by every client
ClientID string `json:"client_id"`
// ClientSecretToken is a secret token that is unique to every client. It's
// initially provided to the server in the controller.Solve request. Every
// other request w/ that client ID must also include the same token.
ClientSecretToken string `json:"client_secret_token"`
// ServerID is the id of the server that a client and any of its nested
// module clients connect to
ServerID string `json:"server_id"`
// If RegisterClient is true, then a call to Session will initialize the
// server if it hasn't already been initialized and register the session's
// attachables with it either way. If false, then the session conn will be
// forwarded to the server
RegisterClient bool `json:"register_client"`
// ClientHostname is the hostname of the client that made the request. It's
// used opportunistically as a best-effort, semi-stable identifier for the
// client across multiple sessions, which can be useful for debugging and for
// minimizing occurrences of both excessive cache misses and excessive cache
// matches.
ClientHostname string `json:"client_hostname"`
// (Optional) Pipeline labels for e.g. vcs info like branch, commit, etc.
Labels telemetry.Labels `json:"labels"`
// ParentClientIDs is a list of session ids that are parents of the current
// session. The first element is the direct parent, the second element is the
// parent of the parent, and so on.
ParentClientIDs []string `json:"parent_client_ids"`
// If this client is for a module function, this digest will be set in the
// grpc context metadata for any api requests back to the engine. It's used by the API
// server to determine which schema to serve and other module context metadata.
ModuleCallerDigest digest.Digest `json:"module_caller_digest"`
// Import configuration for Buildkit's remote cache
UpstreamCacheImportConfig []*controlapi.CacheOptionsEntry
// Export configuration for Buildkit's remote cache
UpstreamCacheExportConfig []*controlapi.CacheOptionsEntry
// Dagger Cloud Token
CloudToken string
// Disable analytics
DoNotTrack bool
}
// ClientIDs returns the ClientID followed by ParentClientIDs.
func (m ClientMetadata) ClientIDs() []string {
return append([]string{m.ClientID}, m.ParentClientIDs...)
}
func (m ClientMetadata) ToGRPCMD() metadata.MD {
return encodeMeta(clientMetadataMetaKey, m)
}
func (m ClientMetadata) AppendToMD(md metadata.MD) metadata.MD {
for k, v := range m.ToGRPCMD() {
md[k] = append(md[k], v...)
}
return md
}
func ContextWithClientMetadata(ctx context.Context, clientMetadata *ClientMetadata) context.Context {
return contextWithMD(ctx, clientMetadata.ToGRPCMD())
}
func ClientMetadataFromContext(ctx context.Context) (*ClientMetadata, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("failed to get metadata from context")
}
clientMetadata := &ClientMetadata{}
if err := decodeMeta(md, clientMetadataMetaKey, clientMetadata); err != nil {
return nil, err
}
return clientMetadata, nil
}
type LocalImportOpts struct {
Path string `json:"path"`
IncludePatterns []string `json:"include_patterns"`
ExcludePatterns []string `json:"exclude_patterns"`
FollowPaths []string `json:"follow_paths"`
ReadSingleFileOnly bool `json:"read_single_file_only"`
MaxFileSize int64 `json:"max_file_size"`
StatPathOnly bool `json:"stat_path_only"`
StatReturnAbsPath bool `json:"stat_return_abs_path"`
}
func (o LocalImportOpts) ToGRPCMD() metadata.MD {
// set both the dagger metadata and the ones used by buildkit
md := encodeMeta(localImportOptsMetaKey, o)
md[localDirImportDirNameMetaKey] = []string{o.Path}
md[localDirImportIncludePatternsMetaKey] = o.IncludePatterns
md[localDirImportExcludePatternsMetaKey] = o.ExcludePatterns
md[localDirImportFollowPathsMetaKey] = o.FollowPaths
return encodeOpts(md)
}
func (o LocalImportOpts) AppendToOutgoingContext(ctx context.Context) context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = make(metadata.MD)
}
for k, v := range o.ToGRPCMD() {
md[k] = append(md[k], v...)
}
return metadata.NewOutgoingContext(ctx, md)
}
func LocalImportOptsFromContext(ctx context.Context) (*LocalImportOpts, error) {
incomingMD, incomingOk := metadata.FromIncomingContext(ctx)
outgoingMD, outgoingOk := metadata.FromOutgoingContext(ctx)
if !incomingOk && !outgoingOk {
return nil, fmt.Errorf("failed to get metadata from context")
}
md := decodeOpts(metadata.Join(incomingMD, outgoingMD))
opts := &LocalImportOpts{}
_, ok := md[localImportOptsMetaKey]
if ok {
if err := decodeMeta(md, localImportOptsMetaKey, opts); err != nil {
return nil, err
}
return opts, nil
}
// otherwise, this is coming from buildkit directly
dirNameVals := md[localDirImportDirNameMetaKey]
if len(dirNameVals) != 1 {
return nil, fmt.Errorf("expected exactly one %s, got %d", localDirImportDirNameMetaKey, len(dirNameVals))
}
opts.Path = dirNameVals[0]
opts.IncludePatterns = md[localDirImportIncludePatternsMetaKey]
opts.ExcludePatterns = md[localDirImportExcludePatternsMetaKey]
opts.FollowPaths = md[localDirImportFollowPathsMetaKey]
return opts, nil
}
type LocalExportOpts struct {
Path string `json:"path"`
IsFileStream bool `json:"is_file_stream"`
FileOriginalName string `json:"file_original_name"`
AllowParentDirPath bool `json:"allow_parent_dir_path"`
FileMode os.FileMode `json:"file_mode"`
// whether to just merge in contents of a directory to the target on the host
// or to replace the target entirely such that it matches the source directory,
// which includes deleting any files that are not in the source directory
Merge bool
}
func (o LocalExportOpts) ToGRPCMD() metadata.MD {
return encodeMeta(localExportOptsMetaKey, o)
}
func (o LocalExportOpts) AppendToOutgoingContext(ctx context.Context) context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = make(metadata.MD)
}
for k, v := range o.ToGRPCMD() {
md[k] = append(md[k], v...)
}
return metadata.NewOutgoingContext(ctx, md)
}
func LocalExportOptsFromContext(ctx context.Context) (*LocalExportOpts, error) {
incomingMD, incomingOk := metadata.FromIncomingContext(ctx)
outgoingMD, outgoingOk := metadata.FromOutgoingContext(ctx)
if !incomingOk && !outgoingOk {
return nil, fmt.Errorf("failed to get metadata from context")
}
md := metadata.Join(incomingMD, outgoingMD)
opts := &LocalExportOpts{}
if err := decodeMeta(md, localExportOptsMetaKey, opts); err != nil {
return nil, err
}
return opts, nil
}
func contextWithMD(ctx context.Context, mds ...metadata.MD) context.Context {
incomingMD, ok := metadata.FromIncomingContext(ctx)
if !ok {
incomingMD = metadata.MD{}
}
outgoingMD, ok := metadata.FromOutgoingContext(ctx)
if !ok {
outgoingMD = metadata.MD{}
}
for _, md := range mds {
for k, v := range md {
incomingMD[k] = v
outgoingMD[k] = v
}
}
ctx = metadata.NewIncomingContext(ctx, incomingMD)
ctx = metadata.NewOutgoingContext(ctx, outgoingMD)
return ctx
}
func encodeMeta(key string, v interface{}) metadata.MD {
b, err := json.Marshal(v)
if err != nil {
panic(err)
}
return metadata.Pairs(key, base64.StdEncoding.EncodeToString(b))
}
func decodeMeta(md metadata.MD, key string, dest interface{}) error {
vals, ok := md[key]
if !ok {
return fmt.Errorf("failed to get %s from metadata", key)
}
if len(vals) != 1 {
return fmt.Errorf("expected exactly one %s value, got %d", key, len(vals))
}
jsonPayload, err := base64.StdEncoding.DecodeString(vals[0])
if err != nil {
return fmt.Errorf("failed to base64-decode %s: %v", key, err)
}
if err := json.Unmarshal(jsonPayload, dest); err != nil {
return fmt.Errorf("failed to JSON-unmarshal %s: %v", key, err)
}
return nil
}
// encodeOpts comes from buildkit session/filesync/filesync.go
func encodeOpts(opts map[string][]string) map[string][]string {
md := make(map[string][]string, len(opts))
for k, v := range opts {
out, encoded := encodeStringForHeader(v)
md[k] = out
if encoded {
md[k+"-encoded"] = []string{"1"}
}
}
return md
}
// decodeOpts comes from buildkit session/filesync/filesync.go
func decodeOpts(opts map[string][]string) map[string][]string {
md := make(map[string][]string, len(opts))
for k, v := range opts {
out := make([]string, len(v))
var isEncoded bool
if v, ok := opts[k+"-encoded"]; ok && len(v) > 0 {
if b, _ := strconv.ParseBool(v[0]); b {
isEncoded = true
}
}
if isEncoded {
for i, s := range v {
out[i], _ = url.QueryUnescape(s)
}
} else {
copy(out, v)
}
md[k] = out
}
return md
}
// encodeStringForHeader encodes a string value so it can be used in grpc header. This encoding
// is backwards compatible and avoids encoding ASCII characters.
//
// encodeStringForHeader comes from buildkit session/filesync/filesync.go
func encodeStringForHeader(inputs []string) ([]string, bool) {
var encode bool
loop:
for _, input := range inputs {
for _, runeVal := range input {
// Only encode non-ASCII characters, and characters that have special
// meaning during decoding.
if runeVal > unicode.MaxASCII {
encode = true
break loop
}
}
}
if !encode {
return inputs, false
}
for i, input := range inputs {
inputs[i] = url.QueryEscape(input)
}
return inputs, true
}