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

Update to work with Terraform 0.12 #15

Merged
merged 1 commit into from
Jun 25, 2019
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
73 changes: 61 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const (
recordTemplateStr = `{{- range .Record.Comments }}
# {{ . }}{{ end }}
resource "aws_route53_record" "{{ .ResourceID }}" {
zone_id = "${aws_route53_zone.{{ .ZoneID }}.zone_id}"
zone_id = {{ zoneReference .ZoneID }}
name = "{{ .Record.Name }}"
type = "{{ .Record.Type }}"
ttl = "{{ .Record.TTL }}"
Expand All @@ -40,11 +40,41 @@ resource "aws_route53_record" "{{ .ResourceID }}" {
`
)

var (
zoneTemplate = template.Must(template.New("zone").Parse(zoneTemplateStr))
recordTemplate = template.Must(template.New("record").Funcs(template.FuncMap{"ensureQuoted": ensureQuoted}).Parse(recordTemplateStr))
type syntaxMode uint8

func (m syntaxMode) String() string {
switch m {
case Modern:
return "modern"
case Legacy:
return "legacy"
default:
panic("Unknown syntax")
}
}

const (
Modern syntaxMode = iota
Legacy
)

type configGenerator struct {
zoneTemplate *template.Template
recordTemplate *template.Template

syntax syntaxMode
}

func newConfigGenerator(syntax syntaxMode) *configGenerator {
g := &configGenerator{syntax: syntax}
g.zoneTemplate = template.Must(template.New("zone").Parse(zoneTemplateStr))
g.recordTemplate = template.Must(template.New("record").Funcs(template.FuncMap{
"ensureQuoted": ensureQuoted,
"zoneReference": g.zoneReference,
}).Parse(recordTemplateStr))
return g
}

type zoneTemplateData struct {
ID string
Domain string
Expand Down Expand Up @@ -87,6 +117,7 @@ var (
domain = flag.String("domain", "", "Name of domain")
zoneFile = flag.String("zone-file", "", "Path to zone file. Defaults to <domain>.zone in working dir")
showVersion = flag.Bool("version", false, "Show version")
legacySyntax = flag.Bool("legacy-syntax", false, "Generate legacy terraform syntax (versions older than 0.12)")
)

func main() {
Expand All @@ -110,13 +141,20 @@ func main() {
log.Fatal(err)
}

generateTerraformForZone(*domain, excludedTypes, fileReader, os.Stdout)
var syntax syntaxMode
if !*legacySyntax {
syntax = Modern
} else {
syntax = Legacy
}
g := newConfigGenerator(syntax)
g.generateTerraformForZone(*domain, excludedTypes, fileReader, os.Stdout)
}

func generateTerraformForZone(domain string, excludedTypes map[uint16]bool, zoneReader io.Reader, output io.Writer) {
func (g *configGenerator) generateTerraformForZone(domain string, excludedTypes map[uint16]bool, zoneReader io.Reader, output io.Writer) {
records := readZoneRecords(zoneReader, excludedTypes)

zoneID, err := generateZoneResource(domain, output)
zoneID, err := g.generateZoneResource(domain, output)
if err != nil {
log.Fatal(err)
}
Expand All @@ -129,7 +167,7 @@ func generateTerraformForZone(domain string, excludedTypes map[uint16]bool, zone

for _, key := range recordKeys {
rec := records[key]
err := generateRecordResource(rec, zoneID, output)
err := g.generateRecordResource(rec, zoneID, output)
if err != nil {
log.Printf("Error: %v\n", err)
continue
Expand Down Expand Up @@ -163,18 +201,18 @@ func readZoneRecords(zoneReader io.Reader, excludedTypes map[uint16]bool) map[re
return records
}

func generateZoneResource(domain string, w io.Writer) (string, error) {
func (g *configGenerator) generateZoneResource(domain string, w io.Writer) (string, error) {
zoneName := strings.TrimRight(domain, ".")
data := zoneTemplateData{
ID: strings.Replace(zoneName, ".", "-", -1),
Domain: zoneName,
}

err := zoneTemplate.Execute(w, data)
err := g.zoneTemplate.Execute(w, data)
return data.ID, err
}

func generateRecordResource(record dnsRecord, zoneID string, w io.Writer) error {
func (g *configGenerator) generateRecordResource(record dnsRecord, zoneID string, w io.Writer) error {
sanitizedName := sanitizeRecordName(record.Name)
id := fmt.Sprintf("%s-%s", sanitizedName, record.Type)

Expand All @@ -184,7 +222,7 @@ func generateRecordResource(record dnsRecord, zoneID string, w io.Writer) error
ZoneID: zoneID,
}

return recordTemplate.Execute(w, data)
return g.recordTemplate.Execute(w, data)
}

func mergeRecords(a, b dnsRecord) dnsRecord {
Expand Down Expand Up @@ -267,3 +305,14 @@ func ensureQuoted(s string) string {
}
return fmt.Sprintf("%q", s)
}

func (g *configGenerator) zoneReference(zone string) string {
switch g.syntax {
case Modern:
return fmt.Sprintf("aws_route53_zone.%s.zone_id", zone)
case Legacy:
return fmt.Sprintf(`"${aws_route53_zone.%s.zone_id}"`, zone)
default:
panic(fmt.Sprintf("Unknown mode %v", g.syntax))
}
}
96 changes: 66 additions & 30 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -11,10 +12,16 @@ import (
"github.com/google/go-cmp/cmp"
)

var diffOpts = cmp.Options{
cmp.Transformer("ignoreSurroundingWhitespace", func(in string) string {
return strings.TrimSpace(in)
}),
var (
diffOpts = cmp.Options{
cmp.Transformer("ignoreSurroundingWhitespace", func(in string) string {
return strings.TrimSpace(in)
}),
}
)

func caseName(name string, syntax syntaxMode) string {
return fmt.Sprintf("%s-%v", name, syntax)
}

func TestGenerateRecordResource(t *testing.T) {
Expand All @@ -25,23 +32,49 @@ func TestGenerateRecordResource(t *testing.T) {
TTL: 3600,
Comments: []string{"This is a test"},
}
expected := `# This is a test

cases := []struct {
name string
expected map[syntaxMode]string
}{
{
name: "basic",
expected: map[syntaxMode]string{
Modern: `# This is a test
resource "aws_route53_record" "foo-bar-A" {
zone_id = aws_route53_zone.test-zone.zone_id
name = "foo.bar"
type = "A"
ttl = "3600"
records = ["127.0.0.1"]
}`,
Legacy: `# This is a test
resource "aws_route53_record" "foo-bar-A" {
zone_id = "${aws_route53_zone.test-zone.zone_id}"
name = "foo.bar"
type = "A"
ttl = "3600"
records = ["127.0.0.1"]
}`

var buf bytes.Buffer
err := generateRecordResource(record, "test-zone", &buf)
if err != nil {
t.Fatal(err)
}`,
},
},
}
for _, tc := range cases {
for _, legacySyntax := range []syntaxMode{Modern, Legacy} {
t.Run(caseName(tc.name, legacySyntax), func(t *testing.T) {
g := newConfigGenerator(legacySyntax)

var buf bytes.Buffer
err := g.generateRecordResource(record, "test-zone", &buf)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(expected, buf.String(), diffOpts); diff != "" {
t.Errorf("Unexpected result from resource generation (-want +got):\n%s", diff)
if diff := cmp.Diff(tc.expected[legacySyntax], buf.String(), diffOpts); diff != "" {
t.Errorf("Unexpected result from resource generation (-want +got):\n%s", diff)
}
})
}
}
}

Expand Down Expand Up @@ -74,24 +107,27 @@ func TestAcceptance(t *testing.T) {
}

for _, n := range fileNames {
t.Run(n, func(t *testing.T) {
file, err := os.Open(n)
if err != nil {
panic(err)
}
expected, err := ioutil.ReadFile(strings.Replace(n, ".zone", ".expected", 1))
if err != nil {
panic(err)
}
for _, syntax := range []syntaxMode{Modern, Legacy} {
t.Run(caseName(n, syntax), func(t *testing.T) {
file, err := os.Open(n)
if err != nil {
panic(err)
}
expected, err := ioutil.ReadFile(strings.Replace(n, ".zone", fmt.Sprintf(".expected-%v", syntax), 1))
if err != nil {
panic(err)
}

var buf bytes.Buffer
domain := strings.Replace(filepath.Base(n), ".zone", "", 1)
excludedTypes := excludedTypesFromString("SOA,NS")
generateTerraformForZone(domain, excludedTypes, file, &buf)
g := newConfigGenerator(syntax)
var buf bytes.Buffer
domain := strings.Replace(filepath.Base(n), ".zone", "", 1)
excludedTypes := excludedTypesFromString("SOA,NS")
g.generateTerraformForZone(domain, excludedTypes, file, &buf)

if diff := cmp.Diff(string(expected), buf.String(), diffOpts); diff != "" {
t.Errorf("Unexpected result from full Terraform output (-want +got):\n%s", diff)
}
})
if diff := cmp.Diff(string(expected), buf.String(), diffOpts); diff != "" {
t.Errorf("Unexpected result from full Terraform output (-want +got):\n%s", diff)
}
})
}
}
}
File renamed without changes.
96 changes: 96 additions & 0 deletions testdata/example.com.expected-modern
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
resource "aws_route53_zone" "example-com" {
name = "example.com"
}

# wwwtest.example.com is another alias for www.example.com
resource "aws_route53_record" "wwwtest-example-com-CNAME" {
zone_id = aws_route53_zone.example-com.zone_id
name = "wwwtest.example.com."
type = "CNAME"
ttl = "3600"
records = ["www.example.com."]
}

# www.example.com is an alias for example.com
resource "aws_route53_record" "www-example-com-CNAME" {
zone_id = aws_route53_zone.example-com.zone_id
name = "www.example.com."
type = "CNAME"
ttl = "3600"
records = ["example.com."]
}

# IPv6 address for ns.example.com
resource "aws_route53_record" "ns-example-com-AAAA" {
zone_id = aws_route53_zone.example-com.zone_id
name = "ns.example.com."
type = "AAAA"
ttl = "3600"
records = ["2001:db8:10::2"]
}

# IPv4 address for ns.example.com
resource "aws_route53_record" "ns-example-com-A" {
zone_id = aws_route53_zone.example-com.zone_id
name = "ns.example.com."
type = "A"
ttl = "3600"
records = ["192.0.2.2"]
}

# IPv4 address for mail3.example.com
resource "aws_route53_record" "mail3-example-com-A" {
zone_id = aws_route53_zone.example-com.zone_id
name = "mail3.example.com."
type = "A"
ttl = "3600"
records = ["192.0.2.5"]
}

# IPv4 address for mail2.example.com
resource "aws_route53_record" "mail2-example-com-A" {
zone_id = aws_route53_zone.example-com.zone_id
name = "mail2.example.com."
type = "A"
ttl = "3600"
records = ["192.0.2.4"]
}

# IPv4 address for mail.example.com
resource "aws_route53_record" "mail-example-com-A" {
zone_id = aws_route53_zone.example-com.zone_id
name = "mail.example.com."
type = "A"
ttl = "3600"
records = ["192.0.2.3"]
}

# mail.example.com is the mailserver for example.com
# equivalent to above line, "@" represents zone origin
# equivalent to above line, but using a relative host name
resource "aws_route53_record" "example-com-MX" {
zone_id = aws_route53_zone.example-com.zone_id
name = "example.com."
type = "MX"
ttl = "3600"
records = ["10 mail.example.com.", "20 mail2.example.com.", "50 mail3.example.com."]
}

# IPv6 address for example.com
resource "aws_route53_record" "example-com-AAAA" {
zone_id = aws_route53_zone.example-com.zone_id
name = "example.com."
type = "AAAA"
ttl = "3600"
records = ["2001:db8:10::1"]
}

# IPv4 address for example.com
resource "aws_route53_record" "example-com-A" {
zone_id = aws_route53_zone.example-com.zone_id
name = "example.com."
type = "A"
ttl = "3600"
records = ["192.0.2.1"]
}