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.*