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:
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
* -text
|
||||
*.bin -text -diff
|
||||
*.md text eol=lf
|
||||
+700
-700
File diff suppressed because it is too large.
Load diff
+78
-78
@@ -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
@@ -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
@@ -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
@@ -77,6 +77,7 @@ message SolveRequest {
|
||||
repeated Exporter Exporters = 13;
|
||||
bool EnableSessionExporter = 14;
|
||||
string SourcePolicySession = 15;
|
||||
int64 CompatibilityVersion = 16;
|
||||
}
|
||||
|
||||
message CacheOptions {
|
||||
|
||||
+33
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+37
-17
@@ -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 {
|
||||
|
||||
Generated
Vendored
+3
@@ -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{
|
||||
|
||||
+381
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -24,6 +24,11 @@ message FrontendCap {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message CompatibilityFeature {
|
||||
int64 version = 1;
|
||||
string feature = 2;
|
||||
}
|
||||
|
||||
message Subrequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
+204
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -71,7 +71,6 @@ func ProvenanceProcessor(slsaVersion provenancetypes.ProvenanceSLSA, attrs map[s
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.MarshalIndent(pr, "", " ")
|
||||
},
|
||||
})
|
||||
|
||||
+73
-18
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,11 @@
|
||||
//go:build !windows
|
||||
|
||||
package appcontext
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var terminationSignals = []os.Signal{unix.SIGTERM, unix.SIGINT}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package appcontext
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
var terminationSignals = []os.Signal{os.Interrupt}
|
||||
+14
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 = ®istrarValue[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
@@ -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
@@ -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
@@ -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})
|
||||
}
|
||||
Generated
Vendored
+1
-42
@@ -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
@@ -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
@@ -1,7 +0,0 @@
|
||||
package otlptracegrpc
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
errNoClient = errors.New("no client")
|
||||
)
|
||||
+1
@@ -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
|
||||
|
||||
+4
@@ -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
|
||||
|
||||
Vendored
+8
-4
@@ -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
|
||||
|
||||
Reference in new issue
Block a user