github.com/GoogleContainerTools/skaffold/v2@v2.13.2/docs-v2/design_proposals/artifact-dependencies.md (about) 1 # Supporting dependencies between build artifacts 2 3 * Author(s): Gaurav Ghosh (@gsquared94) 4 * Design Shepherd: Brian de Alwis (@briandealwis) 5 * Date: 2020-10-01 6 * Status: *Implemented (#4713, #4922)* 7 8 ## Background 9 Refer [this](https://tinyurl.com/skaffold-modules) document which presents the design goals for introducing the concept of _modules_ in Skaffold. A prerequisite to being able to define modules and supporting cross module dependency is to first consider the latest version of the skaffold config (_currently `v2beta8`_) as an implicit module and allowing dependencies between artifacts defined in it. The current document aims to capture the major code changes necessary for achieving this. 10 11 >*Note: Omitted some details for brevity, assuming reader's familiarity with the skaffold [codebase](https://github.com/GoogleContainerTools/skaffold).* 12 13 ## Config schema 14 15 We introduce an `ArtifactDependency` slice within `Artifact` in `config.go` 16 17 ```go 18 type ArtifactDependency struct { 19 ImageName string `yaml:"image" yamltags:"required"` 20 Alias string `yaml:"alias,omitempty"` 21 } 22 ``` 23 24 This allows us to define a build stanza like below where image `leeroy-app` requires image `simple-go-app`: 25 26 ```yaml 27 build: 28 artifacts: 29 - image: leeroy-app 30 requires: 31 - image: simple-go-app 32 alias: BASE 33 - image: simple-go-app 34 35 ``` 36 37 Alias is a token that will be replaced with the image reference in the builder definition files. If no value is provided for `alias` then it defaults to the value of `image`. 38 39 ## Config validation 40 41 We add three new validations to the [validation](https://github.com/GoogleContainerTools/skaffold/blob/10275c66a142719897894308b9e566953712a0fe/pkg/skaffold/schema/validation/validation.go#L37) package after the introduction of artifact dependencies: 42 - Cyclic references among artifacts. 43 - We cannot have image `A` depend on image `B` depend on image `C` depend on image `A`. 44 - We run a simple depth-first-search cycle detection algorithm treating our `Artifact` slice like a directed graph- image `A` depending on image `B` implies a directed edge from `A` to `B`. 45 - Unique artifact aliases. 46 - We ensure that within *each* artifact dependency slice the aliases are unique. 47 - Valid aliases 48 - We validate that `alias`es match the regex `[a-zA-Z_][a-zA-Z0-9_]*` for `ArtifactDependency` defined in `docker` and `custom` builders since these are used as build args and environment variables respectively. 49 50 ## Referencing dependencies 51 52 ### Docker builder 53 54 The `docker` builder will use the `alias` of an `ArtifactDependency` as a build argument key. 55 56 ```yaml 57 build: 58 artifacts: 59 - image: simple-go-app 60 - image: leeroy-app 61 requires: 62 - image: simple-go-app 63 alias: BASE 64 ``` 65 66 Here the alias is used to populate a build arg `BASE=gcr.io/X/simple-go-app:<tag>@sha:<sha>` passed along with a `--build-arg` flag (or a buildKit parameter) when building `leeroy-app`. 67 68 ### Custom builder 69 70 The `custom` builder will be supplied each `ArtifactDependency`'s `alias` and image reference as environment variables keyed on `alias`. So they can be easily referenced in user-defined build definitions. 71 72 ### Buildpacks builder 73 74 Buildpacks supports overriding the run-image and the builder-image in its current schema. We extend this to allow specifying `ArtifactDependency`'s `image` name as the value for the `runImage` and `builder` fields. 75 76 ```yaml 77 build: 78 artifacts: 79 - image: builder-image 80 - image: run-image 81 - image: skaffold-buildpacks 82 buildpacks: 83 builder: builder-image 84 runImage: run-image 85 requires: 86 - image: builder-image 87 - image: run-image 88 ``` 89 90 If there are any additional images in the `required` section it only enforces that they get built prior to the current image. However, the buildpacks builder cannot really reference them in any other way. 91 92 ### Jib builder 93 94 The Jib builder supports [changing the base image](https://cloud.google.com/java/getting-started/jib#base-image). We add a new field `baseImage` to the builder definition that can be set to an `ArtifactDependency`'s `image` field. 95 96 For Maven: 97 98 ```yaml 99 build: 100 artifacts: 101 - image: base-image 102 - image: test-jib-maven 103 jib: 104 type: maven 105 baseImage: base-image 106 requires: 107 - image: base-image 108 ``` 109 110 Similarly, for Gradle: 111 112 ```yaml 113 build: 114 artifacts: 115 - image: base-image 116 - image: test-jib-gradle 117 jib: 118 type: gradle 119 baseImage: base-image 120 requires: 121 - image: base-image 122 ``` 123 124 This will allow Skaffold to override the `jib.from.image` property that sets the base image with a flag like `-Djib.from.image=registry://gcr.io/X/base-image:<tag>@sha:<sha>` 125 126 ### Bazel builder 127 128 The bazel builder doesn't support referencing images directly. Also, it natively supports setting up nested builds. We allow defining `required` artifacts even though they can't be referenced by the builder. We do this to give the user a way of ordering these builds; with the future work around [Skaffold Hooks](https://github.com/GoogleContainerTools/skaffold/issues/1441) there might be some usecases where pre and post build scripts might want a certain ordering of builds. 129 130 ## Builder interfaces 131 There are two builder abstractions -- in [build.go](https://github.com/GoogleContainerTools/skaffold/blob/10275c66a142719897894308b9e566953712a0fe/pkg/skaffold/build/build.go#L37) and [parallel.go](https://github.com/GoogleContainerTools/skaffold/blob/10275c66a142719897894308b9e566953712a0fe/pkg/skaffold/build/parallel.go#L34) 132 133 ```go 134 type Builder interface { 135 Build(ctx context.Context, out io.Writer, tags tag.ImageTags, artifacts []*latest.Artifact) ([]Artifact, error) 136 } 137 ``` 138 ```go 139 type ArtifactBuilder func(ctx context.Context, out io.Writer, artifact *latest.Artifact, tag string) (string, error) 140 ``` 141 The first describes a builder for a list of artifacts. There are a few implementations like `cache`, `local` and `cluster` builders. 142 The second describes a per artifact builder. Again there are a few implementations like `docker`, `buildpacks`, etc. 143 144 We modify both of them to: 145 146 ```go 147 type Builder interface { 148 Build(ctx context.Context, out io.Writer, tags tag.ImageTags, artifacts []*latest.Artifact, existing []Artifact) ([]Artifact, error) 149 } 150 ``` 151 ```go 152 type ArtifactBuilder func(ctx context.Context, out io.Writer, artifact *latest.Artifact, tag string, artifactResolver ArtifactResolver) (string, error) 153 ``` 154 where we define `ArtifactResolver` interface, as: 155 ```go 156 // ArtifactResolver provides an interface to resolve built artifacts by image name. 157 type ArtifactResolver interface { 158 GetImageTag(imageName string) string 159 } 160 ``` 161 162 This necessitates all multi-artifact builder implementations to also accept a slice of already built artifacts' information. This simplifies handling the scenario when some artifacts are either retrievable from cache or do not require a rebuild but are required dependencies for another artifact that needs to be rebuilt (due to cache miss or file changes during a dev loop). 163 All single artifact builders require an `ArtifactResolver` that can provide the required artifacts. This is an optimization over just using `[]Artifact` since we need to retrieve by image name several times during multiple builds. 164 165 ## Build controller 166 167 [InSequence](https://github.com/GoogleContainerTools/skaffold/blob/10275c66a142719897894308b9e566953712a0fe/pkg/skaffold/build/sequence.go) and [InParallel](https://github.com/GoogleContainerTools/skaffold/blob/10275c66a142719897894308b9e566953712a0fe/pkg/skaffold/build/parallel.go) are two build controllers for deciding how to schedule the run of multiple builds together. `InSequence` runs all builds sequentially whereas `InParallel` runs them parallely with a max concurrency defined by a `concurrency` field. 168 169 After introducing inter-artifact dependencies we'll need to run the builds in a topologically sorted order. 170 We introduce a new controller `scheduler.go` and remove `sequence.go` and `parallel.go`. Here we model the `Artifact` slice graph using a set of `go channels` to achieve the topologically sorted build order. 171 172 ```go 173 type status struct { 174 imageName string 175 success chan interface{} 176 failure chan interface{} 177 } 178 179 type artifactChanModel struct { 180 artifact *latest.Artifact 181 artifactStatus status 182 requiredArtifactStatuses []status 183 } 184 185 func (a *artifactChanModel) markSuccess() { 186 // closing channel notifies all listeners waiting for this build that it succeeded 187 close(a.status.success) 188 } 189 190 func (a *artifactChanModel) markFailure() { 191 // closing channel notifies all listeners waiting for this build that it failed 192 close(a.status.failure) 193 } 194 func (a *artifactChanModel) waitForDependencies(ctx context.Context) error { 195 for _, depStatus := range a.requiredArtifactChans { 196 // wait for required builds to complete 197 select { 198 case <-ctx.Done(): 199 return ctx.Err() 200 case <-depStatus.failure: 201 return fmt.Errorf("failed to build required artifact: %q", depStatus.imageName) 202 case <-depStatus.success: 203 } 204 } 205 return nil 206 } 207 ``` 208 209 Each artifact has a success and a failure channel that it closes once it completes building by calling either `markSuccess` or `markFailure` respectively. This notifies *all* listeners waiting for this artifact of a successful or failed build. 210 211 Additionally it has a reference to the channels for each of its dependencies. 212 Calling `waitForDependencies` ensures that all required artifacts' channels have already been closed and as such have finished building before the current artifact build starts. 213 214 > *<ins>Alternative approach</ins>: Another way to do this is to run any popular topologically sorting algorithm on the `Artifact` slice, treating it as a directed graph. However, we can get a simpler implementation at the expense of a few additional `goroutines` the way described above.* 215 216 This class also provides an implementation of the interface `buildStatusRecorder` that should be safe for concurrent access. 217 218 ```go 219 type buildStatusRecorder interface { 220 Record(imageName string, imageTag string, err error) 221 GetImageTag(imageName string) string 222 } 223 ``` 224 225 Finally we have the only exported function in `scheduler.go` that orchestrates all the builds: 226 227 ```go 228 func InOrder(ctx context.Context, out io.Writer, tags tag.ImageTags, artifacts []*latest.Artifact, existing []Artifact, buildArtifact ArtifactBuilder, concurrency int) ([]Artifact, error) 229 ``` 230 231 This function maintains an instance of `buildStatusRecorder` implementation and can pass it as an `ArtifactResolver` to the various `ArtifactBuilder`s while recording the status after each build completion. 232 233 ## Build concurrency 234 235 Skaffold currently allows specifying the `concurrency` property in `build` which affects how many builds can be running at the same time. However it doesn't address the issue of certain builders (`jib` and `bazel`) not being safe for multiple concurrent runs against the same workspace or context. We can fix this also since we are reworking the build controller anyways. 236 237 We define a concept of lease on workspaces by preprocessing the list of artifacts. Each builder tries to acquire a lease on the context/workspace prior to starting the build. Only workspaces associated with concurrency-safe builders allot multiple leases, otherwise it assigns one lease at a time. 238 239 ```go 240 type LeaseProvider interface { 241 Acquire(a *latest.Artifact) (release func(), err error) 242 } 243 244 func NewLeaseProvider(artifacts []latest.Artifact) LeaseProvider 245 ``` 246 247 This integrates with the `InOrder` build controller above. 248 249 ## Build logs reporting 250 251 The code below is the current way the `InParallel` build controller reports the build logs. 252 253 ```go 254 func collectResults(out io.Writer, artifacts []*latest.Artifact, results *sync.Map, outputs []chan string) ([]Artifact, error) { 255 var built []Artifact 256 for i, artifact := range artifacts { 257 // Wait for build to complete. 258 printResult(out, outputs[i]) 259 v, ok := results.Load(artifact.ImageName) 260 if !ok { 261 return nil, fmt.Errorf("could not find build result for image %s", artifact.ImageName) 262 } 263 switch t := v.(type) { 264 case error: 265 return nil, fmt.Errorf("couldn't build %q: %w", artifact.ImageName, t) 266 case Artifact: 267 built = append(built, t) 268 default: 269 return nil, fmt.Errorf("unknown type %T for %s", t, artifact.ImageName) 270 } 271 } 272 return built, nil 273 } 274 ``` 275 276 There are two quirks in this: 277 - It reports in the order of artifacts in the `Artifact` slice instead of the actual order in which they get built. 278 - It only reports a single artifact build failure even though there could have been multiple failures. 279 280 This will prove misleading after the introduction of artifact dependencies since we can have out of order artifact definitions in the skaffold config which with the current reporting strategy would appear to be building in the wrong order, and also build failures due to failed required artifact builds won't be immediately apparant. 281 282 So we introduce a new `BuildLogger` interface as a facade to achieve two things: 283 - Print log messages in the order that it builds. 284 > *<ins>Future work</ins>: This however doesn't solve the problem for concurrently running builds as the current skaffold UX can't show parallel statuses. This will need to be addressed separately when we have a different UX with status bars that can show multiple statuses. 285 Until then, we can limit the max concurrency to 1 (this is what we currently do anyways)* 286 - Report about *all* build failures. 287 288 ## Image cache 289 290 [hash.go](https://github.com/GoogleContainerTools/skaffold/blob/10275c66a142719897894308b9e566953712a0fe/pkg/skaffold/build/cache/hash.go) provides the `getHashForArtifact` function that needs to recursively be called for each of its dependencies and all those values aggregated together would be the hashcode for the artifact. This would ensure that for a cache hit all the cascading dependencies are unchanged. 291 292 > Note: Dependencies provided as environment variables and build args are not resolved yet during hash calculation. That doesn't matter since they are already accounted for above. 293 294 Since `cache` package provides a `Builder` implementation it should additionally append all cache hits to the `existing` `Artifact` slice (see [Builder interfaces](#builder-interfaces) above). 295 296 ## Dev-loop integration 297 298 ### Build 299 300 [dev.go](https://github.com/GoogleContainerTools/skaffold/blob/10275c66a142719897894308b9e566953712a0fe/pkg/skaffold/runner/dev.go#L149) sets up the file monitor callback functions to queue the affected artifact to need rebuild or resync. 301 302 Now we'll have to queue the affected artifact along with all the monitored artifacts that are dependent on it and cascade. To do this we'll need the transpose graph of the `Artifact` slice directed graph that we currently have. One way to implement that would be as follows, which is also safe for concurrent access. 303 304 ```go 305 type artifactDAG struct { 306 m *sync.Map 307 } 308 309 func getArtifactDAG(artifacts []*latest.Artifact) *artifactDAG { 310 dag := &artifactDAG{m: new(sync.Map)} 311 for _, a := range artifacts { 312 for _, d := range a.Dependencies { 313 slice, ok := dag.m.Load(d.ImageName) 314 if !ok { 315 slice = make([]*latest.Artifact, 0) 316 } else { 317 slice = slice.([]*latest.Artifact) 318 } 319 dag.m.Store(d.ImageName, append(slice.([]*latest.Artifact), a)) 320 } 321 } 322 return dag 323 } 324 325 func (dag *artifactDAG) dependents(artifact *latest.Artifact) []*latest.Artifact { 326 slice, ok := dag.m.Load(artifact.ImageName) 327 if !ok { 328 return nil 329 } 330 return slice.([]*latest.Artifact) 331 } 332 ``` 333 334 In `addRebuild` we run a depth-first-search from the target artifact to get its transitive closure in the `artifactDAG` and queue a rebuild for all matching artifacts. 335 336 ```go 337 func addRebuild(dag *artifactDAG, artifact *latest.Artifact, rebuild func(*latest.Artifact), isTarget func(*latest.Artifact) bool) { 338 if isTarget(artifact) { 339 rebuild(artifact) 340 } 341 for _, a := range dag.dependents(artifact) { 342 addRebuild(dag, a, rebuild, isTarget) 343 } 344 } 345 ``` 346 347 Now we can request rebuild for all required artifacts as a callback to the file monitoring event by setting it in the `Dev` [function](https://github.com/GoogleContainerTools/skaffold/blob/10275c66a142719897894308b9e566953712a0fe/pkg/skaffold/runner/dev.go#L161) 348 349 ```diff 350 - r.changeSet.AddRebuild(artifact) 351 + addRebuild(artifactDAG, artifact, r.changeSet.AddRebuild, r.runCtx.Opts.IsTargetImage) 352 353 ``` 354 355 ### Sync 356 357 In this first implementation, we ignore all sync rules in base artifacts. This is because it isn't feasible to propagate sync rules between different builder types. 358 359 We should notify the user that sync rules for a specific artifact are being ignored. 360 ``` 361 Warn: Ignoring sync rules for image "simple-go-app" as it is being used as a required artifact for other images. 362 ``` 363 364 > *<ins>Alternative approach</ins>: We could consider disallowing sync rules altogether in base artifacts. However, the next iteration of this would be supporting individual modules. In that case we would want to support sync rules when the base module runs separately but ignore the rules when run along with its dependents. 365 > As such we prefer to implement ignoring sync rules behavior right now itself.* 366 367 > *<ins>Future work</ins>: We might be able to support propagating manual sync rules from base to derived artifacts. However, that's a lot of complexity to handle, and we can consider it if there is a user ask.*