Skip to content
This repository has been archived by the owner on Aug 9, 2023. It is now read-only.

Commit

Permalink
Added short prompt and ability to filter out "N/A(tenant level accoun…
Browse files Browse the repository at this point in the history
…t)" (#4)

* Added short prompt

* Improved code readability

* Added missing files

* Added different prompts for active and inactive

* Addendum to last commit

* Removed embedded json files

* Removed unused error return of buildPrompt

* Added missing templates

* Renamed promptJsonTemplate to promptTemplate

* Removed unused json tags

* Addendum to last commit

* Addendum to last commit

* Added tenant level account filter

* Fixed tenant name rendering bug

* Addendum to last commit

* Added missing files

* Introduced comparable named slice type, removed subscription slice type

* Removed uneccesary comments

* Improved getActiveSubscription output

* Subscription.Compare panic if other is not a subscription
  • Loading branch information
StiviiK authored Mar 6, 2023
1 parent 0b25492 commit 536249b
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 155 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
#
```bash
azctx is a CLI tool for managing azure cli subscriptions.
It is a helper for the azure cli and provides a simple interface for managing subscriptions.
Pass a subscription name to select a specific subscription.
Pass - to switch to the previous subscription.
It is a helper for the azure cli and provides a simple interface for managing subscriptions.
Pass a subscription name to select a specific subscription.
Pass - to switch to the previous subscription.

Usage:
azctx [- / NAME] [flags]
azctx [- / -- NAME] [flags]
azctx [command]

Available Commands:
Expand All @@ -29,10 +29,12 @@ Available Commands:
version Print the CLI version

Flags:
-c, --current Display the current active subscription
-h, --help help for azctx
-r, --refresh Re-Authenticate and refresh the subscriptions.
Deprecated. Please use azctx login instead.
-c, --current Display the current active subscription
--filter-tenant-level Filter tenant level accounts with no available subscriptions (default true)
-h, --help help for azctx
-r, --refresh Re-Authenticate and refresh the subscriptions.
Deprecated. Please use azctx login instead.
-s, --short Use a short prompt

Use "azctx [command] --help" for more information about a command.
```
Expand Down
3 changes: 3 additions & 0 deletions azurecli/azurecli.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func New(fs afero.Fs) (CLI, error) {
return CLI{}, err
}

// Map the tenant ids to the tenant names
cli.MapTenantIdsToNames()

return cli, nil
}

Expand Down
44 changes: 14 additions & 30 deletions azurecli/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ import (
"github.com/lithammer/fuzzysearch/fuzzy"
)

const TENANT_LEVEL_ACCOUNT_NAME = "N/A(tenant level account)"

var FilterTenantLevelAccount bool = true // Filter tenant level accounts with no available subscriptions, will be manipulated by the flag --filter-tenant-level in cmd/root.go#L47

// Subscriptions returns all subscriptions
func (cli CLI) Subscriptions() []Subscription {
return cli.profile.Subscriptions
func (cli CLI) Subscriptions() utils.ComparableNamedSlice[Subscription] {
filter := func(s Subscription) bool {
return !FilterTenantLevelAccount || !strings.EqualFold(s.Name, TENANT_LEVEL_ACCOUNT_NAME)
}
return cli.profile.Subscriptions.Filter(filter)
}

// SubscriptionNames returns the names of the given subscriptions
func (cli CLI) SubscriptionNames() []string {
return SubscriptionSlice(cli.profile.Subscriptions).SubscriptionNames()
return cli.Subscriptions().Names()
}

// SetSubscription sets the default subscription in the azure config file
Expand All @@ -33,7 +40,7 @@ func (cli *CLI) SetSubscription(subscription Subscription) error {
// ActiveSubscription returns the active subscription
func (cli CLI) ActiveSubscription() (Subscription, error) {
// select the subscription with the is default flag set to true
for _, subscription := range cli.profile.Subscriptions {
for _, subscription := range cli.profile.Subscriptions { // Do not use cli.Subscriptions() here, because we want to return the subscription even if it is a tenant level account
if subscription.IsDefault {
return subscription, nil
}
Expand All @@ -45,7 +52,7 @@ func (cli CLI) ActiveSubscription() (Subscription, error) {
// GetSubscriptionByName returns the azure subscription with the given name
func (cli CLI) GetSubscriptionByName(subscriptionName string) (Subscription, bool) {
// Find the subscription with the given name
for _, subscription := range cli.profile.Subscriptions {
for _, subscription := range cli.Subscriptions() {
if strings.EqualFold(subscription.Name, subscriptionName) {
return subscription, true
}
Expand All @@ -55,7 +62,7 @@ func (cli CLI) GetSubscriptionByName(subscriptionName string) (Subscription, boo
}

// TryFindSubscription fuzzy searches for the azure subscription in the given AzureProfilesConfig
func (cli CLI) TryFindSubscription(subscriptionName string) ([]Subscription, error) {
func (cli CLI) TryFindSubscription(subscriptionName string) (utils.ComparableNamedSlice[Subscription], error) {
// Fuzzy search for the subscription name
subscriptionNames := utils.StringSlice(cli.SubscriptionNames())
results := fuzzy.FindNormalized(strings.ToLower(subscriptionName), subscriptionNames.ToLower())
Expand All @@ -75,34 +82,11 @@ func (cli CLI) TryFindSubscription(subscriptionName string) ([]Subscription, err
// Multiple results found
subscriptions := make([]Subscription, 0)
for _, result := range results {
s, ok := cli.GetSubscriptionByName(result)
if ok {
if s, ok := cli.GetSubscriptionByName(result); ok {
subscriptions = append(subscriptions, s)
}
}

return subscriptions, nil
}
}

func (a SubscriptionSlice) Len() int { return len(a) }
func (a SubscriptionSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a SubscriptionSlice) Less(i, j int) bool {
subA, subB := a[i], a[j]

if subA.Tenant == subB.Tenant {
return subA.Name < subB.Name
}

return subA.Tenant > subB.Tenant
}

// SubscriptionNames returns the names of the given subscriptions
func (subscriptionSlice SubscriptionSlice) SubscriptionNames() []string {
var subscriptionNames []string
for _, subscription := range subscriptionSlice {
subscriptionNames = append(subscriptionNames, subscription.Name)
}

return subscriptionNames
}
7 changes: 5 additions & 2 deletions azurecli/tenants.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,13 @@ func (cli *CLI) MapTenantIdsToNames() {
}

// Rewrite the tenant ids to the tenant names
for i, subscription := range cli.profile.Subscriptions {
for i, subscription := range cli.profile.Subscriptions { // Do not use cli.Subscriptions() here, because we want to map the tenant name even if it is a tenant level account
if tenantName, ok := tenantMap[subscription.Tenant]; ok {
cli.profile.Subscriptions[i].Tenant = fmt.Sprintf("%s (%s)", tenantName, subscription.Tenant)
cli.profile.Subscriptions[i].TenantName = tenantName
continue
}

cli.profile.Subscriptions[i].TenantName = subscription.Tenant
}
} else {
log.Info("If you want to fetch the tenant names, please authenticate the azure cli again using the wraper command: `azctx login`.")
Expand Down
34 changes: 29 additions & 5 deletions azurecli/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package azurecli

import "github.com/spf13/afero"
import (
"strings"

"github.com/StiviiK/azctx/utils"
"github.com/spf13/afero"
)

// CLI represents the azure cli
type CLI struct {
Expand All @@ -11,8 +16,8 @@ type CLI struct {

// Profile represents the AzureProfiles.json file
type Profile struct {
Subscriptions []Subscription `json:"subscriptions"`
InstallationId string `json:"installationId"`
Subscriptions utils.ComparableNamedSlice[Subscription] `json:"subscriptions"`
InstallationId string `json:"installationId"`
}

// Tenant represents the response of the azure management api
Expand All @@ -32,12 +37,31 @@ type Subscription struct {
} `json:"user"`
IsDefault bool `json:"isDefault"`
Tenant string `json:"tenantId"`
TenantName string `` // This is a duplicate of Tenant, used only for display purposes
Environment string `json:"environmentName"`
HomeTenantId string `json:"homeTenantId"`
ManagedBy []struct {
Id string `json:"tenantId"`
} `json:"managedByTenants"`
}

// SubscriptionSlice is a custom sorter for subscriptions
type SubscriptionSlice []Subscription
// Implement utils.Compartable interface
func (s Subscription) Compare(other utils.Comparable) int {
// Check if other is a subscription
otherSubscription, ok := other.(Subscription)
if !ok {
panic("other is not a Subscription")
}

// Sort subscriptions by tenant name and then by subscription name
if strings.EqualFold(s.TenantName, otherSubscription.TenantName) {
return strings.Compare(s.Name, otherSubscription.Name)
}

return strings.Compare(s.TenantName, otherSubscription.TenantName)
}

// Implement utils.Named interface
func (s Subscription) Named() string {
return s.Name
}
24 changes: 13 additions & 11 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ const (
)

var rootCmd = &cobra.Command{
Use: "azctx [- / NAME]",
Use: "azctx [- / -- NAME]",
Short: "azctx is a CLI tool for managing azure cli subscriptions",
Long: `azctx is a CLI tool for managing azure cli subscriptions.
It is a helper for the azure cli and provides a simple interface for managing subscriptions.
Pass a subscription name to select a specific subscription.
Pass - to switch to the previous subscription.`,
SilenceUsage: true,
Run: utils.WrapCobraCommandHandler(rootRunE),
ValidArgs: []string{"-", "NAME"},
}

func init() {
Expand All @@ -44,6 +43,8 @@ func init() {
rootCmd.Flags().BoolP("current", "c", false, "Display the current active subscription")
rootCmd.Flags().BoolP("refresh", "r", false, `Re-Authenticate and refresh the subscriptions.
Deprecated. Please use azctx login instead.`)
rootCmd.Flags().BoolVarP(&prompt.ShortPrompt, "short", "s", false, "Use a short prompt")
rootCmd.Flags().BoolVar(&azurecli.FilterTenantLevelAccount, "filter-tenant-level", true, "Filter tenant level accounts with no available subscriptions")
}

func Execute() {
Expand All @@ -59,9 +60,6 @@ func rootRunE(cmd *cobra.Command, args []string) error {
return err
}

// Try to map the tenant ids to names
cli.MapTenantIdsToNames()

// check the flags
switch {
case cmd.Flags().Changed("current"):
Expand Down Expand Up @@ -91,16 +89,19 @@ func rootRunE(cmd *cobra.Command, args []string) error {

func interactivelySelectSubscription(cli azurecli.CLI) error {
// Ask the user to select a subscription
prompt := prompt.BuildPrompt(cli.Subscriptions())
subscriptions := cli.Subscriptions()
prompt := prompt.BuildPrompt(subscriptions)

// Run the prompt
idx, _, err := prompt.Run()
if err != nil {
return nil
}

// Set the selected subscription as the default
subscriptions := cli.Subscriptions()
log.Info("Setting active subscription to %s (%s)", subscriptions[idx].Name, subscriptions[idx].Id)
err = cli.SetSubscription(subscriptions[idx])
subscription := subscriptions[idx]
log.Info("Setting active subscription to %s/%s (%s/%s)", subscription.TenantName, subscription.Name, subscription.Tenant, subscription.Id)
err = cli.SetSubscription(subscription)
if err != nil {
return err
}
Expand All @@ -118,6 +119,7 @@ func selectSubscriptionByName(cli azurecli.CLI, name string) error {
var subscription azurecli.Subscription
switch length := len(subscriptions); {
case length > 1:
// Run the prompt
prompt := prompt.BuildPrompt(subscriptions)
idx, _, err := prompt.Run()
if err != nil {
Expand All @@ -132,7 +134,7 @@ func selectSubscriptionByName(cli azurecli.CLI, name string) error {
}

// Set the selected subscription as the default
log.Info("Setting active subscription to %s (%s)", subscription.Name, subscription.Id)
log.Info("Setting active subscription to %s/%s (%s/%s)", subscription.TenantName, subscription.Name, subscription.Tenant, subscription.Id)
err = cli.SetSubscription(subscription)
if err != nil {
return err
Expand All @@ -149,7 +151,7 @@ func getActiveSubscription(cli azurecli.CLI) error {
}

// Print the active subscription
log.Info("Active subscription: %s (%s)", subscription.Name, subscription.Id)
log.Info("Active subscription: %s/%s (%s/%s)", subscription.TenantName, subscription.Name, subscription.Tenant, subscription.Id)

return nil
}
56 changes: 28 additions & 28 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,45 @@ module github.com/StiviiK/azctx
go 1.18

require (
github.com/Masterminds/sprig/v3 v3.2.3
github.com/lithammer/fuzzysearch v1.1.5
github.com/manifoldco/promptui v0.9.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/afero v1.8.2
github.com/spf13/cobra v1.5.0
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
github.com/sirupsen/logrus v1.9.0
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.6.1
go.szostok.io/version v1.1.0
golang.org/x/exp v0.0.0-20230304125523-9ff063c70017
)

require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/goccy/go-yaml v1.9.5 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/muesli/termenv v0.12.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/muesli/termenv v0.13.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.szostok.io/version v1.0.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)

require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2
github.com/chzyer/readline v1.5.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/sys v0.0.0-20220702020025-31831981b65f // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 536249b

Please sign in to comment.