diff --git a/go.mod b/go.mod index d9f0f7a..03a550f 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,65 @@ go 1.25.0 require ( github.com/go-chi/chi/v5 v5.3.0 github.com/jackc/pgx/v5 v5.10.0 + github.com/testcontainers/testcontainers-go v0.43.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rogpeppe/go-internal v1.15.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/text v0.29.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.5 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect ) diff --git a/go.sum b/go.sum index df514e9..ebb6bc0 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,54 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -12,26 +57,104 @@ github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM= +github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A= +github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0 h1:ShNOFYAF4lKHvdIG258hi69bSxC88uXnxJkJvNs/IVs= +github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0/go.mod h1:vdq5/RqmGfWeefzyfcVI/pID1rzmc1TDvqXa15bPJks= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/internal/database/migrations/0003_nat_tier.sql b/internal/database/migrations/0003_nat_tier.sql new file mode 100644 index 0000000..22b019a --- /dev/null +++ b/internal/database/migrations/0003_nat_tier.sql @@ -0,0 +1,37 @@ +-- NAT-tier resources: SNAT/masquerade, netmap, and 1:1 static NAT. These are the +-- "global-intent, device-resolved" tier — declared centrally, resolved per device +-- via bindings. Stored here so they are terraformable; compiler rendering of the +-- NAT tier into per-device configs is a tracked follow-up. + +-- SNAT / masquerade. source is a zone name or CIDR; egress is a zone (resolved to +-- the device's egress interface via its binding). address+probability support +-- load-balanced SNAT. +CREATE TABLE snat ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + action TEXT NOT NULL CHECK (action IN ('masquerade', 'snat')), + source TEXT NOT NULL, + egress TEXT NOT NULL, + address TEXT NOT NULL DEFAULT '', + probability REAL, + comment TEXT NOT NULL DEFAULT '' +); + +-- Network-to-network mapping (netmap). Anchored at a device+binding or interface. +CREATE TABLE netmap ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + type TEXT NOT NULL CHECK (type IN ('dnat', 'snat')), + from_net TEXT NOT NULL, + to_net TEXT NOT NULL, + anchor TEXT NOT NULL, -- device:zone or device:interface + comment TEXT NOT NULL DEFAULT '' +); + +-- One-to-one static NAT, bound to the device that owns the external IP. +CREATE TABLE nat ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE, + external TEXT NOT NULL, + internal TEXT NOT NULL, + interface TEXT NOT NULL DEFAULT '', + comment TEXT NOT NULL DEFAULT '' +); diff --git a/internal/model/nat.go b/internal/model/nat.go new file mode 100644 index 0000000..cbae38a --- /dev/null +++ b/internal/model/nat.go @@ -0,0 +1,34 @@ +package model + +// SNATRule is a source-NAT / masquerade intent. Source is a zone or CIDR; Egress +// is a zone (resolved per device to its egress interface via bindings). Address +// and Probability support load-balanced SNAT. +type SNATRule struct { + ID int64 `json:"id"` + Action string `json:"action"` // masquerade | snat + Source string `json:"source"` + Egress string `json:"egress"` + Address string `json:"address,omitempty"` + Probability *float64 `json:"probability,omitempty"` + Comment string `json:"comment,omitempty"` +} + +// NetmapRule maps one network to another, anchored at a device+zone or interface. +type NetmapRule struct { + ID int64 `json:"id"` + Type string `json:"type"` // dnat | snat + FromNet string `json:"from_net"` + ToNet string `json:"to_net"` + Anchor string `json:"anchor"` + Comment string `json:"comment,omitempty"` +} + +// NATRule is a one-to-one static NAT bound to the device owning the external IP. +type NATRule struct { + ID int64 `json:"id"` + Device string `json:"device"` + External string `json:"external"` + Internal string `json:"internal"` + Interface string `json:"interface,omitempty"` + Comment string `json:"comment,omitempty"` +} diff --git a/internal/server/nat.go b/internal/server/nat.go new file mode 100644 index 0000000..bd149b0 --- /dev/null +++ b/internal/server/nat.go @@ -0,0 +1,167 @@ +package server + +import ( + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + + "git.unkin.net/unkin/tomswallapi/internal/model" +) + +// mountNAT wires the NAT-tier resources (snat/masquerade, netmap, 1:1 nat). +func (s *Server) mountNAT(r chi.Router) { + r.Route("/snat", func(r chi.Router) { + r.Get("/", s.listSNAT) + r.Post("/", s.createSNAT) + r.Get("/{id}", s.getSNAT) + r.Delete("/{id}", s.deleteSNAT) + }) + r.Route("/netmap", func(r chi.Router) { + r.Get("/", s.listNetmap) + r.Post("/", s.createNetmap) + r.Get("/{id}", s.getNetmap) + r.Delete("/{id}", s.deleteNetmap) + }) + r.Route("/nat", func(r chi.Router) { + r.Get("/", s.listNAT) + r.Post("/", s.createNAT) + r.Get("/{id}", s.getNAT) + r.Delete("/{id}", s.deleteNAT) + }) +} + +func idParam(w http.ResponseWriter, r *http.Request) (int64, bool) { + id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + if err != nil { + writeError(w, http.StatusBadRequest, "id must be an integer") + return 0, false + } + return id, true +} + +// ---- SNAT ------------------------------------------------------------------ + +func (s *Server) listSNAT(w http.ResponseWriter, r *http.Request) { + list, err := s.store.ListSNAT(r.Context()) + respondList(w, list, err) +} + +func (s *Server) createSNAT(w http.ResponseWriter, r *http.Request) { + var v model.SNATRule + if !decode(w, r, &v) { + return + } + if v.Action != "masquerade" && v.Action != "snat" { + writeError(w, http.StatusBadRequest, "action must be masquerade or snat") + return + } + id, err := s.store.CreateSNAT(r.Context(), v) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + v.ID = id + writeJSON(w, http.StatusCreated, v) +} + +func (s *Server) getSNAT(w http.ResponseWriter, r *http.Request) { + id, ok := idParam(w, r) + if !ok { + return + } + v, err := s.store.GetSNAT(r.Context(), id) + respondOne(w, v, err) +} + +func (s *Server) deleteSNAT(w http.ResponseWriter, r *http.Request) { + id, ok := idParam(w, r) + if !ok { + return + } + respondDelete(w, s.store.DeleteSNAT(r.Context(), id)) +} + +// ---- Netmap ---------------------------------------------------------------- + +func (s *Server) listNetmap(w http.ResponseWriter, r *http.Request) { + list, err := s.store.ListNetmap(r.Context()) + respondList(w, list, err) +} + +func (s *Server) createNetmap(w http.ResponseWriter, r *http.Request) { + var v model.NetmapRule + if !decode(w, r, &v) { + return + } + if v.Type != "dnat" && v.Type != "snat" { + writeError(w, http.StatusBadRequest, "type must be dnat or snat") + return + } + id, err := s.store.CreateNetmap(r.Context(), v) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + v.ID = id + writeJSON(w, http.StatusCreated, v) +} + +func (s *Server) getNetmap(w http.ResponseWriter, r *http.Request) { + id, ok := idParam(w, r) + if !ok { + return + } + v, err := s.store.GetNetmap(r.Context(), id) + respondOne(w, v, err) +} + +func (s *Server) deleteNetmap(w http.ResponseWriter, r *http.Request) { + id, ok := idParam(w, r) + if !ok { + return + } + respondDelete(w, s.store.DeleteNetmap(r.Context(), id)) +} + +// ---- 1:1 NAT --------------------------------------------------------------- + +func (s *Server) listNAT(w http.ResponseWriter, r *http.Request) { + list, err := s.store.ListNAT(r.Context()) + respondList(w, list, err) +} + +func (s *Server) createNAT(w http.ResponseWriter, r *http.Request) { + var v model.NATRule + if !decode(w, r, &v) { + return + } + if v.Device == "" || v.External == "" || v.Internal == "" { + writeError(w, http.StatusBadRequest, "device, external, and internal are required") + return + } + id, err := s.store.CreateNAT(r.Context(), v) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + v.ID = id + writeJSON(w, http.StatusCreated, v) +} + +func (s *Server) getNAT(w http.ResponseWriter, r *http.Request) { + id, ok := idParam(w, r) + if !ok { + return + } + v, err := s.store.GetNAT(r.Context(), id) + respondOne(w, v, err) +} + +func (s *Server) deleteNAT(w http.ResponseWriter, r *http.Request) { + id, ok := idParam(w, r) + if !ok { + return + } + respondDelete(w, s.store.DeleteNAT(r.Context(), id)) +} diff --git a/internal/server/resources.go b/internal/server/resources.go index 0b52647..3bd6b7a 100644 --- a/internal/server/resources.go +++ b/internal/server/resources.go @@ -59,6 +59,7 @@ func (s *Server) mountResources(r chi.Router) { r.Get("/{id}", s.getRule) r.Delete("/{id}", s.deleteRule) }) + s.mountNAT(r) } // respondOne writes a single resource, mapping ErrNotFound to 404. diff --git a/internal/store/nat.go b/internal/store/nat.go new file mode 100644 index 0000000..a661074 --- /dev/null +++ b/internal/store/nat.go @@ -0,0 +1,160 @@ +package store + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + + "git.unkin.net/unkin/tomswallapi/internal/model" +) + +// ---- SNAT / masquerade ----------------------------------------------------- + +func (s *Store) ListSNAT(ctx context.Context) ([]model.SNATRule, error) { + rows, err := s.pool.Query(ctx, + `SELECT id, action, source, egress, address, probability, comment FROM snat ORDER BY id`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []model.SNATRule + for rows.Next() { + var r model.SNATRule + if err := rows.Scan(&r.ID, &r.Action, &r.Source, &r.Egress, &r.Address, &r.Probability, &r.Comment); err != nil { + return nil, err + } + out = append(out, r) + } + return out, rows.Err() +} + +func (s *Store) GetSNAT(ctx context.Context, id int64) (model.SNATRule, error) { + var r model.SNATRule + err := s.pool.QueryRow(ctx, + `SELECT id, action, source, egress, address, probability, comment FROM snat WHERE id = $1`, id, + ).Scan(&r.ID, &r.Action, &r.Source, &r.Egress, &r.Address, &r.Probability, &r.Comment) + if errors.Is(err, pgx.ErrNoRows) { + return r, ErrNotFound + } + return r, err +} + +func (s *Store) CreateSNAT(ctx context.Context, r model.SNATRule) (int64, error) { + var id int64 + err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error { + if err := tx.QueryRow(ctx, ` + INSERT INTO snat (action, source, egress, address, probability, comment) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, + r.Action, r.Source, r.Egress, r.Address, r.Probability, r.Comment, + ).Scan(&id); err != nil { + return err + } + return bump(ctx, tx) + }) + return id, err +} + +func (s *Store) DeleteSNAT(ctx context.Context, id int64) error { + return s.deleteOne(ctx, `DELETE FROM snat WHERE id = $1`, id) +} + +// ---- Netmap ---------------------------------------------------------------- + +func (s *Store) ListNetmap(ctx context.Context) ([]model.NetmapRule, error) { + rows, err := s.pool.Query(ctx, + `SELECT id, type, from_net, to_net, anchor, comment FROM netmap ORDER BY id`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []model.NetmapRule + for rows.Next() { + var r model.NetmapRule + if err := rows.Scan(&r.ID, &r.Type, &r.FromNet, &r.ToNet, &r.Anchor, &r.Comment); err != nil { + return nil, err + } + out = append(out, r) + } + return out, rows.Err() +} + +func (s *Store) GetNetmap(ctx context.Context, id int64) (model.NetmapRule, error) { + var r model.NetmapRule + err := s.pool.QueryRow(ctx, + `SELECT id, type, from_net, to_net, anchor, comment FROM netmap WHERE id = $1`, id, + ).Scan(&r.ID, &r.Type, &r.FromNet, &r.ToNet, &r.Anchor, &r.Comment) + if errors.Is(err, pgx.ErrNoRows) { + return r, ErrNotFound + } + return r, err +} + +func (s *Store) CreateNetmap(ctx context.Context, r model.NetmapRule) (int64, error) { + var id int64 + err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error { + if err := tx.QueryRow(ctx, ` + INSERT INTO netmap (type, from_net, to_net, anchor, comment) + VALUES ($1, $2, $3, $4, $5) RETURNING id`, + r.Type, r.FromNet, r.ToNet, r.Anchor, r.Comment, + ).Scan(&id); err != nil { + return err + } + return bump(ctx, tx) + }) + return id, err +} + +func (s *Store) DeleteNetmap(ctx context.Context, id int64) error { + return s.deleteOne(ctx, `DELETE FROM netmap WHERE id = $1`, id) +} + +// ---- 1:1 static NAT -------------------------------------------------------- + +func (s *Store) ListNAT(ctx context.Context) ([]model.NATRule, error) { + rows, err := s.pool.Query(ctx, + `SELECT id, device, external, internal, interface, comment FROM nat ORDER BY id`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []model.NATRule + for rows.Next() { + var r model.NATRule + if err := rows.Scan(&r.ID, &r.Device, &r.External, &r.Internal, &r.Interface, &r.Comment); err != nil { + return nil, err + } + out = append(out, r) + } + return out, rows.Err() +} + +func (s *Store) GetNAT(ctx context.Context, id int64) (model.NATRule, error) { + var r model.NATRule + err := s.pool.QueryRow(ctx, + `SELECT id, device, external, internal, interface, comment FROM nat WHERE id = $1`, id, + ).Scan(&r.ID, &r.Device, &r.External, &r.Internal, &r.Interface, &r.Comment) + if errors.Is(err, pgx.ErrNoRows) { + return r, ErrNotFound + } + return r, err +} + +func (s *Store) CreateNAT(ctx context.Context, r model.NATRule) (int64, error) { + var id int64 + err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error { + if err := tx.QueryRow(ctx, ` + INSERT INTO nat (device, external, internal, interface, comment) + VALUES ($1, $2, $3, $4, $5) RETURNING id`, + r.Device, r.External, r.Internal, r.Interface, r.Comment, + ).Scan(&id); err != nil { + return err + } + return bump(ctx, tx) + }) + return id, err +} + +func (s *Store) DeleteNAT(ctx context.Context, id int64) error { + return s.deleteOne(ctx, `DELETE FROM nat WHERE id = $1`, id) +} diff --git a/internal/store/store_integration_test.go b/internal/store/store_integration_test.go new file mode 100644 index 0000000..e6d3219 --- /dev/null +++ b/internal/store/store_integration_test.go @@ -0,0 +1,151 @@ +package store_test + +import ( + "context" + "testing" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + + "git.unkin.net/unkin/tomswallapi/internal/database" + "git.unkin.net/unkin/tomswallapi/internal/model" + "git.unkin.net/unkin/tomswallapi/internal/store" +) + +// newTestStore spins up a throwaway Postgres, applies migrations, and returns a +// Store. It skips (rather than fails) when Docker is unavailable, so the default +// `go test -short` run in CI passes without a container runtime. +func newTestStore(t *testing.T) *store.Store { + t.Helper() + if testing.Short() { + t.Skip("skipping container-backed test in -short mode") + } + ctx := context.Background() + pg, err := postgres.Run(ctx, "postgres:17-alpine", + postgres.WithDatabase("tomswallapi"), + postgres.WithUsername("tomswallapi"), + postgres.WithPassword("tomswallapi"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2).WithStartupTimeout(60*time.Second)), + ) + if err != nil { + t.Skipf("skipping: cannot start postgres container (Docker unavailable?): %v", err) + } + t.Cleanup(func() { _ = pg.Terminate(ctx) }) + + dsn, err := pg.ConnectionString(ctx, "sslmode=disable") + if err != nil { + t.Fatalf("connection string: %v", err) + } + db, err := database.New(ctx, dsn) + if err != nil { + t.Fatalf("connect: %v", err) + } + t.Cleanup(db.Close) + if err := db.Migrate(ctx); err != nil { + t.Fatalf("migrate: %v", err) + } + return store.New(db.Pool) +} + +func TestZoneLifecycleAndGeneration(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + gen0, err := s.Generation(ctx) + if err != nil { + t.Fatalf("generation: %v", err) + } + + if err := s.UpsertZone(ctx, model.Zone{Name: "zone-a", Type: "ip", Subnets: []string{"10.1.0.0/24"}}); err != nil { + t.Fatalf("upsert zone: %v", err) + } + gen1, _ := s.Generation(ctx) + if gen1 <= gen0 { + t.Errorf("generation should bump on write: %d -> %d", gen0, gen1) + } + + got, err := s.GetZone(ctx, "zone-a") + if err != nil { + t.Fatalf("get zone: %v", err) + } + if len(got.Subnets) != 1 || got.Subnets[0] != "10.1.0.0/24" { + t.Errorf("subnets round-trip failed: %+v", got) + } + + zones, err := s.ListZones(ctx) + if err != nil || len(zones) != 1 { + t.Fatalf("list zones = %d (%v)", len(zones), err) + } + + if err := s.DeleteZone(ctx, "zone-a"); err != nil { + t.Fatalf("delete zone: %v", err) + } + if _, err := s.GetZone(ctx, "zone-a"); err != store.ErrNotFound { + t.Errorf("expected ErrNotFound after delete, got %v", err) + } + gen2, _ := s.Generation(ctx) + if gen2 <= gen1 { + t.Errorf("generation should bump on delete: %d -> %d", gen1, gen2) + } +} + +func TestRuleGrammarValidationAtStore(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + // Valid rule with a paired selector. + if _, err := s.CreateRule(ctx, model.Rule{ + Action: "accept", Source: []string{"loc"}, Dest: []string{"net:+cloudflare"}, + }); err != nil { + t.Fatalf("valid rule rejected: %v", err) + } + // Bare selector must be rejected before it reaches the DB. + if _, err := s.CreateRule(ctx, model.Rule{ + Action: "accept", Source: []string{"+cloudflare"}, Dest: []string{"loc"}, + }); err == nil { + t.Fatal("bare selector rule should be rejected") + } +} + +func TestNATTierRoundTrip(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + // nat references a device via FK. + if err := s.UpsertDevice(ctx, model.Device{Name: "fw-a", Class: model.ClassFirewall}); err != nil { + t.Fatalf("upsert device: %v", err) + } + + sid, err := s.CreateSNAT(ctx, model.SNATRule{Action: "masquerade", Source: "loc", Egress: "net"}) + if err != nil { + t.Fatalf("create snat: %v", err) + } + if got, err := s.GetSNAT(ctx, sid); err != nil || got.Action != "masquerade" { + t.Errorf("snat round-trip: %+v (%v)", got, err) + } + + nid, err := s.CreateNAT(ctx, model.NATRule{Device: "fw-a", External: "203.0.113.10", Internal: "10.1.0.10"}) + if err != nil { + t.Fatalf("create nat: %v", err) + } + if err := s.DeleteNAT(ctx, nid); err != nil { + t.Fatalf("delete nat: %v", err) + } + if err := s.DeleteNAT(ctx, nid); err != store.ErrNotFound { + t.Errorf("second delete should be ErrNotFound, got %v", err) + } + + // Deleting the device cascades to its nat rows. + _, _ = s.CreateNAT(ctx, model.NATRule{Device: "fw-a", External: "203.0.113.11", Internal: "10.1.0.11"}) + if err := s.DeleteDevice(ctx, "fw-a"); err != nil { + t.Fatalf("delete device: %v", err) + } + nats, _ := s.ListNAT(ctx) + if len(nats) != 0 { + t.Errorf("expected nat rows to cascade-delete with device, got %d", len(nats)) + } +}