diff --git a/.gitignore b/.gitignore index 2cc346b..3df9ef0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ bin .cnab /build/git_askpass.sh az +!pkg/az +!.gitignore diff --git a/pkg/az/action.go b/pkg/az/action.go index e108278..2065433 100644 --- a/pkg/az/action.go +++ b/pkg/az/action.go @@ -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) diff --git a/pkg/az/login.go b/pkg/az/login.go new file mode 100644 index 0000000..eda463a --- /dev/null +++ b/pkg/az/login.go @@ -0,0 +1,67 @@ +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 { + return "az" +} + +func (c *LoginCommand) GetArguments() []string { + if _, err := os.Stat(filepath.Join(os.Getenv("HOME"), ".azure")); err == nil { + return []string{} + } + return []string{"login"} +} + +func (c *LoginCommand) GetFlags() builder.Flags { + flags := builder.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("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("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 +} diff --git a/pkg/az/login_test.go b/pkg/az/login_test.go new file mode 100644 index 0000000..2ef1c8c --- /dev/null +++ b/pkg/az/login_test.go @@ -0,0 +1,104 @@ +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_GetArguments_ExistingAzureDirectory(t *testing.T) { + tempHome := t.TempDir() + os.Setenv("HOME", tempHome) + homeDir := os.Getenv("HOME") + os.MkdirAll(filepath.Join(homeDir, ".azure"), 0755) + defer os.RemoveAll(filepath.Join(homeDir, ".azure")) + + cmd := &LoginCommand{} + args := cmd.GetArguments() + + expectedArgs := []string{} + 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("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("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) +} diff --git a/pkg/az/schema/schema.json b/pkg/az/schema/schema.json index 04f6f3f..94ee7ad 100644 --- a/pkg/az/schema/schema.json +++ b/pkg/az/schema/schema.json @@ -31,7 +31,7 @@ } }, "installBicep": { - "description": "Indicates if Bicep should be install", + "description": "Indicates if Bicep should be installed", "type": "boolean" } }, @@ -174,7 +174,8 @@ }, "additionalProperties": false }, - "group": {"$ref": "#/definitions/group"} + "group": {"$ref": "#/definitions/group"}, + "login": {"$ref": "#/definitions/login"} }, "additionalProperties": false }, @@ -192,6 +193,11 @@ } }, "additionalProperties": false + }, + "login": { + "description": "Login to Azure", + "type": "object", + "additionalProperties": false } }, "type": "object",