Skip to content

Commit

Permalink
Merge pull request #74 from lbergnehr/main
Browse files Browse the repository at this point in the history
Add `login` command
  • Loading branch information
kichristensen authored Jan 24, 2025
2 parents bc4d3dd + 92414a6 commit 29f44ca
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ bin
.cnab
/build/git_askpass.sh
az
!pkg/az
!.gitignore
37 changes: 28 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,18 +219,37 @@ mixins:

### Authenticate

The az mixin supports several authentication methods. All are provided with custom `login` command:

```yaml
az:
description: "Azure CLI login"
arguments:
- login
flags:
service-principal:
username: ${ bundle.credentials.AZURE_SP_CLIENT_ID }
password: ${ bundle.credentials.AZURE_SP_PASSWORD }
tenant: ${ bundle.credentials.AZURE_TENANT }
install:
- az:
login:
```

### Existing Azure CLI Authentication

If you have already authenticated using `az login`, the mixin will use your
existing credentials. This requires the following files to exist in your
`.azure` directory:
- `azureProfile.json`: Contains your Azure profile information.
- `msal_token_cache.json`: Contains the cached authentication tokens.

### Service Principal Authentication

To authenticate using a service principal, set the following environment
variables:
- `AZURE_CLIENT_ID`
- `AZURE_CLIENT_SECRET`
- `AZURE_TENANT_ID`

### Managed Identity Authentication

When running in Azure, you can authenticate using managed identity. By default,
the system-assigned managed identity is used. To use a user-assigned managed
identity, set the `AZURE_CLIENT_ID` environment variable to the client ID of
the managed identity.

### Provision a VM

Create a VM, ignoring the error if it already exists.
Expand Down
2 changes: 2 additions & 0 deletions pkg/az/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ func (s *TypedStep) UnmarshalYAML(unmarshal func(interface{}) error) error {
continue
case "group":
cmd = &GroupCommand{}
case "login":
cmd = &LoginCommand{}
default: // It's a custom user command
customCmd := &UserCommand{}
b, err := yaml.Marshal(step)
Expand Down
83 changes: 83 additions & 0 deletions pkg/az/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package az

import (
"context"
"os"
"path/filepath"

"get.porter.sh/porter/pkg/exec/builder"
)

var (
_ TypedCommand = &LoginCommand{}
_ builder.HasErrorHandling = &LoginCommand{}
)

// LoginCommand handles logging into Azure
type LoginCommand struct {
action string
Description string `yaml:"description"`
}

func (c *LoginCommand) HandleError(ctx context.Context, err builder.ExitError, stdout string, stderr string) error {
// Handle specific login errors if necessary
return err
}

func (c *LoginCommand) GetWorkingDir() string {
return ""
}

func (c *LoginCommand) SetAction(action string) {
c.action = action
}

func (c *LoginCommand) GetCommand() string {
if c.azureDirExists() {
// Use a no-op command since we don't have to log in.
return "true"
}

return "az"
}

func (c *LoginCommand) GetArguments() []string {
if c.azureDirExists() {
return []string{}
}
return []string{"login"}
}

func (c *LoginCommand) GetFlags() builder.Flags {
flags := builder.Flags{}

if c.azureDirExists() {
return flags
}

if os.Getenv("AZURE_CLIENT_ID") != "" && os.Getenv("AZURE_CLIENT_SECRET") != "" && os.Getenv("AZURE_TENANT_ID") != "" {
// Add flags for service principal authentication
flags = append(flags, builder.NewFlag("service-principal", ""))
flags = append(flags, builder.NewFlag("username", os.Getenv("AZURE_CLIENT_ID")))
flags = append(flags, builder.NewFlag("password", os.Getenv("AZURE_CLIENT_SECRET")))
flags = append(flags, builder.NewFlag("tenant", os.Getenv("AZURE_TENANT_ID")))
} else if os.Getenv("AZURE_CLIENT_ID") != "" {
// Add flag for user-assigned managed identity
flags = append(flags, builder.NewFlag("identity", ""))
flags = append(flags, builder.NewFlag("username", os.Getenv("AZURE_CLIENT_ID")))
} else {
// Add flag for system-assigned managed identity
flags = append(flags, builder.NewFlag("identity", ""))
}

return flags
}

func (c *LoginCommand) SuppressesOutput() bool {
return false
}

func (c *LoginCommand) azureDirExists() bool {
_, err := os.Stat(filepath.Join(os.Getenv("HOME"), ".azure"))
return err == nil
}
125 changes: 125 additions & 0 deletions pkg/az/login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package az

import (
"os"
"path/filepath"
"testing"

"get.porter.sh/porter/pkg/exec/builder"
"github.com/stretchr/testify/assert"
)

func TestLoginCommand_GetArguments_ServicePrincipal(t *testing.T) {
tempHome := t.TempDir()
os.Setenv("HOME", tempHome)
os.Setenv("AZURE_CLIENT_ID", "test-client-id")
os.Setenv("AZURE_CLIENT_SECRET", "test-client-secret")
os.Setenv("AZURE_TENANT_ID", "test-tenant-id")
defer os.Unsetenv("AZURE_CLIENT_ID")
defer os.Unsetenv("AZURE_CLIENT_SECRET")
defer os.Unsetenv("AZURE_TENANT_ID")

cmd := &LoginCommand{}
args := cmd.GetArguments()

expectedArgs := []string{"login"}
assert.Equal(t, expectedArgs, args)
}

func TestLoginCommand_GetCommandAndGetArguments_ExistingAzureDirectory(t *testing.T) {
tempHome := t.TempDir()
os.Setenv("HOME", tempHome)
homeDir := os.Getenv("HOME")
if err := os.MkdirAll(filepath.Join(homeDir, ".azure"), 0755); err != nil {
t.Fatal("failed to create .azure directory:", err)
}
defer os.RemoveAll(filepath.Join(homeDir, ".azure"))

cmd := &LoginCommand{}
args := cmd.GetArguments()

expectedArgs := []string{}
assert.Equal(t, "true", cmd.GetCommand())
assert.Equal(t, expectedArgs, args)
}

func TestLoginCommand_GetArguments_ManagedIdentity(t *testing.T) {
tempHome := t.TempDir()
os.Setenv("HOME", tempHome)
os.Unsetenv("AZURE_CLIENT_ID")
os.Unsetenv("AZURE_CLIENT_SECRET")
os.Unsetenv("AZURE_TENANT_ID")

cmd := &LoginCommand{}
args := cmd.GetArguments()

expectedArgs := []string{"login"}
assert.Equal(t, expectedArgs, args)
}

func TestLoginCommand_GetFlags_ServicePrincipal(t *testing.T) {
tempHome := t.TempDir()
os.Setenv("HOME", tempHome)
os.Setenv("AZURE_CLIENT_ID", "test-client-id")
os.Setenv("AZURE_CLIENT_SECRET", "test-client-secret")
os.Setenv("AZURE_TENANT_ID", "test-tenant-id")
defer os.Unsetenv("AZURE_CLIENT_ID")
defer os.Unsetenv("AZURE_CLIENT_SECRET")
defer os.Unsetenv("AZURE_TENANT_ID")

cmd := &LoginCommand{}
flags := cmd.GetFlags()

expectedFlags := builder.Flags{
builder.NewFlag("service-principal", ""),
builder.NewFlag("username", "test-client-id"),
builder.NewFlag("password", "test-client-secret"),
builder.NewFlag("tenant", "test-tenant-id"),
}
assert.Equal(t, expectedFlags, flags)
}

func TestLoginCommand_GetFlags_UserAssignedManagedIdentity(t *testing.T) {
tempHome := t.TempDir()
os.Setenv("HOME", tempHome)
os.Setenv("AZURE_CLIENT_ID", "test-client-id")
defer os.Unsetenv("AZURE_CLIENT_ID")

cmd := &LoginCommand{}
flags := cmd.GetFlags()

expectedFlags := builder.Flags{
builder.NewFlag("identity", ""),
builder.NewFlag("username", "test-client-id"),
}
assert.Equal(t, expectedFlags, flags)
}

func TestLoginCommand_GetFlags_SystemManagedIdentity(t *testing.T) {
tempHome := t.TempDir()
os.Setenv("HOME", tempHome)

cmd := &LoginCommand{}
flags := cmd.GetFlags()

expectedFlags := builder.Flags{
builder.NewFlag("identity", ""),
}
assert.Equal(t, expectedFlags, flags)
}

func TestLoginCommand_GetFlags_ExistingAzureDirectory(t *testing.T) {
tempHome := t.TempDir()
os.Setenv("HOME", tempHome)
homeDir := os.Getenv("HOME")
if err := os.MkdirAll(filepath.Join(homeDir, ".azure"), 0755); err != nil {
t.Fatal("failed to create .azure directory:", err)
}
defer os.RemoveAll(filepath.Join(homeDir, ".azure"))

cmd := &LoginCommand{}
flags := cmd.GetFlags()

expectedFlags := builder.Flags{}
assert.Equal(t, expectedFlags, flags)
}
10 changes: 8 additions & 2 deletions pkg/az/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
}
},
"installBicep": {
"description": "Indicates if Bicep should be install",
"description": "Indicates if Bicep should be installed",
"type": "boolean"
}
},
Expand Down Expand Up @@ -174,7 +174,8 @@
},
"additionalProperties": false
},
"group": {"$ref": "#/definitions/group"}
"group": {"$ref": "#/definitions/group"},
"login": {"$ref": "#/definitions/login"}
},
"additionalProperties": false
},
Expand All @@ -192,6 +193,11 @@
}
},
"additionalProperties": false
},
"login": {
"description": "Login to Azure",
"type": "object",
"additionalProperties": false
}
},
"type": "object",
Expand Down

0 comments on commit 29f44ca

Please sign in to comment.