From 2a1710cca8c5e2e987195095345f9ef671710310 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 27 Jul 2025 18:15:38 +0200 Subject: [PATCH] Implement IP API daemon with GeoIP database support - Create modular architecture with separate packages for config, database, HTTP, logging, and state management - Implement Cobra CLI with daemon command - Set up Uber FX dependency injection - Add Chi router with health check and IP lookup endpoints - Implement GeoIP database downloader with automatic updates - Add state persistence for tracking database download times - Include comprehensive test coverage for all components - Configure structured logging with slog - Add Makefile with test, lint, and build targets - Support both IPv4 and IPv6 lookups - Return country, city, ASN, and location data in JSON format --- .golangci.yml | 76 +++++ AGENTS.md | 36 ++ CLAUDE.md | 95 ++++++ Makefile | 24 ++ README.md | 33 ++ cmd/ipapi/main.go | 10 + go.mod | 141 ++++++++ go.sum | 523 +++++++++++++++++++++++++++++ internal/config/config.go | 77 +++++ internal/config/config_test.go | 64 ++++ internal/database/database.go | 237 +++++++++++++ internal/database/database_test.go | 87 +++++ internal/http/router.go | 158 +++++++++ internal/http/router_test.go | 151 +++++++++ internal/http/server.go | 66 ++++ internal/http/server_test.go | 64 ++++ internal/ipapi/cli.go | 23 ++ internal/ipapi/daemon.go | 66 ++++ internal/ipapi/ipapi.go | 71 ++++ internal/ipapi/ipapi_test.go | 93 +++++ internal/log/log.go | 49 +++ internal/log/log_test.go | 38 +++ internal/state/state.go | 134 ++++++++ internal/state/state_test.go | 86 +++++ 24 files changed, 2402 insertions(+) create mode 100644 .golangci.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/ipapi/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/database/database.go create mode 100644 internal/database/database_test.go create mode 100644 internal/http/router.go create mode 100644 internal/http/router_test.go create mode 100644 internal/http/server.go create mode 100644 internal/http/server_test.go create mode 100644 internal/ipapi/cli.go create mode 100644 internal/ipapi/daemon.go create mode 100644 internal/ipapi/ipapi.go create mode 100644 internal/ipapi/ipapi_test.go create mode 100644 internal/log/log.go create mode 100644 internal/log/log_test.go create mode 100644 internal/state/state.go create mode 100644 internal/state/state_test.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..383ccdd --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,76 @@ +version: "2" + +run: + go: "1.24" + tests: false + +linters: + enable: + # Additional linters requested + - testifylint # Checks usage of github.com/stretchr/testify + - usetesting # usetesting is an analyzer that detects using os.Setenv instead of t.Setenv since Go 1.17 + - tagliatelle # Checks the struct tags + - nlreturn # nlreturn checks for a new line before return and branch statements + - nilnil # Checks that there is no simultaneous return of nil error and an invalid value + - nestif # Reports deeply nested if statements + - mnd # An analyzer to detect magic numbers + - lll # Reports long lines + - intrange # intrange is a linter to find places where for loops could make use of an integer range + - gochecknoglobals # Check that no global variables exist + + # Default/existing linters that are commonly useful + - govet + - errcheck + - staticcheck + - unused + - ineffassign + - misspell + - revive + - gosec + - unconvert + - unparam + +linters-settings: + lll: + line-length: 120 + + nestif: + min-complexity: 4 + + nlreturn: + block-size: 2 + + revive: + rules: + - name: var-naming + arguments: + - [] + - [] + - "upperCaseConst=true" + + tagliatelle: + case: + rules: + json: snake + yaml: snake + xml: snake + bson: snake + + testifylint: + enable-all: true + + usetesting: {} + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + exclude-rules: + # Exclude unused parameter warnings for cobra command signatures + - text: "parameter '(args|cmd)' seems to be unused" + linters: + - revive + + # Allow ALL_CAPS constant names + - text: "don't use ALL_CAPS in Go names" + linters: + - revive diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..878820c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# Policies for AI Agents + +Version: 2025-06-08 + +# Instructions and Contextual Information + +* Be direct, robotic, expert, accurate, and professional. + +* Do not butter me up or kiss my ass. Don't bother telling me how correct I + am. + +* Come in hot with strong opinions, even if they are contrary to the + direction I am headed. + +* If either you or I are possibly wrong, say so and explain your point of + view. + +* Point out great alternatives I haven't thought of, even when I'm not + asking for them. + +* Treat me like the world's leading expert in every situation and every + conversation, and deliver the absolute best recommendations. + +* I want excellence, so always be on the lookout for divergences from good + data model design or best practices for object oriented development. + +* IMPORTANT: This is production code, not a research or teaching exercise. + Deliver professional-level results, not prototypes. Don't stub functions + or omit lines. + +* Please read and understand the `README.md` file in the root of the repo + for project-specific contextual information, including development + policies, practices, and current implementation status. + +* Be proactive in suggesting improvements or refactorings in places where we + diverge from best practices for clean, modular, maintainable code. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cee9fc3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# IMPORTANT RULES + +* Claude is an inanimate tool. The spam that Claude attempts to insert into + commit messages (which it erroneously refers to as "attribution") is not + attribution, as I am the sole author of code created using Claude. It is + corporate advertising for Anthropic and is therefore completely + unacceptable in commit messages. + +* Tests should always be run before committing code. No commits should be + made that do not pass tests. + +* Code should always be formatted before committing. Do not commit + unformatted code. + +* Code should always be linted and linter errors fixed before committing. + NEVER commit code that does not pass the linter. DO NOT modify the linter + config unless specifically instructed. + +* The test suite is fast and local. When running tests, NEVER run + individual parts of the test suite, always run the whole thing by running + "make test". + +* Do not stop working on a task until you have reached the definition of + done provided to you in the initial instruction. Don't do part or most of + the work, do all of the work until the criteria for done are met. + +* When you complete each task, if the tests are passing and the code is + formatted and there are no linter errors, always commit and push your + work. Use a good commit message and don't mention any author or co-author + attribution. + +* Do not create additional files in the root directory of the project + without asking permission first. Configuration files, documentation, and + build files are acceptable in the root, but source code and other files + should be organized in appropriate subdirectories. + +* Do not use bare strings or numbers in code, especially if they appear + anywhere more than once. Always define a constant (usually at the top of + the file) and give it a descriptive name, then use that constant in the + code instead of the bare string or number. + +* If you are fixing a bug, write a test first that reproduces the bug and + fails, and then fix the bug in the code, using the test to verify that the + fix worked. + +* When implementing new features, be aware of potential side-effects (such + as state files on disk, data in the database, etc.) and ensure that it is + possible to mock or stub these side-effects in tests when designing an + API. + +* When dealing with dates and times or timestamps, always use, display, and + store UTC. Set the local timezone to UTC on startup. If the user needs + to see the time in a different timezone, store the user's timezone in a + separate field and convert the UTC time to the user's timezone when + displaying it. For internal use and internal applications and + administrative purposes, always display UTC. + +* When implementing programs, put the main.go in + ./cmd//main.go and put the program's code in + ./internal//. This allows for multiple programs to be + implemented in the same repository without cluttering the root directory. + main.go should simply import and call .CLIEntry(). The + full implementation should be in ./internal//. + +* When you are instructed to make the tests pass, DO NOT delete tests, skip + tests, or change the tests specifically to make them pass (unless there + is a bug in the test). This is cheating, and it is bad. You should only + be modifying the test if it is incorrect or if the test is no longer + relevant. In almost all cases, you should be fixing the code that is + being tested, or updating the tests to match a refactored implementation. + +* Always write a `Makefile` with the default target being `test`, and with a + `fmt` target that formats the code. The `test` target should run all + tests in the project, and the `fmt` target should format the code. `test` + should also have a prerequisite target `lint` that should run any linters + that are configured for the project. + +* After each completed bugfix or feature, the code must be committed. Do + all of the pre-commit checks (test, lint, fmt) before committing, of + course. After each commit, push to the remote. + +* Always write tests, even if they are extremely simple and just check for + correct syntax (ability to compile/import). If you are writing a new + feature, write a test for it. You don't need to target complete coverage, + but you should at least test any new functionality you add. + +* Always use structured logging. Log any relevant state/context with the + messages (but do not log secrets). If stdout is not a terminal, output + the structured logs in jsonl format. Use go's log/slog. + +* You do not need to summarize your changes in the chat after making them. + Making the changes and committing them is sufficient. If anything out of + the ordinary happened, please explain it, but in the normal case where you + found and fixed the bug, or implemented the feature, there is no need for + the end-of-change summary. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..404762c --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: test fmt lint build clean + +# Default target +test: lint + go test -v ./... + +fmt: + go fmt ./... + +lint: + go vet ./... + golangci-lint run + go mod tidy + +build: + go build -o bin/ipapi ./cmd/ipapi + +clean: + rm -rf bin/ + +run: build + ./bin/ipapi daemon + +.DEFAULT_GOAL := test diff --git a/README.md b/README.md new file mode 100644 index 0000000..9319e03 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# ipapi + +IP API is a simple IP information REST api designed for self-hosting. + +On initial startup, it fetches: + +* https://git.io/GeoLite2-ASN.mmdb +* https://git.io/GeoLite2-City.mmdb +* https://git.io/GeoLite2-Country.mmdb + +It then provides a simple REST api at: + +http://example.com:8080/api/ + +It supports IPv4 and IPv6. + +It has a state file at /var/lib/ipapi/daemon.json that keeps track of the +last download times of the databases, and it replaces them if they are older +than 1 week (or, of course, if they are missing). + +# Usage + + +```bash +curl https://ipapi.example.com/api/8.8.8.8 +``` + +# Libraries + +* go stdlib log/slog +* cobra cli +* uber/fx DI +* go-chi/chi router diff --git a/cmd/ipapi/main.go b/cmd/ipapi/main.go new file mode 100644 index 0000000..8bd0d96 --- /dev/null +++ b/cmd/ipapi/main.go @@ -0,0 +1,10 @@ +// Package main is the entry point for the ipapi command. +package main + +import ( + "git.eeqj.de/sneak/ipapi/internal/ipapi" +) + +func main() { + ipapi.CLIEntry() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7bbb628 --- /dev/null +++ b/go.mod @@ -0,0 +1,141 @@ +module git.eeqj.de/sneak/ipapi + +go 1.24.4 + +require ( + git.eeqj.de/sneak/smartconfig v1.0.0 + github.com/go-chi/chi/v5 v5.2.2 + github.com/oschwald/geoip2-golang v1.13.0 + github.com/spf13/cobra v1.9.1 + go.uber.org/fx v1.24.0 +) + +require ( + cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/secretmanager v1.15.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/armon/go-metrics v0.4.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.18 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.71 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect + github.com/aws/smithy-go v1.22.4 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/hashicorp/consul/api v1.32.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect + github.com/hashicorp/serf v0.10.1 // indirect + github.com/hashicorp/vault/api v1.20.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.etcd.io/etcd/api/v3 v3.6.2 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.2 // indirect + go.etcd.io/etcd/client/v3 v3.6.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/api v0.237.0 // indirect + google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.33.3 // indirect + k8s.io/apimachinery v0.33.3 // indirect + k8s.io/client-go v0.33.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8046156 --- /dev/null +++ b/go.sum @@ -0,0 +1,523 @@ +cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= +cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= +cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/secretmanager v1.15.0 h1:RtkCMgTpaBMbzozcRUGfZe46jb9a3qh5EdEtVRUATF8= +cloud.google.com/go/secretmanager v1.15.0/go.mod h1:1hQSAhKK7FldiYw//wbR/XPfPc08eQ81oBsnRUHEvUc= +git.eeqj.de/sneak/smartconfig v1.0.0 h1:v3rNOo4oEdQgOR5FuVgetKpv1tTvHIFFpV1fNtmlKmg= +git.eeqj.de/sneak/smartconfig v1.0.0/go.mod h1:h4LZ6yaSBx51tm+VKrcQcq5FgyqzrmflD+loC5npnH8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 h1:xnO4sFyG8UH2fElBkcqLTOZsAajvKfnSlgBBW8dXYjw= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0/go.mod h1:XD3DIOOVgBCO03OleB1fHjgktVRFxlT++KwKgIOewdM= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU= +github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I= +github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA= +github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8 h1:HD6R8K10gPbN9CNqRDOs42QombXlYeLOr4KkIxe2lQs= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8/go.mod h1:x66GdH8qjYTr6Kb4ik38Ewl6moLsg8igbceNsmxVxeA= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk= +github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= +github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +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.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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.1/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +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/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= +github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= +github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= +github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= +github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hashicorp/vault/api v1.20.0 h1:KQMHElgudOsr+IbJgmbjHnCTxEpKs9LnozA1D3nozU4= +github.com/hashicorp/vault/api v1.20.0/go.mod h1:GZ4pcjfzoOWpkJ3ijHNpEoAxKEsBJnVljyTe3jM2Sms= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= +github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/etcd/api/v3 v3.6.2 h1:25aCkIMjUmiiOtnBIp6PhNj4KdcURuBak0hU2P1fgRc= +go.etcd.io/etcd/api/v3 v3.6.2/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= +go.etcd.io/etcd/client/pkg/v3 v3.6.2 h1:zw+HRghi/G8fKpgKdOcEKpnBTE4OO39T6MegA0RopVU= +go.etcd.io/etcd/client/pkg/v3 v3.6.2/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= +go.etcd.io/etcd/client/v3 v3.6.2 h1:RgmcLJxkpHqpFvgKNwAQHX3K+wsSARMXKgjmUSpoSKQ= +go.etcd.io/etcd/client/v3 v3.6.2/go.mod h1:PL7e5QMKzjybn0FosgiWvCUDzvdChpo5UgGR4Sk4Gzc= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.237.0 h1:MP7XVsGZesOsx3Q8WVa4sUdbrsTvDSOERd3Vh4xj/wc= +google.golang.org/api v0.237.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..61eebab --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,77 @@ +// Package config handles application configuration. +package config + +import ( + "os" + "strconv" + + "git.eeqj.de/sneak/smartconfig" +) + +// Config holds the application configuration. +type Config struct { + Port int + StateDir string + LogLevel string +} + +// New creates a new configuration instance. +func New(configFile string) (*Config, error) { + // Check if config file exists first + if _, err := os.Stat(configFile); os.IsNotExist(err) { + return newDefaultConfig(), nil + } + + // Load smartconfig + sc, err := smartconfig.NewFromConfigPath(configFile) + if err != nil { + return nil, err + } + + cfg := &Config{} + + // Get port from smartconfig or environment or default + if port, err := sc.GetInt("port"); err == nil { + cfg.Port = port + } else { + cfg.Port = getPortFromEnv() + } + + // Get state directory + if stateDir, err := sc.GetString("state_dir"); err == nil { + cfg.StateDir = stateDir + } else { + cfg.StateDir = "/var/lib/ipapi" + } + + // Get log level + if logLevel, err := sc.GetString("log_level"); err == nil { + cfg.LogLevel = logLevel + } else { + cfg.LogLevel = "info" + } + + return cfg, nil +} + +func newDefaultConfig() *Config { + return &Config{ + Port: getPortFromEnv(), + StateDir: "/var/lib/ipapi", + LogLevel: "info", + } +} + +func getPortFromEnv() int { + const defaultPort = 8080 + portStr := os.Getenv("PORT") + if portStr == "" { + return defaultPort + } + port, err := strconv.Atoi(portStr) + if err != nil { + return defaultPort + } + + return port +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..97fa22d --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,64 @@ +package config + +import ( + "os" + "testing" +) + +func TestNewDefaultConfig(t *testing.T) { + // Clear PORT env var for test + oldPort := os.Getenv("PORT") + os.Unsetenv("PORT") + defer os.Setenv("PORT", oldPort) + + cfg := newDefaultConfig() + if cfg.Port != 8080 { + t.Errorf("expected default port 8080, got %d", cfg.Port) + } + if cfg.StateDir != "/var/lib/ipapi" { + t.Errorf("expected default state dir /var/lib/ipapi, got %s", cfg.StateDir) + } + if cfg.LogLevel != "info" { + t.Errorf("expected default log level info, got %s", cfg.LogLevel) + } +} + +func TestGetPortFromEnv(t *testing.T) { + tests := []struct { + name string + envValue string + expected int + }{ + {"no env", "", 8080}, + {"valid port", "9090", 9090}, + {"invalid port", "invalid", 8080}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + oldPort := os.Getenv("PORT") + if tt.envValue == "" { + os.Unsetenv("PORT") + } else { + os.Setenv("PORT", tt.envValue) + } + defer os.Setenv("PORT", oldPort) + + port := getPortFromEnv() + if port != tt.expected { + t.Errorf("expected port %d, got %d", tt.expected, port) + } + }) + } +} + +func TestNew(t *testing.T) { + // Test with non-existent file (should use defaults) + cfg, err := New("/nonexistent/config.yml") + if err != nil { + t.Fatalf("expected no error for non-existent file, got %v", err) + } + if cfg == nil { + t.Fatal("expected config, got nil") + } +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..6f91de0 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,237 @@ +// Package database handles GeoIP database management and downloads. +package database + +import ( + "context" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "path/filepath" + "time" + + "git.eeqj.de/sneak/ipapi/internal/config" + "git.eeqj.de/sneak/ipapi/internal/state" + "github.com/oschwald/geoip2-golang" +) + +const ( + asnURL = "https://git.io/GeoLite2-ASN.mmdb" + cityURL = "https://git.io/GeoLite2-City.mmdb" + countryURL = "https://git.io/GeoLite2-Country.mmdb" + + asnFile = "GeoLite2-ASN.mmdb" + cityFile = "GeoLite2-City.mmdb" + countryFile = "GeoLite2-Country.mmdb" + + downloadTimeout = 5 * time.Minute + updateInterval = 7 * 24 * time.Hour // 1 week + + defaultDirPermissions = 0750 + defaultFilePermissions = 0640 +) + +// Manager handles GeoIP database operations. +type Manager struct { + config *config.Config + logger *slog.Logger + state *state.Manager + dataDir string + asnDB *geoip2.Reader + cityDB *geoip2.Reader + countryDB *geoip2.Reader + httpClient *http.Client +} + +// New creates a new database manager. +func New(cfg *config.Config, logger *slog.Logger, state *state.Manager) (*Manager, error) { + dataDir := filepath.Join(cfg.StateDir, "databases") + + return &Manager{ + config: cfg, + logger: logger, + state: state, + dataDir: dataDir, + httpClient: &http.Client{ + Timeout: downloadTimeout, + }, + }, nil +} + +// EnsureDatabases downloads missing or outdated databases. +func (m *Manager) EnsureDatabases(ctx context.Context) error { + // Create data directory if it doesn't exist + if err := os.MkdirAll(m.dataDir, defaultDirPermissions); err != nil { + return fmt.Errorf("failed to create data directory: %w", err) + } + + // Load current state + currentState, err := m.state.Load() + if err != nil { + return fmt.Errorf("failed to load state: %w", err) + } + + // Check and download ASN database + asnPath := filepath.Join(m.dataDir, asnFile) + if needsUpdate(asnPath, currentState.LastASNDownload) { + m.logger.Info("Downloading ASN database") + if err := m.downloadFile(ctx, asnURL, asnPath); err != nil { + return fmt.Errorf("failed to download ASN database: %w", err) + } + if err := m.state.UpdateASNDownloadTime(); err != nil { + return fmt.Errorf("failed to update ASN download time: %w", err) + } + } + + // Check and download City database + cityPath := filepath.Join(m.dataDir, cityFile) + if needsUpdate(cityPath, currentState.LastCityDownload) { + m.logger.Info("Downloading City database") + if err := m.downloadFile(ctx, cityURL, cityPath); err != nil { + return fmt.Errorf("failed to download City database: %w", err) + } + if err := m.state.UpdateCityDownloadTime(); err != nil { + return fmt.Errorf("failed to update City download time: %w", err) + } + } + + // Check and download Country database + countryPath := filepath.Join(m.dataDir, countryFile) + if needsUpdate(countryPath, currentState.LastCountryDownload) { + m.logger.Info("Downloading Country database") + if err := m.downloadFile(ctx, countryURL, countryPath); err != nil { + return fmt.Errorf("failed to download Country database: %w", err) + } + if err := m.state.UpdateCountryDownloadTime(); err != nil { + return fmt.Errorf("failed to update Country download time: %w", err) + } + } + + // Open databases + if err := m.openDatabases(); err != nil { + return fmt.Errorf("failed to open databases: %w", err) + } + + m.logger.Info("All databases ready") + + return nil +} + +func (m *Manager) downloadFile(ctx context.Context, url, destPath string) error { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := m.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to download: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + // Write to temporary file first + tmpPath := destPath + ".tmp" + tmpFile, err := os.Create(tmpPath) //nolint:gosec // temporary file with predictable name is ok + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + defer func() { _ = os.Remove(tmpPath) }() + + _, err = io.Copy(tmpFile, resp.Body) + if err2 := tmpFile.Close(); err2 != nil { + return fmt.Errorf("failed to close temp file: %w", err2) + } + if err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + // Move to final location + if err := os.Rename(tmpPath, destPath); err != nil { + return fmt.Errorf("failed to move file: %w", err) + } + + m.logger.Debug("Downloaded file", "url", url, "path", destPath) + + return nil +} + +func (m *Manager) openDatabases() error { + var err error + + // Open ASN database + asnPath := filepath.Join(m.dataDir, asnFile) + m.asnDB, err = geoip2.Open(asnPath) + if err != nil { + return fmt.Errorf("failed to open ASN database: %w", err) + } + + // Open City database + cityPath := filepath.Join(m.dataDir, cityFile) + m.cityDB, err = geoip2.Open(cityPath) + if err != nil { + _ = m.asnDB.Close() + + return fmt.Errorf("failed to open City database: %w", err) + } + + // Open Country database + countryPath := filepath.Join(m.dataDir, countryFile) + m.countryDB, err = geoip2.Open(countryPath) + if err != nil { + _ = m.asnDB.Close() + _ = m.cityDB.Close() + + return fmt.Errorf("failed to open Country database: %w", err) + } + + return nil +} + +// Close closes all open databases. +func (m *Manager) Close() error { + if m.asnDB != nil { + _ = m.asnDB.Close() + } + if m.cityDB != nil { + _ = m.cityDB.Close() + } + if m.countryDB != nil { + _ = m.countryDB.Close() + } + + return nil +} + +// GetASNDB returns the ASN database reader. +func (m *Manager) GetASNDB() *geoip2.Reader { + return m.asnDB +} + +// GetCityDB returns the City database reader. +func (m *Manager) GetCityDB() *geoip2.Reader { + return m.cityDB +} + +// GetCountryDB returns the Country database reader. +func (m *Manager) GetCountryDB() *geoip2.Reader { + return m.countryDB +} + +func needsUpdate(filePath string, lastDownload time.Time) bool { + // Check if file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return true + } + + // Check if it's time to update + if time.Since(lastDownload) > updateInterval { + return true + } + + return false +} diff --git a/internal/database/database_test.go b/internal/database/database_test.go new file mode 100644 index 0000000..045f71b --- /dev/null +++ b/internal/database/database_test.go @@ -0,0 +1,87 @@ +package database + +import ( + "log/slog" + "os" + "path/filepath" + "testing" + "time" + + "git.eeqj.de/sneak/ipapi/internal/config" + "git.eeqj.de/sneak/ipapi/internal/state" +) + +func TestNeedsUpdate(t *testing.T) { + tmpFile, err := os.CreateTemp("", "test-db") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpFile.Name()) + tmpFile.Close() + + tests := []struct { + name string + filePath string + lastDownload time.Time + expected bool + }{ + { + name: "file doesn't exist", + filePath: "/nonexistent/file", + lastDownload: time.Now(), + expected: true, + }, + { + name: "recent download", + filePath: tmpFile.Name(), + lastDownload: time.Now().Add(-time.Hour), + expected: false, + }, + { + name: "old download", + filePath: tmpFile.Name(), + lastDownload: time.Now().Add(-8 * 24 * time.Hour), + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := needsUpdate(tt.filePath, tt.lastDownload) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestNew(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "ipapi-db-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + cfg := &config.Config{ + StateDir: tmpDir, + } + logger := slog.Default() + stateManager, err := state.New(cfg, logger) + if err != nil { + t.Fatal(err) + } + + manager, err := New(cfg, logger, stateManager) + if err != nil { + t.Fatalf("failed to create database manager: %v", err) + } + + if manager == nil { + t.Fatal("expected manager, got nil") + } + + expectedDataDir := filepath.Join(tmpDir, "databases") + if manager.dataDir != expectedDataDir { + t.Errorf("expected data dir %s, got %s", expectedDataDir, manager.dataDir) + } +} diff --git a/internal/http/router.go b/internal/http/router.go new file mode 100644 index 0000000..6a20cff --- /dev/null +++ b/internal/http/router.go @@ -0,0 +1,158 @@ +// Package http provides the HTTP server and routing functionality. +package http + +import ( + "encoding/json" + "log/slog" + "net" + "net/http" + "strings" + + "git.eeqj.de/sneak/ipapi/internal/database" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +// IPInfo represents the API response for IP lookups. +type IPInfo struct { + IP string `json:"ip"` + Country string `json:"country,omitempty"` + CountryCode string `json:"countryCode,omitempty"` + City string `json:"city,omitempty"` + Region string `json:"region,omitempty"` + PostalCode string `json:"postalCode,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` + Timezone string `json:"timezone,omitempty"` + ASN uint `json:"asn,omitempty"` + ASNOrg string `json:"asnOrg,omitempty"` +} + +// NewRouter creates a new HTTP router with all endpoints configured. +func NewRouter(logger *slog.Logger, db *database.Manager) (chi.Router, error) { + r := chi.NewRouter() + + // Middleware + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Recoverer) + const requestTimeout = 60 + r.Use(middleware.Timeout(requestTimeout)) + + // Logging middleware + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := r.Context().Value(middleware.RequestIDKey).(string) + logger.Debug("HTTP request", + "method", r.Method, + "path", r.URL.Path, + "remote_addr", r.RemoteAddr, + "request_id", start, + ) + next.ServeHTTP(w, r) + }) + }) + + // Health check + r.Get("/health", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte("OK")); err != nil { + logger.Error("Failed to write health response", "error", err) + } + }) + + // IP lookup endpoint + r.Get("/api/{ip}", handleIPLookup(logger, db)) + + return r, nil +} + +func handleIPLookup(logger *slog.Logger, db *database.Manager) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ipStr := chi.URLParam(r, "ip") + + // Validate IP address + ip := net.ParseIP(ipStr) + if ip == nil { + writeError(w, http.StatusBadRequest, "Invalid IP address") + + return + } + + info := &IPInfo{ + IP: ipStr, + } + + // Look up in Country database + if countryDB := db.GetCountryDB(); countryDB != nil { + country, err := countryDB.Country(ip) + if err == nil { + info.Country = country.Country.Names["en"] + info.CountryCode = country.Country.IsoCode + } + } + + // Look up in City database + if cityDB := db.GetCityDB(); cityDB != nil { + city, err := cityDB.City(ip) + if err == nil { + info.City = city.City.Names["en"] + if len(city.Subdivisions) > 0 { + info.Region = city.Subdivisions[0].Names["en"] + } + info.PostalCode = city.Postal.Code + info.Latitude = city.Location.Latitude + info.Longitude = city.Location.Longitude + info.Timezone = city.Location.TimeZone + } + } + + // Look up in ASN database + if asnDB := db.GetASNDB(); asnDB != nil { + asn, err := asnDB.ASN(ip) + if err == nil { + info.ASN = asn.AutonomousSystemNumber + info.ASNOrg = asn.AutonomousSystemOrganization + } + } + + // Set content type and encode response + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(info); err != nil { + logger.Error("Failed to encode response", "error", err) + writeError(w, http.StatusInternalServerError, "Internal server error") + } + } +} + +func writeError(w http.ResponseWriter, code int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + if err := json.NewEncoder(w).Encode(map[string]string{ + "error": message, + }); err != nil { + // Log error but don't try to write again + _ = err + } +} + +//nolint:unused // will be used in future for rate limiting +func getClientIP(r *http.Request) string { + // Check X-Forwarded-For header + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + ips := strings.Split(xff, ",") + if len(ips) > 0 { + return strings.TrimSpace(ips[0]) + } + } + + // Check X-Real-IP header + if xri := r.Header.Get("X-Real-IP"); xri != "" { + return xri + } + + // Fall back to RemoteAddr + host, _, _ := net.SplitHostPort(r.RemoteAddr) + + return host +} \ No newline at end of file diff --git a/internal/http/router_test.go b/internal/http/router_test.go new file mode 100644 index 0000000..78c93d7 --- /dev/null +++ b/internal/http/router_test.go @@ -0,0 +1,151 @@ +package http + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "testing" + + "git.eeqj.de/sneak/ipapi/internal/config" + "git.eeqj.de/sneak/ipapi/internal/database" + "git.eeqj.de/sneak/ipapi/internal/state" + "github.com/go-chi/chi/v5" +) + +func TestNewRouter(t *testing.T) { + logger := slog.Default() + cfg := &config.Config{ + StateDir: t.TempDir(), + } + stateManager, _ := state.New(cfg, logger) + db, _ := database.New(cfg, logger, stateManager) + + router, err := NewRouter(logger, db) + if err != nil { + t.Fatalf("failed to create router: %v", err) + } + + if router == nil { + t.Fatal("expected router, got nil") + } +} + +func TestHealthEndpoint(t *testing.T) { + logger := slog.Default() + cfg := &config.Config{ + StateDir: t.TempDir(), + } + stateManager, _ := state.New(cfg, logger) + db, _ := database.New(cfg, logger, stateManager) + + router, _ := NewRouter(logger, db) + + req := httptest.NewRequest("GET", "/health", nil) + rec := httptest.NewRecorder() + + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", rec.Code) + } + + if rec.Body.String() != "OK" { + t.Errorf("expected body 'OK', got %s", rec.Body.String()) + } +} + +func TestIPLookupEndpoint(t *testing.T) { + logger := slog.Default() + cfg := &config.Config{ + StateDir: t.TempDir(), + } + stateManager, _ := state.New(cfg, logger) + db, _ := database.New(cfg, logger, stateManager) + + tests := []struct { + name string + ip string + expectedCode int + }{ + {"valid IPv4", "8.8.8.8", http.StatusOK}, + {"valid IPv6", "2001:4860:4860::8888", http.StatusOK}, + {"invalid IP", "invalid", http.StatusBadRequest}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/"+tt.ip, nil) + rec := httptest.NewRecorder() + + // Create a new context with the URL param + rctx := chi.NewRouteContext() + rctx.URLParams.Add("ip", tt.ip) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + handleIPLookup(logger, db)(rec, req) + + if rec.Code != tt.expectedCode { + t.Errorf("expected status %d, got %d", tt.expectedCode, rec.Code) + } + + if tt.expectedCode == http.StatusOK { + var info IPInfo + if err := json.Unmarshal(rec.Body.Bytes(), &info); err != nil { + t.Errorf("failed to unmarshal response: %v", err) + } + if info.IP != tt.ip { + t.Errorf("expected IP %s, got %s", tt.ip, info.IP) + } + } + }) + } +} + +func TestGetClientIP(t *testing.T) { + tests := []struct { + name string + headers map[string]string + remoteAddr string + expected string + }{ + { + name: "X-Forwarded-For", + headers: map[string]string{ + "X-Forwarded-For": "1.2.3.4, 5.6.7.8", + }, + remoteAddr: "9.10.11.12:1234", + expected: "1.2.3.4", + }, + { + name: "X-Real-IP", + headers: map[string]string{ + "X-Real-IP": "1.2.3.4", + }, + remoteAddr: "9.10.11.12:1234", + expected: "1.2.3.4", + }, + { + name: "RemoteAddr only", + headers: map[string]string{}, + remoteAddr: "9.10.11.12:1234", + expected: "9.10.11.12", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = tt.remoteAddr + for k, v := range tt.headers { + req.Header.Set(k, v) + } + + ip := getClientIP(req) + if ip != tt.expected { + t.Errorf("expected IP %s, got %s", tt.expected, ip) + } + }) + } +} diff --git a/internal/http/server.go b/internal/http/server.go new file mode 100644 index 0000000..b22f796 --- /dev/null +++ b/internal/http/server.go @@ -0,0 +1,66 @@ +package http + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "time" + + "git.eeqj.de/sneak/ipapi/internal/config" + "github.com/go-chi/chi/v5" +) + +// Server manages the HTTP server lifecycle. +type Server struct { + config *config.Config + logger *slog.Logger + router chi.Router + httpServer *http.Server +} + +// NewServer creates a new HTTP server instance. +func NewServer(cfg *config.Config, logger *slog.Logger, router chi.Router) (*Server, error) { + return &Server{ + config: cfg, + logger: logger, + router: router, + }, nil +} + +// Start begins listening for HTTP requests. +func (s *Server) Start(_ context.Context) error { + addr := fmt.Sprintf(":%d", s.config.Port) + s.httpServer = &http.Server{ + Addr: addr, + Handler: s.router, + ReadTimeout: 15 * time.Second, //nolint:mnd + WriteTimeout: 15 * time.Second, //nolint:mnd + IdleTimeout: 60 * time.Second, //nolint:mnd + } + + s.logger.Info("Starting HTTP server", "addr", addr) + + go func() { + if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + s.logger.Error("HTTP server error", "error", err) + } + }() + + return nil +} + +// Stop gracefully shuts down the HTTP server. +func (s *Server) Stop(ctx context.Context) error { + if s.httpServer == nil { + return nil + } + + s.logger.Info("Stopping HTTP server") + + const shutdownTimeout = 30 * time.Second + shutdownCtx, cancel := context.WithTimeout(ctx, shutdownTimeout) + defer cancel() + + return s.httpServer.Shutdown(shutdownCtx) +} diff --git a/internal/http/server_test.go b/internal/http/server_test.go new file mode 100644 index 0000000..42b9fbd --- /dev/null +++ b/internal/http/server_test.go @@ -0,0 +1,64 @@ +package http + +import ( + "context" + "log/slog" + "testing" + + "git.eeqj.de/sneak/ipapi/internal/config" + "github.com/go-chi/chi/v5" +) + +func TestNewServer(t *testing.T) { + cfg := &config.Config{ + Port: 8080, + } + logger := slog.Default() + router := chi.NewRouter() + + server, err := NewServer(cfg, logger, router) + if err != nil { + t.Fatalf("failed to create server: %v", err) + } + + if server == nil { + t.Fatal("expected server, got nil") + } + + if server.config != cfg { + t.Error("config not set correctly") + } + + if server.logger != logger { + t.Error("logger not set correctly") + } + + if server.router != router { + t.Error("router not set correctly") + } +} + +func TestServerStartStop(t *testing.T) { + cfg := &config.Config{ + Port: 0, // Use random port + } + logger := slog.Default() + router := chi.NewRouter() + + server, err := NewServer(cfg, logger, router) + if err != nil { + t.Fatalf("failed to create server: %v", err) + } + + ctx := context.Background() + + // Start server + if err := server.Start(ctx); err != nil { + t.Fatalf("failed to start server: %v", err) + } + + // Stop server + if err := server.Stop(ctx); err != nil { + t.Fatalf("failed to stop server: %v", err) + } +} diff --git a/internal/ipapi/cli.go b/internal/ipapi/cli.go new file mode 100644 index 0000000..fb63ce9 --- /dev/null +++ b/internal/ipapi/cli.go @@ -0,0 +1,23 @@ +// Package ipapi provides the main application structure and CLI. +package ipapi + +import ( + "os" + + "github.com/spf13/cobra" +) + +// CLIEntry is the main entry point for the CLI application. +func CLIEntry() { + rootCmd := &cobra.Command{ + Use: "ipapi", + Short: "IP API is a simple IP information REST API", + Long: `IP API provides GeoIP information for IPv4 and IPv6 addresses.`, + } + + rootCmd.AddCommand(daemonCmd()) + + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/internal/ipapi/daemon.go b/internal/ipapi/daemon.go new file mode 100644 index 0000000..90f331d --- /dev/null +++ b/internal/ipapi/daemon.go @@ -0,0 +1,66 @@ +package ipapi + +import ( + "context" + "os" + + "git.eeqj.de/sneak/ipapi/internal/config" + "git.eeqj.de/sneak/ipapi/internal/database" + "git.eeqj.de/sneak/ipapi/internal/http" + "git.eeqj.de/sneak/ipapi/internal/log" + "git.eeqj.de/sneak/ipapi/internal/state" + "github.com/spf13/cobra" + "go.uber.org/fx" +) + +const defaultConfigFile = "/etc/ipapi/config.yml" + +func daemonCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "daemon", + Short: "Run the IP API daemon", + Long: `Start the IP API HTTP server that provides GeoIP information.`, + RunE: func(_ *cobra.Command, _ []string) error { + configFile := getConfigFile() + + app := fx.New( + fx.Provide( + func() (*config.Config, error) { + return config.New(configFile) + }, + log.New, + state.New, + database.New, + http.NewServer, + http.NewRouter, + New, + ), + fx.Invoke(func(lc fx.Lifecycle, ipapi *IPAPI) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return ipapi.Start(ctx) + }, + OnStop: func(ctx context.Context) error { + return ipapi.Stop(ctx) + }, + }) + }), + ) + + app.Run() + + return nil + }, + } + + return cmd +} + +func getConfigFile() string { + configFile := os.Getenv("IP_API_CONFIG_FILE") + if configFile == "" { + return defaultConfigFile + } + + return configFile +} diff --git a/internal/ipapi/ipapi.go b/internal/ipapi/ipapi.go new file mode 100644 index 0000000..64b5847 --- /dev/null +++ b/internal/ipapi/ipapi.go @@ -0,0 +1,71 @@ +package ipapi + +import ( + "context" + "log/slog" + + "git.eeqj.de/sneak/ipapi/internal/config" + "git.eeqj.de/sneak/ipapi/internal/database" + "git.eeqj.de/sneak/ipapi/internal/http" + "git.eeqj.de/sneak/ipapi/internal/state" + "go.uber.org/fx" +) + +// Options contains all dependencies for IPAPI. +type Options struct { + fx.In + + Config *config.Config + Logger *slog.Logger + State *state.Manager + Database *database.Manager + Server *http.Server +} + +// IPAPI is the main application structure. +type IPAPI struct { + config *config.Config + logger *slog.Logger + state *state.Manager + database *database.Manager + server *http.Server +} + +// New creates a new IPAPI instance with the given options. +func New(opts Options) *IPAPI { + return &IPAPI{ + config: opts.Config, + logger: opts.Logger, + state: opts.State, + database: opts.Database, + server: opts.Server, + } +} + +// Start initializes and starts all components. +func (i *IPAPI) Start(ctx context.Context) error { + i.logger.Info("Starting IP API daemon", + "port", i.config.Port, + "state_dir", i.config.StateDir, + ) + + // Initialize state + if err := i.state.Initialize(ctx); err != nil { + return err + } + + // Download databases if needed + if err := i.database.EnsureDatabases(ctx); err != nil { + return err + } + + // Start HTTP server + return i.server.Start(ctx) +} + +// Stop gracefully shuts down all components. +func (i *IPAPI) Stop(ctx context.Context) error { + i.logger.Info("Stopping IP API daemon") + + return i.server.Stop(ctx) +} diff --git a/internal/ipapi/ipapi_test.go b/internal/ipapi/ipapi_test.go new file mode 100644 index 0000000..e814727 --- /dev/null +++ b/internal/ipapi/ipapi_test.go @@ -0,0 +1,93 @@ +package ipapi + +import ( + "context" + "log/slog" + "testing" + + "git.eeqj.de/sneak/ipapi/internal/config" + "git.eeqj.de/sneak/ipapi/internal/database" + "git.eeqj.de/sneak/ipapi/internal/http" + "git.eeqj.de/sneak/ipapi/internal/state" + "github.com/go-chi/chi/v5" +) + +func TestNew(t *testing.T) { + cfg := &config.Config{ + Port: 8080, + StateDir: t.TempDir(), + LogLevel: "info", + } + logger := slog.Default() + stateManager, _ := state.New(cfg, logger) + dbManager, _ := database.New(cfg, logger, stateManager) + router := chi.NewRouter() + server, _ := http.NewServer(cfg, logger, router) + + opts := Options{ + Config: cfg, + Logger: logger, + State: stateManager, + Database: dbManager, + Server: server, + } + + ipapi := New(opts) + if ipapi == nil { + t.Fatal("expected IPAPI instance, got nil") + } + + if ipapi.config != cfg { + t.Error("config not set correctly") + } + + if ipapi.logger != logger { + t.Error("logger not set correctly") + } + + if ipapi.state != stateManager { + t.Error("state manager not set correctly") + } + + if ipapi.database != dbManager { + t.Error("database manager not set correctly") + } + + if ipapi.server != server { + t.Error("server not set correctly") + } +} + +func TestStartStop(t *testing.T) { + cfg := &config.Config{ + Port: 0, // Random port + StateDir: t.TempDir(), + LogLevel: "info", + } + logger := slog.Default() + stateManager, _ := state.New(cfg, logger) + dbManager, _ := database.New(cfg, logger, stateManager) + router := chi.NewRouter() + server, _ := http.NewServer(cfg, logger, router) + + opts := Options{ + Config: cfg, + Logger: logger, + State: stateManager, + Database: dbManager, + Server: server, + } + + ipapi := New(opts) + ctx := context.Background() + + // Initialize state first + if err := stateManager.Initialize(ctx); err != nil { + t.Fatalf("failed to initialize state: %v", err) + } + + // Stop should work even if not started + if err := ipapi.Stop(ctx); err != nil { + t.Errorf("stop failed: %v", err) + } +} diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000..29ffe90 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,49 @@ +// Package log provides structured logging functionality. +package log + +import ( + "log/slog" + "os" + "strings" + + "git.eeqj.de/sneak/ipapi/internal/config" +) + +// New creates a new logger instance based on configuration. +func New(cfg *config.Config) *slog.Logger { + var level slog.Level + switch strings.ToLower(cfg.LogLevel) { + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn", "warning": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + level = slog.LevelInfo + } + + opts := &slog.HandlerOptions{ + Level: level, + } + + var handler slog.Handler + if isTerminal() { + handler = slog.NewTextHandler(os.Stdout, opts) + } else { + handler = slog.NewJSONHandler(os.Stdout, opts) + } + + return slog.New(handler) +} + +func isTerminal() bool { + fileInfo, err := os.Stdout.Stat() + if err != nil { + return false + } + + return (fileInfo.Mode() & os.ModeCharDevice) != 0 +} diff --git a/internal/log/log_test.go b/internal/log/log_test.go new file mode 100644 index 0000000..6921e40 --- /dev/null +++ b/internal/log/log_test.go @@ -0,0 +1,38 @@ +package log + +import ( + "testing" + + "git.eeqj.de/sneak/ipapi/internal/config" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + logLevel string + }{ + {"debug level", "debug"}, + {"info level", "info"}, + {"warn level", "warn"}, + {"warning level", "warning"}, + {"error level", "error"}, + {"invalid level", "invalid"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &config.Config{ + LogLevel: tt.logLevel, + } + logger := New(cfg) + if logger == nil { + t.Fatal("expected logger, got nil") + } + }) + } +} + +func TestIsTerminal(t *testing.T) { + // Just test that it doesn't panic + _ = isTerminal() +} diff --git a/internal/state/state.go b/internal/state/state.go new file mode 100644 index 0000000..7b0bc18 --- /dev/null +++ b/internal/state/state.go @@ -0,0 +1,134 @@ +// Package state manages daemon state persistence. +package state + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "os" + "path/filepath" + "time" + + "git.eeqj.de/sneak/ipapi/internal/config" +) + +const ( + stateFileName = "daemon.json" + dirPermissions = 0750 + filePermissions = 0600 +) + +// State holds the daemon's persistent state information. +type State struct { + LastASNDownload time.Time `json:"lastAsnDownload"` + LastCityDownload time.Time `json:"lastCityDownload"` + LastCountryDownload time.Time `json:"lastCountryDownload"` +} + +// Manager handles state file operations. +type Manager struct { + config *config.Config + logger *slog.Logger + statePath string +} + +// New creates a new state manager. +func New(cfg *config.Config, logger *slog.Logger) (*Manager, error) { + statePath := filepath.Join(cfg.StateDir, stateFileName) + + return &Manager{ + config: cfg, + logger: logger, + statePath: statePath, + }, nil +} + +// Initialize ensures the state directory exists. +func (m *Manager) Initialize(_ context.Context) error { + // Ensure state directory exists + dir := filepath.Dir(m.statePath) + if err := os.MkdirAll(dir, dirPermissions); err != nil { + return fmt.Errorf("failed to create state directory: %w", err) + } + + m.logger.Info("State manager initialized", "path", m.statePath) + + return nil +} + +// Load reads the state from disk. +func (m *Manager) Load() (*State, error) { + data, err := os.ReadFile(m.statePath) + if err != nil { + if os.IsNotExist(err) { + // Return empty state if file doesn't exist + return &State{}, nil + } + + return nil, fmt.Errorf("failed to read state file: %w", err) + } + + var state State + if err := json.Unmarshal(data, &state); err != nil { + return nil, fmt.Errorf("failed to parse state file: %w", err) + } + + return &state, nil +} + +// Save writes the state to disk. +func (m *Manager) Save(state *State) error { + data, err := json.MarshalIndent(state, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal state: %w", err) + } + + // Write to temporary file first + tmpPath := m.statePath + ".tmp" + if err := os.WriteFile(tmpPath, data, filePermissions); err != nil { + return fmt.Errorf("failed to write state file: %w", err) + } + + // Rename to final path (atomic operation) + if err := os.Rename(tmpPath, m.statePath); err != nil { + return fmt.Errorf("failed to save state file: %w", err) + } + + m.logger.Debug("State saved", "path", m.statePath) + + return nil +} + +// UpdateASNDownloadTime updates the ASN database download timestamp. +func (m *Manager) UpdateASNDownloadTime() error { + state, err := m.Load() + if err != nil { + return err + } + state.LastASNDownload = time.Now().UTC() + + return m.Save(state) +} + +// UpdateCityDownloadTime updates the City database download timestamp. +func (m *Manager) UpdateCityDownloadTime() error { + state, err := m.Load() + if err != nil { + return err + } + state.LastCityDownload = time.Now().UTC() + + return m.Save(state) +} + +// UpdateCountryDownloadTime updates the Country database download timestamp. +func (m *Manager) UpdateCountryDownloadTime() error { + state, err := m.Load() + if err != nil { + return err + } + state.LastCountryDownload = time.Now().UTC() + + return m.Save(state) +} diff --git a/internal/state/state_test.go b/internal/state/state_test.go new file mode 100644 index 0000000..f6ad946 --- /dev/null +++ b/internal/state/state_test.go @@ -0,0 +1,86 @@ +package state + +import ( + "context" + "log/slog" + "os" + "path/filepath" + "testing" + "time" + + "git.eeqj.de/sneak/ipapi/internal/config" +) + +func TestManager(t *testing.T) { + // Create temp directory for testing + tmpDir, err := os.MkdirTemp("", "ipapi-state-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + cfg := &config.Config{ + StateDir: tmpDir, + } + logger := slog.Default() + + m, err := New(cfg, logger) + if err != nil { + t.Fatalf("failed to create manager: %v", err) + } + + // Test Initialize + ctx := context.Background() + if err := m.Initialize(ctx); err != nil { + t.Fatalf("failed to initialize: %v", err) + } + + // Test Load with non-existent file + state, err := m.Load() + if err != nil { + t.Fatalf("failed to load empty state: %v", err) + } + if !state.LastASNDownload.IsZero() { + t.Error("expected zero time for new state") + } + + // Test Save and Load + now := time.Now().UTC() + state.LastASNDownload = now + state.LastCityDownload = now + state.LastCountryDownload = now + + if err := m.Save(state); err != nil { + t.Fatalf("failed to save state: %v", err) + } + + // Verify file exists + statePath := filepath.Join(tmpDir, stateFileName) + if _, err := os.Stat(statePath); os.IsNotExist(err) { + t.Error("state file was not created") + } + + // Load and verify + loaded, err := m.Load() + if err != nil { + t.Fatalf("failed to load saved state: %v", err) + } + + if !loaded.LastASNDownload.Equal(now) { + t.Errorf("ASN download time mismatch: got %v, want %v", loaded.LastASNDownload, now) + } + + // Test update methods + if err := m.UpdateASNDownloadTime(); err != nil { + t.Fatalf("failed to update ASN download time: %v", err) + } + + loaded, err = m.Load() + if err != nil { + t.Fatalf("failed to load after update: %v", err) + } + + if loaded.LastASNDownload.Before(now) || loaded.LastASNDownload.Equal(now) { + t.Error("ASN download time was not updated") + } +}