Merge pull request #598 from fluxcd/azure-blob-bucket-provider
This commit is contained in:
commit
ccadce6d16
|
@ -29,6 +29,11 @@ jobs:
|
|||
${{ runner.os }}-go-
|
||||
- name: Verify
|
||||
run: make verify
|
||||
- name: Enable integration tests
|
||||
# Only run integration tests for main branch
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
echo 'GO_TEST_ARGS="-tags integration"' >> $GITHUB_ENV
|
||||
- name: Run tests
|
||||
run: make test
|
||||
- name: Setup Kubernetes
|
||||
|
@ -56,6 +61,11 @@ jobs:
|
|||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17.x
|
||||
- name: Enable integration tests
|
||||
# Only run integration tests for main branch
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
echo 'GO_TEST_ARGS="-tags integration"' >> $GITHUB_ENV
|
||||
- name: Run tests
|
||||
run: make test
|
||||
- name: Prepare
|
||||
|
|
5
Makefile
5
Makefile
|
@ -12,6 +12,9 @@ BUILD_ARGS ?=
|
|||
# Architectures to build images for
|
||||
BUILD_PLATFORMS ?= linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
# Go test arguments, e.g. '-tags=integration'
|
||||
GO_TEST_ARGS ?=
|
||||
|
||||
# Produce CRDs that work back to Kubernetes 1.16
|
||||
CRD_OPTIONS ?= crd:crdVersions=v1
|
||||
|
||||
|
@ -93,7 +96,7 @@ build: check-deps $(LIBGIT2) ## Build manager binary
|
|||
KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)"
|
||||
test: $(LIBGIT2) install-envtest test-api check-deps ## Run tests
|
||||
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \
|
||||
go test $(GO_STATIC_FLAGS) ./... -coverprofile cover.out
|
||||
go test $(GO_STATIC_FLAGS) $(GO_TEST_ARGS) ./... -coverprofile cover.out
|
||||
|
||||
check-deps:
|
||||
ifeq ($(shell uname -s),Darwin)
|
||||
|
|
|
@ -34,12 +34,13 @@ const (
|
|||
GenericBucketProvider string = "generic"
|
||||
AmazonBucketProvider string = "aws"
|
||||
GoogleBucketProvider string = "gcp"
|
||||
AzureBucketProvider string = "azure"
|
||||
)
|
||||
|
||||
// BucketSpec defines the desired state of an S3 compatible bucket
|
||||
type BucketSpec struct {
|
||||
// The S3 compatible storage provider name, default ('generic').
|
||||
// +kubebuilder:validation:Enum=generic;aws;gcp
|
||||
// +kubebuilder:validation:Enum=generic;aws;gcp;azure
|
||||
// +kubebuilder:default:=generic
|
||||
// +optional
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
|
|
@ -336,6 +336,7 @@ spec:
|
|||
- generic
|
||||
- aws
|
||||
- gcp
|
||||
- azure
|
||||
type: string
|
||||
region:
|
||||
description: The bucket region.
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/azure"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
@ -400,6 +401,17 @@ func (r *BucketReconciler) reconcileSource(ctx context.Context, obj *sourcev1.Bu
|
|||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
case sourcev1.AzureBucketProvider:
|
||||
if err = azure.ValidateSecret(secret); err != nil {
|
||||
e := &serror.Event{Err: err, Reason: sourcev1.AuthenticationFailedReason}
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
if provider, err = azure.NewClient(obj, secret); err != nil {
|
||||
e := &serror.Event{Err: err, Reason: "ClientError"}
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
default:
|
||||
if err = minio.ValidateSecret(secret); err != nil {
|
||||
e := &serror.Event{Err: err, Reason: sourcev1.AuthenticationFailedReason}
|
||||
|
|
7
go.mod
7
go.mod
|
@ -6,6 +6,9 @@ replace github.com/fluxcd/source-controller/api => ./api
|
|||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.16.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f
|
||||
github.com/cyphar/filepath-securejoin v0.2.3
|
||||
|
@ -54,7 +57,9 @@ replace helm.sh/helm/v3 v3.8.0 => github.com/hiddeco/helm/v3 v3.8.1-0.2022022311
|
|||
|
||||
require (
|
||||
cloud.google.com/go v0.99.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
|
||||
github.com/BurntSushi/toml v0.4.1 // indirect
|
||||
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
|
@ -96,6 +101,7 @@ require (
|
|||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
|
@ -146,6 +152,7 @@ require (
|
|||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.12.1 // indirect
|
||||
|
|
29
go.sum
29
go.sum
|
@ -53,7 +53,18 @@ cloud.google.com/go/storage v1.16.0 h1:1UwAux2OZP4310YXg5ohqBEpV16Y93uZG4+qOX7K2
|
|||
cloud.google.com/go/storage v1.16.0/go.mod h1:ieKBmUyzcftN5tbxwnXClMKH00CfcQ+xL6NN0r5QfmE=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
|
||||
github.com/Azure/azure-sdk-for-go v56.3.0+incompatible h1:DmhwMrUIvpeoTDiWRDtNHqelNUd3Og8JCkrLHQK795c=
|
||||
github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.0/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 h1:qoVeMsc9/fh/yhxVaA0obYjVH/oI/ihrOoMwsLS9KSA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.1 h1:RxemzI2cHD0A8WyMqHu/UnDjfpGES/cmjtPbQoktWqs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.13.1/go.mod h1:+nVKciyKD2J9TyVcEQ82Bo9b+3F92PiQfHrIE/zqLqM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 h1:sLZ/Y+P/5RRtsXWylBjB5lkgixYfm0MQPiwrSX//JSo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 h1:Px2UA+2RvSSvv+RvJNuUB6n7rs5Wsel4dXLe90Um2n4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
|
@ -68,6 +79,8 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935
|
|||
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
|
@ -264,6 +277,9 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
|
|||
github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684 h1:DBZ2sN7CK6dgvHVpQsQj4sRMCbWTmd17l+5SUCjnQSY=
|
||||
github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/cli v20.10.9+incompatible h1:OJ7YkwQA+k2Oi51lmCojpjiygKpi76P7bg91b2eJxYU=
|
||||
github.com/docker/cli v20.10.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
|
@ -437,7 +453,11 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
|
|||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
|
@ -786,8 +806,10 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
|||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
|
||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
|
@ -848,6 +870,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
|
|||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
@ -1098,6 +1122,7 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
|
@ -1181,6 +1206,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
|||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
|
@ -1197,9 +1223,11 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
|
|||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
@ -1336,6 +1364,7 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
@ -0,0 +1,404 @@
|
|||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
_ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrorDirectoryExists is an error returned when the filename provided
|
||||
// is a directory.
|
||||
ErrorDirectoryExists = errors.New("filename is a directory")
|
||||
)
|
||||
|
||||
const (
|
||||
clientIDField = "clientId"
|
||||
tenantIDField = "tenantId"
|
||||
clientSecretField = "clientSecret"
|
||||
clientCertificateField = "clientCertificate"
|
||||
clientCertificatePasswordField = "clientCertificatePassword"
|
||||
clientCertificateSendChainField = "clientCertificateSendChain"
|
||||
authorityHostField = "authorityHost"
|
||||
accountKeyField = "accountKey"
|
||||
)
|
||||
|
||||
// BlobClient is a minimal Azure Blob client for fetching objects.
|
||||
type BlobClient struct {
|
||||
azblob.ServiceClient
|
||||
}
|
||||
|
||||
// NewClient creates a new Azure Blob storage client.
|
||||
// The credential config on the client is set based on the data from the
|
||||
// Bucket and Secret. It detects credentials in the Secret in the following
|
||||
// order:
|
||||
//
|
||||
// - azidentity.ClientSecretCredential when `tenantId`, `clientId` and
|
||||
// `clientSecret` fields are found.
|
||||
// - azidentity.ClientCertificateCredential when `tenantId`,
|
||||
// `clientCertificate` (and optionally `clientCertificatePassword`) fields
|
||||
// are found.
|
||||
// - azidentity.ManagedIdentityCredential for a User ID, when a `clientId`
|
||||
// field but no `tenantId` is found.
|
||||
// - azblob.SharedKeyCredential when an `accountKey` field is found.
|
||||
// The account name is extracted from the endpoint specified on the Bucket
|
||||
// object.
|
||||
// - azidentity.ChainedTokenCredential with azidentity.EnvironmentCredential
|
||||
// and azidentity.ManagedIdentityCredential.
|
||||
//
|
||||
// If no credentials are found, and the azidentity.ChainedTokenCredential can
|
||||
// not be established. A simple client without credentials is returned.
|
||||
func NewClient(obj *sourcev1.Bucket, secret *corev1.Secret) (c *BlobClient, err error) {
|
||||
c = &BlobClient{}
|
||||
|
||||
var token azcore.TokenCredential
|
||||
|
||||
if secret != nil && len(secret.Data) > 0 {
|
||||
// Attempt AAD Token Credential options first.
|
||||
if token, err = tokenCredentialFromSecret(secret); err != nil {
|
||||
err = fmt.Errorf("failed to create token credential from '%s' Secret: %w", secret.Name, err)
|
||||
return
|
||||
}
|
||||
if token != nil {
|
||||
c.ServiceClient, err = azblob.NewServiceClient(obj.Spec.Endpoint, token, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to Shared Key Credential.
|
||||
var cred *azblob.SharedKeyCredential
|
||||
if cred, err = sharedCredentialFromSecret(obj.Spec.Endpoint, secret); err != nil {
|
||||
return
|
||||
}
|
||||
if cred != nil {
|
||||
c.ServiceClient, err = azblob.NewServiceClientWithSharedKey(obj.Spec.Endpoint, cred, &azblob.ClientOptions{})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Compose token chain based on environment.
|
||||
// This functions as a replacement for azidentity.NewDefaultAzureCredential
|
||||
// to not shell out.
|
||||
token, err = chainCredentialWithSecret(secret)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to create environment credential chain: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
if token != nil {
|
||||
c.ServiceClient, err = azblob.NewServiceClient(obj.Spec.Endpoint, token, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to simple client.
|
||||
c.ServiceClient, err = azblob.NewServiceClientWithNoCredential(obj.Spec.Endpoint, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// ValidateSecret validates if the provided Secret does at least have one valid
|
||||
// set of credentials. The provided Secret may be nil.
|
||||
func ValidateSecret(secret *corev1.Secret) error {
|
||||
if secret == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var valid bool
|
||||
if _, hasTenantID := secret.Data[tenantIDField]; hasTenantID {
|
||||
if _, hasClientID := secret.Data[clientIDField]; hasClientID {
|
||||
if _, hasClientSecret := secret.Data[clientSecretField]; hasClientSecret {
|
||||
valid = true
|
||||
}
|
||||
if _, hasClientCertificate := secret.Data[clientCertificateField]; hasClientCertificate {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, hasClientID := secret.Data[clientIDField]; hasClientID {
|
||||
valid = true
|
||||
}
|
||||
if _, hasAccountKey := secret.Data[accountKeyField]; hasAccountKey {
|
||||
valid = true
|
||||
}
|
||||
if _, hasAuthorityHost := secret.Data[authorityHostField]; hasAuthorityHost {
|
||||
valid = true
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("invalid '%s' secret data: requires a '%s' or '%s' field, a combination of '%s', '%s' and '%s', or '%s', '%s' and '%s'",
|
||||
secret.Name, clientIDField, accountKeyField, tenantIDField, clientIDField, clientSecretField, tenantIDField, clientIDField, clientCertificateField)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BucketExists returns if an object storage bucket with the provided name
|
||||
// exists, or returns a (client) error.
|
||||
func (c *BlobClient) BucketExists(ctx context.Context, bucketName string) (bool, error) {
|
||||
container := c.ServiceClient.NewContainerClient(bucketName)
|
||||
_, err := container.GetProperties(ctx, nil)
|
||||
if err != nil {
|
||||
var stgErr *azblob.StorageError
|
||||
if errors.As(err, &stgErr) {
|
||||
if stgErr.ErrorCode == azblob.StorageErrorCodeContainerNotFound {
|
||||
return false, nil
|
||||
}
|
||||
err = stgErr
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FGetObject gets the object from the provided object storage bucket, and
|
||||
// writes it to targetPath.
|
||||
// It returns the etag of the successfully fetched file, or any error.
|
||||
func (c *BlobClient) FGetObject(ctx context.Context, bucketName, objectName, localPath string) (string, error) {
|
||||
container := c.ServiceClient.NewContainerClient(bucketName)
|
||||
blob := container.NewBlobClient(objectName)
|
||||
|
||||
// Verify if destination already exists.
|
||||
dirStatus, err := os.Stat(localPath)
|
||||
if err == nil {
|
||||
// If the destination exists and is a directory.
|
||||
if dirStatus.IsDir() {
|
||||
return "", ErrorDirectoryExists
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed if file does not exist, return for all other errors.
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Extract top level directory.
|
||||
objectDir, _ := filepath.Split(localPath)
|
||||
if objectDir != "" {
|
||||
// Create any missing top level directories.
|
||||
if err := os.MkdirAll(objectDir, 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Download object.
|
||||
res, err := blob.Download(ctx, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Prepare target file.
|
||||
f, err := os.OpenFile(localPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Calculate hash during write.
|
||||
// NOTE: not actively used at present, as MD5 is not consistently returned
|
||||
// by API.
|
||||
hash := md5.New()
|
||||
|
||||
// Off we go.
|
||||
mw := io.MultiWriter(f, hash)
|
||||
if _, err = io.Copy(mw, res.Body(nil)); err != nil {
|
||||
if err = f.Close(); err != nil {
|
||||
ctrl.LoggerFrom(ctx).Error(err, "failed to close file after copy error")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return *res.ETag, nil
|
||||
}
|
||||
|
||||
// VisitObjects iterates over the items in the provided object storage
|
||||
// bucket, calling visit for every item.
|
||||
// If the underlying client or the visit callback returns an error,
|
||||
// it returns early.
|
||||
func (c *BlobClient) VisitObjects(ctx context.Context, bucketName string, visit func(path, etag string) error) error {
|
||||
container := c.ServiceClient.NewContainerClient(bucketName)
|
||||
|
||||
items := container.ListBlobsFlat(&azblob.ContainerListBlobFlatSegmentOptions{})
|
||||
for items.NextPage(ctx) {
|
||||
resp := items.PageResponse()
|
||||
|
||||
for _, blob := range resp.ContainerListBlobFlatSegmentResult.Segment.BlobItems {
|
||||
if err := visit(*blob.Name, fmt.Sprintf("%x", *blob.Properties.Etag)); err != nil {
|
||||
err = fmt.Errorf("listing objects from bucket '%s' failed: %w", bucketName, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := items.Err(); err != nil {
|
||||
err = fmt.Errorf("listing objects from bucket '%s' failed: %w", bucketName, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close has no effect on BlobClient.
|
||||
func (c *BlobClient) Close(_ context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ObjectIsNotFound checks if the error provided is an azblob.StorageError with
|
||||
// an azblob.StorageErrorCodeBlobNotFound error code.
|
||||
func (c *BlobClient) ObjectIsNotFound(err error) bool {
|
||||
var stgErr *azblob.StorageError
|
||||
if errors.As(err, &stgErr) {
|
||||
if stgErr.ErrorCode == azblob.StorageErrorCodeBlobNotFound {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// tokenCredentialsFromSecret attempts to create an azcore.TokenCredential
|
||||
// based on the data fields of the given Secret. It returns, in order:
|
||||
// - azidentity.ClientSecretCredential when `tenantId`, `clientId` and
|
||||
// `clientSecret` fields are found.
|
||||
// - azidentity.ClientCertificateCredential when `tenantId`,
|
||||
// `clientCertificate` (and optionally `clientCertificatePassword`) fields
|
||||
// are found.
|
||||
// - azidentity.ManagedIdentityCredential for a User ID, when a `clientId`
|
||||
// field but no `tenantId` is found.
|
||||
// - Nil, if no valid set of credential fields was found.
|
||||
func tokenCredentialFromSecret(secret *corev1.Secret) (azcore.TokenCredential, error) {
|
||||
if secret == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
clientID, hasClientID := secret.Data[clientIDField]
|
||||
if tenantID, hasTenantID := secret.Data[tenantIDField]; hasTenantID && hasClientID {
|
||||
if clientSecret, hasClientSecret := secret.Data[clientSecretField]; hasClientSecret && len(clientSecret) > 0 {
|
||||
opts := &azidentity.ClientSecretCredentialOptions{}
|
||||
if authorityHost, hasAuthorityHost := secret.Data[authorityHostField]; hasAuthorityHost {
|
||||
opts.AuthorityHost = azidentity.AuthorityHost(authorityHost)
|
||||
}
|
||||
return azidentity.NewClientSecretCredential(string(tenantID), string(clientID), string(clientSecret), opts)
|
||||
}
|
||||
if clientCertificate, hasClientCertificate := secret.Data[clientCertificateField]; hasClientCertificate && len(clientCertificate) > 0 {
|
||||
certs, key, err := azidentity.ParseCertificates(clientCertificate, secret.Data[clientCertificatePasswordField])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse client certificates: %w", err)
|
||||
}
|
||||
opts := &azidentity.ClientCertificateCredentialOptions{}
|
||||
if authorityHost, hasAuthorityHost := secret.Data[authorityHostField]; hasAuthorityHost {
|
||||
opts.AuthorityHost = azidentity.AuthorityHost(authorityHost)
|
||||
}
|
||||
if v, sendChain := secret.Data[clientCertificateSendChainField]; sendChain {
|
||||
opts.SendCertificateChain = string(v) == "1" || strings.ToLower(string(v)) == "true"
|
||||
}
|
||||
return azidentity.NewClientCertificateCredential(string(tenantID), string(clientID), certs, key, opts)
|
||||
}
|
||||
}
|
||||
if hasClientID {
|
||||
return azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
|
||||
ID: azidentity.ClientID(clientID),
|
||||
})
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// sharedCredentialFromSecret attempts to create an azblob.SharedKeyCredential
|
||||
// based on the data fields of the given Secret. It returns nil if the Secret
|
||||
// does not contain a valid set of credentials.
|
||||
func sharedCredentialFromSecret(endpoint string, secret *corev1.Secret) (*azblob.SharedKeyCredential, error) {
|
||||
if accountKey, hasAccountKey := secret.Data[accountKeyField]; hasAccountKey {
|
||||
accountName, err := extractAccountNameFromEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create shared credential from '%s' Secret data: %w", secret.Name, err)
|
||||
}
|
||||
return azblob.NewSharedKeyCredential(accountName, string(accountKey))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// chainCredentialWithSecret tries to create a set of tokens, and returns an
|
||||
// azidentity.ChainedTokenCredential if at least one of the following tokens was
|
||||
// successfully created:
|
||||
//
|
||||
// - azidentity.EnvironmentCredential with `authorityHost` from Secret, if
|
||||
// provided.
|
||||
// - azidentity.ManagedIdentityCredential with Client ID from AZURE_CLIENT_ID
|
||||
// environment variable, if found.
|
||||
// - azidentity.ManagedIdentityCredential with defaults.
|
||||
//
|
||||
// If no valid token is created, it returns nil.
|
||||
func chainCredentialWithSecret(secret *corev1.Secret) (azcore.TokenCredential, error) {
|
||||
var creds []azcore.TokenCredential
|
||||
|
||||
credOpts := &azidentity.EnvironmentCredentialOptions{}
|
||||
if secret != nil {
|
||||
if authorityHost, hasAuthorityHost := secret.Data[authorityHostField]; hasAuthorityHost {
|
||||
credOpts.AuthorityHost = azidentity.AuthorityHost(authorityHost)
|
||||
}
|
||||
}
|
||||
|
||||
if token, _ := azidentity.NewEnvironmentCredential(credOpts); token != nil {
|
||||
creds = append(creds, token)
|
||||
}
|
||||
if clientID := os.Getenv("AZURE_CLIENT_ID"); clientID != "" {
|
||||
if token, _ := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
|
||||
ID: azidentity.ClientID(clientID),
|
||||
}); token != nil {
|
||||
creds = append(creds, token)
|
||||
}
|
||||
}
|
||||
if token, _ := azidentity.NewManagedIdentityCredential(nil); token != nil {
|
||||
creds = append(creds, token)
|
||||
}
|
||||
|
||||
if len(creds) > 0 {
|
||||
return azidentity.NewChainedTokenCredential(creds, nil)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// extractAccountNameFromEndpoint extracts the Azure account name from the
|
||||
// provided endpoint URL. It parses the endpoint as a URL, and returns the
|
||||
// first subdomain as the assumed account name.
|
||||
// It returns an error when it fails to parse the endpoint as a URL, or if it
|
||||
// does not have any subdomains.
|
||||
func extractAccountNameFromEndpoint(endpoint string) (string, error) {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to extract account name from endpoint: %w", err)
|
||||
}
|
||||
hostname := u.Hostname()
|
||||
parts := strings.Split(hostname, ".")
|
||||
if len(parts) <= 2 {
|
||||
return "", fmt.Errorf("failed to extract account name from endpoint: expected '%s' to be a subdomain", hostname)
|
||||
}
|
||||
return parts[0], nil
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
//go:build integration
|
||||
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var (
|
||||
testTimeout = time.Second * 5
|
||||
)
|
||||
|
||||
var (
|
||||
testAccountName = os.Getenv("TEST_AZURE_ACCOUNT_NAME")
|
||||
testAccountKey = os.Getenv("TEST_AZURE_ACCOUNT_KEY")
|
||||
)
|
||||
|
||||
var (
|
||||
testContainerGenerateName = "azure-client-test-"
|
||||
testFile = "test.yaml"
|
||||
testFileData = `
|
||||
---
|
||||
test: file
|
||||
`
|
||||
testFile2 = "test2.yaml"
|
||||
testFile2Data = `
|
||||
---
|
||||
test: file2
|
||||
`
|
||||
testBucket = sourcev1.Bucket{
|
||||
Spec: sourcev1.BucketSpec{
|
||||
Endpoint: endpointURL(testAccountName),
|
||||
},
|
||||
}
|
||||
testSecret = corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
accountKeyField: []byte(testAccountKey),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestBlobClient_BucketExists(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
client, err := NewClient(testBucket.DeepCopy(), testSecret.DeepCopy())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(client).ToNot(BeNil())
|
||||
|
||||
// Generate test container name.
|
||||
testContainer := generateString(testContainerGenerateName)
|
||||
|
||||
// Create test container.
|
||||
ctx, timeout := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
g.Expect(createContainer(ctx, client, testContainer)).To(Succeed())
|
||||
t.Cleanup(func() {
|
||||
g.Expect(deleteContainer(context.Background(), client, testContainer)).To(Succeed())
|
||||
})
|
||||
|
||||
// Test if the container exists.
|
||||
ctx, timeout = context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
ok, err := client.BucketExists(ctx, testContainer)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(ok).To(BeTrue())
|
||||
}
|
||||
|
||||
func TestBlobClient_BucketNotExists(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
client, err := NewClient(testBucket.DeepCopy(), testSecret.DeepCopy())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(client).ToNot(BeNil())
|
||||
|
||||
// Generate test container name.
|
||||
testContainer := generateString(testContainerGenerateName)
|
||||
|
||||
// Test if the container exists.
|
||||
ctx, timeout := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
ok, err := client.BucketExists(ctx, testContainer)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(ok).To(BeFalse())
|
||||
}
|
||||
|
||||
func TestBlobClient_FGetObject(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
client, err := NewClient(testBucket.DeepCopy(), testSecret.DeepCopy())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(client).ToNot(BeNil())
|
||||
|
||||
// Generate test container name.
|
||||
testContainer := generateString(testContainerGenerateName)
|
||||
|
||||
// Create test container.
|
||||
ctx, timeout := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
g.Expect(createContainer(ctx, client, testContainer)).To(Succeed())
|
||||
t.Cleanup(func() {
|
||||
g.Expect(deleteContainer(context.Background(), client, testContainer)).To(Succeed())
|
||||
})
|
||||
|
||||
// Create test blob.
|
||||
ctx, timeout = context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
g.Expect(createBlob(ctx, client, testContainer, testFile, testFileData))
|
||||
|
||||
localPath := filepath.Join(tempDir, testFile)
|
||||
|
||||
// Test if blob exists.
|
||||
ctx, timeout = context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
_, err = client.FGetObject(ctx, testContainer, testFile, localPath)
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(localPath).To(BeARegularFile())
|
||||
f, _ := os.ReadFile(localPath)
|
||||
g.Expect(f).To(Equal([]byte(testFileData)))
|
||||
}
|
||||
|
||||
func TestBlobClient_FGetObject_NotFoundErr(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
client, err := NewClient(testBucket.DeepCopy(), testSecret.DeepCopy())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(client).ToNot(BeNil())
|
||||
|
||||
// Generate test container name.
|
||||
testContainer := generateString(testContainerGenerateName)
|
||||
|
||||
// Create test container.
|
||||
ctx, timeout := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
g.Expect(createContainer(ctx, client, testContainer)).To(Succeed())
|
||||
t.Cleanup(func() {
|
||||
g.Expect(deleteContainer(context.Background(), client, testContainer)).To(Succeed())
|
||||
})
|
||||
|
||||
// Test blob does not exist.
|
||||
ctx, timeout = context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
_, err = client.FGetObject(ctx, testContainer, "doesnotexist.txt", "doesnotexist.txt")
|
||||
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(client.ObjectIsNotFound(err)).To(BeTrue())
|
||||
}
|
||||
|
||||
func TestBlobClient_VisitObjects(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
client, err := NewClient(testBucket.DeepCopy(), testSecret.DeepCopy())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(client).ToNot(BeNil())
|
||||
|
||||
// Generate test container name.
|
||||
testContainer := generateString(testContainerGenerateName)
|
||||
|
||||
// Create test container.
|
||||
ctx, timeout := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
g.Expect(createContainer(ctx, client, testContainer)).To(Succeed())
|
||||
t.Cleanup(func() {
|
||||
g.Expect(deleteContainer(context.Background(), client, testContainer)).To(Succeed())
|
||||
})
|
||||
|
||||
// Create test blobs.
|
||||
ctx, timeout = context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
g.Expect(createBlob(ctx, client, testContainer, testFile, testFileData))
|
||||
g.Expect(createBlob(ctx, client, testContainer, testFile2, testFile2Data))
|
||||
|
||||
visits := make(map[string]string)
|
||||
|
||||
// Visit objects.
|
||||
ctx, timeout = context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
got := client.VisitObjects(ctx, testContainer, func(path, etag string) error {
|
||||
visits[path] = etag
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Expect(got).To(Succeed())
|
||||
g.Expect(visits[testFile]).ToNot(BeEmpty())
|
||||
g.Expect(visits[testFile2]).ToNot(BeEmpty())
|
||||
g.Expect(visits[testFile]).ToNot(Equal(visits[testFile2]))
|
||||
}
|
||||
|
||||
func TestBlobClient_VisitObjects_CallbackErr(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
client, err := NewClient(testBucket.DeepCopy(), testSecret.DeepCopy())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(client).ToNot(BeNil())
|
||||
|
||||
// Generate test container name.
|
||||
testContainer := generateString(testContainerGenerateName)
|
||||
|
||||
// Create test container.
|
||||
ctx, timeout := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
g.Expect(createContainer(ctx, client, testContainer)).To(Succeed())
|
||||
t.Cleanup(func() {
|
||||
g.Expect(deleteContainer(context.Background(), client, testContainer)).To(Succeed())
|
||||
})
|
||||
|
||||
// Create test blob.
|
||||
ctx, timeout = context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
g.Expect(createBlob(ctx, client, testContainer, testFile, testFileData))
|
||||
|
||||
// Visit object.
|
||||
ctx, timeout = context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
mockErr := fmt.Errorf("mock")
|
||||
err = client.VisitObjects(ctx, testContainer, func(path, etag string) error {
|
||||
return mockErr
|
||||
})
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring("mock"))
|
||||
}
|
||||
|
||||
func createContainer(ctx context.Context, client *BlobClient, name string) error {
|
||||
if _, err := client.CreateContainer(ctx, name, nil); err != nil {
|
||||
var stgErr *azblob.StorageError
|
||||
if errors.As(err, &stgErr) {
|
||||
if stgErr.ErrorCode == azblob.StorageErrorCodeContainerAlreadyExists {
|
||||
return nil
|
||||
}
|
||||
err = stgErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createBlob(ctx context.Context, client *BlobClient, containerName, name, data string) error {
|
||||
container := client.NewContainerClient(containerName)
|
||||
blob := container.NewAppendBlobClient(name)
|
||||
|
||||
ctx, timeout := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer timeout()
|
||||
if _, err := blob.Create(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash := md5.Sum([]byte(data))
|
||||
if _, err := blob.AppendBlock(ctx, streaming.NopCloser(strings.NewReader(data)), &azblob.AppendBlockOptions{
|
||||
TransactionalContentMD5: hash[:16],
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteContainer(ctx context.Context, client *BlobClient, name string) error {
|
||||
if _, err := client.DeleteContainer(ctx, name, nil); err != nil {
|
||||
var stgErr *azblob.StorageError
|
||||
if errors.As(err, &stgErr) {
|
||||
if code := stgErr.ErrorCode; code == azblob.StorageErrorCodeContainerNotFound ||
|
||||
code == azblob.StorageErrorCodeContainerBeingDeleted {
|
||||
return nil
|
||||
}
|
||||
err = stgErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateString(prefix string) string {
|
||||
randBytes := make([]byte, 16)
|
||||
rand.Read(randBytes)
|
||||
return prefix + hex.EncodeToString(randBytes)
|
||||
}
|
|
@ -0,0 +1,378 @@
|
|||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestValidateSecret(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
secret *corev1.Secret
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid UserManagedIdentity Secret",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
clientIDField: []byte("some-client-id-"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid ServicePrincipal Certificate Secret",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
tenantIDField: []byte("some-tenant-id-"),
|
||||
clientIDField: []byte("some-client-id-"),
|
||||
clientCertificateField: []byte("some-certificate"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid ServicePrincipal Secret",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
tenantIDField: []byte("some-tenant-id-"),
|
||||
clientIDField: []byte("some-client-id-"),
|
||||
clientSecretField: []byte("some-client-secret-"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid SharedKey Secret",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
accountKeyField: []byte("some-account-key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid AuthorityHost Secret",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
authorityHostField: []byte("some.host.com"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid ServicePrincipal Secret with missing ClientID and ClientSecret",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
tenantIDField: []byte("some-tenant-id-"),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid empty secret",
|
||||
secret: &corev1.Secret{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid nil secret",
|
||||
secret: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
want := BeNil()
|
||||
if tt.wantErr {
|
||||
want = HaveOccurred()
|
||||
}
|
||||
g.Expect(ValidateSecret(tt.secret)).To(want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobClient_ObjectIsNotFound(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "StorageError with BlobNotFound code",
|
||||
err: &azblob.StorageError{ErrorCode: azblob.StorageErrorCodeBlobNotFound},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "StorageError with different code",
|
||||
err: &azblob.StorageError{ErrorCode: azblob.StorageErrorCodeInternalError},
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
err: errors.New("an error"),
|
||||
},
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
c := &BlobClient{}
|
||||
g.Expect(c.ObjectIsNotFound(tt.err)).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_extractAccountNameFromEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "returns account name for endpoint",
|
||||
endpoint: "https://foo.blob.core.windows.net",
|
||||
want: "foo",
|
||||
},
|
||||
{
|
||||
name: "error for endpoint URL parse err",
|
||||
endpoint: "#http//foo.blob.core.windows.net",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "error for endpoint URL without subdomain",
|
||||
endpoint: "https://windows.net",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
got, err := extractAccountNameFromEndpoint(tt.endpoint)
|
||||
g.Expect(err != nil).To(Equal(tt.wantErr))
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_tokenCredentialFromSecret(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
secret *corev1.Secret
|
||||
want azcore.TokenCredential
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "with ClientID field",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
clientIDField: []byte("client-id"),
|
||||
},
|
||||
},
|
||||
want: &azidentity.ManagedIdentityCredential{},
|
||||
},
|
||||
{
|
||||
name: "with TenantID, ClientID and ClientCertificate fields",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
clientIDField: []byte("client-id"),
|
||||
tenantIDField: []byte("tenant-id"),
|
||||
clientCertificateField: validTls(t),
|
||||
},
|
||||
},
|
||||
want: &azidentity.ClientCertificateCredential{},
|
||||
},
|
||||
{
|
||||
name: "with TenantID, ClientID and ClientSecret fields",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
clientIDField: []byte("client-id"),
|
||||
tenantIDField: []byte("tenant-id"),
|
||||
clientSecretField: []byte("client-secret"),
|
||||
},
|
||||
},
|
||||
want: &azidentity.ClientSecretCredential{},
|
||||
},
|
||||
{
|
||||
name: "empty secret",
|
||||
secret: &corev1.Secret{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got, err := tokenCredentialFromSecret(tt.secret)
|
||||
g.Expect(err != nil).To(Equal(tt.wantErr))
|
||||
if tt.want != nil {
|
||||
g.Expect(got).ToNot(BeNil())
|
||||
g.Expect(got).To(BeAssignableToTypeOf(tt.want))
|
||||
return
|
||||
}
|
||||
g.Expect(got).To(BeNil())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sharedCredentialFromSecret(t *testing.T) {
|
||||
var testKey = []byte("dGVzdA==")
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
secret *corev1.Secret
|
||||
want *azblob.SharedKeyCredential
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "with AccountKey field",
|
||||
endpoint: "https://some.endpoint.com",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
accountKeyField: testKey,
|
||||
},
|
||||
},
|
||||
want: &azblob.SharedKeyCredential{},
|
||||
},
|
||||
{
|
||||
name: "invalid endpoint",
|
||||
endpoint: "#http//some.endpoint.com",
|
||||
secret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
accountKeyField: testKey,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty secret",
|
||||
secret: &corev1.Secret{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got, err := sharedCredentialFromSecret(tt.endpoint, tt.secret)
|
||||
g.Expect(err != nil).To(Equal(tt.wantErr))
|
||||
if tt.want != nil {
|
||||
g.Expect(got).ToNot(BeNil())
|
||||
g.Expect(got).To(BeAssignableToTypeOf(tt.want))
|
||||
return
|
||||
}
|
||||
g.Expect(got).To(BeNil())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_chainCredentialWithSecret(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got, err := chainCredentialWithSecret(nil)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(got).To(BeAssignableToTypeOf(&azidentity.ChainedTokenCredential{}))
|
||||
}
|
||||
|
||||
func Test_extractAccountNameFromEndpoint1(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid URL",
|
||||
endpoint: endpointURL("foo"),
|
||||
want: "foo",
|
||||
},
|
||||
{
|
||||
name: "URL parse error",
|
||||
endpoint: " https://example.com",
|
||||
wantErr: "first path segment in URL cannot contain colon",
|
||||
},
|
||||
{
|
||||
name: "error on non subdomain",
|
||||
endpoint: "https://example.com",
|
||||
wantErr: "expected 'example.com' to be a subdomain",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got, err := extractAccountNameFromEndpoint(tt.endpoint)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(got).To(BeEmpty())
|
||||
return
|
||||
}
|
||||
g.Expect(err).To(BeNil())
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func endpointURL(accountName string) string {
|
||||
return fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
|
||||
}
|
||||
|
||||
func validTls(t *testing.T) []byte {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal("Private key cannot be created.", err.Error())
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
|
||||
var privateKey = &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
}
|
||||
if err = pem.Encode(out, privateKey); err != nil {
|
||||
t.Fatal("Private key cannot be PEM encoded.", err.Error())
|
||||
}
|
||||
|
||||
certTemplate := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1337),
|
||||
}
|
||||
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal("Certificate cannot be created.", err.Error())
|
||||
}
|
||||
var certificate = &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert,
|
||||
}
|
||||
if err = pem.Encode(out, certificate); err != nil {
|
||||
t.Fatal("Certificate cannot be PEM encoded.", err.Error())
|
||||
}
|
||||
|
||||
return out.Bytes()
|
||||
}
|
Loading…
Reference in New Issue