diff --git a/Makefile b/Makefile index bff4f9f..1e3eec9 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,27 @@ generate: @echo "Running code generation..." $(GO) generate ./... +generate-proto: + @echo "Generating gRPC code from proto files..." + @if ! command -v protoc > /dev/null; then \ + echo "protoc not found. Install Protocol Buffers compiler."; \ + exit 1; \ + fi + @if ! command -v protoc-gen-go > /dev/null; then \ + echo "protoc-gen-go not found. Install with: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest"; \ + exit 1; \ + fi + @if ! command -v protoc-gen-go-grpc > /dev/null; then \ + echo "protoc-gen-go-grpc not found. Install with: go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest"; \ + exit 1; \ + fi + @mkdir -p api/proto/generated + @protoc --go_out=api/proto/generated --go_opt=paths=source_relative \ + --go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \ + --proto_path=api/proto \ + api/proto/*.proto + @echo "gRPC code generation complete" + verify: fmt-check lint test @echo "Verification complete" diff --git a/api/proto/audit.proto b/api/proto/audit.proto new file mode 100644 index 0000000..7071c3b --- /dev/null +++ b/api/proto/audit.proto @@ -0,0 +1,56 @@ +syntax = "proto3"; + +package audit.v1; + +option go_package = "git.dcentral.systems/toolz/goplt/api/proto/generated/audit/v1;auditv1"; + +// AuditService provides audit logging operations. +service AuditService { + // Record records an audit log entry. + rpc Record(RecordRequest) returns (RecordResponse); + + // Query queries audit logs based on filters. + rpc Query(QueryRequest) returns (QueryResponse); +} + +// AuditLogEntry represents an audit log entry. +message AuditLogEntry { + string user_id = 1; + string action = 2; // e.g., "user.create", "user.update" + string resource = 3; // e.g., "user", "role" + string resource_id = 4; + string ip_address = 5; + string user_agent = 6; + map metadata = 7; + int64 timestamp = 8; +} + +// RecordRequest contains an audit log entry to record. +message RecordRequest { + AuditLogEntry entry = 1; +} + +// RecordResponse indicates success. +message RecordResponse { + bool success = 1; + string id = 2; // Audit log entry ID +} + +// QueryRequest contains filters for querying audit logs. +message QueryRequest { + optional string user_id = 1; + optional string action = 2; + optional string resource = 3; + optional string resource_id = 4; + optional int64 start_time = 5; + optional int64 end_time = 6; + int32 limit = 7; // Max number of results + int32 offset = 8; // Pagination offset +} + +// QueryResponse contains audit log entries. +message QueryResponse { + repeated AuditLogEntry entries = 1; + int32 total = 2; // Total number of matching entries +} + diff --git a/api/proto/auth.proto b/api/proto/auth.proto new file mode 100644 index 0000000..842d8c3 --- /dev/null +++ b/api/proto/auth.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package auth.v1; + +option go_package = "git.dcentral.systems/toolz/goplt/api/proto/generated/auth/v1;authv1"; + +// AuthService provides authentication operations. +service AuthService { + // Login authenticates a user and returns access and refresh tokens. + rpc Login(LoginRequest) returns (LoginResponse); + + // RefreshToken refreshes an access token using a refresh token. + rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse); + + // ValidateToken validates a JWT token and returns the token claims. + rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse); + + // Logout invalidates a refresh token. + rpc Logout(LogoutRequest) returns (LogoutResponse); +} + +// LoginRequest contains login credentials. +message LoginRequest { + string email = 1; + string password = 2; +} + +// LoginResponse contains authentication tokens. +message LoginResponse { + string access_token = 1; + string refresh_token = 2; + int64 expires_in = 3; // seconds + string token_type = 4; // "Bearer" +} + +// RefreshTokenRequest contains a refresh token. +message RefreshTokenRequest { + string refresh_token = 1; +} + +// RefreshTokenResponse contains new authentication tokens. +message RefreshTokenResponse { + string access_token = 1; + string refresh_token = 2; + int64 expires_in = 3; // seconds + string token_type = 4; // "Bearer" +} + +// ValidateTokenRequest contains a JWT token to validate. +message ValidateTokenRequest { + string token = 1; +} + +// ValidateTokenResponse contains token claims. +message ValidateTokenResponse { + string user_id = 1; + string email = 2; + repeated string roles = 3; + int64 expires_at = 4; +} + +// LogoutRequest contains a refresh token to invalidate. +message LogoutRequest { + string refresh_token = 1; +} + +// LogoutResponse indicates success. +message LogoutResponse { + bool success = 1; +} + diff --git a/api/proto/authz.proto b/api/proto/authz.proto new file mode 100644 index 0000000..ddc7e23 --- /dev/null +++ b/api/proto/authz.proto @@ -0,0 +1,80 @@ +syntax = "proto3"; + +package authz.v1; + +option go_package = "git.dcentral.systems/toolz/goplt/api/proto/generated/authz/v1;authzv1"; + +// AuthzService provides authorization operations. +service AuthzService { + // Authorize checks if a user has a specific permission and returns an error if not. + rpc Authorize(AuthorizeRequest) returns (AuthorizeResponse); + + // HasPermission checks if a user has a specific permission. + rpc HasPermission(HasPermissionRequest) returns (HasPermissionResponse); + + // GetUserPermissions returns all permissions for a user. + rpc GetUserPermissions(GetUserPermissionsRequest) returns (GetUserPermissionsResponse); + + // GetUserRoles returns all roles for a user. + rpc GetUserRoles(GetUserRolesRequest) returns (GetUserRolesResponse); +} + +// Permission represents a permission in the system. +message Permission { + string id = 1; + string code = 2; + string name = 3; + string description = 4; +} + +// Role represents a role in the system. +message Role { + string id = 1; + string name = 2; + string description = 3; + repeated string permissions = 4; // Permission codes +} + +// AuthorizeRequest contains user ID and permission to check. +message AuthorizeRequest { + string user_id = 1; + string permission = 2; +} + +// AuthorizeResponse indicates authorization result. +message AuthorizeResponse { + bool authorized = 1; + string message = 2; +} + +// HasPermissionRequest contains user ID and permission to check. +message HasPermissionRequest { + string user_id = 1; + string permission = 2; +} + +// HasPermissionResponse indicates if the user has the permission. +message HasPermissionResponse { + bool has_permission = 1; +} + +// GetUserPermissionsRequest contains a user ID. +message GetUserPermissionsRequest { + string user_id = 1; +} + +// GetUserPermissionsResponse contains all permissions for the user. +message GetUserPermissionsResponse { + repeated Permission permissions = 1; +} + +// GetUserRolesRequest contains a user ID. +message GetUserRolesRequest { + string user_id = 1; +} + +// GetUserRolesResponse contains all roles for the user. +message GetUserRolesResponse { + repeated Role roles = 1; +} + diff --git a/api/proto/identity.proto b/api/proto/identity.proto new file mode 100644 index 0000000..37b90d8 --- /dev/null +++ b/api/proto/identity.proto @@ -0,0 +1,134 @@ +syntax = "proto3"; + +package identity.v1; + +option go_package = "git.dcentral.systems/toolz/goplt/api/proto/generated/identity/v1;identityv1"; + +// IdentityService provides user management operations. +service IdentityService { + // GetUser retrieves a user by ID. + rpc GetUser(GetUserRequest) returns (GetUserResponse); + + // GetUserByEmail retrieves a user by email address. + rpc GetUserByEmail(GetUserByEmailRequest) returns (GetUserByEmailResponse); + + // CreateUser creates a new user. + rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); + + // UpdateUser updates an existing user. + rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse); + + // DeleteUser deletes a user. + rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse); + + // VerifyEmail verifies a user's email address using a verification token. + rpc VerifyEmail(VerifyEmailRequest) returns (VerifyEmailResponse); + + // RequestPasswordReset requests a password reset token. + rpc RequestPasswordReset(RequestPasswordResetRequest) returns (RequestPasswordResetResponse); + + // ResetPassword resets a user's password using a reset token. + rpc ResetPassword(ResetPasswordRequest) returns (ResetPasswordResponse); +} + +// User represents a user in the system. +message User { + string id = 1; + string email = 2; + string username = 3; + string first_name = 4; + string last_name = 5; + bool email_verified = 6; + int64 created_at = 7; + int64 updated_at = 8; +} + +// GetUserRequest contains a user ID. +message GetUserRequest { + string id = 1; +} + +// GetUserResponse contains a user. +message GetUserResponse { + User user = 1; +} + +// GetUserByEmailRequest contains an email address. +message GetUserByEmailRequest { + string email = 1; +} + +// GetUserByEmailResponse contains a user. +message GetUserByEmailResponse { + User user = 1; +} + +// CreateUserRequest contains user data for creation. +message CreateUserRequest { + string email = 1; + string username = 2; + string password = 3; + string first_name = 4; + string last_name = 5; +} + +// CreateUserResponse contains the created user. +message CreateUserResponse { + User user = 1; +} + +// UpdateUserRequest contains user data for update. +message UpdateUserRequest { + string id = 1; + optional string email = 2; + optional string username = 3; + optional string first_name = 4; + optional string last_name = 5; +} + +// UpdateUserResponse contains the updated user. +message UpdateUserResponse { + User user = 1; +} + +// DeleteUserRequest contains a user ID. +message DeleteUserRequest { + string id = 1; +} + +// DeleteUserResponse indicates success. +message DeleteUserResponse { + bool success = 1; +} + +// VerifyEmailRequest contains a verification token. +message VerifyEmailRequest { + string token = 1; +} + +// VerifyEmailResponse indicates success. +message VerifyEmailResponse { + bool success = 1; +} + +// RequestPasswordResetRequest contains an email address. +message RequestPasswordResetRequest { + string email = 1; +} + +// RequestPasswordResetResponse indicates success. +message RequestPasswordResetResponse { + bool success = 1; +} + +// ResetPasswordRequest contains a reset token and new password. +message ResetPasswordRequest { + string token = 1; + string new_password = 2; +} + +// ResetPasswordResponse indicates success. +message ResetPasswordResponse { + bool success = 1; +} + diff --git a/cmd/api-gateway/main.go b/cmd/api-gateway/main.go new file mode 100644 index 0000000..98516b6 --- /dev/null +++ b/cmd/api-gateway/main.go @@ -0,0 +1,156 @@ +// Package main provides the API Gateway service entry point. +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "time" + + "git.dcentral.systems/toolz/goplt/internal/client" + "git.dcentral.systems/toolz/goplt/internal/di" + "git.dcentral.systems/toolz/goplt/internal/health" + "git.dcentral.systems/toolz/goplt/internal/metrics" + "git.dcentral.systems/toolz/goplt/internal/server" + "git.dcentral.systems/toolz/goplt/pkg/config" + "git.dcentral.systems/toolz/goplt/pkg/errorbus" + "git.dcentral.systems/toolz/goplt/pkg/logger" + "git.dcentral.systems/toolz/goplt/pkg/registry" + "git.dcentral.systems/toolz/goplt/services/gateway" + "go.opentelemetry.io/otel/trace" + "go.uber.org/fx" +) + +func main() { + // Create DI container with core kernel services + container := di.NewContainer( + // Invoke lifecycle hooks + fx.Invoke(di.RegisterLifecycleHooks), + // Create API Gateway + fx.Invoke(func( + cfg config.ConfigProvider, + log logger.Logger, + healthRegistry *health.Registry, + metricsRegistry *metrics.Metrics, + errorBus errorbus.ErrorPublisher, + tracer trace.TracerProvider, + serviceRegistry registry.ServiceRegistry, + clientFactory *client.ServiceClientFactory, + lc fx.Lifecycle, + ) { + // Create HTTP server using server foundation + srv, err := server.NewServer(cfg, log, healthRegistry, metricsRegistry, errorBus, tracer) + if err != nil { + log.Error("Failed to create API Gateway server", + logger.Error(err), + ) + os.Exit(1) + } + + // Setup gateway routes + gateway, err := gateway.NewGateway(cfg, log, clientFactory, serviceRegistry) + if err != nil { + log.Error("Failed to create API Gateway", + logger.Error(err), + ) + os.Exit(1) + } + gateway.SetupRoutes(srv.Router()) + + // Register with Consul + gatewayPort := cfg.GetInt("gateway.port") + if gatewayPort == 0 { + gatewayPort = 8080 + } + gatewayHost := cfg.GetString("gateway.host") + if gatewayHost == "" { + gatewayHost = "localhost" + } + + serviceInstance := ®istry.ServiceInstance{ + ID: fmt.Sprintf("api-gateway-%d", os.Getpid()), + Name: "api-gateway", + Address: gatewayHost, + Port: gatewayPort, + Tags: []string{"gateway", "http"}, + Metadata: map[string]string{ + "version": "1.0.0", + }, + } + + // Register lifecycle hooks + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + // Register with service registry + if err := serviceRegistry.Register(ctx, serviceInstance); err != nil { + log.Warn("Failed to register API Gateway with service registry", + logger.Error(err), + ) + // Continue anyway - gateway can work without registry + } else { + log.Info("API Gateway registered with service registry", + logger.String("service_id", serviceInstance.ID), + ) + } + + // Start HTTP server + addr := fmt.Sprintf("%s:%d", cfg.GetString("server.host"), gatewayPort) + log.Info("API Gateway starting", + logger.String("addr", addr), + ) + + errChan := make(chan error, 1) + go func() { + if err := srv.Start(); err != nil && err != http.ErrServerClosed { + log.Error("API Gateway server failed", + logger.String("error", err.Error()), + ) + errChan <- err + } + }() + + // Wait a short time to detect immediate binding errors + select { + case err := <-errChan: + return fmt.Errorf("API Gateway failed to start: %w", err) + case <-time.After(500 * time.Millisecond): + log.Info("API Gateway started successfully", + logger.String("addr", addr), + ) + return nil + } + }, + OnStop: func(ctx context.Context) error { + // Deregister from service registry + if err := serviceRegistry.Deregister(ctx, serviceInstance.ID); err != nil { + log.Warn("Failed to deregister API Gateway from service registry", + logger.Error(err), + ) + } else { + log.Info("API Gateway deregistered from service registry") + } + + // Shutdown HTTP server + return srv.Shutdown(ctx) + }, + }) + }), + ) + + // Create root context + ctx := context.Background() + + // Start the application + if err := container.Start(ctx); err != nil { + log := logger.GetGlobalLogger() + if log != nil { + log.Error("Failed to start API Gateway", + logger.Error(err), + ) + } else { + fmt.Fprintf(os.Stderr, "Failed to start API Gateway: %v\n", err) + } + os.Exit(1) + } +} diff --git a/cmd/platform/main.go b/cmd/platform/main.go index c7725a2..4eba88f 100644 --- a/cmd/platform/main.go +++ b/cmd/platform/main.go @@ -7,23 +7,28 @@ import ( "os" "git.dcentral.systems/toolz/goplt/internal/di" - "git.dcentral.systems/toolz/goplt/internal/infra/database" - "git.dcentral.systems/toolz/goplt/internal/server" + "git.dcentral.systems/toolz/goplt/internal/health" + "git.dcentral.systems/toolz/goplt/internal/metrics" + "git.dcentral.systems/toolz/goplt/pkg/config" "git.dcentral.systems/toolz/goplt/pkg/logger" "go.uber.org/fx" ) func main() { // Create DI container with lifecycle hooks - // We need to invoke the HTTP server to ensure all providers execute + // This is a minimal entry point for testing core kernel infrastructure + // Services will have their own entry points (cmd/{service}/main.go) container := di.NewContainer( // Invoke lifecycle hooks fx.Invoke(di.RegisterLifecycleHooks), - // Force HTTP server to be created (which triggers all dependencies) - // This ensures database, health, metrics, etc. are all created - fx.Invoke(func(_ *server.Server, _ *database.Client) { - // Both server and database are created, hooks are registered - // This ensures all providers execute + // Verify core kernel services are available + fx.Invoke(func( + _ config.ConfigProvider, + _ logger.Logger, + _ *health.Registry, + _ *metrics.Metrics, + ) { + // Core kernel services are available }), ) diff --git a/config/default.yaml b/config/default.yaml index 5ec5a19..476aa1a 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -24,3 +24,30 @@ tracing: service_name: "platform" service_version: "1.0.0" otlp_endpoint: "" + +registry: + type: consul + consul: + address: "localhost:8500" + datacenter: "dc1" + scheme: "http" + health_check: + interval: "10s" + timeout: "3s" + deregister_after: "30s" + http: "/healthz" + +gateway: + port: 8080 + host: "0.0.0.0" + routes: + - path: "/api/v1/auth/**" + service: "auth-service" + auth_required: false + - path: "/api/v1/users/**" + service: "identity-service" + auth_required: true + cors: + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] + allowed_headers: ["Authorization", "Content-Type"] diff --git a/go.mod b/go.mod index 5a8ea7c..951bb3b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module git.dcentral.systems/toolz/goplt -go 1.24 +go 1.25.3 require ( entgo.io/ent v0.14.5 @@ -23,6 +23,7 @@ require ( ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/armon/go-metrics v0.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/bytedance/sonic v1.14.0 // indirect @@ -30,6 +31,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gin-contrib/sse v1.1.0 // indirect @@ -39,16 +41,28 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/consul/api v1.33.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl/v2 v2.18.1 // indirect + github.com/hashicorp/serf v0.10.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/magiconair/properties v1.8.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/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -78,8 +92,8 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.41.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/mod v0.26.0 // indirect + golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect + golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect diff --git a/go.sum b/go.sum index b8cdaa5..bd2ec72 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,26 @@ entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4= entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +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/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= @@ -18,14 +32,22 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 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/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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -36,6 +58,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -51,12 +77,22 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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/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.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= @@ -64,18 +100,59 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/consul/api v1.33.0 h1:MnFUzN1Bo6YDGi/EsRLbVNgA4pyCymmcswrE5j4OHBM= +github.com/hashicorp/consul/api v1.33.0/go.mod h1:vLz2I/bqqCYiG0qRHGerComvbwSWKswc8rRFtnYBrIw= +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.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/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-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-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +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/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.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo= github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +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/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/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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +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.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= @@ -86,42 +163,86 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +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/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +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/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +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.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/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_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +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/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +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/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -133,16 +254,21 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.18.0 h1:pN6W1ub/G4OfnM+NR9p7xP9R6TltLUzp5JG9yZD3Qg0= github.com/spf13/viper v1.18.0/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 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/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.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -189,19 +315,67 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= @@ -212,11 +386,17 @@ google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +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= diff --git a/internal/client/factory.go b/internal/client/factory.go new file mode 100644 index 0000000..bb9a20d --- /dev/null +++ b/internal/client/factory.go @@ -0,0 +1,51 @@ +// Package client provides service client factory for creating service clients. +package client + +import ( + "context" + "fmt" + + "git.dcentral.systems/toolz/goplt/internal/client/grpc" + "git.dcentral.systems/toolz/goplt/pkg/registry" + "git.dcentral.systems/toolz/goplt/pkg/services" +) + +// ServiceClientFactory creates service clients for inter-service communication. +type ServiceClientFactory struct { + registry registry.ServiceRegistry +} + +// NewServiceClientFactory creates a new service client factory. +func NewServiceClientFactory(reg registry.ServiceRegistry) *ServiceClientFactory { + return &ServiceClientFactory{ + registry: reg, + } +} + +// GetAuthClient returns an AuthServiceClient. +func (f *ServiceClientFactory) GetAuthClient() (services.AuthServiceClient, error) { + return grpc.NewAuthClient(f.registry) +} + +// GetIdentityClient returns an IdentityServiceClient. +func (f *ServiceClientFactory) GetIdentityClient() (services.IdentityServiceClient, error) { + return grpc.NewIdentityClient(f.registry) +} + +// GetAuthzClient returns an AuthzServiceClient. +func (f *ServiceClientFactory) GetAuthzClient() (services.AuthzServiceClient, error) { + return grpc.NewAuthzClient(f.registry) +} + +// GetAuditClient returns an AuditServiceClient. +func (f *ServiceClientFactory) GetAuditClient() (services.AuditServiceClient, error) { + return grpc.NewAuditClient(f.registry) +} + +// DiscoverService discovers service instances for a given service name. +func (f *ServiceClientFactory) DiscoverService(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + if f.registry == nil { + return nil, fmt.Errorf("service registry is not available") + } + return f.registry.Discover(ctx, serviceName) +} diff --git a/internal/client/grpc/audit_client.go b/internal/client/grpc/audit_client.go new file mode 100644 index 0000000..b007e5c --- /dev/null +++ b/internal/client/grpc/audit_client.go @@ -0,0 +1,33 @@ +// Package grpc provides gRPC client implementations for service clients. +package grpc + +import ( + "context" + "fmt" + + "git.dcentral.systems/toolz/goplt/pkg/registry" + "git.dcentral.systems/toolz/goplt/pkg/services" +) + +// AuditClient implements AuditServiceClient using gRPC. +// This is a stub implementation - will be fully implemented when proto files are generated in Phase 4. +type AuditClient struct { + registry registry.ServiceRegistry +} + +// NewAuditClient creates a new gRPC client for the Audit Service. +func NewAuditClient(reg registry.ServiceRegistry) (services.AuditServiceClient, error) { + return &AuditClient{ + registry: reg, + }, nil +} + +// Record records an audit log entry. +func (c *AuditClient) Record(ctx context.Context, entry *services.AuditLogEntry) error { + return fmt.Errorf("not implemented: proto files not yet generated") +} + +// Query queries audit logs based on filters. +func (c *AuditClient) Query(ctx context.Context, filters *services.AuditLogFilters) ([]services.AuditLogEntry, error) { + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} diff --git a/internal/client/grpc/auth_client.go b/internal/client/grpc/auth_client.go new file mode 100644 index 0000000..26f9beb --- /dev/null +++ b/internal/client/grpc/auth_client.go @@ -0,0 +1,75 @@ +// Package grpc provides gRPC client implementations for service clients. +package grpc + +import ( + "context" + "fmt" + + "git.dcentral.systems/toolz/goplt/pkg/registry" + "git.dcentral.systems/toolz/goplt/pkg/services" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// AuthClient implements AuthServiceClient using gRPC. +// This is a stub implementation - will be fully implemented when proto files are generated in Phase 4. +type AuthClient struct { + registry registry.ServiceRegistry + // conn will be set when proto files are available + // conn *grpc.ClientConn +} + +// NewAuthClient creates a new gRPC client for the Auth Service. +func NewAuthClient(reg registry.ServiceRegistry) (services.AuthServiceClient, error) { + return &AuthClient{ + registry: reg, + }, nil +} + +// Login authenticates a user and returns access and refresh tokens. +func (c *AuthClient) Login(ctx context.Context, email, password string) (*services.TokenResponse, error) { + // TODO: Implement when proto files are generated + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} + +// RefreshToken refreshes an access token using a refresh token. +func (c *AuthClient) RefreshToken(ctx context.Context, refreshToken string) (*services.TokenResponse, error) { + // TODO: Implement when proto files are generated + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} + +// ValidateToken validates a JWT token and returns the token claims. +func (c *AuthClient) ValidateToken(ctx context.Context, token string) (*services.TokenClaims, error) { + // TODO: Implement when proto files are generated + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} + +// Logout invalidates a refresh token. +func (c *AuthClient) Logout(ctx context.Context, refreshToken string) error { + // TODO: Implement when proto files are generated + return fmt.Errorf("not implemented: proto files not yet generated") +} + +// connectToService discovers and connects to a service instance. +func connectToService(ctx context.Context, reg registry.ServiceRegistry, serviceName string) (*grpc.ClientConn, error) { + instances, err := reg.Discover(ctx, serviceName) + if err != nil { + return nil, fmt.Errorf("failed to discover service %s: %w", serviceName, err) + } + + if len(instances) == 0 { + return nil, fmt.Errorf("no instances found for service %s", serviceName) + } + + // Use the first healthy instance (load balancing can be added later) + instance := instances[0] + address := fmt.Sprintf("%s:%d", instance.Address, instance.Port) + + // Create gRPC connection + conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("failed to connect to %s at %s: %w", serviceName, address, err) + } + + return conn, nil +} diff --git a/internal/client/grpc/authz_client.go b/internal/client/grpc/authz_client.go new file mode 100644 index 0000000..9aa35d0 --- /dev/null +++ b/internal/client/grpc/authz_client.go @@ -0,0 +1,43 @@ +// Package grpc provides gRPC client implementations for service clients. +package grpc + +import ( + "context" + "fmt" + + "git.dcentral.systems/toolz/goplt/pkg/registry" + "git.dcentral.systems/toolz/goplt/pkg/services" +) + +// AuthzClient implements AuthzServiceClient using gRPC. +// This is a stub implementation - will be fully implemented when proto files are generated in Phase 4. +type AuthzClient struct { + registry registry.ServiceRegistry +} + +// NewAuthzClient creates a new gRPC client for the Authz Service. +func NewAuthzClient(reg registry.ServiceRegistry) (services.AuthzServiceClient, error) { + return &AuthzClient{ + registry: reg, + }, nil +} + +// Authorize checks if a user has a specific permission and returns an error if not. +func (c *AuthzClient) Authorize(ctx context.Context, userID, permission string) error { + return fmt.Errorf("not implemented: proto files not yet generated") +} + +// HasPermission checks if a user has a specific permission. +func (c *AuthzClient) HasPermission(ctx context.Context, userID, permission string) (bool, error) { + return false, fmt.Errorf("not implemented: proto files not yet generated") +} + +// GetUserPermissions returns all permissions for a user. +func (c *AuthzClient) GetUserPermissions(ctx context.Context, userID string) ([]services.Permission, error) { + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} + +// GetUserRoles returns all roles for a user. +func (c *AuthzClient) GetUserRoles(ctx context.Context, userID string) ([]services.Role, error) { + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} diff --git a/internal/client/grpc/identity_client.go b/internal/client/grpc/identity_client.go new file mode 100644 index 0000000..b7d437b --- /dev/null +++ b/internal/client/grpc/identity_client.go @@ -0,0 +1,63 @@ +// Package grpc provides gRPC client implementations for service clients. +package grpc + +import ( + "context" + "fmt" + + "git.dcentral.systems/toolz/goplt/pkg/registry" + "git.dcentral.systems/toolz/goplt/pkg/services" +) + +// IdentityClient implements IdentityServiceClient using gRPC. +// This is a stub implementation - will be fully implemented when proto files are generated in Phase 4. +type IdentityClient struct { + registry registry.ServiceRegistry +} + +// NewIdentityClient creates a new gRPC client for the Identity Service. +func NewIdentityClient(reg registry.ServiceRegistry) (services.IdentityServiceClient, error) { + return &IdentityClient{ + registry: reg, + }, nil +} + +// GetUser retrieves a user by ID. +func (c *IdentityClient) GetUser(ctx context.Context, id string) (*services.User, error) { + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} + +// GetUserByEmail retrieves a user by email address. +func (c *IdentityClient) GetUserByEmail(ctx context.Context, email string) (*services.User, error) { + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} + +// CreateUser creates a new user. +func (c *IdentityClient) CreateUser(ctx context.Context, user *services.CreateUserRequest) (*services.User, error) { + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} + +// UpdateUser updates an existing user. +func (c *IdentityClient) UpdateUser(ctx context.Context, id string, user *services.UpdateUserRequest) (*services.User, error) { + return nil, fmt.Errorf("not implemented: proto files not yet generated") +} + +// DeleteUser deletes a user. +func (c *IdentityClient) DeleteUser(ctx context.Context, id string) error { + return fmt.Errorf("not implemented: proto files not yet generated") +} + +// VerifyEmail verifies a user's email address using a verification token. +func (c *IdentityClient) VerifyEmail(ctx context.Context, token string) error { + return fmt.Errorf("not implemented: proto files not yet generated") +} + +// RequestPasswordReset requests a password reset token. +func (c *IdentityClient) RequestPasswordReset(ctx context.Context, email string) error { + return fmt.Errorf("not implemented: proto files not yet generated") +} + +// ResetPassword resets a user's password using a reset token. +func (c *IdentityClient) ResetPassword(ctx context.Context, token, newPassword string) error { + return fmt.Errorf("not implemented: proto files not yet generated") +} diff --git a/internal/di/providers.go b/internal/di/providers.go index 97d1c3f..b945e0b 100644 --- a/internal/di/providers.go +++ b/internal/di/providers.go @@ -7,6 +7,7 @@ import ( "os" "time" + "git.dcentral.systems/toolz/goplt/internal/client" configimpl "git.dcentral.systems/toolz/goplt/internal/config" errorbusimpl "git.dcentral.systems/toolz/goplt/internal/errorbus" "git.dcentral.systems/toolz/goplt/internal/health" @@ -14,10 +15,12 @@ import ( loggerimpl "git.dcentral.systems/toolz/goplt/internal/logger" "git.dcentral.systems/toolz/goplt/internal/metrics" "git.dcentral.systems/toolz/goplt/internal/observability" + "git.dcentral.systems/toolz/goplt/internal/registry/consul" "git.dcentral.systems/toolz/goplt/internal/server" "git.dcentral.systems/toolz/goplt/pkg/config" "git.dcentral.systems/toolz/goplt/pkg/errorbus" "git.dcentral.systems/toolz/goplt/pkg/logger" + "git.dcentral.systems/toolz/goplt/pkg/registry" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/noop" "go.uber.org/fx" @@ -158,13 +161,11 @@ func ProvideErrorBus() fx.Option { } // ProvideHealthRegistry creates an FX option that provides the health check registry. +// Note: Database health checkers are registered by services that create their own database clients. func ProvideHealthRegistry() fx.Option { - return fx.Provide(func(dbClient *database.Client) (*health.Registry, error) { + return fx.Provide(func() (*health.Registry, error) { registry := health.NewRegistry() - - // Register database health checker - registry.Register("database", health.NewDatabaseChecker(dbClient)) - + // Services will register their own health checkers (e.g., database, external dependencies) return registry, nil }) } @@ -176,6 +177,72 @@ func ProvideMetrics() fx.Option { }) } +// ProvideServiceRegistry creates an FX option that provides the service registry. +func ProvideServiceRegistry() fx.Option { + return fx.Provide(func(cfg config.ConfigProvider) (registry.ServiceRegistry, error) { + registryType := cfg.GetString("registry.type") + if registryType == "" { + registryType = "consul" + } + + switch registryType { + case "consul": + consulCfg := consul.Config{ + Address: cfg.GetString("registry.consul.address"), + Datacenter: cfg.GetString("registry.consul.datacenter"), + Scheme: cfg.GetString("registry.consul.scheme"), + } + + // Set defaults + if consulCfg.Address == "" { + consulCfg.Address = "localhost:8500" + } + if consulCfg.Datacenter == "" { + consulCfg.Datacenter = "dc1" + } + if consulCfg.Scheme == "" { + consulCfg.Scheme = "http" + } + + // Parse health check configuration + healthCheckInterval := cfg.GetDuration("registry.consul.health_check.interval") + if healthCheckInterval == 0 { + healthCheckInterval = 10 * time.Second + } + healthCheckTimeout := cfg.GetDuration("registry.consul.health_check.timeout") + if healthCheckTimeout == 0 { + healthCheckTimeout = 3 * time.Second + } + healthCheckDeregisterAfter := cfg.GetDuration("registry.consul.health_check.deregister_after") + if healthCheckDeregisterAfter == 0 { + healthCheckDeregisterAfter = 30 * time.Second + } + healthCheckHTTP := cfg.GetString("registry.consul.health_check.http") + if healthCheckHTTP == "" { + healthCheckHTTP = "/healthz" + } + + consulCfg.HealthCheck = consul.HealthCheckConfig{ + Interval: healthCheckInterval, + Timeout: healthCheckTimeout, + DeregisterAfter: healthCheckDeregisterAfter, + HTTP: healthCheckHTTP, + } + + return consul.NewRegistry(consulCfg) + default: + return nil, fmt.Errorf("unsupported registry type: %s", registryType) + } + }) +} + +// ProvideServiceClientFactory creates an FX option that provides the service client factory. +func ProvideServiceClientFactory() fx.Option { + return fx.Provide(func(reg registry.ServiceRegistry) (*client.ServiceClientFactory, error) { + return client.NewServiceClientFactory(reg), nil + }) +} + // ProvideTracer creates an FX option that provides the OpenTelemetry tracer. func ProvideTracer() fx.Option { return fx.Provide(func(cfg config.ConfigProvider, lc fx.Lifecycle) (trace.TracerProvider, error) { @@ -318,18 +385,21 @@ func ProvideHTTPServer() fx.Option { }) } -// CoreModule returns an FX option that provides all core services. -// This includes configuration, logging, database, error bus, health checks, metrics, tracing, and HTTP server. +// CoreModule returns an FX option that provides all core kernel infrastructure services. +// This includes configuration, logging, error bus, health checks, metrics, tracing, service registry, and service client factory. +// Note: Database and HTTP server are NOT included - services will create their own instances. +// HTTP server foundation is available via server.NewServer() for services to use. func CoreModule() fx.Option { return fx.Options( ProvideConfig(), ProvideLogger(), - ProvideDatabase(), ProvideErrorBus(), ProvideHealthRegistry(), ProvideMetrics(), ProvideTracer(), - ProvideHTTPServer(), + ProvideServiceRegistry(), + ProvideServiceClientFactory(), + // Note: ProvideDatabase() and ProvideHTTPServer() are removed - services create their own ) } diff --git a/internal/infra/database/client.go b/internal/infra/database/client.go index 15a3bd2..90b2b73 100644 --- a/internal/infra/database/client.go +++ b/internal/infra/database/client.go @@ -22,13 +22,15 @@ type Client struct { // Config holds database configuration. type Config struct { DSN string + Schema string // Schema name for schema isolation (e.g., "identity", "auth", "authz", "audit") MaxConnections int MaxIdleConns int ConnMaxLifetime time.Duration ConnMaxIdleTime time.Duration } -// NewClient creates a new Ent client with connection pooling. +// NewClient creates a new Ent client with connection pooling and schema isolation support. +// If schema is provided, it will be created if it doesn't exist and set as the search path. func NewClient(cfg Config) (*Client, error) { // Open database connection db, err := sql.Open("postgres", cfg.DSN) @@ -51,6 +53,19 @@ func NewClient(cfg Config) (*Client, error) { return nil, fmt.Errorf("failed to ping database: %w", err) } + // Create schema if provided + if cfg.Schema != "" { + if err := createSchemaIfNotExists(ctx, db, cfg.Schema); err != nil { + _ = db.Close() + return nil, fmt.Errorf("failed to create schema %s: %w", cfg.Schema, err) + } + // Set search path to the schema + if _, err := db.ExecContext(ctx, fmt.Sprintf("SET search_path TO %s", cfg.Schema)); err != nil { + _ = db.Close() + return nil, fmt.Errorf("failed to set search path to schema %s: %w", cfg.Schema, err) + } + } + // Create Ent driver drv := entsql.OpenDB(dialect.Postgres, db) @@ -63,6 +78,49 @@ func NewClient(cfg Config) (*Client, error) { }, nil } +// NewClientWithSchema is a convenience function that creates a client with a specific schema. +func NewClientWithSchema(dsn string, schema string) (*Client, error) { + return NewClient(Config{ + DSN: dsn, + Schema: schema, + MaxConnections: 25, + MaxIdleConns: 5, + ConnMaxLifetime: 5 * time.Minute, + ConnMaxIdleTime: 10 * time.Minute, + }) +} + +// createSchemaIfNotExists creates a PostgreSQL schema if it doesn't exist. +func createSchemaIfNotExists(ctx context.Context, db *sql.DB, schemaName string) error { + // Use a transaction to ensure atomicity + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // Check if schema exists + var exists bool + err = tx.QueryRowContext(ctx, + "SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)", + schemaName, + ).Scan(&exists) + if err != nil { + return err + } + + // Create schema if it doesn't exist + if !exists { + // Use fmt.Sprintf for schema name since it's a configuration value, not user input + _, err = tx.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schemaName)) + if err != nil { + return err + } + } + + return tx.Commit() +} + // Close closes the database connection. func (c *Client) Close() error { if err := c.Client.Close(); err != nil { diff --git a/internal/registry/consul/consul.go b/internal/registry/consul/consul.go new file mode 100644 index 0000000..f11c73c --- /dev/null +++ b/internal/registry/consul/consul.go @@ -0,0 +1,198 @@ +// Package consul provides Consul-based service registry implementation. +package consul + +import ( + "context" + "fmt" + "time" + + "git.dcentral.systems/toolz/goplt/pkg/registry" + consulapi "github.com/hashicorp/consul/api" +) + +// ConsulRegistry implements ServiceRegistry using Consul. +type ConsulRegistry struct { + client *consulapi.Client + config *Config +} + +// Config holds Consul registry configuration. +type Config struct { + Address string // Consul agent address (e.g., "localhost:8500") + Datacenter string // Consul datacenter + Scheme string // "http" or "https" + HealthCheck HealthCheckConfig +} + +// HealthCheckConfig holds health check configuration. +type HealthCheckConfig struct { + Interval time.Duration // Health check interval + Timeout time.Duration // Health check timeout + DeregisterAfter time.Duration // Time to wait before deregistering unhealthy service + HTTP string // HTTP health check endpoint (e.g., "/healthz") +} + +// NewRegistry creates a new Consul-based service registry. +func NewRegistry(cfg Config) (*ConsulRegistry, error) { + consulConfig := consulapi.DefaultConfig() + if cfg.Address != "" { + consulConfig.Address = cfg.Address + } + if cfg.Datacenter != "" { + consulConfig.Datacenter = cfg.Datacenter + } + if cfg.Scheme != "" { + consulConfig.Scheme = cfg.Scheme + } + + client, err := consulapi.NewClient(consulConfig) + if err != nil { + return nil, fmt.Errorf("failed to create Consul client: %w", err) + } + + return &ConsulRegistry{ + client: client, + config: &cfg, + }, nil +} + +// Register registers a service instance with Consul. +func (r *ConsulRegistry) Register(ctx context.Context, service *registry.ServiceInstance) error { + registration := &consulapi.AgentServiceRegistration{ + ID: service.ID, + Name: service.Name, + Address: service.Address, + Port: service.Port, + Tags: service.Tags, + Meta: service.Metadata, + } + + // Add health check if configured + if r.config.HealthCheck.HTTP != "" { + healthCheckURL := fmt.Sprintf("http://%s:%d%s", service.Address, service.Port, r.config.HealthCheck.HTTP) + registration.Check = &consulapi.AgentServiceCheck{ + HTTP: healthCheckURL, + Interval: r.config.HealthCheck.Interval.String(), + Timeout: r.config.HealthCheck.Timeout.String(), + DeregisterCriticalServiceAfter: r.config.HealthCheck.DeregisterAfter.String(), + } + } + + return r.client.Agent().ServiceRegister(registration) +} + +// Deregister removes a service instance from Consul. +func (r *ConsulRegistry) Deregister(ctx context.Context, serviceID string) error { + return r.client.Agent().ServiceDeregister(serviceID) +} + +// Discover returns all healthy instances of a service. +func (r *ConsulRegistry) Discover(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + services, _, err := r.client.Health().Service(serviceName, "", true, nil) + if err != nil { + return nil, fmt.Errorf("failed to discover service %s: %w", serviceName, err) + } + + instances := make([]*registry.ServiceInstance, 0, len(services)) + for _, service := range services { + instances = append(instances, ®istry.ServiceInstance{ + ID: service.Service.ID, + Name: service.Service.Service, + Address: service.Service.Address, + Port: service.Service.Port, + Tags: service.Service.Tags, + Metadata: service.Service.Meta, + }) + } + + return instances, nil +} + +// Watch returns a channel that receives updates when service instances change. +func (r *ConsulRegistry) Watch(ctx context.Context, serviceName string) (<-chan []*registry.ServiceInstance, error) { + updates := make(chan []*registry.ServiceInstance, 10) + + go func() { + defer close(updates) + + lastIndex := uint64(0) + for { + select { + case <-ctx.Done(): + return + default: + services, meta, err := r.client.Health().Service(serviceName, "", true, &consulapi.QueryOptions{ + WaitIndex: lastIndex, + WaitTime: 10 * time.Second, + }) + if err != nil { + // Log error and continue + continue + } + + if meta.LastIndex != lastIndex { + instances := make([]*registry.ServiceInstance, 0, len(services)) + for _, service := range services { + instances = append(instances, ®istry.ServiceInstance{ + ID: service.Service.ID, + Name: service.Service.Service, + Address: service.Service.Address, + Port: service.Service.Port, + Tags: service.Service.Tags, + Metadata: service.Service.Meta, + }) + } + + select { + case updates <- instances: + case <-ctx.Done(): + return + } + + lastIndex = meta.LastIndex + } + } + } + }() + + return updates, nil +} + +// Health returns the health status of a service instance. +func (r *ConsulRegistry) Health(ctx context.Context, serviceID string) (*registry.HealthStatus, error) { + entries, _, err := r.client.Health().Service(serviceID, "", false, nil) + if err != nil { + return nil, fmt.Errorf("failed to get health for service %s: %w", serviceID, err) + } + + if len(entries) == 0 { + return ®istry.HealthStatus{ + ServiceID: serviceID, + Status: "unknown", + Message: "service not found", + }, nil + } + + // Check health status from service entry checks + status := "healthy" + message := "all checks passing" + + // Get the first entry (should be the service instance) + entry := entries[0] + for _, check := range entry.Checks { + if check.Status == consulapi.HealthCritical { + status = "critical" + message = check.Output + break + } else if check.Status == consulapi.HealthWarning { + status = "unhealthy" + message = check.Output + } + } + + return ®istry.HealthStatus{ + ServiceID: serviceID, + Status: status, + Message: message, + }, nil +} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go new file mode 100644 index 0000000..c78dbd8 --- /dev/null +++ b/pkg/registry/registry.go @@ -0,0 +1,41 @@ +// Package registry provides service registry interface for service discovery. +package registry + +import ( + "context" +) + +// ServiceRegistry is the interface for service discovery and registration. +type ServiceRegistry interface { + // Register registers a service instance with the registry. + Register(ctx context.Context, service *ServiceInstance) error + + // Deregister removes a service instance from the registry. + Deregister(ctx context.Context, serviceID string) error + + // Discover returns all healthy instances of a service. + Discover(ctx context.Context, serviceName string) ([]*ServiceInstance, error) + + // Watch returns a channel that receives updates when service instances change. + Watch(ctx context.Context, serviceName string) (<-chan []*ServiceInstance, error) + + // Health returns the health status of a service instance. + Health(ctx context.Context, serviceID string) (*HealthStatus, error) +} + +// ServiceInstance represents a service instance in the registry. +type ServiceInstance struct { + ID string // Unique instance ID + Name string // Service name (e.g., "auth-service", "identity-service") + Address string // Service address (IP or hostname) + Port int // Service port + Tags []string // Service tags for filtering + Metadata map[string]string // Additional metadata +} + +// HealthStatus represents the health status of a service instance. +type HealthStatus struct { + ServiceID string // Service instance ID + Status string // Health status: "healthy", "unhealthy", "critical" + Message string // Optional status message +} diff --git a/pkg/services/audit.go b/pkg/services/audit.go new file mode 100644 index 0000000..e01c901 --- /dev/null +++ b/pkg/services/audit.go @@ -0,0 +1,39 @@ +// Package services provides service client interfaces for inter-service communication. +package services + +import ( + "context" +) + +// AuditServiceClient is the interface for communicating with the Audit Service. +type AuditServiceClient interface { + // Record records an audit log entry. + Record(ctx context.Context, entry *AuditLogEntry) error + + // Query queries audit logs based on filters. + Query(ctx context.Context, filters *AuditLogFilters) ([]AuditLogEntry, error) +} + +// AuditLogEntry represents an audit log entry. +type AuditLogEntry struct { + UserID string `json:"user_id"` + Action string `json:"action"` // e.g., "user.create", "user.update" + Resource string `json:"resource"` // e.g., "user", "role" + ResourceID string `json:"resource_id"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + Metadata map[string]string `json:"metadata"` + Timestamp int64 `json:"timestamp"` +} + +// AuditLogFilters contains filters for querying audit logs. +type AuditLogFilters struct { + UserID *string `json:"user_id,omitempty"` + Action *string `json:"action,omitempty"` + Resource *string `json:"resource,omitempty"` + ResourceID *string `json:"resource_id,omitempty"` + StartTime *int64 `json:"start_time,omitempty"` + EndTime *int64 `json:"end_time,omitempty"` + Limit int `json:"limit"` // Max number of results + Offset int `json:"offset"` // Pagination offset +} diff --git a/pkg/services/auth.go b/pkg/services/auth.go new file mode 100644 index 0000000..eac69c0 --- /dev/null +++ b/pkg/services/auth.go @@ -0,0 +1,37 @@ +// Package services provides service client interfaces for inter-service communication. +package services + +import ( + "context" +) + +// AuthServiceClient is the interface for communicating with the Auth Service. +type AuthServiceClient interface { + // Login authenticates a user and returns access and refresh tokens. + Login(ctx context.Context, email, password string) (*TokenResponse, error) + + // RefreshToken refreshes an access token using a refresh token. + RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) + + // ValidateToken validates a JWT token and returns the token claims. + ValidateToken(ctx context.Context, token string) (*TokenClaims, error) + + // Logout invalidates a refresh token. + Logout(ctx context.Context, refreshToken string) error +} + +// TokenResponse contains the authentication tokens. +type TokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int64 `json:"expires_in"` // seconds + TokenType string `json:"token_type"` // "Bearer" +} + +// TokenClaims contains the claims from a validated JWT token. +type TokenClaims struct { + UserID string `json:"user_id"` + Email string `json:"email"` + Roles []string `json:"roles"` + ExpiresAt int64 `json:"expires_at"` +} diff --git a/pkg/services/authz.go b/pkg/services/authz.go new file mode 100644 index 0000000..ffec601 --- /dev/null +++ b/pkg/services/authz.go @@ -0,0 +1,37 @@ +// Package services provides service client interfaces for inter-service communication. +package services + +import ( + "context" +) + +// AuthzServiceClient is the interface for communicating with the Authz Service. +type AuthzServiceClient interface { + // Authorize checks if a user has a specific permission and returns an error if not. + Authorize(ctx context.Context, userID, permission string) error + + // HasPermission checks if a user has a specific permission. + HasPermission(ctx context.Context, userID, permission string) (bool, error) + + // GetUserPermissions returns all permissions for a user. + GetUserPermissions(ctx context.Context, userID string) ([]Permission, error) + + // GetUserRoles returns all roles for a user. + GetUserRoles(ctx context.Context, userID string) ([]Role, error) +} + +// Permission represents a permission in the system. +type Permission struct { + ID string `json:"id"` + Code string `json:"code"` + Name string `json:"name"` + Description string `json:"description"` +} + +// Role represents a role in the system. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Permissions []string `json:"permissions"` // Permission codes +} diff --git a/pkg/services/identity.go b/pkg/services/identity.go new file mode 100644 index 0000000..6ba4968 --- /dev/null +++ b/pkg/services/identity.go @@ -0,0 +1,62 @@ +// Package services provides service client interfaces for inter-service communication. +package services + +import ( + "context" +) + +// IdentityServiceClient is the interface for communicating with the Identity Service. +type IdentityServiceClient interface { + // GetUser retrieves a user by ID. + GetUser(ctx context.Context, id string) (*User, error) + + // GetUserByEmail retrieves a user by email address. + GetUserByEmail(ctx context.Context, email string) (*User, error) + + // CreateUser creates a new user. + CreateUser(ctx context.Context, user *CreateUserRequest) (*User, error) + + // UpdateUser updates an existing user. + UpdateUser(ctx context.Context, id string, user *UpdateUserRequest) (*User, error) + + // DeleteUser deletes a user. + DeleteUser(ctx context.Context, id string) error + + // VerifyEmail verifies a user's email address using a verification token. + VerifyEmail(ctx context.Context, token string) error + + // RequestPasswordReset requests a password reset token. + RequestPasswordReset(ctx context.Context, email string) error + + // ResetPassword resets a user's password using a reset token. + ResetPassword(ctx context.Context, token, newPassword string) error +} + +// User represents a user in the system. +type User struct { + ID string `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + EmailVerified bool `json:"email_verified"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// CreateUserRequest contains the data needed to create a new user. +type CreateUserRequest struct { + Email string `json:"email"` + Username string `json:"username"` + Password string `json:"password"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} + +// UpdateUserRequest contains the data needed to update a user. +type UpdateUserRequest struct { + Email *string `json:"email,omitempty"` + Username *string `json:"username,omitempty"` + FirstName *string `json:"first_name,omitempty"` + LastName *string `json:"last_name,omitempty"` +} diff --git a/services/gateway/gateway.go b/services/gateway/gateway.go new file mode 100644 index 0000000..4363b07 --- /dev/null +++ b/services/gateway/gateway.go @@ -0,0 +1,127 @@ +// Package gateway provides API Gateway implementation. +package gateway + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + + "git.dcentral.systems/toolz/goplt/internal/client" + "git.dcentral.systems/toolz/goplt/pkg/config" + "git.dcentral.systems/toolz/goplt/pkg/logger" + "git.dcentral.systems/toolz/goplt/pkg/registry" + "github.com/gin-gonic/gin" +) + +// Gateway handles routing requests to backend services. +type Gateway struct { + config config.ConfigProvider + log logger.Logger + clientFactory *client.ServiceClientFactory + registry registry.ServiceRegistry + routes []RouteConfig +} + +// RouteConfig defines a route configuration. +type RouteConfig struct { + Path string + Service string + AuthRequired bool +} + +// NewGateway creates a new API Gateway instance. +func NewGateway( + cfg config.ConfigProvider, + log logger.Logger, + clientFactory *client.ServiceClientFactory, + reg registry.ServiceRegistry, +) (*Gateway, error) { + // Load route configurations + routes := loadRoutes(cfg) + + return &Gateway{ + config: cfg, + log: log, + clientFactory: clientFactory, + registry: reg, + routes: routes, + }, nil +} + +// SetupRoutes configures routes on the Gin router. +func (g *Gateway) SetupRoutes(router *gin.Engine) { + // Setup route handlers + for _, route := range g.routes { + route := route // Capture for closure + router.Any(route.Path, g.handleRoute(route)) + } + + // Default handler for unmatched routes + router.NoRoute(func(c *gin.Context) { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Route not found", + "path": c.Request.URL.Path, + }) + }) +} + +// handleRoute returns a handler function for a route. +func (g *Gateway) handleRoute(route RouteConfig) gin.HandlerFunc { + return func(c *gin.Context) { + // TODO: Add authentication middleware if auth_required is true + // TODO: Add rate limiting middleware + // TODO: Add CORS middleware + + // Discover service instances + ctx := c.Request.Context() + instances, err := g.registry.Discover(ctx, route.Service) + if err != nil { + g.log.Error("Failed to discover service", + logger.String("service", route.Service), + logger.Error(err), + ) + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "Service unavailable", + }) + return + } + + if len(instances) == 0 { + g.log.Warn("No instances found for service", + logger.String("service", route.Service), + ) + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "Service unavailable", + }) + return + } + + // Use first healthy instance (load balancing can be added later) + instance := instances[0] + targetURL := fmt.Sprintf("http://%s:%d", instance.Address, instance.Port) + + // Create reverse proxy + target, err := url.Parse(targetURL) + if err != nil { + g.log.Error("Failed to parse target URL", + logger.String("url", targetURL), + logger.Error(err), + ) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal server error", + }) + return + } + + proxy := httputil.NewSingleHostReverseProxy(target) + proxy.ServeHTTP(c.Writer, c.Request) + } +} + +// loadRoutes loads route configurations from config. +func loadRoutes(cfg config.ConfigProvider) []RouteConfig { + // For now, return empty routes - will be loaded from config in future + // This is a placeholder implementation + return []RouteConfig{} +} diff --git a/services/gateway/internal/gateway.go b/services/gateway/internal/gateway.go new file mode 100644 index 0000000..4363b07 --- /dev/null +++ b/services/gateway/internal/gateway.go @@ -0,0 +1,127 @@ +// Package gateway provides API Gateway implementation. +package gateway + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + + "git.dcentral.systems/toolz/goplt/internal/client" + "git.dcentral.systems/toolz/goplt/pkg/config" + "git.dcentral.systems/toolz/goplt/pkg/logger" + "git.dcentral.systems/toolz/goplt/pkg/registry" + "github.com/gin-gonic/gin" +) + +// Gateway handles routing requests to backend services. +type Gateway struct { + config config.ConfigProvider + log logger.Logger + clientFactory *client.ServiceClientFactory + registry registry.ServiceRegistry + routes []RouteConfig +} + +// RouteConfig defines a route configuration. +type RouteConfig struct { + Path string + Service string + AuthRequired bool +} + +// NewGateway creates a new API Gateway instance. +func NewGateway( + cfg config.ConfigProvider, + log logger.Logger, + clientFactory *client.ServiceClientFactory, + reg registry.ServiceRegistry, +) (*Gateway, error) { + // Load route configurations + routes := loadRoutes(cfg) + + return &Gateway{ + config: cfg, + log: log, + clientFactory: clientFactory, + registry: reg, + routes: routes, + }, nil +} + +// SetupRoutes configures routes on the Gin router. +func (g *Gateway) SetupRoutes(router *gin.Engine) { + // Setup route handlers + for _, route := range g.routes { + route := route // Capture for closure + router.Any(route.Path, g.handleRoute(route)) + } + + // Default handler for unmatched routes + router.NoRoute(func(c *gin.Context) { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Route not found", + "path": c.Request.URL.Path, + }) + }) +} + +// handleRoute returns a handler function for a route. +func (g *Gateway) handleRoute(route RouteConfig) gin.HandlerFunc { + return func(c *gin.Context) { + // TODO: Add authentication middleware if auth_required is true + // TODO: Add rate limiting middleware + // TODO: Add CORS middleware + + // Discover service instances + ctx := c.Request.Context() + instances, err := g.registry.Discover(ctx, route.Service) + if err != nil { + g.log.Error("Failed to discover service", + logger.String("service", route.Service), + logger.Error(err), + ) + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "Service unavailable", + }) + return + } + + if len(instances) == 0 { + g.log.Warn("No instances found for service", + logger.String("service", route.Service), + ) + c.JSON(http.StatusServiceUnavailable, gin.H{ + "error": "Service unavailable", + }) + return + } + + // Use first healthy instance (load balancing can be added later) + instance := instances[0] + targetURL := fmt.Sprintf("http://%s:%d", instance.Address, instance.Port) + + // Create reverse proxy + target, err := url.Parse(targetURL) + if err != nil { + g.log.Error("Failed to parse target URL", + logger.String("url", targetURL), + logger.Error(err), + ) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal server error", + }) + return + } + + proxy := httputil.NewSingleHostReverseProxy(target) + proxy.ServeHTTP(c.Writer, c.Request) + } +} + +// loadRoutes loads route configurations from config. +func loadRoutes(cfg config.ConfigProvider) []RouteConfig { + // For now, return empty routes - will be loaded from config in future + // This is a placeholder implementation + return []RouteConfig{} +}