From f70d694d017bd63d218671a1225d848f36732a79 Mon Sep 17 00:00:00 2001 From: johnabass Date: Mon, 13 Jan 2025 12:31:05 -0800 Subject: [PATCH] refactored into a much simpler DI model --- config.go | 119 +++++++++++++++++++++ config_test.go | 118 +++++++++++++++++++++ constants_test.go | 9 -- doc.go | 2 +- go.mod | 18 ++-- go.sum | 36 +++---- option.go | 62 ----------- option_test.go | 111 -------------------- provide.go | 77 ++++++-------- provide_examples_test.go | 121 ++++++++++++++++++++++ provide_test.go | 218 ++++++++++----------------------------- 11 files changed, 470 insertions(+), 421 deletions(-) create mode 100644 config.go create mode 100644 config_test.go delete mode 100644 constants_test.go delete mode 100644 option.go delete mode 100644 option_test.go create mode 100644 provide_examples_test.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..7c9c3d7 --- /dev/null +++ b/config.go @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package praetor + +import ( + "time" + + "github.com/hashicorp/consul/api" +) + +// BasicAuthConfig holds the HTTP basic authorization credentials for Consul. +type BasicAuthConfig struct { + // UserName is the HTTP basic auth user name. + UserName string `json:"userName" yaml:"userName" mapstructure:"userName"` + + // Password is the HTTP basic auth user name. + Password string `json:"password" yaml:"password" mapstructure:"password"` +} + +// TLSConfig holds the TLS options supported by praetor. +type TLSConfig struct { + // Address is the optional address of the consul server. If set, this field's value + // is used as the TLS ServerName. + Address string `json:"address" yaml:"address" mapstructure:"address"` + + // CAFile is the system path to a CA certificate bundle used for Consul communication. + // Defaults to the system bundle if not specified. + CAFile string `json:"caFile" yaml:"caFile" mapstructure:"caFile"` + + // CAPath is the system directory of CA certificates used for Consul communication. + // Defaults to the system bundle if not specified. + CAPath string `json:"caPath" yaml:"caPath" mapstructure:"caPath"` + + // CertificateFile is the system file for the certificate used in Consul communication. + // If this is set, KeyFile must also be set. + CertificateFile string `json:"certificateFile" yaml:"certificateFile" mapstructure:"certificateFile"` + + // KeyFile is the system file for the key used in Consul communication. + // If this is set, CertificateFile must also be set. + KeyFile string `json:"keyFile" yaml:"keyFile" mapstructure:"keyFile"` + + // InsecureSkipVerify controls whether TLS host verification is disabled. + InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify" mapstructure:"insecureSkipVerify"` +} + +// Config is an easily unmarshalable configuration that praetor uses to create +// a consul api.Config. Fields in this struct mirror those of api.Config. +// +// An application can just unmarshal an api.Config directly, rather than using this type. +// This type provides struct tags to standardize fields across various libraries. +type Config struct { + // Scheme is the URI scheme of the consul server. + Scheme string `json:"scheme" yaml:"scheme" mapstructure:"scheme"` + + // Address is the address of the consul server, including port. + Address string `json:"address" yaml:"address" mapstructure:"address"` + + // PathPrefix is the URI path prefix to use when consul is behind an API gateway. + PathPrefix string `json:"pathPrefix" yaml:"pathPrefix" mapstructure:"pathPrefix"` + + // Datacenter is the optional datacenter to use when interacting with the agent. + // If unset, the datacenter of the agent is used. + Datacenter string `json:"datacenter" yaml:"datacenter" mapstructure:"datacenter"` + + // WaitTime specifies the time that watches will block. If unset, the agent's + // default will be used. + WaitTime time.Duration `json:"waitTime" yaml:"waitTime" mapstructure:"waitTime"` + + // Token is a per request ACL token. If unset, the agent's token is used. + Token string `json:"token" yaml:"token" mapstructure:"token"` + + // TokenFile is a file containing the per request ACL token. + TokenFile string `json:"tokenFile" yaml:"tokenFile" mapstructure:"tokenFile"` + + // Namespace is the namespace to send to the agent in requests where no namespace is set. + Namespace string `json:"namespace" yaml:"namespace" mapstructure:"namespace"` + + // Partition is the partition to send to the agent in requests where no namespace is set. + Partition string `json:"partition" yaml:"partition" mapstructure:"partition"` + + // BasicAuth defines the HTTP basic credentials for interacting with the agent. + BasicAuth BasicAuthConfig `json:"basicAuth" yaml:"basicAuth" mapstructure:"basicAuth"` + + // TLS defines the TLS configuration to use for the consul server. + TLS TLSConfig `json:"tls" yaml:"tls" mapstructure:"tls"` +} + +// NewAPIConfig constructs a consul client api.Config from a praetor configuration. +func NewAPIConfig(src Config) (dst api.Config, err error) { + dst = api.Config{ + Scheme: src.Scheme, + Address: src.Address, + PathPrefix: src.PathPrefix, + Datacenter: src.Datacenter, + WaitTime: src.WaitTime, + Token: src.Token, + TokenFile: src.TokenFile, + Namespace: src.Namespace, + Partition: src.Partition, + TLSConfig: api.TLSConfig{ + Address: src.TLS.Address, + CAFile: src.TLS.CAFile, + CAPath: src.TLS.CAPath, + CertFile: src.TLS.CertificateFile, + KeyFile: src.TLS.KeyFile, + InsecureSkipVerify: src.TLS.InsecureSkipVerify, + }, + } + + if len(src.BasicAuth.UserName) > 0 { + dst.HttpAuth = &api.HttpBasicAuth{ + Username: src.BasicAuth.UserName, + Password: src.BasicAuth.Password, + } + } + + return +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..496286d --- /dev/null +++ b/config_test.go @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package praetor + +import ( + "testing" + "time" + + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/suite" +) + +type ConfigTestSuite struct { + suite.Suite +} + +func (suite *ConfigTestSuite) newAPIConfig(src Config) api.Config { + dst, err := NewAPIConfig(src) + suite.Require().NoError(err) + return dst +} + +// newSimpleConfig creates a praetor Config with the simple fields set. +func (suite *ConfigTestSuite) newSimpleConfig() Config { + return Config{ + Scheme: "ftp", + Address: "foobar:8080", + PathPrefix: "/prefix", + Datacenter: "abc", + WaitTime: 5 * time.Minute, + Token: "xyz", + TokenFile: "/etc/app/token", + Namespace: "namespace", + Partition: "partition", + } +} + +// assertSimpleFields asserts that the given consul api.Config's simple fields +// matches what is set by newSimpleConfig. +func (suite *ConfigTestSuite) assertSimpleFields(cfg api.Config) { + suite.Equal("ftp", cfg.Scheme) + suite.Equal("foobar:8080", cfg.Address) + suite.Equal("/prefix", cfg.PathPrefix) + suite.Equal("abc", cfg.Datacenter) + suite.Equal(5*time.Minute, cfg.WaitTime) + suite.Equal("xyz", cfg.Token) + suite.Equal("/etc/app/token", cfg.TokenFile) + suite.Equal("namespace", cfg.Namespace) + suite.Equal("partition", cfg.Partition) + suite.Nil(cfg.HttpClient) + suite.Nil(cfg.Transport) +} + +func (suite *ConfigTestSuite) testNewAPIConfigSimple() { + cfg := suite.newAPIConfig( + suite.newSimpleConfig(), + ) + + suite.assertSimpleFields(cfg) + suite.Nil(cfg.HttpAuth) + suite.Equal(api.TLSConfig{}, cfg.TLSConfig) +} + +func (suite *ConfigTestSuite) testNewAPIConfigHttpAuth() { + src := suite.newSimpleConfig() + src.BasicAuth.UserName = "user" + src.BasicAuth.Password = "password" + + cfg := suite.newAPIConfig(src) + + suite.assertSimpleFields(cfg) + suite.Equal(api.TLSConfig{}, cfg.TLSConfig) + suite.Require().NotNil(cfg.HttpAuth) + suite.Equal( + api.HttpBasicAuth{ + Username: "user", + Password: "password", + }, + *cfg.HttpAuth, + ) +} + +func (suite *ConfigTestSuite) testNewAPIConfigTLS() { + src := suite.newSimpleConfig() + src.TLS.Address = "foobar:9090" + src.TLS.CAFile = "/etc/app/cafile" + src.TLS.CAPath = "/etc/app/capath" + src.TLS.CertificateFile = "/etc/app/certificateFile" + src.TLS.KeyFile = "/etc/app/keyFile" + src.TLS.InsecureSkipVerify = true + + cfg := suite.newAPIConfig(src) + + suite.assertSimpleFields(cfg) + suite.Nil(cfg.HttpAuth) + suite.Equal( + api.TLSConfig{ + Address: "foobar:9090", + CAFile: "/etc/app/cafile", + CAPath: "/etc/app/capath", + CertFile: "/etc/app/certificateFile", + KeyFile: "/etc/app/keyFile", + InsecureSkipVerify: true, + }, + cfg.TLSConfig, + ) +} + +func (suite *ConfigTestSuite) TestNewAPIConfig() { + suite.Run("Simple", suite.testNewAPIConfigSimple) + suite.Run("HttpAuth", suite.testNewAPIConfigHttpAuth) + suite.Run("TLS", suite.testNewAPIConfigTLS) +} + +func TestConfig(t *testing.T) { + suite.Run(t, new(ConfigTestSuite)) +} diff --git a/constants_test.go b/constants_test.go deleted file mode 100644 index 1f88d43..0000000 --- a/constants_test.go +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package praetor - -const ( - testAddress = "localhost:1234" - testScheme = "https" -) diff --git a/doc.go b/doc.go index 9187047..7ce11f9 100644 --- a/doc.go +++ b/doc.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC // SPDX-License-Identifier: Apache-2.0 /* diff --git a/go.mod b/go.mod index 1824551..637da7e 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,36 @@ module github.com/xmidt-org/praetor -go 1.21 +go 1.22.0 + +toolchain go1.23.4 require ( github.com/hashicorp/consul/api v1.31.0 github.com/stretchr/testify v1.10.0 go.uber.org/fx v1.23.0 - go.uber.org/multierr v1.11.0 ) require ( github.com/armon/go-metrics v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.18.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-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect go.uber.org/dig v1.18.0 // indirect + go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/sys v0.19.0 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect + golang.org/x/sys v0.29.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5d0619a..39a28a9 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc 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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 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= @@ -39,8 +39,8 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/hashicorp/consul/api v1.31.0 h1:32BUNLembeSRek0G/ZAM6WNfdEwYdYo8oQ4+JoqGkNQ= github.com/hashicorp/consul/api v1.31.0/go.mod h1:2ZGIiXM3A610NmDULmCHd/aqBJj8CkMfOhswhOafxRg= @@ -52,8 +52,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv 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-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -91,6 +91,7 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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= @@ -101,14 +102,13 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc 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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -126,8 +126,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -173,15 +171,15 @@ go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 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/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 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= @@ -215,10 +213,9 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc 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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= @@ -230,9 +227,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= diff --git a/option.go b/option.go deleted file mode 100644 index 7e63cf8..0000000 --- a/option.go +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package praetor - -import ( - "net/http" - "reflect" - - "github.com/hashicorp/consul/api" -) - -// Option is a functional option for tailoring the consul client -// configuration prior to creating it. Each option can modify the -// *api.Config prior to it being passed to api.NewClient. -type Option func(*api.Config) error - -var ( - optionType = reflect.TypeOf(Option(nil)) - noErrorOptionType = reflect.TypeOf((func(*api.Config))(nil)) -) - -// OptionFunc represents the types of functions that can be coerced into Options. -type OptionFunc interface { - ~func(*api.Config) error | ~func(*api.Config) -} - -// AsOption coerces a function into an Option. -func AsOption[OF OptionFunc](of OF) Option { - // trivial conversions - switch oft := any(of).(type) { - case Option: - return oft - - case func(*api.Config): - return func(cfg *api.Config) error { - oft(cfg) - return nil - } - } - - // now we convert to the underlying type - ofv := reflect.ValueOf(of) - if ofv.CanConvert(optionType) { - return ofv.Convert(optionType).Interface().(Option) - } - - // there are only (2) types, so the other type must be it - f := ofv.Convert(noErrorOptionType).Interface().(func(*api.Config)) - return func(cfg *api.Config) error { - f(cfg) - return nil - } -} - -// WithHTTPClient configures the consul client with a custom HTTP client. -func WithHTTPClient(client *http.Client) Option { - return func(cfg *api.Config) error { - cfg.HttpClient = client - return nil - } -} diff --git a/option_test.go b/option_test.go deleted file mode 100644 index 50c68f3..0000000 --- a/option_test.go +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC -// SPDX-License-Identifier: Apache-2.0 - -package praetor - -import ( - "errors" - "net/http" - "testing" - - "github.com/hashicorp/consul/api" - "github.com/stretchr/testify/suite" -) - -type OptionSuite struct { - suite.Suite -} - -func (suite *OptionSuite) testAsOptionWithOption() { - suite.Run("Success", func() { - opt := Option(func(cfg *api.Config) error { - cfg.Address = testAddress - return nil - }) - - var cfg api.Config - err := AsOption(opt)(&cfg) - suite.NoError(err) - suite.Equal(testAddress, cfg.Address) - }) - - suite.Run("Fail", func() { - expectedErr := errors.New("expected") - opt := Option(func(cfg *api.Config) error { - return expectedErr - }) - - var cfg api.Config - err := AsOption(opt)(&cfg) - suite.ErrorIs(err, expectedErr) - }) -} - -func (suite *OptionSuite) testAsOptionWithClosure() { - suite.Run("Success", func() { - opt := func(cfg *api.Config) error { - cfg.Address = testAddress - return nil - } - - var cfg api.Config - err := AsOption(opt)(&cfg) - suite.NoError(err) - suite.Equal(testAddress, cfg.Address) - }) - - suite.Run("Fail", func() { - expectedErr := errors.New("expected") - opt := func(cfg *api.Config) error { - return expectedErr - } - - var cfg api.Config - err := AsOption(opt)(&cfg) - suite.ErrorIs(err, expectedErr) - }) -} - -func (suite *OptionSuite) testAsOptionNoError() { - opt := func(cfg *api.Config) { - cfg.Address = testAddress - } - - var cfg api.Config - err := AsOption(opt)(&cfg) - suite.NoError(err) - suite.Equal(testAddress, cfg.Address) -} - -func (suite *OptionSuite) testAsOptionCustomType() { - type TestFunc func(*api.Config) - var opt TestFunc = func(cfg *api.Config) { - cfg.Address = testAddress - } - - var cfg api.Config - err := AsOption(opt)(&cfg) - suite.NoError(err) - suite.Equal(testAddress, cfg.Address) -} - -func (suite *OptionSuite) TestAsOption() { - suite.Run("WithOption", suite.testAsOptionWithOption) - suite.Run("WithClosure", suite.testAsOptionWithClosure) - suite.Run("NoError", suite.testAsOptionNoError) - suite.Run("CustomType", suite.testAsOptionCustomType) -} - -func (suite *OptionSuite) TestWithHTTPClient() { - c := new(http.Client) - var cfg api.Config - suite.NoError( - WithHTTPClient(c)(&cfg), - ) - - suite.Same(c, cfg.HttpClient) -} - -func TestOption(t *testing.T) { - suite.Run(t, new(OptionSuite)) -} diff --git a/provide.go b/provide.go index 278e3d2..98f1902 100644 --- a/provide.go +++ b/provide.go @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC // SPDX-License-Identifier: Apache-2.0 package praetor @@ -6,59 +6,50 @@ package praetor import ( "github.com/hashicorp/consul/api" "go.uber.org/fx" - "go.uber.org/multierr" ) -// Decorate is an uber/fx decorator that returns a new consul client Config -// that results from applying any number of options to an existing Config. -// If no options are supplied, this function returns a clone of the original. -func Decorate(original api.Config, opts ...Option) (cfg api.Config, err error) { - cfg = original - for _, o := range opts { - err = multierr.Append(err, o(&cfg)) - } +func newClient(cfg api.Config) (*api.Client, error) { + return api.NewClient(&cfg) +} - return +func newAgent(c *api.Client) *api.Agent { + return c.Agent() } -// New is the standard constructor for a consul client. It allows for -// any number of options to tailor the configuration after the api.Config has -// been unmarshaled or obtained from some external source. -// -// This function may be used directly with fx.Provide as a constructor. More -// commonly, the Provide function in this package is preferred since it allows -// simpler annotation. -func New(cfg api.Config, opts ...Option) (c *api.Client, err error) { - cfg, err = Decorate(cfg, opts...) - if err == nil { - c, err = api.NewClient(&cfg) - } +func newCatalog(c *api.Client) *api.Catalog { + return c.Catalog() +} - return +func newHealth(c *api.Client) *api.Health { + return c.Health() } -// Provide gives a very simple, opinionated way of using New within an fx.App. -// It assumes a global, unnamed api.Config optional dependency and zero or more ClientOptions -// in a value group named 'consul.options'. +// Provide sets up the dependency injection infrastructure for Consul. +// This provider expects an api.Config to be present in the application +// (NOT an *api.Config). In order to bootstrap using praetor's cofiguration, +// use ProvideConfig in addition to this function. // -// Zero or more options that are external to the enclosing fx.App may be supplied to this -// provider function. This allows the consul Client to be modified by command-line options, -// hardcoded values, etc. Any external options supplied to this function take precedence -// over injected options. +// The following components are emitted by this provider: // -// This provider emits a global, unnamed *api.Client. -func Provide(external ...Option) fx.Option { - ctor := New - if len(external) > 0 { - ctor = func(cfg api.Config, injected ...Option) (*api.Client, error) { - return New(cfg, append(injected, external...)...) - } - } +// - *api.Client +// - *api.Agent +// - *api.Catalog +// - *api.Health +func Provide() fx.Option { + return fx.Provide( + newClient, + newAgent, + newCatalog, + newHealth, + ) +} +// ProvideConfig bootstraps an api.Config using a praetor Config. +// +// NOTE: In order to inject a custom *http.Client or *http.Transport, +// use fx.Decorate and decorate the api.Config. +func ProvideConfig() fx.Option { return fx.Provide( - fx.Annotate( - ctor, - fx.ParamTags(`optional:"true"`, `group:"consul.options"`), - ), + NewAPIConfig, ) } diff --git a/provide_examples_test.go b/provide_examples_test.go new file mode 100644 index 0000000..4aa9fad --- /dev/null +++ b/provide_examples_test.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC +// SPDX-License-Identifier: Apache-2.0 + +package praetor + +import ( + "fmt" + "net/http" + "time" + + "github.com/hashicorp/consul/api" + "go.uber.org/fx" +) + +func ExampleProvide_simple() { + fx.New( + fx.NopLogger, + fx.Supply(api.Config{}), // this consul client config can be obtained however desired + Provide(), + fx.Invoke( + // code and have any of these types as dependencies: + + func(client *api.Client) { + fmt.Println("client") + }, + func(agent *api.Agent) { + fmt.Println("agent") + }, + func(agent *api.Catalog) { + fmt.Println("catalog") + }, + func(agent *api.Health) { + fmt.Println("health") + }, + ), + ) + + // Output: + // client + // agent + // catalog + // health +} + +func ExampleProvide_useconfig() { + fx.New( + fx.NopLogger, + // this praetor Config can be obtained externally, e.g. unmarshaled + fx.Supply(Config{ + Scheme: "https", + Address: "foobar:8080", + }), + ProvideConfig(), + Provide(), + fx.Invoke( + func(client *api.Client) { + fmt.Println("client") + }, + func(agent *api.Agent) { + fmt.Println("agent") + }, + func(agent *api.Catalog) { + fmt.Println("catalog") + }, + func(agent *api.Health) { + fmt.Println("health") + }, + ), + ) + + // Output: + // client + // agent + // catalog + // health +} + +func ExampleProvide_injectcustomclient() { + fx.New( + fx.NopLogger, + fx.Supply(Config{ + Scheme: "https", + Address: "foobar:8080", + }), + fx.Supply( + // we want to use this HTTP client for consul + &http.Client{ + Timeout: 5 * time.Minute, + }, + ), + // use standard fx decoration to add a custom HTTP client + fx.Decorate( + func(original api.Config, customClient *http.Client) api.Config { + original.HttpClient = customClient + return original + }, + ), + ProvideConfig(), + Provide(), + fx.Invoke( + func(client *api.Client) { + fmt.Println("client") + }, + func(agent *api.Agent) { + fmt.Println("agent") + }, + func(agent *api.Catalog) { + fmt.Println("catalog") + }, + func(agent *api.Health) { + fmt.Println("health") + }, + ), + ) + + // Output: + // client + // agent + // catalog + // health +} diff --git a/provide_test.go b/provide_test.go index 0d06b4b..dea7d3e 100644 --- a/provide_test.go +++ b/provide_test.go @@ -1,11 +1,9 @@ -// SPDX-FileCopyrightText: 2023 Comcast Cable Communications Management, LLC +// SPDX-FileCopyrightText: 2025 Comcast Cable Communications Management, LLC // SPDX-License-Identifier: Apache-2.0 package praetor import ( - "errors" - "net/http" "testing" "github.com/hashicorp/consul/api" @@ -18,180 +16,68 @@ type ProvideSuite struct { suite.Suite } -func (suite *ProvideSuite) testDecorateSuccess() { - original := api.Config{ - Address: testAddress, - } - - decorated, err := Decorate( - original, - func(cfg *api.Config) error { - cfg.Scheme = testScheme - return nil - }, - ) - - suite.NoError(err) - suite.Equal(api.Config{Address: testAddress, Scheme: testScheme}, decorated) -} - -func (suite *ProvideSuite) testDecorateOptionError() { - original := api.Config{ - Address: testAddress, - } - - expectedErr := errors.New("expected") - - _, actualErr := Decorate( - original, - func(cfg *api.Config) error { - return expectedErr - }, - ) - - suite.ErrorIs(actualErr, expectedErr) -} - -func (suite *ProvideSuite) TestDecorate() { - suite.Run("Success", suite.testDecorateSuccess) - suite.Run("OptionError", suite.testDecorateOptionError) -} - -func (suite *ProvideSuite) testNewSuccess() { - c, err := New( - api.Config{ - Address: testAddress, - }, - func(cfg *api.Config) error { - cfg.Scheme = testScheme - return nil - }, - func(cfg *api.Config) error { - cfg.HttpClient = new(http.Client) - return nil - }, - ) - - suite.NoError(err) - suite.NotNil(c) -} - -func (suite *ProvideSuite) testNewNoOptions() { - c, err := New( - api.Config{ - Address: testAddress, - }, - ) - - suite.NoError(err) - suite.NotNil(c) -} - -func (suite *ProvideSuite) testNewOptionError() { - expectedErr := errors.New("expected") - - c, actualErr := New( - api.Config{ - Address: testAddress, - }, - func(*api.Config) error { - return expectedErr - }, - ) - - suite.ErrorIs(actualErr, expectedErr) - suite.Nil(c) -} - -func (suite *ProvideSuite) TestNew() { - suite.Run("Success", suite.testNewSuccess) - suite.Run("NoOptions", suite.testNewNoOptions) - suite.Run("OptionError", suite.testNewOptionError) -} - -func (suite *ProvideSuite) testProvideDefault() { - var c *api.Client - app := fxtest.New( - suite.T(), - Provide(), - fx.Populate(&c), - ) - - suite.NoError(app.Err()) - suite.NotNil(c) -} - -func (suite *ProvideSuite) testProvideWithOptions() { +func (suite *ProvideSuite) TestProvide() { var ( - c *api.Client - hc = new(http.Client) - - external1 = func(cfg *api.Config) { - // injected options should execute first - suite.Equal("different:9999", cfg.Address) - suite.Equal(testScheme, cfg.Scheme) - cfg.Address = testAddress - } - - external2 = WithHTTPClient(hc) - ) - - app := fxtest.New( - suite.T(), - fx.Supply( - fx.Annotate( - Option(func(cfg *api.Config) error { - cfg.Address = "different:9999" - return nil - }), - fx.ResultTags(`group:"consul.options"`), - ), - fx.Annotate( - Option(func(cfg *api.Config) error { - cfg.Scheme = testScheme - return nil - }), - fx.ResultTags(`group:"consul.options"`), + client *api.Client + agent *api.Agent + catalog *api.Catalog + health *api.Health + + app = fxtest.New( + suite.T(), + fx.Supply(api.Config{}), + Provide(), + fx.Populate( + &client, + &agent, + &catalog, + &health, ), - ), - Provide(AsOption(external1), external2), - fx.Populate(&c), + ) ) suite.NoError(app.Err()) - suite.NotNil(c) + suite.NotNil(client) + suite.NotNil(agent) + suite.NotNil(catalog) + suite.NotNil(health) } -func (suite *ProvideSuite) testProvideWithConfig() { - var c *api.Client - app := fxtest.New( - suite.T(), - fx.Supply( - api.Config{ - Address: "original:8888", - }, - fx.Annotate( - Option(func(cfg *api.Config) error { - // the original configuration should be visible here - suite.Equal("original:8888", cfg.Address) - cfg.Address = "different:9999" - return nil - }), - fx.ResultTags(`group:"consul.options"`), +func (suite *ProvideSuite) TestProvideConfig() { + var ( + config api.Config + client *api.Client + agent *api.Agent + catalog *api.Catalog + health *api.Health + + app = fxtest.New( + suite.T(), + fx.Supply( + Config{ + Scheme: "http", + Address: "foobar:8080", + }, + ), + Provide(), + ProvideConfig(), + fx.Populate( + &config, + &client, + &agent, + &catalog, + &health, ), - ), - Provide(), - fx.Populate(&c), + ) ) suite.NoError(app.Err()) - suite.NotNil(c) -} - -func (suite *ProvideSuite) TestProvide() { - suite.Run("Default", suite.testProvideDefault) - suite.Run("WithOptions", suite.testProvideWithOptions) - suite.Run("WithConfig", suite.testProvideWithConfig) + suite.Equal("http", config.Scheme) + suite.Equal("foobar:8080", config.Address) + suite.NotNil(client) + suite.NotNil(agent) + suite.NotNil(catalog) + suite.NotNil(health) } func TestProvide(t *testing.T) {