-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathimage.go
250 lines (217 loc) · 7.94 KB
/
image.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
package dalec
import (
"context"
goerrors "errors"
"github.com/google/shlex"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/llb/sourceresolver"
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
"github.com/pkg/errors"
)
type DockerImageSpec = dockerspec.DockerOCIImage
type DockerImageConfig = dockerspec.DockerOCIImageConfig
// ImageConfig is the configuration for the output image.
// When the target output is a container image, this is used to configure the image.
type ImageConfig struct {
// Entrypoint sets the image's "entrypoint" field.
// This is used to control the default command to run when the image is run.
Entrypoint string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`
// Cmd sets the image's "cmd" field.
// When entrypoint is set, this is used as the default arguments to the entrypoint.
// When entrypoint is not set, this is used as the default command to run.
Cmd string `yaml:"cmd,omitempty" json:"cmd,omitempty"`
// Env is the list of environment variables to set in the image.
Env []string `yaml:"env,omitempty" json:"env,omitempty"`
// Labels is the list of labels to set in the image metadata.
Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
// Volumes is the list of volumes for the image.
// Volumes instruct the runtime to bypass the any copy-on-write filesystems and mount the volume directly to the container.
Volumes map[string]struct{} `yaml:"volumes,omitempty" json:"volumes,omitempty"`
// WorkingDir is the working directory to set in the image.
// This sets the directory the container will start in.
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
// StopSignal is the signal to send to the container to stop it.
// This is used to stop the container gracefully.
StopSignal string `yaml:"stop_signal,omitempty" json:"stop_signal,omitempty" jsonschema:"example=SIGTERM"`
// Base is the base image to use for the output image.
// This only affects the output image, not the intermediate build image.
// Deprecated: Use [Bases] instead.
Base string `yaml:"base,omitempty" json:"base,omitempty"`
// Bases is used to specify a list of base images to build images for. The
// intent of allowing multiple bases is for cases, such as Windows, where you
// may want to publish multiple versions of a base image in one image.
//
// Windows is the example here because of the way Windows works, the image
// that the base is based off of must match the OS version of the host machine.
// Therefore it is common to have multiple Windows images in one with a
// different value for the os version field of the platform.
//
// For the most part implementations are not expected to support multiple base
// images and may error out if multiple are specified.
//
// This should not be set if [Base] is also set.
Bases []BaseImage `yaml:"bases,omitempty json:bases,omitempty"`
// Post is the post install configuration for the image.
// This allows making additional modifications to the container rootfs after the package(s) are installed.
//
// Use this to perform actions that would otherwise require additional tooling inside the container that is not relevant to
// the resulting container and makes a post-install script as part of the package unnecessary.
Post *PostInstall `yaml:"post,omitempty" json:"post,omitempty"`
// User is the that the image should run as.
User string `yaml:"user,omitempty" json:"user,omitempty"`
}
type BaseImage struct {
// Rootfs represents an image rootfs.
Rootfs Source `yaml:"rootfs" json:"rootfs"`
}
// MergeImageConfig copies the fields from the source [ImageConfig] into the destination [image.Image].
// If a field is not set in the source, it is not modified in the destination.
// Envs from [ImageConfig] are merged into the destination [image.Image] and take precedence.
func MergeImageConfig(dst *DockerImageConfig, src *ImageConfig) error {
if src == nil {
return nil
}
if src.Entrypoint != "" {
split, err := shlex.Split(src.Entrypoint)
if err != nil {
return errors.Wrap(err, "error splitting entrypoint into args")
}
dst.Entrypoint = split
// Reset cmd as this may be totally invalid now
// This is the same behavior as the Dockerfile frontend
dst.Cmd = nil
}
if src.Cmd != "" {
split, err := shlex.Split(src.Cmd)
if err != nil {
return errors.Wrap(err, "error splitting cmd into args")
}
dst.Cmd = split
}
if len(src.Env) > 0 {
// Env is append only
// If the env var already exists, replace it
envIdx := make(map[string]int)
for i, env := range dst.Env {
envIdx[env] = i
}
for _, env := range src.Env {
if idx, ok := envIdx[env]; ok {
dst.Env[idx] = env
} else {
dst.Env = append(dst.Env, env)
}
}
}
if src.WorkingDir != "" {
dst.WorkingDir = src.WorkingDir
}
if src.StopSignal != "" {
dst.StopSignal = src.StopSignal
}
if src.User != "" {
dst.User = src.User
}
for k, v := range src.Volumes {
if dst.Volumes == nil {
dst.Volumes = make(map[string]struct{}, len(src.Volumes))
}
dst.Volumes[k] = v
}
for k, v := range src.Labels {
if dst.Labels == nil {
dst.Labels = make(map[string]string, len(src.Labels))
}
dst.Labels[k] = v
}
return nil
}
func (s *ImageConfig) validate() error {
if s == nil {
return nil
}
var errs []error
if s.Base != "" && len(s.Bases) > 0 {
errs = append(errs, errors.New("cannot specify both image.base and image.bases"))
}
for i, base := range s.Bases {
if err := base.validate(); err != nil {
errs = append(errs, errors.Wrapf(err, "bases[%d]", i))
}
}
return goerrors.Join(errs...)
}
func (s *ImageConfig) fillDefaults() {
if s == nil {
return
}
// s.Bases is a superset of s.Base, so migrate s.Base to s.Bases
if s.Base != "" {
s.Bases = append(s.Bases, BaseImage{
Rootfs: Source{
DockerImage: &SourceDockerImage{
Ref: s.Base,
},
},
})
s.Base = ""
}
for _, bi := range s.Bases {
bi.fillDefaults()
}
}
func (s *BaseImage) validate() error {
if s.Rootfs.DockerImage == nil {
// In the future we may support other source types but this adds a lot of complexity
// that is currently unecessary.
return errors.New("rootfs currently only supports image source types")
}
if err := s.Rootfs.validate(); err != nil {
return errors.Wrap(err, "rootfs")
}
return nil
}
func (s *BaseImage) fillDefaults() {
fillDefaults(&s.Rootfs)
}
func (bi *BaseImage) ResolveImageConfig(ctx context.Context, sOpt SourceOpts, opt sourceresolver.Opt) ([]byte, error) {
// In the future, *BaseImage may support other source types, but for now it only supports Docker images.
//
// Likewise we may support passing in a config separate from the requested image rootfs,
// e.g. through a new field in *BaseImage, but for now we only support resolving the image config from the provided image reference.
_, _, dt, err := sOpt.Resolver.ResolveImageConfig(ctx, bi.Rootfs.DockerImage.Ref, opt)
return dt, err
}
func (bi *BaseImage) ToState(sOpt SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) {
if bi == nil {
return llb.Scratch(), nil
}
return bi.Rootfs.AsState("rootfs", sOpt, opts...)
}
func (s *Spec) GetImageBases(targetKey string) []BaseImage {
if t, ok := s.Targets[targetKey]; ok && t.Image != nil {
// note: this is intentionally only doing a nil check and *not* a length check
// so that an empty list of bases can be used to override the default bases
if t.Image.Bases != nil {
return t.Image.Bases
}
}
if s.Image == nil {
return nil
}
return s.Image.Bases
}
// GetSingleBase looks up the base images to use for the targetKey and returns
// only the first entry.
// If there is more than 1 entry an error is returned.
// If there are no entries then both return values are nil.
func (s *Spec) GetSingleBase(targetKey string) (*BaseImage, error) {
bases := s.GetImageBases(targetKey)
if len(bases) > 1 {
return nil, errors.New("multiple image bases, expected only one")
}
if len(bases) == 0 {
return nil, nil
}
return &bases[0], nil
}