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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ env:
GONOSUMDB: github.com/GoCodeAlone/*
GOPRIVATE: github.com/GoCodeAlone/*

permissions:
contents: read

jobs:
test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -22,3 +26,18 @@ jobs:
run: git config --global url."https://${{ secrets.RELEASES_TOKEN }}@github.com/".insteadOf "https://github.com/"
- run: go build ./...
- run: go test ./... -v -race -count=1

wfctl-strict-contracts:
name: Strict Contract Validation
runs-on: ubuntu-latest
permissions:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Configure git for private modules
run: git config --global url."https://${{ secrets.RELEASES_TOKEN }}@github.com/".insteadOf "https://github.com/"
- name: Validate strict plugin contracts
run: go run github.com/GoCodeAlone/workflow/cmd/wfctl@v0.64.3 plugin validate --file plugin.json --strict-contracts
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

- uses: GoCodeAlone/setup-wfctl@v1
with:
version: v0.64.0
version: v0.64.3

- name: Validate plugin contract for publish (pre-build)
run: wfctl plugin validate-contract --for-publish --tag "${{ inputs.tag || github.ref_name }}" .
Expand Down
13 changes: 10 additions & 3 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
version: 2

project_name: workflow-plugin-azure

before:
hooks:
- "cp plugin.json plugin.json.orig"
- "sed -i.bak 's/\"version\": \".*\"/\"version\": \"{{ .Version }}\"/' plugin.json && rm -f plugin.json.bak"
- "sh -c \"cp plugin.json cmd/workflow-plugin-azure/plugin.json\""
- "sh -c \"rm -rf .release && mkdir -p .release && cp plugin.json .release/plugin.json && cp plugin.contracts.json .release/plugin.contracts.json && sed -i.bak 's/\\\"version\\\": \\\".*\\\"/\\\"version\\\": \\\"{{ .Version }}\\\"/' .release/plugin.json && rm -f .release/plugin.json.bak\""
- "sh -c \"sed -i.bak 's|/releases/download/v[^/]*/|/releases/download/{{ .Tag }}/|g' .release/plugin.json && rm -f .release/plugin.json.bak\""
- "sh -c \"export GOPRIVATE=github.com/GoCodeAlone/*; WFCTL_VERSION=$(GOWORK=off go list -m github.com/GoCodeAlone/workflow | awk '{print $2}') && GOWORK=off go run github.com/GoCodeAlone/workflow/cmd/wfctl@${WFCTL_VERSION} plugin validate --file .release/plugin.json --strict-contracts\""

builds:
- main: ./cmd/workflow-plugin-azure
Expand All @@ -23,7 +27,10 @@ archives:
- formats: [tar.gz]
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
files:
- plugin.json
- src: .release/plugin.json
dst: plugin.json
- plugin.contracts.json
- LICENSE

checksum:
name_template: checksums.txt
Expand Down
11 changes: 10 additions & 1 deletion cmd/workflow-plugin-azure/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@
package main

import (
_ "embed"

"github.com/GoCodeAlone/workflow-plugin-azure/internal"
sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk"
)

// pluginJSON is copied from the repository root by GoReleaser before builds
// and is committed for local builds/tests.
//
//go:embed plugin.json
var pluginJSON []byte

func main() {
sdk.ServeIaCPlugin(internal.NewIaCServer(), sdk.IaCServeOptions{
BuildVersion: sdk.ResolveBuildVersion(internal.Version),
ManifestProvider: sdk.MustEmbedManifest(pluginJSON),
BuildVersion: sdk.ResolveBuildVersion(internal.Version),
})
}
68 changes: 68 additions & 0 deletions cmd/workflow-plugin-azure/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"name": "workflow-plugin-azure",
"version": "0.0.0",
"author": "GoCodeAlone",
"description": "Microsoft Azure infrastructure provider: ACI, AKS, SQL, Redis, VNet, LB, DNS, ACR, APIM, NSG, MSI, Blob Storage, App Service Certificates",
"license": "MIT",
"type": "external",
"tier": "community",
"minEngineVersion": "0.64.3",
"iacServices": [
"workflow.plugin.external.iac.IaCProviderRequired",
"workflow.plugin.external.iac.IaCProviderEnumerator",
"workflow.plugin.external.iac.IaCProviderDriftDetector",
"workflow.plugin.external.iac.IaCProviderCredentialRevoker",
"workflow.plugin.external.iac.IaCProviderMigrationRepairer",
"workflow.plugin.external.iac.IaCProviderValidator",
"workflow.plugin.external.iac.IaCProviderDriftConfigDetector",
"workflow.plugin.external.iac.IaCProviderRequirementMapper",
"workflow.plugin.external.iac.ResourceDriver",
"workflow.plugin.external.iac.IaCStateBackend"
],
"keywords": [
"azure",
"iac",
"infrastructure",
"cloud",
"aci",
"aks",
"sql",
"redis",
"vnet"
],
"homepage": "https://github.com/GoCodeAlone/workflow-plugin-azure",
"repository": "https://github.com/GoCodeAlone/workflow-plugin-azure",
"downloads": [
{
"os": "linux",
"arch": "amd64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-azure/releases/download/v0.0.0/workflow-plugin-azure-linux-amd64.tar.gz"
},
{
"os": "linux",
"arch": "arm64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-azure/releases/download/v0.0.0/workflow-plugin-azure-linux-arm64.tar.gz"
},
{
"os": "darwin",
"arch": "amd64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-azure/releases/download/v0.0.0/workflow-plugin-azure-darwin-amd64.tar.gz"
},
{
"os": "darwin",
"arch": "arm64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-azure/releases/download/v0.0.0/workflow-plugin-azure-darwin-arm64.tar.gz"
}
],
"capabilities": {
"configProvider": false,
"moduleTypes": [
"iac.provider"
],
"stepTypes": [],
"triggerTypes": [],
"iacStateBackends": [
"azure_blob"
]
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis v1.0.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.64.0
github.com/GoCodeAlone/workflow v0.64.3
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0 h1:xb1mI4NZkzvNKQ2F6nk
github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0/go.mod h1:hhGouwAVsonmJ4Lain4jINZ9nZCoc9l9eF3BHbmR8eE=
github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0 h1:cvdLHbM/vzvygQTcAWSJsy+dAPzzwWyjzKMmTBFcFIo=
github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0/go.mod h1:/9ipMG4qM2CHQ14BfXKdVlYRJelef6M8MFI5TbZv67M=
github.com/GoCodeAlone/workflow v0.64.0 h1:2CpbYPwIqdGDb3xi3YJpwcteIum4ehBSrnRql/1YvB4=
github.com/GoCodeAlone/workflow v0.64.0/go.mod h1:659GGDrw3QJ7b625y9rf8QhKIpt1VCoEG0MxKu5tGQs=
github.com/GoCodeAlone/workflow v0.64.3 h1:r0jMoRJXJI8lz44c70mFjGcpy24IWpOTtkX7BC0/fas=
github.com/GoCodeAlone/workflow v0.64.3/go.mod h1:659GGDrw3QJ7b625y9rf8QhKIpt1VCoEG0MxKu5tGQs=
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
175 changes: 174 additions & 1 deletion internal/driver/aci.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package driver
import (
"context"
"fmt"
"sort"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerinstance/armcontainerinstance/v2"
Expand Down Expand Up @@ -79,7 +80,10 @@ func (d *ACIDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) (*
{
Name: str(spec.Name),
Properties: &armcontainerinstance.ContainerProperties{
Image: str(image),
Image: str(image),
Command: aciCommand(spec.Config),
EnvironmentVariables: aciEnvironmentVariables(spec.Config),
Ports: aciContainerPorts(spec.Config),
Resources: &armcontainerinstance.ResourceRequirements{
Requests: &armcontainerinstance.ResourceRequests{
CPU: &cpu,
Expand All @@ -89,6 +93,7 @@ func (d *ACIDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) (*
},
},
},
IPAddress: aciIPAddress(spec.Config),
OSType: ptrOf(armcontainerinstance.OperatingSystemTypesLinux),
RestartPolicy: ptrOf(armcontainerinstance.ContainerGroupRestartPolicyAlways),
},
Expand Down Expand Up @@ -165,3 +170,171 @@ func aciToOutput(name string, cg armcontainerinstance.ContainerGroup) *interface
}

func ptrOf[T any](v T) *T { return &v }

func aciCommand(config map[string]any) []*string {
raw := configList(config, "command")
out := make([]*string, 0, len(raw))
for _, value := range raw {
if strValue, ok := value.(string); ok && strValue != "" {
out = append(out, str(strValue))
}
}
return out
}

func aciEnvironmentVariables(config map[string]any) []*armcontainerinstance.EnvironmentVariable {
vars := make([]*armcontainerinstance.EnvironmentVariable, 0)
vars = append(vars, aciPlainEnvironmentVariables(configMap(config, "env_vars"))...)
vars = append(vars, aciSecureEnvironmentVariables(configMap(config, "env_vars_secret"))...)
return vars
}

func aciPlainEnvironmentVariables(values map[string]any) []*armcontainerinstance.EnvironmentVariable {
keys := sortedConfigMapKeys(values)
out := make([]*armcontainerinstance.EnvironmentVariable, 0, len(keys))
for _, key := range keys {
value, ok := values[key].(string)
if !ok {
continue
}
out = append(out, &armcontainerinstance.EnvironmentVariable{
Name: str(key),
Value: str(value),
})
}
return out
}

func aciSecureEnvironmentVariables(values map[string]any) []*armcontainerinstance.EnvironmentVariable {
keys := sortedConfigMapKeys(values)
out := make([]*armcontainerinstance.EnvironmentVariable, 0, len(keys))
for _, key := range keys {
value, ok := values[key].(string)
if !ok {
continue
}
out = append(out, &armcontainerinstance.EnvironmentVariable{
Name: str(key),
SecureValue: str(value),
})
}
return out
}

func aciContainerPorts(config map[string]any) []*armcontainerinstance.ContainerPort {
portConfigs := aciPortConfigs(config)
out := make([]*armcontainerinstance.ContainerPort, 0, len(portConfigs))
for _, port := range portConfigs {
portValue := port.port
out = append(out, &armcontainerinstance.ContainerPort{
Port: &portValue,
Protocol: ptrOf(armcontainerinstance.ContainerNetworkProtocolTCP),
})
}
return out
}

func aciIPAddress(config map[string]any) *armcontainerinstance.IPAddress {
portConfigs := aciPortConfigs(config)
var publicPorts []*armcontainerinstance.Port
for _, port := range portConfigs {
if !port.public {
continue
}
portValue := port.port
publicPorts = append(publicPorts, &armcontainerinstance.Port{
Port: &portValue,
Protocol: ptrOf(armcontainerinstance.ContainerGroupNetworkProtocolTCP),
})
}
if len(publicPorts) == 0 {
return nil
}
return &armcontainerinstance.IPAddress{
Type: ptrOf(armcontainerinstance.ContainerGroupIPAddressTypePublic),
Ports: publicPorts,
}
}

type aciPortConfig struct {
port int32
public bool
}

func aciPortConfigs(config map[string]any) []aciPortConfig {
raw := configList(config, "ports")
out := make([]aciPortConfig, 0, len(raw))
for _, item := range raw {
values, ok := item.(map[string]any)
if !ok {
continue
}
port, ok := aciPortNumber(values["port"])
if !ok {
continue
}
out = append(out, aciPortConfig{
port: port,
public: boolConfigValue(values["public"]),
})
}
return out
}

func aciPortNumber(value any) (int32, bool) {
const maxPort = 65535
switch typed := value.(type) {
case int:
if typed > 0 && typed <= maxPort {
return int32(typed), true
}
case int32:
if typed > 0 && typed <= maxPort {
return typed, true
}
case int64:
if typed > 0 && typed <= maxPort {
return int32(typed), true
}
case float64:
if typed > 0 && typed <= maxPort {
return int32(typed), true
}
}
return 0, false
}

func configMap(config map[string]any, key string) map[string]any {
switch values := config[key].(type) {
case map[string]any:
return values
case map[string]string:
out := make(map[string]any, len(values))
for name, value := range values {
out[name] = value
}
return out
default:
return nil
}
}

func sortedConfigMapKeys(values map[string]any) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}

func boolConfigValue(value any) bool {
switch typed := value.(type) {
case bool:
return typed
case string:
return typed == "true"
default:
return false
}
}
Loading
Loading