Skip to content

Commit 1d5af10

Browse files
committed
Add back support for azblob cache
Signed-off-by: Pranav Pandit <[email protected]>
1 parent 6702365 commit 1d5af10

File tree

154 files changed

+24266
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

154 files changed

+24266
-6
lines changed

Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ ARG DNSNAME_VERSION=v1.3.1
1414
ARG NYDUS_VERSION=v2.2.4
1515
ARG MINIO_VERSION=RELEASE.2022-05-03T20-36-08Z
1616
ARG MINIO_MC_VERSION=RELEASE.2022-05-04T06-07-55Z
17-
ARG AZURITE_VERSION=3.18.0
17+
ARG AZURITE_VERSION=3.33.0
1818
ARG GOTESTSUM_VERSION=v1.9.0
1919
ARG DELVE_VERSION=v1.23.1
2020

@@ -413,6 +413,9 @@ RUN apk add --no-cache shadow shadow-uidmap sudo vim iptables ip6tables dnsmasq
413413
ARG NERDCTL_VERSION
414414
RUN curl -Ls https://raw.githubusercontent.com/containerd/nerdctl/$NERDCTL_VERSION/extras/rootless/containerd-rootless.sh > /usr/bin/containerd-rootless.sh \
415415
&& chmod 0755 /usr/bin/containerd-rootless.sh
416+
ARG AZURITE_VERSION
417+
RUN apk add --no-cache nodejs npm \
418+
&& npm install -g azurite@${AZURITE_VERSION}
416419
# The entrypoint script is needed for enabling nested cgroup v2 (https://github.com/moby/buildkit/issues/3265#issuecomment-1309631736)
417420
RUN curl -Ls https://raw.githubusercontent.com/moby/moby/v25.0.1/hack/dind > /docker-entrypoint.sh \
418421
&& chmod 0755 /docker-entrypoint.sh

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Join `#buildkit` channel on [Docker Community Slack](https://dockr.ly/comm-slack
6767
- [Local directory](#local-directory-1)
6868
- [GitHub Actions cache (experimental)](#github-actions-cache-experimental)
6969
- [S3 cache (experimental)](#s3-cache-experimental)
70+
- [Azure Blob Storage cache (experimental)](#azure-blob-storage-cache-experimental)
7071
- [Consistent hashing](#consistent-hashing)
7172
- [Metadata](#metadata)
7273
- [Systemd socket activation](#systemd-socket-activation)
@@ -589,6 +590,55 @@ Other options are:
589590
* `manifests_prefix=<prefix>`: set global prefix to store / read manifests on s3 (default: `manifests/`)
590591
* `name=<manifest>`: name of the manifest to use (default `buildkit`)
591592

593+
#### Azure Blob Storage cache (experimental)
594+
595+
```bash
596+
buildctl build ... \
597+
--output type=image,name=docker.io/username/image,push=true \
598+
--export-cache type=azblob,account_url=https://myaccount.blob.core.windows.net,name=my_image \
599+
--import-cache type=azblob,account_url=https://myaccount.blob.core.windows.net,name=my_image
600+
```
601+
602+
The following attributes are required:
603+
* `account_url`: The Azure Blob Storage account URL (default: `$BUILDKIT_AZURE_STORAGE_ACCOUNT_URL`)
604+
605+
Storage locations:
606+
* blobs: `<account_url>/<container>/<prefix><blobs_prefix>/<sha256>`, default: `<account_url>/<container>/blobs/<sha256>`
607+
* manifests: `<account_url>/<container>/<prefix><manifests_prefix>/<name>`, default: `<account_url>/<container>/manifests/<name>`
608+
609+
Azure Blob Storage configuration:
610+
* `container`: The Azure Blob Storage container name (default: `buildkit-cache` or `$BUILDKIT_AZURE_STORAGE_CONTAINER` if set)
611+
* `blobs_prefix`: Global prefix to store / read blobs on the Azure Blob Storage container (`<container>`) (default: `blobs/`)
612+
* `manifests_prefix`: Global prefix to store / read blobs on the Azure Blob Storage container (`<container>`) (default: `manifests/`)
613+
614+
Azure Blob Storage authentication:
615+
616+
There are 2 options supported for Azure Blob Storage authentication:
617+
618+
* Any system using environment variables supported by the [Azure SDK for Go](https://docs.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication). The configuration must be available for the buildkit daemon, not for the client.
619+
* Secret Access Key, using the `secret_access_key` attribute to specify the primary or secondary account key for your Azure Blob Storage account. [Azure Blob Storage account keys](https://docs.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage)
620+
621+
> [!NOTE]
622+
> Account name can also be specified with `account_name` attribute (or `$BUILDKIT_AZURE_STORAGE_ACCOUNT_NAME`)
623+
> if it is not part of the account URL host.
624+
625+
`--export-cache` options:
626+
* `type=azblob`
627+
* `mode=<min|max>`: specify cache layers to export (default: `min`)
628+
* `min`: only export layers for the resulting image
629+
* `max`: export all the layers of all intermediate steps
630+
* `prefix=<prefix>`: set global prefix to store / read files on the Azure Blob Storage container (`<container>`) (default: empty)
631+
* `name=<manifest>`: specify name of the manifest to use (default: `buildkit`)
632+
* Multiple manifest names can be specified at the same time, separated by `;`. The standard use case is to use the git sha1 as name, and the branch name as duplicate, and load both with 2 `import-cache` commands.
633+
* `ignore-error=<false|true>`: specify if error is ignored in case cache export fails (default: `false`)
634+
635+
`--import-cache` options:
636+
* `type=azblob`
637+
* `prefix=<prefix>`: set global prefix to store / read files on the Azure Blob Storage container (`<container>`) (default: empty)
638+
* `blobs_prefix=<prefix>`: set global prefix to store / read blobs on the Azure Blob Storage container (`<container>`) (default: `blobs/`)
639+
* `manifests_prefix=<prefix>`: set global prefix to store / read manifests on the Azure Blob Storage container (`<container>`) (default: `manifests/`)
640+
* `name=<manifest>`: name of the manifest to use (default: `buildkit`)
641+
592642
### Consistent hashing
593643

594644
If you have multiple BuildKit daemon instances, but you don't want to use registry for sharing cache across the cluster,

cache/remotecache/azblob/exporter.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package azblob
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"time"
10+
11+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
12+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
13+
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
14+
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
15+
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
16+
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
17+
"github.com/containerd/containerd/v2/core/content"
18+
"github.com/containerd/containerd/v2/pkg/labels"
19+
"github.com/moby/buildkit/cache/remotecache"
20+
v1 "github.com/moby/buildkit/cache/remotecache/v1"
21+
"github.com/moby/buildkit/session"
22+
"github.com/moby/buildkit/solver"
23+
"github.com/moby/buildkit/util/bklog"
24+
"github.com/moby/buildkit/util/compression"
25+
"github.com/moby/buildkit/util/progress"
26+
digest "github.com/opencontainers/go-digest"
27+
"github.com/pkg/errors"
28+
)
29+
30+
// ResolveCacheExporterFunc for "azblob" cache exporter.
31+
func ResolveCacheExporterFunc() remotecache.ResolveCacheExporterFunc {
32+
return func(ctx context.Context, g session.Group, attrs map[string]string) (remotecache.Exporter, error) {
33+
config, err := getConfig(attrs)
34+
if err != nil {
35+
return nil, errors.Wrap(err, "failed to create azblob config")
36+
}
37+
38+
containerClient, err := createContainerClient(ctx, config)
39+
if err != nil {
40+
return nil, errors.Wrap(err, "failed to create container client")
41+
}
42+
43+
cc := v1.NewCacheChains()
44+
return &exporter{
45+
CacheExporterTarget: cc,
46+
chains: cc,
47+
containerClient: containerClient,
48+
config: config,
49+
}, nil
50+
}
51+
}
52+
53+
var _ remotecache.Exporter = &exporter{}
54+
55+
type exporter struct {
56+
solver.CacheExporterTarget
57+
chains *v1.CacheChains
58+
containerClient *container.Client
59+
config *Config
60+
}
61+
62+
func (ce *exporter) Name() string {
63+
return "exporting cache to Azure Blob Storage"
64+
}
65+
66+
func (ce *exporter) Finalize(ctx context.Context) (map[string]string, error) {
67+
config, descs, err := ce.chains.Marshal(ctx)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
for i, l := range config.Layers {
73+
dgstPair, ok := descs[l.Blob]
74+
if !ok {
75+
return nil, errors.Errorf("missing blob %s", l.Blob)
76+
}
77+
if dgstPair.Descriptor.Annotations == nil {
78+
return nil, errors.Errorf("invalid descriptor without annotations")
79+
}
80+
var diffID digest.Digest
81+
v, ok := dgstPair.Descriptor.Annotations[labels.LabelUncompressed]
82+
if !ok {
83+
return nil, errors.Errorf("invalid descriptor without uncompressed annotation")
84+
}
85+
dgst, err := digest.Parse(v)
86+
if err != nil {
87+
return nil, errors.Wrap(err, "failed to parse uncompressed annotation")
88+
}
89+
diffID = dgst
90+
91+
key := blobKey(ce.config, dgstPair.Descriptor.Digest)
92+
93+
exists, err := blobExists(ctx, ce.containerClient, key)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
bklog.G(ctx).Debugf("layers %s exists = %t", key, exists)
99+
100+
if !exists {
101+
layerDone := progress.OneOff(ctx, fmt.Sprintf("writing layer %s", l.Blob))
102+
ra, err := dgstPair.Provider.ReaderAt(ctx, dgstPair.Descriptor)
103+
if err != nil {
104+
err = errors.Wrapf(err, "failed to get reader for %s", dgstPair.Descriptor.Digest)
105+
return nil, layerDone(err)
106+
}
107+
if err := ce.uploadBlobIfNotExists(ctx, key, content.NewReader(ra)); err != nil {
108+
return nil, layerDone(err)
109+
}
110+
layerDone(nil)
111+
}
112+
113+
la := &v1.LayerAnnotations{
114+
DiffID: diffID,
115+
Size: dgstPair.Descriptor.Size,
116+
MediaType: dgstPair.Descriptor.MediaType,
117+
}
118+
if v, ok := dgstPair.Descriptor.Annotations["buildkit/createdat"]; ok {
119+
var t time.Time
120+
if err := (&t).UnmarshalText([]byte(v)); err != nil {
121+
return nil, err
122+
}
123+
la.CreatedAt = t.UTC()
124+
}
125+
config.Layers[i].Annotations = la
126+
}
127+
128+
dt, err := json.Marshal(config)
129+
if err != nil {
130+
return nil, errors.Wrap(err, "failed to marshal config")
131+
}
132+
133+
for _, name := range ce.config.Names {
134+
if innerError := ce.uploadManifest(ctx, manifestKey(ce.config, name), bytesToReadSeekCloser(dt)); innerError != nil {
135+
return nil, errors.Wrapf(innerError, "error writing manifest %s", name)
136+
}
137+
}
138+
139+
return nil, nil
140+
}
141+
142+
func (ce *exporter) Config() remotecache.Config {
143+
return remotecache.Config{
144+
Compression: compression.New(compression.Default),
145+
}
146+
}
147+
148+
// For uploading manifests, use the Upload API which follows "last writer wins" sematics
149+
// This is slightly slower than UploadStream call but is safe to call concurrently from multiple threads. Refer to:
150+
// https://github.com/Azure/azure-sdk-for-go/issues/18490#issuecomment-1170806877
151+
func (ce *exporter) uploadManifest(ctx context.Context, manifestKey string, reader io.ReadSeekCloser) error {
152+
defer reader.Close()
153+
blobClient := ce.containerClient.NewBlockBlobClient(manifestKey)
154+
155+
ctx, cnclFn := context.WithCancelCause(ctx)
156+
ctx, _ = context.WithTimeoutCause(ctx, time.Minute*5, errors.WithStack(context.DeadlineExceeded))
157+
defer cnclFn(errors.WithStack(context.Canceled))
158+
159+
_, err := blobClient.Upload(ctx, reader, &blockblob.UploadOptions{})
160+
if err != nil {
161+
return errors.Wrapf(err, "failed to upload blob %s: %v", manifestKey, err)
162+
}
163+
164+
return nil
165+
}
166+
167+
// For uploading blobs, use the UploadStream with access conditions which state that only upload if the blob
168+
// does not already exist. Since blobs are content addressable, this is the right thing to do for blobs and it gives
169+
// a performance improvement over the Upload API used for uploading manifests.
170+
func (ce *exporter) uploadBlobIfNotExists(ctx context.Context, blobKey string, reader io.Reader) error {
171+
blobClient := ce.containerClient.NewBlockBlobClient(blobKey)
172+
173+
uploadCtx, cnclFn := context.WithCancelCause(ctx)
174+
uploadCtx, _ = context.WithTimeoutCause(uploadCtx, time.Minute*5, errors.WithStack(context.DeadlineExceeded))
175+
defer cnclFn(errors.WithStack(context.Canceled))
176+
177+
// Only upload if the blob doesn't exist
178+
_, err := blobClient.UploadStream(uploadCtx, reader, &blockblob.UploadStreamOptions{
179+
BlockSize: IOChunkSize,
180+
Concurrency: IOConcurrency,
181+
AccessConditions: &blob.AccessConditions{
182+
ModifiedAccessConditions: &blob.ModifiedAccessConditions{
183+
IfNoneMatch: to.Ptr(azcore.ETagAny),
184+
},
185+
},
186+
})
187+
188+
if err == nil {
189+
return nil
190+
}
191+
192+
if bloberror.HasCode(err, bloberror.BlobAlreadyExists) {
193+
return nil
194+
}
195+
196+
return errors.Wrapf(err, "failed to upload blob %s: %v", blobKey, err)
197+
}
198+
199+
var _ io.ReadSeekCloser = &readSeekCloser{}
200+
201+
type readSeekCloser struct {
202+
io.Reader
203+
io.Seeker
204+
io.Closer
205+
}
206+
207+
func bytesToReadSeekCloser(dt []byte) io.ReadSeekCloser {
208+
bytesReader := bytes.NewReader(dt)
209+
return &readSeekCloser{
210+
Reader: bytesReader,
211+
Seeker: bytesReader,
212+
Closer: io.NopCloser(bytesReader),
213+
}
214+
}

0 commit comments

Comments
 (0)