vendor: update buildkit to v0.30.0-rc1

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Tonis Tiigi authored and Sebastiaan van Stijn committed 2026-05-07 10:38:00 +02:00
1 parent bb2c5dc629
commit f6319f0cc2
84 files changed
+4394 -1398

No files matched your search

@@ -440,6 +440,7 @@ func newGraphDriverController(ctx context.Context, rt http.RoundTripper, opt Opt
ImageSource: src,
DownloadManager: dist.DownloadManager,
V2MetadataService: dist.V2MetadataService,
RegistryHosts: opt.RegistryHosts,
Exporter: exp,
Transport: rt,
Layers: layers,
@@ -10,6 +10,7 @@ import (
"github.com/containerd/containerd/v2/core/content"
c8dimages "github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/containerd/containerd/v2/pkg/gc"
"github.com/containerd/containerd/v2/pkg/rootfs"
cerrdefs "github.com/containerd/errdefs"
@@ -84,6 +85,7 @@ type Opt struct {
DownloadManager *xfer.LayerDownloadManager
V2MetadataService distmetadata.V2MetadataService
RegistryHosts docker.RegistryHosts
Transport nethttp.RoundTripper
Exporter exporter.Exporter
Layers LayerAccess
@@ -116,6 +118,7 @@ func NewWorker(opt Opt) (*Worker, error) {
gs, err := git.NewSource(git.Opt{
CacheAccessor: cm,
RegistryHosts: opt.RegistryHosts,
})
if err == nil {
sm.Register(gs)
+3 -3
View File
@@ -55,7 +55,7 @@ require (
github.com/miekg/dns v1.1.72
github.com/mistifyio/go-zfs/v4 v4.0.0
github.com/mitchellh/copystructure v1.2.0
github.com/moby/buildkit v0.29.0
github.com/moby/buildkit v0.30.0-rc1
github.com/moby/docker-image-spec v1.3.1
github.com/moby/go-archive v0.2.0
github.com/moby/ipvs v1.1.0
@@ -108,7 +108,7 @@ require (
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
golang.org/x/time v0.15.0
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gotest.tools/v3 v3.5.2
@@ -222,7 +222,7 @@ require (
github.com/in-toto/attestation v1.1.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/knqyf263/go-plugin v0.9.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
+12 -12
View File
@@ -226,8 +226,8 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v29.3.1+incompatible h1:M04FDj2TRehDacrosh7Vlkgc7AuQoWloQkf1PA5hmoI=
github.com/docker/cli v29.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.4.2+incompatible h1:nhxMY4v7wB0QMMc5ppeqV6FBMwzqv0n4t2gogu/R2DQ=
github.com/docker/cli v29.4.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
@@ -505,8 +505,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/knqyf263/go-plugin v0.9.0 h1:CQs2+lOPIlkZVtcb835ZYDEoyyWJWLbSTWeCs0EwTwI=
github.com/knqyf263/go-plugin v0.9.0/go.mod h1:2z5lCO1/pez6qGo8CvCxSlBFSEat4MEp1DrnA+f7w8Q=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -549,8 +549,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/buildkit v0.29.0 h1:wxLEFbCOJntEDjSNNN2YWd8zxltZxT5muDQ0LzpbtpU=
github.com/moby/buildkit v0.29.0/go.mod h1:Dmv2FeDe34t75QuzeU87rBoZpAAkcpT5zeu4hXzmASc=
github.com/moby/buildkit v0.30.0-rc1 h1:meXm1eqqweZ7EPnTbRiU8c4BjgjDKs7rUczHUsU0nJo=
github.com/moby/buildkit v0.30.0-rc1/go.mod h1:4ha5jioeEAREXOpmpEUSQ4bKZON1yEc/co9I57+zCZs=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
@@ -1043,8 +1043,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20260401024825-9d38bb4040a9 h1:w8JYjr7zHemS95YA5FFwk+fUv5tdQU4I8twN9bFdxVU=
google.golang.org/genproto v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:YCEC8W7HTtK7iBv+pI7g7hGAi7qdGB6bQXw3BIYAusM=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
@@ -1090,10 +1090,10 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.77 h1:iQtQTjFUOcTT19fI8sTCzYXsjeVs56et3D8AbKS2Uks=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.77/go.mod h1:oV+IO8kGh0B7TxErbydDe2+BRmi9g/W0CkpVV+QBTJU=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.78 h1:jgqg4gyu2BaYW9L6uzEtGLf8GNREwk/z4UFdwt5F3pE=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.78/go.mod h1:VjuVda6m2qGkpCVfrFkpTGyvkdlZ2N5/yfo89tujlg8=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.78 h1:PC3yNs51cX5LZ7U57a7xielBcoXB3xnV+rXD8V0H0DQ=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.78/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc=
pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk=
resenje.org/singleflight v0.4.3 h1:l7foFYg8X/VEHPxWs1K/Pw77807RMVzvXgWGb0J1sdM=
+1
View File
@@ -1,2 +1,3 @@
* -text
*.bin -text -diff
*.md text eol=lf
+700 -700
View File
File diff suppressed because it is too large. Load diff
+78 -78
View File
@@ -1,79 +1,79 @@
# Finite State Entropy
This package provides Finite State Entropy encoding and decoding.
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
encoding provides a fast near-optimal symbol encoding/decoding
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
## News
* Feb 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `(error)` | An internal error occurred. |
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
# Performance
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
beneficial to transpose all your input values down by 64.
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
around 300MB/s decompression speed.
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
# Plans
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
# Finite State Entropy
This package provides Finite State Entropy encoding and decoding.
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
encoding provides a fast near-optimal symbol encoding/decoding
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
## News
* Feb 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `(error)` | An internal error occurred. |
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
# Performance
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
beneficial to transpose all your input values down by 64.
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
around 300MB/s decompression speed.
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
# Plans
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
+89 -89
View File
@@ -1,89 +1,89 @@
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
## News
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
This ensures that most functionality is well tested.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
## News
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
This ensures that most functionality is well tested.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
+11 -2
View File
@@ -409,6 +409,7 @@ type SolveRequest struct {
Exporters []*Exporter `protobuf:"bytes,13,rep,name=Exporters,proto3" json:"Exporters,omitempty"`
EnableSessionExporter bool `protobuf:"varint,14,opt,name=EnableSessionExporter,proto3" json:"EnableSessionExporter,omitempty"`
SourcePolicySession string `protobuf:"bytes,15,opt,name=SourcePolicySession,proto3" json:"SourcePolicySession,omitempty"`
CompatibilityVersion int64 `protobuf:"varint,16,opt,name=CompatibilityVersion,proto3" json:"CompatibilityVersion,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -548,6 +549,13 @@ func (x *SolveRequest) GetSourcePolicySession() string {
return ""
}
func (x *SolveRequest) GetCompatibilityVersion() int64 {
if x != nil {
return x.CompatibilityVersion
}
return 0
}
type CacheOptions struct {
state protoimpl.MessageState `protogen:"open.v1"`
// ExportRefDeprecated is deprecated in favor or the new Exports since BuildKit v0.4.0.
@@ -2058,7 +2066,7 @@ const file_github_com_moby_buildkit_api_services_control_control_proto_rawDesc =
" \x01(\tR\n" +
"RecordType\x12\x16\n" +
"\x06Shared\x18\v \x01(\bR\x06Shared\x12\x18\n" +
"\aParents\x18\f \x03(\tR\aParents\"\xa6\b\n" +
"\aParents\x18\f \x03(\tR\aParents\"\xda\b\n" +
"\fSolveRequest\x12\x10\n" +
"\x03Ref\x18\x01 \x01(\tR\x03Ref\x12.\n" +
"\n" +
@@ -2077,7 +2085,8 @@ const file_github_com_moby_buildkit_api_services_control_control_proto_rawDesc =
"\fSourcePolicy\x18\f \x01(\v2%.moby.buildkit.v1.sourcepolicy.PolicyR\fSourcePolicy\x128\n" +
"\tExporters\x18\r \x03(\v2\x1a.moby.buildkit.v1.ExporterR\tExporters\x124\n" +
"\x15EnableSessionExporter\x18\x0e \x01(\bR\x15EnableSessionExporter\x120\n" +
"\x13SourcePolicySession\x18\x0f \x01(\tR\x13SourcePolicySession\x1aJ\n" +
"\x13SourcePolicySession\x18\x0f \x01(\tR\x13SourcePolicySession\x122\n" +
"\x14CompatibilityVersion\x18\x10 \x01(\x03R\x14CompatibilityVersion\x1aJ\n" +
"\x1cExporterAttrsDeprecatedEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a@\n" +
+1
View File
@@ -77,6 +77,7 @@ message SolveRequest {
repeated Exporter Exporters = 13;
bool EnableSessionExporter = 14;
string SourcePolicySession = 15;
int64 CompatibilityVersion = 16;
}
message CacheOptions {
@@ -143,6 +143,7 @@ func (m *SolveRequest) CloneVT() *SolveRequest {
r.SourcePolicy = m.SourcePolicy.CloneVT()
r.EnableSessionExporter = m.EnableSessionExporter
r.SourcePolicySession = m.SourcePolicySession
r.CompatibilityVersion = m.CompatibilityVersion
if rhs := m.ExporterAttrsDeprecated; rhs != nil {
tmpContainer := make(map[string]string, len(rhs))
for k, v := range rhs {
@@ -1043,6 +1044,9 @@ func (this *SolveRequest) EqualVT(that *SolveRequest) bool {
if this.SourcePolicySession != that.SourcePolicySession {
return false
}
if this.CompatibilityVersion != that.CompatibilityVersion {
return false
}
return string(this.unknownFields) == string(that.unknownFields)
}
@@ -2249,6 +2253,13 @@ func (m *SolveRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if m.CompatibilityVersion != 0 {
i = protohelpers.EncodeVarint(dAtA, i, uint64(m.CompatibilityVersion))
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0x80
}
if len(m.SourcePolicySession) > 0 {
i -= len(m.SourcePolicySession)
copy(dAtA[i:], m.SourcePolicySession)
@@ -4174,6 +4185,9 @@ func (m *SolveRequest) SizeVT() (n int) {
if l > 0 {
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
if m.CompatibilityVersion != 0 {
n += 2 + protohelpers.SizeOfVarint(uint64(m.CompatibilityVersion))
}
n += len(m.unknownFields)
return n
}
@@ -6326,6 +6340,25 @@ func (m *SolveRequest) UnmarshalVT(dAtA []byte) error {
}
m.SourcePolicySession = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 16:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field CompatibilityVersion", wireType)
}
m.CompatibilityVersion = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.CompatibilityVersion |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+129 -11
View File
@@ -483,6 +483,26 @@ func Git(url, fragment string, opts ...GitOption) State {
addCap(&gi.Constraints, pb.CapSourceGitMTime)
}
if gi.FetchByCommit {
attrs[pb.AttrGitFetchByCommit] = "true"
addCap(&gi.Constraints, pb.CapSourceGitFetchByCommit)
}
if gi.Bundle != "" {
attrs[pb.AttrGitBundle] = gi.Bundle
addCap(&gi.Constraints, pb.CapSourceGitBundle)
}
if gi.BundleOCISessionID != "" {
attrs[pb.AttrOCILayoutSessionID] = gi.BundleOCISessionID
}
if gi.BundleOCIStoreID != "" {
attrs[pb.AttrOCILayoutStoreID] = gi.BundleOCIStoreID
}
if gi.CheckoutBundle {
attrs[pb.AttrGitCheckoutBundle] = "true"
addCap(&gi.Constraints, pb.CapSourceGitCheckoutBundle)
}
addCap(&gi.Constraints, pb.CapSourceGit)
source := NewSource("git://"+id, attrs, gi.Constraints)
@@ -500,17 +520,22 @@ func (fn gitOptionFunc) SetGitOption(gi *GitInfo) {
type GitInfo struct {
constraintsWrapper
KeepGitDir bool
AuthTokenSecret string
AuthHeaderSecret string
addAuthCap bool
KnownSSHHosts string
MountSSHSock string
Checksum string
Ref string
SubDir string
SkipSubmodules bool
MTime string
KeepGitDir bool
AuthTokenSecret string
AuthHeaderSecret string
addAuthCap bool
KnownSSHHosts string
MountSSHSock string
Checksum string
Ref string
SubDir string
SkipSubmodules bool
MTime string
Bundle string
BundleOCISessionID string
BundleOCIStoreID string
CheckoutBundle bool
FetchByCommit bool
}
func GitRef(v string) GitOption {
@@ -577,6 +602,99 @@ func GitChecksum(v string) GitOption {
})
}
// GitFetchByCommit makes the git source trust the provided checksum as the
// commit to fetch, without resolving the ref against the remote. The ref, if
// set, is applied locally after the commit is fetched so that cache keys
// still depend on it. This is useful when the remote ref may have moved
// since the original resolution.
//
// Unqualified ref names are canonicalized to "refs/heads/<name>" to match
// the normal path's cache keys for branches. Tags must be passed fully
// qualified ("refs/tags/<name>").
func GitFetchByCommit() GitOption {
return gitOptionFunc(func(gi *GitInfo) {
gi.FetchByCommit = true
})
}
// GitBundleInfo carries scheme-specific configuration for [GitBundleURL].
// It is populated by [GitBundleOption] values and consumed by GitBundleURL.
type GitBundleInfo struct {
// OCISessionID pins the OCI-layout bundle fetch to a specific client
// session. Only meaningful when the locator uses the
// "oci-layout+blob://" scheme.
OCISessionID string
// OCIStoreID overrides the OCI-layout store name derived from the
// bundle locator body. Only meaningful when the locator uses the
// "oci-layout+blob://" scheme.
OCIStoreID string
}
// GitBundleOption configures bundle-specific behavior for [GitBundleURL].
type GitBundleOption interface {
SetGitBundleOption(*GitBundleInfo)
}
type gitBundleOptionFunc func(*GitBundleInfo)
func (fn gitBundleOptionFunc) SetGitBundleOption(bi *GitBundleInfo) {
fn(bi)
}
// GitBundleURL configures the git source to import the commit history from a
// pre-built git bundle instead of fetching from the upstream Git remote. The
// locator must use one of the following schemes:
//
// docker-image+blob://<registry-ref>@sha256:<digest>
// oci-layout+blob://<ref>@sha256:<digest>
//
// [GitChecksum] is required when GitBundleURL is used and the commit it
// identifies must be reachable from a ref inside the bundle (i.e. the bundle
// must contain that commit; BuildKit validates this after import). Auth and
// SSH options are ignored in bundle mode. Submodules, if present, still fetch
// from their own remotes. See [GitCheckoutBundle] for the checkout side; it
// is mutually exclusive with [KeepGitDir] and [GitSubDir].
//
// Scheme-specific behavior can be tuned via [GitBundleOption] values, e.g.
// [GitBundleOCIStore] for the "oci-layout+blob://" scheme.
func GitBundleURL(locator string, opts ...GitBundleOption) GitOption {
bi := &GitBundleInfo{}
for _, o := range opts {
o.SetGitBundleOption(bi)
}
return gitOptionFunc(func(gi *GitInfo) {
gi.Bundle = locator
gi.BundleOCISessionID = bi.OCISessionID
gi.BundleOCIStoreID = bi.OCIStoreID
})
}
// GitBundleOCIStore configures the OCI-layout session and store id used to
// resolve a bundle locator with the "oci-layout+blob://" scheme. When unset,
// the locator body (the part before "@digest") is used as the store id and no
// session is pinned. This mirrors [ImageBlobOCIStore] for regular OCI-layout
// blobs.
//
// Only meaningful when passed to [GitBundleURL] together with a locator that
// uses the "oci-layout+blob://" scheme.
func GitBundleOCIStore(sessionID, storeID string) GitBundleOption {
return gitBundleOptionFunc(func(bi *GitBundleInfo) {
bi.OCISessionID = sessionID
bi.OCIStoreID = storeID
})
}
// GitCheckoutBundle produces a single-file git bundle at the checkout mount
// root (filename "bundle") containing the resolved commit, instead of a
// worktree. Mutually exclusive with [KeepGitDir] and [GitSubDir]. Submodules
// are not included in the bundle. Pair with [GitBundleURL] to re-import the
// bundle back into a later build.
func GitCheckoutBundle() GitOption {
return gitOptionFunc(func(gi *GitInfo) {
gi.CheckoutBundle = true
})
}
// AuthOption can be used with either HTTP or Git sources.
type AuthOption interface {
GitOption
+3 -23
View File
@@ -36,8 +36,8 @@ import (
type SolveOpt struct {
Exports []ExportEntry
CompatibilityVersion int
EnableSessionExporter bool
LocalDirs map[string]string // Deprecated: use LocalMounts
LocalMounts map[string]fsutil.FS
OCIStores map[string]content.Store
SharedKey string
@@ -95,11 +95,7 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
return nil, errors.New("invalid with def and cb")
}
mounts, err := prepareMounts(&opt)
if err != nil {
return nil, err
}
syncedDirs, err := prepareSyncedFiles(def, mounts)
syncedDirs, err := prepareSyncedFiles(def, opt.LocalMounts)
if err != nil {
return nil, err
}
@@ -311,6 +307,7 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
Cache: &cacheOpt.options,
Entitlements: slices.Clone(opt.AllowedEntitlements),
Internal: opt.Internal,
CompatibilityVersion: int64(opt.CompatibilityVersion),
SourcePolicy: opt.SourcePolicy,
}
if opt.SourcePolicyProvider != nil {
@@ -586,20 +583,3 @@ func parseCacheOptions(ctx context.Context, isGateway bool, opt SolveOpt) (*cach
}
return &res, nil
}
func prepareMounts(opt *SolveOpt) (map[string]fsutil.FS, error) {
// merge local mounts and fallback local directories together
mounts := make(map[string]fsutil.FS)
maps.Copy(mounts, opt.LocalMounts)
for k, dir := range opt.LocalDirs {
mount, err := fsutil.NewFS(dir)
if err != nil {
return nil, err
}
if _, ok := mounts[k]; ok {
return nil, errors.Errorf("local mount %s already exists", k)
}
mounts[k] = mount
}
return mounts, nil
}
+4
View File
@@ -7,7 +7,10 @@ import (
// Config provides containerd configuration data for the server
type Config struct {
// Deprecated: Use Log.Level with "debug" set instead.
Debug bool `toml:"debug"`
// Deprecated: Use Log.Level with "trace" set instead.
Trace bool `toml:"trace"`
// Root is the path to a directory where buildkit will store persistent data
@@ -63,6 +66,7 @@ type SystemConfig struct {
type LogConfig struct {
Format string `toml:"format"`
Level string `toml:"level"`
}
type GRPCConfig struct {
+10 -1
View File
@@ -34,6 +34,7 @@ import (
"github.com/moby/buildkit/solver/bboltcachestorage"
"github.com/moby/buildkit/solver/llbsolver"
"github.com/moby/buildkit/solver/llbsolver/cdidevices"
"github.com/moby/buildkit/solver/llbsolver/compat"
"github.com/moby/buildkit/solver/llbsolver/history"
"github.com/moby/buildkit/solver/llbsolver/proc"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
@@ -389,6 +390,14 @@ func (c *Controller) Solve(ctx context.Context, req *controlapi.SolveRequest) (*
}
translateLegacySolveRequest(req)
compatibilityVersion := int(req.CompatibilityVersion)
if compatibilityVersion == 0 {
compatibilityVersion = compat.CompatibilityVersionCurrent
}
if err := compat.ValidateCompatibilityVersion(compatibilityVersion); err != nil {
return nil, err
}
defer func() {
time.AfterFunc(time.Second, c.throttledGC)
}()
@@ -538,7 +547,7 @@ func (c *Controller) Solve(ctx context.Context, req *controlapi.SolveRequest) (*
FrontendOpt: req.FrontendAttrs,
FrontendInputs: req.FrontendInputs,
CacheImports: cacheImports,
}, llbsolver.ExporterRequest{
}, compatibilityVersion, llbsolver.ExporterRequest{
Exporters: expis,
CacheExporters: cacheExporters,
EnableSessionExporter: req.EnableSessionExporter,
+13 -50
View File
@@ -2,55 +2,36 @@ package gateway
import (
"context"
"sync"
"time"
"github.com/moby/buildkit/client/buildid"
"github.com/moby/buildkit/frontend/gateway"
gwapi "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/util/registrar"
"github.com/pkg/errors"
"google.golang.org/grpc"
)
type GatewayForwarder struct {
mu sync.RWMutex
updateCond *sync.Cond
builds map[string]gateway.LLBBridgeForwarder
registrar *registrar.Registrar[string, gateway.LLBBridgeForwarder]
}
func NewGatewayForwarder() *GatewayForwarder {
gwf := &GatewayForwarder{
builds: map[string]gateway.LLBBridgeForwarder{},
return &GatewayForwarder{
registrar: registrar.New[string, gateway.LLBBridgeForwarder](),
}
gwf.updateCond = sync.NewCond(gwf.mu.RLocker())
return gwf
}
func (gwf *GatewayForwarder) Register(server *grpc.Server) {
gwapi.RegisterLLBBridgeServer(server, gwf)
}
func (gwf *GatewayForwarder) RegisterBuild(ctx context.Context, id string, bridge gateway.LLBBridgeForwarder) error {
gwf.mu.Lock()
defer gwf.mu.Unlock()
if _, ok := gwf.builds[id]; ok {
return errors.Errorf("build ID %s exists", id)
}
gwf.builds[id] = bridge
gwf.updateCond.Broadcast()
return nil
func (gwf *GatewayForwarder) RegisterBuild(ctx context.Context, id string, bridge gateway.LLBBridgeForwarder) {
gwf.registrar.Register(id, bridge)
}
func (gwf *GatewayForwarder) UnregisterBuild(ctx context.Context, id string) {
gwf.mu.Lock()
defer gwf.mu.Unlock()
delete(gwf.builds, id)
gwf.updateCond.Broadcast()
gwf.registrar.Discard(id)
}
func (gwf *GatewayForwarder) lookupForwarder(ctx context.Context) (gateway.LLBBridgeForwarder, error) {
@@ -59,32 +40,14 @@ func (gwf *GatewayForwarder) lookupForwarder(ctx context.Context) (gateway.LLBBr
return nil, errors.New("no buildid found in context")
}
ctx, cancel := context.WithCancelCause(ctx)
ctx, _ = context.WithTimeoutCause(ctx, 3*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet
defer func() { cancel(errors.WithStack(context.Canceled)) }()
go func() {
<-ctx.Done()
gwf.mu.Lock()
gwf.updateCond.Broadcast()
gwf.mu.Unlock()
}()
gwf.mu.RLock()
defer gwf.mu.RUnlock()
for {
select {
case <-ctx.Done():
return nil, errdefs.NewUnknownJobError(bid)
default:
fwd, err := gwf.registrar.Get(ctx, bid)
if err != nil {
if errors.Is(err, context.Canceled) {
return nil, errors.WithStack(errdefs.NewUnknownJobError(bid))
}
fwd, ok := gwf.builds[bid]
if !ok {
gwf.updateCond.Wait()
continue
}
return fwd, nil
return nil, err
}
return fwd, nil
}
func (gwf *GatewayForwarder) ResolveImageConfig(ctx context.Context, req *gwapi.ResolveImageConfigRequest) (*gwapi.ResolveImageConfigResponse, error) {
+2 -2
View File
@@ -20,7 +20,7 @@ import (
"github.com/moby/buildkit/util/network"
rootlessmountopts "github.com/moby/buildkit/util/rootless/mountopts"
"github.com/moby/buildkit/util/system"
traceexec "github.com/moby/buildkit/util/tracing/exec"
"github.com/moby/buildkit/util/tracing/childprocess"
"github.com/moby/sys/user"
"github.com/moby/sys/userns"
specs "github.com/opencontainers/runtime-spec/specs-go"
@@ -120,7 +120,7 @@ func GenerateSpec(ctx context.Context, meta executor.Meta, mounts []executor.Mou
if tracingSocket != "" {
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md
meta.Env = append(meta.Env, tracingEnvVars...)
meta.Env = append(meta.Env, traceexec.Environ(ctx)...)
meta.Env = append(meta.Env, childprocess.Environ(ctx)...)
}
opts = append(opts,
+1 -1
View File
@@ -246,7 +246,7 @@ func (e *imageExporterInstance) Export(ctx context.Context, src *exporter.Source
}
}()
desc, err := e.opt.ImageWriter.Commit(ctx, src, buildInfo.SessionID, buildInfo.InlineCache, &opts)
desc, err := e.opt.ImageWriter.Commit(ctx, src, buildInfo.SessionID, buildInfo.InlineCache, &opts, buildInfo.CompatibilityVersion, e.Type())
if err != nil {
return nil, nil, nil, err
}
+7 -1
View File
@@ -25,6 +25,8 @@ import (
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver"
solvererrdefs "github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/solver/llbsolver/compat"
"github.com/moby/buildkit/solver/result"
attestationTypes "github.com/moby/buildkit/util/attestation"
"github.com/moby/buildkit/util/bklog"
@@ -63,10 +65,14 @@ type ImageWriter struct {
opt WriterOpt
}
func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, sessionID string, inlineCache exptypes.InlineCache, opts *ImageCommitOpts) (*ocispecs.Descriptor, error) {
func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, sessionID string, inlineCache exptypes.InlineCache, opts *ImageCommitOpts, compatibilityVersion int, exporterType string) (*ocispecs.Descriptor, error) {
if _, ok := inp.Metadata[exptypes.ExporterPlatformsKey]; len(inp.Refs) > 0 && !ok {
return nil, errors.Errorf("unable to export multiple refs, missing platforms mapping")
}
if compatibilityVersion == compat.CompatibilityVersion013 && opts.RefCfg.Compression.Type == compression.Zstd {
feature := fmt.Sprintf("%s exporter compression=%s", exporterType, opts.RefCfg.Compression.Type.String())
return nil, solvererrdefs.NewUnsupportedCompatibilityFeatureError(compatibilityVersion, feature)
}
isMap := len(inp.Refs) > 0
+4 -3
View File
@@ -50,9 +50,10 @@ type ExporterInstance interface {
}
type ExportBuildInfo struct {
Ref string
InlineCache exptypes.InlineCache
SessionID string
Ref string
InlineCache exptypes.InlineCache
SessionID string
CompatibilityVersion int
}
type DescriptorReference interface {
+1 -1
View File
@@ -159,7 +159,7 @@ func (e *imageExporterInstance) Export(ctx context.Context, src *exporter.Source
}
}()
desc, err := e.opt.ImageWriter.Commit(ctx, src, buildInfo.SessionID, buildInfo.InlineCache, &opts)
desc, err := e.opt.ImageWriter.Commit(ctx, src, buildInfo.SessionID, buildInfo.InlineCache, &opts, buildInfo.CompatibilityVersion, e.Type())
if err != nil {
return nil, nil, nil, err
}
+7 -1
View File
@@ -21,7 +21,13 @@ type Epoch struct {
func ParseBuildArgs(opt map[string]string) (string, bool) {
v, ok := opt[frontendSourceDateEpochArg]
return v, ok
if !ok {
return "", false
}
if _, err := parseTime(frontendSourceDateEpochArg, v); err != nil {
return "", false
}
return v, true
}
func ParseExporterAttrs(opt map[string]string) (*Epoch, map[string]string, error) {
+2
View File
@@ -2,6 +2,7 @@ package builder
import (
"context"
"maps"
"strings"
"github.com/containerd/platforms"
@@ -131,6 +132,7 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) {
rb, err := bc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (*dockerui.BuildResult, error) {
opt := convertOpt
opt.BuildArgs = maps.Clone(opt.BuildArgs)
opt.TargetPlatform = platform
if idx != 0 {
opt.Warn = nil
+29 -4
View File
@@ -3,6 +3,7 @@ package dfgitutil
import (
"net/url"
"path"
"strconv"
"strings"
@@ -59,6 +60,11 @@ type GitRef struct {
// MTime controls file modification time policy: "checkout" (default) or "commit".
MTime string
// FetchByCommit, when true, trusts Checksum as the commit and skips comparing
// it against the remote ref. The commit is fetched directly and the ref name
// (if any) is applied locally.
FetchByCommit bool
}
// ParseGitRef parses a git ref.
@@ -134,7 +140,7 @@ func (gf *GitRef) loadQuery(query url.Values) error {
case 0, 1:
if len(v) == 0 || v[0] == "" {
switch k {
case "submodules", "keep-git-dir":
case "submodules", "keep-git-dir", "fetch-by-commit":
v = nil
default:
return errors.Errorf("query %q has no value", k)
@@ -192,6 +198,18 @@ func (gf *GitRef) loadQuery(query url.Values) error {
default:
return errors.Errorf("invalid mtime value: %q (must be \"checkout\" or \"commit\")", v[0])
}
case "fetch-by-commit":
var vv bool
if len(v) == 0 {
vv = true
} else {
var err error
vv, err = strconv.ParseBool(v[0])
if err != nil {
return errors.Errorf("invalid fetch-by-commit value: %q", v[0])
}
}
gf.FetchByCommit = vv
default:
return errors.Errorf("unexpected query %q", k)
}
@@ -223,16 +241,23 @@ func (gf *GitRef) loadQuery(query url.Values) error {
return nil
}
// FragmentFormat returns a simplified git URL in fragment format with only ref.
// FragmentFormat returns a simplified git URL in fragment format.
// If the URL cannot be parsed, the original string is returned with false.
func FragmentFormat(remote string) (string, bool) {
func FragmentFormat(remote string, withSubdir bool) (string, bool) {
gitRef, _, err := ParseGitRef(remote)
if err != nil || gitRef == nil {
return remote, false
}
u := gitRef.Remote
if gitRef.Ref != "" {
subdir := ""
if withSubdir {
subdir = strings.TrimPrefix(path.Join("/", gitRef.SubDir), "/")
}
if gitRef.Ref != "" || subdir != "" {
u += "#" + gitRef.Ref
if subdir != "" {
u += ":" + subdir
}
}
return u, true
}
@@ -208,6 +208,7 @@ type dispatchContext struct {
opt ConvertOpt
platformOpt *platformOpt
globalArgs *llb.EnvList
epoch *time.Time
shlex *shell.Lex
outline outlineCapture
lint *linter.Linter
@@ -300,9 +301,13 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
return nil, err
}
opt.Epoch, err = resolveSourceDateEpoch(opt.Epoch, globalArgs)
if err != nil {
return nil, err
var resolvedEpoch *time.Time
if sourceDateEpoch, ok := getBuildArgValue(opt.BuildArgs, globalArgs, "SOURCE_DATE_EPOCH"); ok {
resolvedEpoch, err = resolveSourceDateEpochValue(ctx, sourceDateEpoch, opt, stages, globalArgs, shlex)
if err != nil {
return nil, err
}
globalArgs = setBuildArgValue(opt.BuildArgs, globalArgs, "SOURCE_DATE_EPOCH", formatSourceDateEpochValue(resolvedEpoch))
}
metaResolver := opt.MetaResolver
@@ -314,6 +319,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
opt: opt,
platformOpt: platformOpt,
globalArgs: globalArgs,
epoch: resolvedEpoch,
shlex: shlex,
outline: outline,
lint: lint,
@@ -353,25 +359,39 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
return target, nil
}
func resolveSourceDateEpoch(explicit *time.Time, globalArgs *llb.EnvList) (*time.Time, error) {
if explicit != nil {
return explicit, nil
func getBuildArgValue(buildArgs map[string]string, globalArgs *llb.EnvList, key string) (string, bool) {
if v, ok := buildArgs[key]; ok {
return v, true
}
if globalArgs == nil {
return nil, nil
return "", false
}
v, ok := globalArgs.Get("SOURCE_DATE_EPOCH")
v, ok := globalArgs.Get(key)
if !ok || v == "" {
return nil, nil
return "", false
}
return v, true
}
sde, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "invalid SOURCE_DATE_EPOCH: %s", v)
func setBuildArgValue(buildArgs map[string]string, globalArgs *llb.EnvList, key, value string) *llb.EnvList {
if _, ok := buildArgs[key]; ok {
if value == "" {
delete(buildArgs, key)
} else {
buildArgs[key] = value
}
}
tm := time.Unix(sde, 0).UTC()
return &tm, nil
if globalArgs != nil {
if _, ok := globalArgs.Get(key); ok {
if value == "" {
updated := globalArgs.Delete(key)
globalArgs = &updated
} else {
globalArgs = globalArgs.AddOrReplace(key, value)
}
}
}
return globalArgs
}
func (dctx *dispatchContext) buildDispatchStates(stages []instructions.Stage) error {
@@ -402,7 +422,7 @@ func (dctx *dispatchContext) buildDispatchStates(stages []instructions.Stage) er
stageName: st.Name,
prefixPlatform: dctx.opt.MultiPlatformRequested,
outline: dctx.outline.clone(),
epoch: dctx.opt.Epoch,
epoch: dctx.epoch,
}
if v := st.Platform; v != "" {
@@ -1598,7 +1618,7 @@ func dispatchHealthcheck(d *dispatchState, c *instructions.HealthCheckCommand, l
StartInterval: c.Health.StartInterval,
Retries: c.Health.Retries,
}
return commitToHistory(&d.image, fmt.Sprintf("HEALTHCHECK %q", d.image.Config.Healthcheck), false, nil, d.epoch)
return commitToHistory(&d.image, fmt.Sprintf("HEALTHCHECK %+v", *d.image.Config.Healthcheck), false, nil, d.epoch)
}
func dispatchUser(d *dispatchState, c *instructions.UserCommand, commit bool) error {
@@ -167,6 +167,9 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error {
if gitRef.Submodules != nil && !*gitRef.Submodules {
gitOptions = append(gitOptions, llb.GitSkipSubmodules())
}
if gitRef.FetchByCommit {
gitOptions = append(gitOptions, llb.GitFetchByCommit())
}
st := llb.Git(gitRef.Remote, "", gitOptions...)
opts := append([]llb.CopyOption{&llb.CopyInfo{
@@ -0,0 +1,381 @@
package dockerfile2llb
import (
"archive/tar"
"bytes"
"context"
"io"
"maps"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/llb/sourceresolver"
"github.com/moby/buildkit/frontend/dockerfile/dfgitutil"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/moby/buildkit/frontend/dockerfile/shell"
"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/gitutil/gitobject"
archivecompression "github.com/moby/go-archive/compression"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
type sourceDateEpochStateOpt struct {
LogName string
}
func resolveSourceDateEpochValue(ctx context.Context, v string, opt ConvertOpt, stages []instructions.Stage, globalArgs *llb.EnvList, shlex *shell.Lex) (*time.Time, error) {
if v == "" {
return nil, nil
}
if sde, err := strconv.ParseInt(v, 10, 64); err == nil {
tm := time.Unix(sde, 0).UTC()
return &tm, nil
}
state, stateOpt, err := resolveSourceDateEpochState(ctx, v, opt, stages, globalArgs, shlex)
if err != nil {
return nil, err
}
if state == nil || opt.Client == nil {
return nil, nil
}
return resolveSourceDateEpochFromState(ctx, *state, opt.Client, stateOpt)
}
func formatSourceDateEpochValue(tm *time.Time) string {
if tm == nil {
return ""
}
return strconv.FormatInt(tm.Unix(), 10)
}
func resolveSourceDateEpochState(ctx context.Context, value string, opt ConvertOpt, stages []instructions.Stage, globalArgs *llb.EnvList, shlex *shell.Lex) (*llb.State, sourceDateEpochStateOpt, error) {
if value == "context" {
if opt.Client == nil {
return nil, sourceDateEpochStateOpt{}, nil
}
mainContextState, err := opt.Client.MainContext(ctx)
if err != nil {
return nil, sourceDateEpochStateOpt{}, err
}
return mainContextState, sourceDateEpochStateOpt{
LogName: "[internal] resolve main build context metadata",
}, nil
}
if opt.Client != nil {
nc, err := opt.Client.NamedContext(value, dockerui.ContextOpt{})
if err != nil {
return nil, sourceDateEpochStateOpt{}, err
}
if nc != nil {
st, _, err := nc.Load(ctx)
if err != nil {
return nil, sourceDateEpochStateOpt{}, err
}
return st, sourceDateEpochStateOpt{
LogName: "[internal] resolve SOURCE_DATE_EPOCH named context " + value,
}, nil
}
}
for i := range stages {
if !strings.EqualFold(stages[i].Name, value) {
continue
}
args := globalArgs
if globalArgs != nil {
updated := globalArgs.Delete("SOURCE_DATE_EPOCH")
args = &updated
}
sourceState, err := sourceDateEpochStageSource(stages[i], opt.BuildArgs, args, shlex)
if err != nil {
return nil, sourceDateEpochStateOpt{}, parser.WithLocation(err, stages[i].Location)
}
return sourceState, sourceDateEpochStateOpt{
LogName: "[internal] resolve SOURCE_DATE_EPOCH source stage " + stages[i].Name,
}, nil
}
return nil, sourceDateEpochStateOpt{}, errors.Errorf("invalid SOURCE_DATE_EPOCH: %s", value)
}
func sourceDateEpochStageSource(stage instructions.Stage, buildArgs map[string]string, globalArgs *llb.EnvList, shlex *shell.Lex) (*llb.State, error) {
stageBaseName, _, err := shlex.ProcessWord(stage.BaseName, globalArgs)
if err != nil {
return nil, errors.Wrapf(err, "failed to process source stage base name %q", stage.BaseName)
}
if stageBaseName != emptyImageName {
return nil, errors.New("SOURCE_DATE_EPOCH stage must use FROM scratch")
}
env := globalArgs
var sourceState *llb.State
for _, cmd := range stage.Commands {
switch c := cmd.(type) {
case *instructions.ArgCommand:
env, err = applySourceDateEpochStageArgs(c.Args, env, buildArgs, shlex)
if err != nil {
return nil, err
}
case *instructions.AddCommand:
if sourceState != nil {
return nil, errors.New("SOURCE_DATE_EPOCH stage must contain exactly one remote ADD")
}
sourceState, err = sourceDateEpochAddSource(c, env, shlex)
if err != nil {
return nil, err
}
default:
return nil, errors.Errorf("SOURCE_DATE_EPOCH stage does not meet source-only requirements: unsupported %s instruction", cmd.Name())
}
}
if sourceState == nil {
return nil, errors.New("SOURCE_DATE_EPOCH stage must contain exactly one remote ADD")
}
return sourceState, nil
}
func applySourceDateEpochStageArgs(args []instructions.KeyValuePairOptional, env *llb.EnvList, buildArgs map[string]string, shlex *shell.Lex) (*llb.EnvList, error) {
for _, arg := range args {
if v, ok := buildArgs[arg.Key]; ok {
env = env.AddOrReplace(arg.Key, v)
continue
}
if arg.Value == nil {
continue
}
v, _, err := shlex.ProcessWord(*arg.Value, env)
if err != nil {
return nil, err
}
env = env.AddOrReplace(arg.Key, v)
}
return env, nil
}
func sourceDateEpochAddSource(cmd *instructions.AddCommand, env *llb.EnvList, shlex *shell.Lex) (*llb.State, error) {
if len(cmd.SourceContents) != 0 || len(cmd.SourcePaths) != 1 {
return nil, errors.New("SOURCE_DATE_EPOCH stage must contain exactly one remote ADD source")
}
src, _, err := shlex.ProcessWord(cmd.SourcePaths[0], env)
if err != nil {
return nil, err
}
if isHTTPSource(src) {
var checksum digest.Digest
if cmd.Checksum != "" {
expandedChecksum, _, err := shlex.ProcessWord(cmd.Checksum, env)
if err != nil {
return nil, err
}
checksum, err = digest.Parse(expandedChecksum)
if err != nil {
return nil, err
}
}
st := llb.HTTP(src, llb.Filename(sourceDateEpochHTTPFilename(src)), llb.Checksum(checksum))
return &st, nil
}
gitRef, isGit, gitRefErr := dfgitutil.ParseGitRef(src)
if gitRefErr != nil && isGit {
return nil, gitRefErr
}
if gitRefErr == nil && !gitRef.IndistinguishableFromLocal {
gitOptions := []llb.GitOption{
llb.GitRef(gitRef.Ref),
}
if cmd.KeepGitDir != nil && *cmd.KeepGitDir {
gitOptions = append(gitOptions, llb.KeepGitDir())
}
if gitRef.KeepGitDir != nil && *gitRef.KeepGitDir {
gitOptions = append(gitOptions, llb.KeepGitDir())
}
if cmd.Checksum != "" {
expandedChecksum, _, err := shlex.ProcessWord(cmd.Checksum, env)
if err != nil {
return nil, err
}
gitOptions = append(gitOptions, llb.GitChecksum(expandedChecksum))
} else if gitRef.Checksum != "" {
gitOptions = append(gitOptions, llb.GitChecksum(gitRef.Checksum))
}
if gitRef.SubDir != "" {
gitOptions = append(gitOptions, llb.GitSubDir(gitRef.SubDir))
}
if gitRef.Submodules != nil && !*gitRef.Submodules {
gitOptions = append(gitOptions, llb.GitSkipSubmodules())
}
st := llb.Git(gitRef.Remote, "", gitOptions...)
return &st, nil
}
return nil, errors.New("SOURCE_DATE_EPOCH stage source must be a single HTTP(S) or Git ADD")
}
func sourceDateEpochHTTPFilename(src string) string {
u, err := url.Parse(src)
if err == nil {
if base := path.Base(u.Path); base != "." && base != "/" {
return base
}
}
return "__unnamed__"
}
func resolveSourceDateEpochFromState(ctx context.Context, st llb.State, client *dockerui.Client, opt sourceDateEpochStateOpt) (*time.Time, error) {
sourceOp, err := sourceOpFromState(ctx, &st, llb.WithCaps(client.BuildOpts().Caps))
if err != nil {
return nil, err
}
if sourceOp == nil {
return nil, nil
}
metaOpt := sourceresolver.Opt{
LogName: opt.LogName,
}
if strings.HasPrefix(sourceOp.Identifier, "git://") {
metaOpt.GitOpt = &sourceresolver.ResolveGitOpt{ReturnObject: true}
}
isHTTP := strings.HasPrefix(sourceOp.Identifier, "http://") || strings.HasPrefix(sourceOp.Identifier, "https://")
md, err := client.GatewayClient().ResolveSourceMetadata(ctx, sourceOp, metaOpt)
if err != nil {
return nil, err
}
if tm, ok, err := sourceDateEpochFromMetadata(md); ok || err != nil {
return tm, err
}
filename := sourceOp.Attrs[pb.AttrHTTPFilename]
if !isHTTP || filename == "" {
return nil, nil
}
sourceState := llb.NewState(llb.NewSource(sourceOp.Identifier, maps.Clone(sourceOp.Attrs), llb.Constraints{}).Output())
def, err := sourceState.Marshal(ctx, llb.WithCaps(client.BuildOpts().Caps))
if err != nil {
return nil, err
}
res, err := client.GatewayClient().Solve(ctx, gwclient.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
return archiveMaxTimeFromRef(ctx, ref, filename, true)
}
func sourceOpFromState(ctx context.Context, st *llb.State, opts ...llb.ConstraintsOpt) (*pb.SourceOp, error) {
if st == nil {
return nil, nil
}
def, err := st.Marshal(ctx, opts...)
if err != nil {
return nil, err
}
dt := def.ToPB().Def
var src *pb.SourceOp
for _, d := range dt {
var op pb.Op
if err := op.Unmarshal(d); err != nil {
return nil, err
}
opSrc := op.GetSource()
if opSrc == nil {
continue
}
if src != nil {
return nil, nil
}
src = opSrc
}
return cloneSourceOp(src), nil
}
func cloneSourceOp(op *pb.SourceOp) *pb.SourceOp {
if op == nil {
return nil
}
return &pb.SourceOp{
Identifier: op.Identifier,
Attrs: maps.Clone(op.Attrs),
}
}
func sourceDateEpochFromMetadata(md *sourceresolver.MetaResponse) (*time.Time, bool, error) {
if md.Git != nil && len(md.Git.CommitObject) > 0 {
obj, err := gitobject.Parse(md.Git.CommitObject)
if err != nil {
return nil, false, err
}
commit, err := obj.ToCommit()
if err != nil {
return nil, false, err
}
return commit.Committer.When, true, nil
}
if md.HTTP != nil && md.HTTP.LastModified != nil {
return md.HTTP.LastModified, true, nil
}
return nil, false, nil
}
func archiveMaxTimeFromRef(ctx context.Context, ref gwclient.Reference, filename string, allowNonArchive bool) (*time.Time, error) {
dt, err := ref.ReadFile(ctx, gwclient.ReadRequest{
Filename: filename,
})
if err != nil {
return nil, err
}
rc, err := archivecompression.DecompressStream(bytes.NewReader(dt))
if err != nil {
if allowNonArchive {
return nil, nil
}
return nil, err
}
defer rc.Close()
tr := tar.NewReader(rc)
var maxTime *time.Time
for {
hdr, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
return maxTime, nil
}
if allowNonArchive {
return nil, nil
}
return nil, err
}
if !hdr.FileInfo().Mode().IsRegular() {
continue
}
tm := hdr.ModTime.UTC()
if maxTime == nil || tm.After(*maxTime) {
maxTime = &tm
}
}
}
@@ -1,6 +1,7 @@
package dockerfile2llb
import (
"maps"
"slices"
"github.com/moby/buildkit/util/system"
@@ -15,6 +16,16 @@ func clone(src dockerspec.DockerOCIImage) dockerspec.DockerOCIImage {
img.Config.Cmd = slices.Clone(src.Config.Cmd)
img.Config.Entrypoint = slices.Clone(src.Config.Entrypoint)
img.Config.OnBuild = slices.Clone(src.Config.OnBuild)
img.Config.ExposedPorts = maps.Clone(src.Config.ExposedPorts)
img.Config.Volumes = maps.Clone(src.Config.Volumes)
img.Config.Labels = maps.Clone(src.Config.Labels)
img.Config.Shell = slices.Clone(src.Config.Shell)
if src.Config.Healthcheck != nil {
hc := *src.Config.Healthcheck
hc.Test = slices.Clone(src.Config.Healthcheck.Test)
img.Config.Healthcheck = &hc
}
img.History = slices.Clone(src.History)
return img
}
-13
View File
@@ -4,7 +4,6 @@ import (
"net"
"strconv"
"strings"
"time"
"github.com/containerd/platforms"
"github.com/docker/go-units"
@@ -113,18 +112,6 @@ func parseNetMode(v string) (pb.NetMode, error) {
}
}
func parseSourceDateEpoch(v string) (*time.Time, error) {
if v == "" {
return nil, nil
}
sde, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "invalid SOURCE_DATE_EPOCH: %s", v)
}
tm := time.Unix(sde, 0).UTC()
return &tm, nil
}
func parseLocalSessionIDs(opt map[string]string) map[string]string {
m := map[string]string{}
for k, v := range opt {
+4 -9
View File
@@ -8,7 +8,6 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/containerd/platforms"
"github.com/distribution/reference"
@@ -51,14 +50,12 @@ const (
keyHostnameArg = "build-arg:BUILDKIT_SANDBOX_HOSTNAME"
keyDockerfileLintArg = "build-arg:BUILDKIT_DOCKERFILE_CHECK"
keyContextKeepGitDirArg = "build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR"
keySourceDateEpoch = "build-arg:SOURCE_DATE_EPOCH"
)
type Config struct {
BuildArgs map[string]string
CacheIDNamespace string
CgroupParent string
Epoch *time.Time
ExtraHosts []llb.HostIP
Hostname string
ImageResolveMode llb.ResolveMode
@@ -147,6 +144,10 @@ func (bc *Client) BuildOpts() client.BuildOpts {
return bc.bopts
}
func (bc *Client) GatewayClient() client.Client {
return bc.client
}
func (bc *Client) init() error {
opts := bc.bopts.Opts
@@ -251,12 +252,6 @@ func (bc *Client) init() error {
}
bc.CacheImports = cacheImports
epoch, err := parseSourceDateEpoch(opts[keySourceDateEpoch])
if err != nil {
return err
}
bc.Epoch = epoch
attests, err := attestations.Parse(opts)
if err != nil {
return err
+146 -1
View File
@@ -4,15 +4,23 @@ import (
"archive/tar"
"bytes"
"context"
"io"
"maps"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"time"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/llb/sourceresolver"
"github.com/moby/buildkit/frontend/dockerfile/dfgitutil"
"github.com/moby/buildkit/frontend/gateway/client"
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/gitutil/gitobject"
archivecompression "github.com/moby/go-archive/compression"
"github.com/pkg/errors"
)
@@ -36,10 +44,14 @@ var httpPrefix = regexp.MustCompile(`^https?://`)
type buildContext struct {
context *llb.State // set if not local
dockerfile *llb.State // override remoteContext if set
contextRef client.Reference
contextLocalName string
dockerfileLocalName string
filename string
forceLocalDockerfile bool
sourceOp *pb.SourceOp
httpContextIsArchive bool
httpContextFilename string
}
func (bc *Client) marshalOpts() []llb.ConstraintsOpt {
@@ -75,16 +87,25 @@ func (bc *Client) initContext(ctx context.Context) (*buildContext, error) {
keepGit = &v
}
var extraGitOpts []llb.GitOption
if opts[keySourceDateEpoch] != "" {
if opts[buildArgPrefix+"SOURCE_DATE_EPOCH"] != "" {
extraGitOpts = append(extraGitOpts, llb.GitMTimeCommit())
}
if st, ok, err := DetectGitContext(opts[localNameContext], keepGit, extraGitOpts...); ok {
if err != nil {
return nil, err
}
sourceOp, err := sourceOpFromState(ctx, st, bc.marshalOpts()...)
if err != nil {
return nil, errors.Wrapf(err, "failed to derive git source op")
}
bctx.context = st
bctx.dockerfile = st
bctx.sourceOp = sourceOp
} else if st, filename, ok := DetectHTTPContext(opts[localNameContext]); ok {
sourceOp, err := sourceOpFromState(ctx, st, bc.marshalOpts()...)
if err != nil {
return nil, errors.Wrapf(err, "failed to derive http source op")
}
def, err := st.Marshal(ctx, bc.marshalOpts()...)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal httpcontext")
@@ -115,11 +136,15 @@ func (bc *Client) initContext(ctx context.Context) (*buildContext, error) {
AttemptUnpack: true,
}))
bctx.context = &bc
bctx.httpContextIsArchive = true
} else {
bctx.filename = filename
bctx.context = st
}
bctx.contextRef = ref
bctx.dockerfile = bctx.context
bctx.sourceOp = sourceOp
bctx.httpContextFilename = filename
} else if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil {
inputs, err := bc.client.Inputs(ctx)
if err != nil {
@@ -148,6 +173,123 @@ func (bc *Client) initContext(ctx context.Context) (*buildContext, error) {
return bctx, nil
}
func (bc *Client) ResolveMainContextSourceDateEpoch(ctx context.Context) (*time.Time, error) {
bctx, err := bc.buildContext(ctx)
if err != nil {
return nil, err
}
if bctx.sourceOp == nil {
return nil, nil
}
opt := sourceresolver.Opt{
LogName: "[internal] resolve main build context metadata",
}
if strings.HasPrefix(bctx.sourceOp.Identifier, "git://") {
opt.GitOpt = &sourceresolver.ResolveGitOpt{ReturnObject: true}
}
md, err := bc.client.ResolveSourceMetadata(ctx, cloneSourceOp(bctx.sourceOp), opt)
if err != nil {
return nil, err
}
if md.Git != nil && len(md.Git.CommitObject) > 0 {
obj, err := gitobject.Parse(md.Git.CommitObject)
if err != nil {
return nil, err
}
commit, err := obj.ToCommit()
if err != nil {
return nil, err
}
return commit.Committer.When, nil
}
if md.HTTP != nil {
if md.HTTP.LastModified != nil {
return md.HTTP.LastModified, nil
}
if bctx.httpContextIsArchive {
return archiveMaxTimeFromHTTPArchive(ctx, bctx)
}
}
return nil, nil
}
func archiveMaxTimeFromHTTPArchive(ctx context.Context, bctx *buildContext) (*time.Time, error) {
if bctx.contextRef == nil || bctx.httpContextFilename == "" {
return nil, nil
}
dt, err := bctx.contextRef.ReadFile(ctx, client.ReadRequest{
Filename: bctx.httpContextFilename,
})
if err != nil {
return nil, err
}
rc, err := archivecompression.DecompressStream(bytes.NewReader(dt))
if err != nil {
return nil, err
}
defer rc.Close()
tr := tar.NewReader(rc)
var maxTime *time.Time
for {
hdr, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
return maxTime, nil
}
return nil, err
}
if !hdr.FileInfo().Mode().IsRegular() {
continue
}
tm := hdr.ModTime.UTC()
if maxTime == nil || tm.After(*maxTime) {
maxTime = &tm
}
}
}
func cloneSourceOp(op *pb.SourceOp) *pb.SourceOp {
if op == nil {
return nil
}
return &pb.SourceOp{
Identifier: op.Identifier,
Attrs: maps.Clone(op.Attrs),
}
}
func sourceOpFromState(ctx context.Context, st *llb.State, opts ...llb.ConstraintsOpt) (*pb.SourceOp, error) {
if st == nil {
return nil, nil
}
def, err := st.Marshal(ctx, opts...)
if err != nil {
return nil, err
}
dt := def.ToPB().Def
var src *pb.SourceOp
for _, d := range dt {
var op pb.Op
if err := op.Unmarshal(d); err != nil {
return nil, err
}
opSrc := op.GetSource()
if opSrc == nil {
continue
}
if src != nil {
return nil, errors.New("state marshaled to multiple source ops")
}
src = opSrc
}
if src == nil {
return nil, errors.New("state did not marshal to a source op")
}
return cloneSourceOp(src), nil
}
func DetectGitContext(ref string, keepGit *bool, opts ...llb.GitOption) (*llb.State, bool, error) {
g, isGit, err := dfgitutil.ParseGitRef(ref)
if err != nil {
@@ -175,6 +317,9 @@ func DetectGitContext(ref string, keepGit *bool, opts ...llb.GitOption) (*llb.St
if g.MTime != "" {
gitOpts = append(gitOpts, llb.GitMTime(g.MTime))
}
if g.FetchByCommit {
gitOpts = append(gitOpts, llb.GitFetchByCommit())
}
st := llb.Git(g.Remote, "", gitOpts...)
return &st, true, nil
+7 -7
View File
@@ -151,14 +151,14 @@ func (nc *NamedContext) load(ctx context.Context, count int) (*llb.State, *docke
return st, nil, nil
case "http", "https":
st, ok, err := DetectGitContext(nc.input, nil)
if !ok {
httpst := llb.HTTP(nc.input, llb.WithCustomName("[context "+nc.nameWithPlatform+"] "+nc.input))
st = &httpst
if ok {
if err != nil {
return nil, nil, err
}
return st, nil, nil
}
if err != nil {
return nil, nil, err
}
return st, nil, nil
httpst := llb.HTTP(nc.input, llb.Filename("context"), llb.WithCustomName("[context "+nc.nameWithPlatform+"] "+nc.input))
return &httpst, nil, nil
case "oci-layout":
refSpec := strings.TrimPrefix(vv[1], "//")
ref, err := reference.Parse(refSpec)
+36
View File
@@ -3,6 +3,8 @@ package client
import (
"context"
"io"
"maps"
"slices"
"syscall"
"github.com/moby/buildkit/client/llb"
@@ -154,6 +156,40 @@ type SolveRequest struct {
SourcePolicies []*spb.Policy
}
// Clone returns a deep copy of the solve request.
func (r SolveRequest) Clone() SolveRequest {
if r.Definition != nil {
r.Definition = r.Definition.CloneVT()
}
r.FrontendOpt = maps.Clone(r.FrontendOpt)
if len(r.FrontendInputs) > 0 {
inputs := r.FrontendInputs
r.FrontendInputs = make(map[string]*pb.Definition, len(inputs))
for k, v := range inputs {
if v != nil {
v = v.CloneVT()
}
r.FrontendInputs[k] = v
}
}
if len(r.CacheImports) > 0 {
r.CacheImports = slices.Clone(r.CacheImports)
for i, ci := range r.CacheImports {
ci.Attrs = maps.Clone(ci.Attrs)
r.CacheImports[i] = ci
}
}
if len(r.SourcePolicies) > 0 {
r.SourcePolicies = slices.Clone(r.SourcePolicies)
for i, p := range r.SourcePolicies {
if p != nil {
r.SourcePolicies[i] = p.CloneVT()
}
}
}
return r
}
type CacheOptionsEntry struct {
Type string
Attrs map[string]string
+13 -2
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"crypto/subtle"
"sync"
"time"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/util/grpcerrors"
@@ -13,6 +14,8 @@ import (
"google.golang.org/grpc/codes"
)
const sessionAuthTimeout = 60 * time.Second
var salt []byte
var saltOnce sync.Once
@@ -25,10 +28,12 @@ func getSalt() []byte {
return salt
}
func CredentialsFunc(sm *session.Manager, g session.Group) func(string) (session, username, secret string, err error) {
func CredentialsFunc(ctx context.Context, sm *session.Manager, g session.Group) func(string) (session, username, secret string, err error) {
return func(host string) (string, string, string, error) {
ctx, cancel := context.WithTimeoutCause(ctx, sessionAuthTimeout, errors.Wrap(context.DeadlineExceeded, "resolving credentials from session"))
defer cancel()
var sessionID, user, secret string
err := sm.Any(context.TODO(), g, func(ctx context.Context, id string, c session.Caller) error {
err := sm.Any(ctx, g, func(ctx context.Context, id string, c session.Caller) error {
client := NewAuthClient(c.Conn())
resp, err := client.Credentials(ctx, &CredentialsRequest{
@@ -53,6 +58,8 @@ func CredentialsFunc(sm *session.Manager, g session.Group) func(string) (session
}
func FetchToken(ctx context.Context, req *FetchTokenRequest, sm *session.Manager, g session.Group) (resp *FetchTokenResponse, err error) {
ctx, cancel := context.WithTimeoutCause(ctx, sessionAuthTimeout, errors.Wrap(context.DeadlineExceeded, "fetching auth token from session"))
defer cancel()
err = sm.Any(ctx, g, func(ctx context.Context, id string, c session.Caller) error {
client := NewAuthClient(c.Conn())
@@ -69,6 +76,8 @@ func FetchToken(ctx context.Context, req *FetchTokenRequest, sm *session.Manager
}
func VerifyTokenAuthority(ctx context.Context, host string, pubKey *[32]byte, sm *session.Manager, g session.Group) (sessionID string, ok bool, err error) {
ctx, cancel := context.WithTimeoutCause(ctx, sessionAuthTimeout, errors.Wrap(context.DeadlineExceeded, "verifying token authority from session"))
defer cancel()
var verified bool
err = sm.Any(ctx, g, func(ctx context.Context, id string, c session.Caller) error {
client := NewAuthClient(c.Conn())
@@ -101,6 +110,8 @@ func VerifyTokenAuthority(ctx context.Context, host string, pubKey *[32]byte, sm
}
func GetTokenAuthority(ctx context.Context, host string, sm *session.Manager, g session.Group) (sessionID string, pubKey *[32]byte, err error) {
ctx, cancel := context.WithTimeoutCause(ctx, sessionAuthTimeout, errors.Wrap(context.DeadlineExceeded, "getting token authority from session"))
defer cancel()
err = sm.Any(ctx, g, func(ctx context.Context, id string, c session.Caller) error {
client := NewAuthClient(c.Conn())
+3
View File
@@ -16,9 +16,11 @@ import (
type callerContentStore struct {
store content.Store
storeID string
caller session.Caller
}
func (cs *callerContentStore) choose(ctx context.Context) context.Context {
ctx = cs.caller.Context(ctx)
nsheader := metadata.Pairs(GRPCHeaderID, cs.storeID)
md, ok := metadata.FromOutgoingContext(ctx) // merge with outgoing context.
if !ok {
@@ -87,5 +89,6 @@ func NewCallerStore(c session.Caller, storeID string) content.Store {
return &callerContentStore{
store: proxy.NewContentStore(client),
storeID: storeID,
caller: c,
}
}
+17
View File
@@ -0,0 +1,17 @@
package session
import "context"
// contextWithCaller returns a context that is canceled when either the request
// context is done or the session context is closed.
func contextWithCaller(ctx context.Context, callerCtx context.Context) context.Context {
ctx, cancel := context.WithCancelCause(ctx)
context.AfterFunc(callerCtx, func() {
cause := context.Cause(callerCtx)
if cause == nil {
cause = context.Canceled
}
cancel(cause)
})
return ctx
}
+4
View File
@@ -207,6 +207,7 @@ func FSSync(ctx context.Context, c session.Caller, opt FSSendRequestOpt) error {
opts[keyDirName] = []string{opt.Name}
ctx = c.Context(ctx)
ctx, cancel := context.WithCancelCause(ctx)
defer func() { cancel(errors.WithStack(context.Canceled)) }()
@@ -362,6 +363,8 @@ func CopyToCaller(ctx context.Context, fs fsutil.FS, id int, c session.Caller, p
return errors.Errorf("method %s not supported by the client", method)
}
ctx = c.Context(ctx)
client := NewFileSendClient(c.Conn())
opts, ok := metadata.FromOutgoingContext(ctx)
@@ -388,6 +391,7 @@ func CopyFileWriter(ctx context.Context, md map[string]string, id int, c session
return nil, errors.Errorf("method %s not supported by the client", method)
}
ctx = c.Context(ctx)
client := NewFileSendClient(c.Conn())
opts, ok := metadata.FromOutgoingContext(ctx)
+6 -2
View File
@@ -7,6 +7,10 @@ import (
"github.com/pkg/errors"
)
// ErrNoActiveSessions is returned when a session group does not contain any
// active session IDs.
var ErrNoActiveSessions = errors.New("no active sessions")
type Group interface {
SessionIterator() Iterator
}
@@ -69,7 +73,7 @@ func (sm *Manager) Any(ctx context.Context, g Group, f func(context.Context, str
if lastErr != nil {
return lastErr
}
return errors.Errorf("no active sessions")
return ErrNoActiveSessions
}
timeoutCtx, cancel := context.WithCancelCause(ctx)
@@ -80,7 +84,7 @@ func (sm *Manager) Any(ctx context.Context, g Group, f func(context.Context, str
lastErr = err
continue
}
if err := f(ctx, id, c); err != nil {
if err := f(c.Context(ctx), id, c); err != nil {
lastErr = err
continue
}
+78 -22
View File
@@ -4,6 +4,8 @@ import (
"context"
"math"
"net"
"net/http"
"strconv"
"sync/atomic"
"time"
@@ -21,6 +23,40 @@ import (
"google.golang.org/grpc/health/grpc_health_v1"
)
type healthCheckConfig struct {
interval time.Duration
defaultTimeout time.Duration
failureThreshold int
successResetThreshold int
}
var defaultHealthCheckConfig = healthCheckConfig{
interval: 5 * time.Second,
defaultTimeout: 15 * time.Second,
failureThreshold: 2,
successResetThreshold: 1,
}
const headerSessionHealthCustomTimeout = "X-Buildkit-Session-Health-Custom-Timeout"
func healthCheckConfigFromHeaders(h http.Header) healthCheckConfig {
cfg := defaultHealthCheckConfig
if v := h.Get(headerSessionHealthCustomTimeout); v != "" {
if ms, err := strconv.Atoi(v); err == nil && ms > 0 {
d := time.Duration(ms) * time.Millisecond
// Avoid test overrides that are unrealistically aggressive for slower machines.
d = max(d, time.Second)
cfg.interval = d
cfg.defaultTimeout = d
cfg.failureThreshold = 1
cfg.successResetThreshold = 1
}
}
return cfg
}
func serve(ctx context.Context, grpcServer *grpc.Server, conn net.Conn) {
go func() {
<-ctx.Done()
@@ -30,7 +66,7 @@ func serve(ctx context.Context, grpcServer *grpc.Server, conn net.Conn) {
(&http2.Server{}).ServeConn(conn, &http2.ServeConnOpts{Handler: grpcServer})
}
func grpcClientConn(ctx context.Context, conn net.Conn) (context.Context, *grpc.ClientConn, error) {
func grpcClientConn(ctx context.Context, conn net.Conn, opts map[string][]string) (context.Context, *grpc.ClientConn, error) {
var dialCount int64
dialer := grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
if c := atomic.AddInt64(&dialCount, 1); c > 1 {
@@ -63,22 +99,29 @@ func grpcClientConn(ctx context.Context, conn net.Conn) (context.Context, *grpc.
}
ctx, cancel := context.WithCancelCause(ctx)
go monitorHealth(ctx, cc, cancel)
go monitorHealth(ctx, conn, cc, cancel, healthCheckConfigFromHeaders(http.Header(opts)))
return ctx, cc, nil
}
func monitorHealth(ctx context.Context, cc *grpc.ClientConn, cancelConn func(error)) {
defer cancelConn(errors.WithStack(context.Canceled))
defer cc.Close()
ticker := time.NewTicker(5 * time.Second)
func monitorHealth(ctx context.Context, conn net.Conn, cc *grpc.ClientConn, cancelConn func(error), cfg healthCheckConfig) {
closed := false
closeConn := func(err error) {
if closed {
return
}
closed = true
cancelConn(err)
cc.Close()
go conn.Close()
}
defer closeConn(errors.WithStack(context.Canceled))
ticker := time.NewTicker(cfg.interval)
defer ticker.Stop()
healthClient := grpc_health_v1.NewHealthClient(cc)
failedBefore := false
consecutiveFailures := 0
consecutiveSuccessful := 0
defaultHealthcheckDuration := 30 * time.Second
lastHealthcheckDuration := time.Duration(0)
for {
@@ -90,13 +133,24 @@ func monitorHealth(ctx context.Context, cc *grpc.ClientConn, cancelConn func(err
// So, this healthcheck is purposely long, and can tolerate some failures on purpose.
healthcheckStart := time.Now()
timeout := time.Duration(math.Max(float64(cfg.defaultTimeout), float64(lastHealthcheckDuration)*1.5))
resultCh := make(chan error, 1)
go func() {
checkCtx, cancel := context.WithCancelCause(ctx)
checkCtx, _ = context.WithTimeoutCause(checkCtx, timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet
_, err := healthClient.Check(checkCtx, &grpc_health_v1.HealthCheckRequest{})
cancel(errors.WithStack(context.Canceled))
resultCh <- err
}()
timeout := time.Duration(math.Max(float64(defaultHealthcheckDuration), float64(lastHealthcheckDuration)*1.5))
ctx, cancel := context.WithCancelCause(ctx)
ctx, _ = context.WithTimeoutCause(ctx, timeout, errors.WithStack(context.DeadlineExceeded)) //nolint:govet
_, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
cancel(errors.WithStack(context.Canceled))
var err error
select {
case <-ctx.Done():
return
case err = <-resultCh:
case <-time.After(timeout):
err = errors.WithStack(context.DeadlineExceeded)
}
lastHealthcheckDuration = time.Since(healthcheckStart)
logFields := logrus.Fields{
@@ -110,19 +164,21 @@ func monitorHealth(ctx context.Context, cc *grpc.ClientConn, cancelConn func(err
return
default:
}
if failedBefore {
bklog.G(ctx).Error("healthcheck failed fatally")
consecutiveFailures++
consecutiveSuccessful = 0
if consecutiveFailures >= cfg.failureThreshold {
err = errors.Wrap(err, "session healthcheck failed fatally")
bklog.G(ctx).WithError(err).Error("healthcheck failed fatally")
closeConn(err)
return
}
failedBefore = true
consecutiveSuccessful = 0
bklog.G(ctx).WithFields(logFields).Warn("healthcheck failed")
bklog.G(ctx).WithError(err).WithFields(logFields).Warn("healthcheck failed")
} else {
consecutiveSuccessful++
consecutiveFailures = 0
if consecutiveSuccessful >= 5 && failedBefore {
failedBefore = false
if consecutiveSuccessful >= cfg.successResetThreshold {
bklog.G(ctx).WithFields(logFields).Debug("reset healthcheck failure")
}
}
+4 -4
View File
@@ -13,7 +13,7 @@ import (
// Caller can invoke requests on the session
type Caller interface {
Context() context.Context
Context(context.Context) context.Context
Supports(method string) bool
Conn() *grpc.ClientConn
SharedKey() string
@@ -107,7 +107,7 @@ func (sm *Manager) handleConn(ctx context.Context, conn net.Conn, opts map[strin
id := h.Get(headerSessionID)
sharedKey := h.Get(headerSessionSharedKey)
ctx, cc, err := grpcClientConn(ctx, conn)
ctx, cc, err := grpcClientConn(ctx, conn, opts)
if err != nil {
sm.mu.Unlock()
return err
@@ -190,8 +190,8 @@ func (sm *Manager) Get(ctx context.Context, id string, noWait bool) (Caller, err
return c, nil
}
func (c *client) Context() context.Context {
return c.context()
func (c *client) Context(ctx context.Context) context.Context {
return contextWithCaller(ctx, c.context())
}
func (c *client) SharedKey() string {
+1
View File
@@ -16,6 +16,7 @@ type SecretStore interface {
var ErrNotFound = errors.Errorf("not found")
func GetSecret(ctx context.Context, c session.Caller, id string) ([]byte, error) {
ctx = c.Context(ctx)
client := NewSecretsClient(c.Conn())
resp, err := client.GetSecret(ctx, &GetSecretRequest{
ID: id,
+5 -3
View File
@@ -37,18 +37,19 @@ func (s *server) run(ctx context.Context, l net.Listener, id string) error {
}
client := NewSSHClient(s.caller.Conn())
rpcCtx := s.caller.Context(ctx)
opts := make(map[string][]string)
opts[KeySSHID] = []string{id}
ctx := metadata.NewOutgoingContext(ctx, opts)
rpcCtx = metadata.NewOutgoingContext(rpcCtx, opts)
stream, err := client.ForwardAgent(ctx)
stream, err := client.ForwardAgent(rpcCtx)
if err != nil {
conn.Close()
return err
}
go Copy(ctx, conn, stream, stream.CloseSend)
go Copy(rpcCtx, conn, stream, stream.CloseSend)
}
})
@@ -112,6 +113,7 @@ func MountSSHSocket(ctx context.Context, c session.Caller, opt SocketOpt) (sockP
}
func CheckSSHID(ctx context.Context, c session.Caller, id string) error {
ctx = c.Context(ctx)
client := NewSSHClient(c.Conn())
_, err := client.CheckAgent(ctx, &CheckAgentRequest{ID: id})
return errors.WithStack(err)
+1
View File
@@ -16,6 +16,7 @@ const (
)
func New(ctx context.Context, c session.Caller, url *url.URL) (*Upload, error) {
ctx = c.Context(ctx)
opts := map[string][]string{
keyPath: {url.Path},
keyHost: {url.Host},
+43
View File
@@ -0,0 +1,43 @@
package errdefs
import (
fmt "fmt"
"github.com/containerd/typeurl/v2"
"github.com/moby/buildkit/util/grpcerrors"
)
func init() {
typeurl.Register((*CompatibilityFeature)(nil), "github.com/moby/buildkit", "errdefs.CompatibilityFeature+json")
}
type UnsupportedCompatibilityFeatureError struct {
*CompatibilityFeature
error
}
func (e *UnsupportedCompatibilityFeatureError) Error() string {
msg := fmt.Sprintf("unsupported compatibility-version %d feature %s", e.Version, e.Feature)
if e.error != nil {
msg += ": " + e.error.Error()
}
return msg
}
func (e *UnsupportedCompatibilityFeatureError) Unwrap() error {
return e.error
}
func (e *UnsupportedCompatibilityFeatureError) ToProto() grpcerrors.TypedErrorProto {
return e.CompatibilityFeature
}
func NewUnsupportedCompatibilityFeatureError(version int, feature string) error {
return &UnsupportedCompatibilityFeatureError{
CompatibilityFeature: &CompatibilityFeature{Version: int64(version), Feature: feature},
}
}
func (v *CompatibilityFeature) WrapError(err error) error {
return &UnsupportedCompatibilityFeatureError{error: err, CompatibilityFeature: v}
}
+90 -34
View File
@@ -214,6 +214,58 @@ func (x *FrontendCap) GetName() string {
return ""
}
type CompatibilityFeature struct {
state protoimpl.MessageState `protogen:"open.v1"`
Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
Feature string `protobuf:"bytes,2,opt,name=feature,proto3" json:"feature,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CompatibilityFeature) Reset() {
*x = CompatibilityFeature{}
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CompatibilityFeature) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CompatibilityFeature) ProtoMessage() {}
func (x *CompatibilityFeature) ProtoReflect() protoreflect.Message {
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CompatibilityFeature.ProtoReflect.Descriptor instead.
func (*CompatibilityFeature) Descriptor() ([]byte, []int) {
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{4}
}
func (x *CompatibilityFeature) GetVersion() int64 {
if x != nil {
return x.Version
}
return 0
}
func (x *CompatibilityFeature) GetFeature() string {
if x != nil {
return x.Feature
}
return ""
}
type Subrequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
@@ -223,7 +275,7 @@ type Subrequest struct {
func (x *Subrequest) Reset() {
*x = Subrequest{}
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[4]
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -235,7 +287,7 @@ func (x *Subrequest) String() string {
func (*Subrequest) ProtoMessage() {}
func (x *Subrequest) ProtoReflect() protoreflect.Message {
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[4]
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -248,7 +300,7 @@ func (x *Subrequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use Subrequest.ProtoReflect.Descriptor instead.
func (*Subrequest) Descriptor() ([]byte, []int) {
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{4}
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{5}
}
func (x *Subrequest) GetName() string {
@@ -275,7 +327,7 @@ type Solve struct {
func (x *Solve) Reset() {
*x = Solve{}
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[5]
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -287,7 +339,7 @@ func (x *Solve) String() string {
func (*Solve) ProtoMessage() {}
func (x *Solve) ProtoReflect() protoreflect.Message {
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[5]
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -300,7 +352,7 @@ func (x *Solve) ProtoReflect() protoreflect.Message {
// Deprecated: Use Solve.ProtoReflect.Descriptor instead.
func (*Solve) Descriptor() ([]byte, []int) {
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{5}
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{6}
}
func (x *Solve) GetInputIDs() []string {
@@ -382,7 +434,7 @@ type FileAction struct {
func (x *FileAction) Reset() {
*x = FileAction{}
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[6]
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -394,7 +446,7 @@ func (x *FileAction) String() string {
func (*FileAction) ProtoMessage() {}
func (x *FileAction) ProtoReflect() protoreflect.Message {
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[6]
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -407,7 +459,7 @@ func (x *FileAction) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileAction.ProtoReflect.Descriptor instead.
func (*FileAction) Descriptor() ([]byte, []int) {
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{6}
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{7}
}
func (x *FileAction) GetIndex() int64 {
@@ -427,7 +479,7 @@ type ContentCache struct {
func (x *ContentCache) Reset() {
*x = ContentCache{}
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[7]
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -439,7 +491,7 @@ func (x *ContentCache) String() string {
func (*ContentCache) ProtoMessage() {}
func (x *ContentCache) ProtoReflect() protoreflect.Message {
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[7]
mi := &file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -452,7 +504,7 @@ func (x *ContentCache) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContentCache.ProtoReflect.Descriptor instead.
func (*ContentCache) Descriptor() ([]byte, []int) {
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{7}
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP(), []int{8}
}
func (x *ContentCache) GetIndex() int64 {
@@ -476,7 +528,10 @@ const file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDesc = "" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +
"\x06source\x18\x02 \x01(\tR\x06source\"!\n" +
"\vFrontendCap\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\" \n" +
"\x04name\x18\x01 \x01(\tR\x04name\"J\n" +
"\x14CompatibilityFeature\x12\x18\n" +
"\aversion\x18\x01 \x01(\x03R\aversion\x12\x18\n" +
"\afeature\x18\x02 \x01(\tR\afeature\" \n" +
"\n" +
"Subrequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"\xbf\x02\n" +
@@ -509,28 +564,29 @@ func file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescGZIP() []
return file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDescData
}
var file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_goTypes = []any{
(*Vertex)(nil), // 0: errdefs.Vertex
(*Source)(nil), // 1: errdefs.Source
(*Frontend)(nil), // 2: errdefs.Frontend
(*FrontendCap)(nil), // 3: errdefs.FrontendCap
(*Subrequest)(nil), // 4: errdefs.Subrequest
(*Solve)(nil), // 5: errdefs.Solve
(*FileAction)(nil), // 6: errdefs.FileAction
(*ContentCache)(nil), // 7: errdefs.ContentCache
nil, // 8: errdefs.Solve.DescriptionEntry
(*pb.SourceInfo)(nil), // 9: pb.SourceInfo
(*pb.Range)(nil), // 10: pb.Range
(*pb.Op)(nil), // 11: pb.Op
(*Vertex)(nil), // 0: errdefs.Vertex
(*Source)(nil), // 1: errdefs.Source
(*Frontend)(nil), // 2: errdefs.Frontend
(*FrontendCap)(nil), // 3: errdefs.FrontendCap
(*CompatibilityFeature)(nil), // 4: errdefs.CompatibilityFeature
(*Subrequest)(nil), // 5: errdefs.Subrequest
(*Solve)(nil), // 6: errdefs.Solve
(*FileAction)(nil), // 7: errdefs.FileAction
(*ContentCache)(nil), // 8: errdefs.ContentCache
nil, // 9: errdefs.Solve.DescriptionEntry
(*pb.SourceInfo)(nil), // 10: pb.SourceInfo
(*pb.Range)(nil), // 11: pb.Range
(*pb.Op)(nil), // 12: pb.Op
}
var file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_depIdxs = []int32{
9, // 0: errdefs.Source.info:type_name -> pb.SourceInfo
10, // 1: errdefs.Source.ranges:type_name -> pb.Range
11, // 2: errdefs.Solve.op:type_name -> pb.Op
6, // 3: errdefs.Solve.file:type_name -> errdefs.FileAction
7, // 4: errdefs.Solve.cache:type_name -> errdefs.ContentCache
8, // 5: errdefs.Solve.description:type_name -> errdefs.Solve.DescriptionEntry
10, // 0: errdefs.Source.info:type_name -> pb.SourceInfo
11, // 1: errdefs.Source.ranges:type_name -> pb.Range
12, // 2: errdefs.Solve.op:type_name -> pb.Op
7, // 3: errdefs.Solve.file:type_name -> errdefs.FileAction
8, // 4: errdefs.Solve.cache:type_name -> errdefs.ContentCache
9, // 5: errdefs.Solve.description:type_name -> errdefs.Solve.DescriptionEntry
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
@@ -543,7 +599,7 @@ func file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_init() {
if File_github_com_moby_buildkit_solver_errdefs_errdefs_proto != nil {
return
}
file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[5].OneofWrappers = []any{
file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_msgTypes[6].OneofWrappers = []any{
(*Solve_File)(nil),
(*Solve_Cache)(nil),
}
@@ -553,7 +609,7 @@ func file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDesc), len(file_github_com_moby_buildkit_solver_errdefs_errdefs_proto_rawDesc)),
NumEnums: 0,
NumMessages: 9,
NumMessages: 10,
NumExtensions: 0,
NumServices: 0,
},
+5
View File
@@ -24,6 +24,11 @@ message FrontendCap {
string name = 1;
}
message CompatibilityFeature {
int64 version = 1;
string feature = 2;
}
message Subrequest {
string name = 1;
}
+204
View File
@@ -96,6 +96,24 @@ func (m *FrontendCap) CloneMessageVT() proto.Message {
return m.CloneVT()
}
func (m *CompatibilityFeature) CloneVT() *CompatibilityFeature {
if m == nil {
return (*CompatibilityFeature)(nil)
}
r := new(CompatibilityFeature)
r.Version = m.Version
r.Feature = m.Feature
if len(m.unknownFields) > 0 {
r.unknownFields = make([]byte, len(m.unknownFields))
copy(r.unknownFields, m.unknownFields)
}
return r
}
func (m *CompatibilityFeature) CloneMessageVT() proto.Message {
return m.CloneVT()
}
func (m *Subrequest) CloneVT() *Subrequest {
if m == nil {
return (*Subrequest)(nil)
@@ -298,6 +316,28 @@ func (this *FrontendCap) EqualMessageVT(thatMsg proto.Message) bool {
}
return this.EqualVT(that)
}
func (this *CompatibilityFeature) EqualVT(that *CompatibilityFeature) bool {
if this == that {
return true
} else if this == nil || that == nil {
return false
}
if this.Version != that.Version {
return false
}
if this.Feature != that.Feature {
return false
}
return string(this.unknownFields) == string(that.unknownFields)
}
func (this *CompatibilityFeature) EqualMessageVT(thatMsg proto.Message) bool {
that, ok := thatMsg.(*CompatibilityFeature)
if !ok {
return false
}
return this.EqualVT(that)
}
func (this *Subrequest) EqualVT(that *Subrequest) bool {
if this == that {
return true
@@ -646,6 +686,51 @@ func (m *FrontendCap) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
return len(dAtA) - i, nil
}
func (m *CompatibilityFeature) MarshalVT() (dAtA []byte, err error) {
if m == nil {
return nil, nil
}
size := m.SizeVT()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBufferVT(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *CompatibilityFeature) MarshalToVT(dAtA []byte) (int, error) {
size := m.SizeVT()
return m.MarshalToSizedBufferVT(dAtA[:size])
}
func (m *CompatibilityFeature) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
if m == nil {
return 0, nil
}
i := len(dAtA)
_ = i
var l int
_ = l
if m.unknownFields != nil {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if len(m.Feature) > 0 {
i -= len(m.Feature)
copy(dAtA[i:], m.Feature)
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Feature)))
i--
dAtA[i] = 0x12
}
if m.Version != 0 {
i = protohelpers.EncodeVarint(dAtA, i, uint64(m.Version))
i--
dAtA[i] = 0x8
}
return len(dAtA) - i, nil
}
func (m *Subrequest) MarshalVT() (dAtA []byte, err error) {
if m == nil {
return nil, nil
@@ -963,6 +1048,23 @@ func (m *FrontendCap) SizeVT() (n int) {
return n
}
func (m *CompatibilityFeature) SizeVT() (n int) {
if m == nil {
return 0
}
var l int
_ = l
if m.Version != 0 {
n += 1 + protohelpers.SizeOfVarint(uint64(m.Version))
}
l = len(m.Feature)
if l > 0 {
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
n += len(m.unknownFields)
return n
}
func (m *Subrequest) SizeVT() (n int) {
if m == nil {
return 0
@@ -1470,6 +1572,108 @@ func (m *FrontendCap) UnmarshalVT(dAtA []byte) error {
}
return nil
}
func (m *CompatibilityFeature) UnmarshalVT(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: CompatibilityFeature: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: CompatibilityFeature: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType)
}
m.Version = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Version |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Feature", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Feature = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return protohelpers.ErrInvalidLength
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *Subrequest) UnmarshalVT(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
+44
View File
@@ -11,6 +11,7 @@ import (
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/solver/llbsolver/compat"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/bkmaps"
"github.com/moby/buildkit/util/flightcontrol"
@@ -90,6 +91,34 @@ func (s *state) ResolverCache() ResolverCache {
return s
}
func (s *state) CompatibilityVersion() (int, error) {
s.mu.Lock()
jobs := make([]*Job, 0, len(s.jobs))
for j := range s.jobs {
jobs = append(jobs, j)
}
s.mu.Unlock()
version := 0
for _, j := range jobs {
v, err := j.CompatibilityVersion()
if err != nil {
return 0, err
}
if version == 0 {
version = v
continue
}
if version != v {
return 0, errors.Errorf("conflicting compatibility versions in shared solve state: %d != %d", version, v)
}
}
if version == 0 {
return compat.CompatibilityVersionCurrent, nil
}
return version, nil
}
func (s *state) Lock(key any) (values []any, release func(any) error, err error) {
var rcs []ResolverCache
s.mu.Lock()
@@ -865,6 +894,21 @@ func (j *Job) UniqueID() string {
return j.uniqueID
}
func (j *Job) CompatibilityVersion() (int, error) {
version := compat.CompatibilityVersionCurrent
if err := j.EachValue(context.TODO(), compat.JobValueKey, func(v any) error {
parsed, ok := v.(int)
if !ok {
return errors.Errorf("invalid compatibility version %T", v)
}
version = parsed
return nil
}); err != nil {
return 0, err
}
return version, nil
}
func (j *Job) InContext(ctx context.Context, f func(context.Context, JobContext) error) error {
return f(progress.WithProgress(ctx, j.pw), j)
}
+1
View File
@@ -39,6 +39,7 @@ type llbBridge struct {
cms map[string]solver.CacheManager
cmsMu sync.Mutex
sm *session.Manager
provenanceStore *provenanceStore
executorOnce sync.Once
executorErr error
+36
View File
@@ -0,0 +1,36 @@
package compat
import (
"slices"
"github.com/pkg/errors"
)
const (
CompatibilityVersion013 = 10
CompatibilityVersion015 = 20
CompatibilityVersionCurrent = CompatibilityVersion015
)
// JobValueKey is the key used to store the compatibility version on a solver
// job via Job.SetValue/EachValue.
const JobValueKey = "llb.compatibilityversion"
var supportedCompatibilityVersions = []int{
CompatibilityVersion013,
CompatibilityVersion015,
}
func SupportedCompatibilityVersions() []int {
return slices.Clone(supportedCompatibilityVersions)
}
func ValidateCompatibilityVersion(version int) error {
if slices.Contains(supportedCompatibilityVersions, version) {
return nil
}
if version > CompatibilityVersionCurrent {
return errors.Errorf("unsupported compatibility-version %d: upgrade buildkit (max supported: %d)", version, CompatibilityVersionCurrent)
}
return errors.Errorf("unsupported compatibility-version %d (supported: %v)", version, supportedCompatibilityVersions)
}
+9 -4
View File
@@ -40,7 +40,7 @@ func (s *Solver) getSessionExporters(ctx context.Context, sessionID string, id i
}
client := sessionexporter.NewExporterClient(caller.Conn())
ctx = caller.Context(ctx)
var ids []string
if err := inp.EachRef(func(ref cache.ImmutableRef) error {
ids = append(ids, ref.ID())
@@ -188,11 +188,16 @@ func (s *Solver) runExporters(ctx context.Context, ref string, exporters []expor
defer inlineCacheMu.Unlock()
return runInlineCacheExporter(ctx, exp, inlineCacheExporter, job, cached)
})
compatibilityVersion, err := job.CompatibilityVersion()
if err != nil {
return err
}
resp, finalize, desc, expErr := exp.Export(ctx, inp, exporter.ExportBuildInfo{
Ref: ref,
SessionID: job.SessionID,
InlineCache: inlineCache,
Ref: ref,
SessionID: job.SessionID,
InlineCache: inlineCache,
CompatibilityVersion: compatibilityVersion,
})
resps[i], finalizeFuncs[i], descs[i] = resp, finalize, desc
if expErr != nil {
+1
View File
@@ -58,6 +58,7 @@ func (p *policyEvaluator) evaluate(ctx context.Context, op *pb.Op, max int) (boo
}
verifier := policysession.NewPolicyVerifierClient(caller.Conn())
ctx = caller.Context(ctx)
req := &policysession.CheckPolicyRequest{
Platform: op.Platform,
Source: &gatewaypb.ResolveSourceMetaResponse{
-1
View File
@@ -71,7 +71,6 @@ func ProvenanceProcessor(slsaVersion provenancetypes.ProvenanceSLSA, attrs map[s
if err != nil {
return nil, err
}
return json.MarshalIndent(pr, "", " ")
},
})
+73 -18
View File
@@ -38,12 +38,14 @@ type resultWithBridge struct {
// provenanceBridge provides scoped access to LLBBridge and captures the request it makes for provenance
type provenanceBridge struct {
*llbBridge
mu sync.Mutex
req *frontend.SolveRequest
mu sync.Mutex
req *frontend.SolveRequest
rootReq *frontend.SolveRequest
images []provenancetypes.ImageSource
builds []resultWithBridge
subBridges []*provenanceBridge
images []provenancetypes.ImageSource
builds []resultWithBridge
subBridges []*provenanceBridge
provenanceRefRecordIDs []string
}
func (b *provenanceBridge) eachRef(f func(r solver.ResultProxy) error) error {
@@ -165,6 +167,7 @@ func (b *provenanceBridge) ResolveSourceMetadata(ctx context.Context, op *pb.Sou
}
func (b *provenanceBridge) Solve(ctx context.Context, req frontend.SolveRequest, sid string) (res *frontend.Result, err error) {
req = req.Clone()
if req.Definition != nil && req.Definition.Def != nil && req.Frontend != "" {
return nil, errors.New("cannot solve with both Definition and Frontend specified")
}
@@ -180,7 +183,11 @@ func (b *provenanceBridge) Solve(ctx context.Context, req frontend.SolveRequest,
if !ok {
return nil, errors.Errorf("invalid frontend: %s", req.Frontend)
}
wb := &provenanceBridge{llbBridge: b.llbBridge, req: &req}
rootReq := b.rootReq
if !hasRequestProvenance(rootReq) {
rootReq = b.req
}
wb := &provenanceBridge{llbBridge: b.llbBridge, req: &req, rootReq: rootReq}
res, err = f.Solve(ctx, wb, b.llbBridge, req.FrontendOpt, req.FrontendInputs, sid, b.sm)
if err != nil {
fe := errdefs.Frontend{
@@ -202,6 +209,9 @@ func (b *provenanceBridge) Solve(ctx context.Context, req frontend.SolveRequest,
return err
})
}
if err == nil {
err = b.registerProvenanceRefs(res)
}
return
}
@@ -397,17 +407,9 @@ func NewProvenanceCreator(ctx context.Context, slsaVersion provenancetypes.Prove
switch mode {
case "min":
args := make(map[string]string)
for k, v := range pr.BuildDefinition.ExternalParameters.Request.Args {
if strings.HasPrefix(k, "build-arg:") || strings.HasPrefix(k, "label:") {
pr.RunDetails.Metadata.Completeness.Request = false
continue
}
args[k] = v
if scrubMinRequest(&pr.BuildDefinition.ExternalParameters.Request) {
pr.RunDetails.Metadata.Completeness.Request = false
}
pr.BuildDefinition.ExternalParameters.Request.Args = args
pr.BuildDefinition.ExternalParameters.Request.Secrets = nil
pr.BuildDefinition.ExternalParameters.Request.SSH = nil
case "max":
dgsts, err := provenance.AddBuildConfig(ctx, pr, cp, res, withUsage)
if err != nil {
@@ -478,6 +480,41 @@ func NewProvenanceCreator(ctx context.Context, slsaVersion provenancetypes.Prove
return pc, nil
}
func scrubMinRequest(req *provenancetypes.Parameters) bool {
if req == nil {
return false
}
var incomplete bool
if len(req.Args) > 0 {
args := make(map[string]string, len(req.Args))
for k, v := range req.Args {
if strings.HasPrefix(k, "build-arg:") || strings.HasPrefix(k, "label:") {
incomplete = true
continue
}
args[k] = v
}
req.Args = args
}
if len(req.Secrets) > 0 {
req.Secrets = nil
}
if len(req.SSH) > 0 {
req.SSH = nil
}
for _, in := range req.Inputs {
if in != nil && scrubMinRequest(in.Request) {
incomplete = true
}
}
if req.Root != nil && scrubMinRequest(req.Root.Request) {
incomplete = true
}
return incomplete
}
func (p *ProvenanceCreator) PredicateType() string {
if p.slsaVersion == provenancetypes.ProvenanceSLSA02 {
return slsa02.PredicateSLSAProvenance
@@ -506,6 +543,12 @@ func (p *ProvenanceCreator) Predicate(ctx context.Context) (any, error) {
p.pr.RunDetails.Metadata.BuildKitMetadata.SysUsage = sysSamples
}
compatibilityVersion, err := p.j.CompatibilityVersion()
if err != nil {
return nil, err
}
p.pr.BuildDefinition.ExternalParameters.Request.CompatibilityVersion = compatibilityVersion
if p.slsaVersion == provenancetypes.ProvenanceSLSA02 {
return p.pr.ConvertToSLSA02(), nil
}
@@ -665,14 +708,26 @@ func getRefProvenance(ref solver.ResultProxy, br *provenanceBridge) (*provenance
if !ok {
return nil, errors.Errorf("invalid provenance type %T", p)
}
pr = pr.Clone()
if br.req != nil {
if pr == nil {
return nil, errors.Errorf("missing provenance for %s", ref.ID())
}
pr.Frontend = br.req.Frontend
pr.Args = provenance.FilterArgs(br.req.FrontendOpt)
pr.Request.Frontend = br.req.Frontend
pr.Request.Args = provenance.FilterArgs(br.req.FrontendOpt)
pr.Request.Inputs = br.inputProvenance(br.req.FrontendInputs)
if root := br.rootRequestProvenance(pr.Sources); root != nil {
req := provenance.RequestProvenance(pr.Request.Frontend, pr.Request.Args, pr.Sources)
if req.Request == nil {
req.Request = &provenancetypes.Parameters{}
}
req.Request.Inputs = pr.Request.Inputs
if !root.Equal(req) {
pr.Request.Root = root
}
}
// TODO: should also save some output options like compression
}
+61 -17
View File
@@ -2,6 +2,7 @@ package provenance
import (
"cmp"
"maps"
"slices"
distreference "github.com/distribution/reference"
@@ -15,16 +16,36 @@ import (
type Result = result.Result[*Capture]
type Capture struct {
Frontend string
Args map[string]string
Request provenancetypes.Parameters
Sources provenancetypes.Sources
Secrets []provenancetypes.Secret
SSH []provenancetypes.SSH
NetworkAccess bool
IncompleteMaterials bool
Samples map[digest.Digest]*resourcestypes.Samples
}
func (c *Capture) Clone() *Capture {
if c == nil {
return nil
}
out := &Capture{
NetworkAccess: c.NetworkAccess,
IncompleteMaterials: c.IncompleteMaterials,
}
if req := c.Request.Clone(); req != nil {
out.Request = *req
}
out.Sources.Images = append(out.Sources.Images, c.Sources.Images...)
out.Sources.ImageBlobs = append(out.Sources.ImageBlobs, c.Sources.ImageBlobs...)
out.Sources.Local = append(out.Sources.Local, c.Sources.Local...)
out.Sources.Git = append(out.Sources.Git, c.Sources.Git...)
out.Sources.HTTP = append(out.Sources.HTTP, c.Sources.HTTP...)
if len(c.Samples) > 0 {
out.Samples = make(map[digest.Digest]*resourcestypes.Samples, len(c.Samples))
maps.Copy(out.Samples, c.Samples)
}
return out
}
func (c *Capture) Merge(c2 *Capture) error {
if c2 == nil {
return nil
@@ -44,11 +65,15 @@ func (c *Capture) Merge(c2 *Capture) error {
for _, h := range c2.Sources.HTTP {
c.AddHTTP(h)
}
for _, s := range c2.Secrets {
c.AddSecret(s)
for _, s := range c2.Request.Secrets {
if s != nil {
c.AddSecret(*s)
}
}
for _, s := range c2.SSH {
c.AddSSH(s)
for _, s := range c2.Request.SSH {
if s != nil {
c.AddSSH(*s)
}
}
if c2.NetworkAccess {
c.NetworkAccess = true
@@ -75,10 +100,10 @@ func (c *Capture) Sort() {
slices.SortFunc(c.Sources.HTTP, func(a, b provenancetypes.HTTPSource) int {
return cmp.Compare(a.URL, b.URL)
})
slices.SortFunc(c.Secrets, func(a, b provenancetypes.Secret) int {
slices.SortFunc(c.Request.Secrets, func(a, b *provenancetypes.Secret) int {
return cmp.Compare(a.ID, b.ID)
})
slices.SortFunc(c.SSH, func(a, b provenancetypes.SSH) int {
slices.SortFunc(c.Request.SSH, func(a, b *provenancetypes.SSH) int {
return cmp.Compare(a.ID, b.ID)
})
}
@@ -152,14 +177,33 @@ func (c *Capture) AddLocal(l provenancetypes.LocalSource) {
func (c *Capture) AddGit(g provenancetypes.GitSource) {
g.URL = urlutil.RedactCredentials(g.URL)
// Dedupe on the tuple (URL, Bundle.URL). Two records with the same
// URL but different bundle identity (e.g. the same repo referenced
// once normally and once through a bundle, or through two different
// bundle locators) must both be preserved so neither material is
// silently dropped from the provenance. Bundle.URL is the canonical
// bundle identity: since scheme/ref/digest are derived from it,
// different URLs imply different bundle identity.
for _, v := range c.Sources.Git {
if v.URL == g.URL {
if v.URL != g.URL {
continue
}
if bundleKey(v.Bundle) == bundleKey(g.Bundle) {
return
}
}
c.Sources.Git = append(c.Sources.Git, g)
}
// bundleKey returns a comparable identity for dedupe. Nil bundles collapse
// to the empty key.
func bundleKey(b *provenancetypes.GitBundle) string {
if b == nil {
return ""
}
return b.URL
}
func (c *Capture) AddHTTP(h provenancetypes.HTTPSource) {
h.URL = urlutil.RedactCredentials(h.URL)
for _, v := range c.Sources.HTTP {
@@ -171,30 +215,30 @@ func (c *Capture) AddHTTP(h provenancetypes.HTTPSource) {
}
func (c *Capture) AddSecret(s provenancetypes.Secret) {
for i, v := range c.Secrets {
for i, v := range c.Request.Secrets {
if v.ID == s.ID {
if !s.Optional {
c.Secrets[i].Optional = false
c.Request.Secrets[i].Optional = false
}
return
}
}
c.Secrets = append(c.Secrets, s)
c.Request.Secrets = append(c.Request.Secrets, &s)
}
func (c *Capture) AddSSH(s provenancetypes.SSH) {
if s.ID == "" {
s.ID = "default"
}
for i, v := range c.SSH {
for i, v := range c.Request.SSH {
if v.ID == s.ID {
if !s.Optional {
c.SSH[i].Optional = false
c.Request.SSH[i].Optional = false
}
return
}
}
c.SSH = append(c.SSH, s)
c.Request.SSH = append(c.Request.SSH, &s)
}
func (c *Capture) AddSamples(dgst digest.Digest, samples *resourcestypes.Samples) {
+128 -45
View File
@@ -9,8 +9,10 @@ import (
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
"github.com/moby/buildkit/frontend/dockerfile/dfgitutil"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
srctypes "github.com/moby/buildkit/source/types"
"github.com/moby/buildkit/util/purl"
"github.com/moby/buildkit/util/urlutil"
digest "github.com/opencontainers/go-digest"
"github.com/package-url/packageurl-go"
)
@@ -68,10 +70,55 @@ func slsaMaterials(srcs provenancetypes.Sources) ([]slsa.ProvenanceMaterial, err
}
for _, s := range srcs.Git {
// Git material URI is always the raw repository URL (same shape
// for bundle-backed and normal git sources).
out = append(out, slsa.ProvenanceMaterial{
URI: s.URL,
Digest: digestSetForCommit(s.Commit),
})
if s.Bundle == nil {
continue
}
// Bundle-backed git source: parse the canonical locator into
// its scheme / ref-body / digest components on demand. On parse
// failure (shouldn't happen — validateBundleAttrs rejects bad
// locators at LLB op construction time) skip bundle material
// emission rather than erroring.
scheme, refBody, bundleDgst := parseBundleLocatorURL(s.Bundle.URL)
if scheme == "" || refBody == "" || bundleDgst == "" {
continue
}
bundlePurlType := packageurl.TypeDocker
if scheme == srctypes.OCIBlobScheme {
bundlePurlType = packageurl.TypeOCI
}
bundleURI, err := purl.RefToPURL(bundlePurlType, refBody, nil)
if err != nil {
return nil, err
}
bundleURI, err = setPURLQualifier(bundleURI, packageurl.Qualifier{
Key: "ref_type",
Value: "bundle",
})
if err != nil {
return nil, err
}
bundleURI, err = setPURLQualifier(bundleURI, packageurl.Qualifier{
Key: "vcs_url",
Value: s.URL,
})
if err != nil {
return nil, err
}
out = append(out, slsa.ProvenanceMaterial{
URI: bundleURI,
Digest: slsa.DigestSet{
bundleDgst.Algorithm().String(): bundleDgst.Hex(),
},
})
}
for _, s := range srcs.HTTP {
@@ -86,6 +133,31 @@ func slsaMaterials(srcs provenancetypes.Sources) ([]slsa.ProvenanceMaterial, err
return out, nil
}
// parseBundleLocatorURL parses a "<scheme>://<ref>@<algo>:<hex>" bundle
// locator into its components. It returns empty values on parse failure; the
// caller handles that by falling back to the non-bundle emission path.
//
// The identifier package has a stricter parseBundleLocator used at LLB op
// construction time (validateBundleAttrs) that validates the reference and
// digest algorithm. We intentionally don't import that helper here because
// the identifier package imports this provenance package. Since the locator
// has already been validated upstream, the logic here just splits the
// locator back into its parts for purl emission.
func parseBundleLocatorURL(raw string) (scheme, refBody string, dgst digest.Digest) {
const sep = "://"
i := strings.Index(raw, sep)
if i <= 0 {
return "", "", ""
}
scheme = raw[:i]
body := raw[i+len(sep):]
at := strings.LastIndex(body, "@")
if at <= 0 {
return "", "", ""
}
return scheme, body[:at], digest.Digest(body[at+1:])
}
func digestSetForCommit(commit string) slsa.DigestSet {
dset := slsa.DigestSet{}
if len(commit) == 64 {
@@ -112,11 +184,15 @@ func setPURLQualifier(uri string, q packageurl.Qualifier) (string, error) {
}
func findMaterial(srcs provenancetypes.Sources, uri string) (*slsa.ProvenanceMaterial, bool) {
uri, _ = dfgitutil.FragmentFormat(uri)
configURI := uri
if formatted, ok := dfgitutil.FragmentFormat(uri, true); ok {
configURI = formatted
}
uri, _ = dfgitutil.FragmentFormat(uri, false)
for _, s := range srcs.Git {
if s.URL == uri {
return &slsa.ProvenanceMaterial{
URI: s.URL,
URI: configURI,
Digest: digestSetForCommit(s.Commit),
}, true
}
@@ -147,39 +223,25 @@ func NewPredicate(c *Capture) (*provenancetypes.ProvenancePredicateSLSA1, error)
})
}
args := maps.Clone(c.Args)
contextKey := "context"
if v, ok := args["contextkey"]; ok && v != "" {
contextKey = v
} else if v, ok := c.Args["input:context"]; ok && v != "" {
contextKey = "input:context"
}
ext := provenancetypes.ProvenanceExternalParametersSLSA1{}
if v, ok := args[contextKey]; ok && v != "" {
if m, ok := findMaterial(c.Sources, v); ok {
ext.ConfigSource.URI = m.URI
ext.ConfigSource.Digest = m.Digest
} else {
ext.ConfigSource.URI = v
}
ext.ConfigSource.URI = urlutil.RedactCredentials(ext.ConfigSource.URI)
delete(args, contextKey)
}
if v, ok := args["filename"]; ok && v != "" {
ext.ConfigSource.Path = v
delete(args, "filename")
}
reqProv := RequestProvenance(c.Request.Frontend, maps.Clone(c.Request.Args), c.Sources)
ext.ConfigSource = reqProv.ConfigSource
vcs := make(map[string]string)
for k, v := range args {
req := c.Request.Clone()
if req == nil {
req = &provenancetypes.Parameters{}
}
if reqProv.Request != nil {
req.Frontend = reqProv.Request.Frontend
req.Args = reqProv.Request.Args
}
for k, v := range req.Args {
if strings.HasPrefix(k, "vcs:") {
if k == "vcs:source" {
v = urlutil.RedactCredentials(v)
}
delete(args, k)
delete(req.Args, k)
if v != "" {
vcs[strings.TrimPrefix(k, "vcs:")] = v
}
@@ -189,27 +251,12 @@ func NewPredicate(c *Capture) (*provenancetypes.ProvenancePredicateSLSA1, error)
internal := provenancetypes.ProvenanceInternalParametersSLSA1{}
internal.BuilderPlatform = platforms.Format(platforms.Normalize(platforms.DefaultSpec()))
req := provenancetypes.Parameters{}
req.Frontend = c.Frontend
req.Args = args
for _, s := range c.Secrets {
req.Secrets = append(req.Secrets, &provenancetypes.Secret{
ID: s.ID,
Optional: s.Optional,
})
}
for _, s := range c.SSH {
req.SSH = append(req.SSH, &provenancetypes.SSH{
ID: s.ID,
Optional: s.Optional,
})
}
for _, s := range c.Sources.Local {
req.Locals = append(req.Locals, &provenancetypes.LocalSource{
Name: s.Name,
})
}
ext.Request = req
ext.Request = *req
incompleteMaterials := c.IncompleteMaterials
if !incompleteMaterials {
@@ -230,7 +277,7 @@ func NewPredicate(c *Capture) (*provenancetypes.ProvenancePredicateSLSA1, error)
RunDetails: provenancetypes.ProvenanceRunDetailsSLSA1{
Metadata: &provenancetypes.ProvenanceMetadataSLSA1{
Completeness: provenancetypes.BuildKitComplete{
Request: c.Frontend != "",
Request: c.Request.Frontend != "",
ResolvedDependencies: !incompleteMaterials,
},
Hermetic: !incompleteMaterials && !c.NetworkAccess,
@@ -245,6 +292,42 @@ func NewPredicate(c *Capture) (*provenancetypes.ProvenancePredicateSLSA1, error)
return pr, nil
}
func RequestProvenance(frontend string, args map[string]string, srcs provenancetypes.Sources) *provenancetypes.RequestProvenance {
args = maps.Clone(args)
contextKey := "context"
if v, ok := args["contextkey"]; ok && v != "" {
contextKey = v
} else if v, ok := args["input:context"]; ok && v != "" {
contextKey = "input:context"
}
ext := provenancetypes.RequestProvenance{
Request: &provenancetypes.Parameters{
Frontend: frontend,
Args: args,
},
}
if v, ok := args[contextKey]; ok && v != "" {
if m, ok := findMaterial(srcs, v); ok {
ext.ConfigSource.URI = m.URI
ext.ConfigSource.Digest = m.Digest
} else {
ext.ConfigSource.URI = v
}
ext.ConfigSource.URI = urlutil.RedactCredentials(ext.ConfigSource.URI)
delete(args, contextKey)
}
if v, ok := args["filename"]; ok && v != "" {
ext.ConfigSource.Path = v
delete(args, "filename")
}
if len(args) == 0 {
ext.Request.Args = nil
}
return &ext
}
func FilterArgs(m map[string]string) map[string]string {
var hostSpecificArgs = map[string]struct{}{
"cgroup-parent": {},
+162 -6
View File
@@ -71,6 +71,19 @@ type ImageBlobSource struct {
type GitSource struct {
URL string
Commit string
// Bundle, when non-nil, records the bundle blob that the git source
// was resolved from. Only present on bundle-backed git sources; nil
// for normal remote-backed git sources.
Bundle *GitBundle `json:"bundle,omitempty"`
}
// GitBundle describes the bundle blob a git source was resolved from. URL
// is the full locator (e.g. "docker-image+blob://example.com/repo@sha256:...")
// and is the canonical data: scheme, reference body, and digest are all
// derivable from it and are parsed on demand by consumers (such as the purl
// emitter in predicate.go).
type GitBundle struct {
URL string `json:"url"`
}
type HTTPSource struct {
@@ -82,16 +95,40 @@ type LocalSource struct {
Name string `json:"name"`
}
// Equal reports whether the local source matches another local source.
func (l *LocalSource) Equal(other *LocalSource) bool {
if l == nil || other == nil {
return l == other
}
return *l == *other
}
type Secret struct {
ID string `json:"id"`
Optional bool `json:"optional,omitempty"`
}
// Equal reports whether the secret matches another secret.
func (s *Secret) Equal(other *Secret) bool {
if s == nil || other == nil {
return s == other
}
return *s == *other
}
type SSH struct {
ID string `json:"id"`
Optional bool `json:"optional,omitempty"`
}
// Equal reports whether the SSH mount matches another SSH mount.
func (s *SSH) Equal(other *SSH) bool {
if s == nil || other == nil {
return s == other
}
return *s == *other
}
type Sources struct {
Images []ImageSource
ImageBlobs []ImageBlobSource
@@ -157,6 +194,17 @@ type ProvenanceConfigSourceSLSA1 struct {
Path string `json:"path,omitempty"`
}
// Clone returns a deep copy of the config source.
func (c ProvenanceConfigSourceSLSA1) Clone() ProvenanceConfigSourceSLSA1 {
c.Digest = maps.Clone(c.Digest)
return c
}
// Equal reports whether the config source matches another config source.
func (c ProvenanceConfigSourceSLSA1) Equal(other ProvenanceConfigSourceSLSA1) bool {
return c.URI == other.URI && c.Path == other.Path && maps.Equal(c.Digest, other.Digest)
}
type ProvenanceInternalParametersSLSA1 struct {
BuildConfig *BuildConfig `json:"buildConfig,omitempty"`
BuilderPlatform string `json:"builderPlatform"`
@@ -172,13 +220,121 @@ type ProvenanceMetadataSLSA1 struct {
}
type Parameters struct {
Frontend string `json:"frontend,omitempty"`
Args map[string]string `json:"args,omitempty"`
Secrets []*Secret `json:"secrets,omitempty"`
SSH []*SSH `json:"ssh,omitempty"`
Locals []*LocalSource `json:"locals,omitempty"`
Frontend string `json:"frontend,omitempty"`
Args map[string]string `json:"args,omitempty"`
Secrets []*Secret `json:"secrets,omitempty"`
SSH []*SSH `json:"ssh,omitempty"`
Locals []*LocalSource `json:"locals,omitempty"`
Inputs map[string]*RequestProvenance `json:"inputs,omitempty"`
Root *RequestProvenance `json:"root,omitempty"`
CompatibilityVersion int `json:"compatibilityVersion,omitempty"`
// TODO: select export attributes
// TODO: frontend inputs
}
// Clone returns a deep copy of the request parameters.
func (p *Parameters) Clone() *Parameters {
if p == nil {
return nil
}
out := &Parameters{
Frontend: p.Frontend,
Args: maps.Clone(p.Args),
CompatibilityVersion: p.CompatibilityVersion,
}
if len(p.Secrets) > 0 {
out.Secrets = slices.Clone(p.Secrets)
for i, s := range out.Secrets {
if s != nil {
s2 := *s
out.Secrets[i] = &s2
}
}
}
if len(p.SSH) > 0 {
out.SSH = slices.Clone(p.SSH)
for i, s := range out.SSH {
if s != nil {
s2 := *s
out.SSH[i] = &s2
}
}
}
if len(p.Locals) > 0 {
out.Locals = slices.Clone(p.Locals)
for i, l := range out.Locals {
if l != nil {
l2 := *l
out.Locals[i] = &l2
}
}
}
if len(p.Inputs) > 0 {
out.Inputs = make(map[string]*RequestProvenance, len(p.Inputs))
for k, in := range p.Inputs {
out.Inputs[k] = in.Clone()
}
}
out.Root = p.Root.Clone()
return out
}
// Equal reports whether the request parameters match another set of parameters.
func (p *Parameters) Equal(other *Parameters) bool {
if p == nil || other == nil {
return p == other
}
if p.Frontend != other.Frontend || p.CompatibilityVersion != other.CompatibilityVersion || !maps.Equal(p.Args, other.Args) {
return false
}
if !slices.EqualFunc(p.Secrets, other.Secrets, func(a, b *Secret) bool {
return a.Equal(b)
}) {
return false
}
if !slices.EqualFunc(p.SSH, other.SSH, func(a, b *SSH) bool {
return a.Equal(b)
}) {
return false
}
if !slices.EqualFunc(p.Locals, other.Locals, func(a, b *LocalSource) bool {
return a.Equal(b)
}) {
return false
}
if len(p.Inputs) != len(other.Inputs) {
return false
}
for k, v := range p.Inputs {
v2, ok := other.Inputs[k]
if !ok || !v.Equal(v2) {
return false
}
}
return p.Root.Equal(other.Root)
}
type RequestProvenance struct {
ConfigSource ProvenanceConfigSourceSLSA1 `json:"configSource"`
Request *Parameters `json:"request,omitempty"`
}
// Clone returns a deep copy of the request provenance.
func (r *RequestProvenance) Clone() *RequestProvenance {
if r == nil {
return nil
}
return &RequestProvenance{
ConfigSource: r.ConfigSource.Clone(),
Request: r.Request.Clone(),
}
}
// Equal reports whether the request provenance matches another request provenance.
func (r *RequestProvenance) Equal(other *RequestProvenance) bool {
if r == nil || other == nil {
return r == other
}
return r.ConfigSource.Equal(other.ConfigSource) && r.Request.Equal(other.Request)
}
type Environment struct {
+247
View File
@@ -0,0 +1,247 @@
package llbsolver
import (
"slices"
"sync"
"github.com/moby/buildkit/frontend"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/solver/llbsolver/provenance"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
"github.com/moby/buildkit/solver/pb"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
type provenanceStore struct {
mu sync.Mutex
records map[string]*provenanceRecord
byDigest map[digest.Digest]map[string]struct{}
}
type provenanceRecord struct {
defDigest digest.Digest
request *provenancetypes.RequestProvenance
}
func newProvenanceStore() *provenanceStore {
return &provenanceStore{
records: map[string]*provenanceRecord{},
byDigest: map[digest.Digest]map[string]struct{}{},
}
}
func (s *provenanceStore) register(def *pb.Definition, req *provenancetypes.RequestProvenance) (string, digest.Digest, error) {
if s == nil || def == nil || req == nil {
return "", "", nil
}
dgst, err := definitionHeadDigest(def)
if err != nil {
return "", "", err
}
if dgst == "" {
return "", "", nil
}
recordID := identity.NewID()
s.mu.Lock()
s.records[recordID] = &provenanceRecord{
defDigest: dgst,
request: req.Clone(),
}
if s.byDigest[dgst] == nil {
s.byDigest[dgst] = map[string]struct{}{}
}
s.byDigest[dgst][recordID] = struct{}{}
s.mu.Unlock()
return recordID, dgst, nil
}
func (s *provenanceStore) unregister(recordIDs []string) {
if s == nil || len(recordIDs) == 0 {
return
}
s.mu.Lock()
for _, recordID := range recordIDs {
rec := s.records[recordID]
delete(s.records, recordID)
if rec != nil {
delete(s.byDigest[rec.defDigest], recordID)
if len(s.byDigest[rec.defDigest]) == 0 {
delete(s.byDigest, rec.defDigest)
}
}
}
s.mu.Unlock()
}
func (s *provenanceStore) lookup(def *pb.Definition) (*provenancetypes.RequestProvenance, bool) {
if s == nil || def == nil {
return nil, false
}
dgst, err := definitionHeadDigest(def)
if err != nil || dgst == "" {
return nil, false
}
s.mu.Lock()
records := make([]*provenanceRecord, 0, len(s.byDigest[dgst]))
for recordID := range s.byDigest[dgst] {
if rec := s.records[recordID]; rec != nil && rec.request != nil {
records = append(records, rec)
}
}
s.mu.Unlock()
if len(records) == 0 {
return nil, false
}
req := records[0].request.Clone()
if req.Request != nil {
req.Request.Root = nil
}
for _, rec := range records[1:] {
req2 := rec.request.Clone()
if req2.Request != nil {
req2.Request.Root = nil
}
if !req.Equal(req2) {
return nil, false
}
}
return req, true
}
func (b *provenanceBridge) registerProvenanceRefs(res *frontend.Result) error {
if b == nil || res == nil {
return nil
}
return res.EachRef(func(ref solver.ResultProxy) error {
if ref == nil {
return nil
}
return b.registerProvenanceRef(ref.Definition(), ref)
})
}
func (b *provenanceBridge) registerProvenanceRef(def *pb.Definition, ref solver.ResultProxy) error {
if def == nil || ref == nil || b.provenanceStore == nil {
return nil
}
req, ok := b.findByResult(ref)
if !ok {
return nil
}
var srcs provenancetypes.Sources
if p := ref.Provenance(); p != nil {
c, ok := p.(*provenance.Capture)
if !ok {
return errors.Errorf("invalid provenance type %T", p)
}
if c != nil {
srcs = c.Sources
}
}
reqProv := req.bridge.requestProvenance(srcs)
recordID, _, err := b.provenanceStore.register(def, reqProv)
if err != nil {
return err
}
if recordID == "" {
return nil
}
b.addProvenanceRefRecordID(recordID)
return nil
}
func (b *provenanceBridge) addProvenanceRefRecordID(recordID string) {
if b == nil || recordID == "" {
return
}
b.mu.Lock()
b.provenanceRefRecordIDs = append(b.provenanceRefRecordIDs, recordID)
b.mu.Unlock()
}
func (b *provenanceBridge) releaseProvenanceRefs() {
if b == nil || b.provenanceStore == nil {
return
}
for _, sb := range b.subBridges {
sb.releaseProvenanceRefs()
}
b.mu.Lock()
recordIDs := slices.Clone(b.provenanceRefRecordIDs)
b.provenanceRefRecordIDs = nil
b.mu.Unlock()
b.provenanceStore.unregister(recordIDs)
}
func (b *provenanceBridge) requestProvenance(srcs provenancetypes.Sources) *provenancetypes.RequestProvenance {
if b == nil || b.req == nil {
return nil
}
req := provenance.RequestProvenance(b.req.Frontend, provenance.FilterArgs(b.req.FrontendOpt), srcs)
if req.Request == nil {
req.Request = &provenancetypes.Parameters{}
}
if inputs := b.inputProvenance(b.req.FrontendInputs); len(inputs) > 0 {
req.Request.Inputs = inputs
}
if root := b.rootRequestProvenance(srcs); root != nil && !root.Equal(req) {
req.Request.Root = root
}
return req
}
func (b *provenanceBridge) inputProvenance(inputs map[string]*pb.Definition) map[string]*provenancetypes.RequestProvenance {
if len(inputs) == 0 || b == nil || b.provenanceStore == nil {
return nil
}
out := make(map[string]*provenancetypes.RequestProvenance)
for name, def := range inputs {
if in, ok := b.provenanceStore.lookup(def); ok {
out[name] = in
}
}
if len(out) == 0 {
return nil
}
return out
}
func (b *provenanceBridge) rootRequestProvenance(srcs provenancetypes.Sources) *provenancetypes.RequestProvenance {
if b == nil || !hasRequestProvenance(b.rootReq) {
return nil
}
root := provenance.RequestProvenance(b.rootReq.Frontend, provenance.FilterArgs(b.rootReq.FrontendOpt), srcs)
if root.Request == nil {
root.Request = &provenancetypes.Parameters{}
}
if inputs := b.inputProvenance(b.rootReq.FrontendInputs); len(inputs) > 0 {
root.Request.Inputs = inputs
}
if root.Request.Frontend == "" && len(root.Request.Args) == 0 && len(root.Request.Inputs) == 0 {
return nil
}
return root
}
func hasRequestProvenance(req *frontend.SolveRequest) bool {
if req == nil {
return false
}
return req.Frontend != "" || len(req.FrontendOpt) > 0 || len(req.FrontendInputs) > 0
}
func definitionHeadDigest(def *pb.Definition) (digest.Digest, error) {
if def == nil || len(def.Def) == 0 {
return "", nil
}
var op pb.Op
if err := op.UnmarshalVT(def.Def[len(def.Def)-1]); err != nil {
return "", errors.Wrap(err, "failed to parse llb proto op")
}
if len(op.Inputs) == 0 {
return "", nil
}
return digest.Digest(op.Inputs[0].Digest), nil
}
+29 -5
View File
@@ -2,6 +2,7 @@ package llbsolver
import (
"context"
"maps"
"os"
"strings"
"time"
@@ -19,6 +20,7 @@ import (
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/solver/llbsolver/compat"
"github.com/moby/buildkit/solver/llbsolver/history"
"github.com/moby/buildkit/solver/result"
spb "github.com/moby/buildkit/sourcepolicy/pb"
@@ -73,6 +75,7 @@ type Solver struct {
history *history.Queue
sysSampler *resources.Sampler[*resourcestypes.SysSample]
provenanceEnv map[string]any
provenanceStore *provenanceStore
}
// Processor defines a processing function to be applied after solving, but
@@ -103,6 +106,7 @@ func New(opt Opt) (*Solver, error) {
entitlements: opt.Entitlements,
history: opt.HistoryQueue,
provenanceEnv: opt.ProvenanceEnv,
provenanceStore: newProvenanceStore(),
}
sampler, err := resources.NewSysSampler()
@@ -145,6 +149,7 @@ func (s *Solver) bridge(b solver.Builder) *provenanceBridge {
resolveCacheImporterFuncs: s.resolveCacheImporterFuncs,
cms: map[string]solver.CacheManager{},
sm: s.sm,
provenanceStore: s.provenanceStore,
}}
}
@@ -152,7 +157,21 @@ func (s *Solver) Bridge(b solver.Builder) frontend.FrontendLLBBridge {
return s.bridge(b)
}
func (s *Solver) Solve(ctx context.Context, id string, sessionID string, req frontend.SolveRequest, exp ExporterRequest, ent []entitlements.Entitlement, post []Processor, internal bool, srcPol *spb.Policy, policySession string) (_ *client.SolveResponse, err error) {
func (s *Solver) Solve(ctx context.Context, id string, sessionID string, req frontend.SolveRequest, compatibilityVersion int, exp ExporterRequest, ent []entitlements.Entitlement, post []Processor, internal bool, srcPol *spb.Policy, policySession string) (_ *client.SolveResponse, err error) {
hasNamedDockerfileContext := false
for k := range req.FrontendOpt {
if k == "context:dockerfile.v0" || strings.HasPrefix(k, "context:dockerfile.v0::") {
hasNamedDockerfileContext = true
break
}
}
if req.Frontend == "gateway.v0" && req.FrontendOpt[frontend.KeySource] == "dockerfile.v0" && !hasNamedDockerfileContext {
frontendOpt := maps.Clone(req.FrontendOpt)
delete(frontendOpt, frontend.KeySource)
req.Frontend = "dockerfile.v0"
req.FrontendOpt = frontendOpt
}
j, err := s.solver.NewJob(id)
if err != nil {
return nil, err
@@ -201,10 +220,17 @@ func (s *Solver) Solve(ctx context.Context, id string, sessionID string, req fro
if policySession != "" {
j.SetValue(keySourcePolicySession, policySession)
}
if compatibilityVersion == 0 {
compatibilityVersion = compat.CompatibilityVersionCurrent
}
j.SetValue(compat.JobValueKey, compatibilityVersion)
j.SessionID = sessionID
br := s.bridge(j)
defer br.releaseProvenanceRefs()
rootReq := req.Clone()
br.rootReq = &rootReq
var fwd gateway.LLBBridgeForwarder
if s.gatewayForwarder != nil && req.Definition == nil && req.Frontend == "" {
fwd = gateway.NewBridgeForwarder(ctx, br, br, s.workerController.Infos(), req.FrontendInputs, sessionID, s.sm)
@@ -213,10 +239,8 @@ func (s *Solver) Solve(ctx context.Context, id string, sessionID string, req fro
// s.recordBuildHistory can block for several seconds on
// LeaseManager calls, and there is a fixed 3s timeout in
// GatewayForwarder on build registration.
if err := s.gatewayForwarder.RegisterBuild(ctx, id, fwd); err != nil {
return nil, err
}
defer s.gatewayForwarder.UnregisterBuild(context.WithoutCancel(ctx), id)
s.gatewayForwarder.RegisterBuild(ctx, id, fwd)
defer s.gatewayForwarder.UnregisterBuild(context.Background(), id)
}
if !internal {
+3
View File
@@ -9,6 +9,9 @@ const AttrMountSSHSock = "git.mountsshsock"
const AttrGitChecksum = "git.checksum"
const AttrGitSkipSubmodules = "git.skipsubmodules"
const AttrGitMTime = "git.mtime"
const AttrGitFetchByCommit = "git.fetchbycommit"
const AttrGitBundle = "git.bundle"
const AttrGitCheckoutBundle = "git.checkoutbundle"
const AttrGitSignatureVerifyPubKey = "git.sig.pubkey"
const AttrGitSignatureVerifyRejectExpired = "git.sig.rejectexpired"
+21
View File
@@ -35,6 +35,9 @@ const (
CapSourceGitSkipSubmodules apicaps.CapID = "source.git.skipsubmodules"
CapSourceGitSignatureVerify apicaps.CapID = "source.git.signatureverify"
CapSourceGitMTime apicaps.CapID = "source.git.mtime"
CapSourceGitFetchByCommit apicaps.CapID = "source.git.fetchbycommit"
CapSourceGitBundle apicaps.CapID = "source.git.bundle"
CapSourceGitCheckoutBundle apicaps.CapID = "source.git.checkoutbundle"
CapSourceHTTP apicaps.CapID = "source.http"
CapSourceHTTPAuth apicaps.CapID = "source.http.auth"
@@ -262,6 +265,24 @@ func init() {
Status: apicaps.CapStatusExperimental,
})
Caps.Init(apicaps.Cap{
ID: CapSourceGitFetchByCommit,
Enabled: true,
Status: apicaps.CapStatusExperimental,
})
Caps.Init(apicaps.Cap{
ID: CapSourceGitBundle,
Enabled: true,
Status: apicaps.CapStatusExperimental,
})
Caps.Init(apicaps.Cap{
ID: CapSourceGitCheckoutBundle,
Enabled: true,
Status: apicaps.CapStatusExperimental,
})
Caps.Init(apicaps.Cap{
ID: CapSourceHTTP,
Enabled: true,
+2
View File
@@ -184,6 +184,8 @@ type JobContext interface {
// ResolverCache returns object for memorizing/synchronizing remote resolving decisions during the job.
// Steps from same build job will share the same resolver cache.
ResolverCache() ResolverCache
// CompatibilityVersion returns the solve-wide compatibility version for the current job.
CompatibilityVersion() (int, error)
}
type ResolverCache interface {
+128
View File
@@ -0,0 +1,128 @@
// Package blobfetch provides a shared helper for resolving a single
// content-addressable blob from either a docker-image registry or an OCI layout
// content store, identified by its digest. It is factored out of the
// containerblob source so other consumers (for example the git-bundle flow on
// the git source) can reuse the same resolution logic without depending on the
// source implementation.
package blobfetch
import (
"context"
"io"
"time"
"github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/moby/buildkit/session"
sessioncontent "github.com/moby/buildkit/session/content"
srctypes "github.com/moby/buildkit/source/types"
"github.com/moby/buildkit/util/iohelper"
"github.com/moby/buildkit/util/resolver"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// FetchOpt describes how to resolve a blob.
type FetchOpt struct {
// Scheme identifies the source kind. Must be one of
// srctypes.DockerImageBlobScheme or srctypes.OCIBlobScheme.
Scheme string
// Ref is the image-ref form of the blob, i.e. "<name>@<digest>" for
// registry blobs, or the reference body accepted by the containerd
// reference parser for OCI-layout blobs.
Ref string
// Digest is the blob digest to retrieve.
Digest digest.Digest
// RegistryHosts is used when Scheme is DockerImageBlobScheme.
RegistryHosts docker.RegistryHosts
// SessionManager is used to reach the client when Scheme is
// OCIBlobScheme.
SessionManager *session.Manager
// SessionID, if set, pins the OCI-layout fetch to a specific client
// session.
SessionID string
// StoreID is the client-side OCI-layout store name. Required for
// Scheme == OCIBlobScheme.
StoreID string
}
// FetchBlob opens a read stream for the requested blob. The caller owns the
// returned ReadCloser and must Close it. The returned digest is the blob
// digest from the locator (echoed for convenience).
func FetchBlob(ctx context.Context, g session.Group, opt FetchOpt) (io.ReadCloser, digest.Digest, error) {
if err := opt.Digest.Validate(); err != nil {
return nil, "", errors.Wrap(err, "invalid blob digest")
}
switch opt.Scheme {
case srctypes.OCIBlobScheme:
if opt.StoreID == "" {
return nil, "", errors.Errorf("oci-layout blob source requires store id")
}
rc, err := fetchFromOCILayoutStore(ctx, g, opt)
if err != nil {
return nil, "", err
}
return rc, opt.Digest, nil
case srctypes.DockerImageBlobScheme:
r := resolver.DefaultPool.GetResolver(opt.RegistryHosts, opt.Ref, resolver.ScopeType{}, opt.SessionManager, g)
f, err := r.Fetcher(ctx, opt.Ref)
if err != nil {
return nil, "", err
}
fd, ok := f.(remotes.FetcherByDigest)
if !ok {
return nil, "", errors.Errorf("invalid blob fetcher: %T", f)
}
rc, _, err := fd.FetchByDigest(ctx, opt.Digest)
if err != nil {
return nil, "", err
}
return rc, opt.Digest, nil
default:
return nil, "", errors.Errorf("unsupported blob scheme %q", opt.Scheme)
}
}
func fetchFromOCILayoutStore(ctx context.Context, g session.Group, opt FetchOpt) (io.ReadCloser, error) {
var rc io.ReadCloser
err := withOCICaller(ctx, g, opt, func(ctx context.Context, caller session.Caller) error {
store := sessioncontent.NewCallerStore(caller, "oci:"+opt.StoreID)
info, err := store.Info(ctx, opt.Digest)
if err != nil {
return err
}
readerAt, err := store.ReaderAt(ctx, ocispecs.Descriptor{
Digest: info.Digest,
Size: info.Size,
})
if err != nil {
return err
}
rc = iohelper.ReadCloser(readerAt)
return nil
})
if err != nil {
return nil, err
}
return rc, nil
}
func withOCICaller(ctx context.Context, g session.Group, opt FetchOpt, f func(context.Context, session.Caller) error) error {
if opt.SessionID != "" {
timeoutCtx, cancel := context.WithCancelCause(ctx)
timeoutCtx, _ = context.WithTimeoutCause(timeoutCtx, 5*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet
defer func() { cancel(errors.WithStack(context.Canceled)) }()
caller, err := opt.SessionManager.Get(timeoutCtx, opt.SessionID, false)
if err != nil {
return err
}
return f(ctx, caller)
}
return opt.SessionManager.Any(ctx, g, func(ctx context.Context, _ string, caller session.Caller) error {
return f(ctx, caller)
})
}
+12 -78
View File
@@ -9,20 +9,15 @@ import (
"os"
"time"
"github.com/containerd/containerd/v2/core/remotes"
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/session"
sessioncontent "github.com/moby/buildkit/session/content"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver"
srctypes "github.com/moby/buildkit/source/types"
"github.com/moby/buildkit/source/containerblob/blobfetch"
"github.com/moby/buildkit/source/util/pathutil"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/iohelper"
"github.com/moby/buildkit/util/resolver"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
@@ -68,31 +63,17 @@ func (p *puller) ensureResolver(ctx context.Context, g session.Group) error {
return errors.Wrap(err, "invalid reference digest")
}
var (
rc io.ReadCloser
err error
)
if p.id.Scheme() == srctypes.OCIBlobScheme {
rc, err = p.fetchFromOCILayoutStore(ctx, g, dgst)
if err != nil {
return err
}
} else {
r := resolver.DefaultPool.GetResolver(p.src.RegistryHosts, p.id.Reference.String(), resolver.ScopeType{}, p.SessionManager, g)
f, err := r.Fetcher(ctx, p.id.Reference.String())
if err != nil {
return err
}
fd, ok := f.(remotes.FetcherByDigest)
if !ok {
return errors.Errorf("invalid blob fetcher: %T", f)
}
rc, _, err = fd.FetchByDigest(ctx, dgst)
if err != nil {
return err
}
rc, _, err := blobfetch.FetchBlob(ctx, g, blobfetch.FetchOpt{
Scheme: p.id.Scheme(),
Ref: p.id.Reference.String(),
Digest: dgst,
RegistryHosts: p.src.RegistryHosts,
SessionManager: p.SessionManager,
SessionID: p.id.SessionID,
StoreID: p.id.StoreID,
})
if err != nil {
return err
}
p.rc = rc
@@ -100,53 +81,6 @@ func (p *puller) ensureResolver(ctx context.Context, g session.Group) error {
return nil
}
func (p *puller) fetchFromOCILayoutStore(ctx context.Context, g session.Group, dgst digest.Digest) (io.ReadCloser, error) {
if p.id.StoreID == "" {
return nil, errors.Errorf("oci-layout blob source requires store id")
}
var rc io.ReadCloser
err := p.withOCICaller(ctx, g, func(ctx context.Context, caller session.Caller) error {
store := sessioncontent.NewCallerStore(caller, "oci:"+p.id.StoreID)
info, err := store.Info(ctx, dgst)
if err != nil {
return err
}
readerAt, err := store.ReaderAt(ctx, ocispecs.Descriptor{
Digest: info.Digest,
Size: info.Size,
})
if err != nil {
return err
}
rc = iohelper.ReadCloser(readerAt)
return nil
})
if err != nil {
return nil, err
}
return rc, nil
}
func (p *puller) withOCICaller(ctx context.Context, g session.Group, f func(context.Context, session.Caller) error) error {
if p.id.SessionID != "" {
timeoutCtx, cancel := context.WithCancelCause(ctx)
timeoutCtx, _ = context.WithTimeoutCause(timeoutCtx, 5*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet
defer func() { cancel(errors.WithStack(context.Canceled)) }()
caller, err := p.SessionManager.Get(timeoutCtx, p.id.SessionID, false)
if err != nil {
return err
}
return f(ctx, caller)
}
return p.SessionManager.Any(ctx, g, func(ctx context.Context, _ string, caller session.Caller) error {
return f(ctx, caller)
})
}
func (p *puller) CacheKey(ctx context.Context, jobCtx solver.JobContext, index int) (cacheKey string, imgDigest string, cacheOpts solver.CacheOpts, cacheDone bool, err error) {
dgst := p.id.Reference.Digest()
if err := dgst.Validate(); err != nil {
+487
View File
@@ -0,0 +1,487 @@
package git
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/source/containerblob/blobfetch"
srctypes "github.com/moby/buildkit/source/types"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/gitutil"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
const (
// bundleFileName is the name of the bundle file written at the
// checkout mount root when git.checkoutbundle is set. Documented on
// the GitCheckoutBundle godoc.
bundleFileName = "bundle"
// bundleImportFileName is the on-disk name of the transient bundle
// file streamed in from the blob locator, colocated with the bare
// repo and removed once the import completes.
bundleImportFileName = "bundle.pack"
// bundleFallbackRef is the ref name used when the user's ref is empty
// or a commit SHA. Bundle creation needs a named ref so the consumer's
// fetch refspec has something to land; consumer-side resolution
// short-circuits on SHA when Ref is empty/SHA, so the fallback name
// is not user-visible.
bundleFallbackRef = "refs/heads/main"
)
// bundleTargetRef normalizes the user's ref into a fully-qualified ref name
// suitable as the tip ref inside the emitted bundle.
//
// - Empty / commit SHA: falls back to bundleFallbackRef. The consumer
// short-circuits on SHA so the chosen branch name is not user-visible,
// but bundle creation needs a named ref for the fetch refspec to land.
// - "refs/tags/<name>": preserved as a tag ref so a tag-addressed checkout
// keeps the tag qualifier on the consumer side.
// - Anything else (e.g. "master", "refs/heads/main", "v1"): stripped of a
// leading refs/ or heads/ qualifier and re-prefixed with refs/heads/.
//
// Design note: refs/tags/<x> is preserved, but a bare "v1" is mapped to
// refs/heads/v1 rather than refs/tags/v1 because we cannot tell from a
// symbolic ref alone whether it originated as a tag or a branch; refs/heads/
// is the safer default for tip placement.
func bundleTargetRef(ref string) string {
if ref == "" || gitutil.IsCommitSHA(ref) {
return bundleFallbackRef
}
if strings.HasPrefix(ref, "refs/tags/") {
return ref
}
name := strings.TrimPrefix(ref, "refs/")
name = strings.TrimPrefix(name, "heads/")
return "refs/heads/" + name
}
// detectBundleSHA256 probes a raw git bundle and returns whether it carries
// sha256 object IDs. git ls-remote reads bundle files directly, so this works
// before a destination repo exists and lets callers initialize the right object
// format for a subsequent fetch/import.
func detectBundleSHA256(ctx context.Context, bundlePath string) (bool, error) {
buf, err := gitCLI().Run(ctx, "ls-remote", "--", bundlePath)
if err != nil {
return false, errors.Wrapf(err, "failed to inspect git bundle %s", bundlePath)
}
for line := range strings.SplitSeq(string(buf), "\n") {
if line == "" {
continue
}
sha, _, _ := strings.Cut(line, "\t")
if !gitutil.IsCommitSHA(sha) {
return false, errors.Errorf("failed to inspect git bundle %s: invalid object ID %q", bundlePath, sha)
}
return len(sha) == 64, nil
}
return false, errors.Errorf("failed to inspect git bundle %s: no refs found", bundlePath)
}
// stageBundle downloads the bundle blob into an isolated temp bare repo,
// imports it, and validates that the pinned commit is present. The returned
// URL (file://<tmpRepoDir>) is suitable for use as a git fetch source (for
// the main fetch flow) or as the remote URL for an ls-remote call (for
// metadata resolution), so both paths converge on the same git primitives
// as a normal origin fetch.
//
// The returned cleanup removes the temp dir and must be called once the
// caller is done with the staged URL.
func (gs *gitSourceHandler) stageBundle(ctx context.Context, g session.Group) (_ string, _ func() error, retErr error) {
scheme, ref, storeID, dgst, err := parseBundleLocator(gs.src.Bundle)
if err != nil {
return "", nil, err
}
tmpDir, err := os.MkdirTemp("", "buildkit-bundle-")
if err != nil {
return "", nil, errors.Wrap(err, "failed to create temp dir for bundle")
}
cleanup := func() error { return os.RemoveAll(tmpDir) }
defer func() {
if retErr != nil {
cleanup()
}
}()
if err := gs.downloadBundleToFile(ctx, g, scheme, ref, storeID, dgst, tmpDir, bundleImportFileName); err != nil {
return "", nil, err
}
bundlePath := filepath.Join(tmpDir, bundleImportFileName)
sha256, err := detectBundleSHA256(ctx, bundlePath)
if err != nil {
return "", nil, err
}
gs.sha256 = sha256
// Initialize an isolated bare repo in the temp dir and import the
// bundle there. This lets us validate the bundle and stage the refs
// under their natural names without touching the shared bare repo.
tmpRepoDir := filepath.Join(tmpDir, "repo.git")
if err := os.Mkdir(tmpRepoDir, 0700); err != nil {
return "", nil, errors.Wrap(err, "failed to create temp bare repo dir")
}
tmpGit := gitCLI(gitutil.WithGitDir(tmpRepoDir))
initArgs := []string{"-c", "init.defaultBranch=master", "init", "--bare"}
if sha256 {
initArgs = append(initArgs, "--object-format=sha256")
}
if _, err := tmpGit.Run(ctx, initArgs...); err != nil {
return "", nil, errors.Wrap(err, "failed to init temp bare repo")
}
if _, err := tmpGit.Run(ctx, "fetch", bundlePath, "+refs/*:refs/*"); err != nil {
return "", nil, errors.Wrapf(err, "failed to import git bundle %s into temp", ref)
}
// Confirm the pinned commit is actually present in the bundle. The
// subsequent fetch in the main flow would surface a generic "failed to
// fetch" error; this check gives a clearer message.
if _, err := tmpGit.Run(ctx, "cat-file", "-e", gs.src.Checksum+"^{commit}"); err != nil {
return "", nil, errors.Errorf("commit %s not found in bundle %s", gs.src.Checksum, ref)
}
return "file://" + tmpRepoDir, cleanup, nil
}
// ensureStagedBundle returns the file:// URL of a staged temp bare repo
// produced from gs.src.Bundle, lazily staging it on first call and reusing
// the same staging dir on subsequent calls for the lifetime of gs.
//
// The call-site contract is: callers may call ensureStagedBundle any number
// of times and must never run the cleanup themselves. Cleanup is attached to
// the job via JobContext.Cleanup on the first staging so it fires at
// end-of-solve regardless of whether Snapshot runs (covering the
// CacheKey-then-cache-hit path where tryRemoteFetch never executes). If no
// jobCtx is available (e.g. ResolveMetadata path with a nil jobCtx), the
// returned cleanup is retained on the handler and teardown must come from a
// later call that does provide a jobCtx, or from the caller running
// releaseStagedBundle explicitly.
//
// Handler methods run sequentially per solve, so ensureStagedBundle does not
// guard against concurrent calls.
func (gs *gitSourceHandler) ensureStagedBundle(ctx context.Context, jobCtx solver.JobContext, g session.Group) (string, error) {
if gs.stagedBundleURL != "" {
return gs.stagedBundleURL, nil
}
url, cleanup, err := gs.stageBundle(ctx, g)
if err != nil {
return "", err
}
// Make the cleanup idempotent so it is safe to call from both the
// job cleanup path and an explicit releaseStagedBundle (or the
// CacheKey fallback path below) without double-removing the temp dir.
once := sync.OnceValue(cleanup)
gs.stagedBundleURL = url
gs.stagedBundleCleanup = once
if jobCtx != nil {
if err := jobCtx.Cleanup(func() error { return once() }); err != nil {
// Failed to register the job cleanup — run it synchronously
// so we do not leak the temp dir.
_ = once()
gs.stagedBundleURL = ""
gs.stagedBundleCleanup = nil
return "", err
}
// The job cleanup now owns the teardown; clearing the
// per-handler pointer avoids a second invocation from any
// fallback path. The stored url stays set so repeat callers on
// this handler get the cached URL for the remainder of the
// solve, before the job cleanup fires.
gs.stagedBundleCleanup = nil
} else {
bklog.G(ctx).Debug("bundle staged without jobCtx; cleanup deferred to handler teardown")
}
return url, nil
}
// releaseStagedBundle tears down any staged bundle owned by the handler. It
// is a no-op when the cleanup has already been attached to a JobContext or
// when nothing was staged. Safe to call multiple times.
func (gs *gitSourceHandler) releaseStagedBundle() error {
cleanup := gs.stagedBundleCleanup
gs.stagedBundleCleanup = nil
gs.stagedBundleURL = ""
if cleanup == nil {
return nil
}
return cleanup()
}
// downloadBundleToFile streams the bundle blob into a file named bundleName
// inside bundleDir while verifying its digest against the locator. The file
// is created via os.Root so a pre-existing symlink at bundleName inside
// bundleDir cannot redirect the write outside of bundleDir.
func (gs *gitSourceHandler) downloadBundleToFile(ctx context.Context, g session.Group, scheme, ref, storeID string, expectedDigest digest.Digest, bundleDir, bundleName string) (retErr error) {
rc, err := gs.openBundleBlob(ctx, g, scheme, ref, storeID, expectedDigest)
if err != nil {
return errors.Wrapf(err, "failed to fetch git bundle %s", ref)
}
defer rc.Close()
bundleDirRoot, err := os.OpenRoot(bundleDir)
if err != nil {
return errors.Wrapf(err, "failed to open bundle dir root %s", bundleDir)
}
defer bundleDirRoot.Close()
f, err := bundleDirRoot.Create(bundleName)
if err != nil {
return errors.Wrapf(err, "failed to create bundle file %s", bundleName)
}
defer func() {
if retErr != nil {
f.Close()
bundleDirRoot.Remove(bundleName)
}
}()
d := digest.SHA256.Digester()
if _, err := io.Copy(io.MultiWriter(f, d.Hash()), rc); err != nil {
return errors.Wrapf(err, "failed to download git bundle %s", ref)
}
if err := f.Close(); err != nil {
return errors.Wrapf(err, "failed to close bundle file %s", bundleName)
}
got := d.Digest()
if expectedDigest != "" && got != expectedDigest {
return errors.Errorf("expected checksum to match %s, got %s", expectedDigest, got)
}
return nil
}
// openBundleBlob resolves the blob using blobfetch. storeID is the pre-derived
// OCI-layout store id (empty for registry blobs).
func (gs *gitSourceHandler) openBundleBlob(ctx context.Context, g session.Group, scheme, ref, storeID string, dgst digest.Digest) (io.ReadCloser, error) {
opt := blobfetch.FetchOpt{
Scheme: scheme,
Ref: ref,
Digest: dgst,
RegistryHosts: gs.registryHosts,
SessionManager: gs.sm,
}
if scheme == srctypes.OCIBlobScheme {
opt.SessionID = gs.src.BundleOCISessionID
opt.StoreID = storeID
if gs.src.BundleOCIStoreID != "" {
opt.StoreID = gs.src.BundleOCIStoreID
}
}
rc, _, err := blobfetch.FetchBlob(ctx, g, opt)
return rc, err
}
// resolveBundleMetadata stages the bundle in an isolated temp bare repo and
// delegates to the shared ls-remote code path using that repo's file:// URL.
// Bundle mode lands refs in refs/heads/* and refs/tags/* just like a normal
// origin fetch, so the resolution logic is identical once the URL swap is
// done — and the resulting Metadata.Ref has the same shape as non-bundle
// mode (e.g. "refs/heads/master" for symbolic ref "master").
//
// For a pinned-commit / empty user ref, short-circuit: the user has already
// told us the commit, so there is nothing for ls-remote to add and no point
// downloading the bundle just for metadata resolution. The bundle will still
// be staged during the Snapshot path if the commit is not already present.
func (gs *gitSourceHandler) resolveBundleMetadata(ctx context.Context, jobCtx solver.JobContext) (*Metadata, error) {
// Bundle mode requires git.checksum; if Ref is empty or a SHA, the
// commit is already pinned and no lookup is needed. Report Ref and
// Checksum as the commit SHA — same shape as the commit-SHA short
// circuit in non-bundle resolveMetadata.
if gs.src.Ref == "" || gitutil.IsCommitSHA(gs.src.Ref) {
ref := gs.src.Ref
if ref == "" {
ref = gs.src.Checksum
}
if gs.src.Checksum != "" && !strings.HasPrefix(ref, gs.src.Checksum) {
return nil, errors.Errorf("expected checksum to match %s, got %s", gs.src.Checksum, ref)
}
return &Metadata{Ref: ref, Checksum: ref}, nil
}
var g session.Group
if jobCtx != nil {
g = jobCtx.Session()
}
stagedURL, err := gs.ensureStagedBundle(ctx, jobCtx, g)
if err != nil {
return nil, err
}
return gs.resolveMetadataFromURL(ctx, g, stagedURL)
}
// checkoutAsBundle writes a single-file git bundle at the checkout mount root
// instead of a worktree. This is the checkout counterpart of bundle import and
// is used when GitCheckoutBundle() is set on the identifier. Submodules are
// not included in the bundle.
func (gs *gitSourceHandler) checkoutAsBundle(ctx context.Context, repo *gitRepo, g session.Group) (_ cache.ImmutableRef, retErr error) {
ref := gs.src.Ref
commit := gs.src.Checksum
if commit == "" {
commit = ref
}
checkoutRef, err := gs.cache.New(ctx, nil, g, cache.CachePolicyRetain, cache.WithDescription(fmt.Sprintf("git bundle checkout for %s#%s", gs.src.Remote, ref)))
if err != nil {
return nil, errors.Wrapf(err, "failed to create new mutable for bundle checkout")
}
defer func() {
if retErr != nil && checkoutRef != nil {
checkoutRef.Release(context.WithoutCancel(ctx))
}
}()
mount, err := checkoutRef.Mount(ctx, false, g)
if err != nil {
return nil, err
}
lm := snapshot.LocalMounter(mount)
checkoutDir, err := lm.Mount()
if err != nil {
return nil, err
}
defer func() {
if retErr != nil && lm != nil {
lm.Unmount()
}
}()
// The bundle needs to carry a named ref that points at the target
// commit, so `git fetch <bundle> +refs/*:refs/*` on the consumer side
// has something to land. We preserve the user's ref name (normalized
// into refs/heads/* or refs/tags/*) so the bundle reads as if it were
// produced by the upstream remote; no fake internal namespace.
targetRef := bundleTargetRef(ref)
// Stage the bundle in an isolated temp bare repo rather than mutating
// the shared bare repo's ref namespace. We fetch the pinned commit
// from the shared repo into the temp bare repo and give it the
// target-ref name there, so `git bundle create` sees a clean,
// self-contained repo with exactly the ref we want in the bundle.
tmpDir, err := os.MkdirTemp("", "buildkit-bundle-create-")
if err != nil {
return nil, errors.Wrap(err, "failed to create temp dir for bundle creation")
}
defer os.RemoveAll(tmpDir)
tmpRepoDir := filepath.Join(tmpDir, "repo.git")
if err := os.Mkdir(tmpRepoDir, 0700); err != nil {
return nil, errors.Wrap(err, "failed to create temp bare repo dir for bundle creation")
}
tmpGit := repo.New(gitutil.WithGitDir(tmpRepoDir))
initArgs := []string{"-c", "init.defaultBranch=master", "init", "--bare"}
if gs.sha256 {
initArgs = append(initArgs, "--object-format=sha256")
}
if _, err := tmpGit.Run(ctx, initArgs...); err != nil {
return nil, errors.Wrap(err, "failed to init temp bare repo for bundle creation")
}
// Pull the pinned commit from the shared bare repo into temp, then
// point the target ref at it. The shared bare repo maps user refs to
// refs/tags/<name> via the main-path fetch refspec, so there is no
// natural-name ref to pull; update-ref alone places the tip against
// the pinned commit under the target name.
sharedURL := "file://" + repo.dir
if _, err := tmpGit.Run(ctx, "fetch", sharedURL, commit); err != nil {
return nil, errors.Wrapf(err, "failed to fetch commit %s from shared repo for bundle creation", commit)
}
if _, err := tmpGit.Run(ctx, "update-ref", targetRef, commit); err != nil {
return nil, errors.Wrapf(err, "failed to create target ref for bundle %s", commit)
}
// Write the bundle under a temp path we fully own, then move it into
// the checkout mount through os.OpenRoot so a pre-existing symlink at
// <checkoutDir>/bundle cannot redirect the final artifact outside the
// mount. git bundle create is an external process that only accepts
// path strings, so the only safe landing pattern is: write to a path
// outside the mount, then atomically transfer the bytes through an
// os.Root-scoped file handle.
stagePath := filepath.Join(tmpDir, "out.bundle")
if _, err := tmpGit.Run(ctx, "bundle", "create", stagePath, targetRef); err != nil {
return nil, errors.Wrapf(err, "failed to create git bundle for %s", commit)
}
if err := writeBundleToMount(checkoutDir, bundleFileName, stagePath); err != nil {
return nil, err
}
outPath := filepath.Join(checkoutDir, bundleFileName)
if idmap := mount.IdentityMapping(); idmap != nil {
uid, gid := idmap.RootPair()
if err := os.Lchown(outPath, uid, gid); err != nil {
return nil, errors.Wrap(err, "failed to remap git bundle ownership")
}
}
lm.Unmount()
lm = nil
snap, err := checkoutRef.Commit(ctx)
if err != nil {
return nil, err
}
checkoutRef = nil
return snap, nil
}
// writeBundleToMount copies the bytes at stagePath into
// <checkoutDir>/<bundleName> via os.OpenRoot, so a symlink at the destination
// cannot redirect the write outside checkoutDir. The staging file is expected
// to be a path the caller fully controls (created under its own temp dir),
// not a path inside checkoutDir.
func writeBundleToMount(checkoutDir, bundleName, stagePath string) (retErr error) {
src, err := os.Open(stagePath)
if err != nil {
return errors.Wrapf(err, "failed to open staged bundle %s", stagePath)
}
defer src.Close()
checkoutRoot, err := os.OpenRoot(checkoutDir)
if err != nil {
return errors.Wrapf(err, "failed to open checkout dir root %s", checkoutDir)
}
defer checkoutRoot.Close()
// Remove any pre-existing entry at bundleName. If it's a symlink,
// os.Root.Remove deletes the link itself, not the target, so a symlink
// planted in the mount cannot redirect the subsequent create.
if err := checkoutRoot.Remove(bundleName); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrapf(err, "failed to clear bundle dest %s", bundleName)
}
dst, err := checkoutRoot.Create(bundleName)
if err != nil {
return errors.Wrapf(err, "failed to create bundle file %s", bundleName)
}
defer func() {
if retErr != nil {
dst.Close()
checkoutRoot.Remove(bundleName)
}
}()
if _, err := io.Copy(dst, src); err != nil {
return errors.Wrapf(err, "failed to write bundle to %s", bundleName)
}
if err := dst.Close(); err != nil {
return errors.Wrapf(err, "failed to close bundle file %s", bundleName)
}
return nil
}
+102 -5
View File
@@ -1,11 +1,16 @@
package git
import (
"strings"
"github.com/containerd/containerd/v2/pkg/reference"
"github.com/moby/buildkit/solver/llbsolver/provenance"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
"github.com/moby/buildkit/source"
srctypes "github.com/moby/buildkit/source/types"
"github.com/moby/buildkit/util/gitutil"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
type GitIdentifier struct {
@@ -20,6 +25,24 @@ type GitIdentifier struct {
KnownSSHHosts string
SkipSubmodules bool
MTime string // "checkout" (default) or "commit"
FetchByCommit bool
// Bundle, when non-empty, instructs the git source to fetch commits from a
// pre-built git bundle stored as a blob instead of fetching from the remote
// repository. The locator must use the "docker-image+blob://" or
// "oci-layout+blob://" scheme.
Bundle string
// BundleOCISessionID, when set, pins the OCI-layout bundle fetch to a
// specific client session. Only meaningful when Bundle uses the
// oci-layout+blob:// scheme.
BundleOCISessionID string
// BundleOCIStoreID, when set, overrides the OCI-layout store name
// derived from the bundle locator. Only meaningful when Bundle uses the
// oci-layout+blob:// scheme.
BundleOCIStoreID string
// CheckoutBundle, when true, produces a single-file git bundle at the
// checkout mount root (filename "bundle") instead of a worktree.
CheckoutBundle bool
VerifySignature *GitSignatureVerifyOptions
}
@@ -55,14 +78,18 @@ func (GitIdentifier) Scheme() string {
var _ source.Identifier = (*GitIdentifier)(nil)
func (id *GitIdentifier) Capture(c *provenance.Capture, pin string) error {
url := id.Remote
remoteURL := id.Remote
if id.Ref != "" {
url += "#" + id.Ref
remoteURL += "#" + id.Ref
}
c.AddGit(provenancetypes.GitSource{
URL: url,
gs := provenancetypes.GitSource{
URL: remoteURL,
Commit: pin,
})
}
if id.Bundle != "" {
gs.Bundle = &provenancetypes.GitBundle{URL: id.Bundle}
}
c.AddGit(gs)
if id.AuthTokenSecret != "" {
c.AddSecret(provenancetypes.Secret{
ID: id.AuthTokenSecret,
@@ -83,3 +110,73 @@ func (id *GitIdentifier) Capture(c *provenance.Capture, pin string) error {
}
return nil
}
// validateBundleAttrs enforces the bundle-mode constraints that are statically
// decidable at identifier parse time.
func validateBundleAttrs(id *GitIdentifier) error {
if id.Bundle != "" {
if _, _, _, _, err := parseBundleLocator(id.Bundle); err != nil {
return err
}
if id.Checksum == "" {
return errors.Errorf("git.bundle requires git.checksum to be set")
}
// Reject the pathological "Ref=sha1, Checksum=sha2" case: if both
// are set and Ref is itself a commit SHA, the two must agree.
// Without this check bundle mode silently overwrites Ref with
// Checksum in tryRemoteFetch and the mismatch never surfaces.
if id.Ref != "" && gitutil.IsCommitSHA(id.Ref) && !strings.HasPrefix(id.Ref, id.Checksum) {
return errors.Errorf("expected checksum to match %s, got %s", id.Checksum, id.Ref)
}
}
if id.CheckoutBundle {
if id.KeepGitDir {
return errors.Errorf("git.checkoutbundle is incompatible with git.keepgitdir")
}
if id.Subdir != "" {
return errors.Errorf("git.checkoutbundle is incompatible with git.subdir")
}
}
return nil
}
// splitBundleLocator splits a locator of the form "<scheme>://<body>" into
// (scheme, body). It returns ("","") if raw does not contain a "://" separator.
// This avoids net/url for schemes like "oci-layout+blob" whose body contains
// a colon after '@' (the digest) and confuses url.Parse.
func splitBundleLocator(raw string) (string, string) {
const sep = "://"
i := strings.Index(raw, sep)
if i < 0 {
return "", ""
}
return raw[:i], raw[i+len(sep):]
}
// parseBundleLocator extracts the scheme, normalized reference string, the
// reference locator (i.e. the body before '@digest', used as the OCI-layout
// store id), and the digest from a bundle locator of the form
// "<scheme>://<ref>@sha256:<hex>". Only sha256 digests are accepted because
// downloadBundleToFile hashes with sha256 and would fail to match any other
// algorithm.
func parseBundleLocator(raw string) (string, string, string, digest.Digest, error) {
scheme, body := splitBundleLocator(raw)
if scheme == "" {
return "", "", "", "", errors.Errorf("failed to parse git.bundle locator %q: missing scheme", raw)
}
if scheme != srctypes.DockerImageBlobScheme && scheme != srctypes.OCIBlobScheme {
return "", "", "", "", errors.Errorf("git.bundle locator scheme %q is not supported; expected %q or %q", scheme, srctypes.DockerImageBlobScheme, srctypes.OCIBlobScheme)
}
ref, err := reference.Parse(body)
if err != nil {
return "", "", "", "", errors.Wrapf(err, "failed to parse git.bundle locator %q", raw)
}
dgst := ref.Digest()
if err := dgst.Validate(); err != nil {
return "", "", "", "", errors.Wrapf(err, "failed to parse git.bundle locator %q: invalid digest", raw)
}
if dgst.Algorithm() != digest.SHA256 {
return "", "", "", "", errors.Errorf("git.bundle locator %q: digest algorithm %q not supported, expected %q", raw, dgst.Algorithm(), digest.SHA256)
}
return scheme, ref.String(), ref.Locator, dgst, nil
}
+253 -32
View File
@@ -16,6 +16,7 @@ import (
"strings"
"time"
"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/identity"
@@ -24,6 +25,7 @@ import (
"github.com/moby/buildkit/session/sshforward"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver"
"github.com/moby/buildkit/solver/llbsolver/compat"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/source"
srctypes "github.com/moby/buildkit/source/types"
@@ -44,11 +46,16 @@ var defaultBranch = regexp.MustCompile(`refs/heads/(\S+)`)
type Opt struct {
CacheAccessor cache.Accessor
// RegistryHosts is used to fetch bundle blobs from a docker registry
// when a git source uses GitBundleURL("docker-image+blob://...").
// Optional: when unset, bundle mode requires oci-layout+blob.
RegistryHosts docker.RegistryHosts
}
type Source struct {
cache cache.Accessor
locker *locker.Locker
cache cache.Accessor
locker *locker.Locker
registryHosts docker.RegistryHosts
}
type Metadata struct {
@@ -74,8 +81,9 @@ func Supported() error {
func NewSource(opt Opt) (*Source, error) {
gs := &Source{
cache: opt.CacheAccessor,
locker: locker.New(),
cache: opt.CacheAccessor,
locker: locker.New(),
registryHosts: opt.RegistryHosts,
}
return gs, nil
}
@@ -137,11 +145,24 @@ func (gs *Source) Identifier(scheme, ref string, attrs map[string]string, platfo
id.VerifySignature.IgnoreSignedTag = v == "true"
case pb.AttrGitMTime:
id.MTime = v
case pb.AttrGitFetchByCommit:
id.FetchByCommit = v == "true"
case pb.AttrGitBundle:
id.Bundle = v
case pb.AttrGitCheckoutBundle:
id.CheckoutBundle = v == "true"
case pb.AttrOCILayoutSessionID:
id.BundleOCISessionID = v
case pb.AttrOCILayoutStoreID:
id.BundleOCIStoreID = v
}
}
if err := validateGitRef(id.Ref); err != nil {
return nil, err
}
if err := validateBundleAttrs(id); err != nil {
return nil, err
}
return id, nil
}
@@ -254,6 +275,12 @@ type gitSourceHandler struct {
sha256 bool
sm *session.Manager
authArgs []string
// stagedBundleURL / stagedBundleCleanup cache the temp bundle staging
// across resolveBundleMetadata (CacheKey) and tryRemoteFetch (Snapshot)
// so the bundle blob is downloaded only once per handler.
stagedBundleURL string
stagedBundleCleanup func() error
}
func (gs *gitSourceHandler) shaToCacheKey(sha, ref string) string {
@@ -273,6 +300,9 @@ func (gs *gitSourceHandler) shaToCacheKey(sha, ref string) string {
if gs.src.MTime != "" && gs.src.MTime != "checkout" {
key += "(mtime=" + gs.src.MTime + ")"
}
if gs.src.CheckoutBundle {
key += "(bundle)"
}
return key
}
@@ -282,18 +312,28 @@ func (gs *Source) ResolveMetadata(ctx context.Context, id *GitIdentifier, sm *se
Source: gs,
sm: sm,
}
// The handler is scoped to this call, so any staged bundle it owns
// must be released before returning. When a jobCtx is present,
// ensureStagedBundle hands ownership to the job and this is a no-op;
// without a jobCtx (e.g. direct ResolveMetadata callers) this is the
// only hook that frees the temp dir.
defer func() {
if err := gsh.releaseStagedBundle(); err != nil {
bklog.G(ctx).Warnf("failed to release staged git bundle: %v", err)
}
}()
md, err := gsh.resolveMetadata(ctx, jobCtx)
if err != nil {
return nil, err
}
gsh.cacheCommit = md.Checksum
gsh.sha256 = len(md.Checksum) == 64
if !opt.ReturnObject && id.VerifySignature == nil {
return md, nil
}
gsh.cacheCommit = md.Checksum
gsh.sha256 = len(md.Checksum) == 64
if err := gsh.addGitObjectsToMetadata(ctx, jobCtx, md); err != nil {
return nil, err
}
@@ -487,10 +527,6 @@ func (gs *gitSourceHandler) remoteKey() string {
}
func (gs *gitSourceHandler) resolveMetadata(ctx context.Context, jobCtx solver.JobContext) (md *Metadata, retErr error) {
remote := gs.src.Remote
gs.locker.Lock(remote)
defer gs.locker.Unlock(remote)
if gs.src.Checksum != "" {
matched, err := regexp.MatchString("^[a-fA-F0-9]+$", gs.src.Checksum)
if err != nil || !matched {
@@ -498,6 +534,19 @@ func (gs *gitSourceHandler) resolveMetadata(ctx context.Context, jobCtx solver.J
}
}
if gs.src.Bundle != "" {
// Bundle mode stages the bundle in a temp bare repo and runs the
// shared ls-remote code path against its file:// URL so the
// resulting Metadata has the same ref shape as the non-bundle
// path. Bundle mode does not need the per-remote lock: the temp
// bare repo is private to this call.
return gs.resolveBundleMetadata(ctx, jobCtx)
}
remote := gs.src.Remote
gs.locker.Lock(remote)
defer gs.locker.Unlock(remote)
if gitutil.IsCommitSHA(gs.src.Ref) {
if gs.src.Checksum != "" && !strings.HasPrefix(gs.src.Ref, gs.src.Checksum) {
return nil, errors.Errorf("expected checksum to match %s, got %s", gs.src.Checksum, gs.src.Ref)
@@ -508,6 +557,29 @@ func (gs *gitSourceHandler) resolveMetadata(ctx context.Context, jobCtx solver.J
}, nil
}
if gs.src.FetchByCommit {
if gs.src.Checksum == "" {
return nil, errors.Errorf("fetch-by-commit requires a checksum or a commit SHA ref")
}
if !gitutil.IsCommitSHA(gs.src.Checksum) {
return nil, errors.Errorf("fetch-by-commit requires a full commit SHA checksum, got %q", gs.src.Checksum)
}
// Canonicalize unqualified refs so that cache keys match those
// produced by the normal path's ls-remote-driven normalization.
// Unqualified names are treated as branches (git's preferred
// resolution); tags must be passed as "refs/tags/<name>".
if gs.src.Ref != "" && !strings.HasPrefix(gs.src.Ref, "refs/") {
gs.src.Ref = "refs/heads/" + gs.src.Ref
}
if gs.src.Ref == "" {
gs.src.Ref = gs.src.Checksum
}
return &Metadata{
Ref: gs.src.Ref,
Checksum: gs.src.Checksum,
}, nil
}
var g session.Group
if jobCtx != nil {
g = jobCtx.Session()
@@ -544,6 +616,14 @@ func (gs *gitSourceHandler) resolveMetadata(ctx context.Context, jobCtx solver.J
gs.getAuthToken(ctx, g)
return gs.resolveMetadataFromURL(ctx, g, gs.src.Remote)
}
// resolveMetadataFromURL runs ls-remote against the given URL and parses the
// result into a Metadata. The URL may be the user's configured remote (for
// non-bundle mode) or a file:// URL for a staged bundle. Auth and lock
// management are the caller's responsibility.
func (gs *gitSourceHandler) resolveMetadataFromURL(ctx context.Context, g session.Group, remoteURL string) (*Metadata, error) {
tmpGit, cleanup, err := gs.emptyGitCli(ctx, g)
if err != nil {
return nil, err
@@ -552,16 +632,16 @@ func (gs *gitSourceHandler) resolveMetadata(ctx context.Context, jobCtx solver.J
ref := gs.src.Ref
if ref == "" {
ref, err = getDefaultBranch(ctx, tmpGit, gs.src.Remote)
ref, err = getDefaultBranch(ctx, tmpGit, remoteURL)
if err != nil {
return nil, err
}
}
// TODO: should we assume that remote tag is immutable? add a timer?
buf, err := tmpGit.Run(ctx, "ls-remote", "--", gs.src.Remote, ref, ref+"^{}")
buf, err := tmpGit.Run(ctx, "ls-remote", "--", remoteURL, ref, ref+"^{}")
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch remote %s", urlutil.RedactCredentials(remote))
return nil, errors.Wrapf(err, "failed to fetch remote %s", urlutil.RedactCredentials(remoteURL))
}
lines := strings.Split(string(buf), "\n")
@@ -616,7 +696,7 @@ func (gs *gitSourceHandler) resolveMetadata(ctx context.Context, jobCtx solver.J
return nil, errors.Errorf("expected checksum to match %s, got %s", gs.src.Checksum, exp)
}
}
md = &Metadata{
md := &Metadata{
Ref: usedRef,
Checksum: sha,
}
@@ -725,12 +805,12 @@ func (gs *gitSourceHandler) remoteFetch(ctx context.Context, jobCtx solver.JobCo
g = jobCtx.Session()
}
repo, err := gs.tryRemoteFetch(ctx, g, false)
repo, err := gs.tryRemoteFetch(ctx, jobCtx, g, false)
if err != nil {
var wce *wouldClobberExistingTagError
var ulre *unableToUpdateLocalRefError
if errors.As(err, &wce) || errors.As(err, &ulre) {
repo, err = gs.tryRemoteFetch(ctx, g, true)
repo, err = gs.tryRemoteFetch(ctx, jobCtx, g, true)
if err != nil {
return nil, err
}
@@ -748,6 +828,11 @@ func (gs *gitSourceHandler) remoteFetch(ctx context.Context, jobCtx solver.JobCo
}()
ref := gs.src.Ref
// With fetch-by-commit, the user-provided ref is not written into the
// bare repo, so resolve the checksum directly.
if gs.src.FetchByCommit && gs.src.Checksum != "" {
ref = gs.src.Checksum
}
git := repo.GitCLI
if gs.src.Checksum != "" {
actualHashBuf, err := repo.Run(ctx, "rev-parse", ref)
@@ -789,6 +874,14 @@ func (gs *gitSourceHandler) Snapshot(ctx context.Context, jobCtx solver.JobConte
g = jobCtx.Session()
}
gs.getAuthToken(ctx, g)
compatibilityVersion := 0
if jobCtx != nil {
var err error
compatibilityVersion, err = jobCtx.CompatibilityVersion()
if err != nil {
return nil, err
}
}
snapshotKey := cacheKey + ":" + gs.src.Subdir
gs.locker.Lock(snapshotKey)
@@ -808,7 +901,12 @@ func (gs *gitSourceHandler) Snapshot(ctx context.Context, jobCtx solver.JobConte
}
defer repo.Release()
ref, err := gs.checkout(ctx, repo, g)
var ref cache.ImmutableRef
if gs.src.CheckoutBundle {
ref, err = gs.checkoutAsBundle(ctx, repo, g)
} else {
ref, err = gs.checkout(ctx, repo, g, compatibilityVersion)
}
if err != nil {
return nil, err
}
@@ -836,7 +934,7 @@ func (g *gitRepo) Release() error {
return err
}
func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group, reset bool) (_ *gitRepo, retErr error) {
func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, jobCtx solver.JobContext, g session.Group, reset bool) (_ *gitRepo, retErr error) {
repo := &gitRepo{}
defer func() {
@@ -846,13 +944,32 @@ func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group,
}
}()
// Auth args only apply to non-bundle fetches. In bundle mode the
// payload comes from a blob fetch, so there is no http remote to
// authenticate against.
authArgs := gs.authArgs
if gs.src.Bundle != "" {
authArgs = nil
}
git, cleanup, err := gs.emptyGitCli(ctx, g)
if err != nil {
return nil, err
}
repo.releasers = append(repo.releasers, cleanup)
gitDir, unmountGitDir, err := gs.mountRemote(ctx, gs.src.Remote, gs.authArgs, gs.sha256, reset, g)
// Bundle mode may need to create the shared bare repo before the main
// fetch. Stage the bundle first so stageBundle can probe the bundle's
// object format via ls-remote and set gs.sha256 for mountRemote.
stagedURL := ""
if gs.src.Bundle != "" {
stagedURL, err = gs.ensureStagedBundle(ctx, jobCtx, g)
if err != nil {
return nil, err
}
}
gitDir, unmountGitDir, err := gs.mountRemote(ctx, gs.src.Remote, authArgs, gs.sha256, reset, g)
if err != nil {
return nil, err
}
@@ -862,6 +979,40 @@ func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group,
git = git.New(gitutil.WithGitDir(gitDir))
repo.GitCLI = git
// fetchSource is the URL used in place of the "origin" remote name for
// the main fetch. In bundle mode it points at a staged bundle's file://
// URL; an empty value means fetch from the configured "origin" remote.
fetchSource := ""
// Bundle mode: stage the bundle into an isolated temp bare repo and
// fetch from it as a file:// remote. The rest of this function runs
// unchanged — git accepts a URL anywhere a remote name is expected,
// so the fetch flow treats the staged repo like a normal origin.
if gs.src.Bundle != "" {
// When the user did not supply a symbolic ref, use the pinned
// commit as the ref directly. validateBundleAttrs guarantees
// Checksum is set.
if gs.src.Ref == "" || gitutil.IsCommitSHA(gs.src.Ref) {
gs.src.Ref = gs.src.Checksum
}
// Bundle mode enters tryRemoteFetch from Snapshot. If CacheKey
// has not run yet, prime gs.cacheCommit from the pinned checksum
// to make the downstream cacheCommit validation a no-op rather
// than a spurious mismatch.
if gs.cacheCommit == "" {
gs.cacheCommit = gs.src.Checksum
}
// If the pinned commit is already present in the shared bare
// repo (from a prior origin or bundle fetch), skip staging
// entirely. The doFetch check below will no-op the fetch.
if _, err := repo.Run(ctx, "cat-file", "-e", gs.src.Checksum+"^{commit}"); err != nil {
// Reuse the staged bundle if CacheKey or the eager bundle-format
// probe above already built one. ensureStagedBundle owns the
// teardown (wired to jobCtx.Cleanup on first call), so the
// cleanup is intentionally not appended to repo.releasers.
fetchSource = stagedURL
}
}
ref := gs.src.Ref
if ref == "" {
ref, err = getDefaultBranch(ctx, git, gs.src.Remote)
@@ -870,10 +1021,16 @@ func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group,
}
gs.src.Ref = ref
}
// fetchRef is the identifier used to fetch from the remote. For fetch-by-commit
// mode this is the checksum (commit SHA); otherwise it is the ref itself.
fetchRef := ref
if gs.src.FetchByCommit && gs.src.Checksum != "" {
fetchRef = gs.src.Checksum
}
doFetch := true
if gitutil.IsCommitSHA(ref) {
if gitutil.IsCommitSHA(fetchRef) {
// skip fetch if commit already exists
if _, err := git.Run(ctx, "cat-file", "-e", "--", ref+"^{commit}"); err == nil {
if _, err := git.Run(ctx, "cat-file", "-e", "--", fetchRef+"^{commit}"); err == nil {
doFetch = false
}
}
@@ -896,8 +1053,16 @@ func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group,
// make sure no old lock files have leaked
gitDirRoot.RemoveAll("shallow.lock")
origin := "origin"
if fetchSource != "" {
origin = fetchSource
}
args := []string{"fetch"}
if !gitutil.IsCommitSHA(ref) { // TODO: find a branch from ls-remote?
// For fetch-by-commit, assume the server supports
// uploadpack.allowReachableSHA1InWant / allowAnySHA1InWant and
// fetch only the requested commit, without the tag/unshallow
// fallback used by the generic SHA-ref path.
if gs.src.FetchByCommit || !gitutil.IsCommitSHA(fetchRef) { // TODO: find a branch from ls-remote?
args = append(args, "--depth=1", "--no-tags")
} else {
args = append(args, "--tags")
@@ -905,11 +1070,11 @@ func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group,
args = append(args, "--unshallow")
}
}
args = append(args, "origin")
args = append(args, origin)
if gitutil.IsCommitSHA(ref) {
args = append(args, ref)
} else {
args = append(args, "--force", "--", ref+":"+targetRef)
args = append(args, "--force", "--", fetchRef+":"+targetRef)
}
if _, err := git.Run(ctx, args...); err != nil {
err := errors.Wrapf(err, "failed to fetch remote %s", urlutil.RedactCredentials(gs.src.Remote))
@@ -928,7 +1093,7 @@ func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group,
}
// verify that commit matches the cache key commit
dt, err := git.Run(ctx, "rev-parse", ref)
dt, err := git.Run(ctx, "rev-parse", fetchRef)
if err != nil {
return nil, err
}
@@ -951,7 +1116,7 @@ func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group,
if _, err := gitDirRoot.Lstat("shallow"); err == nil {
args = append(args, "--unshallow")
}
args = append(args, "origin", gs.cacheCommit)
args = append(args, origin, gs.cacheCommit)
if _, err := git.Run(ctx, args...); err != nil {
return nil, errors.Wrapf(err, "failed to fetch remote %s", urlutil.RedactCredentials(gs.src.Remote))
}
@@ -975,9 +1140,27 @@ func (gs *gitSourceHandler) tryRemoteFetch(ctx context.Context, g session.Group,
return repo, nil
}
func (gs *gitSourceHandler) checkout(ctx context.Context, repo *gitRepo, g session.Group) (_ cache.ImmutableRef, retErr error) {
func (gs *gitSourceHandler) checkout(ctx context.Context, repo *gitRepo, g session.Group, compatibilityVersion int) (_ cache.ImmutableRef, retErr error) {
ref := gs.src.Ref
checkoutRef, err := gs.cache.New(ctx, nil, g, cache.WithRecordType(client.UsageRecordTypeGitCheckout), cache.WithDescription(fmt.Sprintf("git snapshot for %s#%s", urlutil.RedactCredentials(gs.src.Remote), ref)))
// refOrCommit is the identifier to use against the bare repo. For
// fetch-by-commit, the user-provided ref is not written into the bare
// repo, so fall back to the commit checksum which is always present.
refOrCommit := ref
if gs.src.FetchByCommit && gs.src.Checksum != "" {
refOrCommit = gs.src.Checksum
}
// In bundle mode, if the user did not supply a symbolic Ref (empty or
// a SHA), fall back to the pinned checksum so the downstream
// `git checkout` / `git fetch` has a valid tip. With a symbolic ref
// present, use it as-is: bundle import lands refs in the natural
// refs/heads/* / refs/tags/* namespace in the shared bare repo, so
// `git checkout <ref>` and `git fetch origin <ref>` resolve it
// normally and the resulting .git carries the natural ref name.
if gs.src.Bundle != "" && (ref == "" || gitutil.IsCommitSHA(ref)) {
ref = gs.src.Checksum
refOrCommit = ref
}
checkoutRef, err := gs.cache.New(ctx, nil, g, cache.WithRecordType(client.UsageRecordTypeGitCheckout), cache.WithDescription(fmt.Sprintf("git snapshot for %s#%s", urlutil.RedactCredentials(gs.src.Remote), gs.src.Ref)))
if err != nil {
return nil, errors.Wrapf(err, "failed to create new mutable for %s", urlutil.RedactCredentials(gs.src.Remote))
}
@@ -1034,7 +1217,7 @@ func (gs *gitSourceHandler) checkout(ctx context.Context, repo *gitRepo, g sessi
return nil, err
}
gitCatFileBuf, err := git.Run(ctx, "cat-file", "-t", ref)
gitCatFileBuf, err := git.Run(ctx, "cat-file", "-t", refOrCommit)
if err != nil {
return nil, err
}
@@ -1047,6 +1230,11 @@ func (gs *gitSourceHandler) checkout(ctx context.Context, repo *gitRepo, g sessi
targetRef = "refs/tags/" + pullref
}
pullref += ":" + targetRef
} else if gs.src.FetchByCommit && ref != refOrCommit {
// Fetch the commit object from the bare repo and save it under the
// user-provided ref name in the checkout clone. The ref does not
// need to exist in the bare repo.
pullref = refOrCommit + ":" + ref
} else if gitutil.IsCommitSHA(ref) {
pullref = "refs/buildkit/" + identity.NewID()
_, err = git.Run(ctx, "update-ref", pullref, ref)
@@ -1084,7 +1272,7 @@ func (gs *gitSourceHandler) checkout(ctx context.Context, repo *gitRepo, g sessi
}
}
checkoutGit := git.New(gitutil.WithWorkTree(cd), gitutil.WithGitDir(gitDir))
_, err = checkoutGit.Run(ctx, "checkout", "--no-overlay", ref, "--", ".")
_, err = checkoutGit.Run(ctx, "checkout", "--no-overlay", refOrCommit, "--", ".")
if err != nil {
return nil, errors.Wrapf(err, "failed to checkout remote %s", urlutil.RedactCredentials(gs.src.Remote))
}
@@ -1138,8 +1326,14 @@ func (gs *gitSourceHandler) checkout(ctx context.Context, repo *gitRepo, g sessi
}
}
if compatibilityVersion == compat.CompatibilityVersion013 {
if err := resetCompatibility014FileModes(checkoutDir); err != nil {
return nil, errors.Wrapf(err, "failed to normalize compatibility file modes for %s", urlutil.RedactCredentials(gs.src.Remote))
}
}
if gs.src.MTime == "commit" {
commitTime, err := getCommitTime(ctx, git, ref)
commitTime, err := getCommitTime(ctx, git, refOrCommit)
if err != nil {
return nil, errors.Wrapf(err, "failed to get commit time for %s", urlutil.RedactCredentials(gs.src.Remote))
}
@@ -1220,6 +1414,33 @@ func resetSnapshotMtimes(dir string, t time.Time) error {
return nil
}
// resetCompatibility014FileModes restores the pre-v0.15 git checkout file
// mode for non-executable regular files, which were stored with group/other
// write bits set before the exec-option propagation fix. Executable files are
// left untouched: their pre-v0.15 behavior is not covered by the current
// compatibility matrix, and blindly adding write bits to 0o755 would be a
// guess.
func resetCompatibility014FileModes(dir string) error {
return filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || d.Type()&os.ModeSymlink != 0 {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
mode := info.Mode()
if !mode.IsRegular() || mode&0o111 != 0 {
return nil
}
return os.Chmod(p, mode|0o222)
})
}
type wouldClobberExistingTagError struct {
error
}
+48
View File
@@ -0,0 +1,48 @@
package appcontext
import (
"context"
"os"
"os/signal"
"sync"
"github.com/moby/buildkit/util/bklog"
"github.com/pkg/errors"
)
var appContextCache context.Context
var appContextOnce sync.Once
// Context returns a static context that reacts to termination signals of the
// running process. Useful in CLI tools.
func Context() context.Context {
appContextOnce.Do(func() {
signals := make(chan os.Signal, 2048)
signal.Notify(signals, terminationSignals...)
const exitLimit = 3
retries := 0
ctx := context.Background()
for _, f := range inits {
ctx = f(ctx) //nolint:fatcontext
}
ctx, cancel := context.WithCancelCause(ctx)
appContextCache = ctx //nolint:fatcontext
go func() {
for {
<-signals
retries++
err := errors.Errorf("got %d SIGTERM/SIGINTs, forcing shutdown", retries)
cancel(err)
if retries >= exitLimit {
bklog.G(ctx).Error(err.Error())
os.Exit(1)
}
}
}()
})
return appContextCache
}
+11
View File
@@ -0,0 +1,11 @@
//go:build !windows
package appcontext
import (
"os"
"golang.org/x/sys/unix"
)
var terminationSignals = []os.Signal{unix.SIGTERM, unix.SIGINT}
@@ -0,0 +1,7 @@
package appcontext
import (
"os"
)
var terminationSignals = []os.Signal{os.Interrupt}
+14
View File
@@ -0,0 +1,14 @@
package appcontext
import (
"context"
)
type Initializer func(context.Context) context.Context
var inits []Initializer
// Register stores a new context initializer that runs on app context creation
func Register(f Initializer) {
inits = append(inits, f)
}
+34 -5
View File
@@ -27,6 +27,7 @@ type GitCLI struct {
sshAuthSock string
sshKnownHosts string
hostGitConfig bool
}
// Option provides a variadic option for configuring the git client.
@@ -97,6 +98,15 @@ func WithSSHKnownHosts(sshKnownHosts string) Option {
}
}
// WithHostGitConfig allows git to read the host system and user git config.
// This is intended for client-side local git inspection. The default remains
// isolated so daemon-side callers do not leak host configuration into git.
func WithHostGitConfig() Option {
return func(b *GitCLI) {
b.hostGitConfig = true
}
}
type StreamFunc func(context.Context) (io.WriteCloser, io.WriteCloser, func())
// WithStreams configures a callback for getting the streams for a command. The
@@ -108,7 +118,7 @@ func WithStreams(streams StreamFunc) Option {
}
}
// New initializes a new git client
// NewGitCLI initializes a new git client
func NewGitCLI(opts ...Option) *GitCLI {
c := &GitCLI{}
for _, opt := range opts {
@@ -191,9 +201,28 @@ func (cli *GitCLI) Run(ctx context.Context, args ...string) (_ []byte, err error
"GIT_TERMINAL_PROMPT=0",
"GIT_SSH_COMMAND=" + getGitSSHCommand(cli.sshKnownHosts),
// "GIT_TRACE=1",
"GIT_CONFIG_NOSYSTEM=1", // Disable reading from system gitconfig.
"HOME=/dev/null", // Disable reading from user gitconfig.
"LC_ALL=C", // Ensure consistent output.
"LC_ALL=C", // Ensure consistent output.
}
if cli.hostGitConfig {
for _, ev := range [...]string{
"HOME",
"XDG_CONFIG_HOME",
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
"GIT_CONFIG_GLOBAL",
"GIT_CONFIG_SYSTEM",
} {
if v, ok := os.LookupEnv(ev); ok {
cmd.Env = append(cmd.Env, ev+"="+v)
}
}
} else {
cmd.Env = append(cmd.Env,
"GIT_CONFIG_NOSYSTEM=1", // Disable reading from system gitconfig.
"HOME="+os.DevNull, // Disable reading from user gitconfig.
"GIT_CONFIG_GLOBAL="+os.DevNull, // Disable reading from global gitconfig.
)
}
for _, ev := range proxyEnvVars {
if v, ok := os.LookupEnv(ev); ok {
@@ -244,7 +273,7 @@ func (cli *GitCLI) Run(ctx context.Context, args ...string) (_ []byte, err error
}
func getGitSSHCommand(knownHosts string) string {
gitSSHCommand := "ssh -F /dev/null"
gitSSHCommand := "ssh -F " + os.DevNull
if knownHosts != "" {
gitSSHCommand += " -o UserKnownHostsFile=" + knownHosts
} else {
+1 -13
View File
@@ -1,7 +1,6 @@
package progress
import (
"maps"
"slices"
"sync"
"time"
@@ -81,18 +80,7 @@ func (ps *MultiWriter) Write(id string, v any) error {
}
func (ps *MultiWriter) WriteRawProgress(p *Progress) error {
meta := p.meta
if len(ps.meta) > 0 {
meta = map[string]any{}
maps.Copy(meta, p.meta)
for k, v := range ps.meta {
if _, ok := meta[k]; !ok {
meta[k] = v
}
}
}
p.meta = meta
return ps.writeRawProgress(p)
return ps.writeRawProgress(p.Decorate(ps.meta))
}
func (ps *MultiWriter) writeRawProgress(p *Progress) error {
+16 -12
View File
@@ -235,18 +235,7 @@ func (pw *progressWriter) Write(id string, v any) error {
}
func (pw *progressWriter) WriteRawProgress(p *Progress) error {
meta := p.meta
if len(pw.meta) > 0 {
meta = map[string]any{}
maps.Copy(meta, p.meta)
for k, v := range pw.meta {
if _, ok := meta[k]; !ok {
meta[k] = v
}
}
}
p.meta = meta
return pw.writeRawProgress(p)
return pw.writeRawProgress(p.Decorate(pw.meta))
}
func (pw *progressWriter) writeRawProgress(p *Progress) error {
@@ -271,6 +260,21 @@ func (p *Progress) Meta(key string) (any, bool) {
return v, ok
}
// Decorate merges in missing metadata without overwriting existing keys.
func (p *Progress) Decorate(meta map[string]any) *Progress {
if len(meta) == 0 {
return p
}
merged := make(map[string]any, len(p.meta)+len(meta))
maps.Copy(merged, meta)
maps.Copy(merged, p.meta)
newP := *p
newP.meta = merged
return &newP
}
type noOpWriter struct{}
func (pw *noOpWriter) Write(_ string, _ any) error {
+110
View File
@@ -0,0 +1,110 @@
package registrar
import (
"context"
"sync"
"time"
)
type Registrar[K comparable, V any] struct {
mu sync.Mutex
values map[K]*registrarValue[V]
}
func New[K comparable, V any]() *Registrar[K, V] {
return &Registrar[K, V]{
values: make(map[K]*registrarValue[V]),
}
}
// Register will register the value with the given id.
// This value will persist until Discard is called with the same id.
func (r *Registrar[K, V]) Register(id K, val V) {
reg := r.getOrCreateRegistrar(id, nil)
reg.Register(val, nil)
}
// Get will retrieve a registered value and will wait a small time period for that
// value to appear if it hasn't been registered yet.
func (r *Registrar[K, V]) Get(ctx context.Context, id K) (v V, _ error) {
onCreate := func(reg *registrarValue[V]) {
select {
case <-reg.notifyCh:
return
case <-time.After(3 * time.Second):
r.Discard(id)
}
}
reg := r.getOrCreateRegistrar(id, onCreate)
select {
case <-ctx.Done():
return v, context.Cause(ctx)
case <-reg.notifyCh:
return reg.value, reg.err
}
}
// Discard will remove the given value from the registrar after it has been registered
// with Register.
func (r *Registrar[K, V]) Discard(id K) {
r.mu.Lock()
reg, ok := r.values[id]
delete(r.values, id)
r.mu.Unlock()
if ok {
var value V
reg.Register(value, context.Canceled)
}
}
// getOrCreateRegistrar will create a registrar with the given id to be retrieved at a later time.
// The same id will return the same registrar.
//
// If the registrar is newly created, the onCreate function is invoked in a separate goroutine
// if it is present. If nil, this function is ignored.
func (r *Registrar[K, V]) getOrCreateRegistrar(id K, onCreate func(*registrarValue[V])) *registrarValue[V] {
r.mu.Lock()
defer r.mu.Unlock()
reg, ok := r.values[id]
if !ok {
reg = &registrarValue[V]{
notifyCh: make(chan struct{}),
}
r.values[id] = reg
if onCreate != nil {
go onCreate(reg)
}
}
return reg
}
type registrarValue[V any] struct {
// notifyCh is the notification channel that gets closed when
// the bridge is registered.
notifyCh chan struct{}
value V
err error
isSet bool
mu sync.Mutex
}
func (r *registrarValue[V]) Register(value V, err error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.isSet {
return
}
r.value = value
r.err = err
r.isSet = true
close(r.notifyCh)
}
+17 -7
View File
@@ -47,6 +47,7 @@ func newAuthHandlerNS(sm *session.Manager) *authHandlerNS {
}
func (a *authHandlerNS) get(ctx context.Context, host string, sm *session.Manager, g session.Group) *authFetcher {
hasSession := false
if g != nil {
if iter := g.SessionIterator(); iter != nil {
for {
@@ -54,6 +55,7 @@ func (a *authHandlerNS) get(ctx context.Context, host string, sm *session.Manage
if id == "" {
break
}
hasSession = true
h, ok := a.fetchers[host+"/"+id]
if ok {
h.lastUsed = time.Now()
@@ -63,6 +65,14 @@ func (a *authHandlerNS) get(ctx context.Context, host string, sm *session.Manage
}
}
if !hasSession {
h, ok := a.fetchers[host+"/"]
if ok {
h.lastUsed = time.Now()
return h
}
}
// link existing fetcher
for k, h := range a.fetchers {
parts := strings.SplitN(k, "/", 2)
@@ -78,7 +88,7 @@ func (a *authHandlerNS) get(ctx context.Context, host string, sm *session.Manage
return h
}
} else {
sessionID, username, password, err := sessionauth.CredentialsFunc(sm, g)(host)
sessionID, username, password, err := sessionauth.CredentialsFunc(ctx, sm, g)(host)
if err == nil {
if username == h.common.Username && password == h.common.Secret {
a.fetchers[host+"/"+sessionID] = h
@@ -140,8 +150,8 @@ func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) err
return nil
}
func (a *dockerAuthorizer) getCredentials(host string) (sessionID, username, secret string, err error) {
return sessionauth.CredentialsFunc(a.sm, a.session)(host)
func (a *dockerAuthorizer) getCredentials(ctx context.Context, host string) (sessionID, username, secret string, err error) {
return sessionauth.CredentialsFunc(ctx, a.sm, a.session)(host)
}
func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
@@ -185,12 +195,12 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
var username, secret string
sessionID, pubKey, err := sessionauth.GetTokenAuthority(ctx, host, a.sm, a.session)
if err != nil {
if err != nil && !errors.Is(err, session.ErrNoActiveSessions) {
return err
}
if pubKey == nil {
sessionID, username, secret, err = a.getCredentials(host)
if err != nil {
sessionID, username, secret, err = a.getCredentials(ctx, host)
if err != nil && !errors.Is(err, session.ErrNoActiveSessions) {
return err
}
}
@@ -205,7 +215,7 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
return nil
case auth.BasicAuth:
sessionID, username, secret, err := a.getCredentials(host)
sessionID, username, secret, err := a.getCredentials(ctx, host)
if err != nil {
return err
}
+43
View File
@@ -0,0 +1,43 @@
package childprocess
const (
// go.opentelemetry.io/otel/propagation doesn't export these as constants.
traceparentHeader = "traceparent"
tracestateHeader = "tracestate"
)
type textMap struct {
parent string
state string
}
func (tm *textMap) Get(key string) string {
switch key {
case traceparentHeader:
return tm.parent
case tracestateHeader:
return tm.state
default:
return ""
}
}
func (tm *textMap) Set(key string, value string) {
switch key {
case traceparentHeader:
tm.parent = value
case tracestateHeader:
tm.state = value
}
}
func (tm *textMap) Keys() []string {
var k []string
if tm.parent != "" {
k = append(k, traceparentHeader)
}
if tm.state != "" {
k = append(k, tracestateHeader)
}
return k
}
+36
View File
@@ -0,0 +1,36 @@
package childprocess
import (
"context"
"os"
"github.com/moby/buildkit/util/appcontext"
"go.opentelemetry.io/otel/propagation"
)
func init() {
appcontext.Register(initContext)
}
func initContext(ctx context.Context) context.Context {
// open-telemetry/opentelemetry-specification#740
parent := os.Getenv("TRACEPARENT")
state := os.Getenv("TRACESTATE")
if parent != "" {
tc := propagation.TraceContext{}
return tc.Extract(ctx, &textMap{parent: parent, state: state})
}
// deprecated: removed in v0.11.0
// previously defined in https://github.com/open-telemetry/opentelemetry-swift/blob/4ea467ed4b881d7329bf2254ca7ed7f2d9d6e1eb/Sources/OpenTelemetrySdk/Trace/Propagation/EnvironmentContextPropagator.swift#L14-L15
parent = os.Getenv("OTEL_TRACE_PARENT")
state = os.Getenv("OTEL_TRACE_STATE")
if parent == "" {
return ctx
}
tc := propagation.TraceContext{}
return tc.Extract(ctx, &textMap{parent: parent, state: state})
}
@@ -1,4 +1,4 @@
package detect
package childprocess
import (
"context"
@@ -6,11 +6,6 @@ import (
"go.opentelemetry.io/otel/propagation"
)
const (
traceparentHeader = "traceparent"
tracestateHeader = "tracestate"
)
// Environ returns list of environment variables that need to be sent to the child process
// in order for it to pick up cross-process tracing from same state.
func Environ(ctx context.Context) []string {
@@ -39,39 +34,3 @@ func Environ(ctx context.Context) []string {
return env
}
type textMap struct {
parent string
state string
}
func (tm *textMap) Get(key string) string {
switch key {
case traceparentHeader:
return tm.parent
case tracestateHeader:
return tm.state
default:
return ""
}
}
func (tm *textMap) Set(key string, value string) {
switch key {
case traceparentHeader:
tm.parent = value
case tracestateHeader:
tm.state = value
}
}
func (tm *textMap) Keys() []string {
var k []string
if tm.parent != "" {
k = append(k, traceparentHeader)
}
if tm.state != "" {
k = append(k, tracestateHeader)
}
return k
}
+1 -1
View File
@@ -80,7 +80,7 @@ func (c *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
c.lock.Lock()
defer c.lock.Unlock()
if c.tracesClient == nil {
return errNoClient
return errors.New("no client")
}
_, err := c.tracesClient.Export(ctx, &coltracepb.ExportTraceServiceRequest{
-7
View File
@@ -1,7 +0,0 @@
package otlptracegrpc
import "errors"
var (
errNoClient = errors.New("no client")
)
+1
View File
@@ -159,6 +159,7 @@ func NewWorker(ctx context.Context, opt WorkerOpt) (*Worker, error) {
if err := git.Supported(); err == nil {
gs, err := git.NewSource(git.Opt{
CacheAccessor: cm,
RegistryHosts: opt.RegistryHosts,
})
if err != nil {
return nil, err
@@ -238,6 +238,8 @@ type CommonLanguageSettings struct {
// The destination where API teams want this client library to be published.
Destinations []ClientLibraryDestination `protobuf:"varint,2,rep,packed,name=destinations,proto3,enum=google.api.ClientLibraryDestination" json:"destinations,omitempty"`
// Configuration for which RPCs should be generated in the GAPIC client.
//
// Note: This field should not be used in most cases.
SelectiveGapicGeneration *SelectiveGapicGeneration `protobuf:"bytes,3,opt,name=selective_gapic_generation,json=selectiveGapicGeneration,proto3" json:"selective_gapic_generation,omitempty"`
}
@@ -1249,6 +1251,8 @@ func (x *MethodSettings) GetBatching() *BatchingConfigProto {
// This message is used to configure the generation of a subset of the RPCs in
// a service for client libraries.
//
// Note: This feature should not be used in most cases.
type SelectiveGapicGeneration struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
+8 -4
View File
@@ -950,7 +950,7 @@ github.com/ishidawataru/sctp
# github.com/jmoiron/sqlx v1.4.0
## explicit; go 1.10
github.com/jmoiron/sqlx/types
# github.com/klauspost/compress v1.18.5
# github.com/klauspost/compress v1.18.6
## explicit; go 1.24
github.com/klauspost/compress
github.com/klauspost/compress/fse
@@ -978,7 +978,7 @@ github.com/mitchellh/hashstructure/v2
# github.com/mitchellh/reflectwalk v1.0.2
## explicit
github.com/mitchellh/reflectwalk
# github.com/moby/buildkit v0.29.0
# github.com/moby/buildkit v0.30.0-rc1
## explicit; go 1.25.5
github.com/moby/buildkit/api/services/control
github.com/moby/buildkit/api/types
@@ -1065,6 +1065,7 @@ github.com/moby/buildkit/solver/errdefs
github.com/moby/buildkit/solver/internal/pipe
github.com/moby/buildkit/solver/llbsolver
github.com/moby/buildkit/solver/llbsolver/cdidevices
github.com/moby/buildkit/solver/llbsolver/compat
github.com/moby/buildkit/solver/llbsolver/errdefs
github.com/moby/buildkit/solver/llbsolver/file
github.com/moby/buildkit/solver/llbsolver/history
@@ -1079,6 +1080,7 @@ github.com/moby/buildkit/solver/pb
github.com/moby/buildkit/solver/result
github.com/moby/buildkit/source
github.com/moby/buildkit/source/containerblob
github.com/moby/buildkit/source/containerblob/blobfetch
github.com/moby/buildkit/source/containerimage
github.com/moby/buildkit/source/git
github.com/moby/buildkit/source/http
@@ -1090,6 +1092,7 @@ github.com/moby/buildkit/sourcepolicy/pb
github.com/moby/buildkit/sourcepolicy/policysession
github.com/moby/buildkit/util/apicaps
github.com/moby/buildkit/util/apicaps/pb
github.com/moby/buildkit/util/appcontext
github.com/moby/buildkit/util/appdefaults
github.com/moby/buildkit/util/archutil
github.com/moby/buildkit/util/attestation
@@ -1132,6 +1135,7 @@ github.com/moby/buildkit/util/pull
github.com/moby/buildkit/util/pull/pullprogress
github.com/moby/buildkit/util/purl
github.com/moby/buildkit/util/push
github.com/moby/buildkit/util/registrar
github.com/moby/buildkit/util/resolvconf
github.com/moby/buildkit/util/resolver
github.com/moby/buildkit/util/resolver/config
@@ -1146,8 +1150,8 @@ github.com/moby/buildkit/util/suggest
github.com/moby/buildkit/util/system
github.com/moby/buildkit/util/throttle
github.com/moby/buildkit/util/tracing
github.com/moby/buildkit/util/tracing/childprocess
github.com/moby/buildkit/util/tracing/detect
github.com/moby/buildkit/util/tracing/exec
github.com/moby/buildkit/util/tracing/otlptracegrpc
github.com/moby/buildkit/util/tracing/transform
github.com/moby/buildkit/util/urlutil
@@ -1970,7 +1974,7 @@ google.golang.org/api/transport/http
# google.golang.org/genproto v0.0.0-20260401024825-9d38bb4040a9
## explicit; go 1.25.0
google.golang.org/genproto/googleapis/logging/type
# google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9
# google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d
## explicit; go 1.25.0
google.golang.org/genproto/googleapis/api
google.golang.org/genproto/googleapis/api/annotations