Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add login command #74

Merged
merged 7 commits into from
Jan 24, 2025
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
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
Loading