/
creator.go
218 lines (176 loc) · 6.72 KB
/
creator.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
package ihop
import (
"encoding/json"
"fmt"
"time"
"github.com/paketo-buildpacks/packit/v2/scribe"
)
//go:generate faux --interface ImageClient --output fakes/image_client.go
type ImageClient interface {
Build(definitionImage DefinitionImage, platform string) (Image, error)
Update(Image) (Image, error)
}
//go:generate faux --interface ImageBuilder --output fakes/image_builder.go
type ImageBuilder interface {
Execute(definitionImage DefinitionImage, platform string) ImageBuildPromise
}
//go:generate faux --interface LayerCreator --output fakes/layer_creator.go
type LayerCreator interface {
Create(Image, DefinitionImage, SBOM) (Layer, error)
}
// A Stack holds all of the Build and Run images for a built stack.
type Stack struct {
Build []Image
Run []Image
}
// A Creator can be used to generate a Stack.
type Creator struct {
docker ImageClient
builder ImageBuilder
userLayerCreator LayerCreator
sbomLayerCreator LayerCreator
osReleaseLayerCreator LayerCreator
now func() time.Time
logger scribe.Logger
}
// NewCreator returns a Creator configured with the given arguments.
func NewCreator(docker ImageClient, builder ImageBuilder, userLayerCreator, sbomLayerCreator LayerCreator, osReleaseLayerCreator LayerCreator, now func() time.Time, logger scribe.Logger) Creator {
return Creator{
docker: docker,
builder: builder,
userLayerCreator: userLayerCreator,
sbomLayerCreator: sbomLayerCreator,
osReleaseLayerCreator: osReleaseLayerCreator,
now: now,
logger: logger,
}
}
// Execute builds a Stack using the given Definition.
func (c Creator) Execute(def Definition) (Stack, error) {
c.logger.Title("Building %s", def.ID)
var stack Stack
for _, platform := range def.Platforms {
c.logger.Process("Building on %s", platform)
build, run, err := c.create(def, platform)
if err != nil {
return Stack{}, err
}
stack.Build = append(stack.Build, build)
stack.Run = append(stack.Run, run)
}
return stack, nil
}
func (c Creator) create(def Definition, platform string) (Image, Image, error) {
c.logger.Subprocess("Building base images")
// invoke the builder to start the build process for the build and run images
buildPromise := c.builder.Execute(def.Build, platform)
runPromise := c.builder.Execute(def.Run, platform)
// wait for the build image to complete building
build, buildSBOM, err := buildPromise.Resolve()
if err != nil {
return Image{}, Image{}, err
}
// wait for the run image to complete building
run, runSBOM, err := runPromise.Resolve()
if err != nil {
return Image{}, Image{}, err
}
c.logger.Action("Build complete for base images")
// determine which packages appear in each image
packages := NewPackages(buildSBOM.Packages(), runSBOM.Packages())
timestamp := c.now()
c.logger.Subprocess("build: Decorating base image")
c.logger.Action("Adding CNB_* environment variables")
build.Env = append(build.Env, fmt.Sprintf("CNB_USER_ID=%d", def.Build.UID))
build.Env = append(build.Env, fmt.Sprintf("CNB_GROUP_ID=%d", def.Build.GID))
build.Env = append(build.Env, fmt.Sprintf("CNB_STACK_ID=%s", def.ID))
// update the base build image with common configuration metadata
build, err = c.mutate(build, def, def.Build, buildSBOM, packages.Intersection, packages.BuildComplement, timestamp)
if err != nil {
return Image{}, Image{}, err
}
c.logger.Subprocess("run: Decorating base image")
// update the base run image with common configuration metadata
run, err = c.mutate(run, def, def.Run, runSBOM, packages.Intersection, packages.RunComplement, timestamp)
if err != nil {
return Image{}, Image{}, err
}
if def.containsOsReleaseOverwrites() {
// update /etc/os-release" in the run images in the Docker daemon
c.logger.Action("Updating /etc/os-release")
layer, err := c.osReleaseLayerCreator.Create(run, def.Run, runSBOM)
if err != nil {
return Image{}, Image{}, err
}
run.Layers = append(run.Layers, layer)
}
// if the EXPERIMENTAL_ATTACH_RUN_IMAGE_SBOM environment variable is set,
// attach an SBOM layer to the run image
if def.IncludeExperimentalSBOM {
c.logger.Action("Attaching experimental SBOM")
layer, err := c.sbomLayerCreator.Create(run, def.Run, runSBOM)
if err != nil {
return Image{}, Image{}, err
}
run.Labels["io.buildpacks.base.sbom"] = layer.DiffID
run.Layers = append(run.Layers, layer)
}
// update the build and run images in the Docker daemon
c.logger.Subprocess("build: Updating image")
build, err = c.docker.Update(build)
if err != nil {
return Image{}, Image{}, err
}
c.logger.Subprocess("run: Updating image")
run, err = c.docker.Update(run)
if err != nil {
return Image{}, Image{}, err
}
c.logger.Break()
return build, run, nil
}
func (c Creator) mutate(image Image, def Definition, imageDef DefinitionImage, sbom SBOM, intersection, complement []string, now time.Time) (Image, error) {
// add the common CNB labels to the given image
c.logger.Action("Adding io.buildpacks.stack.* labels")
image.Labels["io.buildpacks.stack.id"] = def.ID
image.Labels["io.buildpacks.stack.description"] = imageDef.Description
image.Labels["io.buildpacks.stack.distro.name"] = sbom.Distro.Name
image.Labels["io.buildpacks.stack.distro.version"] = sbom.Distro.Version
image.Labels["io.buildpacks.stack.homepage"] = def.Homepage
image.Labels["io.buildpacks.stack.maintainer"] = def.Maintainer
image.Labels["io.buildpacks.stack.metadata"] = "{}"
image.Labels["io.buildpacks.stack.released"] = now.Format(time.RFC3339)
// if the stack descriptor requests to use the depredated mixins feature,
// include the mixins label as a JSON-encoded list of package names
if def.Deprecated.Mixins {
c.logger.Action("Adding io.buildpacks.stack.mixins label")
var mixins []string
mixins = append(mixins, intersection...)
mixins = append(mixins, complement...)
output, err := json.Marshal(mixins)
if err != nil {
return Image{}, err
}
image.Labels["io.buildpacks.stack.mixins"] = string(output)
}
// if the stack descriptor requests to use the deprecated legacy SBOM
// feature, include the packages label as a JSON-encoded object
if def.Deprecated.LegacySBOM {
c.logger.Action("Adding io.paketo.stack.packages label")
var err error
image.Labels["io.paketo.stack.packages"], err = sbom.LegacyFormat()
if err != nil {
return Image{}, err
}
}
// create and attach a layer that creates a cnb user in the container image
// filesystem
c.logger.Action("Creating cnb user")
layer, err := c.userLayerCreator.Create(image, imageDef, sbom)
if err != nil {
return Image{}, err
}
image.Layers = append(image.Layers, layer)
image.User = fmt.Sprintf("%d:%d", imageDef.UID, imageDef.GID)
return image, nil
}