Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions azblob/Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
GO = go
GO=go
EXT=

PLAKAR = plakar
VERSION = v1.1.0-beta.2

GOOS := $(shell go env GOOS)
GOARCH := $(shell go env GOARCH)
PTAR := azblob_$(VERSION)_$(GOOS)_$(GOARCH).ptar

all: build

build:
${GO} build -v -o azblob-importer${EXT} ./importer
${GO} build -v -o azblob-exporter${EXT} ./exporter
${GO} build -v -o azblob-storage${EXT} ./storage

package: build
rm -f $(PTAR)
$(PLAKAR) pkg create ./manifest.yaml $(VERSION)

uninstall:
-$(PLAKAR) pkg rm azblob

install: package
$(PLAKAR) pkg add ./$(PTAR)

reinstall: uninstall install

clean:
rm -f azblob-importer azblob-exporter azblob-storage
rm -f azblob-importer azblob-exporter azblob-storage azblob_*.ptar
15 changes: 15 additions & 0 deletions azblob/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,33 @@ The supported configuration options are:
* `account_key`: Azure Storage account key
* `endpoint`: custom endpoint (e.g. Azurite or non-standard Azure environments)
* `no_auth`: disable authentication (useful for public containers or testing)
* `use_managed_identity`: enable Azure AD authentication through `DefaultAzureCredential`
* `managed_identity_client_id`: optional client ID for user-assigned managed identity

At least one authentication method must be provided:

* `connection_string`
* OR `account_name` + `account_key`
* OR `no_auth=true` with a valid `endpoint`
* OR `use_managed_identity=true` with:
* `account_name` (service URL inferred as `https://<account_name>.blob.core.windows.net`)
* or `endpoint`

Managed identity notes:

* System-assigned identity: set `use_managed_identity=true`.
* User-assigned identity: set `use_managed_identity=true` and `managed_identity_client_id=<client-id>`.

---

## Examples

```sh
# add a store
$ plakar add store azblob://container_name --connection-string "DefaultEndpointsProtocol=https;AccountName=account_name;AccountKey=account_key;EndpointSuffix=core.windows.net"
$ plakar add store azblob://container_name --account-name account_name --account-key account_key
$ plakar add store azblob://container_name --use-managed-identity=true account_name=account_name

# back up a container
$ plakar at /tmp/store backup azblob://container_name

Expand Down
57 changes: 44 additions & 13 deletions azblob/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/PlakarKorp/kloset/connectors"
"github.com/PlakarKorp/kloset/connectors/exporter"
Expand All @@ -21,28 +22,32 @@ type azblobExporter struct {
path string
endpoint string

accountName string
accountKey string
connectionString string
noAuth bool
accountName string
accountKey string
connectionString string
noAuth bool
useManagedIdentity bool
managedIdentityClientID string

client *azblob.Client
}

func NewExporter(ctx context.Context, _ *connectors.Options, proto string, params map[string]string) (exporter.Exporter, error) {
container, prefix, endpoint, accountName, accountKey, connectionString, noAuth, err := parse(params, proto)
container, prefix, endpoint, accountName, accountKey, connectionString, noAuth, useManagedIdentity, managedIdentityClientID, err := parse(params, proto)
if err != nil {
return nil, err
}

exp := &azblobExporter{
containerName: container,
path: prefix,
endpoint: endpoint,
accountName: accountName,
accountKey: accountKey,
connectionString: connectionString,
noAuth: noAuth,
containerName: container,
path: prefix,
endpoint: endpoint,
accountName: accountName,
accountKey: accountKey,
connectionString: connectionString,
noAuth: noAuth,
useManagedIdentity: useManagedIdentity,
managedIdentityClientID: managedIdentityClientID,
}

if err := exp.connect(ctx); err != nil {
Expand Down Expand Up @@ -95,8 +100,34 @@ func (g *azblobExporter) connect(ctx context.Context) error {
g.client = client
return nil

case g.useManagedIdentity:
serviceURL := g.endpoint
if serviceURL == "" {
if g.accountName == "" {
return fmt.Errorf("managed identity requires endpoint or account_name")
}
serviceURL = fmt.Sprintf("https://%s.blob.core.windows.net", g.accountName)
}

opts := &azidentity.ManagedIdentityCredentialOptions{}
if g.managedIdentityClientID != "" {
opts.ID = azidentity.ClientID(g.managedIdentityClientID)
}

cred, err := azidentity.NewManagedIdentityCredential(opts)
if err != nil {
return err
}

client, err := azblob.NewClient(strings.TrimRight(serviceURL, "/")+"/", cred, nil)
if err != nil {
return err
}
g.client = client
return nil

default:
return fmt.Errorf("missing credentials: provide connection_string, or account_name/account_key, or no_auth=true with endpoint")
return fmt.Errorf("missing credentials: provide connection_string, or account_name/account_key, or no_auth=true with endpoint, or use_managed_identity=true with endpoint or account_name")
}
}

Expand Down
59 changes: 57 additions & 2 deletions azblob/exporter/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,45 @@
"type": "boolean",
"default": false,
"description": "Disable authentication, useful for public blobs or specific emulator/test setups"
},
"use_managed_identity": {
"type": "boolean",
"default": false,
"description": "Enable Azure AD authentication via DefaultAzureCredential (supports system-assigned and user-assigned managed identity)"
},
"managed_identity_client_id": {
"type": "string",
"minLength": 1,
"description": "Optional client ID for user-assigned managed identity"
}
},
"oneOf": [
{
"required": [
"location",
"connection_string"
]
],
"not": {
"properties": {
"use_managed_identity": {
"const": true
}
}
}
},
{
"required": [
"location",
"account_name",
"account_key"
]
],
"not": {
"properties": {
"use_managed_identity": {
"const": true
}
}
}
},
{
"required": [
Expand All @@ -63,6 +87,37 @@
"no_auth": {
"const": true
}
},
"not": {
"properties": {
"use_managed_identity": {
"const": true
}
}
}
},
{
"required": [
"location",
"use_managed_identity",
"account_name"
],
"properties": {
"use_managed_identity": {
"const": true
}
}
},
{
"required": [
"location",
"use_managed_identity",
"endpoint"
],
"properties": {
"use_managed_identity": {
"const": true
}
}
}
]
Expand Down
5 changes: 5 additions & 0 deletions azblob/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/PlakarKorp/integrations/azblob
go 1.24.0

require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
github.com/PlakarKorp/go-kloset-sdk v1.1.0-beta.1
github.com/PlakarKorp/kloset v1.1.0-beta.2
Expand All @@ -11,15 +12,19 @@ require (
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/PlakarKorp/integration-grpc v1.1.0-beta.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/nickball/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tink-crypto/tink-go/v2 v2.6.0 // indirect
Expand Down
7 changes: 7 additions & 0 deletions azblob/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16AP
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
Expand Down Expand Up @@ -80,6 +84,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
Expand Down Expand Up @@ -156,6 +162,7 @@ golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
Expand Down
59 changes: 45 additions & 14 deletions azblob/importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/PlakarKorp/kloset/connectors"
"github.com/PlakarKorp/kloset/connectors/importer"
Expand All @@ -25,29 +26,33 @@ type azblobImporter struct {
base string
endpoint string

accountName string
accountKey string
connectionString string
noAuth bool
accountName string
accountKey string
connectionString string
noAuth bool
useManagedIdentity bool
managedIdentityClientID string

client *azblob.Client
}

func NewImporter(ctx context.Context, _ *connectors.Options, proto string, params map[string]string) (importer.Importer, error) {
container, prefix, endpoint, accountName, accountKey, connectionString, noAuth, err := parse(params, proto)
container, prefix, endpoint, accountName, accountKey, connectionString, noAuth, useManagedIdentity, managedIdentityClientID, err := parse(params, proto)
if err != nil {
return nil, err
}

imp := &azblobImporter{
containerName: container,
path: prefix,
base: "/" + prefix,
endpoint: endpoint,
accountName: accountName,
accountKey: accountKey,
connectionString: connectionString,
noAuth: noAuth,
containerName: container,
path: prefix,
base: "/" + prefix,
endpoint: endpoint,
accountName: accountName,
accountKey: accountKey,
connectionString: connectionString,
noAuth: noAuth,
useManagedIdentity: useManagedIdentity,
managedIdentityClientID: managedIdentityClientID,
}

if err := imp.connect(ctx); err != nil {
Expand Down Expand Up @@ -100,8 +105,34 @@ func (g *azblobImporter) connect(ctx context.Context) error {
g.client = client
return nil

case g.useManagedIdentity:
serviceURL := g.endpoint
if serviceURL == "" {
if g.accountName == "" {
return fmt.Errorf("managed identity requires endpoint or account_name")
}
serviceURL = fmt.Sprintf("https://%s.blob.core.windows.net", g.accountName)
}

opts := &azidentity.ManagedIdentityCredentialOptions{}
if g.managedIdentityClientID != "" {
opts.ID = azidentity.ClientID(g.managedIdentityClientID)
}

cred, err := azidentity.NewManagedIdentityCredential(opts)
if err != nil {
return err
}

client, err := azblob.NewClient(strings.TrimRight(serviceURL, "/")+"/", cred, nil)
if err != nil {
return err
}
g.client = client
return nil

default:
return fmt.Errorf("missing credentials: provide connection_string, or account_name/account_key, or no_auth=true with endpoint")
return fmt.Errorf("missing credentials: provide connection_string, or account_name/account_key, or no_auth=true with endpoint, or use_managed_identity=true with endpoint or account_name")
}
}

Expand Down
Loading