-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathgateway.go
More file actions
267 lines (227 loc) · 7.22 KB
/
gateway.go
File metadata and controls
267 lines (227 loc) · 7.22 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
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
package frontend
import (
"context"
"fmt"
"sync"
"sync/atomic"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/frontend/dockerui"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/bklog"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/project-dalec/dalec"
)
const (
requestIDKey = "requestid"
dalecSubrequstForwardBuild = "dalec.forward.build"
gatewayFrontend = "gateway.v0"
)
func getDockerfile(ctx context.Context, client gwclient.Client, build *dalec.SourceBuild, defPb *pb.Definition) ([]byte, error) {
dockerfilePath := dockerui.DefaultDockerfileName
if build.DockerfilePath != "" {
dockerfilePath = build.DockerfilePath
}
// First we need to read the dockerfile to determine what frontend to forward to
res, err := client.Solve(ctx, gwclient.SolveRequest{
Definition: defPb,
})
if err != nil {
return nil, errors.Wrap(err, "error getting build context")
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
dt, err := ref.ReadFile(ctx, gwclient.ReadRequest{
Filename: dockerfilePath,
})
if err != nil {
return nil, errors.Wrap(err, "error reading dockerfile")
}
return dt, nil
}
// ForwarderFromClient creates a [dalec.ForwarderFunc] from a gateway client.
// This is used for forwarding builds to other frontends in [dalec.Source2LLBGetter]
func ForwarderFromClient(ctx context.Context, client gwclient.Client) dalec.ForwarderFunc {
return func(st llb.State, spec *dalec.SourceBuild, opts ...llb.ConstraintsOpt) (llb.State, error) {
if spec == nil {
spec = &dalec.SourceBuild{}
}
def, err := st.Marshal(ctx, opts...)
if err != nil {
return llb.Scratch(), err
}
defPb := def.ToPB()
dockerfileDt, err := getDockerfile(ctx, client, spec, defPb)
if err != nil {
return llb.Scratch(), err
}
req, err := newSolveRequest(
toDockerfile(ctx, st, dockerfileDt, spec, dalec.ProgressGroup("prepare dockerfile to forward to frontend")),
copyForForward(ctx, client),
)
if err != nil {
return llb.Scratch(), err
}
res, err := client.Solve(ctx, req)
if err != nil {
return llb.Scratch(), err
}
ref, err := res.SingleRef()
if err != nil {
return llb.Scratch(), err
}
return ref.ToState()
}
}
func GetBuildArg(client gwclient.Client, k string) (string, bool) {
opts := client.BuildOpts().Opts
if opts != nil {
if v, ok := opts["build-arg:"+k]; ok {
return v, true
}
}
return "", false
}
func SourceOptFromUIClient(ctx context.Context, c gwclient.Client, dc *dockerui.Client, platform *ocispecs.Platform) dalec.SourceOpts {
sOpt := dalec.SourceOpts{
TargetPlatform: platform,
Resolver: c,
Forward: ForwarderFromClient(ctx, c),
GetContext: func(ref string, opts ...llb.LocalOption) (*llb.State, error) {
if ref == dockerui.DefaultLocalNameContext {
return dc.MainContext(ctx, opts...)
}
nc, err := dc.NamedContext(ref, dockerui.ContextOpt{
ResolveMode: dc.ImageResolveMode.String(),
AsyncLocalOpts: func() []llb.LocalOption {
return opts
},
Platform: platform,
})
if err != nil {
return nil, err
}
if nc == nil {
return nil, nil
}
st, _, err := nc.Load(ctx)
return st, err
},
GitCredHelperOpt: withCredHelper(c),
}
sOpt.SourceFilter = sync.OnceValues(func() (dalec.SourceFilterConfig, error) {
return loadSourceFilterConfig(ctx, c, sOpt.GetContext)
})
return sOpt
}
func SourceOptFromClient(ctx context.Context, c gwclient.Client, platform *ocispecs.Platform) (dalec.SourceOpts, error) {
dc, err := dockerui.NewClient(c)
if err != nil {
return dalec.SourceOpts{}, err
}
return SourceOptFromUIClient(ctx, c, dc, platform), nil
}
var (
supportsDiffMergeOnce sync.Once
supportsDiffMerge atomic.Bool
)
// SupportsDiffMerge checks if the given client supports the diff and merge operations.
func SupportsDiffMerge(client gwclient.Client) bool {
supportsDiffMergeOnce.Do(func() {
if client.BuildOpts().Opts["build-arg:DALEC_DISABLE_DIFF_MERGE"] == "1" {
supportsDiffMerge.Store(false)
return
}
supportsDiffMerge.Store(checkDiffMerge(client))
})
return supportsDiffMerge.Load()
}
func checkDiffMerge(client gwclient.Client) bool {
caps := client.BuildOpts().LLBCaps
if caps.Supports(pb.CapMergeOp) != nil {
return false
}
if caps.Supports(pb.CapDiffOp) != nil {
return false
}
return true
}
// copyForForward copies all the inputs and build opts from the initial request in order to forward to another frontend.
func copyForForward(ctx context.Context, client gwclient.Client) solveRequestOpt {
return func(req *gwclient.SolveRequest) error {
// Inputs are any additional build contexts or really any llb that the client sent along.
inputs, err := client.Inputs(ctx)
if err != nil {
return err
}
if req.FrontendInputs == nil {
req.FrontendInputs = make(map[string]*pb.Definition, len(inputs))
}
for k, v := range inputs {
if _, ok := req.FrontendInputs[k]; ok {
// Do not overwrite existing inputs
continue
}
def, err := v.Marshal(ctx)
if err != nil {
return errors.Wrap(err, "error marshaling frontend input")
}
req.FrontendInputs[k] = def.ToPB()
}
opts := client.BuildOpts().Opts
if req.FrontendOpt == nil {
req.FrontendOpt = make(map[string]string, len(opts))
}
for k, v := range opts {
if k == "filename" || k == "dockerfilekey" || k == "target" {
// These are some well-known keys that the dockerfile frontend uses
// which we'll be overriding with our own values (as needed) in the
// caller.
// Basically there should not be a need, nor is it desirable, to forward these along.
continue
}
if _, ok := req.FrontendOpt[k]; ok {
// Do not overwrite existing opts
continue
}
req.FrontendOpt[k] = v
}
return nil
}
}
const keyTopLevelTarget = "dalec.target"
type BuildOpstGetter interface {
BuildOpts() gwclient.BuildOpts
}
// GetTargetKey returns the key that should be used to select the [dalec.Target] from the [dalec.Spec]
func GetTargetKey(client BuildOpstGetter) string {
return client.BuildOpts().Opts[keyTopLevelTarget]
}
// Warn sends a warning to the client for the provided state.
func Warn(ctx context.Context, client gwclient.Client, st llb.State, msg string) {
// Note: This will attempt to marshal the state to get its digest for metadata
// on the warning message, but it is not required to actually write the message.
// For this reason we can continue on error.
def, err := st.Marshal(ctx)
if err != nil {
bklog.G(ctx).WithError(err).WithField("warn", msg).Warn("Error marshalling state for outputting warning message")
}
var dgst digest.Digest
if def != nil {
dgst, err = def.Head()
if err != nil {
bklog.G(ctx).WithError(err).WithField("warn", msg).Warn("Could not get state digest for outputting warning message")
}
}
if err := client.Warn(ctx, dgst, msg, gwclient.WarnOpts{}); err != nil {
bklog.G(ctx).WithError(err).WithField("warn", msg).Warn("Error writing warning message")
}
}
func Warnf(ctx context.Context, client gwclient.Client, st llb.State, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
Warn(ctx, client, st, msg)
}