Skip to content

Commit 081ebcc

Browse files
authored
Merge pull request #5 from silinternational/feature/dns-for-lambdas
get Lambda DNS values from Terraform Cloud
2 parents 0bbd054 + e43f39d commit 081ebcc

File tree

3 files changed

+211
-62
lines changed

3 files changed

+211
-62
lines changed

cmd/cli/multiregion/dns.go

+157-50
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,23 @@ import (
1616
)
1717

1818
type DnsCommand struct {
19-
cfClient *cloudflare.API
20-
cfZone *cloudflare.ResourceContainer
21-
domainName string
22-
tfcOrg string
23-
tfcToken string
24-
testMode bool
19+
cfClient *cloudflare.API
20+
cfZone *cloudflare.ResourceContainer
21+
domainName string
22+
failback bool
23+
tfcOrg string
24+
tfcOrgAlt string
25+
tfcToken string
26+
tfcTokenAlt string
27+
testMode bool
2528
}
2629

27-
type AlbDnsValues struct {
28-
internal string
29-
external string
30+
type DnsValues struct {
31+
albInternal string
32+
albExternal string
33+
bot string
34+
mfa string
35+
twosv string
3036
}
3137

3238
func InitDnsCmd(parentCmd *cobra.Command) {
@@ -50,24 +56,16 @@ func InitDnsCmd(parentCmd *cobra.Command) {
5056
func runDnsCommand(failback bool) {
5157
pFlags := getPersistentFlags()
5258

53-
d := newDnsCommand(pFlags)
59+
d := newDnsCommand(pFlags, failback)
5460

55-
var clusterWorkspaceName string
56-
if failback {
57-
clusterWorkspaceName = clusterWorkspace(pFlags)
58-
} else {
59-
clusterWorkspaceName = clusterSecondaryWorkspace(pFlags)
60-
}
61-
62-
dnsValues := d.getAlbDnsValuesFromTfc(clusterWorkspaceName)
63-
64-
d.setDnsRecordValues(pFlags.idp, dnsValues, failback)
61+
values := d.getDnsValuesFromTfc(pFlags)
62+
d.setDnsRecordValues(pFlags.idp, values)
6563
}
6664

67-
func newDnsCommand(pFlags PersistentFlags) *DnsCommand {
65+
func newDnsCommand(pFlags PersistentFlags, failback bool) *DnsCommand {
6866
d := DnsCommand{
6967
testMode: pFlags.readOnlyMode,
70-
domainName: viper.GetString("domain-name"),
68+
domainName: viper.GetString(flagDomainName),
7169
}
7270

7371
if d.domainName == "" {
@@ -93,46 +91,56 @@ func newDnsCommand(pFlags PersistentFlags) *DnsCommand {
9391
d.cfZone = cloudflare.ZoneIdentifier(zoneID)
9492

9593
d.tfcToken = pFlags.tfcToken
94+
d.tfcTokenAlt = pFlags.tfcTokenAlt
9695
d.tfcOrg = pFlags.org
96+
d.tfcOrgAlt = pFlags.orgAlt
97+
d.failback = failback
9798

9899
return &d
99100
}
100101

101-
func (d *DnsCommand) setDnsRecordValues(idpKey string, dnsValues AlbDnsValues, failback bool) {
102-
if failback {
102+
func (d *DnsCommand) setDnsRecordValues(idpKey string, dnsValues DnsValues) {
103+
if d.failback {
103104
fmt.Println("Setting DNS records to primary region...")
104105
} else {
105106
fmt.Println("Setting DNS records to secondary region...")
106107
}
107108

108109
dnsRecords := []struct {
109-
name string
110-
optionValue string
111-
defaultValue string
110+
name string
111+
valueFlag string
112+
tfcValue string
112113
}{
113114
// "mfa-api" is the TOTP API, also known as serverless-mfa-api
114-
{"mfa-api", "mfa-api-value", ""},
115+
{"mfa-api", "mfa-api-value", dnsValues.mfa},
115116

116117
// "twosv-api" is the Webauthn API, also known as serverless-mfa-api-go
117-
{"twosv-api", "twosv-api-value", ""},
118+
{"twosv-api", "twosv-api-value", dnsValues.twosv},
118119

119120
// "support-bot" is the idp-support-bot API that is configured in the Slack API dashboard
120-
{"sherlock", "support-bot-value", ""},
121+
{"sherlock", "support-bot-value", dnsValues.bot},
121122

122123
// ECS services
123-
{idpKey + "-email", "email-service-value", dnsValues.internal},
124-
{idpKey + "-broker", "id-broker-value", dnsValues.internal},
125-
{idpKey + "-pw-api", "pw-api-value", dnsValues.external},
126-
{idpKey, "ssp-value", dnsValues.external},
127-
{idpKey + "-sync", "id-sync-value", dnsValues.external},
124+
{idpKey + "-email", "email-service-value", dnsValues.albInternal},
125+
{idpKey + "-broker", "id-broker-value", dnsValues.albInternal},
126+
{idpKey + "-pw-api", "pw-api-value", dnsValues.albExternal},
127+
{idpKey, "ssp-value", dnsValues.albExternal},
128+
{idpKey + "-sync", "id-sync-value", dnsValues.albExternal},
128129
}
129130

130131
for _, record := range dnsRecords {
131-
value := getOption(record.optionValue, record.defaultValue)
132+
value := getDnsValue(record.valueFlag, record.tfcValue)
132133
d.setCloudflareCname(record.name, value)
133134
}
134135
}
135136

137+
func getDnsValue(valueFlag, tfcValue string) string {
138+
if tfcValue != "" {
139+
return tfcValue
140+
}
141+
return viper.GetString(valueFlag)
142+
}
143+
136144
func (d *DnsCommand) setCloudflareCname(name, value string) {
137145
if value == "" {
138146
fmt.Printf(" skipping %s (no value provided)\n", name)
@@ -177,27 +185,48 @@ func (d *DnsCommand) setCloudflareCname(name, value string) {
177185
}
178186
}
179187

180-
func (d *DnsCommand) getAlbDnsValuesFromTfc(workspaceName string) (values AlbDnsValues) {
181-
config := &tfe.Config{
182-
Token: d.tfcToken,
183-
RetryServerErrors: true,
188+
func (d *DnsCommand) getDnsValuesFromTfc(pFlags PersistentFlags) (values DnsValues) {
189+
ctx := context.Background()
190+
191+
var clusterWorkspaceName string
192+
if d.failback {
193+
clusterWorkspaceName = clusterWorkspace(pFlags)
194+
} else {
195+
clusterWorkspaceName = clusterSecondaryWorkspace(pFlags)
184196
}
185197

186-
client, err := tfe.NewClient(config)
187-
if err != nil {
188-
fmt.Printf("Error creating Terraform client: %s", err)
189-
return
198+
internal, external := d.getAlbValuesFromTfc(ctx, clusterWorkspaceName)
199+
values.albInternal = internal
200+
values.albExternal = external
201+
202+
bot := "idp-support-bot-prod"
203+
if pFlags.env != envProd {
204+
bot = "idp-support-bot-dev" // TODO: consider renaming the workspace name so this can be simplified
190205
}
206+
values.bot = d.getLambdaDnsValueFromTfc(ctx, bot)
191207

192-
ctx := context.Background()
208+
twosv := "serverless-mfa-api-go-prod"
209+
if pFlags.env != envProd {
210+
twosv = "serverless-mfa-api-go-dev" // TODO: consider renaming the workspace name so this can be simplified
211+
}
212+
values.twosv = d.getLambdaDnsValueFromTfc(ctx, twosv)
193213

194-
w, err := client.Workspaces.Read(ctx, d.tfcOrg, workspaceName)
214+
mfa := "serverless-mfa-api-prod"
215+
if pFlags.env != envProd {
216+
mfa = "serverless-mfa-api-dev" // TODO: consider renaming the workspace name so this can be simplified
217+
}
218+
values.mfa = d.getLambdaDnsValueFromTfc(ctx, mfa)
219+
return
220+
}
221+
222+
func (d *DnsCommand) getAlbValuesFromTfc(ctx context.Context, workspaceName string) (internal, external string) {
223+
workspaceID, client, err := d.findTfcWorkspace(ctx, workspaceName)
195224
if err != nil {
196-
fmt.Printf("Error reading Terraform workspace %s: %s", workspaceName, err)
225+
fmt.Printf("Failed to get ALB DNS values: %s\n Will use DNS config values if provided.\n", err)
197226
return
198227
}
199228

200-
outputs, err := client.StateVersionOutputs.ReadCurrent(ctx, w.ID)
229+
outputs, err := client.StateVersionOutputs.ReadCurrent(ctx, workspaceID)
201230
if err != nil {
202231
fmt.Printf("Error reading Terraform state outputs on workspace %s: %s", workspaceName, err)
203232
return
@@ -207,10 +236,88 @@ func (d *DnsCommand) getAlbDnsValuesFromTfc(workspaceName string) (values AlbDns
207236
itemValue, _ := item.Value.(string)
208237
switch item.Name {
209238
case "alb_dns_name":
210-
values.external = itemValue
239+
external = itemValue
211240
case "internal_alb_dns_name":
212-
values.internal = itemValue
241+
internal = itemValue
213242
}
214243
}
215244
return
216245
}
246+
247+
func (d *DnsCommand) getLambdaDnsValueFromTfc(ctx context.Context, workspaceName string) string {
248+
outputName := "secondary_region_domain_name"
249+
if d.failback {
250+
outputName = "primary_region_domain_name"
251+
}
252+
return d.getTfcOutputFromWorkspace(ctx, workspaceName, outputName)
253+
}
254+
255+
func (d *DnsCommand) getTfcOutputFromWorkspace(ctx context.Context, workspaceName, outputName string) string {
256+
workspaceID, client, err := d.findTfcWorkspace(ctx, workspaceName)
257+
if err != nil {
258+
fmt.Printf("Failed to get DNS value from %s: %s\n Will use config value if provided.\n", workspaceName, err)
259+
return ""
260+
}
261+
262+
outputs, err := client.StateVersionOutputs.ReadCurrent(ctx, workspaceID)
263+
if err != nil {
264+
fmt.Printf("Error reading Terraform state outputs on workspace %s: %s", workspaceName, err)
265+
return ""
266+
}
267+
268+
for _, item := range outputs.Items {
269+
if item.Name == outputName {
270+
if itemValue, ok := item.Value.(string); ok {
271+
return itemValue
272+
}
273+
break
274+
}
275+
}
276+
277+
fmt.Printf("Value for %s not found in %s\n", outputName, workspaceName)
278+
return ""
279+
}
280+
281+
// findTfcWorkspace looks for a workspace by name in two different Terraform Cloud accounts and returns
282+
// the workspace ID and an API client for the account where the workspace was found
283+
func (d *DnsCommand) findTfcWorkspace(ctx context.Context, workspaceName string) (id string, client *tfe.Client, err error) {
284+
config := &tfe.Config{
285+
Token: d.tfcToken,
286+
RetryServerErrors: true,
287+
}
288+
289+
client, err = tfe.NewClient(config)
290+
if err != nil {
291+
err = fmt.Errorf("error creating Terraform client: %s", err)
292+
return
293+
}
294+
295+
w, err := client.Workspaces.Read(ctx, d.tfcOrg, workspaceName)
296+
if err == nil {
297+
id = w.ID
298+
return
299+
}
300+
301+
if d.tfcTokenAlt == "" {
302+
err = fmt.Errorf("error reading Terraform workspace %s: %s", workspaceName, err)
303+
return
304+
}
305+
306+
fmt.Printf("Workspace %s not found using %s, trying %s\n", workspaceName, flagTfcToken, flagTfcTokenAlternate)
307+
308+
config.Token = d.tfcTokenAlt
309+
client, err = tfe.NewClient(config)
310+
if err != nil {
311+
err = fmt.Errorf("error creating alternate Terraform client: %s", err)
312+
return
313+
}
314+
315+
w, err = client.Workspaces.Read(ctx, d.tfcOrgAlt, workspaceName)
316+
if err != nil {
317+
err = fmt.Errorf("error reading Terraform workspace %s using %s: %s", workspaceName, flagTfcTokenAlternate, err)
318+
return
319+
}
320+
321+
id = w.ID
322+
return
323+
}

cmd/cli/multiregion/multiregion.go

+50-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import (
1212
"github.com/spf13/viper"
1313
)
1414

15+
const (
16+
flagDomainName = "domain-name"
17+
flagEnv = "env"
18+
flagRegion2 = "region2"
19+
flagTfcToken = "tfc-token"
20+
flagOrgAlternate = "org-alternate"
21+
flagTfcTokenAlternate = "tfc-token-alternate"
22+
)
23+
24+
const envProd = "prod"
25+
1526
func SetupMultiregionCmd(parentCommand *cobra.Command) {
1627
multiregionCmd := &cobra.Command{
1728
Use: "multiregion",
@@ -26,26 +37,38 @@ func SetupMultiregionCmd(parentCommand *cobra.Command) {
2637
InitStatusCmd(multiregionCmd)
2738

2839
var domainName string
29-
multiregionCmd.PersistentFlags().StringVar(&domainName, "domain-name", "", "Domain name")
30-
if err := viper.BindPFlag("domain-name", multiregionCmd.PersistentFlags().Lookup("domain-name")); err != nil {
40+
multiregionCmd.PersistentFlags().StringVar(&domainName, flagDomainName, "", "Domain name")
41+
if err := viper.BindPFlag(flagDomainName, multiregionCmd.PersistentFlags().Lookup(flagDomainName)); err != nil {
3142
outputFlagError(multiregionCmd, err)
3243
}
3344

3445
var env string
35-
multiregionCmd.PersistentFlags().StringVar(&env, "env", "prod", "Execution environment (default: prod)")
36-
if err := viper.BindPFlag("env", multiregionCmd.PersistentFlags().Lookup("env")); err != nil {
46+
multiregionCmd.PersistentFlags().StringVar(&env, flagEnv, envProd, "Execution environment")
47+
if err := viper.BindPFlag(flagEnv, multiregionCmd.PersistentFlags().Lookup(flagEnv)); err != nil {
3748
outputFlagError(multiregionCmd, err)
3849
}
3950

4051
var region2 string
41-
multiregionCmd.PersistentFlags().StringVar(&region2, "region2", "", "Secondary AWS region")
42-
if err := viper.BindPFlag("region2", multiregionCmd.PersistentFlags().Lookup("region2")); err != nil {
52+
multiregionCmd.PersistentFlags().StringVar(&region2, flagRegion2, "", "Secondary AWS region")
53+
if err := viper.BindPFlag(flagRegion2, multiregionCmd.PersistentFlags().Lookup(flagRegion2)); err != nil {
4354
outputFlagError(multiregionCmd, err)
4455
}
4556

4657
var tfcToken string
47-
multiregionCmd.PersistentFlags().StringVar(&tfcToken, "tfc-token", "", "Token for Terraform Cloud authentication")
48-
if err := viper.BindPFlag("tfc-token", multiregionCmd.PersistentFlags().Lookup("tfc-token")); err != nil {
58+
multiregionCmd.PersistentFlags().StringVar(&tfcToken, flagTfcToken, "", "Token for Terraform Cloud authentication")
59+
if err := viper.BindPFlag(flagTfcToken, multiregionCmd.PersistentFlags().Lookup(flagTfcToken)); err != nil {
60+
outputFlagError(multiregionCmd, err)
61+
}
62+
63+
var orgAlt string
64+
multiregionCmd.PersistentFlags().StringVar(&orgAlt, flagOrgAlternate, "", "Alternate Terraform Cloud organization")
65+
if err := viper.BindPFlag(flagOrgAlternate, multiregionCmd.PersistentFlags().Lookup(flagOrgAlternate)); err != nil {
66+
outputFlagError(multiregionCmd, err)
67+
}
68+
69+
var tfcTokenAlt string
70+
multiregionCmd.PersistentFlags().StringVar(&tfcTokenAlt, flagTfcTokenAlternate, "", "Alternate token for Terraform Cloud")
71+
if err := viper.BindPFlag(flagTfcTokenAlternate, multiregionCmd.PersistentFlags().Lookup(flagTfcTokenAlternate)); err != nil {
4972
outputFlagError(multiregionCmd, err)
5073
}
5174
}
@@ -59,20 +82,35 @@ type PersistentFlags struct {
5982
env string
6083
idp string
6184
org string
85+
orgAlt string
6286
readOnlyMode bool
6387
secondaryRegion string
6488
tfcToken string
89+
tfcTokenAlt string
6590
}
6691

6792
func getPersistentFlags() PersistentFlags {
68-
return PersistentFlags{
69-
env: getRequiredParam("env"),
93+
pFlags := PersistentFlags{
94+
env: getRequiredParam(flagEnv),
7095
idp: getRequiredParam("idp"),
7196
org: getRequiredParam("org"),
72-
tfcToken: getRequiredParam("tfc-token"),
73-
secondaryRegion: getRequiredParam("region2"),
97+
tfcToken: getRequiredParam(flagTfcToken),
98+
secondaryRegion: getRequiredParam(flagRegion2),
7499
readOnlyMode: viper.GetBool("read-only-mode"),
100+
tfcTokenAlt: getOption(flagTfcTokenAlternate, ""),
101+
orgAlt: getOption(flagOrgAlternate, viper.GetString("org")),
102+
}
103+
104+
if pFlags.orgAlt != "" && pFlags.tfcTokenAlt == "" {
105+
log.Fatalf("%[1]s was specified without %[2]s. Please include %[2]s or remove %[1]s.",
106+
flagOrgAlternate, flagTfcTokenAlternate)
75107
}
108+
109+
if pFlags.orgAlt == "" {
110+
pFlags.orgAlt = pFlags.org
111+
}
112+
113+
return pFlags
76114
}
77115

78116
func getRequiredParam(key string) string {

0 commit comments

Comments
 (0)