1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
package azblob
import (
"context"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/pkg/errors"
)
const (
attrSecretAccessKey = "secret_access_key"
attrAccountName = "account_name"
attrAccountURL = "account_url"
attrPrefix = "prefix"
attrManifestsPrefix = "manifests_prefix"
attrBlobsPrefix = "blobs_prefix"
attrName = "name"
attrContainer = "container"
IOConcurrency = 4
IOChunkSize = 32 * 1024 * 1024
)
type Config struct {
AccountURL string
Container string
Prefix string
ManifestsPrefix string
BlobsPrefix string
Names []string
AccountName string
secretAccessKey string
}
func getConfig(attrs map[string]string) (*Config, error) {
accountURLString, ok := attrs[attrAccountURL]
if !ok {
accountURLString, ok = os.LookupEnv("BUILDKIT_AZURE_STORAGE_ACCOUNT_URL")
if !ok {
return &Config{}, errors.New("either ${BUILDKIT_AZURE_STORAGE_ACCOUNT_URL} or account_url attribute is required for azblob cache")
}
}
accountURL, err := url.Parse(accountURLString)
if err != nil {
return &Config{}, errors.Wrap(err, "azure storage account url provided is not a valid url")
}
accountName, ok := attrs[attrAccountName]
if !ok {
accountName, ok = os.LookupEnv("BUILDKIT_AZURE_STORAGE_ACCOUNT_NAME")
if !ok {
accountName = strings.Split(accountURL.Hostname(), ".")[0]
}
}
if accountName == "" {
return &Config{}, errors.New("unable to retrieve account name from account url or ${BUILDKIT_AZURE_STORAGE_ACCOUNT_NAME} or account_name attribute for azblob cache")
}
container, ok := attrs[attrContainer]
if !ok {
container, ok = os.LookupEnv("BUILDKIT_AZURE_STORAGE_CONTAINER")
if !ok {
container = "buildkit-cache"
}
}
prefix, ok := attrs[attrPrefix]
if !ok {
prefix, _ = os.LookupEnv("BUILDKIT_AZURE_STORAGE_PREFIX")
}
manifestsPrefix, ok := attrs[attrManifestsPrefix]
if !ok {
manifestsPrefix = "manifests"
}
blobsPrefix, ok := attrs[attrBlobsPrefix]
if !ok {
blobsPrefix = "blobs"
}
names := []string{"buildkit"}
name, ok := attrs[attrName]
if ok {
splittedNames := strings.Split(name, ";")
if len(splittedNames) > 0 {
names = splittedNames
}
}
secretAccessKey := attrs[attrSecretAccessKey]
config := Config{
AccountURL: accountURLString,
AccountName: accountName,
Container: container,
Prefix: prefix,
Names: names,
ManifestsPrefix: manifestsPrefix,
BlobsPrefix: blobsPrefix,
secretAccessKey: secretAccessKey,
}
return &config, nil
}
func createContainerClient(ctx context.Context, config *Config) (*azblob.ContainerClient, error) {
var serviceClient *azblob.ServiceClient
if config.secretAccessKey != "" {
sharedKey, err := azblob.NewSharedKeyCredential(config.AccountName, config.secretAccessKey)
if err != nil {
return nil, errors.Wrap(err, "failed to create shared key")
}
serviceClient, err = azblob.NewServiceClientWithSharedKey(config.AccountURL, sharedKey, &azblob.ClientOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to created service client from shared key")
}
} else {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, errors.Wrap(err, "failed to create default azure credentials")
}
serviceClient, err = azblob.NewServiceClient(config.AccountURL, cred, &azblob.ClientOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to create service client")
}
}
ctx, cnclFn := context.WithCancelCause(ctx)
ctx, _ = context.WithTimeoutCause(ctx, time.Second*60, errors.WithStack(context.DeadlineExceeded))
defer cnclFn(errors.WithStack(context.Canceled))
containerClient, err := serviceClient.NewContainerClient(config.Container)
if err != nil {
return nil, errors.Wrap(err, "error creating container client")
}
_, err = containerClient.GetProperties(ctx, &azblob.ContainerGetPropertiesOptions{})
if err == nil {
return containerClient, nil
}
var se *azblob.StorageError
if errors.As(err, &se) && se.ErrorCode == azblob.StorageErrorCodeContainerNotFound {
ctx, cnclFn := context.WithCancelCause(ctx)
ctx, _ = context.WithTimeoutCause(ctx, time.Minute*5, errors.WithStack(context.DeadlineExceeded))
defer cnclFn(errors.WithStack(context.Canceled))
_, err := containerClient.Create(ctx, &azblob.ContainerCreateOptions{})
if err != nil {
return nil, errors.Wrapf(err, "failed to create cache container %s", config.Container)
}
return containerClient, nil
}
return nil, errors.Wrapf(err, "failed to get properties of cache container %s", config.Container)
}
func manifestKey(config *Config, name string) string {
key := filepath.Join(config.Prefix, config.ManifestsPrefix, name)
return key
}
func blobKey(config *Config, digest string) string {
key := filepath.Join(config.Prefix, config.BlobsPrefix, digest)
return key
}
func blobExists(ctx context.Context, containerClient *azblob.ContainerClient, blobKey string) (bool, error) {
blobClient, err := containerClient.NewBlobClient(blobKey)
if err != nil {
return false, errors.Wrap(err, "error creating blob client")
}
ctx, cnclFn := context.WithCancelCause(ctx)
ctx, _ = context.WithTimeoutCause(ctx, time.Second*60, errors.WithStack(context.DeadlineExceeded))
defer cnclFn(errors.WithStack(context.Canceled))
_, err = blobClient.GetProperties(ctx, &azblob.BlobGetPropertiesOptions{})
if err == nil {
return true, nil
}
var se *azblob.StorageError
if errors.As(err, &se) && se.ErrorCode == azblob.StorageErrorCodeBlobNotFound {
return false, nil
}
return false, errors.Wrapf(err, "failed to check blob %s existence", blobKey)
}
|