From 772469d21bb845b4eb6673a6447fde0a2a1bb396 Mon Sep 17 00:00:00 2001 From: Dessa Simpson Date: Sun, 30 Nov 2025 19:25:34 -0700 Subject: [PATCH] initial commit --- .envrc | 9 + .pre-commit-config.yaml | 26 ++ README.md | 5 + devbox.json | 7 + devbox.lock | 63 ++++ go.mod | 37 ++ go.sum | 109 ++++++ testdata/bad_include_multiple_documents.yaml | 1 + .../bad_include_tag_value_not_scalar.yaml | 1 + testdata/bad_invalid_record_missing_type.yaml | 3 + .../bad_invalid_record_missing_value.yaml | 3 + testdata/bad_invalid_tag.yaml | 1 + testdata/bad_invalid_type.yaml | 1 + testdata/bad_invalid_yaml.yaml | 1 + testdata/bad_missing_ns.yaml | 7 + testdata/bad_missing_soa.yaml | 5 + testdata/bad_multiple_documents.yaml | 8 + testdata/bad_record_type_mismatch.yaml | 4 + testdata/bad_recursion_1.yaml | 7 + testdata/bad_recursion_2.yaml | 7 + testdata/example.org.yaml | 9 + testdata/nested/multilayer/zone.yaml | 4 + testdata/nested/zone.yaml | 2 + testdata/zones.yaml | 64 ++++ yamlplugin.go | 99 +++++ yamlzone.go | 294 +++++++++++++++ yamlzone_test.go | 344 ++++++++++++++++++ 27 files changed, 1121 insertions(+) create mode 100644 .envrc create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100644 go.mod create mode 100644 go.sum create mode 100644 testdata/bad_include_multiple_documents.yaml create mode 100644 testdata/bad_include_tag_value_not_scalar.yaml create mode 100644 testdata/bad_invalid_record_missing_type.yaml create mode 100644 testdata/bad_invalid_record_missing_value.yaml create mode 100644 testdata/bad_invalid_tag.yaml create mode 100644 testdata/bad_invalid_type.yaml create mode 100644 testdata/bad_invalid_yaml.yaml create mode 100644 testdata/bad_missing_ns.yaml create mode 100644 testdata/bad_missing_soa.yaml create mode 100644 testdata/bad_multiple_documents.yaml create mode 100644 testdata/bad_record_type_mismatch.yaml create mode 100644 testdata/bad_recursion_1.yaml create mode 100644 testdata/bad_recursion_2.yaml create mode 100644 testdata/example.org.yaml create mode 100644 testdata/nested/multilayer/zone.yaml create mode 100644 testdata/nested/zone.yaml create mode 100644 testdata/zones.yaml create mode 100644 yamlplugin.go create mode 100644 yamlzone.go create mode 100644 yamlzone_test.go diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..eff60eb --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/bin/bash + +# Automatically sets up your devbox environment whenever you cd into this +# directory via our direnv integration: + +eval "$(devbox generate direnv --print-envrc)" + +# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/ +# for more details diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..10e35b9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-merge-conflict + - id: detect-private-key + + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.1 + hooks: + - id: go-fmt + - id: go-vet + - id: go-imports + - id: go-unit-tests + - id: go-build + - id: go-mod-tidy + + - repo: https://github.com/golangci/golangci-lint + rev: v1.62.2 + hooks: + - id: golangci-lint diff --git a/README.md b/README.md new file mode 100644 index 0000000..2cf6132 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# yaml + +## Name + +*yaml* - enables serving zone data from a YAML file. diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..f189e9d --- /dev/null +++ b/devbox.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.2/.schema/devbox.schema.json", + "packages": [ + "go@1.24.10", + "goimports@latest" + ] +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..48f9396 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,63 @@ +{ + "lockfile_version": "1", + "packages": { + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2025-11-30T18:29:45Z", + "resolved": "github:NixOS/nixpkgs/23258e03aaa49b3a68597e3e50eb0cbce7e42e9d?lastModified=1764527385&narHash=sha256-nA5ywiGKl76atrbdZ5Aucd8SjF%2Fv8ew9b9QsC%2BMKL14%3D" + }, + "go@1.24.10": { + "last_modified": "2025-11-23T21:50:36Z", + "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#go_1_24", + "source": "devbox-search", + "version": "1.24.10", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/mzqh3c6jhjb8nglq6aivgmhqzrgc1416-go-1.24.10", + "default": true + } + ], + "store_path": "/nix/store/mzqh3c6jhjb8nglq6aivgmhqzrgc1416-go-1.24.10" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/qjw3k4983j843n19xifai1vn79hhd2hh-go-1.24.10", + "default": true + } + ], + "store_path": "/nix/store/qjw3k4983j843n19xifai1vn79hhd2hh-go-1.24.10" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/0q1jah9apr7mayvwqgdg0ls0k5mw1586-go-1.24.10", + "default": true + } + ], + "store_path": "/nix/store/0q1jah9apr7mayvwqgdg0ls0k5mw1586-go-1.24.10" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/39x7yrmrxdws2866fbmav3zhwy01fkis-go-1.24.10", + "default": true + } + ], + "store_path": "/nix/store/39x7yrmrxdws2866fbmav3zhwy01fkis-go-1.24.10" + } + } + }, + "goimports@latest": { + "last_modified": "2022-02-15T04:21:50Z", + "resolved": "github:NixOS/nixpkgs/3a641defd170a4ef25ce8c7c64cb13f91f867fca#goimports", + "source": "devbox-search", + "version": "2021-01-13" + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1a69b33 --- /dev/null +++ b/go.mod @@ -0,0 +1,37 @@ +module go.dxs.gay/coredns-yaml + +go 1.25.3 + +require ( + github.com/coredns/caddy v1.1.4-0.20250930002214-15135a999495 + github.com/coredns/coredns v1.13.1 + github.com/goccy/go-yaml v1.18.0 + github.com/miekg/dns v1.1.68 +) + +require ( + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2f572e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,109 @@ +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coredns/caddy v1.1.4-0.20250930002214-15135a999495 h1:JFeOmbjLnVRhvmLHyuO3M1pfXWlPWpwkdM8UqXZRtBg= +github.com/coredns/caddy v1.1.4-0.20250930002214-15135a999495/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coredns/coredns v1.13.1 h1:yhYvf/QVwHNjBK65RkC8d9VW91dP9XKem3BOon4eokg= +github.com/coredns/coredns v1.13.1/go.mod h1:UHmBXdGEn/WQ1jdyMYgOxFh/VklkE//arIAxptwVAZI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testdata/bad_include_multiple_documents.yaml b/testdata/bad_include_multiple_documents.yaml new file mode 100644 index 0000000..3a76890 --- /dev/null +++ b/testdata/bad_include_multiple_documents.yaml @@ -0,0 +1 @@ +!include bad_multiple_documents.yaml diff --git a/testdata/bad_include_tag_value_not_scalar.yaml b/testdata/bad_include_tag_value_not_scalar.yaml new file mode 100644 index 0000000..96ae3ef --- /dev/null +++ b/testdata/bad_include_tag_value_not_scalar.yaml @@ -0,0 +1 @@ +"@": !include [] diff --git a/testdata/bad_invalid_record_missing_type.yaml b/testdata/bad_invalid_record_missing_type.yaml new file mode 100644 index 0000000..153385c --- /dev/null +++ b/testdata/bad_invalid_record_missing_type.yaml @@ -0,0 +1,3 @@ +"@": + - ttl: 60 + value: 192.0.2.1 diff --git a/testdata/bad_invalid_record_missing_value.yaml b/testdata/bad_invalid_record_missing_value.yaml new file mode 100644 index 0000000..152d1f2 --- /dev/null +++ b/testdata/bad_invalid_record_missing_value.yaml @@ -0,0 +1,3 @@ +"@": + - type: A + ttl: 60 diff --git a/testdata/bad_invalid_tag.yaml b/testdata/bad_invalid_tag.yaml new file mode 100644 index 0000000..a2297aa --- /dev/null +++ b/testdata/bad_invalid_tag.yaml @@ -0,0 +1 @@ +!foo bar diff --git a/testdata/bad_invalid_type.yaml b/testdata/bad_invalid_type.yaml new file mode 100644 index 0000000..323fae0 --- /dev/null +++ b/testdata/bad_invalid_type.yaml @@ -0,0 +1 @@ +foobar diff --git a/testdata/bad_invalid_yaml.yaml b/testdata/bad_invalid_yaml.yaml new file mode 100644 index 0000000..98232c6 --- /dev/null +++ b/testdata/bad_invalid_yaml.yaml @@ -0,0 +1 @@ +{ diff --git a/testdata/bad_missing_ns.yaml b/testdata/bad_missing_ns.yaml new file mode 100644 index 0000000..4eba307 --- /dev/null +++ b/testdata/bad_missing_ns.yaml @@ -0,0 +1,7 @@ +"@": + - type: SOA + value: ns1.example.com. admin.example.com. 1 1 1 1 1 + - type: A + value: 192.0.2.100 + - type: AAAA + value: 2001:db8::100 diff --git a/testdata/bad_missing_soa.yaml b/testdata/bad_missing_soa.yaml new file mode 100644 index 0000000..1d0f442 --- /dev/null +++ b/testdata/bad_missing_soa.yaml @@ -0,0 +1,5 @@ +"@": + - type: A + value: 192.0.2.100 + - type: AAAA + value: 2001:db8::100 diff --git a/testdata/bad_multiple_documents.yaml b/testdata/bad_multiple_documents.yaml new file mode 100644 index 0000000..2088cf3 --- /dev/null +++ b/testdata/bad_multiple_documents.yaml @@ -0,0 +1,8 @@ +--- +"@": + - type: A + - value: 1.2.3.4 +--- +"@": + - type: A + - value: 4.3.2.1 diff --git a/testdata/bad_record_type_mismatch.yaml b/testdata/bad_record_type_mismatch.yaml new file mode 100644 index 0000000..bf3cf04 --- /dev/null +++ b/testdata/bad_record_type_mismatch.yaml @@ -0,0 +1,4 @@ +"@": + - type: A + value: 1.2.3.4 + ttl: "not a number" diff --git a/testdata/bad_recursion_1.yaml b/testdata/bad_recursion_1.yaml new file mode 100644 index 0000000..830069a --- /dev/null +++ b/testdata/bad_recursion_1.yaml @@ -0,0 +1,7 @@ +com: + example: + "@": + - type: A + ttl: 3600 + value: 192.0.2.1 + cloud: !include bad_recursion_2.yaml diff --git a/testdata/bad_recursion_2.yaml b/testdata/bad_recursion_2.yaml new file mode 100644 index 0000000..68dcb43 --- /dev/null +++ b/testdata/bad_recursion_2.yaml @@ -0,0 +1,7 @@ +foo: + bar: + "@": + - type: A + ttl: 3600 + value: 192.0.2.1 + cloud: !include bad_recursion_1.yaml diff --git a/testdata/example.org.yaml b/testdata/example.org.yaml new file mode 100644 index 0000000..2555361 --- /dev/null +++ b/testdata/example.org.yaml @@ -0,0 +1,9 @@ +"@": + - type: SOA + value: ns1.example.com. admin.example.com. 1 1 1 1 1 + - type: A + value: 192.0.2.100 + - type: AAAA + value: 2001:db8::100 + - type: NS + value: ns1.example.com diff --git a/testdata/nested/multilayer/zone.yaml b/testdata/nested/multilayer/zone.yaml new file mode 100644 index 0000000..2b0e3a7 --- /dev/null +++ b/testdata/nested/multilayer/zone.yaml @@ -0,0 +1,4 @@ +"@": + - type: A + ttl: 3600 + value: 192.0.2.1 diff --git a/testdata/nested/zone.yaml b/testdata/nested/zone.yaml new file mode 100644 index 0000000..8cbdbe5 --- /dev/null +++ b/testdata/nested/zone.yaml @@ -0,0 +1,2 @@ +nested: + multilayer: !include multilayer/zone.yaml diff --git a/testdata/zones.yaml b/testdata/zones.yaml new file mode 100644 index 0000000..b45ea3b --- /dev/null +++ b/testdata/zones.yaml @@ -0,0 +1,64 @@ +# Comment +com: # Comment + example: + # Comment + "@": + - type: SOA + value: ns1.example.com. admin.example.com. 1 1 1 1 1 + - type: A + value: 192.0.2.1 + - type: AAAA + value: 2001:db8::1 + - type: MX + ttl: 3600 + value: 10 mail.example.com + - type: TXT + ttl: 300 + value: "v=spf1 a mx include:mail.example.com ~all" + - type: CAA + ttl: 86400 + value: 0 issue "letsencrypt.org" + - type: TXT + ttl: 3600 + value: "foo=bar" + - type: NS + value: ns1.example.com + www: + - type: CNAME + ttl: 3600 + value: example.com + mail: + - type: A + value: 192.0.2.2 + status: + - type: A + ttl: 3600 + value: 198.51.100.24 + - type: A + ttl: 3600 + value: 203.0.113.24 + unused: [] + _tcp: + _xmpp-server: + - type: SRV + ttl: 3600 + value: 10 0 5269 example.com + internal: + "@": + - type: A + ttl: 3600 + value: 10.0.0.1 + ftp: + - type: A + ttl: 3600 + value: 10.0.0.2 + partner: + - type: NS + ttl: 3600 + value: ns1.example.org + - type: NS + ttl: 3600 + value: ns2.example.org + folders: !include nested/zone.yaml +org: + example: !include example.org.yaml diff --git a/yamlplugin.go b/yamlplugin.go new file mode 100644 index 0000000..7f42f4a --- /dev/null +++ b/yamlplugin.go @@ -0,0 +1,99 @@ +package yamlzone + +import ( + "context" + "fmt" + "strconv" + + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +type YamlPlugin struct { + Next plugin.Handler + Zone *Zone + Config YamlPluginConfig +} + +type YamlPluginConfig struct { + DefaultTtl uint64 +} + +var log = clog.NewWithPlugin("yaml") + +func (y YamlPlugin) lookupRRs(qname string, qtype string) ([]dns.RR, bool) { + records, ok := y.Zone.LookupType(qname, qtype) + if !ok { + return nil, false + } + rrs := []dns.RR{} + for _, record := range records { + ttl := record.Ttl + if ttl == 0 { + ttl = y.Config.DefaultTtl + } + rr, err := dns.NewRR(fmt.Sprintf("%s %d %s %s", qname, ttl, record.Type, record.Value)) + if err != nil { + return nil, false + } + rrs = append(rrs, rr) + } + return rrs, true +} + +func (y YamlPlugin) Name() string { return "yaml" } + +func (y YamlPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.Name() + qtype := state.Type() + + reply := new(dns.Msg) + reply.SetReply(r) + reply.Authoritative = true + + rrs, ok := y.lookupRRs(qname, qtype) + if !ok { + return dns.RcodeNameError, nil + } + reply.Answer = rrs + w.WriteMsg(reply) + return dns.RcodeSuccess, nil +} + +func setup(c *caddy.Controller) error { + c.Next() // yaml + filename := "zones.yaml" + if c.NextArg() { + filename = c.Val() + } + defaultTtl := uint64(60) + if c.NextArg() { + var err error + defaultTtl, err = strconv.ParseUint(c.Val(), 10, 32) + if err != nil { + return plugin.Error("yaml", fmt.Errorf("failed to parse default TTL: %w", err)) + } + } + zone, err := LoadZone(filename) + if err != nil { + return plugin.Error("yaml", fmt.Errorf("failed to load zone from %s: %w", filename, err)) + } + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return &YamlPlugin{ + Next: next, + Zone: zone, + Config: YamlPluginConfig{ + DefaultTtl: defaultTtl, + }, + } + }) + return nil +} + +func init() { plugin.Register("yaml", setup) } diff --git a/yamlzone.go b/yamlzone.go new file mode 100644 index 0000000..5438c42 --- /dev/null +++ b/yamlzone.go @@ -0,0 +1,294 @@ +package yamlzone + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/goccy/go-yaml" + "github.com/goccy/go-yaml/ast" + "github.com/goccy/go-yaml/parser" +) + +type Record struct { + Type string + Ttl uint64 + Value string +} + +type Zone struct { + Subzones map[string]*Zone + Records []Record + GlueRecords []Record + IsDelegationPoint bool +} + +type contextKey string + +const ( + ctxDirectory = contextKey("directory") + ctxFiles = contextKey("files") +) + +func LoadZone(filename string) (*Zone, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + return LoadZoneBytes(data, filename) +} + +func LoadZoneBytes(data []byte, filename string) (*Zone, error) { + // Check for multiple documents before unmarshaling + file, err := parser.ParseBytes(data, 0) + if err != nil { + return nil, err + } + if len(file.Docs) != 1 { + return nil, fmt.Errorf("expected exactly one document, got %d", len(file.Docs)) + } + + z := &Zone{} + ctx := context.WithValue(context.TODO(), ctxDirectory, filepath.Dir(filename)) + ctx = context.WithValue(ctx, ctxFiles, map[string]bool{filename: true}) + err = yaml.UnmarshalContext(ctx, data, z) + if err != nil { + return nil, err + } + if err := z.Validate(".", false); err != nil { + return nil, err + } + return z, nil +} + +func (z *Zone) Validate(name string, zoneApexPresent bool) error { + nameservers := map[string]bool{} + otherRecords := []Record{} + isZoneApex := false + for _, record := range z.Records { + switch record.Type { + case "SOA": + isZoneApex = true + case "NS": + nameservers[record.Value] = true + default: + otherRecords = append(otherRecords, record) + } + } + + zoneApexPresent = zoneApexPresent || isZoneApex + z.IsDelegationPoint = !isZoneApex && len(nameservers) > 0 + + if !zoneApexPresent { + // Outside zone + if z.IsDelegationPoint { + return fmt.Errorf("%s: delegation point found outside zone", name) + } else if len(otherRecords) > 0 { + return fmt.Errorf("%s: records found outside zone: %v", name, otherRecords) + } + } else if isZoneApex { + // Zone apex + if len(nameservers) == 0 { + return fmt.Errorf("%s: zone apex missing NS records", name) + } + } else if len(nameservers) > 0 { + // Delegation point (does not fall through to subzone validation) + if len(otherRecords) > 0 { + return fmt.Errorf("%s: non-glue, non-NS records found at delegation point: %v", name, otherRecords) + } + for subname, subzone := range z.Subzones { + glueRecords, err := subzone.GetGlueRecords(concatName(name, subname), nameservers) + if err != nil { + return err + } + z.GlueRecords = append(z.GlueRecords, glueRecords...) + } + return nil + } + // Subzone validation + // Either we're outside a zone, at a zone apex, or at a non-delegated subzone + for subname, subzone := range z.Subzones { + if err := subzone.Validate(concatName(name, subname), zoneApexPresent); err != nil { + return err + } + } + return nil +} + +func (z *Zone) GetGlueRecords(name string, nameservers map[string]bool) ([]Record, error) { + // If the domain is not a nameserver, it must have no records + if _, ok := nameservers[name]; !ok { + if len(z.Records) > 0 { + return nil, fmt.Errorf("%s: non-glue records found under delegation point: %v", name, z.Records) + } + } + // Any records under a delegation point must be glue records + for _, record := range z.Records { + if !(record.Type == "A" || record.Type == "AAAA") { + return nil, fmt.Errorf("%s: non-glue record found under delegation point: %v", name, record) + } + } + for subname, subzone := range z.Subzones { + if glueRecords, err := subzone.GetGlueRecords(concatName(name, subname), nameservers); err != nil { + return nil, err + } else { + z.GlueRecords = append(z.GlueRecords, glueRecords...) + } + } + return z.GlueRecords, nil +} + +func (r *Record) UnmarshalYAML(ctx context.Context, data []byte) error { + type rawRecord struct { + Type string `yaml:"type"` + Ttl uint64 `yaml:"ttl"` + Value string `yaml:"value"` + } + var rr rawRecord + if err := yaml.UnmarshalContext(ctx, data, &rr); err != nil { + return err + } + if rr.Type == "" { + return fmt.Errorf("record: missing type") + } + if rr.Value == "" { + return fmt.Errorf("record: missing value") + } + *r = Record(rr) + return nil +} + +func (z *Zone) UnmarshalYAML(ctx context.Context, data []byte) error { + // Store the current directory so recursive includes can be resolved + dir, ok := ctx.Value(ctxDirectory).(string) + if !ok { + dir = "." + } + + // Store the list of files that have been included in this chain to avoid + // infinite recursion + files, ok := ctx.Value(ctxFiles).(map[string]bool) + if !ok { + files = map[string]bool{} + } + + // Parse the YAML data into an AST to check for !include tags and handle + // mapping vs sequence nodes + file, err := parser.ParseBytes(data, 0) + if err != nil { + return err + } + + node := file.Docs[0].Body + + switch node.Type() { + case ast.TagType: + tagNode := node.(*ast.TagNode) + if tagNode.Start.Value != "!include" { + return fmt.Errorf("encountered unexpected tag %s", tagNode.Start.Value) + } + includeFilenameNode, ok := tagNode.Value.(ast.ScalarNode) + if !ok { + return fmt.Errorf("include: expected scalar") + } + includeFilename := includeFilenameNode.GetValue().(string) + qualifiedIncludeFilename := filepath.Join(dir, includeFilename) + includedData, err := os.ReadFile(qualifiedIncludeFilename) + if err != nil { + return err + } + + // Check for multiple documents in included file before unmarshaling + includedFile, err := parser.ParseBytes(includedData, 0) + if err != nil { + return err + } + if len(includedFile.Docs) != 1 { + return fmt.Errorf("%s: expected exactly one document, got %d", qualifiedIncludeFilename, len(includedFile.Docs)) + } + + if _, ok := files[qualifiedIncludeFilename]; ok { + return fmt.Errorf("infinite recursion detected in %s", qualifiedIncludeFilename) + } + files[qualifiedIncludeFilename] = true + subctx := context.WithValue(ctx, ctxFiles, files) + subctx = context.WithValue(subctx, ctxDirectory, filepath.Dir(qualifiedIncludeFilename)) + err = yaml.UnmarshalContext(subctx, includedData, z) + if err != nil { + return err + } + case ast.SequenceType: + var records []Record + if err := yaml.UnmarshalContext(ctx, data, &records); err != nil { + return err + } + z.Records = records + case ast.MappingType: + var subzones map[string]*Zone + if err := yaml.UnmarshalContext(ctx, data, &subzones); err != nil { + return err + } + if _, ok := subzones["@"]; ok { + z.Records = subzones["@"].Records + delete(subzones, "@") + } + z.Subzones = subzones + default: + return fmt.Errorf("expected a sequence, mapping, or !include tag") + } + return nil +} + +// Converts a DNS name to the corresponding path in the zone tree +// "www.example.com" -> ["com", "example", "www"] +func nameToPath(name string) []string { + parts := strings.Split(name, ".") + path := []string{} + for i := len(parts) - 1; i >= 0; i-- { + path = append(path, parts[i]) + } + return path +} + +func (z *Zone) Lookup(name string) ([]Record, bool) { + path := nameToPath(name) + for _, label := range path { + // Support empty name and trailing dot + if label == "" { + continue + } + if sz, ok := z.Subzones[label]; ok { + z = sz + } else { + return nil, false + } + } + return z.Records, true +} + +func (z *Zone) FilterRecords(records []Record, recordType string) []Record { + filtered := []Record{} + for _, record := range records { + if record.Type == recordType { + filtered = append(filtered, record) + } + } + return filtered +} + +func (z *Zone) LookupType(name string, recordType string) ([]Record, bool) { + records, ok := z.Lookup(name) + if !ok { + return nil, false + } + return z.FilterRecords(records, recordType), true +} + +func concatName(name string, subname string) string { + if name == "." { + return subname + "." + } + return subname + "." + name +} diff --git a/yamlzone_test.go b/yamlzone_test.go new file mode 100644 index 0000000..66b4491 --- /dev/null +++ b/yamlzone_test.go @@ -0,0 +1,344 @@ +package yamlzone + +import ( + "context" + "strings" + "testing" + + "github.com/goccy/go-yaml" +) + +func assertOk(t *testing.T, ok bool) { + if !ok { + t.Fatalf("Expected ok, got false") + } +} + +func assertNotOk(t *testing.T, ok bool, records []Record) { + if ok { + t.Fatalf("Expected not ok, got ok (%v)", records) + } +} + +func assertRecordCount(t *testing.T, records []Record, expected int) { + if len(records) != expected { + t.Fatalf("Expected %d records, got %d (%v)", expected, len(records), records) + } +} + +func assertRecordType(t *testing.T, record Record, expected string) { + if record.Type != expected { + t.Fatalf("Expected %s record, got %s", expected, record.Type) + } +} + +func assertRecordTtl(t *testing.T, record Record, expected uint64) { + if record.Ttl != expected { + t.Fatalf("Expected TTL %d, got %d", expected, record.Ttl) + } +} + +func assertRecordValue(t *testing.T, record Record, expected string) { + if record.Value != expected { + t.Fatalf("Expected %s, got %s", expected, record.Value) + } +} + +func assertRecord(t *testing.T, record Record, expectedType string, expectedTtl uint64, expectedValue string) { + assertRecordType(t, record, expectedType) + assertRecordTtl(t, record, expectedTtl) + assertRecordValue(t, record, expectedValue) +} + +func TestEmptyZone(t *testing.T) { + var zEmpty Zone + + t.Run("Unmarshal", func(t *testing.T) { + err := yaml.Unmarshal([]byte("{}"), &zEmpty) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + if t.Failed() { + return + } + + t.Run("Lookup empty string", func(t *testing.T) { + records, ok := zEmpty.Lookup("") + assertOk(t, ok) + assertRecordCount(t, records, 0) + }) + + t.Run("Lookup single dot", func(t *testing.T) { + records, ok := zEmpty.Lookup(".") + assertOk(t, ok) + assertRecordCount(t, records, 0) + }) + + t.Run("Lookup example.com", func(t *testing.T) { + records, ok := zEmpty.Lookup("example.com") + assertNotOk(t, ok, records) + }) + + t.Run("LookupType example.com A", func(t *testing.T) { + records, ok := zEmpty.LookupType("example.com", "A") + assertNotOk(t, ok, records) + }) +} + +func TestSimpleZone(t *testing.T) { + const zSimpleFile = "testdata/example.org.yaml" + var zSimple *Zone + + t.Run("Load", func(t *testing.T) { + var err error + zSimple, err = LoadZone(zSimpleFile) + if err != nil { + t.Fatalf("Expected no error, got \"%v\"", err) + } + }) + + if t.Failed() { + return + } + + t.Run("Lookup", func(t *testing.T) { + records, ok := zSimple.Lookup("") + assertOk(t, ok) + assertRecordCount(t, records, 4) + assertRecord(t, records[0], "SOA", 0, "ns1.example.com. admin.example.com. 1 1 1 1 1") + assertRecord(t, records[1], "A", 0, "192.0.2.100") + assertRecord(t, records[2], "AAAA", 0, "2001:db8::100") + assertRecord(t, records[3], "NS", 0, "ns1.example.com") + }) + + t.Run("LookupType", func(t *testing.T) { + records, ok := zSimple.LookupType("", "A") + if !ok { + t.Fatalf("Expected ok, got false") + } + + assertRecordCount(t, records, 1) + assertRecord(t, records[0], "A", 0, "192.0.2.100") + }) +} + +func TestFullZone(t *testing.T) { + const zFullFile = "testdata/zones.yaml" + var zFull *Zone + + t.Run("Load", func(t *testing.T) { + var err error + zFull, err = LoadZone(zFullFile) + if err != nil { + t.Fatalf("Expected no error, got \"%v\"", err) + } + }) + + if t.Failed() { + return + } + + t.Run("Lookup example.com", func(t *testing.T) { + records, ok := zFull.Lookup("example.com") + if !ok { + t.Fatalf("Expected ok, got false") + } + + assertRecordCount(t, records, 8) + assertRecord(t, records[0], "SOA", 0, "ns1.example.com. admin.example.com. 1 1 1 1 1") + assertRecord(t, records[1], "A", 0, "192.0.2.1") + assertRecord(t, records[2], "AAAA", 0, "2001:db8::1") + assertRecord(t, records[3], "MX", 3600, "10 mail.example.com") // Default TTL + assertRecord(t, records[4], "TXT", 300, "v=spf1 a mx include:mail.example.com ~all") + assertRecord(t, records[5], "CAA", 86400, "0 issue \"letsencrypt.org\"") + assertRecord(t, records[6], "TXT", 3600, "foo=bar") + assertRecord(t, records[7], "NS", 0, "ns1.example.com") + + }) + + t.Run("LookupType example.com TXT", func(t *testing.T) { + records, ok := zFull.LookupType("example.com", "TXT") + assertOk(t, ok) + assertRecordCount(t, records, 2) + assertRecord(t, records[0], "TXT", 300, "v=spf1 a mx include:mail.example.com ~all") + assertRecord(t, records[1], "TXT", 3600, "foo=bar") + }) + + t.Run("Lookup www.example.com", func(t *testing.T) { + records, ok := zFull.Lookup("www.example.com") + assertOk(t, ok) + assertRecordCount(t, records, 1) + assertRecord(t, records[0], "CNAME", 3600, "example.com") + + }) + + t.Run("LookupType www.example.com CNAME", func(t *testing.T) { + records, ok := zFull.LookupType("www.example.com", "CNAME") + assertOk(t, ok) + assertRecordCount(t, records, 1) + assertRecord(t, records[0], "CNAME", 3600, "example.com") + }) + + t.Run("Lookup www.example.com TXT", func(t *testing.T) { + records, ok := zFull.LookupType("www.example.com", "TXT") + assertOk(t, ok) + assertRecordCount(t, records, 0) + }) + + t.Run("Lookup status.example.com", func(t *testing.T) { + records, ok := zFull.Lookup("status.example.com") + assertOk(t, ok) + assertRecordCount(t, records, 2) + assertRecord(t, records[0], "A", 3600, "198.51.100.24") + assertRecord(t, records[1], "A", 3600, "203.0.113.24") + }) + + t.Run("Lookup partner.example.com", func(t *testing.T) { + records, ok := zFull.Lookup("partner.example.com") + assertOk(t, ok) + assertRecordCount(t, records, 2) + assertRecord(t, records[0], "NS", 3600, "ns1.example.org") + assertRecord(t, records[1], "NS", 3600, "ns2.example.org") + }) + + t.Run("Lookup unused.example.com", func(t *testing.T) { + records, ok := zFull.Lookup("unused.example.com") + assertOk(t, ok) + assertRecordCount(t, records, 0) + }) + + t.Run("Lookup ftp.internal.example.com", func(t *testing.T) { + records, ok := zFull.Lookup("ftp.internal.example.com") + assertOk(t, ok) + assertRecordCount(t, records, 1) + assertRecord(t, records[0], "A", 3600, "10.0.0.2") + }) + + t.Run("Lookup _xmpp-server._tcp.example.com", func(t *testing.T) { + records, ok := zFull.Lookup("_xmpp-server._tcp.example.com") + assertOk(t, ok) + assertRecordCount(t, records, 1) + assertRecord(t, records[0], "SRV", 3600, "10 0 5269 example.com") + }) + + t.Run("Lookup multilayer.nested.folders.example.com", func(t *testing.T) { + records, ok := zFull.Lookup("multilayer.nested.folders.example.com") + assertOk(t, ok) + assertRecordCount(t, records, 1) + assertRecord(t, records[0], "A", 3600, "192.0.2.1") + }) +} + +func TestBadZones(t *testing.T) { + type badZone struct { + name string + filename string + errorSubstring string + } + var badZones = []badZone{ + { + name: "NonexistentFile", + filename: "testdata/bad_nonexistent.yaml", + errorSubstring: "open testdata/bad_nonexistent.yaml: no such file or directory", + }, + { + name: "Directory", + filename: "testdata/bad_directory.yaml", + errorSubstring: "read testdata/bad_directory.yaml: is a directory", + }, + { + name: "InvalidYaml", + filename: "testdata/bad_invalid_yaml.yaml", + errorSubstring: "[1:1]", + }, + { + name: "InvalidTag", + filename: "testdata/bad_invalid_tag.yaml", + errorSubstring: "encountered unexpected tag !foo", + }, + { + name: "InvalidType", + filename: "testdata/bad_invalid_type.yaml", + errorSubstring: "expected a sequence, mapping, or !include tag", + }, + { + name: "MultipleDocuments", + filename: "testdata/bad_multiple_documents.yaml", + errorSubstring: "expected exactly one document, got 2", + }, + { + name: "InvalidRecordMissingType", + filename: "testdata/bad_invalid_record_missing_type.yaml", + errorSubstring: "record: missing type", + }, + { + name: "InvalidRecordMissingValue", + filename: "testdata/bad_invalid_record_missing_value.yaml", + errorSubstring: "record: missing value", + }, + { + name: "InfiniteRecursion", + filename: "testdata/bad_recursion_1.yaml", + errorSubstring: "infinite recursion detected in testdata/bad_recursion_1.yaml", + }, + { + name: "IncludeTagValueNotScalar", + filename: "testdata/bad_include_tag_value_not_scalar.yaml", + errorSubstring: "include: expected scalar", + }, + { + name: "RecordTypeMismatch", + filename: "testdata/bad_record_type_mismatch.yaml", + errorSubstring: "cannot unmarshal string into Go struct field", + }, + { + name: "MissingSoa", + filename: "testdata/bad_missing_soa.yaml", + errorSubstring: "records found outside zone", + }, + { + name: "MissingNs", + filename: "testdata/bad_missing_ns.yaml", + errorSubstring: "zone apex missing NS records", + }, + } + + for _, badZone := range badZones { + t.Run(badZone.name, func(t *testing.T) { + z, err := LoadZone(badZone.filename) + if err == nil { + t.Errorf("Expected error, got nil (%v)", z) + } else if !strings.Contains(err.Error(), badZone.errorSubstring) { + t.Errorf("Expected error \"%s\", got \"%s\"", badZone.errorSubstring, err.Error()) + } + }) + t.Run("Include"+badZone.name, func(t *testing.T) { + z, err := LoadZoneBytes([]byte("!include "+badZone.filename), ".") + if err == nil { + t.Errorf("Expected error, got nil (%v)", z) + } else if !strings.Contains(err.Error(), badZone.errorSubstring) { + t.Errorf("Expected error \"%s\", got \"%s\"", badZone.errorSubstring, err.Error()) + } + }) + } +} + +// This test exists solely to reach 100% test coverage. It triggers an error +// condition that should never normally be possible, but is guarded against for +// completeness. +func TestDirectUnmarshalWithInvalidBytes(t *testing.T) { + // Directly call Zone.UnmarshalYAML with invalid bytes to trigger + // yamlzone.go:103 (error in parser.ParseBytes) + z := &Zone{} + invalidBytes := []byte("[invalid yaml") + + err := z.UnmarshalYAML(context.TODO(), invalidBytes) + if err == nil { + t.Fatalf("Expected error, got nil") + } + if !strings.Contains(err.Error(), "[1:") { + t.Errorf("Expected parser error, got %v", err) + } +}