Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to `workflow-plugin-azure` are documented here.

## Unreleased

### Added

- Added `IaCProviderOwnership` support backed by the Azure ARM `workflow-owner`
tag for cross-driver ownership discovery and assignment.

### Changed

- Raised `minEngineVersion` to `0.69.1` for the ownership service contract.

## v2.0.0-rc1 — 2026-05-17

### Breaking changes (workflow#699)
Expand Down
3 changes: 2 additions & 1 deletion cmd/workflow-plugin-azure/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "MIT",
"type": "external",
"tier": "community",
"minEngineVersion": "0.68.2",
"minEngineVersion": "0.69.1",
"iacServices": [
"workflow.plugin.external.iac.IaCProviderRequired",
"workflow.plugin.external.iac.IaCProviderEnumerator",
Expand All @@ -17,6 +17,7 @@
"workflow.plugin.external.iac.IaCProviderDriftConfigDetector",
"workflow.plugin.external.iac.IaCProviderRequirementMapper",
"workflow.plugin.external.iac.IaCProviderRegionLister",
"workflow.plugin.external.iac.IaCProviderOwnership",
"workflow.plugin.external.iac.ResourceDriver",
"workflow.plugin.external.iac.IaCStateBackend"
],
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
github.com/GoCodeAlone/workflow v0.68.2
github.com/GoCodeAlone/workflow v0.69.7
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
)
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFG
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 h1:L7G3dExHBgUxsO3qpTGhk/P2dgnYyW48yn7AO33Tbek=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0/go.mod h1:Ms6gYEy0+A2knfKrwdatsggTXYA2+ICKug8w7STorFw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.2.0 h1:HYGD75g0bQ3VO/Omedm54v4LrD3B1cGImuRF3AJ5wLo=
Expand Down Expand Up @@ -70,8 +72,8 @@ github.com/GoCodeAlone/modular/modules/jsonschema v1.17.0 h1:zoWioqUvuNNDfnjHA1s
github.com/GoCodeAlone/modular/modules/jsonschema v1.17.0/go.mod h1:GDU/jsD6AddmXKedj0wZwieUIaQsTBSGMzuj+XHXMrw=
github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.10.0 h1:+2M/ecyCxDiXfJM4ibcERuu/BBeIbLTQNcVgRsllR64=
github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.10.0/go.mod h1:tlVH1mA5yuU8CB7R7+HXIRaBixZoNid6h+5tew5u3FU=
github.com/GoCodeAlone/workflow v0.68.2 h1:U0ksQOkIwDReuw+nz4kRoCeYwahoBaItqLzwYIRm758=
github.com/GoCodeAlone/workflow v0.68.2/go.mod h1:4UwFYm1cM8a/AvGNb1CZAuob0b0gq7552sxcNMdDALA=
github.com/GoCodeAlone/workflow v0.69.7 h1:LgRTJtbicyOeucyQmHw/F7rjfYP8T15C01p7jNm6kP0=
github.com/GoCodeAlone/workflow v0.69.7/go.mod h1:nWB662ILBUUjL2NBlj7RchyiI4CZ2+UxnpQcbIA2tWE=
github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM=
github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0=
github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318=
Expand Down
57 changes: 42 additions & 15 deletions internal/driver/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package driver
import (
"context"
"fmt"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/GoCodeAlone/workflow/interfaces"
Expand Down Expand Up @@ -45,18 +46,26 @@ func (c *realBlobClient) DeleteContainer(ctx context.Context, containerName stri

// BlobDriver manages Azure Blob Storage containers (infra.storage).
type BlobDriver struct {
resourceGroup string
location string
client BlobClientInterface
subscriptionID string
resourceGroup string
location string
storageAccount string
client BlobClientInterface
}

var _ interfaces.ResourceDriver = (*BlobDriver)(nil)

// SensitiveKeys returns output keys whose values should be masked in logs and plan output.
func (d *BlobDriver) SensitiveKeys() []string { return nil }

func NewBlobDriver(resourceGroup, location string, client BlobClientInterface) *BlobDriver {
return &BlobDriver{resourceGroup: resourceGroup, location: location, client: client}
func NewBlobDriver(subscriptionID, resourceGroup, location, storageAccount string, client BlobClientInterface) *BlobDriver {
return &BlobDriver{
subscriptionID: subscriptionID,
resourceGroup: resourceGroup,
location: location,
storageAccount: storageAccount,
client: client,
}
}

func (d *BlobDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) {
Expand All @@ -68,17 +77,14 @@ func (d *BlobDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) (
return &interfaces.ResourceOutput{
Name: spec.Name,
Type: "infra.storage",
ProviderID: containerName,
ProviderID: d.containerARMID(containerName),
Outputs: map[string]any{"container_name": containerName},
Status: "active",
}, nil
}

func (d *BlobDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) {
containerName := ref.Name
if ref.ProviderID != "" {
containerName = ref.ProviderID
}
containerName := d.containerNameFromRef(ref)

props, err := d.client.GetContainerProperties(ctx, containerName)
if err != nil {
Expand All @@ -91,7 +97,7 @@ func (d *BlobDriver) Read(ctx context.Context, ref interfaces.ResourceRef) (*int
return &interfaces.ResourceOutput{
Name: ref.Name,
Type: "infra.storage",
ProviderID: containerName,
ProviderID: d.containerARMID(containerName),
Outputs: outputs,
Status: "active",
}, nil
Expand All @@ -102,10 +108,7 @@ func (d *BlobDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spe
}

func (d *BlobDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error {
containerName := ref.Name
if ref.ProviderID != "" {
containerName = ref.ProviderID
}
containerName := d.containerNameFromRef(ref)
return d.client.DeleteContainer(ctx, containerName)
}

Expand All @@ -127,3 +130,27 @@ func (d *BlobDriver) HealthCheck(ctx context.Context, ref interfaces.ResourceRef
func (d *BlobDriver) Scale(_ context.Context, _ interfaces.ResourceRef, _ int) (*interfaces.ResourceOutput, error) {
return nil, fmt.Errorf("blob: scale not supported")
}

func (d *BlobDriver) containerARMID(containerName string) string {
if d.subscriptionID == "" || d.resourceGroup == "" || d.storageAccount == "" {
return containerName
}
return fmt.Sprintf(
"/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Storage/storageAccounts/%s/blobServices/default/containers/%s",
d.subscriptionID,
d.resourceGroup,
d.storageAccount,
containerName,
)
}

func (d *BlobDriver) containerNameFromRef(ref interfaces.ResourceRef) string {
if ref.ProviderID == "" {
return ref.Name
}
const marker = "/blobServices/default/containers/"
if idx := strings.LastIndex(ref.ProviderID, marker); idx >= 0 {
return ref.ProviderID[idx+len(marker):]
}
return ref.ProviderID
}
53 changes: 37 additions & 16 deletions internal/driver/blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,26 @@ import (
)

type mockBlobClient struct {
createFn func(ctx context.Context, containerName string) error
getFn func(ctx context.Context, containerName string) (map[string]string, error)
deleteFn func(ctx context.Context, containerName string) error
createdName string
readName string
deletedName string
createFn func(ctx context.Context, containerName string) error
getFn func(ctx context.Context, containerName string) (map[string]string, error)
deleteFn func(ctx context.Context, containerName string) error
}

func (m *mockBlobClient) CreateContainer(ctx context.Context, containerName string) error {
m.createdName = containerName
return m.createFn(ctx, containerName)
}

func (m *mockBlobClient) GetContainerProperties(ctx context.Context, containerName string) (map[string]string, error) {
m.readName = containerName
return m.getFn(ctx, containerName)
}

func (m *mockBlobClient) DeleteContainer(ctx context.Context, containerName string) error {
m.deletedName = containerName
return m.deleteFn(ctx, containerName)
}

Expand All @@ -38,7 +44,7 @@ func TestBlobDriver_Create(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
out, err := drv.Create(context.Background(), interfaces.ResourceSpec{
Name: "test-blob",
Type: "infra.storage",
Expand All @@ -53,6 +59,10 @@ func TestBlobDriver_Create(t *testing.T) {
if out.Outputs["container_name"] != "mycontainer" {
t.Errorf("container_name = %v, want mycontainer", out.Outputs["container_name"])
}
wantID := "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/account/blobServices/default/containers/mycontainer"
if out.ProviderID != wantID {
t.Errorf("ProviderID = %q, want %q", out.ProviderID, wantID)
}
}

func TestBlobDriver_Read(t *testing.T) {
Expand All @@ -62,11 +72,18 @@ func TestBlobDriver_Read(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
out, err := drv.Read(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: "mycontainer"})
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
id := "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/account/blobServices/default/containers/mycontainer"
out, err := drv.Read(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: id})
if err != nil {
t.Fatalf("Read: %v", err)
}
if client.readName != "mycontainer" {
t.Errorf("readName = %q, want mycontainer", client.readName)
}
if out.ProviderID != id {
t.Errorf("ProviderID = %q, want %q", out.ProviderID, id)
}
if out.Outputs["custom-tag"] != "value" {
t.Errorf("custom-tag = %v, want value", out.Outputs["custom-tag"])
}
Expand All @@ -79,7 +96,7 @@ func TestBlobDriver_Create_Error(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
_, err := drv.Create(context.Background(), interfaces.ResourceSpec{
Name: "test-blob",
Config: map[string]any{"container_name": "mycontainer"},
Expand All @@ -96,7 +113,7 @@ func TestBlobDriver_Update(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
out, err := drv.Update(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: "mycontainer"}, interfaces.ResourceSpec{
Name: "test-blob",
Config: map[string]any{},
Expand All @@ -116,7 +133,7 @@ func TestBlobDriver_Update_Error(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
_, err := drv.Update(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: "mycontainer"}, interfaces.ResourceSpec{
Name: "test-blob",
Config: map[string]any{},
Expand All @@ -135,14 +152,18 @@ func TestBlobDriver_Delete(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
err := drv.Delete(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: "mycontainer"})
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
id := "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/account/blobServices/default/containers/mycontainer"
err := drv.Delete(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: id})
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Error("expected DeleteContainer to be called")
}
if client.deletedName != "mycontainer" {
t.Errorf("deletedName = %q, want mycontainer", client.deletedName)
}
}

func TestBlobDriver_Delete_Error(t *testing.T) {
Expand All @@ -152,15 +173,15 @@ func TestBlobDriver_Delete_Error(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
err := drv.Delete(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: "mycontainer"})
if err == nil {
t.Fatal("expected error, got nil")
}
}

func TestBlobDriver_Diff_NilCurrent(t *testing.T) {
drv := NewBlobDriver("rg", "eastus", nil)
drv := NewBlobDriver("sub", "rg", "eastus", "account", nil)
diff, err := drv.Diff(context.Background(), interfaces.ResourceSpec{Name: "x"}, nil)
if err != nil {
t.Fatal(err)
Expand All @@ -171,7 +192,7 @@ func TestBlobDriver_Diff_NilCurrent(t *testing.T) {
}

func TestBlobDriver_Diff_NoChanges(t *testing.T) {
drv := NewBlobDriver("rg", "eastus", nil)
drv := NewBlobDriver("sub", "rg", "eastus", "account", nil)
diff, err := drv.Diff(context.Background(), interfaces.ResourceSpec{Name: "x"}, &interfaces.ResourceOutput{})
if err != nil {
t.Fatal(err)
Expand All @@ -188,7 +209,7 @@ func TestBlobDriver_HealthCheck_Healthy(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
h, err := drv.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: "mycontainer"})
if err != nil {
t.Fatal(err)
Expand All @@ -205,7 +226,7 @@ func TestBlobDriver_HealthCheck_Unhealthy(t *testing.T) {
},
}

drv := NewBlobDriver("rg", "eastus", client)
drv := NewBlobDriver("sub", "rg", "eastus", "account", client)
h, err := drv.HealthCheck(context.Background(), interfaces.ResourceRef{Name: "test-blob", ProviderID: "mycontainer"})
if err != nil {
t.Fatal(err)
Expand Down
2 changes: 1 addition & 1 deletion internal/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func NewAll(subscriptionID, resourceGroup, location, storageAccount string, cred
"infra.api_gateway": NewAPIMDriver(resourceGroup, location, &realAPIMClient{inner: apimRaw}),
"infra.firewall": NewNSGDriver(resourceGroup, location, &realNSGClient{inner: nsgRaw}),
"infra.iam_role": NewMSIDriver(resourceGroup, location, &realMSIClient{inner: msiRaw}),
"infra.storage": NewBlobDriver(resourceGroup, location, &realBlobClient{inner: blobRaw}),
"infra.storage": NewBlobDriver(subscriptionID, resourceGroup, location, storageAccount, &realBlobClient{inner: blobRaw}),
"infra.certificate": NewCertDriver(resourceGroup, location, &realCertClient{inner: certRaw}),
}, nil
}
Expand Down
Loading
Loading