Skip to content

Commit 99f089b

Browse files
committed
feat(loader): add viper-backed shared config loader for downstream tools
Introduces a new loader/ subpackage that resolves Keyfactor config from layered sources (CLI flag > env > config file > defaults) and returns a populated auth_providers.Server. Designed so multiple Keyfactor Go tools (kfacme-cli, kfutil, terraform-provider-keyfactor, future tools) can share the same env vars and the same ~/.keyfactor/command_config.{json,yml,yaml} layout, with each tool's extension fields living in a named sub-block (e.g. acme:) under each Server entry. What this PR adds: * New `loader/` subpackage. Viper is the only new transitive dependency, and it stays scoped to this subpackage — consumers that don't import loader keep their slim dep trees. * `Loader` type with functional options: WithConfigFile, WithProfile, WithFlagSet (for cobra/pflag integration), WithDefaults, WithToolNamespace. * File discovery preserves kfc-auth's existing convention: explicit --config-file flag > WithConfigFile option > KEYFACTOR_AUTH_CONFIG_FILE > ~/.keyfactor/command_config.{json,yaml,yml}. * Profile selection: WithProfile > KEYFACTOR_AUTH_CONFIG_PROFILE > DefaultConfigProfile ("default"). * Per-tool sub-blocks under each Server, decoded into caller-supplied structs via Loader.DecodeExtras(namespace, target). Tool-namespaced env vars (e.g. KEYFACTOR_ACME_BASE_URL → servers.<profile>.acme.base_url) are bound by reflection over the schema struct's mapstructure tags. * Override merge with strict mode: any auth-method-specific field set in a sub-block forces the sub-block to declare the entire tuple; partial overrides return a clear error naming the missing field. This catches "I forgot client_secret" mistakes at load time rather than at API call time. See ResolvedAuth() docs and the partial_override.yaml fixture. What this PR adds for the validation split (Phase 2, folded in): * New `auth_providers.AuthCreds` type: the auth-only view of a Server, decoupled from Command target fields (Host / Port / APIPath). Lets callers validate credentials independently of whether they're targeting Command, ACME, or anything else. * `AuthCreds.Validate()` returns the resolved auth method (basic / oauth2 / token / kerberos) or a structured error. * `AuthCredsFromServer()` extracts the auth fields from a Server. * Existing CommandAuthConfig.ValidateAuthConfig() is UNCHANGED in signature and behavior — kfutil and terraform-provider keep validating "auth + Command URL + reachability" exactly as before. The new AuthCreds-only path is opt-in for callers that don't target Command. What this PR adds to the Server struct (additive, non-breaking): * New `Extras map[string]any` field with `json:"-" yaml:"-" mapstructure:",remain"`. The loader populates it with the unknown keys at each Server entry. Existing JSON/YAML wire format is unchanged because the tags exclude Extras from direct marshaling; the loader handles sub-block decoding separately. Tests: * loader/loader_test.go covers shared-creds inheritance, separate-creds override, ACME-only profile (no Host), mixed auth methods per tool, partial-override rejection, profile selection, env-var override, tool-namespace env binding, and the AuthCreds.Validate matrix. * Existing auth_providers tests continue to pass on the unchanged code paths. Pre-existing Azure / config-format failures on origin/release-v1.6 are unaffected by this PR. Backward compatibility: * Every existing exported symbol in auth_providers/ keeps its signature. * Server gains one field (Extras) with marshal-suppressing tags; existing serialization paths produce identical output. * No consumer is forced to import loader/ — opt-in adoption. Follow-up work (separate PRs in other repos): * kfacme-cli migrates internal/config to use loader/, with a one-time ~/.keyfactor/kfacme_config.json → ~/.keyfactor/command_config.yaml migrator. * kfutil wires its currently-defined-but-ignored flags through the loader and retires its local ConfigurationFile mirror types. * terraform-provider-keyfactor falls back to the loader when the HCL provider block is incomplete.
1 parent e114c03 commit 99f089b

18 files changed

Lines changed: 1694 additions & 15 deletions

auth_providers/auth_credentials.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright 2026 Keyfactor
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package auth_providers
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
)
21+
22+
// AuthMethod is the credential family that satisfies a Keyfactor auth
23+
// request. Returned by AuthCreds.Validate.
24+
type AuthMethod string
25+
26+
const (
27+
// AuthMethodUnset means no complete credential tuple was found.
28+
AuthMethodUnset AuthMethod = ""
29+
// AuthMethodBasic requires Username + Password (and optionally Domain).
30+
AuthMethodBasic AuthMethod = "basic"
31+
// AuthMethodOAuth2 requires ClientID + ClientSecret + TokenURL.
32+
AuthMethodOAuth2 AuthMethod = "oauth2"
33+
// AuthMethodToken requires AccessToken (a static bearer).
34+
AuthMethodToken AuthMethod = "token"
35+
// AuthMethodKerberos requires at least one of:
36+
// KerberosKeytab,
37+
// KerberosCCache,
38+
// (Username + Password + KerberosRealm).
39+
AuthMethodKerberos AuthMethod = "kerberos"
40+
)
41+
42+
// AuthCreds is the auth-only view of a Server, decoupled from Command
43+
// target fields (Host/Port/APIPath). It lets callers validate
44+
// credentials independently of whether they're targeting Command,
45+
// ACME, or anything else.
46+
//
47+
// AuthCreds is intentionally a flat struct so it round-trips cleanly
48+
// through Viper / mapstructure. The loader subpackage uses it for
49+
// sub-block override merging.
50+
type AuthCreds struct {
51+
// AuthType is an optional explicit method selector ("basic",
52+
// "oauth2", "token", "kerberos"). When set it disambiguates
53+
// otherwise-overlapping inputs (e.g. an OAuth2 client_id and a
54+
// static AccessToken set on the same profile). When unset,
55+
// Validate infers the method from which fields are populated.
56+
AuthType string `mapstructure:"auth_type" json:"auth_type,omitempty" yaml:"auth_type,omitempty"`
57+
58+
// Basic
59+
Username string `mapstructure:"username" json:"username,omitempty" yaml:"username,omitempty"`
60+
Password string `mapstructure:"password" json:"password,omitempty" yaml:"password,omitempty"`
61+
Domain string `mapstructure:"domain" json:"domain,omitempty" yaml:"domain,omitempty"`
62+
63+
// OAuth2 client credentials
64+
ClientID string `mapstructure:"client_id" json:"client_id,omitempty" yaml:"client_id,omitempty"`
65+
ClientSecret string `mapstructure:"client_secret" json:"client_secret,omitempty" yaml:"client_secret,omitempty"`
66+
TokenURL string `mapstructure:"token_url" json:"token_url,omitempty" yaml:"token_url,omitempty"`
67+
Scopes []string `mapstructure:"scopes" json:"scopes,omitempty" yaml:"scopes,omitempty"`
68+
Audience string `mapstructure:"audience" json:"audience,omitempty" yaml:"audience,omitempty"`
69+
70+
// Static bearer
71+
AccessToken string `mapstructure:"access_token" json:"access_token,omitempty" yaml:"access_token,omitempty"`
72+
73+
// Kerberos
74+
KerberosRealm string `mapstructure:"kerberos_realm" json:"kerberos_realm,omitempty" yaml:"kerberos_realm,omitempty"`
75+
KerberosKeytab string `mapstructure:"kerberos_keytab" json:"kerberos_keytab,omitempty" yaml:"kerberos_keytab,omitempty"`
76+
KerberosConfig string `mapstructure:"kerberos_config" json:"kerberos_config,omitempty" yaml:"kerberos_config,omitempty"`
77+
KerberosCCache string `mapstructure:"kerberos_ccache" json:"kerberos_ccache,omitempty" yaml:"kerberos_ccache,omitempty"`
78+
KerberosSPN string `mapstructure:"kerberos_spn" json:"kerberos_spn,omitempty" yaml:"kerberos_spn,omitempty"`
79+
}
80+
81+
// AuthCredsFromServer extracts the credential fields from a Server into
82+
// a standalone AuthCreds. Used by the loader to build the server-level
83+
// view before applying per-tool sub-block overrides.
84+
func AuthCredsFromServer(s *Server) *AuthCreds {
85+
if s == nil {
86+
return &AuthCreds{}
87+
}
88+
return &AuthCreds{
89+
AuthType: s.AuthType,
90+
Username: s.Username,
91+
Password: s.Password,
92+
Domain: s.Domain,
93+
ClientID: s.ClientID,
94+
ClientSecret: s.ClientSecret,
95+
TokenURL: s.OAuthTokenUrl,
96+
Scopes: append([]string(nil), s.Scopes...),
97+
Audience: s.Audience,
98+
AccessToken: s.AccessToken,
99+
KerberosRealm: s.KerberosRealm,
100+
KerberosKeytab: s.KerberosKeytab,
101+
KerberosConfig: s.KerberosConfig,
102+
KerberosCCache: s.KerberosCCache,
103+
KerberosSPN: s.KerberosSPN,
104+
}
105+
}
106+
107+
// Validate reports whether the credentials form a complete tuple for
108+
// some auth method, and returns the resolved method.
109+
//
110+
// Method selection rules (first match wins):
111+
// 1. If AuthType is set, it forces the chosen method; all required
112+
// fields for that method must be present.
113+
// 2. Otherwise, the first method whose required fields are fully
114+
// populated is selected.
115+
//
116+
// Validation is strict: a partially-populated method (e.g. ClientID
117+
// without ClientSecret) returns an error naming the missing fields
118+
// even when another method would have been complete. This catches
119+
// configuration mistakes early.
120+
func (a *AuthCreds) Validate() (AuthMethod, error) {
121+
if a == nil {
122+
return AuthMethodUnset, fmt.Errorf("auth credentials are nil")
123+
}
124+
125+
// What's populated, by method.
126+
hasBasic := a.Username != "" || a.Password != ""
127+
hasOAuth2 := a.ClientID != "" || a.ClientSecret != "" || a.TokenURL != ""
128+
hasToken := a.AccessToken != ""
129+
hasKerberos := a.KerberosRealm != "" || a.KerberosKeytab != "" || a.KerberosCCache != "" || a.KerberosSPN != ""
130+
131+
// Forced by AuthType.
132+
if a.AuthType != "" {
133+
switch strings.ToLower(a.AuthType) {
134+
case string(AuthMethodBasic):
135+
return AuthMethodBasic, validateBasic(a)
136+
case string(AuthMethodOAuth2):
137+
return AuthMethodOAuth2, validateOAuth2(a)
138+
case string(AuthMethodToken):
139+
return AuthMethodToken, validateToken(a)
140+
case string(AuthMethodKerberos):
141+
return AuthMethodKerberos, validateKerberos(a)
142+
default:
143+
return AuthMethodUnset, fmt.Errorf("unknown auth_type %q (expected basic, oauth2, token, or kerberos)", a.AuthType)
144+
}
145+
}
146+
147+
// Strict-mode partial detection: if a method's first field is set
148+
// but the rest are not, it's an error even if another method would
149+
// have validated. This makes "I forgot client_secret" produce a
150+
// clear message instead of silently falling through.
151+
if hasOAuth2 {
152+
if err := validateOAuth2(a); err != nil {
153+
return AuthMethodUnset, err
154+
}
155+
return AuthMethodOAuth2, nil
156+
}
157+
if hasBasic {
158+
if err := validateBasic(a); err != nil {
159+
return AuthMethodUnset, err
160+
}
161+
return AuthMethodBasic, nil
162+
}
163+
if hasToken {
164+
// Token only needs AccessToken; validateToken is just a
165+
// non-empty check.
166+
return AuthMethodToken, validateToken(a)
167+
}
168+
if hasKerberos {
169+
if err := validateKerberos(a); err != nil {
170+
return AuthMethodUnset, err
171+
}
172+
return AuthMethodKerberos, nil
173+
}
174+
175+
return AuthMethodUnset, fmt.Errorf("no auth credentials configured (set username/password, client_id/client_secret/token_url, access_token, or a Kerberos tuple)")
176+
}
177+
178+
func validateBasic(a *AuthCreds) error {
179+
var missing []string
180+
if a.Username == "" {
181+
missing = append(missing, "username")
182+
}
183+
if a.Password == "" {
184+
missing = append(missing, "password")
185+
}
186+
if len(missing) > 0 {
187+
return fmt.Errorf("basic auth missing required field(s): %s", strings.Join(missing, ", "))
188+
}
189+
return nil
190+
}
191+
192+
func validateOAuth2(a *AuthCreds) error {
193+
var missing []string
194+
if a.ClientID == "" {
195+
missing = append(missing, "client_id")
196+
}
197+
if a.ClientSecret == "" {
198+
missing = append(missing, "client_secret")
199+
}
200+
if a.TokenURL == "" {
201+
missing = append(missing, "token_url")
202+
}
203+
if len(missing) > 0 {
204+
return fmt.Errorf("oauth2 auth missing required field(s): %s", strings.Join(missing, ", "))
205+
}
206+
return nil
207+
}
208+
209+
func validateToken(a *AuthCreds) error {
210+
if a.AccessToken == "" {
211+
return fmt.Errorf("token auth missing required field: access_token")
212+
}
213+
return nil
214+
}
215+
216+
func validateKerberos(a *AuthCreds) error {
217+
// Kerberos accepts any of: keytab, ccache, or username+password+realm.
218+
if a.KerberosKeytab != "" || a.KerberosCCache != "" {
219+
return nil
220+
}
221+
if a.Username != "" && a.Password != "" && a.KerberosRealm != "" {
222+
return nil
223+
}
224+
return fmt.Errorf("kerberos auth requires one of: kerberos_keytab, kerberos_ccache, or (username + password + kerberos_realm)")
225+
}

auth_providers/command_config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ type Server struct {
4747
KerberosConfig string `json:"kerberos_config,omitempty" yaml:"kerberos_config,omitempty"` // KerberosConfig is the path to krb5.conf.
4848
KerberosCCache string `json:"kerberos_ccache,omitempty" yaml:"kerberos_ccache,omitempty"` // KerberosCCache is the path to the credential cache.
4949
KerberosSPN string `json:"kerberos_spn,omitempty" yaml:"kerberos_spn,omitempty"` // KerberosSPN is the Service Principal Name.
50+
51+
// Extras holds per-tool sub-blocks decoded alongside the canonical
52+
// fields. Populated by the loader subpackage when consumers register
53+
// their tool namespaces. Direct access is uncommon — prefer
54+
// loader.DecodeExtras(namespace, target).
55+
//
56+
// The `mapstructure:",remain"` tag tells mapstructure (used by Viper
57+
// inside the loader) to put any keys it doesn't recognize from the
58+
// parent struct into this map, preserving them across read/write
59+
// cycles. The json/yaml `-` tags keep Extras out of the canonical
60+
// wire format for the existing ReadConfig*/WriteConfig* paths; the
61+
// loader handles sub-block serialization separately.
62+
Extras map[string]any `json:"-" yaml:"-" mapstructure:",remain"`
5063
}
5164

5265
// AuthProvider represents the authentication provider configuration.

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ require (
2020
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
2121
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0
2222
github.com/jcmturner/gokrb5/v8 v8.4.4
23+
github.com/spf13/pflag v1.0.10
24+
github.com/spf13/viper v1.21.0
2325
github.com/stretchr/testify v1.11.1
2426
golang.org/x/oauth2 v0.34.0
2527
gopkg.in/yaml.v2 v2.4.0
@@ -31,6 +33,8 @@ require (
3133
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
3234
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
3335
github.com/davecgh/go-spew v1.1.1 // indirect
36+
github.com/fsnotify/fsnotify v1.9.0 // indirect
37+
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
3438
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
3539
github.com/google/uuid v1.6.0 // indirect
3640
github.com/hashicorp/go-uuid v1.0.3 // indirect
@@ -40,8 +44,15 @@ require (
4044
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
4145
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
4246
github.com/kylelemons/godebug v1.1.0 // indirect
47+
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
4348
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
4449
github.com/pmezard/go-difflib v1.0.0 // indirect
50+
github.com/sagikazarmark/locafero v0.11.0 // indirect
51+
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
52+
github.com/spf13/afero v1.15.0 // indirect
53+
github.com/spf13/cast v1.10.0 // indirect
54+
github.com/subosito/gotenv v1.6.0 // indirect
55+
go.yaml.in/yaml/v3 v3.0.4 // indirect
4556
golang.org/x/crypto v0.47.0 // indirect
4657
golang.org/x/net v0.49.0 // indirect
4758
golang.org/x/sys v0.40.0 // indirect

go.sum

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
2-
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
31
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
42
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
53
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
@@ -19,8 +17,16 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQ
1917
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2018
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2119
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
20+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
21+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
22+
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
23+
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
24+
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
25+
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
2226
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
2327
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
28+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
29+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2430
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2531
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
2632
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -50,12 +56,26 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5056
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5157
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
5258
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
59+
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
60+
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
5361
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
5462
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
5563
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5664
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5765
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
5866
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
67+
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
68+
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
69+
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
70+
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
71+
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
72+
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
73+
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
74+
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
75+
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
76+
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
77+
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
78+
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
5979
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6080
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
6181
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -65,14 +85,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
6585
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
6686
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
6787
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
88+
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
89+
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
6890
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
91+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
92+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
6993
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
7094
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
7195
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
72-
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
73-
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
74-
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
75-
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
7696
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
7797
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
7898
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -82,9 +102,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
82102
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
83103
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
84104
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
85-
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
86-
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
87-
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
88105
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
89106
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
90107
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
@@ -98,9 +115,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
98115
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99116
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
100117
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101-
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
102-
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
103-
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
104118
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
105119
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
106120
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -110,9 +124,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
110124
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
111125
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
112126
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
113-
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
114-
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
115-
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
116127
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
117128
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
118129
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

0 commit comments

Comments
 (0)