diff --git a/README.md b/README.md index a1d77f7..e8468f1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # tfz53 (previously knows as bzfttr53rdutil) -A conversion utility for creating [Terraform](https://terraform.io) resource definitions for AWS Route53 from BIND zonefiles. +A conversion utility for creating [Terraform](https://terraform.io) or [Cloudformation](https://aws.amazon.com/cloudformation/) resource definitions for AWS Route53 from BIND zonefiles. ## Installation Download the [latest release](https://github.com/carlpett/tfz53/releases/latest). @@ -7,6 +7,9 @@ Download the [latest release](https://github.com/carlpett/tfz53/releases/latest) ## Usage `tfz53 -domain [flags] > route53-domain.tf` +`tfz53 -cloudformation -domain [flags] > route53-domain.cfn.yaml` + + ## Flags | Name | Description | Default | |------------|----------------------------------------------------|-----------------| diff --git a/go.mod b/go.mod index 8fdaf69..50e0ed1 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/carlpett/tfz53 require ( github.com/google/go-cmp v0.3.0 + github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 github.com/miekg/dns v1.0.8 golang.org/x/crypto v0.0.0-20180718160520-a2144134853f // indirect golang.org/x/net v0.0.0-20180719001425-81d44fd177a9 diff --git a/go.sum b/go.sum index 273a70d..3ea2ffa 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/miekg/dns v1.0.8 h1:Zi8HNpze3NeRWH1PQV6O71YcvJRQ6j0lORO6DAEmAAI= github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= golang.org/x/crypto v0.0.0-20180718160520-a2144134853f h1:lRy+hhwk7YT7MsKejxuz0C5Q1gk6p/QoPQYEmKmGFb8= diff --git a/main.go b/main.go index f2ca3e0..14ee9c4 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "strings" "text/template" + "github.com/iancoleman/strcase" "github.com/miekg/dns" "golang.org/x/net/idna" ) @@ -24,11 +25,11 @@ var ( ) const ( - zoneTemplateStr = `resource "aws_route53_zone" "{{ .ID }}" { + tfZoneTemplateStr = `resource "aws_route53_zone" "{{ .ID }}" { name = "{{ .Domain }}" } ` - recordTemplateStr = `{{- range .Record.Comments }} + tfRecordTemplateStr = `{{- range .Record.Comments }} # {{ . }}{{ end }} resource "aws_route53_record" "{{ .ResourceID }}" { zone_id = {{ zoneReference .ZoneID }} @@ -37,6 +38,25 @@ resource "aws_route53_record" "{{ .ResourceID }}" { ttl = "{{ .Record.TTL }}" records = [{{ range $idx, $elem := .Record.Data }}{{ if $idx }}, {{ end }}{{ ensureQuoted $elem }}{{ end }}] } +` + + cfnZoneTemplateStr = `Resources: + {{ .ID }}: + Type: AWS::Route53::HostedZone + Properties: + Name: "{{ .Domain }}" +` + cfnRecordTemplateStr = `{{- range .Record.Comments }} + # {{ . }}{{ end }} + {{ .ResourceID }}: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref {{ .ZoneID }} + Name: "{{ .Record.Name }}" + Type: "{{ .Record.Type }}" + TTL: "{{ .Record.TTL }}" + ResourceRecords:{{ range $idx, $elem := .Record.Data }} + - {{ ensureQuoted $elem }}{{ end }} ` ) @@ -48,6 +68,8 @@ func (m syntaxMode) String() string { return "modern" case Legacy: return "legacy" + case Cloudformation: + return "cloudformation" default: panic("Unknown syntax") } @@ -56,6 +78,7 @@ func (m syntaxMode) String() string { const ( Modern syntaxMode = iota Legacy + Cloudformation ) type configGenerator struct { @@ -66,6 +89,16 @@ type configGenerator struct { } func newConfigGenerator(syntax syntaxMode) *configGenerator { + var zoneTemplateStr string + var recordTemplateStr string + if syntax == Cloudformation { + zoneTemplateStr = cfnZoneTemplateStr + recordTemplateStr = cfnRecordTemplateStr + } else { + zoneTemplateStr = tfZoneTemplateStr + recordTemplateStr = tfRecordTemplateStr + } + g := &configGenerator{syntax: syntax} g.zoneTemplate = template.Must(template.New("zone").Parse(zoneTemplateStr)) g.recordTemplate = template.Must(template.New("record").Funcs(template.FuncMap{ @@ -118,6 +151,7 @@ var ( zoneFile = flag.String("zone-file", "", "Path to zone file. Defaults to .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)") + cloudformation = flag.Bool("cloudformation", false, "Generate cloudformation syntax") ) func main() { @@ -142,16 +176,18 @@ func main() { } var syntax syntaxMode - if !*legacySyntax { + if *cloudformation { + syntax = Cloudformation + } else if !*legacySyntax { syntax = Modern } else { syntax = Legacy } g := newConfigGenerator(syntax) - g.generateTerraformForZone(*domain, excludedTypes, fileReader, os.Stdout) + g.generateTemplateForZone(*domain, excludedTypes, fileReader, os.Stdout) } -func (g *configGenerator) generateTerraformForZone(domain string, excludedTypes map[uint16]bool, zoneReader io.Reader, output io.Writer) { +func (g *configGenerator) generateTemplateForZone(domain string, excludedTypes map[uint16]bool, zoneReader io.Reader, output io.Writer) { records := readZoneRecords(zoneReader, excludedTypes) zoneID, err := g.generateZoneResource(domain, output) @@ -203,8 +239,15 @@ func readZoneRecords(zoneReader io.Reader, excludedTypes map[uint16]bool) map[re func (g *configGenerator) generateZoneResource(domain string, w io.Writer) (string, error) { zoneName := strings.TrimRight(domain, ".") + var zoneID string + + if g.syntax == Cloudformation { + zoneID = strcase.ToCamel(zoneName) + } else { + zoneID = strings.Replace(zoneName, ".", "-", -1) + } data := zoneTemplateData{ - ID: strings.Replace(zoneName, ".", "-", -1), + ID: zoneID, Domain: zoneName, } @@ -215,6 +258,9 @@ func (g *configGenerator) generateZoneResource(domain string, w io.Writer) (stri func (g *configGenerator) generateRecordResource(record dnsRecord, zoneID string, w io.Writer) error { sanitizedName := sanitizeRecordName(record.Name) id := fmt.Sprintf("%s-%s", sanitizedName, record.Type) + if g.syntax == Cloudformation { + id = strcase.ToCamel(id) + } data := recordTemplateData{ ResourceID: id, diff --git a/main_test.go b/main_test.go index ce25296..eb34782 100644 --- a/main_test.go +++ b/main_test.go @@ -56,6 +56,17 @@ resource "aws_route53_record" "foo-bar-A" { ttl = "3600" records = ["127.0.0.1"] }`, + Cloudformation: `# This is a test +Resources: + FooBarA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref TestZone + Name: "foo.bar" + Type: "A" + TTL: ""3600" + ResourceRecords: + - "127.0.0.1"`, }, }, } @@ -108,7 +119,7 @@ func TestAcceptance(t *testing.T) { } for _, n := range fileNames { - for _, syntax := range []syntaxMode{Modern, Legacy} { + for _, syntax := range []syntaxMode{Modern, Legacy, Cloudformation} { t.Run(caseName(n, syntax), func(t *testing.T) { file, err := os.Open(n) if err != nil { @@ -123,7 +134,7 @@ func TestAcceptance(t *testing.T) { var buf bytes.Buffer domain := strings.Replace(filepath.Base(n), ".zone", "", 1) excludedTypes := excludedTypesFromString("SOA,NS") - g.generateTerraformForZone(domain, excludedTypes, file, &buf) + g.generateTemplateForZone(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) diff --git a/testdata/example.com.expected-cloudformation b/testdata/example.com.expected-cloudformation new file mode 100644 index 0000000..1305728 --- /dev/null +++ b/testdata/example.com.expected-cloudformation @@ -0,0 +1,119 @@ +Resources: + ExampleCom: + Type: AWS::Route53::HostedZone + Properties: + Name: "example.com" + + # wwwtest.example.com is another alias for www.example.com + WwwtestExampleComCNAME: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "wwwtest.example.com." + Type: "CNAME" + TTL: "3600" + ResourceRecords: + - "www.example.com." + + # www.example.com is an alias for example.com + WwwExampleComCNAME: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "www.example.com." + Type: "CNAME" + TTL: "3600" + ResourceRecords: + - "example.com." + + # IPv6 address for ns.example.com + NsExampleComAAAA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "ns.example.com." + Type: "AAAA" + TTL: "3600" + ResourceRecords: + - "2001:db8:10::2" + + # IPv4 address for ns.example.com + NsExampleComA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "ns.example.com." + Type: "A" + TTL: "3600" + ResourceRecords: + - "192.0.2.2" + + # IPv4 address for mail3.example.com + Mail3ExampleComA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "mail3.example.com." + Type: "A" + TTL: "3600" + ResourceRecords: + - "192.0.2.5" + + # IPv4 address for mail2.example.com + Mail2ExampleComA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "mail2.example.com." + Type: "A" + TTL: "3600" + ResourceRecords: + - "192.0.2.4" + + # IPv4 address for mail.example.com + MailExampleComA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "mail.example.com." + Type: "A" + TTL: "3600" + ResourceRecords: + - "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 + ExampleComMX: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "example.com." + Type: "MX" + TTL: "3600" + ResourceRecords: + - "10 mail.example.com." + - "20 mail2.example.com." + - "50 mail3.example.com." + + # IPv6 address for example.com + ExampleComAAAA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "example.com." + Type: "AAAA" + TTL: "3600" + ResourceRecords: + - "2001:db8:10::1" + + # IPv4 address for example.com + ExampleComA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: !Ref ExampleCom + Name: "example.com." + Type: "A" + TTL: "3600" + ResourceRecords: + - "192.0.2.1"