-
Notifications
You must be signed in to change notification settings - Fork 0
/
account.go
158 lines (143 loc) 路 5.31 KB
/
account.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package main
import (
"context"
"fmt"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/toricls/acos"
)
const ERR_AWS_ORGANIZATION_NOT_ENABLED = "This AWS account is not part of AWS Organizations organization. "
const ERR_INFUFFICIENT_IAM_PERMISSIONS = "Failed to perform \"%s\". Make sure you have sufficient IAM permissions. See https://github.com/toricls/acos#prerequisites for the details.\n"
type GetAccountsOption struct {
AccountIds []string
OuId string
}
// getAccounts returns a list of AWS accounts which the caller has access to.
func getAccounts(ctx context.Context, opt GetAccountsOption) (acos.Accounts, error) {
var availableAccnts acos.Accounts
var err error
if len(opt.AccountIds) > 0 {
fmt.Fprintln(os.Stderr, "Account IDs specified. Retrieving accounts information...")
availableAccnts, err = getAccountsByIds(ctx, opt.AccountIds)
if !acos.IsOrganizationEnabled(err) {
fmt.Fprint(os.Stderr, ERR_AWS_ORGANIZATION_NOT_ENABLED)
} else if !acos.HasPermissionToOrganizationsApi(err) {
fmt.Fprintf(os.Stderr, ERR_INFUFFICIENT_IAM_PERMISSIONS, "organizations:ListAccounts")
}
} else if len(opt.OuId) > 0 {
fmt.Fprintf(os.Stderr, "Retrieving AWS accounts under the OU '%s'...\n", opt.OuId)
availableAccnts, err = getAccountsByOu(ctx, opt.OuId)
if !acos.OuExists(err) {
// To avoid duplicated error messages, we override the AWS error by our own.
err = fmt.Errorf("error the OU \"%s\" doesn't exist", opt.OuId)
// Stop the process here and we don't fall back to using the "getCallerAccount" func
// because the specified OU ID is not just valid.
return nil, err
} else if !acos.IsOrganizationEnabled(err) {
fmt.Fprint(os.Stderr, ERR_AWS_ORGANIZATION_NOT_ENABLED)
} else if !acos.HasPermissionToOrganizationsApi(err) {
fmt.Fprintf(os.Stderr, ERR_INFUFFICIENT_IAM_PERMISSIONS, "organizations:ListAccountsForParent")
}
} else {
availableAccnts, err = getAccountsInOrg(ctx)
if !acos.IsOrganizationEnabled(err) {
fmt.Fprint(os.Stderr, ERR_AWS_ORGANIZATION_NOT_ENABLED)
} else if !acos.HasPermissionToOrganizationsApi(err) {
fmt.Fprintf(os.Stderr, ERR_INFUFFICIENT_IAM_PERMISSIONS, "organizations:ListAccounts")
}
}
if err != nil {
fmt.Fprintln(os.Stderr, "Falling back to using \"sts:GetCallerIdentity\" and \"iam:ListAccountAliases\" to obtain your AWS account information... ")
availableAccnts, err = getCallerAccount(ctx)
}
return availableAccnts, err
}
func getAccountsByIds(ctx context.Context, accountIds []string) (acos.Accounts, error) {
accounts, err := acos.ListAccounts(ctx)
if err != nil {
return nil, err
}
availableAccnts := make(acos.Accounts, len(accountIds))
for _, id := range accountIds {
if len(id) == 0 {
continue
}
if a, ok := accounts[id]; ok {
availableAccnts[id] = acos.Account{
Id: a.Id,
Name: a.Name,
}
} else {
fmt.Fprintf(os.Stderr, "Account ID '%s' is not found in your AWS organization\n", id)
}
}
return availableAccnts, nil
}
func getAccountsByOu(ctx context.Context, ouId string) (acos.Accounts, error) {
// Should regex the ouId before calling the API?
// Doc - https://docs.aws.amazon.com/organizations/latest/APIReference/API_ListAccountsForParent.html#organizations-ListAccountsForParent-request-ParentId
return acos.ListAccountsByOu(ctx, ouId)
}
func getAccountsInOrg(ctx context.Context) (acos.Accounts, error) {
return acos.ListAccounts(ctx)
}
// getCallerAccount returns the AWS account information of the caller.
//
// This function expects to be used when the caller is not part of AWS Organizations organization,
// or when the caller doesn't have IAM permissions to perform "organizations:ListAccounts".
func getCallerAccount(ctx context.Context) (acos.Accounts, error) {
accnt, err := acos.GetCallerAccount(ctx)
if err != nil {
return nil, err
}
acosAccounts := make(acos.Accounts)
acosAccounts[accnt[0]] = acos.Account{
Id: &accnt[0],
Name: &accnt[1],
}
return acosAccounts, nil
}
// promptAccountsSelection prompts the user to select AWS accounts to retrieve costs.
// It returns an error if the `accnts` arg doesn't contain any Account.
// If the `accnts` arg contains only one Account, it never prompts the user.
func promptAccountsSelection(accnts acos.Accounts) (acos.Accounts, error) {
if len(accnts) == 0 {
return nil, fmt.Errorf("error no accounts found")
} else if len(accnts) == 1 {
// No need to prompt the user to select accounts if there is only one account.
return accnts, nil
}
opts := make([]string, len(accnts))
accntIds := make([]string, len(accnts))
i := 0
for _, a := range accnts {
opts[i] = fmt.Sprintf("%s - %s", *a.Id, *a.Name)
accntIds[i] = *a.Id
i++
}
q := &survey.MultiSelect{
Message: "Select accounts:",
Options: opts,
PageSize: 10,
}
var selIdx []int
err := survey.AskOne(
q,
&selIdx,
survey.WithPageSize(10),
survey.WithKeepFilter(true), // Assuming people often use prefix/suffix to group related AWS account names like "myproduct-prod", "myproduct-dev".
survey.WithStdio(os.Stdin, os.Stderr, os.Stderr), // Use stderr for the prompt message to avoid messing up the JSON output.
)
if err != nil {
return nil, err
}
if len(selIdx) == 0 {
return nil, fmt.Errorf("error no accounts selected")
}
result := make(acos.Accounts)
for _, v := range selIdx {
accntId := accntIds[v]
result[accntId] = accnts[accntId]
}
return result, nil
}