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
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,27 @@ Copy the binary on the same system where garm is running, and [point to it in th

The config file for this external provider is a simple toml used to configure the azure credentials it needs to spin up virtual machines.

For now, only service principles credentials and azure managed identity are supported. An example can be found [in the testdata folder](./testdata/config.toml).
For now, service principals, azure managed identities and workload identities are supported. An example can be found [in the testdata folder](./testdata/config.toml).

```toml
location = "westeurope"
use_ephemeral_storage = true
virtual_network_cidr = "10.0.0.0/16"
use_accelerated_networking = true

[credentials]
subscription_id = "sample_sub_id"

# The service principle service credentials can be used when azure managed identity
# The workload identity can be used when other methods are not available
# or if a more precise selection of the workload on which to apply permissions is needed.
# This seems to be the latest recommended method from Azure.
# More information here: https://learn.microsoft.com/en-us/entra/workload-id/workload-identities-overview
[credentials.workload_identity]
tenant_id = "sample_tenant_id"
client_id = "sample_client_id"
federated_token_file = "/path/to/federated_token_file"

# The service principal credentials can be used when azure managed identity
# is not available.
[credentials.service_principal]
# you can create a SP using:
Expand All @@ -41,7 +53,7 @@ subscription_id = "sample_sub_id"
client_secret = "super secret client secret"

# The managed identity token source is always added to the chain of possible authentication
# sources. The client ID can be overwritten if needed.
# sources. The client ID can be overwritten if needed.
[credentials.managed_identity]
# The client ID to use. This config value is optional.
client_id = "sample_client_id"
Expand Down
47 changes: 39 additions & 8 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package config
import (
"fmt"
"net"
"os"
"regexp"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
Expand Down Expand Up @@ -76,9 +77,10 @@ func (c *Config) Validate() error {
}

type Credentials struct {
SubscriptionID string `toml:"subscription_id"`
SPCredentials ServicePrincipalCredentials `toml:"service_principal"`
ManagedIdentity ManagedIdentityCredentials `toml:"managed_identity"`
SubscriptionID string `toml:"subscription_id"`
SPCredentials ServicePrincipalCredentials `toml:"service_principal"`
WorkloadIdentity WorkloadIdentityCredentials `toml:"workload_identity"`
ManagedIdentity ManagedIdentityCredentials `toml:"managed_identity"`
// ClientOptions is the azure identity client options that will be used to authenticate
// against an azure cloud. This is a heavy handed approach for now, defining the entire
// ClientOptions here, but should allow users to use this provider with AzureStack or any
Expand Down Expand Up @@ -117,17 +119,38 @@ func (c Credentials) GetCredentials() (azcore.TokenCredential, error) {
creds = append(creds, spCreds)
}

o := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: c.ClientOptions}
// Let's try with Workload Identity first, since this is the preferred method nowadays.
if c.WorkloadIdentity.ClientID != "" {
wiOpts := &azidentity.WorkloadIdentityCredentialOptions{ClientOptions: c.ClientOptions}
wiOpts.ClientID = c.WorkloadIdentity.ClientID
if c.WorkloadIdentity.TenantID != "" {
wiOpts.TenantID = c.WorkloadIdentity.TenantID
}
// NOTE: AuthorityHost is ignored for now
if c.WorkloadIdentity.FederatedTokenFile != "" {
if _, err := os.Stat(c.WorkloadIdentity.FederatedTokenFile); err != nil {
return nil, fmt.Errorf("federated token file %s does not exist: %w", c.WorkloadIdentity.FederatedTokenFile, err)
}
wiOpts.TokenFilePath = c.WorkloadIdentity.FederatedTokenFile
}
wiCred, err := azidentity.NewWorkloadIdentityCredential(wiOpts)
if err == nil {
creds = append(creds, wiCred)
}
}

// After Workload Identity is configured (or not), we can try Managed Identity.
miOpts := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: c.ClientOptions}
if c.ManagedIdentity.ClientID != "" {
o.ID = azidentity.ClientID(c.ManagedIdentity.ClientID)
miOpts.ID = azidentity.ClientID(c.ManagedIdentity.ClientID)
}
miCred, err := azidentity.NewManagedIdentityCredential(o)
miCred, err := azidentity.NewManagedIdentityCredential(miOpts)
if err == nil {
creds = append(creds, miCred)
}

if len(creds) == 0 {
return nil, fmt.Errorf("failed to get credentials")
return nil, fmt.Errorf("failed to get credentials: %w", err)
}

chain, err := azidentity.NewChainedTokenCredential(creds, nil)
Expand All @@ -154,7 +177,7 @@ func (c ServicePrincipalCredentials) Validate() error {
}

if c.ClientSecret == "" {
return fmt.Errorf("missing subscription_id")
return fmt.Errorf("missing client_secret")
}

return nil
Expand All @@ -173,6 +196,14 @@ func (c ServicePrincipalCredentials) Auth(opts azcore.ClientOptions) (azcore.Tok
return cred, nil
}

type WorkloadIdentityCredentials struct {
TenantID string `toml:"tenant_id"`
ClientID string `toml:"client_id"`
FederatedTokenFile string `toml:"federated_token_file"`
// AuthorityHost is not handled yet.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't need this now, we can just remove the commented code and add it later.

// AuthorityHost string `toml:"authority_host"`
}

type ManagedIdentityCredentials struct {
ClientID string `toml:"client_id"`
}
42 changes: 28 additions & 14 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,25 @@ func TestNewConfig(t *testing.T) {
[credentials]
subscription_id = "sample_sub_id"

# The service principle service credentials can be used when azure managed identity
# is not available.
[credentials.service_principal]
# you can create a SP using:
# az ad sp create-for-rbac --scopes /subscriptions/<subscription ID> --role Contributor
tenant_id = "sample_tenant_id"
client_id = "sample_client_id"
client_secret = "super secret client secret"

# The managed identity token source is always added to the chain of possible authentication
# sources. The client ID can be overwritten if needed.
[credentials.managed_identity]
# The client ID to use. This config value is optional.
client_id = "sample_client_id"
[credentials.workload_identity]
tenant_id = "sample_tenant_id"
client_id = "sample_client_id"
federated_token_file = "/dev/null"

# The service principle service credentials can be used when azure managed identity
# is not available.
[credentials.service_principal]
# you can create a SP using:
# az ad sp create-for-rbac --scopes /subscriptions/<subscription ID> --role Contributor
tenant_id = "sample_tenant_id"
client_id = "sample_client_id"
client_secret = "super secret client secret"

# The managed identity token source is always added to the chain of possible authentication
# sources. The client ID can be overwritten if needed.
[credentials.managed_identity]
# The client ID to use. This config value is optional.
client_id = "sample_client_id"
`

// Create a temporary file
Expand All @@ -194,10 +199,19 @@ func TestNewConfig(t *testing.T) {
require.Equal(t, "sample_client_id", cfg.Credentials.SPCredentials.ClientID, "ClientID is not as expected")
require.Equal(t, "super secret client secret", cfg.Credentials.SPCredentials.ClientSecret, "ClientSecret is not as expected")
require.Equal(t, "sample_client_id", cfg.Credentials.ManagedIdentity.ClientID, "ManagedIdentity ClientID is not as expected")
require.Equal(t, "sample_tenant_id", cfg.Credentials.WorkloadIdentity.TenantID, "WorkloadIdentity TenantID is not as expected")
require.Equal(t, "sample_client_id", cfg.Credentials.WorkloadIdentity.ClientID, "WorkloadIdentity ClientID is not as expected")
require.Equal(t, "/dev/null", cfg.Credentials.WorkloadIdentity.FederatedTokenFile, "WorkloadIdentity FederatedTokenFile is not as expected")

require.True(t, cfg.UseEphemeralStorage, "UseEphemeralStorage is not as expected")
require.Equal(t, "10.10.0.0/24", cfg.VirtualNetworkCIDR, "VirtualNetworkCIDR is not as expected")
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-Network/providers/Microsoft.Network/virtualNetworks/vnet-Default/subnets/snet-default", cfg.VnetSubnetID, "VnetSubnetID is not as expected")
require.True(t, cfg.UseAcceleratedNetworking, "UseAcceleratedNetworking is not as expected")
require.True(t, cfg.DisableIsolatedNetworks, "DisableIsolatedNetworks is not as expected")
}

func TestAbsentTokenFile(t *testing.T) {
conf, err := NewConfig("../testdata/config_with_workload_identity.toml")
require.Error(t, err, "NewConfig should return an error")
require.Nil(t, conf)
}
15 changes: 12 additions & 3 deletions testdata/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ use_accelerated_networking = true
[credentials]
subscription_id = "sample_sub_id"

# The service principle service credentials can be used when azure managed identity
# The workload identity can be used when other methods are not available
# or if a more precise selection of the workload on which to apply permissions is needed.
# This seems to be the latest recommended method from Azure.
# More information here: https://learn.microsoft.com/en-us/entra/workload-id/workload-identities-overview
[credentials.workload_identity]
tenant_id = "sample_tenant_id"
client_id = "sample_client_id"
federated_token_file = "/path/to/federated_token_file"

# The service principal credentials can be used when azure managed identity
# is not available.
[credentials.service_principal]
# you can create a SP using:
Expand All @@ -16,7 +25,7 @@ subscription_id = "sample_sub_id"
client_secret = "super secret client secret"

# The managed identity token source is always added to the chain of possible authentication
# sources. The client ID can be overwritten if needed.
# sources. The client ID can be overwritten if needed.
[credentials.managed_identity]
# The client ID to use. This config value is optional.
client_id = "sample_client_id"
client_id = "sample_client_id"
14 changes: 14 additions & 0 deletions testdata/config_with_workload_identity.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
location = "westeurope"
use_ephemeral_storage = true
virtual_network_cidr = "10.10.0.0/24"
use_accelerated_networking = true
vnet_subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-Network/providers/Microsoft.Network/virtualNetworks/vnet-Default/subnets/snet-default"
disable_isolated_networks = true

[credentials]
subscription_id = "sample_sub_id"

[credentials.workload_identity]
tenant_id = "sample_tenant_id"
client_id = "sample_client_id"
federated_token_file = "/path/to/federated_token_file"