diff --git a/Makefile b/Makefile index 169b07c9..8ac044cc 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,10 @@ ifdef pv docker volume ls -f name=orb -f dangling=true -q | xargs -r docker volume rm endif +.PHONY: install-dev-tools +install-dev-tools: + @go install github.com/mfridman/tparse@latest + agent_bin: echo "ORB_VERSION: $(ORB_VERSION)-$(COMMIT_HASH)" diff --git a/README.md b/README.md index f48d1da5..a8a7cdf3 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,28 @@ Currently, only the `local` and `git` sources are supported for config manager. - [Local](./docs/configs/local.md) - [Git](./docs/configs/git.md) +### Secrets Manager +The `secrets_manager` section specifies how Orb agent should retrieve and inject secrets into policies. The secrets manager can reference external secret stores like HashiCorp Vault to retrieve sensitive information such as credentials without hardcoding them in configuration files. + +```yaml +orb: + secrets_manager: + active: vault + sources: + vault: + address: "https://vault.example.com:8200" + namespace: "my-namespace" + timeout: 60 + auth: "token" + auth_args: + token: "${VAULT_TOKEN}" + schedule: "*/5 * * * *" + ... +``` + +Supported secrets managers: +- [HashiCorp Vault](./docs/secretsmgr/vault.md) + ### Backends The `backends` section specifies what Orb agent backends should be enabled. Each Orb agent backend offers specific discovery or observability capabilities and may require specific configuration information. @@ -52,7 +74,8 @@ A special `common` subsection under `backends` defines configuration settings th common: diode: target: grpc://192.168.0.22:8080/diode - api_key: ${DIODE_API_KEY} + client_id: ${DIODE_CLIENT_ID} + client_secret: ${DIODE_CLIENT_SECRET} agent_name: agent01 ``` diff --git a/agent/agent.go b/agent/agent.go index 884da513..164bff83 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -4,17 +4,18 @@ import ( "context" "errors" "fmt" + "log/slog" "runtime" "time" "github.com/google/uuid" - "github.com/mitchellh/mapstructure" - "go.uber.org/zap" + "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" "github.com/netboxlabs/orb-agent/agent/config" "github.com/netboxlabs/orb-agent/agent/configmgr" "github.com/netboxlabs/orb-agent/agent/policymgr" + "github.com/netboxlabs/orb-agent/agent/secretsmgr" "github.com/netboxlabs/orb-agent/agent/version" ) @@ -29,7 +30,7 @@ type Agent interface { } type orbAgent struct { - logger *zap.Logger + logger *slog.Logger config config.Config backends map[string]backend.Backend backendState map[string]*backend.State @@ -49,8 +50,9 @@ type orbAgent struct { // AgentGroup channels sent from core groupsInfos map[string]groupInfo - policyManager policymgr.PolicyManager - configManager configmgr.Manager + policyManager policymgr.PolicyManager + configManager configmgr.Manager + secretsManager secretsmgr.Manager } type groupInfo struct { @@ -61,49 +63,68 @@ type groupInfo struct { var _ Agent = (*orbAgent)(nil) // New creates a new agent -func New(logger *zap.Logger, c config.Config) (Agent, error) { - pm, err := policymgr.New(logger, c) +func New(logger *slog.Logger, c config.Config) (Agent, error) { + sm := secretsmgr.New(logger, c.OrbAgent.SecretsManger) + pm, err := policymgr.New(logger, sm, c) if err != nil { - logger.Error("error during create policy manager, exiting", zap.Error(err)) + logger.Error("error during create policy manager, exiting", slog.Any("error", err)) return nil, err } if pm.GetRepo() == nil { - logger.Error("policy manager failed to get repository", zap.Error(err)) + logger.Error("policy manager failed to get repository", slog.Any("error", err)) return nil, err } - cm := configmgr.New(logger, pm, c.OrbAgent.ConfigManager) - return &orbAgent{logger: logger, config: c, policyManager: pm, configManager: cm, groupsInfos: make(map[string]groupInfo)}, nil + cm := configmgr.New(logger, pm, c.OrbAgent.ConfigManager.Active) + + return &orbAgent{ + logger: logger, config: c, policyManager: pm, configManager: cm, + secretsManager: sm, groupsInfos: make(map[string]groupInfo), + }, nil } -func (a *orbAgent) startBackends(agentCtx context.Context) error { - a.logger.Info("registered backends", zap.Strings("values", backend.GetList())) - a.logger.Info("requested backends", zap.Any("values", a.config.OrbAgent.Backends)) - if len(a.config.OrbAgent.Backends) == 0 { +func (a *orbAgent) startBackends(agentCtx context.Context, cfgBackends map[string]any, labels map[string]string) error { + a.logger.Info("registered backends", slog.Any("values", backend.GetList())) + if len(cfgBackends) == 0 { return errors.New("no backends specified") } - a.backends = make(map[string]backend.Backend, len(a.config.OrbAgent.Backends)) + a.backends = make(map[string]backend.Backend, len(cfgBackends)) a.backendState = make(map[string]*backend.State) var commonConfig config.BackendCommons - if v, prs := a.config.OrbAgent.Backends["common"]; prs { - if err := mapstructure.Decode(v, &commonConfig); err != nil { - return fmt.Errorf("failed to decode common backend config: %w", err) + if v, prs := cfgBackends["common"]; prs { + bytes, err := yaml.Marshal(v) + if err != nil { + return err } + err = yaml.Unmarshal(bytes, &commonConfig) + if err != nil { + a.logger.Info("failed to marshal common backend config", slog.Any("error", err)) + return err + } + } else { + commonConfig = config.BackendCommons{} } - commonConfig.Otel.AgentLabels = a.config.OrbAgent.Labels + commonConfig.Otel.AgentLabels = labels a.backendsCommon = commonConfig - delete(a.config.OrbAgent.Backends, "common") - - for name, configurationEntry := range a.config.OrbAgent.Backends { - + delete(cfgBackends, "common") + + for name, configurationEntry := range cfgBackends { + var cEntity map[string]any + if configurationEntry != nil { + var ok bool + cEntity, ok = configurationEntry.(map[string]any) + if !ok { + return errors.New("invalid backend configuration format for backend: " + name) + } + } if !backend.HaveBackend(name) { return errors.New("specified backend does not exist: " + name) } be := backend.GetBackend(name) - if err := be.Configure(a.logger, a.policyManager.GetRepo(), configurationEntry, a.backendsCommon); err != nil { - a.logger.Info("failed to configure backend", zap.String("backend", name), zap.Error(err)) + if err := be.Configure(a.logger, a.policyManager.GetRepo(), cEntity, a.backendsCommon); err != nil { + a.logger.Info("failed to configure backend", slog.String("backend", name), slog.Any("error", err)) return err } backendCtx := context.WithValue(agentCtx, routineKey, name) @@ -115,7 +136,7 @@ func (a *orbAgent) startBackends(agentCtx context.Context) error { LastRestartTS: time.Now(), } if err := be.Start(context.WithCancel(backendCtx)); err != nil { - a.logger.Info("failed to start backend", zap.String("backend", name), zap.Error(err)) + a.logger.Info("failed to start backend", slog.String("backend", name), slog.Any("error", err)) var errMessage string if initialState == backend.BackendError { errMessage = err.Error() @@ -134,20 +155,33 @@ func (a *orbAgent) startBackends(agentCtx context.Context) error { func (a *orbAgent) Start(ctx context.Context, cancelFunc context.CancelFunc) error { startTime := time.Now() defer func(t time.Time) { - a.logger.Debug("Startup of agent execution duration", zap.String("Start() execution duration", time.Since(t).String())) + a.logger.Debug("Startup of agent execution duration", slog.String("Start() execution duration", time.Since(t).String())) }(startTime) agentCtx := context.WithValue(ctx, routineKey, "agentRoutine") asyncCtx, cancelAllAsync := context.WithCancel(context.WithValue(ctx, routineKey, "asyncParent")) a.asyncContext = asyncCtx a.rpcFromCancelFunc = cancelAllAsync a.cancelFunction = cancelFunc - a.logger.Info("agent started", zap.String("version", version.GetBuildVersion()), zap.Any("routine", agentCtx.Value(routineKey))) + a.logger.Info("agent started", slog.String("version", version.GetBuildVersion()), slog.Any("routine", agentCtx.Value(routineKey))) + a.logger.Info("requested backends", slog.Any("values", a.config.OrbAgent.Backends)) - if err := a.startBackends(ctx); err != nil { + if err := a.secretsManager.Start(ctx); err != nil { + a.logger.Error("error during start secrets manager", slog.Any("error", err)) return err } - if err := a.configManager.Start(a.config, a.backends); err != nil { + var err error + if a.config.OrbAgent.Backends, + a.config.OrbAgent.ConfigManager, + err = a.secretsManager.SolveConfigSecrets(a.config.OrbAgent.Backends, a.config.OrbAgent.ConfigManager); err != nil { + return err + } + + if err = a.startBackends(ctx, a.config.OrbAgent.Backends, a.config.OrbAgent.Labels); err != nil { + return err + } + + if err = a.configManager.Start(a.config, a.backends); err != nil { return err } @@ -162,27 +196,27 @@ func (a *orbAgent) logonWithHeartbeat() { } func (a *orbAgent) logoffWithHeartbeat(ctx context.Context) { - a.logger.Debug("stopping heartbeat, going offline status", zap.Any("routine", ctx.Value(routineKey))) + a.logger.Debug("stopping heartbeat, going offline status", slog.Any("routine", ctx.Value(routineKey))) if a.heartbeatCtx != nil { a.heartbeatCancel() } } func (a *orbAgent) Stop(ctx context.Context) { - a.logger.Info("routine call for stop agent", zap.Any("routine", ctx.Value(routineKey))) + a.logger.Info("routine call for stop agent", slog.Any("routine", ctx.Value(routineKey))) if a.rpcFromCancelFunc != nil { a.rpcFromCancelFunc() } for name, b := range a.backends { if state, _, _ := b.GetRunningStatus(); state == backend.Running { - a.logger.Debug("stopping backend", zap.String("backend", name)) + a.logger.Debug("stopping backend", slog.String("backend", name)) if err := b.Stop(ctx); err != nil { - a.logger.Error("error while stopping the backend", zap.String("backend", name)) + a.logger.Error("error while stopping the backend", slog.String("backend", name)) } } } a.logoffWithHeartbeat(ctx) - a.logger.Debug("stopping agent with number of go routines and go calls", zap.Int("goroutines", runtime.NumGoroutine()), zap.Int64("gocalls", runtime.NumCgoCall())) + a.logger.Debug("stopping agent with number of go routines and go calls", slog.Int("goroutines", runtime.NumGoroutine()), slog.Int64("gocalls", runtime.NumCgoCall())) if a.policyRequestSucceeded != nil { a.policyRequestSucceeded() } @@ -198,22 +232,30 @@ func (a *orbAgent) RestartBackend(ctx context.Context, name string, reason strin } be := a.backends[name] - a.logger.Info("restarting backend", zap.String("backend", name), zap.String("reason", reason)) + a.logger.Info("restarting backend", slog.String("backend", name), slog.String("reason", reason)) a.backendState[name].RestartCount++ a.backendState[name].LastRestartTS = time.Now() a.backendState[name].LastRestartReason = reason - a.logger.Info("removing policies", zap.String("backend", name)) + a.logger.Info("removing policies", slog.String("backend", name)) if err := a.policyManager.RemoveBackendPolicies(be, true); err != nil { - a.logger.Error("failed to remove policies", zap.String("backend", name), zap.Error(err)) + a.logger.Error("failed to remove policies", slog.String("backend", name), slog.Any("error", err)) + } + var beConfig map[string]any + if a.config.OrbAgent.Backends[name] != nil { + var ok bool + beConfig, ok = a.config.OrbAgent.Backends[name].(map[string]any) + if !ok { + return errors.New("backend not found: " + name) + } } - if err := be.Configure(a.logger, a.policyManager.GetRepo(), a.config.OrbAgent.Backends[name], a.backendsCommon); err != nil { + if err := be.Configure(a.logger, a.policyManager.GetRepo(), beConfig, a.backendsCommon); err != nil { return err } - a.logger.Info("resetting backend", zap.String("backend", name)) + a.logger.Info("resetting backend", slog.String("backend", name)) if err := be.FullReset(ctx); err != nil { a.backendState[name].LastError = fmt.Sprintf("failed to reset backend: %v", err) - a.logger.Error("failed to reset backend", zap.String("backend", name), zap.Error(err)) + a.logger.Error("failed to reset backend", slog.String("backend", name), slog.Any("error", err)) } return nil @@ -222,12 +264,12 @@ func (a *orbAgent) RestartBackend(ctx context.Context, name string, reason strin func (a *orbAgent) RestartAll(ctx context.Context, reason string) error { ctx = a.configManager.GetContext(ctx) a.logoffWithHeartbeat(ctx) - a.logger.Info("restarting comms", zap.String("reason", reason)) + a.logger.Info("restarting comms", slog.String("reason", reason)) for name := range a.backends { - a.logger.Info("restarting backend", zap.String("backend", name), zap.String("reason", reason)) + a.logger.Info("restarting backend", slog.String("backend", name), slog.String("reason", reason)) err := a.RestartBackend(ctx, name, reason) if err != nil { - a.logger.Error("failed to restart backend", zap.Error(err)) + a.logger.Error("failed to restart backend", slog.Any("error", err)) } } a.logger.Info("all backends and comms were restarted") @@ -237,6 +279,6 @@ func (a *orbAgent) RestartAll(ctx context.Context, reason string) error { func (a *orbAgent) extendContext(routine string) (context.Context, context.CancelFunc) { uuidTraceID := uuid.NewString() - a.logger.Debug("creating context for receiving message", zap.String("routine", routine), zap.String("trace-id", uuidTraceID)) + a.logger.Debug("creating context for receiving message", slog.String("routine", routine), slog.String("trace-id", uuidTraceID)) return context.WithCancel(context.WithValue(context.WithValue(a.asyncContext, routineKey, routine), config.ContextKey("trace-id"), uuidTraceID)) } diff --git a/agent/agent_prof_test.go b/agent/agent_prof_test.go deleted file mode 100644 index dc280b40..00000000 --- a/agent/agent_prof_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package agent - -import ( - "context" - "testing" -) - -func Test_orbAgent_startBackends(t *testing.T) { - type args struct { - agentCtx context.Context - } - tests := []struct { - name string - - args args - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - a := orbAgent{} - if err := a.startBackends(tt.args.agentCtx); (err != nil) != tt.wantErr { - t.Errorf("startBackends() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/agent/backend/backend.go b/agent/backend/backend.go index 7de278da..46b967cf 100644 --- a/agent/backend/backend.go +++ b/agent/backend/backend.go @@ -2,10 +2,9 @@ package backend import ( "context" + "log/slog" "time" - "go.uber.org/zap" - "github.com/netboxlabs/orb-agent/agent/config" "github.com/netboxlabs/orb-agent/agent/policies" ) @@ -47,14 +46,14 @@ func (s RunningStatus) String() string { // Backend is the interface that all backends must implement type Backend interface { - Configure(*zap.Logger, policies.PolicyRepo, map[string]interface{}, config.BackendCommons) error + Configure(*slog.Logger, policies.PolicyRepo, map[string]any, config.BackendCommons) error Version() (string, error) Start(ctx context.Context, cancelFunc context.CancelFunc) error Stop(ctx context.Context) error FullReset(ctx context.Context) error GetStartTime() time.Time - GetCapabilities() (map[string]interface{}, error) + GetCapabilities() (map[string]any, error) GetRunningStatus() (RunningStatus, string, error) GetInitialState() RunningStatus diff --git a/agent/backend/cmd.go b/agent/backend/cmd.go new file mode 100644 index 00000000..f9ac62ca --- /dev/null +++ b/agent/backend/cmd.go @@ -0,0 +1,90 @@ +package backend + +import ( + "github.com/go-cmd/cmd" +) + +// Commander abstracts the functionality from go-cmd/cmd package +type Commander interface { + Start() <-chan CmdStatus + Stop() error + Status() CmdStatus + + // For accessing output channels + GetStdout() <-chan string + GetStderr() <-chan string +} + +// CmdStatus holds the status of a command +type CmdStatus struct { + PID int // Process ID of command + Complete bool // Whether the command has completed + Exit int // Exit code of the command + Error error // Go error + StopTs int64 // Timestamp when the command was stopped +} + +// CmdOptions holds the options for command execution +type CmdOptions struct { + Buffered bool // Whether to buffer the output + Streaming bool // Whether to stream the output +} + +// NewCmdOptions creates a new command with specific options +var NewCmdOptions = func(options CmdOptions, name string, args ...string) Commander { + cmdOptions := cmd.Options{ + Buffered: options.Buffered, + Streaming: options.Streaming, + } + return &CmdWrapper{Cmd: cmd.NewCmdOptions(cmdOptions, name, args...)} +} + +// CmdWrapper wraps the cmd.Cmd struct to implement CmdInterface +type CmdWrapper struct { + *cmd.Cmd +} + +// ConvertStatus converts cmd.Status to our Status +func ConvertStatus(status cmd.Status) CmdStatus { + return CmdStatus{ + PID: status.PID, + Complete: status.Complete, + Exit: status.Exit, + Error: status.Error, + StopTs: status.StopTs, + } +} + +// Start starts the command and returns a channel for its status +func (c *CmdWrapper) Start() <-chan CmdStatus { + statusChan := make(chan CmdStatus, 1) + origChan := c.Cmd.Start() + + go func() { + status := <-origChan + statusChan <- ConvertStatus(status) + close(statusChan) + }() + + return statusChan +} + +// Stop stops the running command +func (c *CmdWrapper) Stop() error { + return c.Cmd.Stop() +} + +// Status returns the current command status +func (c *CmdWrapper) Status() CmdStatus { + return ConvertStatus(c.Cmd.Status()) +} + +// GetStdout returns the stdout channel +func (c *CmdWrapper) GetStdout() <-chan string { + return c.Cmd.Stdout +} + +// GetStderr returns the stderr channel +func (c *CmdWrapper) GetStderr() <-chan string { + return c.Cmd.Stderr +} diff --git a/agent/backend/devicediscovery/device_discovery.go b/agent/backend/devicediscovery/device_discovery.go index 0fffa444..88823ad6 100644 --- a/agent/backend/devicediscovery/device_discovery.go +++ b/agent/backend/devicediscovery/device_discovery.go @@ -5,11 +5,10 @@ import ( "context" "errors" "fmt" + "log/slog" "net/http" "time" - "github.com/go-cmd/cmd" - "go.uber.org/zap" "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" @@ -32,7 +31,7 @@ const ( ) type deviceDiscoveryBackend struct { - logger *zap.Logger + logger *slog.Logger policyRepo policies.PolicyRepo exec string @@ -41,12 +40,13 @@ type deviceDiscoveryBackend struct { apiProtocol string diodeTarget string - diodeAPIKey string + diodeClientID string + diodeClientSecret string diodeAppNamePrefix string startTime time.Time - proc *cmd.Cmd - statusChan <-chan cmd.Status + proc backend.Commander + statusChan <-chan backend.CmdStatus cancelFunc context.CancelFunc ctx context.Context } @@ -65,7 +65,9 @@ func Register() bool { return true } -func (d *deviceDiscoveryBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo, config map[string]interface{}, common config.BackendCommons) error { +func (d *deviceDiscoveryBackend) Configure(logger *slog.Logger, repo policies.PolicyRepo, + config map[string]any, common config.BackendCommons, +) error { d.logger = logger d.policyRepo = repo @@ -78,7 +80,8 @@ func (d *deviceDiscoveryBackend) Configure(logger *zap.Logger, repo policies.Pol } d.diodeTarget = common.Diode.Target - d.diodeAPIKey = common.Diode.APIKey + d.diodeClientID = common.Diode.ClientID + d.diodeClientSecret = common.Diode.ClientSecret d.diodeAppNamePrefix = common.Diode.AgentName return nil @@ -86,7 +89,9 @@ func (d *deviceDiscoveryBackend) Configure(logger *zap.Logger, repo policies.Pol func (d *deviceDiscoveryBackend) Version() (string, error) { var info info - err := d.request("status", &info, http.MethodGet, http.NoBody, "application/json", versionTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/status", d.apiProtocol, d.apiHost, d.apiPort) + err := backend.CommonRequest("device-discovery", d.proc, d.logger, url, &info, http.MethodGet, + http.NoBody, "application/json", versionTimeout, "detail") if err != nil { return "", err } @@ -102,15 +107,16 @@ func (d *deviceDiscoveryBackend) Start(ctx context.Context, cancelFunc context.C "--host", d.apiHost, "--port", d.apiPort, "--diode-target", d.diodeTarget, - "--diode-api-key", "********", + "--diode-client-id", d.diodeClientID, + "--diode-client-secret", "********", "--diode-app-name-prefix", d.diodeAppNamePrefix, } - d.logger.Info("device-discovery startup", zap.Strings("arguments", pvOptions)) + d.logger.Info("device-discovery startup", slog.Any("arguments", pvOptions)) - pvOptions[7] = d.diodeAPIKey + pvOptions[9] = d.diodeClientSecret - d.proc = cmd.NewCmdOptions(cmd.Options{ + d.proc = backend.NewCmdOptions(backend.CmdOptions{ Buffered: false, Streaming: true, }, d.exec, pvOptions...) @@ -124,20 +130,22 @@ func (d *deviceDiscoveryBackend) Start(ctx context.Context, cancelFunc context.C close(doneChan) } }() - for d.proc.Stdout != nil || d.proc.Stderr != nil { + stdout := d.proc.GetStdout() + stderr := d.proc.GetStderr() + for stdout != nil || stderr != nil { select { - case line, open := <-d.proc.Stdout: + case line, open := <-stdout: if !open { - d.proc.Stdout = nil + stdout = nil continue } - d.logger.Info("device-discovery stdout", zap.String("log", line)) - case line, open := <-d.proc.Stderr: + d.logger.Info("device-discovery stdout", slog.String("log", line)) + case line, open := <-stderr: if !open { - d.proc.Stderr = nil + stderr = nil continue } - d.logger.Info("device-discovery stderr", zap.String("log", line)) + d.logger.Info("device-discovery stderr", slog.String("log", line)) } } }() @@ -148,37 +156,40 @@ func (d *deviceDiscoveryBackend) Start(ctx context.Context, cancelFunc context.C status := d.proc.Status() if status.Error != nil { - d.logger.Error("device-discovery startup error", zap.Error(status.Error)) + d.logger.Error("device-discovery startup error", slog.Any("error", status.Error)) return status.Error } if status.Complete { err := d.proc.Stop() if err != nil { - d.logger.Error("proc.Stop error", zap.Error(err)) + d.logger.Error("proc.Stop error", slog.Any("error", err)) } return errors.New("device-discovery startup error, check log") } - d.logger.Info("device-discovery process started", zap.Int("pid", status.PID)) + d.logger.Info("device-discovery process started", slog.Int("pid", status.PID)) + var version string var readinessErr error - for backoff := 0; backoff < readinessBackoff; backoff++ { - version, readinessErr := d.Version() + for backoff := range readinessBackoff { + version, readinessErr = d.Version() if readinessErr == nil { - d.logger.Info("device-discovery readiness ok, got version ", zap.String("device_discovery_version", version)) + d.logger.Info("device-discovery readiness ok, got version ", + slog.String("device_discovery_version", version)) break } backoffDuration := time.Duration(backoff) * time.Second - d.logger.Info("device-discovery is not ready, trying again with backoff", zap.String("backoff backoffDuration", backoffDuration.String())) + d.logger.Info("device-discovery is not ready, trying again with backoff", + slog.String("backoff backoffDuration", backoffDuration.String())) time.Sleep(backoffDuration) } if readinessErr != nil { - d.logger.Error("device-discovery error on readiness", zap.Error(readinessErr)) + d.logger.Error("device-discovery error on readiness", slog.Any("error", readinessErr)) err := d.proc.Stop() if err != nil { - d.logger.Error("proc.Stop error", zap.Error(err)) + d.logger.Error("proc.Stop error", slog.Any("error", err)) } return readinessErr } @@ -187,22 +198,22 @@ func (d *deviceDiscoveryBackend) Start(ctx context.Context, cancelFunc context.C } func (d *deviceDiscoveryBackend) Stop(ctx context.Context) error { - d.logger.Info("routine call to stop device-discovery", zap.Any("routine", ctx.Value(config.ContextKey("routine")))) + d.logger.Info("routine call to stop device-discovery", slog.Any("routine", ctx.Value(config.ContextKey("routine")))) defer d.cancelFunc() err := d.proc.Stop() finalStatus := <-d.statusChan if err != nil { - d.logger.Error("device-discovery shutdown error", zap.Error(err)) + d.logger.Error("device-discovery shutdown error", slog.Any("error", err)) } - d.logger.Info("device-discovery process stopped", zap.Int("pid", finalStatus.PID), zap.Int("exit_code", finalStatus.Exit)) + d.logger.Info("device-discovery process stopped", slog.Int("pid", finalStatus.PID), slog.Int("exit_code", finalStatus.Exit)) return nil } func (d *deviceDiscoveryBackend) FullReset(ctx context.Context) error { // force a stop, which stops scrape as well. if proc is dead, it no ops. - if state, _, _ := d.getProcRunningStatus(); state == backend.Running { + if state, _, _ := backend.GetRunningStatus(d.proc); state == backend.Running { if err := d.Stop(ctx); err != nil { - d.logger.Error("failed to stop backend on restart procedure", zap.Error(err)) + d.logger.Error("failed to stop backend on restart procedure", slog.Any("error", err)) return err } } @@ -210,7 +221,7 @@ func (d *deviceDiscoveryBackend) FullReset(ctx context.Context) error { backendCtx, cancelFunc := context.WithCancel(context.WithValue(ctx, config.ContextKey("routine"), "device-discovery")) // start it if err := d.Start(backendCtx, cancelFunc); err != nil { - d.logger.Error("failed to start backend on restart procedure", zap.Error(err)) + d.logger.Error("failed to start backend on restart procedure", slog.Any("error", err)) return err } return nil @@ -220,9 +231,11 @@ func (d *deviceDiscoveryBackend) GetStartTime() time.Time { return d.startTime } -func (d *deviceDiscoveryBackend) GetCapabilities() (map[string]interface{}, error) { - caps := make(map[string]interface{}) - err := d.request("capabilities", &caps, http.MethodGet, http.NoBody, "application/json", capabilitiesTimeout) +func (d *deviceDiscoveryBackend) GetCapabilities() (map[string]any, error) { + caps := make(map[string]any) + url := fmt.Sprintf("%s://%s:%s/api/v1/capabilities", d.apiProtocol, d.apiHost, d.apiPort) + err := backend.CommonRequest("device-discovery", d.proc, d.logger, url, &caps, http.MethodGet, + http.NoBody, "application/json", capabilitiesTimeout, "detail") if err != nil { return nil, err } @@ -231,7 +244,7 @@ func (d *deviceDiscoveryBackend) GetCapabilities() (map[string]interface{}, erro func (d *deviceDiscoveryBackend) GetRunningStatus() (backend.RunningStatus, string, error) { // first check process status - runningStatus, errMsg, err := d.getProcRunningStatus() + runningStatus, errMsg, err := backend.GetRunningStatus(d.proc) // if it's not running, we're done if runningStatus != backend.Running { return runningStatus, errMsg, err @@ -252,28 +265,31 @@ func (d *deviceDiscoveryBackend) ApplyPolicy(data policies.PolicyData, updatePol if updatePolicy { // To update a policy it's necessary first remove it and then apply a new version if err := d.RemovePolicy(data); err != nil { - d.logger.Warn("policy failed to remove", zap.String("policy_id", data.ID), zap.String("policy_name", data.Name), zap.Error(err)) + d.logger.Warn("policy failed to remove", slog.String("policy_id", data.ID), + slog.String("policy_name", data.Name), slog.Any("error", err)) } } - d.logger.Debug("device-discovery policy apply", zap.String("policy_id", data.ID), zap.Any("data", data.Data)) + d.logger.Debug("device-discovery policy apply", slog.String("policy_id", data.ID), slog.Any("data", data.Data)) - fullPolicy := map[string]interface{}{ - "policies": map[string]interface{}{ + fullPolicy := map[string]any{ + "policies": map[string]any{ data.Name: data.Data, }, } policyYaml, err := yaml.Marshal(fullPolicy) if err != nil { - d.logger.Warn("policy yaml marshal failure", zap.String("policy_id", data.ID), zap.Any("policy", fullPolicy)) + d.logger.Warn("policy yaml marshal failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) return err } - var resp map[string]interface{} - err = d.request("policies", &resp, http.MethodPost, bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout) + var resp map[string]any + url := fmt.Sprintf("%s://%s:%s/api/v1/policies", d.apiProtocol, d.apiHost, d.apiPort) + err = backend.CommonRequest("device-discovery", d.proc, d.logger, url, &resp, http.MethodPost, + bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout, "detail") if err != nil { - d.logger.Warn("policy application failure", zap.String("policy_id", data.ID), zap.ByteString("policy", policyYaml)) + d.logger.Warn("policy application failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) return err } @@ -281,8 +297,8 @@ func (d *deviceDiscoveryBackend) ApplyPolicy(data policies.PolicyData, updatePol } func (d *deviceDiscoveryBackend) RemovePolicy(data policies.PolicyData) error { - d.logger.Debug("device-discovery policy remove", zap.String("policy_id", data.ID)) - var resp interface{} + d.logger.Debug("device-discovery policy remove", slog.String("policy_id", data.ID)) + var resp any var name string // Since we use Name for removing policies not IDs, if there is a change, we need to remove the previous name of the policy if data.PreviousPolicyData != nil && data.PreviousPolicyData.Name != data.Name { @@ -290,7 +306,9 @@ func (d *deviceDiscoveryBackend) RemovePolicy(data policies.PolicyData) error { } else { name = data.Name } - err := d.request(fmt.Sprintf("policies/%s", name), &resp, http.MethodDelete, http.NoBody, "application/json", removePolicyTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/policies/%s", d.apiProtocol, d.apiHost, d.apiPort, name) + err := backend.CommonRequest("device-discovery", d.proc, d.logger, url, &resp, http.MethodDelete, + http.NoBody, "application/json", removePolicyTimeout, "detail") if err != nil { return err } diff --git a/agent/backend/devicediscovery/device_discovery_test.go b/agent/backend/devicediscovery/device_discovery_test.go new file mode 100644 index 00000000..52e49c40 --- /dev/null +++ b/agent/backend/devicediscovery/device_discovery_test.go @@ -0,0 +1,202 @@ +package devicediscovery_test + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/backend/devicediscovery" + "github.com/netboxlabs/orb-agent/agent/backend/mocks" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/policies" +) + +type StatusResponse struct { + StartTime string `json:"start_time"` + Version string `json:"version"` + UpTime float64 `json:"up_time"` +} + +func TestDeviceDiscoveryBackendStart(t *testing.T) { + // Create server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.URL.Path == "/api/v1/status" { + response := StatusResponse{ + Version: "1.3.4", + StartTime: "2023-10-01T12:00:00Z", + UpTime: 123.456, + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.URL.Path == "/api/v1/capabilities" { + capabilities := map[string]any{ + "capability": true, + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(capabilities) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if strings.Contains(r.URL.Path, "/api/v1/policies") { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy applied successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy removed successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + // Parse server URL + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + // Create logger + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // Create a mock repository + repo, err := policies.NewMemRepo() + assert.NoError(t, err) + + // Create a mock command + mockCmd := &mocks.MockCmd{} + mocks.SetupSuccessfulProcess(mockCmd, 12345) + + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(options backend.CmdOptions, name string, args ...string) backend.Commander { + // Assert that the correct parameters were passed + assert.Equal(t, "device-discovery", name, "Expected command name to be device-discovery") + assert.Contains(t, args, "--port", "Expected args to contain port") + assert.Contains(t, args, "--host", "Expected args to contain host") + assert.False(t, options.Buffered, "Expected buffered to be false") + assert.True(t, options.Streaming, "Expected streaming to be true") + return mockCmd + } + + assert.True(t, devicediscovery.Register(), "Failed to register DeviceDiscovery backend") + + assert.True(t, backend.HaveBackend("device_discovery"), "Failed to get DeviceDiscovery backend") + + be := backend.GetBackend("device_discovery") + + assert.Equal(t, backend.Unknown, be.GetInitialState()) + + // Configure backend + err = be.Configure(logger, repo, map[string]any{ + "host": serverURL.Hostname(), + "port": serverURL.Port(), + }, config.BackendCommons{}) + assert.NoError(t, err) + + // Start the backend + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + // Assert successful start + assert.NoError(t, err) + + // Get Running status + status, _, err := be.GetRunningStatus() + assert.NoError(t, err) + assert.Equal(t, backend.Running, status, "Expected backend to be running") + + // Get capabilities + capabilities, err := be.GetCapabilities() + assert.NoError(t, err) + assert.Equal(t, capabilities["capability"], true, "Expected capability to be true") + + data := policies.PolicyData{ + ID: "dummy-policy-id", + Name: "dummy-policy-name", + Data: map[string]any{"key": "value"}, + } + // Apply policy + err = be.ApplyPolicy(data, false) + assert.NoError(t, err) + + // Update policy + err = be.ApplyPolicy(data, true) + assert.NoError(t, err) + + // Assert restart + err = be.FullReset(ctx) + assert.NoError(t, err) + + // Verify expectations + mockCmd.AssertExpectations(t) +} + +func TestDeviceDiscoveryBackendCompleted(t *testing.T) { + // Create a mock command that simulates a failure + mockCmd := &mocks.MockCmd{} + mocks.SetupCompletedProcess(mockCmd, 0, nil) + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(_ backend.CmdOptions, _ string, _ ...string) backend.Commander { + return mockCmd + } + + assert.True(t, devicediscovery.Register(), "Failed to register DeviceDiscovery backend") + + assert.True(t, backend.HaveBackend("device_discovery"), "Failed to get DeviceDiscovery backend") + + be := backend.GetBackend("device_discovery") + + // Configure backend with invalid parameters + err := be.Configure(slog.Default(), nil, map[string]any{ + "host": "invalid-host", + }, config.BackendCommons{}) + assert.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + assert.Error(t, err) +} diff --git a/agent/backend/devicediscovery/utils.go b/agent/backend/devicediscovery/utils.go deleted file mode 100644 index f326546a..00000000 --- a/agent/backend/devicediscovery/utils.go +++ /dev/null @@ -1,96 +0,0 @@ -package devicediscovery - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "go.uber.org/zap" - - "github.com/netboxlabs/orb-agent/agent/backend" -) - -func (d *deviceDiscoveryBackend) getProcRunningStatus() (backend.RunningStatus, string, error) { - if d.proc == nil { - return backend.Unknown, "backend not started yet", nil - } - status := d.proc.Status() - - if status.Error != nil { - errMsg := fmt.Sprintf("device-discovery process error: %v", status.Error) - return backend.BackendError, errMsg, status.Error - } - - if status.Complete { - err := d.proc.Stop() - return backend.Offline, "device-discovery process ended", err - } - - if status.StopTs > 0 { - return backend.Offline, "device-discovery process ended", nil - } - return backend.Running, "", nil -} - -// note this needs to be stateless because it is called for multiple go routines -func (d *deviceDiscoveryBackend) request(url string, payload interface{}, method string, body io.Reader, contentType string, timeout int32) error { - client := http.Client{ - Timeout: time.Second * time.Duration(timeout), - } - - status, _, err := d.getProcRunningStatus() - if status != backend.Running { - d.logger.Warn("skipping device discovery REST API request because process is not running or is unresponsive", zap.String("url", url), zap.String("method", method), zap.Error(err)) - return err - } - - URL := fmt.Sprintf("%s://%s:%s/api/v1/%s", d.apiProtocol, d.apiHost, d.apiPort, url) - - req, err := http.NewRequest(method, URL, body) - if err != nil { - d.logger.Error("received error from payload", zap.Error(err)) - return err - } - - req.Header.Add("Content-Type", contentType) - res, getErr := client.Do(req) - - if getErr != nil { - d.logger.Error("received error from payload", zap.Error(getErr)) - return getErr - } - - defer func() { - if err := res.Body.Close(); err != nil { - d.logger.Error("failed to close response body", zap.Error(err)) - } - }() - - if (res.StatusCode < 200) || (res.StatusCode > 299) { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("non 2xx HTTP error code from device-discovery, no or invalid body: %d", res.StatusCode) - } - if len(body) == 0 { - return fmt.Errorf("%d empty body", res.StatusCode) - } else if body[0] == '{' { - var jsonBody map[string]interface{} - err := json.Unmarshal(body, &jsonBody) - if err == nil { - if errMsg, ok := jsonBody["detail"]; ok { - return fmt.Errorf("%d %s", res.StatusCode, errMsg) - } - } - } - } - - if res.Body != nil { - err = json.NewDecoder(res.Body).Decode(&payload) - if err != nil { - return err - } - } - return nil -} diff --git a/agent/backend/devicediscovery/vars.go b/agent/backend/devicediscovery/vars.go deleted file mode 100644 index a8261d39..00000000 --- a/agent/backend/devicediscovery/vars.go +++ /dev/null @@ -1,11 +0,0 @@ -package devicediscovery - -import ( - "github.com/spf13/viper" -) - -// RegisterBackendSpecificVariables registers the backend specific variables for the device discovery backend -func RegisterBackendSpecificVariables(v *viper.Viper) { - v.SetDefault("orb.backends.device_discovery.host", defaultAPIHost) - v.SetDefault("orb.backends.device_discovery.port", defaultAPIPort) -} diff --git a/agent/backend/mocks/mocks.go b/agent/backend/mocks/mocks.go new file mode 100644 index 00000000..81f06765 --- /dev/null +++ b/agent/backend/mocks/mocks.go @@ -0,0 +1,171 @@ +package mocks + +import ( + "errors" + "time" + + "github.com/stretchr/testify/mock" + + "github.com/netboxlabs/orb-agent/agent/backend" +) + +// MockCmd is a testify-based mock implementation of backend.CmdInterface +type MockCmd struct { + mock.Mock +} + +// Start mocks the Start method +func (m *MockCmd) Start() <-chan backend.CmdStatus { + args := m.Called() + return args.Get(0).(<-chan backend.CmdStatus) +} + +// Stop mocks the Stop method +func (m *MockCmd) Stop() error { + args := m.Called() + return args.Error(0) +} + +// Status mocks the Status method +func (m *MockCmd) Status() backend.CmdStatus { + args := m.Called() + return args.Get(0).(backend.CmdStatus) +} + +// GetStdout mocks the GetStdout method +func (m *MockCmd) GetStdout() <-chan string { + args := m.Called() + return args.Get(0).(<-chan string) +} + +// GetStderr mocks the GetStderr method +func (m *MockCmd) GetStderr() <-chan string { + args := m.Called() + return args.Get(0).(<-chan string) +} + +// SetupSuccessfulProcess configures the mock for a successful running process +func SetupSuccessfulProcess(mockCmd *MockCmd, pid int) (<-chan string, <-chan string) { + // Create status channel - create it as bidirectional first + statusCh := make(chan backend.CmdStatus, 1) + // Create a receive-only channel to return from the mock + var readOnlyStatusCh <-chan backend.CmdStatus = statusCh + + status := backend.CmdStatus{ + PID: pid, + Complete: false, + Exit: -1, // Not completed yet + Error: nil, + } + + // Create stdout/stderr channels + stdoutCh := make(chan string, 10) + stderrCh := make(chan string, 10) + + // Create read-only versions of stdout/stderr channels + var readOnlyStdoutCh <-chan string = stdoutCh + var readOnlyStderrCh <-chan string = stderrCh + + // Configure mock behavior - pass the read-only channels + mockCmd.On("Start").Return(readOnlyStatusCh) + mockCmd.On("Status").Return(status) + mockCmd.On("GetStdout").Return(readOnlyStdoutCh) + mockCmd.On("GetStderr").Return(readOnlyStderrCh) + mockCmd.On("Stop").Return(nil) + + // Send the status in another goroutine to simulate async behavior + go func() { + // Delay before sending to simulate process startup + time.Sleep(10 * time.Millisecond) + statusCh <- status + // Simulate some output + stdoutCh <- "success" + stderrCh <- "error" + }() + + return readOnlyStdoutCh, readOnlyStderrCh +} + +// SetupCompletedProcess configures the mock for a process that has completed +func SetupCompletedProcess(mockCmd *MockCmd, exitCode int, err error) { + // Create status channel + statusCh := make(chan backend.CmdStatus, 1) + // Create a read-only view of the channel + var readOnlyStatusCh <-chan backend.CmdStatus = statusCh + + status := backend.CmdStatus{ + PID: 12345, // Example PID + Complete: true, + Exit: exitCode, + Error: err, + StopTs: time.Now().UnixNano(), + } + + // Empty channels for stdout/stderr since process is complete + stdoutCh := make(chan string) + stderrCh := make(chan string) + + // Create read-only versions + var readOnlyStdoutCh <-chan string = stdoutCh + var readOnlyStderrCh <-chan string = stderrCh + + // Configure mock behavior + mockCmd.On("Start").Return(readOnlyStatusCh) + mockCmd.On("Status").Return(status) + mockCmd.On("Stop").Return(nil) + + mockCmd.On("GetStdout").Return(readOnlyStdoutCh) + mockCmd.On("GetStderr").Return(readOnlyStderrCh) + + // Close the channels + close(stdoutCh) + close(stderrCh) + + // Send status immediately + go func() { + statusCh <- status + }() +} + +// SetupFailingProcess configures the mock for a process that fails to start +func SetupFailingProcess(mockCmd *MockCmd, errorMsg string) { + // Create error + cmdError := errors.New(errorMsg) + + // Create status channel + statusCh := make(chan backend.CmdStatus, 1) + // Create a read-only view of the channel + var readOnlyStatusCh <-chan backend.CmdStatus = statusCh + + status := backend.CmdStatus{ + PID: 0, + Complete: true, + Exit: 1, + Error: cmdError, + StopTs: time.Now().UnixNano(), + } + + // Empty channels for stdout/stderr + stdoutCh := make(chan string) + stderrCh := make(chan string) + + // Create read-only versions + var readOnlyStdoutCh <-chan string = stdoutCh + var readOnlyStderrCh <-chan string = stderrCh + + // Configure mock behavior + mockCmd.On("Start").Return(readOnlyStatusCh) + mockCmd.On("Status").Return(status) + mockCmd.On("Stop").Return(nil) + mockCmd.On("GetStdout").Return(readOnlyStdoutCh) + mockCmd.On("GetStderr").Return(readOnlyStderrCh) + + // Close the channels + close(stdoutCh) + close(stderrCh) + + // Send status immediately + go func() { + statusCh <- status + }() +} diff --git a/agent/backend/networkdiscovery/network_discovery.go b/agent/backend/networkdiscovery/network_discovery.go index d0581bf0..19c01440 100644 --- a/agent/backend/networkdiscovery/network_discovery.go +++ b/agent/backend/networkdiscovery/network_discovery.go @@ -5,11 +5,10 @@ import ( "context" "errors" "fmt" + "log/slog" "net/http" "time" - "github.com/go-cmd/cmd" - "go.uber.org/zap" "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" @@ -32,7 +31,7 @@ const ( ) type networkDiscoveryBackend struct { - logger *zap.Logger + logger *slog.Logger policyRepo policies.PolicyRepo exec string @@ -41,12 +40,13 @@ type networkDiscoveryBackend struct { apiProtocol string diodeTarget string - diodeAPIKey string + diodeClientID string + diodeClientSecret string diodeAppNamePrefix string startTime time.Time - proc *cmd.Cmd - statusChan <-chan cmd.Status + proc backend.Commander + statusChan <-chan backend.CmdStatus cancelFunc context.CancelFunc ctx context.Context } @@ -65,7 +65,9 @@ func Register() bool { return true } -func (d *networkDiscoveryBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo, config map[string]interface{}, common config.BackendCommons) error { +func (d *networkDiscoveryBackend) Configure(logger *slog.Logger, repo policies.PolicyRepo, + config map[string]any, common config.BackendCommons, +) error { d.logger = logger d.policyRepo = repo @@ -78,7 +80,8 @@ func (d *networkDiscoveryBackend) Configure(logger *zap.Logger, repo policies.Po } d.diodeTarget = common.Diode.Target - d.diodeAPIKey = common.Diode.APIKey + d.diodeClientID = common.Diode.ClientID + d.diodeClientSecret = common.Diode.ClientSecret d.diodeAppNamePrefix = common.Diode.AgentName return nil @@ -86,7 +89,9 @@ func (d *networkDiscoveryBackend) Configure(logger *zap.Logger, repo policies.Po func (d *networkDiscoveryBackend) Version() (string, error) { var info info - err := d.request("status", &info, http.MethodGet, http.NoBody, "application/json", versionTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/status", d.apiProtocol, d.apiHost, d.apiPort) + err := backend.CommonRequest("network-discovery", d.proc, d.logger, url, &info, http.MethodGet, + http.NoBody, "application/json", versionTimeout, "detail") if err != nil { return "", err } @@ -102,15 +107,16 @@ func (d *networkDiscoveryBackend) Start(ctx context.Context, cancelFunc context. "--host", d.apiHost, "--port", d.apiPort, "--diode-target", d.diodeTarget, - "--diode-api-key", "********", + "--diode-client-id", d.diodeClientID, + "--diode-client-secret", "********", "--diode-app-name-prefix", d.diodeAppNamePrefix, } - d.logger.Info("network-discovery startup", zap.Strings("arguments", pvOptions)) + d.logger.Info("network-discovery startup", slog.Any("arguments", pvOptions)) - pvOptions[7] = d.diodeAPIKey + pvOptions[9] = d.diodeClientSecret - d.proc = cmd.NewCmdOptions(cmd.Options{ + d.proc = backend.NewCmdOptions(backend.CmdOptions{ Buffered: false, Streaming: true, }, d.exec, pvOptions...) @@ -124,20 +130,22 @@ func (d *networkDiscoveryBackend) Start(ctx context.Context, cancelFunc context. close(doneChan) } }() - for d.proc.Stdout != nil || d.proc.Stderr != nil { + stdout := d.proc.GetStdout() + stderr := d.proc.GetStderr() + for stdout != nil || stderr != nil { select { - case line, open := <-d.proc.Stdout: + case line, open := <-stdout: if !open { - d.proc.Stdout = nil + stdout = nil continue } - d.logger.Info("network-discovery stdout", zap.String("log", line)) - case line, open := <-d.proc.Stderr: + d.logger.Info("network-discovery stdout", slog.String("log", line)) + case line, open := <-stderr: if !open { - d.proc.Stderr = nil + stderr = nil continue } - d.logger.Info("network-discovery stderr", zap.String("log", line)) + d.logger.Info("network-discovery stderr", slog.String("log", line)) } } }() @@ -148,37 +156,40 @@ func (d *networkDiscoveryBackend) Start(ctx context.Context, cancelFunc context. status := d.proc.Status() if status.Error != nil { - d.logger.Error("network-discovery startup error", zap.Error(status.Error)) + d.logger.Error("network-discovery startup error", slog.Any("error", status.Error)) return status.Error } if status.Complete { err := d.proc.Stop() if err != nil { - d.logger.Error("proc.Stop error", zap.Error(err)) + d.logger.Error("proc.Stop error", slog.Any("error", err)) } return errors.New("network-discovery startup error, check log") } - d.logger.Info("network-discovery process started", zap.Int("pid", status.PID)) + d.logger.Info("network-discovery process started", slog.Int("pid", status.PID)) + var version string var readinessErr error - for backoff := 0; backoff < readinessBackoff; backoff++ { - version, readinessErr := d.Version() + for backoff := range readinessBackoff { + version, readinessErr = d.Version() if readinessErr == nil { - d.logger.Info("network-discovery readiness ok, got version ", zap.String("network_discovery_version", version)) + d.logger.Info("network-discovery readiness ok, got version ", + slog.String("network_discovery_version", version)) break } backoffDuration := time.Duration(backoff) * time.Second - d.logger.Info("network-discovery is not ready, trying again with backoff", zap.String("backoff backoffDuration", backoffDuration.String())) + d.logger.Info("network-discovery is not ready, trying again with backoff", + slog.String("backoff backoffDuration", backoffDuration.String())) time.Sleep(backoffDuration) } if readinessErr != nil { - d.logger.Error("network-discovery error on readiness", zap.Error(readinessErr)) + d.logger.Error("network-discovery error on readiness", slog.Any("error", readinessErr)) err := d.proc.Stop() if err != nil { - d.logger.Error("proc.Stop error", zap.Error(err)) + d.logger.Error("proc.Stop error", slog.Any("error", err)) } return readinessErr } @@ -187,22 +198,23 @@ func (d *networkDiscoveryBackend) Start(ctx context.Context, cancelFunc context. } func (d *networkDiscoveryBackend) Stop(ctx context.Context) error { - d.logger.Info("routine call to stop network-discovery", zap.Any("routine", ctx.Value(config.ContextKey("routine")))) + d.logger.Info("routine call to stop network-discovery", slog.Any("routine", ctx.Value(config.ContextKey("routine")))) defer d.cancelFunc() err := d.proc.Stop() finalStatus := <-d.statusChan if err != nil { - d.logger.Error("network-discovery shutdown error", zap.Error(err)) + d.logger.Error("network-discovery shutdown error", slog.Any("error", err)) } - d.logger.Info("network-discovery process stopped", zap.Int("pid", finalStatus.PID), zap.Int("exit_code", finalStatus.Exit)) + d.logger.Info("network-discovery process stopped", slog.Int("pid", finalStatus.PID), + slog.Int("exit_code", finalStatus.Exit)) return nil } func (d *networkDiscoveryBackend) FullReset(ctx context.Context) error { // force a stop, which stops scrape as well. if proc is dead, it no ops. - if state, _, _ := d.getProcRunningStatus(); state == backend.Running { + if state, _, _ := backend.GetRunningStatus(d.proc); state == backend.Running { if err := d.Stop(ctx); err != nil { - d.logger.Error("failed to stop backend on restart procedure", zap.Error(err)) + d.logger.Error("failed to stop backend on restart procedure", slog.Any("error", err)) return err } } @@ -210,7 +222,7 @@ func (d *networkDiscoveryBackend) FullReset(ctx context.Context) error { backendCtx, cancelFunc := context.WithCancel(context.WithValue(ctx, config.ContextKey("routine"), "network-discovery")) // start it if err := d.Start(backendCtx, cancelFunc); err != nil { - d.logger.Error("failed to start backend on restart procedure", zap.Error(err)) + d.logger.Error("failed to start backend on restart procedure", slog.Any("error", err)) return err } return nil @@ -220,9 +232,11 @@ func (d *networkDiscoveryBackend) GetStartTime() time.Time { return d.startTime } -func (d *networkDiscoveryBackend) GetCapabilities() (map[string]interface{}, error) { - caps := make(map[string]interface{}) - err := d.request("capabilities", &caps, http.MethodGet, http.NoBody, "application/json", capabilitiesTimeout) +func (d *networkDiscoveryBackend) GetCapabilities() (map[string]any, error) { + caps := make(map[string]any) + url := fmt.Sprintf("%s://%s:%s/api/v1/capabilities", d.apiProtocol, d.apiHost, d.apiPort) + err := backend.CommonRequest("network-discovery", d.proc, d.logger, url, &caps, http.MethodGet, + http.NoBody, "application/json", capabilitiesTimeout, "detail") if err != nil { return nil, err } @@ -231,7 +245,7 @@ func (d *networkDiscoveryBackend) GetCapabilities() (map[string]interface{}, err func (d *networkDiscoveryBackend) GetRunningStatus() (backend.RunningStatus, string, error) { // first check process status - runningStatus, errMsg, err := d.getProcRunningStatus() + runningStatus, errMsg, err := backend.GetRunningStatus(d.proc) // if it's not running, we're done if runningStatus != backend.Running { return runningStatus, errMsg, err @@ -252,28 +266,31 @@ func (d *networkDiscoveryBackend) ApplyPolicy(data policies.PolicyData, updatePo if updatePolicy { // To update a policy it's necessary first remove it and then apply a new version if err := d.RemovePolicy(data); err != nil { - d.logger.Warn("policy failed to remove", zap.String("policy_id", data.ID), zap.String("policy_name", data.Name), zap.Error(err)) + d.logger.Warn("policy failed to remove", slog.String("policy_id", data.ID), + slog.String("policy_name", data.Name), slog.Any("error", err)) } } - d.logger.Debug("network-discovery policy apply", zap.String("policy_id", data.ID), zap.Any("data", data.Data)) + d.logger.Debug("network-discovery policy apply", slog.String("policy_id", data.ID), slog.Any("data", data.Data)) - fullPolicy := map[string]interface{}{ - "policies": map[string]interface{}{ + fullPolicy := map[string]any{ + "policies": map[string]any{ data.Name: data.Data, }, } policyYaml, err := yaml.Marshal(fullPolicy) if err != nil { - d.logger.Warn("policy yaml marshal failure", zap.String("policy_id", data.ID), zap.Any("policy", fullPolicy)) + d.logger.Warn("policy yaml marshal failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) return err } - var resp map[string]interface{} - err = d.request("policies", &resp, http.MethodPost, bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout) + var resp map[string]any + url := fmt.Sprintf("%s://%s:%s/api/v1/%s", d.apiProtocol, d.apiHost, d.apiPort, "policies") + err = backend.CommonRequest("network-discovery", d.proc, d.logger, url, &resp, http.MethodPost, + bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout, "detail") if err != nil { - d.logger.Warn("policy application failure", zap.String("policy_id", data.ID), zap.ByteString("policy", policyYaml)) + d.logger.Warn("policy application failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) return err } @@ -281,14 +298,16 @@ func (d *networkDiscoveryBackend) ApplyPolicy(data policies.PolicyData, updatePo } func (d *networkDiscoveryBackend) RemovePolicy(data policies.PolicyData) error { - d.logger.Debug("network-discovery policy remove", zap.String("policy_id", data.ID)) - var resp interface{} + d.logger.Debug("network-discovery policy remove", slog.String("policy_id", data.ID)) + var resp any name := data.Name // Since we use Name for removing policies not IDs, if there is a change, we need to remove the previous name of the policy if data.PreviousPolicyData != nil && data.PreviousPolicyData.Name != data.Name { name = data.PreviousPolicyData.Name } - err := d.request(fmt.Sprintf("policies/%s", name), &resp, http.MethodDelete, http.NoBody, "application/json", removePolicyTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/policies/%s", d.apiProtocol, d.apiHost, d.apiPort, name) + err := backend.CommonRequest("network-discovery", d.proc, d.logger, url, &resp, http.MethodDelete, + http.NoBody, "application/json", removePolicyTimeout, "detail") if err != nil { return err } diff --git a/agent/backend/networkdiscovery/network_discovery_test.go b/agent/backend/networkdiscovery/network_discovery_test.go new file mode 100644 index 00000000..783041cc --- /dev/null +++ b/agent/backend/networkdiscovery/network_discovery_test.go @@ -0,0 +1,202 @@ +package networkdiscovery_test + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/backend/mocks" + "github.com/netboxlabs/orb-agent/agent/backend/networkdiscovery" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/policies" +) + +type StatusResponse struct { + StartTime string `json:"start_time"` + Version string `json:"version"` + UpTime float64 `json:"up_time"` +} + +func TestNetworkDiscoveryBackendStart(t *testing.T) { + // Create server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.URL.Path == "/api/v1/status" { + response := StatusResponse{ + Version: "1.3.4", + StartTime: "2023-10-01T12:00:00Z", + UpTime: 123.456, + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.URL.Path == "/api/v1/capabilities" { + capabilities := map[string]any{ + "capability": true, + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(capabilities) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if strings.Contains(r.URL.Path, "/api/v1/policies") { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy applied successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy removed successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + // Parse server URL + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + // Create logger + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // Create a mock repository + repo, err := policies.NewMemRepo() + assert.NoError(t, err) + + // Create a mock command + mockCmd := &mocks.MockCmd{} + mocks.SetupSuccessfulProcess(mockCmd, 12345) + + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(options backend.CmdOptions, name string, args ...string) backend.Commander { + // Assert that the correct parameters were passed + assert.Equal(t, "network-discovery", name, "Expected command name to be network-discovery") + assert.Contains(t, args, "--port", "Expected args to contain port") + assert.Contains(t, args, "--host", "Expected args to contain host") + assert.False(t, options.Buffered, "Expected buffered to be false") + assert.True(t, options.Streaming, "Expected streaming to be true") + return mockCmd + } + + assert.True(t, networkdiscovery.Register(), "Failed to register NetworkDiscovery backend") + + assert.True(t, backend.HaveBackend("network_discovery"), "Failed to get NetworkDiscovery backend") + + be := backend.GetBackend("network_discovery") + + assert.Equal(t, backend.Unknown, be.GetInitialState()) + + // Configure backend + err = be.Configure(logger, repo, map[string]any{ + "host": serverURL.Hostname(), + "port": serverURL.Port(), + }, config.BackendCommons{}) + assert.NoError(t, err) + + // Start the backend + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + // Assert successful start + assert.NoError(t, err) + + // Get Running status + status, _, err := be.GetRunningStatus() + assert.NoError(t, err) + assert.Equal(t, backend.Running, status, "Expected backend to be running") + + // Get capabilities + capabilities, err := be.GetCapabilities() + assert.NoError(t, err) + assert.Equal(t, capabilities["capability"], true, "Expected capability to be true") + + data := policies.PolicyData{ + ID: "dummy-policy-id", + Name: "dummy-policy-name", + Data: map[string]any{"key": "value"}, + } + // Apply policy + err = be.ApplyPolicy(data, false) + assert.NoError(t, err) + + // Update policy + err = be.ApplyPolicy(data, true) + assert.NoError(t, err) + + // Assert restart + err = be.FullReset(ctx) + assert.NoError(t, err) + + // Verify expectations + mockCmd.AssertExpectations(t) +} + +func TestNetworkDiscoveryBackendCompleted(t *testing.T) { + // Create a mock command that simulates a failure + mockCmd := &mocks.MockCmd{} + mocks.SetupCompletedProcess(mockCmd, 0, nil) + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(_ backend.CmdOptions, _ string, _ ...string) backend.Commander { + return mockCmd + } + + assert.True(t, networkdiscovery.Register(), "Failed to register NetworkDiscovery backend") + + assert.True(t, backend.HaveBackend("network_discovery"), "Failed to get NetworkDiscovery backend") + + be := backend.GetBackend("network_discovery") + + // Configure backend with invalid parameters + err := be.Configure(slog.Default(), nil, map[string]any{ + "host": "invalid-host", + }, config.BackendCommons{}) + assert.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + assert.Error(t, err) +} diff --git a/agent/backend/networkdiscovery/utils.go b/agent/backend/networkdiscovery/utils.go deleted file mode 100644 index 0fb2691a..00000000 --- a/agent/backend/networkdiscovery/utils.go +++ /dev/null @@ -1,96 +0,0 @@ -package networkdiscovery - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "go.uber.org/zap" - - "github.com/netboxlabs/orb-agent/agent/backend" -) - -func (d *networkDiscoveryBackend) getProcRunningStatus() (backend.RunningStatus, string, error) { - if d.proc == nil { - return backend.Unknown, "backend not started yet", nil - } - status := d.proc.Status() - - if status.Error != nil { - errMsg := fmt.Sprintf("network-discovery process error: %v", status.Error) - return backend.BackendError, errMsg, status.Error - } - - if status.Complete { - err := d.proc.Stop() - return backend.Offline, "network-discovery process ended", err - } - - if status.StopTs > 0 { - return backend.Offline, "network-discovery process ended", nil - } - return backend.Running, "", nil -} - -// note this needs to be stateless because it is called for multiple go routines -func (d *networkDiscoveryBackend) request(url string, payload interface{}, method string, body io.Reader, contentType string, timeout int32) error { - client := http.Client{ - Timeout: time.Second * time.Duration(timeout), - } - - status, _, err := d.getProcRunningStatus() - if status != backend.Running { - d.logger.Warn("skipping network discovery REST API request because process is not running or is unresponsive", zap.String("url", url), zap.String("method", method), zap.Error(err)) - return err - } - - URL := fmt.Sprintf("%s://%s:%s/api/v1/%s", d.apiProtocol, d.apiHost, d.apiPort, url) - - req, err := http.NewRequest(method, URL, body) - if err != nil { - d.logger.Error("received error from payload", zap.Error(err)) - return err - } - - req.Header.Add("Content-Type", contentType) - res, getErr := client.Do(req) - - if getErr != nil { - d.logger.Error("received error from payload", zap.Error(getErr)) - return getErr - } - - defer func() { - if err := res.Body.Close(); err != nil { - d.logger.Error("failed to close response body", zap.Error(err)) - } - }() - - if (res.StatusCode < 200) || (res.StatusCode > 299) { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("non 2xx HTTP error code from network-discovery, no or invalid body: %d", res.StatusCode) - } - if len(body) == 0 { - return fmt.Errorf("%d empty body", res.StatusCode) - } else if body[0] == '{' { - var jsonBody map[string]interface{} - err := json.Unmarshal(body, &jsonBody) - if err == nil { - if errMsg, ok := jsonBody["detail"]; ok { - return fmt.Errorf("%d %s", res.StatusCode, errMsg) - } - } - } - } - - if res.Body != nil { - err = json.NewDecoder(res.Body).Decode(&payload) - if err != nil { - return err - } - } - return nil -} diff --git a/agent/backend/networkdiscovery/vars.go b/agent/backend/networkdiscovery/vars.go deleted file mode 100644 index 405c7360..00000000 --- a/agent/backend/networkdiscovery/vars.go +++ /dev/null @@ -1,11 +0,0 @@ -package networkdiscovery - -import ( - "github.com/spf13/viper" -) - -// RegisterBackendSpecificVariables registers the backend specific variables for the network discovery backend -func RegisterBackendSpecificVariables(v *viper.Viper) { - v.SetDefault("orb.backends.network_discovery.host", defaultAPIHost) - v.SetDefault("orb.backends.network_discovery.port", defaultAPIPort) -} diff --git a/agent/backend/otel/otel.go b/agent/backend/otel/otel.go index 8ddac7e4..de4f39a0 100644 --- a/agent/backend/otel/otel.go +++ b/agent/backend/otel/otel.go @@ -5,11 +5,10 @@ import ( "context" "errors" "fmt" + "log/slog" "net/http" "time" - "github.com/go-cmd/cmd" - "go.uber.org/zap" "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" @@ -32,7 +31,7 @@ const ( ) type openTelemetryBackend struct { - logger *zap.Logger + logger *slog.Logger policyRepo policies.PolicyRepo exec string @@ -41,9 +40,9 @@ type openTelemetryBackend struct { apiProtocol string startTime time.Time - proc *cmd.Cmd + proc backend.Commander agentLabels map[string]string - statusChan <-chan cmd.Status + statusChan <-chan backend.CmdStatus cancelFunc context.CancelFunc ctx context.Context } @@ -64,8 +63,8 @@ func Register() bool { } // Configure initializes the backend with the given configuration -func (o *openTelemetryBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo, - config map[string]interface{}, common config.BackendCommons, +func (o *openTelemetryBackend) Configure(logger *slog.Logger, repo policies.PolicyRepo, + config map[string]any, common config.BackendCommons, ) error { o.logger = logger o.policyRepo = repo @@ -89,7 +88,9 @@ func (o *openTelemetryBackend) GetInitialState() backend.RunningStatus { func (o *openTelemetryBackend) Version() (string, error) { var info info - err := o.request("status", &info, http.MethodGet, http.NoBody, "application/json", versionTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/status", o.apiProtocol, o.apiHost, o.apiPort) + err := backend.CommonRequest("opentelemetry-infinity", o.proc, o.logger, url, &info, http.MethodGet, + http.NoBody, "application/json", versionTimeout, "message") if err != nil { return "", err } @@ -107,9 +108,9 @@ func (o *openTelemetryBackend) Start(ctx context.Context, cancelFunc context.Can "--server_port", o.apiPort, } - o.logger.Info("opentelemetry infinity startup", zap.Strings("arguments", pvOptions)) + o.logger.Info("opentelemetry infinity startup", slog.Any("arguments", pvOptions)) - o.proc = cmd.NewCmdOptions(cmd.Options{ + o.proc = backend.NewCmdOptions(backend.CmdOptions{ Buffered: false, Streaming: true, }, o.exec, pvOptions...) @@ -123,20 +124,22 @@ func (o *openTelemetryBackend) Start(ctx context.Context, cancelFunc context.Can close(doneChan) } }() - for o.proc.Stdout != nil || o.proc.Stderr != nil { + stdout := o.proc.GetStdout() + stderr := o.proc.GetStderr() + for stdout != nil || stderr != nil { select { - case line, open := <-o.proc.Stdout: + case line, open := <-stdout: if !open { - o.proc.Stdout = nil + stdout = nil continue } - o.logger.Info("opentelemetry infinity stdout", zap.String("log", line)) - case line, open := <-o.proc.Stderr: + o.logger.Info("opentelemetry infinity stdout", slog.String("log", line)) + case line, open := <-stderr: if !open { - o.proc.Stderr = nil + stderr = nil continue } - o.logger.Info("opentelemetry infinity stderr", zap.String("log", line)) + o.logger.Info("opentelemetry infinity stderr", slog.String("log", line)) } } }() @@ -147,37 +150,40 @@ func (o *openTelemetryBackend) Start(ctx context.Context, cancelFunc context.Can status := o.proc.Status() if status.Error != nil { - o.logger.Error("opentelemetry infinity startup error", zap.Error(status.Error)) + o.logger.Error("opentelemetry infinity startup error", slog.Any("error", status.Error)) return status.Error } if status.Complete { err := o.proc.Stop() if err != nil { - o.logger.Error("proc.Stop error", zap.Error(err)) + o.logger.Error("proc.Stop error", slog.Any("error", err)) } return errors.New("opentelemetry infinity startup error, check log") } - o.logger.Info("opentelemetry infinity process started", zap.Int("pid", status.PID)) + o.logger.Info("opentelemetry infinity process started", slog.Int("pid", status.PID)) + var version string var readinessErr error - for backoff := 0; backoff < readinessBackoff; backoff++ { - version, readinessErr := o.Version() + for backoff := range readinessBackoff { + version, readinessErr = o.Version() if readinessErr == nil { - o.logger.Info("opentelemetry infinity readiness ok, got version ", zap.String("device_discovery_version", version)) + o.logger.Info("opentelemetry infinity readiness ok, got version ", + slog.String("device_discovery_version", version)) break } backoffDuration := time.Duration(backoff) * time.Second - o.logger.Info("opentelemetry infinity is not ready, trying again with backoff", zap.String("backoff backoffDuration", backoffDuration.String())) + o.logger.Info("opentelemetry infinity is not ready, trying again with backoff", + slog.String("backoff backoffDuration", backoffDuration.String())) time.Sleep(backoffDuration) } if readinessErr != nil { - o.logger.Error("opentelemetry infinity error on readiness", zap.Error(readinessErr)) + o.logger.Error("opentelemetry infinity error on readiness", slog.Any("error", readinessErr)) err := o.proc.Stop() if err != nil { - o.logger.Error("proc.Stop error", zap.Error(err)) + o.logger.Error("proc.Stop error", slog.Any("error", err)) } return readinessErr } @@ -186,22 +192,24 @@ func (o *openTelemetryBackend) Start(ctx context.Context, cancelFunc context.Can } func (o *openTelemetryBackend) Stop(ctx context.Context) error { - o.logger.Info("routine call to stop opentelemetry infinity", zap.Any("routine", ctx.Value(config.ContextKey("routine")))) + o.logger.Info("routine call to stop opentelemetry infinity", + slog.Any("routine", ctx.Value(config.ContextKey("routine")))) defer o.cancelFunc() err := o.proc.Stop() finalStatus := <-o.statusChan if err != nil { - o.logger.Error("opentelemetry infinity shutdown error", zap.Error(err)) + o.logger.Error("opentelemetry infinity shutdown error", slog.Any("error", err)) } - o.logger.Info("opentelemetry infinity process stopped", zap.Int("pid", finalStatus.PID), zap.Int("exit_code", finalStatus.Exit)) + o.logger.Info("opentelemetry infinity process stopped", slog.Int("pid", finalStatus.PID), + slog.Int("exit_code", finalStatus.Exit)) return nil } func (o *openTelemetryBackend) FullReset(ctx context.Context) error { // force a stop, which stops scrape as well. if proc is dead, it no ops. - if state, _, _ := o.getProcRunningStatus(); state == backend.Running { + if state, _, _ := backend.GetRunningStatus(o.proc); state == backend.Running { if err := o.Stop(ctx); err != nil { - o.logger.Error("failed to stop backend on restart procedure", zap.Error(err)) + o.logger.Error("failed to stop backend on restart procedure", slog.Any("error", err)) return err } } @@ -209,7 +217,7 @@ func (o *openTelemetryBackend) FullReset(ctx context.Context) error { backendCtx, cancelFunc := context.WithCancel(context.WithValue(ctx, config.ContextKey("routine"), "opentelemetry")) // start it if err := o.Start(backendCtx, cancelFunc); err != nil { - o.logger.Error("failed to start backend on restart procedure", zap.Error(err)) + o.logger.Error("failed to start backend on restart procedure", slog.Any("error", err)) return err } return nil @@ -220,9 +228,11 @@ func (o *openTelemetryBackend) GetStartTime() time.Time { } // GetCapabilities this will only print a default backend config -func (o *openTelemetryBackend) GetCapabilities() (map[string]interface{}, error) { - caps := make(map[string]interface{}) - err := o.request("capabilities", &caps, http.MethodGet, http.NoBody, "application/json", capabilitiesTimeout) +func (o *openTelemetryBackend) GetCapabilities() (map[string]any, error) { + caps := make(map[string]any) + url := fmt.Sprintf("%s://%s:%s/api/v1/capabilities", o.apiProtocol, o.apiHost, o.apiPort) + err := backend.CommonRequest("opentelemetry-infinity", o.proc, o.logger, url, &caps, http.MethodGet, + http.NoBody, "application/json", capabilitiesTimeout, "message") if err != nil { return nil, err } @@ -232,7 +242,7 @@ func (o *openTelemetryBackend) GetCapabilities() (map[string]interface{}, error) // GetRunningStatus returns cross-reference the Processes using the os, with the policies and contexts func (o *openTelemetryBackend) GetRunningStatus() (backend.RunningStatus, string, error) { // first check process status - runningStatus, errMsg, err := o.getProcRunningStatus() + runningStatus, errMsg, err := backend.GetRunningStatus(o.proc) // if it's not running, we're done if runningStatus != backend.Running { return runningStatus, errMsg, err @@ -249,26 +259,29 @@ func (o *openTelemetryBackend) ApplyPolicy(data policies.PolicyData, updatePolic if updatePolicy { // To update a policy it's necessary first remove it and then apply a new version if err := o.RemovePolicy(data); err != nil { - o.logger.Warn("policy failed to remove", zap.String("policy_id", data.ID), zap.String("policy_name", data.Name), zap.Error(err)) + o.logger.Warn("policy failed to remove", slog.String("policy_id", data.ID), + slog.String("policy_name", data.Name), slog.Any("error", err)) } } - o.logger.Debug("opentelemetry infinity policy apply", zap.String("policy_id", data.ID), zap.Any("data", data.Data)) + o.logger.Debug("opentelemetry infinity policy apply", slog.String("policy_id", data.ID), slog.Any("data", data.Data)) - fullPolicy := map[string]interface{}{ + fullPolicy := map[string]any{ data.Name: data.Data, } policyYaml, err := yaml.Marshal(fullPolicy) if err != nil { - o.logger.Warn("policy yaml marshal failure", zap.String("policy_id", data.ID), zap.Any("policy", fullPolicy)) + o.logger.Warn("policy yaml marshal failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) return err } - var resp map[string]interface{} - err = o.request("policies", &resp, http.MethodPost, bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout) + var resp map[string]any + url := fmt.Sprintf("%s://%s:%s/api/v1/policies", o.apiProtocol, o.apiHost, o.apiPort) + err = backend.CommonRequest("opentelemetry-infinity", o.proc, o.logger, url, &resp, http.MethodPost, + bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout, "message") if err != nil { - o.logger.Warn("policy application failure", zap.String("policy_id", data.ID), zap.ByteString("policy", policyYaml)) + o.logger.Warn("policy application failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) return err } @@ -276,8 +289,8 @@ func (o *openTelemetryBackend) ApplyPolicy(data policies.PolicyData, updatePolic } func (o *openTelemetryBackend) RemovePolicy(data policies.PolicyData) error { - o.logger.Debug("opentelemetry policy remove", zap.String("policy_id", data.ID)) - var resp interface{} + o.logger.Debug("opentelemetry policy remove", slog.String("policy_id", data.ID)) + var resp any var name string // Since we use Name for removing policies not IDs, if there is a change, we need to remove the previous name of the policy if data.PreviousPolicyData != nil && data.PreviousPolicyData.Name != data.Name { @@ -285,7 +298,9 @@ func (o *openTelemetryBackend) RemovePolicy(data policies.PolicyData) error { } else { name = data.Name } - err := o.request(fmt.Sprintf("policies/%s", name), &resp, http.MethodDelete, http.NoBody, "application/json", removePolicyTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/policies/%s", o.apiProtocol, o.apiHost, o.apiPort, name) + err := backend.CommonRequest("opentelemetry-infinity", o.proc, o.logger, url, &resp, http.MethodDelete, + http.NoBody, "application/json", removePolicyTimeout, "message") if err != nil { return err } diff --git a/agent/backend/otel/otel_test.go b/agent/backend/otel/otel_test.go new file mode 100644 index 00000000..0e311fc0 --- /dev/null +++ b/agent/backend/otel/otel_test.go @@ -0,0 +1,201 @@ +// otel_test.go +package otel_test + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/backend/mocks" + "github.com/netboxlabs/orb-agent/agent/backend/otel" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/policies" +) + +type StatusResponse struct { + StartTime string `json:"start_time"` + Version string `json:"version"` + UpTime float64 `json:"up_time"` +} + +func TestOpenTelemetryBackendStart(t *testing.T) { + // Create server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.URL.Path == "/api/v1/status" { + response := StatusResponse{ + Version: "1.3.4", + StartTime: "2023-10-01T12:00:00Z", + UpTime: 123.456, + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.URL.Path == "/api/v1/capabilities" { + capabilities := map[string]any{ + "capability": true, + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(capabilities) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if strings.Contains(r.URL.Path, "/api/v1/policies") { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy applied successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy removed successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + // Parse server URL + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + // Create logger + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // Create a mock repository + repo, err := policies.NewMemRepo() + assert.NoError(t, err) + + // Create a mock command + mockCmd := &mocks.MockCmd{} + mocks.SetupSuccessfulProcess(mockCmd, 12345) + + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(options backend.CmdOptions, name string, args ...string) backend.Commander { + // Assert that the correct parameters were passed + assert.Equal(t, "otlpinf", name, "Expected command name to be otlpinf") + assert.Contains(t, args, "run", "Expected args to contain 'run'") + assert.Contains(t, args, "--server_host", "Expected args to contain server host") + assert.False(t, options.Buffered, "Expected buffered to be false") + assert.True(t, options.Streaming, "Expected streaming to be true") + return mockCmd + } + + assert.True(t, otel.Register(), "Failed to register OpenTelemetry backend") + + assert.True(t, backend.HaveBackend("otel"), "Failed to get OpenTelemetry backend") + + be := backend.GetBackend("otel") + + // Configure backend + err = be.Configure(logger, repo, map[string]any{ + "host": serverURL.Hostname(), + "port": serverURL.Port(), + }, config.BackendCommons{}) + assert.NoError(t, err) + + // Start the backend + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + // Assert successful start + assert.NoError(t, err) + + // Get Running status + status, _, err := be.GetRunningStatus() + assert.NoError(t, err) + assert.Equal(t, backend.Running, status, "Expected backend to be running") + + // Get capabilities + capabilities, err := be.GetCapabilities() + assert.NoError(t, err) + assert.Equal(t, capabilities["capability"], true, "Expected capability to be true") + + data := policies.PolicyData{ + ID: "dummy-policy-id", + Name: "dummy-policy-name", + Data: map[string]any{"key": "value"}, + } + // Apply policy + err = be.ApplyPolicy(data, false) + assert.NoError(t, err) + + // Update policy + err = be.ApplyPolicy(data, true) + assert.NoError(t, err) + + // Assert restart + err = be.FullReset(ctx) + assert.NoError(t, err) + + // Verify expectations + mockCmd.AssertExpectations(t) +} + +func TestOpenTelemetryBackendCompleted(t *testing.T) { + // Create a mock command that simulates a failure + mockCmd := &mocks.MockCmd{} + mocks.SetupCompletedProcess(mockCmd, 0, nil) + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(_ backend.CmdOptions, _ string, _ ...string) backend.Commander { + return mockCmd + } + + assert.True(t, otel.Register(), "Failed to register Opentelemetry backend") + + assert.True(t, backend.HaveBackend("otel"), "Failed to get Opentelemetry backend") + + be := backend.GetBackend("otel") + + // Configure backend with invalid parameters + err := be.Configure(slog.Default(), nil, map[string]any{ + "host": "invalid-host", + }, config.BackendCommons{}) + assert.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + assert.Error(t, err) +} diff --git a/agent/backend/otel/utils.go b/agent/backend/otel/utils.go deleted file mode 100644 index d061d85e..00000000 --- a/agent/backend/otel/utils.go +++ /dev/null @@ -1,109 +0,0 @@ -package otel - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "go.uber.org/zap" - "gopkg.in/yaml.v3" - - "github.com/netboxlabs/orb-agent/agent/backend" -) - -func (d *openTelemetryBackend) getProcRunningStatus() (backend.RunningStatus, string, error) { - if d.proc == nil { - return backend.Unknown, "backend not started yet", nil - } - status := d.proc.Status() - - if status.Error != nil { - errMsg := fmt.Sprintf("opentelemetry infinity process error: %v", status.Error) - return backend.BackendError, errMsg, status.Error - } - - if status.Complete { - err := d.proc.Stop() - return backend.Offline, "opentelemetry infinity process ended", err - } - - if status.StopTs > 0 { - return backend.Offline, "opentelemetry infinity process ended", nil - } - return backend.Running, "", nil -} - -// note this needs to be stateless because it is called for multiple go routines -func (d *openTelemetryBackend) request(url string, payload interface{}, method string, body io.Reader, contentType string, timeout int32) error { - client := http.Client{ - Timeout: time.Second * time.Duration(timeout), - } - - status, _, err := d.getProcRunningStatus() - if status != backend.Running { - d.logger.Warn("skipping opentelemetry infinity REST API request because process is not running or is unresponsive", zap.String("url", url), zap.String("method", method), zap.Error(err)) - return err - } - - URL := fmt.Sprintf("%s://%s:%s/api/v1/%s", d.apiProtocol, d.apiHost, d.apiPort, url) - - req, err := http.NewRequest(method, URL, body) - if err != nil { - d.logger.Error("received error from payload", zap.Error(err)) - return err - } - - req.Header.Add("Content-Type", contentType) - res, getErr := client.Do(req) - - if getErr != nil { - d.logger.Error("received error from payload", zap.Error(getErr)) - return getErr - } - - defer func() { - if err := res.Body.Close(); err != nil { - d.logger.Error("failed to close response body", zap.Error(err)) - } - }() - - if (res.StatusCode < 200) || (res.StatusCode > 299) { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("non 2xx HTTP error code from opentelemetry infinity, no or invalid body: %d", res.StatusCode) - } - if len(body) == 0 { - return fmt.Errorf("%d empty body", res.StatusCode) - } else if body[0] == '{' { - var jsonBody map[string]interface{} - if err := json.Unmarshal(body, &jsonBody); err == nil { - if errMsg, ok := jsonBody["message"]; ok { - return fmt.Errorf("%d %s", res.StatusCode, errMsg) - } - } else { - return fmt.Errorf("%d %s", res.StatusCode, body) - } - } else { - return fmt.Errorf("%d %s", res.StatusCode, body) - } - } - - // Read and decode response body - if res.Body != nil { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %w", err) - } - if err = json.Unmarshal(body, &payload); err == nil { - return nil - } - var yamlErr error - if yamlErr = yaml.Unmarshal(body, &payload); yamlErr == nil { - return nil - } - return fmt.Errorf("failed to decode response as JSON: %w and YAML: %w", err, yamlErr) - } - return nil -} diff --git a/agent/backend/otel/vars.go b/agent/backend/otel/vars.go deleted file mode 100644 index b62ab39e..00000000 --- a/agent/backend/otel/vars.go +++ /dev/null @@ -1,8 +0,0 @@ -package otel - -import "github.com/spf13/viper" - -// RegisterBackendSpecificVariables registers the backend specific variables for the otel backend -func RegisterBackendSpecificVariables(v *viper.Viper) { - v.SetDefault("orb.backends.otel.server_port", "10222") -} diff --git a/agent/backend/pktvisor/pktvisor.go b/agent/backend/pktvisor/pktvisor.go index 9548be54..026c7dd2 100644 --- a/agent/backend/pktvisor/pktvisor.go +++ b/agent/backend/pktvisor/pktvisor.go @@ -1,17 +1,17 @@ package pktvisor import ( + "bytes" "context" "errors" "fmt" + "log/slog" "net/http" "os" "os/exec" "strconv" "time" - "github.com/go-cmd/cmd" - "go.uber.org/zap" "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" @@ -43,12 +43,12 @@ type AppInfo struct { } type pktvisorBackend struct { - logger *zap.Logger + logger *slog.Logger binary string configFile string pktvisorVersion string - proc *cmd.Cmd - statusChan <-chan cmd.Status + proc backend.Commander + statusChan <-chan backend.CmdStatus startTime time.Time cancelFunc context.CancelFunc ctx context.Context @@ -76,7 +76,7 @@ func (p *pktvisorBackend) GetInitialState() backend.RunningStatus { func (p *pktvisorBackend) GetRunningStatus() (backend.RunningStatus, string, error) { // first check process status - runningStatus, errMsg, err := p.getProcRunningStatus() + runningStatus, errMsg, err := backend.GetRunningStatus(p.proc) // if it's not running, we're done if runningStatus != backend.Running { return runningStatus, errMsg, err @@ -108,7 +108,7 @@ func (p *pktvisorBackend) Start(ctx context.Context, cancelFunc context.CancelFu _, err := exec.LookPath(p.binary) if err != nil { - p.logger.Error("pktvisor startup error: binary not found", zap.Error(err)) + p.logger.Error("pktvisor startup error: binary not found", slog.Any("error", err)) return err } @@ -134,9 +134,9 @@ func (p *pktvisorBackend) Start(ctx context.Context, cancelFunc context.CancelFu pvOptions = append(pvOptions, "--cp-custom", ctx.Value("agent_id").(string)) } - p.logger.Info("pktvisor startup", zap.Strings("arguments", pvOptions)) + p.logger.Info("pktvisor startup", slog.Any("arguments", pvOptions)) - p.proc = cmd.NewCmdOptions(cmd.Options{ + p.proc = backend.NewCmdOptions(backend.CmdOptions{ Buffered: false, Streaming: true, }, p.binary, pvOptions...) @@ -150,20 +150,22 @@ func (p *pktvisorBackend) Start(ctx context.Context, cancelFunc context.CancelFu close(doneChan) } }() - for p.proc.Stdout != nil || p.proc.Stderr != nil { + stdout := p.proc.GetStdout() + stderr := p.proc.GetStderr() + for stdout != nil || stderr != nil { select { - case line, open := <-p.proc.Stdout: + case line, open := <-stdout: if !open { - p.proc.Stdout = nil + stdout = nil continue } - p.logger.Info("pktvisor stdout", zap.String("log", line)) - case line, open := <-p.proc.Stderr: + p.logger.Info("pktvisor stdout", slog.String("log", line)) + case line, open := <-stderr: if !open { - p.proc.Stderr = nil + stderr = nil continue } - p.logger.Info("pktvisor stderr", zap.String("log", line)) + p.logger.Info("pktvisor stderr", slog.String("log", line)) } } }() @@ -174,38 +176,42 @@ func (p *pktvisorBackend) Start(ctx context.Context, cancelFunc context.CancelFu status := p.proc.Status() if status.Error != nil { - p.logger.Error("pktvisor startup error", zap.Error(status.Error)) + p.logger.Error("pktvisor startup error", slog.Any("error", status.Error)) return status.Error } if status.Complete { err = p.proc.Stop() if err != nil { - p.logger.Error("proc.Stop error", zap.Error(err)) + p.logger.Error("proc.Stop error", slog.Any("error", err)) } return errors.New("pktvisor startup error, check log") } - p.logger.Info("pktvisor process started", zap.Int("pid", status.PID)) + p.logger.Info("pktvisor process started", slog.Int("pid", status.PID)) var readinessError error - for backoff := 0; backoff < readinessBackoff; backoff++ { + for backoff := range readinessBackoff { var appMetrics AppInfo - readinessError = p.request("metrics/app", &appMetrics, http.MethodGet, http.NoBody, "application/json", readinessTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/metrics/app", p.adminAPIProtocol, p.adminAPIHost, p.adminAPIPort) + readinessError = backend.CommonRequest("pktvisor", p.proc, p.logger, url, &appMetrics, http.MethodGet, + http.NoBody, "application/json", readinessTimeout, "error") if readinessError == nil { - p.logger.Info("pktvisor readiness ok, got version ", zap.String("pktvisor_version", appMetrics.App.Version)) + p.logger.Info("pktvisor readiness ok, got version ", + slog.String("pktvisor_version", appMetrics.App.Version)) break } backoffDuration := time.Duration(backoff) * time.Second - p.logger.Info("pktvisor is not ready, trying again with backoff", zap.String("backoff backoffDuration", backoffDuration.String())) + p.logger.Info("pktvisor is not ready, trying again with backoff", + slog.String("backoff backoffDuration", backoffDuration.String())) time.Sleep(backoffDuration) } if readinessError != nil { - p.logger.Error("pktvisor error on readiness", zap.Error(readinessError)) + p.logger.Error("pktvisor error on readiness", slog.Any("error", readinessError)) err = p.proc.Stop() if err != nil { - p.logger.Error("proc.Stop error", zap.Error(err)) + p.logger.Error("proc.Stop error", slog.Any("error", err)) } return readinessError } @@ -214,20 +220,23 @@ func (p *pktvisorBackend) Start(ctx context.Context, cancelFunc context.CancelFu } func (p *pktvisorBackend) Stop(ctx context.Context) error { - p.logger.Info("routine call to stop pktvisor", zap.Any("routine", ctx.Value(config.ContextKey("routine")))) + p.logger.Info("routine call to stop pktvisor", slog.Any("routine", ctx.Value(config.ContextKey("routine")))) defer p.cancelFunc() err := p.proc.Stop() finalStatus := <-p.statusChan if err != nil { - p.logger.Error("pktvisor shutdown error", zap.Error(err)) + p.logger.Error("pktvisor shutdown error", slog.Any("error", err)) } - p.logger.Info("pktvisor process stopped", zap.Int("pid", finalStatus.PID), zap.Int("exit_code", finalStatus.Exit)) + p.logger.Info("pktvisor process stopped", slog.Int("pid", finalStatus.PID), + slog.Int("exit_code", finalStatus.Exit)) return nil } // Configure this will set configurations, but if not set, will use the following defaults -func (p *pktvisorBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo, config map[string]interface{}, common config.BackendCommons) error { +func (p *pktvisorBackend) Configure(logger *slog.Logger, repo policies.PolicyRepo, + config map[string]any, common config.BackendCommons, +) error { p.logger = logger p.policyRepo = repo @@ -244,8 +253,8 @@ func (p *pktvisorBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo } // Prepare the visor configuration structure - visorConfig := make(map[string]interface{}) - configSection := make(map[string]interface{}) + visorConfig := make(map[string]any) + configSection := make(map[string]any) // Process all config entries for key, value := range config { @@ -278,13 +287,15 @@ func (p *pktvisorBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo yamlData, err := yaml.Marshal(fullConfig) if err != nil { if rerr := os.Remove(tmpFile.Name()); rerr != nil { - p.logger.Error("failed to remove temp config file", zap.String("file", tmpFile.Name()), zap.Error(rerr)) + p.logger.Error("failed to remove temp config file", slog.String("file", tmpFile.Name()), + slog.Any("error", rerr)) } return fmt.Errorf("failed to marshal config: %w", err) } if err := os.WriteFile(tmpFile.Name(), yamlData, 0o644); err != nil { if rerr := os.Remove(tmpFile.Name()); rerr != nil { - p.logger.Error("failed to remove temp config file", zap.String("file", tmpFile.Name()), zap.Error(rerr)) + p.logger.Error("failed to remove temp config file", slog.String("file", tmpFile.Name()), + slog.Any("error", rerr)) } return fmt.Errorf("failed to write config file: %w", err) } @@ -294,28 +305,31 @@ func (p *pktvisorBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo if common.Otel.Host != "" && common.Otel.Port != 0 { p.otelReceiverHost = common.Otel.Host p.otelReceiverPort = common.Otel.Port - p.logger.Info("configured otel receiver host", zap.String("host", p.otelReceiverHost), zap.Int("port", p.otelReceiverPort)) + p.logger.Info("configured otel receiver host", slog.String("host", p.otelReceiverHost), + slog.Int("port", p.otelReceiverPort)) } return nil } -func (p *pktvisorBackend) GetCapabilities() (map[string]interface{}, error) { - var taps interface{} - err := p.request("taps", &taps, http.MethodGet, http.NoBody, "application/json", tapsTimeout) +func (p *pktvisorBackend) GetCapabilities() (map[string]any, error) { + var taps any + url := fmt.Sprintf("%s://%s:%s/api/v1/taps", p.adminAPIProtocol, p.adminAPIHost, p.adminAPIPort) + err := backend.CommonRequest("pktvisor", p.proc, p.logger, url, &taps, http.MethodGet, + http.NoBody, "application/json", tapsTimeout, "error") if err != nil { return nil, err } - jsonBody := make(map[string]interface{}) + jsonBody := make(map[string]any) jsonBody["taps"] = taps return jsonBody, nil } func (p *pktvisorBackend) FullReset(ctx context.Context) error { // force a stop, which stops scrape as well. if proc is dead, it no ops. - if state, _, _ := p.getProcRunningStatus(); state == backend.Running { + if state, _, _ := backend.GetRunningStatus(p.proc); state == backend.Running { if err := p.Stop(ctx); err != nil { - p.logger.Error("failed to stop backend on restart procedure", zap.Error(err)) + p.logger.Error("failed to stop backend on restart procedure", slog.Any("error", err)) return err } } @@ -325,7 +339,7 @@ func (p *pktvisorBackend) FullReset(ctx context.Context) error { // start it if err := p.Start(backendCtx, cancelFunc); err != nil { - p.logger.Error("failed to start backend on restart procedure", zap.Error(err)) + p.logger.Error("failed to start backend on restart procedure", slog.Any("error", err)) return err } @@ -339,3 +353,68 @@ func Register() bool { }) return true } + +func (p *pktvisorBackend) ApplyPolicy(data policies.PolicyData, updatePolicy bool) error { + if updatePolicy { + // To update a policy it's necessary first remove it and then apply a new version + if err := p.RemovePolicy(data); err != nil { + p.logger.Warn("policy failed to remove", slog.String("policy_id", data.ID), + slog.String("policy_name", data.Name), slog.Any("error", err)) + } + } + + p.logger.Debug("pktvisor policy apply", slog.String("policy_id", data.ID), slog.Any("data", data.Data)) + + fullPolicy := map[string]any{ + "version": "1.0", + "visor": map[string]any{ + "policies": map[string]any{ + data.Name: data.Data, + }, + }, + } + + policyYaml, err := yaml.Marshal(fullPolicy) + if err != nil { + p.logger.Warn("yaml policy marshal failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) + return err + } + + var resp map[string]any + url := fmt.Sprintf("%s://%s:%s/api/v1/policies", p.adminAPIProtocol, p.adminAPIHost, p.adminAPIPort) + err = backend.CommonRequest("pktvisor", p.proc, p.logger, url, &resp, http.MethodPost, + bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout, "error") + if err != nil { + p.logger.Warn("yaml policy application failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) + return err + } + + return nil +} + +func (p *pktvisorBackend) RemovePolicy(data policies.PolicyData) error { + p.logger.Debug("pktvisor policy remove", slog.String("policy_id", data.ID)) + var resp any + var name string + // Since we use Name for removing policies not IDs, if there is a change, we need to remove the previous name of the policy + if data.PreviousPolicyData != nil && data.PreviousPolicyData.Name != data.Name { + name = data.PreviousPolicyData.Name + } else { + name = data.Name + } + url := fmt.Sprintf("%s://%s:%s/api/v1/policies/%s", p.adminAPIProtocol, p.adminAPIHost, p.adminAPIPort, name) + err := backend.CommonRequest("pktvisor", p.proc, p.logger, url, &resp, http.MethodDelete, + http.NoBody, "application/json", removePolicyTimeout, "error") + if err != nil { + return err + } + return nil +} + +func (p *pktvisorBackend) getAppInfo() (AppInfo, error) { + var appInfo AppInfo + url := fmt.Sprintf("%s://%s:%s/api/v1/metrics/app", p.adminAPIProtocol, p.adminAPIHost, p.adminAPIPort) + err := backend.CommonRequest("pktvisor", p.proc, p.logger, url, &appInfo, http.MethodGet, + http.NoBody, "application/json", versionTimeout, "error") + return appInfo, err +} diff --git a/agent/backend/pktvisor/pktvisor_test.go b/agent/backend/pktvisor/pktvisor_test.go new file mode 100644 index 00000000..b341942b --- /dev/null +++ b/agent/backend/pktvisor/pktvisor_test.go @@ -0,0 +1,217 @@ +package pktvisor_test + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/backend/mocks" + "github.com/netboxlabs/orb-agent/agent/backend/pktvisor" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/policies" +) + +type StatusResponse struct { + StartTime string `json:"start_time"` + Version string `json:"version"` + UpTime float64 `json:"up_time"` +} + +func TestPktvisorBackendStart(t *testing.T) { + // Create server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.URL.Path == "/api/v1/metrics/app" { + var response pktvisor.AppInfo + response.App.Version = "1.2.3" + response.App.UpTimeMin = 42.5 + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.URL.Path == "/api/v1/taps" { + capabilities := map[string]any{ + "iface": "eth0", + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(capabilities) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if strings.Contains(r.URL.Path, "/api/v1/policies") { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy applied successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy removed successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + // Create a temporary directory and file for the test + tempDir := t.TempDir() + binaryPath := path.Join(tempDir, "pktvisord") + dummyBinary, err := os.Create(binaryPath) + require.NoError(t, err) + err = dummyBinary.Close() + require.NoError(t, err) + + // Make the binary executable + err = os.Chmod(binaryPath, 0o755) + require.NoError(t, err) + + // Add our temp directory to the PATH + err = os.Setenv("PATH", tempDir+string(os.PathListSeparator)+os.Getenv("PATH")) + require.NoError(t, err) + + // Parse server URL + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + // Create logger + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // Create a mock repository + repo, err := policies.NewMemRepo() + assert.NoError(t, err) + + // Create a mock command + mockCmd := &mocks.MockCmd{} + mocks.SetupSuccessfulProcess(mockCmd, 12345) + + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(options backend.CmdOptions, name string, args ...string) backend.Commander { + // Assert that the correct parameters were passed + assert.Equal(t, "pktvisord", name, "Expected command name to be pktvisord") + assert.Contains(t, args, "--admin-api", "Expected args to contain admin-api") + assert.False(t, options.Buffered, "Expected buffered to be false") + assert.True(t, options.Streaming, "Expected streaming to be true") + return mockCmd + } + + assert.True(t, pktvisor.Register(), "Failed to register Pktvisor backend") + + assert.True(t, backend.HaveBackend("pktvisor"), "Failed to get Pktvisor backend") + + be := backend.GetBackend("pktvisor") + + assert.Equal(t, backend.Unknown, be.GetInitialState()) + + // Configure backend + err = be.Configure(logger, repo, map[string]any{ + "host": serverURL.Hostname(), + "port": serverURL.Port(), + }, config.BackendCommons{}) + assert.NoError(t, err) + + // Start the backend + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + // Assert successful start + assert.NoError(t, err) + + // Get Running status + status, _, err := be.GetRunningStatus() + assert.NoError(t, err) + assert.Equal(t, backend.Running, status, "Expected backend to be running") + + // Get capabilities + capabilities, err := be.GetCapabilities() + assert.NoError(t, err) + assert.Equal(t, map[string]any{"iface": "eth0"}, capabilities["taps"], "Expected taps") + + data := policies.PolicyData{ + ID: "dummy-policy-id", + Name: "dummy-policy-name", + Data: map[string]any{"key": "value"}, + } + // Apply policy + err = be.ApplyPolicy(data, false) + assert.NoError(t, err) + + // Update policy + err = be.ApplyPolicy(data, true) + assert.NoError(t, err) + + // Assert restart + err = be.FullReset(ctx) + assert.NoError(t, err) + + // Verify expectations + mockCmd.AssertExpectations(t) +} + +func TestPktvisorBackendCompleted(t *testing.T) { + // Create a mock command that simulates a failure + mockCmd := &mocks.MockCmd{} + mocks.SetupCompletedProcess(mockCmd, 0, nil) + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(_ backend.CmdOptions, _ string, _ ...string) backend.Commander { + return mockCmd + } + + assert.True(t, pktvisor.Register(), "Failed to register Pktvisor backend") + + assert.True(t, backend.HaveBackend("pktvisor"), "Failed to get Pktvisor backend") + + be := backend.GetBackend("pktvisor") + + // Configure backend with invalid parameters + err := be.Configure(slog.Default(), nil, map[string]any{ + "host": "invalid-host", + }, config.BackendCommons{}) + assert.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + assert.Error(t, err) +} diff --git a/agent/backend/pktvisor/policy.go b/agent/backend/pktvisor/policy.go deleted file mode 100644 index 58c203bf..00000000 --- a/agent/backend/pktvisor/policy.go +++ /dev/null @@ -1,64 +0,0 @@ -package pktvisor - -import ( - "bytes" - "fmt" - "net/http" - - "go.uber.org/zap" - "gopkg.in/yaml.v3" - - "github.com/netboxlabs/orb-agent/agent/policies" -) - -func (p *pktvisorBackend) ApplyPolicy(data policies.PolicyData, updatePolicy bool) error { - if updatePolicy { - // To update a policy it's necessary first remove it and then apply a new version - if err := p.RemovePolicy(data); err != nil { - p.logger.Warn("policy failed to remove", zap.String("policy_id", data.ID), zap.String("policy_name", data.Name), zap.Error(err)) - } - } - - p.logger.Debug("pktvisor policy apply", zap.String("policy_id", data.ID), zap.Any("data", data.Data)) - - fullPolicy := map[string]interface{}{ - "version": "1.0", - "visor": map[string]interface{}{ - "policies": map[string]interface{}{ - data.Name: data.Data, - }, - }, - } - - policyYaml, err := yaml.Marshal(fullPolicy) - if err != nil { - p.logger.Warn("yaml policy marshal failure", zap.String("policy_id", data.ID), zap.Any("policy", fullPolicy)) - return err - } - - var resp map[string]interface{} - err = p.request("policies", &resp, http.MethodPost, bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout) - if err != nil { - p.logger.Warn("yaml policy application failure", zap.String("policy_id", data.ID), zap.ByteString("policy", policyYaml)) - return err - } - - return nil -} - -func (p *pktvisorBackend) RemovePolicy(data policies.PolicyData) error { - p.logger.Debug("pktvisor policy remove", zap.String("policy_id", data.ID)) - var resp interface{} - var name string - // Since we use Name for removing policies not IDs, if there is a change, we need to remove the previous name of the policy - if data.PreviousPolicyData != nil && data.PreviousPolicyData.Name != data.Name { - name = data.PreviousPolicyData.Name - } else { - name = data.Name - } - err := p.request(fmt.Sprintf("policies/%s", name), &resp, http.MethodDelete, http.NoBody, "application/json", removePolicyTimeout) - if err != nil { - return err - } - return nil -} diff --git a/agent/backend/pktvisor/utils.go b/agent/backend/pktvisor/utils.go deleted file mode 100644 index fd62633e..00000000 --- a/agent/backend/pktvisor/utils.go +++ /dev/null @@ -1,103 +0,0 @@ -package pktvisor - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "go.uber.org/zap" - - "github.com/netboxlabs/orb-agent/agent/backend" -) - -// note this needs to be stateless because it is called for multiple go routines -func (p *pktvisorBackend) request(url string, payload interface{}, method string, body io.Reader, contentType string, timeout int32) error { - client := http.Client{ - Timeout: time.Second * time.Duration(timeout), - } - - status, _, err := p.getProcRunningStatus() - if status != backend.Running { - p.logger.Warn("skipping pktvisor REST API request because process is not running or is unresponsive", zap.String("url", url), zap.String("method", method), zap.Error(err)) - return err - } - - URL := fmt.Sprintf("%s://%s:%s/api/v1/%s", p.adminAPIProtocol, p.adminAPIHost, p.adminAPIPort, url) - - req, err := http.NewRequest(method, URL, body) - if err != nil { - p.logger.Error("received error from payload", zap.Error(err)) - return err - } - - req.Header.Add("Content-Type", contentType) - res, getErr := client.Do(req) - - if getErr != nil { - p.logger.Error("received error from payload", zap.Error(getErr)) - return getErr - } - - defer func() { - if err := res.Body.Close(); err != nil { - p.logger.Error("failed to close response body", zap.Error(err)) - } - }() - - if (res.StatusCode < 200) || (res.StatusCode > 299) { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("non 2xx HTTP error code from pktvisord, no or invalid body: %d", res.StatusCode) - } - if len(body) == 0 { - return fmt.Errorf("%d empty body", res.StatusCode) - } else if body[0] == '{' { - var jsonBody map[string]interface{} - err := json.Unmarshal(body, &jsonBody) - if err == nil { - if errMsg, ok := jsonBody["error"]; ok { - return fmt.Errorf("%d %s", res.StatusCode, errMsg) - } - } - } - } - - if res.Body != nil { - err = json.NewDecoder(res.Body).Decode(&payload) - if err != nil { - return err - } - } - return nil -} - -func (p *pktvisorBackend) getProcRunningStatus() (backend.RunningStatus, string, error) { - if p.proc == nil { - return backend.Unknown, "backend not started yet", nil - } - status := p.proc.Status() - - if status.Error != nil { - errMsg := fmt.Sprintf("pktvisor process error: %v", status.Error) - return backend.BackendError, errMsg, status.Error - } - - if status.Complete { - err := p.proc.Stop() - return backend.Offline, "pktvisor process ended", err - } - - if status.StopTs > 0 { - return backend.Offline, "pktvisor process ended", nil - } - return backend.Running, "", nil -} - -// also used for HTTP REST API readiness check -func (p *pktvisorBackend) getAppInfo() (AppInfo, error) { - var appInfo AppInfo - err := p.request("metrics/app", &appInfo, http.MethodGet, http.NoBody, "application/json", versionTimeout) - return appInfo, err -} diff --git a/agent/backend/pktvisor/vars.go b/agent/backend/pktvisor/vars.go deleted file mode 100644 index 5616c368..00000000 --- a/agent/backend/pktvisor/vars.go +++ /dev/null @@ -1,12 +0,0 @@ -package pktvisor - -import ( - "github.com/spf13/viper" -) - -// RegisterBackendSpecificVariables registers the backend specific variables for the pktvisor backend -func RegisterBackendSpecificVariables(v *viper.Viper) { - v.SetDefault("orb.backends.pktvisor.binary", "/usr/local/bin/pktvisord") - v.SetDefault("orb.backends.pktvisor.host", "localhost") - v.SetDefault("orb.backends.pktvisor.port", "10853") -} diff --git a/agent/backend/utils.go b/agent/backend/utils.go new file mode 100644 index 00000000..3a4e7591 --- /dev/null +++ b/agent/backend/utils.go @@ -0,0 +1,107 @@ +package backend + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "time" + + "gopkg.in/yaml.v3" +) + +// GetRunningStatus checks the status of the backend process +func GetRunningStatus(proc Commander) (RunningStatus, string, error) { + if proc == nil { + return Unknown, "backend not started yet", nil + } + status := proc.Status() + + if status.Error != nil { + errMsg := fmt.Sprintf("process error: %v", status.Error) + return BackendError, errMsg, status.Error + } + + if status.Complete { + err := proc.Stop() + return Offline, "backend process ended", err + } + + if status.StopTs > 0 { + return Offline, "backend process ended", nil + } + return Running, "", nil +} + +// CommonRequest is a generic function to make HTTP requests to the backend +func CommonRequest(backendName string, proc Commander, logger *slog.Logger, url string, payload any, + method string, body io.Reader, contentType string, timeout int32, errorMsg string, +) error { + client := http.Client{ + Timeout: time.Second * time.Duration(timeout), + } + + status, _, err := GetRunningStatus(proc) + if status != Running { + logger.Warn("skipping REST API request because process is not running or is unresponsive", + slog.String("backend", backendName), slog.String("url", url), slog.String("method", method), slog.Any("error", err)) + return err + } + + req, err := http.NewRequest(method, url, body) + if err != nil { + return fmt.Errorf("received error from payload %v", slog.Any("error", err)) + } + + req.Header.Add("Content-Type", contentType) + res, getErr := client.Do(req) + + if getErr != nil { + return fmt.Errorf("received error from payload %v", slog.Any("error", err)) + } + + defer func() { + if err := res.Body.Close(); err != nil { + logger.Error("failed to close response body", slog.Any("error", err)) + } + }() + + if (res.StatusCode < 200) || (res.StatusCode > 299) { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("non 2xx HTTP error code from %s, no or invalid body: %d", backendName, res.StatusCode) + } + if len(body) == 0 { + return fmt.Errorf("%d empty body", res.StatusCode) + } else if body[0] == '{' { + var jsonBody map[string]any + if err := json.Unmarshal(body, &jsonBody); err == nil { + if errMsg, ok := jsonBody[errorMsg]; ok { + return fmt.Errorf("%d %s", res.StatusCode, errMsg) + } + } else { + return fmt.Errorf("%d %s", res.StatusCode, body) + } + } else { + return fmt.Errorf("%d %s", res.StatusCode, body) + } + } + + // Read and decode response body + if res.Body != nil { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + if err = json.Unmarshal(body, &payload); err == nil { + return nil + } + var yamlErr error + if yamlErr = yaml.Unmarshal(body, &payload); yamlErr == nil { + return nil + } + return fmt.Errorf("failed to decode response as JSON: %w and YAML: %w", err, yamlErr) + } + return nil +} diff --git a/agent/backend/worker/utils.go b/agent/backend/worker/utils.go deleted file mode 100644 index 830e654e..00000000 --- a/agent/backend/worker/utils.go +++ /dev/null @@ -1,96 +0,0 @@ -package worker - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "go.uber.org/zap" - - "github.com/netboxlabs/orb-agent/agent/backend" -) - -func (d *workerBackend) getProcRunningStatus() (backend.RunningStatus, string, error) { - if d.proc == nil { - return backend.Unknown, "backend not started yet", nil - } - status := d.proc.Status() - - if status.Error != nil { - errMsg := fmt.Sprintf("worker process error: %v", status.Error) - return backend.BackendError, errMsg, status.Error - } - - if status.Complete { - err := d.proc.Stop() - return backend.Offline, "worker process ended", err - } - - if status.StopTs > 0 { - return backend.Offline, "worker process ended", nil - } - return backend.Running, "", nil -} - -// note this needs to be stateless because it is called for multiple go routines -func (d *workerBackend) request(url string, payload interface{}, method string, body io.Reader, contentType string, timeout int32) error { - client := http.Client{ - Timeout: time.Second * time.Duration(timeout), - } - - status, _, err := d.getProcRunningStatus() - if status != backend.Running { - d.logger.Warn("skipping device discovery REST API request because process is not running or is unresponsive", zap.String("url", url), zap.String("method", method), zap.Error(err)) - return err - } - - URL := fmt.Sprintf("%s://%s:%s/api/v1/%s", d.apiProtocol, d.apiHost, d.apiPort, url) - - req, err := http.NewRequest(method, URL, body) - if err != nil { - d.logger.Error("received error from payload", zap.Error(err)) - return err - } - - req.Header.Add("Content-Type", contentType) - res, getErr := client.Do(req) - - if getErr != nil { - d.logger.Error("received error from payload", zap.Error(getErr)) - return getErr - } - - defer func() { - if err := res.Body.Close(); err != nil { - d.logger.Error("failed to close response body", zap.Error(err)) - } - }() - - if (res.StatusCode < 200) || (res.StatusCode > 299) { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("non 2xx HTTP error code from worker, no or invalid body: %d", res.StatusCode) - } - if len(body) == 0 { - return fmt.Errorf("%d empty body", res.StatusCode) - } else if body[0] == '{' { - var jsonBody map[string]interface{} - err := json.Unmarshal(body, &jsonBody) - if err == nil { - if errMsg, ok := jsonBody["detail"]; ok { - return fmt.Errorf("%d %s", res.StatusCode, errMsg) - } - } - } - } - - if res.Body != nil { - err = json.NewDecoder(res.Body).Decode(&payload) - if err != nil { - return err - } - } - return nil -} diff --git a/agent/backend/worker/vars.go b/agent/backend/worker/vars.go deleted file mode 100644 index 278f732e..00000000 --- a/agent/backend/worker/vars.go +++ /dev/null @@ -1,11 +0,0 @@ -package worker - -import ( - "github.com/spf13/viper" -) - -// RegisterBackendSpecificVariables registers the backend specific variables for the worker backend -func RegisterBackendSpecificVariables(v *viper.Viper) { - v.SetDefault("orb.backends.worker.host", defaultAPIHost) - v.SetDefault("orb.backends.worker.port", defaultAPIPort) -} diff --git a/agent/backend/worker/worker.go b/agent/backend/worker/worker.go index 47e68ba0..e8bee41c 100644 --- a/agent/backend/worker/worker.go +++ b/agent/backend/worker/worker.go @@ -5,11 +5,10 @@ import ( "context" "errors" "fmt" + "log/slog" "net/http" "time" - "github.com/go-cmd/cmd" - "go.uber.org/zap" "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" @@ -32,7 +31,7 @@ const ( ) type workerBackend struct { - logger *zap.Logger + logger *slog.Logger policyRepo policies.PolicyRepo exec string @@ -41,12 +40,13 @@ type workerBackend struct { apiProtocol string diodeTarget string - diodeAPIKey string + diodeClientID string + diodeClientSecret string diodeAppNamePrefix string startTime time.Time - proc *cmd.Cmd - statusChan <-chan cmd.Status + proc backend.Commander + statusChan <-chan backend.CmdStatus cancelFunc context.CancelFunc ctx context.Context } @@ -65,7 +65,9 @@ func Register() bool { return true } -func (d *workerBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo, config map[string]interface{}, common config.BackendCommons) error { +func (d *workerBackend) Configure(logger *slog.Logger, repo policies.PolicyRepo, + config map[string]any, common config.BackendCommons, +) error { d.logger = logger d.policyRepo = repo @@ -78,7 +80,8 @@ func (d *workerBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo, } d.diodeTarget = common.Diode.Target - d.diodeAPIKey = common.Diode.APIKey + d.diodeClientID = common.Diode.ClientID + d.diodeClientSecret = common.Diode.ClientSecret d.diodeAppNamePrefix = common.Diode.AgentName return nil @@ -86,7 +89,9 @@ func (d *workerBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo, func (d *workerBackend) Version() (string, error) { var info info - err := d.request("status", &info, http.MethodGet, http.NoBody, "application/json", versionTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/status", d.apiProtocol, d.apiHost, d.apiPort) + err := backend.CommonRequest("worker", d.proc, d.logger, url, &info, http.MethodGet, + http.NoBody, "application/json", versionTimeout, "detail") if err != nil { return "", err } @@ -102,15 +107,16 @@ func (d *workerBackend) Start(ctx context.Context, cancelFunc context.CancelFunc "--host", d.apiHost, "--port", d.apiPort, "--diode-target", d.diodeTarget, - "--diode-api-key", "********", + "--diode-client-id", d.diodeClientID, + "--diode-client-secret", "********", "--diode-app-name-prefix", d.diodeAppNamePrefix, } - d.logger.Info("worker startup", zap.Strings("arguments", pvOptions)) + d.logger.Info("worker startup", slog.Any("arguments", pvOptions)) - pvOptions[7] = d.diodeAPIKey + pvOptions[9] = d.diodeClientSecret - d.proc = cmd.NewCmdOptions(cmd.Options{ + d.proc = backend.NewCmdOptions(backend.CmdOptions{ Buffered: false, Streaming: true, }, d.exec, pvOptions...) @@ -124,20 +130,22 @@ func (d *workerBackend) Start(ctx context.Context, cancelFunc context.CancelFunc close(doneChan) } }() - for d.proc.Stdout != nil || d.proc.Stderr != nil { + stdout := d.proc.GetStdout() + stderr := d.proc.GetStderr() + for stdout != nil || stderr != nil { select { - case line, open := <-d.proc.Stdout: + case line, open := <-stderr: if !open { - d.proc.Stdout = nil + stdout = nil continue } - d.logger.Info("worker stdout", zap.String("log", line)) - case line, open := <-d.proc.Stderr: + d.logger.Info("worker stdout", slog.String("log", line)) + case line, open := <-stderr: if !open { - d.proc.Stderr = nil + stderr = nil continue } - d.logger.Info("worker stderr", zap.String("log", line)) + d.logger.Info("worker stderr", slog.String("log", line)) } } }() @@ -148,37 +156,40 @@ func (d *workerBackend) Start(ctx context.Context, cancelFunc context.CancelFunc status := d.proc.Status() if status.Error != nil { - d.logger.Error("worker startup error", zap.Error(status.Error)) + d.logger.Error("worker startup error", slog.Any("error", status.Error)) return status.Error } if status.Complete { err := d.proc.Stop() if err != nil { - d.logger.Error("proc.Stop error", zap.Error(err)) + d.logger.Error("proc.Stop error", slog.Any("error", err)) } return errors.New("worker startup error, check log") } - d.logger.Info("worker process started", zap.Int("pid", status.PID)) + d.logger.Info("worker process started", slog.Int("pid", status.PID)) + var version string var readinessErr error - for backoff := 0; backoff < readinessBackoff; backoff++ { - version, readinessErr := d.Version() + for backoff := range readinessBackoff { + version, readinessErr = d.Version() if readinessErr == nil { - d.logger.Info("worker readiness ok, got version ", zap.String("worker_version", version)) + d.logger.Info("worker readiness ok, got version ", + slog.String("worker_version", version)) break } backoffDuration := time.Duration(backoff) * time.Second - d.logger.Info("worker is not ready, trying again with backoff", zap.String("backoff backoffDuration", backoffDuration.String())) + d.logger.Info("worker is not ready, trying again with backoff", + slog.String("backoff backoffDuration", backoffDuration.String())) time.Sleep(backoffDuration) } if readinessErr != nil { - d.logger.Error("worker error on readiness", zap.Error(readinessErr)) + d.logger.Error("worker error on readiness", slog.Any("error", readinessErr)) err := d.proc.Stop() if err != nil { - d.logger.Error("proc.Stop error", zap.Error(err)) + d.logger.Error("proc.Stop error", slog.Any("error", err)) } return readinessErr } @@ -187,22 +198,23 @@ func (d *workerBackend) Start(ctx context.Context, cancelFunc context.CancelFunc } func (d *workerBackend) Stop(ctx context.Context) error { - d.logger.Info("routine call to stop worker", zap.Any("routine", ctx.Value(config.ContextKey("routine")))) + d.logger.Info("routine call to stop worker", slog.Any("routine", ctx.Value(config.ContextKey("routine")))) defer d.cancelFunc() err := d.proc.Stop() finalStatus := <-d.statusChan if err != nil { - d.logger.Error("worker shutdown error", zap.Error(err)) + d.logger.Error("worker shutdown error", slog.Any("error", err)) } - d.logger.Info("worker process stopped", zap.Int("pid", finalStatus.PID), zap.Int("exit_code", finalStatus.Exit)) + d.logger.Info("worker process stopped", slog.Int("pid", finalStatus.PID), + slog.Int("exit_code", finalStatus.Exit)) return nil } func (d *workerBackend) FullReset(ctx context.Context) error { // force a stop, which stops scrape as well. if proc is dead, it no ops. - if state, _, _ := d.getProcRunningStatus(); state == backend.Running { + if state, _, _ := backend.GetRunningStatus(d.proc); state == backend.Running { if err := d.Stop(ctx); err != nil { - d.logger.Error("failed to stop backend on restart procedure", zap.Error(err)) + d.logger.Error("failed to stop backend on restart procedure", slog.Any("error", err)) return err } } @@ -210,7 +222,7 @@ func (d *workerBackend) FullReset(ctx context.Context) error { backendCtx, cancelFunc := context.WithCancel(context.WithValue(ctx, config.ContextKey("routine"), "worker")) // start it if err := d.Start(backendCtx, cancelFunc); err != nil { - d.logger.Error("failed to start backend on restart procedure", zap.Error(err)) + d.logger.Error("failed to start backend on restart procedure", slog.Any("error", err)) return err } return nil @@ -220,9 +232,11 @@ func (d *workerBackend) GetStartTime() time.Time { return d.startTime } -func (d *workerBackend) GetCapabilities() (map[string]interface{}, error) { - caps := make(map[string]interface{}) - err := d.request("capabilities", &caps, http.MethodGet, http.NoBody, "application/json", capabilitiesTimeout) +func (d *workerBackend) GetCapabilities() (map[string]any, error) { + caps := make(map[string]any) + url := fmt.Sprintf("%s://%s:%s/api/v1/capabilities", d.apiProtocol, d.apiHost, d.apiPort) + err := backend.CommonRequest("worker", d.proc, d.logger, url, &caps, http.MethodGet, + http.NoBody, "application/json", capabilitiesTimeout, "detail") if err != nil { return nil, err } @@ -231,7 +245,7 @@ func (d *workerBackend) GetCapabilities() (map[string]interface{}, error) { func (d *workerBackend) GetRunningStatus() (backend.RunningStatus, string, error) { // first check process status - runningStatus, errMsg, err := d.getProcRunningStatus() + runningStatus, errMsg, err := backend.GetRunningStatus(d.proc) // if it's not running, we're done if runningStatus != backend.Running { return runningStatus, errMsg, err @@ -252,28 +266,31 @@ func (d *workerBackend) ApplyPolicy(data policies.PolicyData, updatePolicy bool) if updatePolicy { // To update a policy it's necessary first remove it and then apply a new version if err := d.RemovePolicy(data); err != nil { - d.logger.Warn("policy failed to remove", zap.String("policy_id", data.ID), zap.String("policy_name", data.Name), zap.Error(err)) + d.logger.Warn("policy failed to remove", slog.String("policy_id", data.ID), + slog.String("policy_name", data.Name), slog.Any("error", err)) } } - d.logger.Debug("worker policy apply", zap.String("policy_id", data.ID), zap.Any("data", data.Data)) + d.logger.Debug("worker policy apply", slog.String("policy_id", data.ID), slog.Any("data", data.Data)) - fullPolicy := map[string]interface{}{ - "policies": map[string]interface{}{ + fullPolicy := map[string]any{ + "policies": map[string]any{ data.Name: data.Data, }, } policyYaml, err := yaml.Marshal(fullPolicy) if err != nil { - d.logger.Warn("policy yaml marshal failure", zap.String("policy_id", data.ID), zap.Any("policy", fullPolicy)) + d.logger.Warn("policy yaml marshal failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) return err } - var resp map[string]interface{} - err = d.request("policies", &resp, http.MethodPost, bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout) + var resp map[string]any + url := fmt.Sprintf("%s://%s:%s/api/v1/policies", d.apiProtocol, d.apiHost, d.apiPort) + err = backend.CommonRequest("worker", d.proc, d.logger, url, &resp, http.MethodPost, + bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout, "detail") if err != nil { - d.logger.Warn("policy application failure", zap.String("policy_id", data.ID), zap.ByteString("policy", policyYaml)) + d.logger.Warn("policy application failure", slog.String("policy_id", data.ID), slog.String("policy_name", data.Name)) return err } @@ -281,8 +298,8 @@ func (d *workerBackend) ApplyPolicy(data policies.PolicyData, updatePolicy bool) } func (d *workerBackend) RemovePolicy(data policies.PolicyData) error { - d.logger.Debug("worker policy remove", zap.String("policy_id", data.ID)) - var resp interface{} + d.logger.Debug("worker policy remove", slog.String("policy_id", data.ID)) + var resp any var name string // Since we use Name for removing policies not IDs, if there is a change, we need to remove the previous name of the policy if data.PreviousPolicyData != nil && data.PreviousPolicyData.Name != data.Name { @@ -290,7 +307,9 @@ func (d *workerBackend) RemovePolicy(data policies.PolicyData) error { } else { name = data.Name } - err := d.request(fmt.Sprintf("policies/%s", name), &resp, http.MethodDelete, http.NoBody, "application/json", removePolicyTimeout) + url := fmt.Sprintf("%s://%s:%s/api/v1/policies/%s", d.apiProtocol, d.apiHost, d.apiPort, name) + err := backend.CommonRequest("worker", d.proc, d.logger, url, &resp, http.MethodDelete, + http.NoBody, "application/json", removePolicyTimeout, "detail") if err != nil { return err } diff --git a/agent/backend/worker/worker_test.go b/agent/backend/worker/worker_test.go new file mode 100644 index 00000000..04a2d50d --- /dev/null +++ b/agent/backend/worker/worker_test.go @@ -0,0 +1,202 @@ +package worker_test + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/backend/mocks" + "github.com/netboxlabs/orb-agent/agent/backend/worker" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/policies" +) + +type StatusResponse struct { + StartTime string `json:"start_time"` + Version string `json:"version"` + UpTime float64 `json:"up_time"` +} + +func TestWorkerBackendStart(t *testing.T) { + // Create server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.URL.Path == "/api/v1/status" { + response := StatusResponse{ + Version: "1.3.4", + StartTime: "2023-10-01T12:00:00Z", + UpTime: 123.456, + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.URL.Path == "/api/v1/capabilities" { + capabilities := map[string]any{ + "capability": true, + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(capabilities) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if strings.Contains(r.URL.Path, "/api/v1/policies") { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy applied successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } else if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusOK) + response := map[string]any{ + "status": "success", + "message": "Policy removed successfully", + } + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + } else { + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + // Parse server URL + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + // Create logger + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // Create a mock repository + repo, err := policies.NewMemRepo() + assert.NoError(t, err) + + // Create a mock command + mockCmd := &mocks.MockCmd{} + mocks.SetupSuccessfulProcess(mockCmd, 12345) + + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(options backend.CmdOptions, name string, args ...string) backend.Commander { + // Assert that the correct parameters were passed + assert.Equal(t, "orb-worker", name, "Expected command name to be orb-worker") + assert.Contains(t, args, "--port", "Expected args to contain port") + assert.Contains(t, args, "--host", "Expected args to contain host") + assert.False(t, options.Buffered, "Expected buffered to be false") + assert.True(t, options.Streaming, "Expected streaming to be true") + return mockCmd + } + + assert.True(t, worker.Register(), "Failed to register Worker backend") + + assert.True(t, backend.HaveBackend("worker"), "Failed to get Worker backend") + + be := backend.GetBackend("worker") + + // Configure backend + err = be.Configure(logger, repo, map[string]any{ + "host": serverURL.Hostname(), + "port": serverURL.Port(), + }, config.BackendCommons{}) + assert.NoError(t, err) + + assert.Equal(t, backend.Unknown, be.GetInitialState()) + + // Start the backend + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + // Assert successful start + assert.NoError(t, err) + + // Get Running status + status, _, err := be.GetRunningStatus() + assert.NoError(t, err) + assert.Equal(t, backend.Running, status, "Expected backend to be running") + + // Get capabilities + capabilities, err := be.GetCapabilities() + assert.NoError(t, err) + assert.Equal(t, capabilities["capability"], true, "Expected capability to be true") + + data := policies.PolicyData{ + ID: "dummy-policy-id", + Name: "dummy-policy-name", + Data: map[string]any{"key": "value"}, + } + // Apply policy + err = be.ApplyPolicy(data, false) + assert.NoError(t, err) + + // Update policy + err = be.ApplyPolicy(data, true) + assert.NoError(t, err) + + // Assert restart + err = be.FullReset(ctx) + assert.NoError(t, err) + + // Verify expectations + mockCmd.AssertExpectations(t) +} + +func TestWorkerBackendCompleted(t *testing.T) { + // Create a mock command that simulates a failure + mockCmd := &mocks.MockCmd{} + mocks.SetupCompletedProcess(mockCmd, 0, nil) + // Save original function and restore after test + originalNewCmdOptions := backend.NewCmdOptions + defer func() { + backend.NewCmdOptions = originalNewCmdOptions + }() + + // Override NewCmdOptions to return our mock + backend.NewCmdOptions = func(_ backend.CmdOptions, _ string, _ ...string) backend.Commander { + return mockCmd + } + + assert.True(t, worker.Register(), "Failed to register Worker backend") + + assert.True(t, backend.HaveBackend("worker"), "Failed to get Worker backend") + + be := backend.GetBackend("worker") + + // Configure backend with invalid parameters + err := be.Configure(slog.Default(), nil, map[string]any{ + "host": "invalid-host", + }, config.BackendCommons{}) + assert.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + err = be.Start(ctx, cancel) + + assert.Error(t, err) +} diff --git a/agent/config/config_test.go b/agent/config/config_test.go new file mode 100644 index 00000000..8089ede3 --- /dev/null +++ b/agent/config/config_test.go @@ -0,0 +1,164 @@ +package config_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netboxlabs/orb-agent/agent/config" +) + +func TestResolveEnv(t *testing.T) { + err := os.Setenv("TEST_VAR", "test_value") + assert.NoError(t, err, "failed to set environment variable") + defer func() { + err := os.Unsetenv("TEST_VAR") + assert.NoError(t, err, "failed to unset environment variable") + }() + + tests := []struct { + input string + expected string + hasError bool + }{ + {"${TEST_VAR}", "test_value", false}, + {"${UNSET_VAR}", "", true}, + {"no_env_var", "no_env_var", false}, + } + + for _, test := range tests { + result, err := config.ResolveEnv(test.input) + if test.hasError { + assert.Error(t, err, "expected error for input %s", test.input) + } else { + assert.NoError(t, err, "unexpected error for input %s", test.input) + } + assert.Equal(t, test.expected, result, "unexpected result for input %s", test.input) + } +} + +func TestResolveEnvError(t *testing.T) { + err := os.Unsetenv("UNSET_VAR") // Ensure the variable is not set + assert.NoError(t, err, "failed to unset environment variable") + + _, err = config.ResolveEnv("${UNSET_VAR}") + assert.Error(t, err, "expected error for unset environment variable") +} + +func TestResolveEnvInMap(t *testing.T) { + err := os.Setenv("TEST_VAR", "test_value") + assert.NoError(t, err, "failed to set environment variable") + defer func() { + err := os.Unsetenv("TEST_VAR") + assert.NoError(t, err, "failed to unset environment variable") + }() + + data := map[string]any{ + "key1": "${TEST_VAR}", + "key2": "static_value", + "nested": map[string]any{ + "key3": "${TEST_VAR}", + }, + "list": []any{ + "${TEST_VAR}", + }, + } + + err = config.ResolveEnvInMap(data) + assert.NoError(t, err, "unexpected error") + + assert.Equal(t, "test_value", data["key1"], "unexpected value for key1") + assert.Equal(t, "static_value", data["key2"], "unexpected value for key2") + + nested, ok := data["nested"].(map[string]any) + assert.True(t, ok, "expected nested to be a map") + assert.Equal(t, "test_value", nested["key3"], "unexpected value for nested.key3") +} + +func TestResolveEnvInMapError(t *testing.T) { + err := os.Unsetenv("UNSET_VAR") // Ensure the variable is not set + assert.NoError(t, err, "failed to unset environment variable") + + data := map[string]any{ + "key1": "${UNSET_VAR}", + } + + err = config.ResolveEnvInMap(data) + assert.Error(t, err, "expected error for unset environment variable in map") + + // Test with a nested map + data = map[string]any{ + "key1": map[string]any{ + "nested_key": "${UNSET_VAR}", + }, + } + err = config.ResolveEnvInMap(data) + assert.Error(t, err, "expected error for unset environment variable in nested map") + + // Test with a nested slice + data = map[string]any{ + "key1": []any{"${UNSET_VAR}"}, + } + err = config.ResolveEnvInMap(data) + assert.Error(t, err, "expected error for unset environment variable in nested slice") +} + +func TestResolveEnvInSlice(t *testing.T) { + err := os.Setenv("TEST_VAR", "test_value") + assert.NoError(t, err, "failed to set environment variable") + defer func() { + err := os.Unsetenv("TEST_VAR") + assert.NoError(t, err, "failed to unset environment variable") + }() + + data := []any{ + "${TEST_VAR}", + "static_value", + map[string]any{ + "key1": "${TEST_VAR}", + }, + []any{"${TEST_VAR}"}, + } + + err = config.ResolveEnvInSlice(data) + assert.NoError(t, err, "unexpected error") + + assert.Equal(t, "test_value", data[0], "unexpected value at index 0") + assert.Equal(t, "static_value", data[1], "unexpected value at index 1") + + nestedMap, ok := data[2].(map[string]any) + assert.True(t, ok, "expected index 2 to be a map") + assert.Equal(t, "test_value", nestedMap["key1"], "unexpected value for nestedMap.key1") + + nestedSlice, ok := data[3].([]any) + assert.True(t, ok, "expected index 3 to be a slice") + assert.Equal(t, "test_value", nestedSlice[0], "unexpected value for nestedSlice[0]") +} + +func TestResolveEnvInSliceError(t *testing.T) { + err := os.Unsetenv("UNSET_VAR") // Ensure the variable is not set + assert.NoError(t, err, "failed to unset environment variable") + + data := []any{ + "${UNSET_VAR}", + } + + err = config.ResolveEnvInSlice(data) + assert.Error(t, err, "expected error for unset environment variable in slice") + + // Test with a nested map + data = []any{ + map[string]any{ + "key1": "${UNSET_VAR}", + }, + } + err = config.ResolveEnvInSlice(data) + assert.Error(t, err, "expected error for unset environment variable in nested map") + // Test with a nested slice + data = []any{ + []any{"${UNSET_VAR}"}, + } + err = config.ResolveEnvInSlice(data) + assert.Error(t, err, "expected error for unset environment variable in nested slice") +} diff --git a/agent/config/env.go b/agent/config/env.go new file mode 100644 index 00000000..1da897a5 --- /dev/null +++ b/agent/config/env.go @@ -0,0 +1,70 @@ +package config + +import ( + "errors" + "os" + "strings" +) + +// ResolveEnv replaces environment variable placeholders in the format ${VAR} with their actual values. +func ResolveEnv(value string) (string, error) { + // Check if the value starts with "${" and ends with "}" + if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") { + // Extract the environment variable name + envVar := value[2 : len(value)-1] + // Get the value of the environment variable + envValue := os.Getenv(envVar) + if envValue != "" { + return envValue, nil + } + return "", errors.New("a provided environment variable is not set") + } + // Return the original value if no substitution occurs + return value, nil +} + +// ResolveEnvInMap recursively traverses and resolves env vars in map[string]any structures. +func ResolveEnvInMap(data map[string]any) error { + for key, val := range data { + switch v := val.(type) { + case string: + resolved, err := ResolveEnv(v) + if err != nil { + return err + } + data[key] = resolved + case map[string]any: + if err := ResolveEnvInMap(v); err != nil { + return err + } + case []any: + if err := ResolveEnvInSlice(v); err != nil { + return err + } + } + } + return nil +} + +// ResolveEnvInSlice handles []any elements recursively. +func ResolveEnvInSlice(slice []any) error { + for i, val := range slice { + switch v := val.(type) { + case string: + resolved, err := ResolveEnv(v) + if err != nil { + return err + } + slice[i] = resolved + case map[string]any: + if err := ResolveEnvInMap(v); err != nil { + return err + } + case []any: + if err := ResolveEnvInSlice(v); err != nil { + return err + } + } + } + return nil +} diff --git a/agent/config/types.go b/agent/config/types.go index db52d57c..11e1640b 100644 --- a/agent/config/types.go +++ b/agent/config/types.go @@ -5,106 +5,96 @@ type ContextKey string // PolicyPayload represents the payload for the agent policy type PolicyPayload struct { - Action string `json:"action"` - ID string `json:"id"` - DatasetID string `json:"dataset_id"` - AgentGroupID string `json:"agent_group_id"` - Name string `json:"name"` - Backend string `json:"backend"` - Format string `json:"format"` - Version int32 `json:"version"` - Data interface{} `json:"data"` -} - -// APIConfig represents the configuration for the API connection -type APIConfig struct { - Address string `mapstructure:"address"` - Token string `mapstructure:"token"` -} - -// MQTTConfig represents the configuration for the MQTT connection -type MQTTConfig struct { - Connect bool `mapstructure:"connect"` - Address string `mapstructure:"address"` - ID string `mapstructure:"id"` - Key string `mapstructure:"key"` - ChannelID string `mapstructure:"channel_id"` -} - -// CloudManager represents the cloud ConfigManager configuration -type CloudManager struct { - Config struct { - AgentName string `mapstructure:"agent_name"` - AutoProvision bool `mapstructure:"auto_provision"` - } `mapstructure:"config"` - API APIConfig `mapstructure:"api"` - MQTT MQTTConfig `mapstructure:"mqtt"` - TLS struct { - Verify bool `mapstructure:"verify"` - } `mapstructure:"tls"` - DB struct { - File string `mapstructure:"file"` - } `mapstructure:"db"` - Labels map[string]string `mapstructure:"labels"` + Action string `json:"action"` + ID string `json:"id"` + DatasetID string `json:"dataset_id"` + AgentGroupID string `json:"agent_group_id"` + Name string `json:"name"` + Backend string `json:"backend"` + Format string `json:"format"` + Version int32 `json:"version"` + Data any `json:"data"` } // LocalManager represents the local ConfigManager configuration. type LocalManager struct { - Config string `mapstructure:"config"` + Config string `yaml:"config"` } // GitManager represents the Git ConfigManager configuration. type GitManager struct { - URL string `mapstructure:"url"` - Branch string `mapstructure:"branch"` - Auth string `mapstructure:"auth"` - Schedule *string `mapstructure:"schedule, omitempty"` - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - PrivateKey string `mapstructure:"private_key"` + URL string `yaml:"url"` + Branch string `yaml:"branch"` + Auth string `yaml:"auth"` + Schedule *string `yaml:"schedule,omitempty"` + Username string `yaml:"username"` + Password string `yaml:"password"` + PrivateKey string `yaml:"private_key"` } -// ManagerSources represents the configuration for manager sources, including cloud, local and git. -type ManagerSources struct { - Cloud CloudManager `mapstructure:"orbcloud"` - Local LocalManager `mapstructure:"local"` - Git GitManager `mapstructure:"git"` +// Sources represents the configuration for manager sources, including cloud, local and git. +type Sources struct { + Local LocalManager `yaml:"local"` + Git GitManager `yaml:"git"` } // ManagerConfig represents the configuration for the Config Manager type ManagerConfig struct { - Active string `mapstructure:"active"` - Sources ManagerSources `mapstructure:"sources"` + Active string `yaml:"active"` + Sources Sources `yaml:"sources"` +} + +// VaultManager represents the configuration for the Vault manager +type VaultManager struct { + Auth string `yaml:"auth"` + AuthArgs map[string]any `yaml:"auth_args"` + Address string `yaml:"address"` + Namespace string `yaml:"namespace"` + Timeout *int `yaml:"timeout,omitempty"` + Schedule *string `yaml:"schedule,omitempty"` +} + +// SecretsSources represents the configuration for manager sources, including vault. +type SecretsSources struct { + Vault VaultManager `yaml:"vault"` +} + +// ManagerSecrets represents the configuration for the Secrets Manager +type ManagerSecrets struct { + Active string `yaml:"active"` + Sources SecretsSources `yaml:"sources"` } // BackendCommons represents common configuration for backends type BackendCommons struct { Otel struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - AgentLabels map[string]string `mapstructure:"agent_labels"` - } `mapstructure:"otel"` + Host string `yaml:"host"` + Port int `yaml:"port"` + AgentLabels map[string]string `yaml:"agent_labels"` + } `yaml:"otel"` Diode struct { - Target string `mapstructure:"target"` - APIKey string `mapstructure:"api_key"` - AgentName string `mapstructure:"agent_name"` + Target string `yaml:"target"` + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret"` + AgentName string `yaml:"agent_name"` } } // OrbAgent represents the configuration for the Orb agent type OrbAgent struct { - Backends map[string]map[string]interface{} `mapstructure:"backends"` - Policies map[string]map[string]interface{} `mapstructure:"policies"` - Labels map[string]string `mapstructure:"labels"` - ConfigManager ManagerConfig `mapstructure:"config_manager"` + Backends map[string]any `yaml:"backends"` + Policies map[string]any `yaml:"policies"` + Labels map[string]string `yaml:"labels"` + ConfigManager ManagerConfig `yaml:"config_manager"` + SecretsManger ManagerSecrets `yaml:"secrets_manager"` Debug struct { - Enable bool `mapstructure:"enable"` - } `mapstructure:"debug"` - ConfigFile string `mapstructure:"config_file"` + Enable bool `yaml:"enable"` + } `yaml:"debug"` + ConfigFile string `yaml:"config_file"` } // Config represents the overall configuration type Config struct { - Version float64 `mapstructure:"version"` - OrbAgent OrbAgent `mapstructure:"orb"` + Version float64 `yaml:"version"` + OrbAgent OrbAgent `yaml:"orb"` } diff --git a/agent/configmgr/cloud.go b/agent/configmgr/cloud.go deleted file mode 100644 index 916652a8..00000000 --- a/agent/configmgr/cloud.go +++ /dev/null @@ -1,234 +0,0 @@ -package configmgr - -import ( - "bytes" - "context" - "crypto/tls" - "database/sql" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "strings" - "time" - - "github.com/jmoiron/sqlx" - migrate "github.com/rubenv/sql-migrate" - "go.uber.org/zap" - - "github.com/netboxlabs/orb-agent/agent/backend" - "github.com/netboxlabs/orb-agent/agent/config" - "github.com/netboxlabs/orb-agent/agent/policymgr" -) - -var _ Manager = (*cloudConfigManager)(nil) - -type cloudConfigManager struct { - logger *zap.Logger - pMgr policymgr.PolicyManager - config config.CloudManager - db *sqlx.DB -} - -func (cc *cloudConfigManager) migrateDB() error { - migrations := &migrate.MemoryMigrationSource{ - Migrations: []*migrate.Migration{ - { - Id: "cloud_config_1", - Up: []string{ - `CREATE TABLE IF NOT EXISTS cloud_config ( - address TEXT NOT NULL, - id TEXT NOT NULL, - key TEXT NOT NULL, - channel TEXT NOT NULL, - ts_created INTEGER NOT NULL - )`, - }, - Down: []string{ - "DROP TABLE cloud_config", - }, - }, - }, - } - - _, err := migrate.Exec(cc.db.DB, "sqlite3", migrations, migrate.Up) - - return err -} - -func (cc *cloudConfigManager) request(address string, token string, response interface{}, method string, body []byte) error { - tlsConfig := &tls.Config{InsecureSkipVerify: false} - if !cc.config.TLS.Verify { - tlsConfig.InsecureSkipVerify = true - } - transport := &http.Transport{TLSClientConfig: tlsConfig} - client := http.Client{ - Timeout: time.Second * 10, - Transport: transport, - } - URL := fmt.Sprintf("%s/api/v1/agents", address) - - req, err := http.NewRequest(method, URL, bytes.NewBuffer(body)) - if err != nil { - return err - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) - - res, getErr := client.Do(req) - if getErr != nil { - return getErr - } - defer func() { - if err := res.Body.Close(); err != nil { - cc.logger.Error("failed to close response body", zap.Error(err)) - } - }() - - if (res.StatusCode < 200) || (res.StatusCode > 299) { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("expected 2xx status code, no or invalid body: %d", res.StatusCode) - } - if body[0] == '{' { - var jsonBody map[string]interface{} - err := json.Unmarshal(body, &jsonBody) - if err == nil { - if errMsg, ok := jsonBody["error"]; ok { - return fmt.Errorf("%d %s", res.StatusCode, errMsg) - } - } - } - return fmt.Errorf("%d %s", res.StatusCode, body) - } - - err = json.NewDecoder(res.Body).Decode(&response) - if err != nil { - return err - } - return nil -} - -func (cc *cloudConfigManager) autoProvision(apiAddress string, token string) (config.MQTTConfig, error) { - type AgentRes struct { - ID string `json:"id"` - Key string `json:"key"` - ChannelID string `json:"channel_id"` - } - - type AgentReq struct { - Name string `json:"name"` - AgentLabels map[string]string `json:"agent_labels"` - } - - aname := cc.config.Config.AgentName - if aname == "" { - hostname, err := os.Hostname() - if err != nil { - return config.MQTTConfig{}, err - } - aname = hostname - } - - agentReq := AgentReq{Name: strings.Replace(aname, ".", "-", -1), AgentLabels: cc.config.Labels} - body, err := json.Marshal(agentReq) - if err != nil { - return config.MQTTConfig{}, err - } - - cc.logger.Info("attempting auto provision", zap.String("address", apiAddress)) - - var result AgentRes - err = cc.request(apiAddress, token, &result, http.MethodPost, body) - if err != nil { - return config.MQTTConfig{}, err - } - - // save to local config - address := "" - _, err = cc.db.Exec(`INSERT INTO cloud_config VALUES ($1, $2, $3, $4, datetime('now'))`, address, result.ID, result.Key, result.ChannelID) - if err != nil { - return config.MQTTConfig{}, err - } - - return config.MQTTConfig{ - ID: result.ID, - Key: result.Key, - ChannelID: result.ChannelID, - }, nil -} - -func (cc *cloudConfigManager) Start(_ config.Config, _ map[string]backend.Backend) error { - cc.logger.Info("using local config db", zap.String("filename", cc.config.DB.File)) - db, err := sqlx.Connect("sqlite3", cc.config.DB.File) - if err != nil { - return err - } - - cc.db = db - // currently we require address to be specified, it cannot be auto provisioned. - // this may change in the future - mqtt := cc.config.MQTT - - if len(mqtt.ID) > 0 && len(mqtt.Key) > 0 && len(mqtt.ChannelID) > 0 { - cc.logger.Info("using explicitly specified cloud configuration", - zap.String("address", mqtt.Address), - zap.String("id", mqtt.ID)) - return nil - } - - // if full config is not available, possibly attempt auto provision configuration - if !cc.config.Config.AutoProvision { - return errors.New("valid cloud MQTT config was not specified, and auto_provision was disabled") - } - - err = cc.migrateDB() - if err != nil { - return err - } - - // see if we have an existing auto provisioned configuration saved locally - q := `SELECT id, key, channel FROM cloud_config ORDER BY ts_created DESC LIMIT 1` - dba := config.MQTTConfig{} - if err := cc.db.QueryRowx(q).Scan(&dba.ID, &dba.Key, &dba.ChannelID); err != nil { - if err != sql.ErrNoRows { - return err - } - } else { - // successfully loaded previous auto provision - dba.Address = mqtt.Address - cc.logger.Info("using previous auto provisioned cloud configuration loaded from local storage", - zap.String("address", mqtt.Address), - zap.String("id", dba.ID)) - return nil - } - - // attempt a live auto provision - apiConfig := cc.config.API - if len(apiConfig.Token) == 0 { - return errors.New("wanted to auto provision, but no API token was available") - } - - result, err := cc.autoProvision(apiConfig.Address, apiConfig.Token) - if err != nil { - return err - } - result.Address = mqtt.Address - cc.logger.Info("using auto provisioned cloud configuration", - zap.String("address", mqtt.Address), - zap.String("id", result.ID)) - - result.Connect = true - return nil -} - -func (cc *cloudConfigManager) GetContext(ctx context.Context) context.Context { - if cc.config.MQTT.ID != "" { - ctx = context.WithValue(ctx, config.ContextKey("agent_id"), cc.config.MQTT.ID) - } else { - ctx = context.WithValue(ctx, config.ContextKey("agent_id"), "auto-provisioning-without-id") - } - return ctx -} diff --git a/agent/configmgr/git.go b/agent/configmgr/git.go index de7fa1ea..7ac75f6a 100644 --- a/agent/configmgr/git.go +++ b/agent/configmgr/git.go @@ -4,9 +4,8 @@ import ( "context" "errors" "io" - "os" + "log/slog" "slices" - "strings" "github.com/go-co-op/gocron/v2" gitv5 "github.com/go-git/go-git/v5" @@ -18,7 +17,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport/ssh" "github.com/go-git/go-git/v5/storage/memory" "github.com/google/uuid" - "go.uber.org/zap" "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" @@ -29,7 +27,7 @@ import ( var _ Manager = (*gitConfigManager)(nil) type gitConfigManager struct { - logger *zap.Logger + logger *slog.Logger pMgr policymgr.PolicyManager config config.GitManager scheduler gocron.Scheduler @@ -50,22 +48,6 @@ type ( } ) -func resolveEnv(value string) (string, error) { - // Check if the value starts with ${ and ends with } - if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") { - // Extract the environment variable name - envVar := value[2 : len(value)-1] - // Get the value of the environment variable - envValue := os.Getenv(envVar) - if envValue != "" { - return envValue, nil - } - return "", errors.New("a provided environment variable is not set") - } - // Return the original value if no substitution occurs - return value, nil -} - func (gc *gitConfigManager) readPolicies(tree *object.Tree, matchingPolicies []string) (map[policyPath]policyData, error) { policiesByPath := make(map[policyPath]policyData) allPolicies := make(map[string]map[string]any) @@ -82,7 +64,7 @@ func (gc *gitConfigManager) readPolicies(tree *object.Tree, matchingPolicies []s } defer func() { if err := reader.Close(); err != nil { - gc.logger.Error("failed to close file", zap.Error(err)) + gc.logger.Error("failed to close file", slog.Any("error", err)) } }() @@ -126,7 +108,7 @@ func (gc *gitConfigManager) removePolicies(policiesByPath map[policyPath]policyD appliedPolicies, err := gc.pMgr.GetRepo().GetAll() if err != nil { - gc.logger.Error("failed to get applied policies", zap.Error(err)) + gc.logger.Error("failed to get applied policies", slog.Any("error", err)) return } @@ -135,7 +117,7 @@ func (gc *gitConfigManager) removePolicies(policiesByPath map[policyPath]policyD key := policyKey{Backend: policy.Backend, Name: policy.Name} if _, exists := definedPolicies[key]; !exists { if err := gc.pMgr.RemovePolicy(policy.ID, policy.Name, policy.Backend); err != nil { - gc.logger.Error("failed to remove policy", zap.Error(err)) + gc.logger.Error("failed to remove policy", slog.Any("error", err)) } } } @@ -172,7 +154,7 @@ func (gc *gitConfigManager) processSelector(file *object.File, cfg config.Config } defer func() { if err := reader.Close(); err != nil { - gc.logger.Error("failed to close file", zap.Error(err)) + gc.logger.Error("failed to close file", slog.Any("error", err)) } }() @@ -205,14 +187,14 @@ func (gc *gitConfigManager) processSelector(file *object.File, cfg config.Config } } if matches { - gc.logger.Info("Selector matched", zap.String("selector", selectorName)) + gc.logger.Info("Selector matched", slog.String("selector", selectorName)) for pName, policy := range entry.Policies { if policy.Enabled != nil && !*policy.Enabled { continue } if _, exists := policyPathsSet[policy.Path]; exists { - gc.logger.Warn("Policy path already exists", zap.String("selector", selectorName), - zap.String("policy", pName), zap.String("path", policy.Path)) + gc.logger.Warn("Policy path already exists", slog.String("selector", selectorName), + slog.String("policy", pName), slog.String("path", policy.Path)) } policyPathsSet[policy.Path] = struct{}{} } @@ -236,14 +218,14 @@ func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]back RefSpecs: []gitconfig.RefSpec{"refs/heads/*:refs/heads/*"}, }) if err != nil && err != gitv5.NoErrAlreadyUpToDate { - gc.logger.Error("Failed to fetch latest changes", zap.Error(err)) + gc.logger.Error("Failed to fetch latest changes", slog.Any("error", err)) return } // Get the latest reference (HEAD) ref, err := gc.repo.Reference(plumbing.ReferenceName("refs/heads/"+gc.config.Branch), true) if err != nil { - gc.logger.Error("Failed to get latest branch reference", zap.Error(err)) + gc.logger.Error("Failed to get latest branch reference", slog.Any("error", err)) return } @@ -256,13 +238,13 @@ func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]back // Get the latest commit commit, err := gc.repo.CommitObject(ref.Hash()) if err != nil { - gc.logger.Error("Failed to get commit object", zap.Error(err)) + gc.logger.Error("Failed to get commit object", slog.Any("error", err)) return } tree, err := commit.Tree() if err != nil { - gc.logger.Error("Failed to get commit tree", zap.Error(err)) + gc.logger.Error("Failed to get commit tree", slog.Any("error", err)) return } @@ -278,20 +260,20 @@ func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]back // Get the last commit's tree oldCommit, err := gc.repo.CommitObject(gc.lastRef) if err != nil { - gc.logger.Error("Failed to get old commit object", zap.Error(err)) + gc.logger.Error("Failed to get old commit object", slog.Any("error", err)) return } oldTree, err := oldCommit.Tree() if err != nil { - gc.logger.Error("Failed to get old commit tree", zap.Error(err)) + gc.logger.Error("Failed to get old commit tree", slog.Any("error", err)) return } // Check for file changes changes, err := oldTree.Diff(tree) if err != nil { - gc.logger.Error("Failed to get diff", zap.Error(err)) + gc.logger.Error("Failed to get diff", slog.Any("error", err)) return } @@ -300,7 +282,7 @@ func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]back matchingPolicies, err := gc.processSelector(selectorFile, cfg) if err != nil { - gc.logger.Error("Failed to process selector", zap.Error(err)) + gc.logger.Error("Failed to process selector", slog.Any("error", err)) return } @@ -319,7 +301,7 @@ func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]back policiesByPath, err := gc.readPolicies(tree, matchingPolicies) if err != nil { - gc.logger.Error("Failed to read policies", zap.Error(err)) + gc.logger.Error("Failed to read policies", slog.Any("error", err)) return } @@ -333,7 +315,7 @@ func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]back } policies := policiesByPath[policyPath(policy)] if err = gc.applyPolicies(policies, backends); err != nil { - gc.logger.Error("failed to apply policies", zap.Error(err)) + gc.logger.Error("failed to apply policies", slog.Any("error", err)) } } @@ -345,7 +327,7 @@ func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]back for path, policies := range policiesByPath { if change.To.Name == string(path) { if err = gc.applyPolicies(policies, backends); err != nil { - gc.logger.Error("Failed to apply policies", zap.Error(err)) + gc.logger.Error("Failed to apply policies", slog.Any("error", err)) } } } @@ -355,13 +337,14 @@ func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]back func (gc *gitConfigManager) Start(cfg config.Config, backends map[string]backend.Backend) error { var err error gc.version = 1 + gc.config = cfg.OrbAgent.ConfigManager.Sources.Git if gc.config.URL == "" { return errors.New("URL is required for Git Config Manager") } if gc.config.Auth == "basic" { - if gc.config.Password, err = resolveEnv(gc.config.Password); err != nil { + if gc.config.Password, err = config.ResolveEnv(gc.config.Password); err != nil { return err } gc.authMethod = &http.BasicAuth{ @@ -370,7 +353,7 @@ func (gc *gitConfigManager) Start(cfg config.Config, backends map[string]backend } } else if gc.config.Auth == "ssh" { if gc.config.PrivateKey != "" { - if gc.config.Password, err = resolveEnv(gc.config.Password); err != nil { + if gc.config.Password, err = config.ResolveEnv(gc.config.Password); err != nil { return err } gc.authMethod, err = ssh.NewPublicKeysFromFile("git", gc.config.PrivateKey, gc.config.Password) @@ -422,7 +405,7 @@ func (gc *gitConfigManager) Start(cfg config.Config, backends map[string]backend for _, ref := range refs { if ref.Name().IsBranch() { branchName = ref.Name().Short() - gc.logger.Info("detected default branch", zap.String("branch", branchName)) + gc.logger.Info("detected default branch", slog.String("branch", branchName)) break } } @@ -432,7 +415,7 @@ func (gc *gitConfigManager) Start(cfg config.Config, backends map[string]backend } } - gc.logger.Info("cloning repository", zap.String("url", gc.config.URL), zap.String("branch", branchName)) + gc.logger.Info("cloning repository", slog.String("url", gc.config.URL), slog.String("branch", branchName)) // Now clone the repository with the determined branch gc.repo, err = gitv5.Clone(memory.NewStorage(), nil, &gitv5.CloneOptions{ @@ -506,7 +489,8 @@ func (gc *gitConfigManager) Start(cfg config.Config, backends map[string]backend } gc.scheduler = s task := gocron.NewTask(gc.schedule, cfg, backends) - if _, err = gc.scheduler.NewJob(gocron.CronJob(*gc.config.Schedule, false), task, gocron.WithSingletonMode(gocron.LimitModeReschedule)); err != nil { + if _, err = gc.scheduler.NewJob(gocron.CronJob(*gc.config.Schedule, false), task, + gocron.WithSingletonMode(gocron.LimitModeReschedule)); err != nil { return err } gc.scheduler.Start() diff --git a/agent/configmgr/git_test.go b/agent/configmgr/git_test.go new file mode 100644 index 00000000..6e77a70e --- /dev/null +++ b/agent/configmgr/git_test.go @@ -0,0 +1,120 @@ +package configmgr_test + +import ( + "log/slog" + "os" + "path/filepath" + "testing" + "time" + + gitv5 "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/configmgr" +) + +// TestGitStart tests the Start method of gitConfigManager. +func TestGitStart(t *testing.T) { + // Create a temporary directory for the fake remote repository. + remoteDir, err := os.MkdirTemp("", "fake-remote") + require.NoError(t, err) + + defer func() { + if err := os.RemoveAll(remoteDir); err != nil { + require.NoError(t, err, "failed to remove temp dir") + } + }() + + // Initialize a new git repository in remoteDir. + remoteRepo, err := gitv5.PlainInit(remoteDir, false) + require.NoError(t, err) + + // Create a minimal selector.yaml file. + selectorContent := ` +test_selector: + selector: + env: test + policies: + test_policy: + enabled: true + path: "policy.yaml" +` + selectorPath := filepath.Join(remoteDir, "selector.yaml") + err = os.WriteFile(selectorPath, []byte(selectorContent), 0o644) + require.NoError(t, err) + + // Create a minimal policy file. + policyContent := ` +backend1: + test_policy: + key: "value" +` + policyPath := filepath.Join(remoteDir, "policy.yaml") + err = os.WriteFile(policyPath, []byte(policyContent), 0o644) + require.NoError(t, err) + + // Add and commit files. + w, err := remoteRepo.Worktree() + require.NoError(t, err) + _, err = w.Add("selector.yaml") + require.NoError(t, err) + _, err = w.Add("policy.yaml") + require.NoError(t, err) + commitMsg := "initial commit" + _, err = w.Commit(commitMsg, &gitv5.CommitOptions{ + Author: &object.Signature{ + Name: "tester", + Email: "tester@example.com", + When: time.Now(), + }, + }) + require.NoError(t, err) + + // Build a file:// URL for the repository. + repoURL := "file://" + remoteDir + + // Create a fake policy manager. + pMgr := new(mockPolicyManager) + + // Build a minimal config for testing. + schedule := "* * * * *" + cfg := config.Config{ + OrbAgent: config.OrbAgent{ + Labels: map[string]string{"env": "test"}, + ConfigManager: config.ManagerConfig{ + Active: "git", + Sources: config.Sources{ + Git: config.GitManager{ + URL: repoURL, + Branch: "", // let it detect default branch + Auth: "none", // no auth for file:// protocol + Schedule: &schedule, // every minute + }, + }, + }, + }, + } + + // Create a logger. + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // Instantiate the gitConfigManager. + gc := configmgr.New(logger, pMgr, cfg.OrbAgent.ConfigManager.Active) + + // Call Start + backends := map[string]backend.Backend{ + "backend1": &mockBackend{name: "backend1"}, + } + + pMgr.On("ManagePolicy", mock.MatchedBy(func(payload config.PolicyPayload) bool { + return payload.Backend == "backend1" && + payload.Action == "manage" + })).Return() + + err = gc.Start(cfg, backends) + require.NoError(t, err) +} diff --git a/agent/configmgr/local.go b/agent/configmgr/local.go index 8cca9518..f7258e0d 100644 --- a/agent/configmgr/local.go +++ b/agent/configmgr/local.go @@ -3,9 +3,9 @@ package configmgr import ( "context" "errors" + "log/slog" "github.com/google/uuid" - "go.uber.org/zap" "github.com/netboxlabs/orb-agent/agent/backend" "github.com/netboxlabs/orb-agent/agent/config" @@ -15,22 +15,24 @@ import ( var _ Manager = (*localConfigManager)(nil) type localConfigManager struct { - logger *zap.Logger + logger *slog.Logger pMgr policymgr.PolicyManager - config config.LocalManager } func (lc *localConfigManager) Start(cfg config.Config, backends map[string]backend.Backend) error { if cfg.OrbAgent.Policies == nil { return errors.New("no policies specified") } - for beName, policy := range cfg.OrbAgent.Policies { _, ok := backends[beName] if !ok { return errors.New("backend not found: " + beName) } - for pName, data := range policy { + newPolicy, ok := policy.(map[string]any) + if !ok { + return errors.New("invalid policy format for backend: " + beName) + } + for pName, data := range newPolicy { policyID := uuid.NewSHA1(uuid.Nil, []byte(pName+beName)).String() id := uuid.NewString() payload := config.PolicyPayload{ diff --git a/agent/configmgr/local_test.go b/agent/configmgr/local_test.go new file mode 100644 index 00000000..7ad78465 --- /dev/null +++ b/agent/configmgr/local_test.go @@ -0,0 +1,108 @@ +package configmgr_test + +import ( + "log/slog" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/configmgr" +) + +// Test the localConfigManager implementation +func TestLocalConfigManager(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + pMgr := new(mockPolicyManager) + + t.Run("Start with policies", func(t *testing.T) { + cfg := config.ManagerConfig{ + Active: "local", + } + + // Create test configuration with policies + testConfig := config.Config{ + OrbAgent: config.OrbAgent{ + Policies: map[string]any{ + "testbackend": map[string]any{ + "testpolicy": map[string]string{ + "key": "value", + }, + }, + }, + }, + } + + // Create a mock backend + mockBE := &mockBackend{name: "testbackend"} + backends := map[string]backend.Backend{ + "testbackend": mockBE, + } + + // Setup expectations + pMgr.On("ManagePolicy", mock.MatchedBy(func(payload config.PolicyPayload) bool { + return payload.Name == "testpolicy" && + payload.Backend == "testbackend" && + payload.Action == "manage" + })).Return() + + // Create and start the manager + mgr := configmgr.New(logger, pMgr, cfg.Active) + err := mgr.Start(testConfig, backends) + + // Verify + assert.NoError(t, err) + pMgr.AssertExpectations(t) + }) + + t.Run("Start with no policies", func(t *testing.T) { + cfg := config.ManagerConfig{ + Active: "local", + } + + // Create test configuration with no policies + testConfig := config.Config{} + + backends := map[string]backend.Backend{} + + // Create the manager + mgr := configmgr.New(logger, pMgr, cfg.Active) + err := mgr.Start(testConfig, backends) + + // Should return an error + assert.Error(t, err) + assert.Contains(t, err.Error(), "no policies specified") + }) + + t.Run("Start with missing backend", func(t *testing.T) { + cfg := config.ManagerConfig{ + Active: "local", + } + + // Create test configuration with policies for non-existent backend + testConfig := config.Config{ + OrbAgent: config.OrbAgent{ + Policies: map[string]any{ + "nonexistentbackend": map[string]any{ + "testpolicy": map[string]string{ + "key": "value", + }, + }, + }, + }, + } + + backends := map[string]backend.Backend{} + + // Create the manager + mgr := configmgr.New(logger, pMgr, cfg.Active) + err := mgr.Start(testConfig, backends) + + // Should return an error + assert.Error(t, err) + assert.Contains(t, err.Error(), "backend not found") + }) +} diff --git a/agent/configmgr/manager.go b/agent/configmgr/manager.go index cef3461e..33e79955 100644 --- a/agent/configmgr/manager.go +++ b/agent/configmgr/manager.go @@ -2,8 +2,7 @@ package configmgr import ( "context" - - "go.uber.org/zap" + "log/slog" "github.com/netboxlabs/orb-agent/agent/backend" "github.com/netboxlabs/orb-agent/agent/config" @@ -17,15 +16,13 @@ type Manager interface { } // New creates a new instance of ConfigManager based on the configuration -func New(logger *zap.Logger, mgr policymgr.PolicyManager, c config.ManagerConfig) Manager { - switch c.Active { +func New(logger *slog.Logger, pMgr policymgr.PolicyManager, active string) Manager { + switch active { case "local": - return &localConfigManager{logger: logger, pMgr: mgr, config: c.Sources.Local} - case "cloud": - return &cloudConfigManager{logger: logger, pMgr: mgr, config: c.Sources.Cloud} + return &localConfigManager{logger: logger, pMgr: pMgr} case "git": - return &gitConfigManager{logger: logger, pMgr: mgr, config: c.Sources.Git} + return &gitConfigManager{logger: logger, pMgr: pMgr} default: - return &localConfigManager{logger: logger, pMgr: mgr, config: c.Sources.Local} + return &localConfigManager{logger: logger, pMgr: pMgr} } } diff --git a/agent/configmgr/manager_test.go b/agent/configmgr/manager_test.go new file mode 100644 index 00000000..cdd31a52 --- /dev/null +++ b/agent/configmgr/manager_test.go @@ -0,0 +1,173 @@ +package configmgr_test + +import ( + "context" + "log/slog" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/configmgr" + "github.com/netboxlabs/orb-agent/agent/policies" +) + +// Mock implementations for testing + +type mockPolicyManager struct { + mock.Mock +} + +func (m *mockPolicyManager) ManagePolicy(payload config.PolicyPayload) { + m.Called(payload) +} + +func (m *mockPolicyManager) RemovePolicyDataset(policyID string, datasetID string, be backend.Backend) { + m.Called(policyID, datasetID, be) +} + +func (m *mockPolicyManager) GetPolicyState() ([]policies.PolicyData, error) { + args := m.Called() + return args.Get(0).([]policies.PolicyData), args.Error(1) +} + +func (m *mockPolicyManager) GetRepo() policies.PolicyRepo { + args := m.Called() + return args.Get(0).(policies.PolicyRepo) +} + +func (m *mockPolicyManager) ApplyBackendPolicies(be backend.Backend) error { + args := m.Called(be) + return args.Error(0) +} + +func (m *mockPolicyManager) RemoveBackendPolicies(be backend.Backend, permanently bool) error { + args := m.Called(be, permanently) + return args.Error(0) +} + +func (m *mockPolicyManager) RemovePolicy(policyID string, policyName string, beName string) error { + args := m.Called(policyID, policyName, beName) + return args.Error(0) +} + +type mockBackend struct { + mock.Mock + name string +} + +func (m *mockBackend) Configure(logger *slog.Logger, repo policies.PolicyRepo, cfg map[string]any, commons config.BackendCommons) error { + args := m.Called(logger, repo, cfg, commons) + return args.Error(0) +} + +func (m *mockBackend) Start(ctx context.Context, cancelFunc context.CancelFunc) error { + args := m.Called(ctx, cancelFunc) + return args.Error(0) +} + +func (m *mockBackend) Stop(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func (m *mockBackend) GetStartTime() time.Time { + args := m.Called() + return args.Get(0).(time.Time) +} + +func (m *mockBackend) GetRunningStatus() (backend.RunningStatus, string, error) { + args := m.Called() + return args.Get(0).(backend.RunningStatus), args.String(1), args.Error(2) +} + +func (m *mockBackend) GetInitialState() backend.RunningStatus { + args := m.Called() + return args.Get(0).(backend.RunningStatus) +} + +func (m *mockBackend) Version() (string, error) { + args := m.Called() + return args.String(0), args.Error(1) +} + +func (m *mockBackend) GetName() string { + return m.name +} + +func (m *mockBackend) GetCapabilities() (map[string]any, error) { + args := m.Called() + return args.Get(0).(map[string]any), args.Error(1) +} + +func (m *mockBackend) FullReset(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func (m *mockBackend) ApplyPolicy(policy policies.PolicyData, updatePolicy bool) error { + args := m.Called(policy, updatePolicy) + return args.Error(0) +} + +func (m *mockBackend) RemovePolicy(policy policies.PolicyData) error { + args := m.Called(policy) + return args.Error(0) +} + +// Test the manager.New function +func TestManagerNew(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + pMgr := new(mockPolicyManager) + + t.Run("LocalManager", func(t *testing.T) { + cfg := config.ManagerConfig{ + Active: "local", + Sources: config.Sources{ + Local: config.LocalManager{}, + }, + } + + mgr := configmgr.New(logger, pMgr, cfg.Active) + assert.NotNil(t, mgr) + // Check we got the expected implementation + ctx := context.Background() + resultCtx := mgr.GetContext(ctx) + assert.Equal(t, ctx, resultCtx) + }) + + t.Run("GitManager", func(t *testing.T) { + cfg := config.ManagerConfig{ + Active: "git", + Sources: config.Sources{ + Git: config.GitManager{ + URL: "https://github.com/example/repo.git", + }, + }, + } + + mgr := configmgr.New(logger, pMgr, cfg.Active) + assert.NotNil(t, mgr) + // Check we got the expected implementation + ctx := context.Background() + resultCtx := mgr.GetContext(ctx) + assert.Equal(t, ctx, resultCtx) + }) + + t.Run("DefaultToLocalManager", func(t *testing.T) { + cfg := config.ManagerConfig{ + Active: "unknown", + } + + mgr := configmgr.New(logger, pMgr, cfg.Active) + assert.NotNil(t, mgr) + // Check we got the local implementation + ctx := context.Background() + resultCtx := mgr.GetContext(ctx) + assert.Equal(t, ctx, resultCtx) + }) +} diff --git a/agent/docker/Dockerfile b/agent/docker/Dockerfile index e591cecb..62d50e65 100644 --- a/agent/docker/Dockerfile +++ b/agent/docker/Dockerfile @@ -29,7 +29,7 @@ FROM python:3.12-slim-bullseye RUN \ apt update && \ - apt install --yes --force-yes --no-install-recommends nmap openssh-client && \ + apt install --yes --no-install-recommends nmap openssh-client && \ rm -rf /var/lib/apt RUN mkdir -p /opt/orb diff --git a/agent/policies/repo.go b/agent/policies/repo.go index a00f88eb..26796c5d 100644 --- a/agent/policies/repo.go +++ b/agent/policies/repo.go @@ -2,8 +2,6 @@ package policies import ( "errors" - - "go.uber.org/zap" ) // PolicyRepo is the interface for policy repositories @@ -20,8 +18,6 @@ type PolicyRepo interface { } type policyMemRepo struct { - logger *zap.Logger - db map[string]PolicyData nameMap map[string]string } @@ -36,9 +32,8 @@ func (p policyMemRepo) GetByName(policyName string) (PolicyData, error) { } // NewMemRepo creates a new in-memory policy repository -func NewMemRepo(logger *zap.Logger) (PolicyRepo, error) { +func NewMemRepo() (PolicyRepo, error) { r := &policyMemRepo{ - logger: logger, db: make(map[string]PolicyData), nameMap: make(map[string]string), } diff --git a/agent/policies/repo_test.go b/agent/policies/repo_test.go new file mode 100644 index 00000000..009893a3 --- /dev/null +++ b/agent/policies/repo_test.go @@ -0,0 +1,396 @@ +package policies_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netboxlabs/orb-agent/agent/policies" +) + +func TestNewMemRepo(t *testing.T) { + repo, err := policies.NewMemRepo() + + assert.NoError(t, err) + assert.NotNil(t, repo) +} + +func TestExists(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Non-existent policy + exists := repo.Exists("non-existent-id") + assert.False(t, exists) + + // Create a policy + pd := policies.PolicyData{ + ID: "test-id", + Name: "test-policy", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Now it should exist + exists = repo.Exists("test-id") + assert.True(t, exists) +} + +func TestGet(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Try to get a non-existent policy + _, err = repo.Get("non-existent-id") + assert.Error(t, err) + + // Create a policy + pd := policies.PolicyData{ + ID: "test-id", + Name: "test-policy", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Get the policy + retrievedPD, err := repo.Get("test-id") + require.NoError(t, err) + + assert.Equal(t, pd.ID, retrievedPD.ID) + assert.Equal(t, pd.Name, retrievedPD.Name) + assert.Equal(t, pd.Backend, retrievedPD.Backend) + assert.Equal(t, pd.Version, retrievedPD.Version) + assert.Equal(t, pd.Datasets, retrievedPD.Datasets) + assert.Equal(t, pd.GroupIDs, retrievedPD.GroupIDs) + assert.Equal(t, pd.State, retrievedPD.State) +} + +func TestGetByName(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Try to get a non-existent policy by name + _, err = repo.GetByName("non-existent-name") + assert.Error(t, err) + + // Create a policy + pd := policies.PolicyData{ + ID: "test-id", + Name: "test-policy", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Get the policy by name + retrievedPD, err := repo.GetByName("test-policy") + require.NoError(t, err) + + assert.Equal(t, pd.ID, retrievedPD.ID) + assert.Equal(t, pd.Name, retrievedPD.Name) +} + +func TestUpdate(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Create a policy + pd := policies.PolicyData{ + ID: "test-id", + Name: "test-policy", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Update the policy + pd.Version = 2 + pd.State = policies.Running + + err = repo.Update(pd) + require.NoError(t, err) + + // Get the updated policy + retrievedPD, err := repo.Get("test-id") + require.NoError(t, err) + + assert.Equal(t, int32(2), retrievedPD.Version) + assert.Equal(t, policies.Running, retrievedPD.State) +} + +func TestRemove(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Try to remove a non-existent policy + err = repo.Remove("non-existent-id") + assert.Error(t, err) + + // Create a policy + pd := policies.PolicyData{ + ID: "test-id", + Name: "test-policy", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Remove the policy + err = repo.Remove("test-id") + assert.NoError(t, err) + + // Verify it's removed + exists := repo.Exists("test-id") + assert.False(t, exists) + + // Verify name mapping is also removed + _, err = repo.GetByName("test-policy") + assert.Error(t, err) +} + +func TestGetAll(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Get all policies when none exist + allPolicies, err := repo.GetAll() + require.NoError(t, err) + assert.Empty(t, allPolicies) + + // Create policies + pd1 := policies.PolicyData{ + ID: "test-id-1", + Name: "test-policy-1", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + pd2 := policies.PolicyData{ + ID: "test-id-2", + Name: "test-policy-2", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset2": true}, + GroupIDs: map[string]bool{"group2": true}, + Data: map[string]any{"key": "value"}, + State: policies.Running, + } + + err = repo.Update(pd1) + require.NoError(t, err) + + err = repo.Update(pd2) + require.NoError(t, err) + + // Get all policies + allPolicies, err = repo.GetAll() + require.NoError(t, err) + + assert.Len(t, allPolicies, 2) + + // Create a map of policies by ID for easier checking + policiesMap := make(map[string]policies.PolicyData) + for _, p := range allPolicies { + policiesMap[p.ID] = p + } + + // Verify both policies are returned + assert.Contains(t, policiesMap, "test-id-1") + assert.Contains(t, policiesMap, "test-id-2") +} + +func TestEnsureDataset(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Try to ensure dataset for a non-existent policy + err = repo.EnsureDataset("non-existent-id", "dataset2") + assert.Error(t, err) + + // Create a policy + pd := policies.PolicyData{ + ID: "test-id", + Name: "test-policy", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Ensure a new dataset + err = repo.EnsureDataset("test-id", "dataset2") + assert.NoError(t, err) + + // Verify the dataset is added + retrievedPD, err := repo.Get("test-id") + require.NoError(t, err) + + assert.True(t, retrievedPD.Datasets["dataset1"]) + assert.True(t, retrievedPD.Datasets["dataset2"]) +} + +func TestRemoveDataset(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Try to remove dataset from a non-existent policy + _, err = repo.RemoveDataset("non-existent-id", "dataset1") + assert.Error(t, err) + + // Create a policy with multiple datasets + pd := policies.PolicyData{ + ID: "test-id", + Name: "test-policy", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true, "dataset2": true, "dataset3": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Remove a dataset that doesn't affect policy removal + removePolicy, err := repo.RemoveDataset("test-id", "dataset1") + require.NoError(t, err) + assert.False(t, removePolicy) + + // Verify the dataset is removed but policy still exists + retrievedPD, err := repo.Get("test-id") + require.NoError(t, err) + assert.False(t, retrievedPD.Datasets["dataset1"]) + assert.True(t, retrievedPD.Datasets["dataset2"]) + assert.True(t, retrievedPD.Datasets["dataset3"]) + + // Remove another dataset + removePolicy, err = repo.RemoveDataset("test-id", "dataset2") + require.NoError(t, err) + assert.False(t, removePolicy) + + // Remove the last dataset which should trigger policy removal + removePolicy, err = repo.RemoveDataset("test-id", "dataset3") + require.NoError(t, err) + assert.True(t, removePolicy) + + // Verify the datasets are all removed + retrievedPD, err = repo.Get("test-id") + require.NoError(t, err) + assert.Empty(t, retrievedPD.Datasets) +} + +func TestEnsureGroupID(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Try to ensure group ID for a non-existent policy + err = repo.EnsureGroupID("non-existent-id", "group2") + assert.Error(t, err) + + // Create a policy + pd := policies.PolicyData{ + ID: "test-id", + Name: "test-policy", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Ensure a new group ID + err = repo.EnsureGroupID("test-id", "group2") + assert.NoError(t, err) + + // Verify the group ID is added + retrievedPD, err := repo.Get("test-id") + require.NoError(t, err) + + assert.True(t, retrievedPD.GroupIDs["group1"]) + assert.True(t, retrievedPD.GroupIDs["group2"]) +} + +func TestUpdateRenamingPolicy(t *testing.T) { + repo, err := policies.NewMemRepo() + require.NoError(t, err) + + // Create a policy + pd := policies.PolicyData{ + ID: "test-id", + Name: "old-name", + Backend: "test-backend", + Version: 1, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + Data: map[string]any{"key": "value"}, + State: policies.Unknown, + } + + err = repo.Update(pd) + require.NoError(t, err) + + // Verify we can get by old name + _, err = repo.GetByName("old-name") + require.NoError(t, err) + + // Update the policy with a new name + pd.Name = "new-name" + err = repo.Update(pd) + require.NoError(t, err) + + // Verify we can get by new name + retrievedPD, err := repo.GetByName("new-name") + require.NoError(t, err) + assert.Equal(t, "test-id", retrievedPD.ID) + + // Verify we cannot get by old name + _, err = repo.GetByName("old-name") + assert.Error(t, err) +} diff --git a/agent/policies/types.go b/agent/policies/types.go index 97ba0970..a5c563a9 100644 --- a/agent/policies/types.go +++ b/agent/policies/types.go @@ -13,7 +13,7 @@ type PolicyData struct { Name string Backend string Version int32 - Data interface{} + Data any State PolicyState BackendErr string LastScrapeBytes int64 @@ -66,7 +66,7 @@ func (s PolicyState) String() string { } // Scan scans the value into the PolicyState -func (s *PolicyState) Scan(value interface{}) error { +func (s *PolicyState) Scan(value any) error { *s = policyStateRevMap[string(value.([]byte))] return nil } diff --git a/agent/policies/types_test.go b/agent/policies/types_test.go new file mode 100644 index 00000000..952842e9 --- /dev/null +++ b/agent/policies/types_test.go @@ -0,0 +1,95 @@ +package policies_test + +import ( + "database/sql/driver" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/netboxlabs/orb-agent/agent/policies" +) + +func TestPolicyStateString(t *testing.T) { + testCases := []struct { + state policies.PolicyState + expected string + }{ + {policies.Unknown, "unknown"}, + {policies.Running, "running"}, + {policies.FailedToApply, "failed_to_apply"}, + {policies.Offline, "offline"}, + {policies.NoTapMatch, "no_tap_match"}, + } + + for _, tc := range testCases { + assert.Equal(t, tc.expected, tc.state.String()) + } +} + +func TestPolicyStateScan(t *testing.T) { + testCases := []struct { + input []byte + expected policies.PolicyState + }{ + {[]byte("unknown"), policies.Unknown}, + {[]byte("running"), policies.Running}, + {[]byte("failed_to_apply"), policies.FailedToApply}, + {[]byte("offline"), policies.Offline}, + {[]byte("no_tap_match"), policies.NoTapMatch}, + } + + for _, tc := range testCases { + var state policies.PolicyState + err := state.Scan(tc.input) + + assert.NoError(t, err) + assert.Equal(t, tc.expected, state) + } +} + +func TestPolicyStateValue(t *testing.T) { + testCases := []struct { + state policies.PolicyState + expected driver.Value + }{ + {policies.Unknown, "unknown"}, + {policies.Running, "running"}, + {policies.FailedToApply, "failed_to_apply"}, + {policies.Offline, "offline"}, + {policies.NoTapMatch, "no_tap_match"}, + } + + for _, tc := range testCases { + value, err := tc.state.Value() + + assert.NoError(t, err) + assert.Equal(t, tc.expected, value) + } +} + +func TestGetDatasetIDs(t *testing.T) { + testCases := []struct { + datasets map[string]bool + expectedN int + }{ + {map[string]bool{}, 0}, + {map[string]bool{"dataset1": true}, 1}, + {map[string]bool{"dataset1": true, "dataset2": true}, 2}, + {map[string]bool{"dataset1": true, "dataset2": true, "dataset3": true}, 3}, + } + + for _, tc := range testCases { + pd := policies.PolicyData{ + Datasets: tc.datasets, + } + + ids := pd.GetDatasetIDs() + + assert.Len(t, ids, tc.expectedN) + + // Verify all expected IDs are in the result + for id := range tc.datasets { + assert.Contains(t, ids, id) + } + } +} diff --git a/agent/policymgr/manager.go b/agent/policymgr/manager.go index a022275c..6d6b859e 100644 --- a/agent/policymgr/manager.go +++ b/agent/policymgr/manager.go @@ -3,13 +3,13 @@ package policymgr import ( "errors" "fmt" + "log/slog" "strings" - "go.uber.org/zap" - "github.com/netboxlabs/orb-agent/agent/backend" "github.com/netboxlabs/orb-agent/agent/config" "github.com/netboxlabs/orb-agent/agent/policies" + "github.com/netboxlabs/orb-agent/agent/secretsmgr" ) // PolicyManager is the interface for managing policies @@ -26,19 +26,22 @@ type PolicyManager interface { var _ PolicyManager = (*policyManager)(nil) type policyManager struct { - logger *zap.Logger + logger *slog.Logger config config.Config - repo policies.PolicyRepo + repo policies.PolicyRepo + secrets secretsmgr.Manager } // New creates a new instance of PolicyManager -func New(logger *zap.Logger, c config.Config) (PolicyManager, error) { - repo, err := policies.NewMemRepo(logger) +func New(logger *slog.Logger, secrets secretsmgr.Manager, c config.Config) (PolicyManager, error) { + repo, err := policies.NewMemRepo() if err != nil { return nil, err } - return &policyManager{logger: logger, config: c, repo: repo}, nil + policyManager := &policyManager{logger: logger, config: c, repo: repo, secrets: secrets} + policyManager.secrets.RegisterUpdatePoliciesCallback(policyManager.policiesChanged) + return policyManager, nil } func (a *policyManager) GetRepo() policies.PolicyRepo { @@ -51,12 +54,12 @@ func (a *policyManager) GetPolicyState() ([]policies.PolicyData, error) { func (a *policyManager) ManagePolicy(payload config.PolicyPayload) { a.logger.Info("managing agent policy from core", - zap.String("action", payload.Action), - zap.String("name", payload.Name), - zap.String("dataset", payload.DatasetID), - zap.String("backend", payload.Backend), - zap.String("id", payload.ID), - zap.Int32("version", payload.Version)) + slog.String("action", payload.Action), + slog.String("name", payload.Name), + slog.String("dataset", payload.DatasetID), + slog.String("backend", payload.Backend), + slog.String("id", payload.ID), + slog.Int("version", int(payload.Version))) switch payload.Action { case "manage": @@ -76,25 +79,28 @@ func (a *policyManager) ManagePolicy(payload config.PolicyPayload) { if payload.DatasetID != "" { err := a.repo.EnsureDataset(payload.ID, payload.DatasetID) if err != nil { - a.logger.Warn("policy failed to ensure dataset id", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name), zap.String("dataset_id", payload.DatasetID), zap.Error(err)) + a.logger.Warn("policy failed to ensure dataset id", slog.String("policy_id", payload.ID), + slog.String("policy_name", payload.Name), slog.String("dataset_id", payload.DatasetID), slog.Any("error", err)) } } if payload.AgentGroupID != "" { err := a.repo.EnsureGroupID(payload.ID, payload.AgentGroupID) if err != nil { - a.logger.Warn("policy failed to ensure agent group id", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name), zap.String("agent_group_id", payload.AgentGroupID), zap.Error(err)) + a.logger.Warn("policy failed to ensure agent group id", slog.String("policy_id", payload.ID), + slog.String("policy_name", payload.Name), slog.String("agent_group_id", payload.AgentGroupID), slog.Any("error", err)) } } // if policy already exist and has no version upgrade, has no need to apply it again currentPolicy, err := a.repo.Get(payload.ID) if err != nil { - a.logger.Error("failed to retrieve policy", zap.String("policy_id", payload.ID), zap.Error(err)) + a.logger.Error("failed to retrieve policy", slog.String("policy_id", payload.ID), slog.Any("error", err)) return } if currentPolicy.Backend == pd.Backend && currentPolicy.Version >= pd.Version && currentPolicy.State == policies.Running { - a.logger.Info("a better version of this policy has already been applied, skipping", zap.String("policy_id", pd.ID), zap.String("policy_name", pd.Name), zap.String("attempted_version", fmt.Sprint(pd.Version)), zap.String("current_version", fmt.Sprint(currentPolicy.Version))) + a.logger.Info("a better version of this policy has already been applied, skipping", slog.String("policy_id", pd.ID), slog.String("policy_name", pd.Name), + slog.String("attempted_version", fmt.Sprint(pd.Version)), slog.String("current_version", fmt.Sprint(currentPolicy.Version))) return } updatePolicy = true @@ -107,7 +113,7 @@ func (a *policyManager) ManagePolicy(payload config.PolicyPayload) { // new policy we have not seen before, associate with this dataset // on first time we see policy, we *require* dataset if payload.DatasetID == "" { - a.logger.Error("policy RPC for unseen policy did not include dataset ID, skipping", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name)) + a.logger.Error("policy RPC for unseen policy did not include dataset ID, skipping", slog.String("policy_id", payload.ID), slog.String("policy_name", payload.Name)) return } pd.Datasets = map[string]bool{payload.DatasetID: true} @@ -118,29 +124,36 @@ func (a *policyManager) ManagePolicy(payload config.PolicyPayload) { } if !backend.HaveBackend(payload.Backend) { - a.logger.Warn("policy failed to apply because backend is not available", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name)) + a.logger.Warn("policy failed to apply because backend is not available", slog.String("policy_id", payload.ID), slog.String("policy_name", payload.Name)) pd.State = policies.FailedToApply pd.BackendErr = "backend not available" } else { // attempt to apply the policy to the backend. status of policy application (running/failed) is maintained there. be := backend.GetBackend(payload.Backend) + newPayload, err := a.secrets.SolvePolicySecrets(payload) + if err != nil { + a.logger.Error("failed to solve secrets", slog.String("policy_id", payload.ID), slog.String("policy_name", payload.Name), slog.Any("error", err)) + return + } + pd.Data = newPayload.Data a.applyPolicy(payload, be, &pd, updatePolicy) + pd.Data = payload.Data } // save policy (with latest status) to local policy db err := a.repo.Update(pd) if err != nil { - a.logger.Error("got error in update last status", zap.Error(err)) + a.logger.Error("got error in update last status", slog.Any("error", err)) return } return case "remove": err := a.RemovePolicy(payload.ID, payload.Name, payload.Backend) if err != nil { - a.logger.Error("policy failed to be removed", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name), zap.Error(err)) + a.logger.Error("policy failed to be removed", slog.String("policy_id", payload.ID), slog.String("policy_name", payload.Name), slog.Any("error", err)) } return default: - a.logger.Error("unknown policy action, ignored", zap.String("action", payload.Action)) + a.logger.Error("unknown policy action, ignored", slog.String("action", payload.Action)) } } @@ -155,7 +168,7 @@ func (a *policyManager) RemovePolicy(policyID string, policyName string, beName be := backend.GetBackend(beName) err := be.RemovePolicy(pd) if err != nil { - a.logger.Error("backend remove policy failed: will still remove from PolicyManager", zap.String("policy_id", policyID), zap.Error(err)) + a.logger.Error("backend remove policy failed: will still remove from PolicyManager", slog.String("policy_id", policyID), slog.Any("error", err)) } // Remove policy from orb-agent local repo err = a.repo.Remove(pd.ID) @@ -168,24 +181,24 @@ func (a *policyManager) RemovePolicy(policyID string, policyName string, beName func (a *policyManager) RemovePolicyDataset(policyID string, datasetID string, be backend.Backend) { policyData, err := a.repo.Get(policyID) if err != nil { - a.logger.Warn("failed to retrieve policy data", zap.String("policy_id", policyID), zap.String("policy_name", policyData.Name), zap.Error(err)) + a.logger.Warn("failed to retrieve policy data", slog.String("policy_id", policyID), slog.String("policy_name", policyData.Name), slog.Any("error", err)) return } removePolicy, err := a.repo.RemoveDataset(policyID, datasetID) if err != nil { - a.logger.Warn("failed to remove policy dataset", zap.String("dataset_id", datasetID), zap.String("policy_name", policyData.Name), zap.Error(err)) + a.logger.Warn("failed to remove policy dataset", slog.String("dataset_id", datasetID), slog.String("policy_name", policyData.Name), slog.Any("error", err)) return } if removePolicy { // Remove policy via http request err := be.RemovePolicy(policyData) if err != nil { - a.logger.Warn("policy failed to remove", zap.String("policy_id", policyID), zap.String("policy_name", policyData.Name), zap.Error(err)) + a.logger.Warn("policy failed to remove", slog.String("policy_id", policyID), slog.String("policy_name", policyData.Name), slog.Any("error", err)) } // Remove policy from orb-agent local repo err = a.repo.Remove(policyData.ID) if err != nil { - a.logger.Warn("policy failed to remove local", zap.String("policy_id", policyData.ID), zap.String("policy_name", policyData.Name), zap.Error(err)) + a.logger.Warn("policy failed to remove local", slog.String("policy_id", policyData.ID), slog.String("policy_name", policyData.Name), slog.Any("error", err)) } } } @@ -193,7 +206,7 @@ func (a *policyManager) RemovePolicyDataset(policyID string, datasetID string, b func (a *policyManager) applyPolicy(payload config.PolicyPayload, be backend.Backend, pd *policies.PolicyData, updatePolicy bool) { err := be.ApplyPolicy(*pd, updatePolicy) if err != nil { - a.logger.Warn("policy failed to apply", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name), zap.Error(err)) + a.logger.Warn("policy failed to apply", slog.String("policy_id", payload.ID), slog.String("policy_name", payload.Name), slog.Any("error", err)) switch { case strings.Contains(err.Error(), "422"): pd.State = policies.NoTapMatch @@ -202,7 +215,7 @@ func (a *policyManager) applyPolicy(payload config.PolicyPayload, be backend.Bac } pd.BackendErr = err.Error() } else { - a.logger.Info("policy applied successfully", zap.String("policy_id", payload.ID), zap.String("policy_name", payload.Name)) + a.logger.Info("policy applied successfully", slog.String("policy_id", payload.ID), slog.String("policy_name", payload.Name)) pd.State = policies.Running pd.BackendErr = "" } @@ -211,14 +224,14 @@ func (a *policyManager) applyPolicy(payload config.PolicyPayload, be backend.Bac func (a *policyManager) RemoveBackendPolicies(be backend.Backend, permanently bool) error { plcies, err := a.repo.GetAll() if err != nil { - a.logger.Error("failed to retrieve list of policies", zap.Error(err)) + a.logger.Error("failed to retrieve list of policies", slog.Any("error", err)) return err } for _, plcy := range plcies { err := be.RemovePolicy(plcy) if err != nil { - a.logger.Error("failed to remove policy from backend", zap.String("policy_id", plcy.ID), zap.String("policy_name", plcy.Name), zap.Error(err)) + a.logger.Error("failed to remove policy from backend", slog.String("policy_id", plcy.ID), slog.String("policy_name", plcy.Name), slog.Any("error", err)) // note we continue here: even if the backend failed to remove, we update our policy repo to remove it } if permanently { @@ -240,18 +253,18 @@ func (a *policyManager) RemoveBackendPolicies(be backend.Backend, permanently bo func (a *policyManager) ApplyBackendPolicies(be backend.Backend) error { plcies, err := a.repo.GetAll() if err != nil { - a.logger.Error("failed to retrieve list of policies", zap.Error(err)) + a.logger.Error("failed to retrieve list of policies", slog.Any("error", err)) return err } for _, policy := range plcies { err := be.ApplyPolicy(policy, false) if err != nil { - a.logger.Warn("policy failed to apply", zap.String("policy_id", policy.ID), zap.String("policy_name", policy.Name), zap.Error(err)) + a.logger.Warn("policy failed to apply", slog.String("policy_id", policy.ID), slog.String("policy_name", policy.Name), slog.Any("error", err)) policy.State = policies.FailedToApply policy.BackendErr = err.Error() } else { - a.logger.Info("policy applied successfully", zap.String("policy_id", policy.ID), zap.String("policy_name", policy.Name)) + a.logger.Info("policy applied successfully", slog.String("policy_id", policy.ID), slog.String("policy_name", policy.Name)) policy.State = policies.Running policy.BackendErr = "" } @@ -262,3 +275,43 @@ func (a *policyManager) ApplyBackendPolicies(be backend.Backend) error { } return nil } + +func (a *policyManager) policiesChanged(policiesIDs map[string]bool) { + for id, valid := range policiesIDs { + policy, err := a.repo.Get(id) + if err != nil { + a.logger.Error("failed to get policy", slog.Any("error", err)) + continue + } + if !valid { + if err := a.RemovePolicy(policy.ID, policy.Name, policy.Backend); err != nil { + a.logger.Error("failed to remove policy", slog.Any("error", err)) + } + continue + } + if !backend.HaveBackend(policy.Backend) { + a.logger.Warn("policy failed to apply because backend is not available", slog.String("policy_id", policy.ID), slog.String("policy_name", policy.Name)) + policy.State = policies.FailedToApply + policy.BackendErr = "backend not available" + } else { + payload := config.PolicyPayload{ + ID: policy.ID, + Name: policy.Name, + Data: policy.Data, + } + newPayload, err := a.secrets.SolvePolicySecrets(payload) + if err != nil { + a.logger.Error("failed to solve secrets", slog.String("policy_id", policy.ID), slog.String("policy_name", policy.Name), slog.Any("error", err)) + continue + } + policy.Data = newPayload.Data + be := backend.GetBackend(policy.Backend) + a.applyPolicy(payload, be, &policy, true) + policy.Data = payload.Data + } + + if err = a.repo.Update(policy); err != nil { + a.logger.Error("got error in update last status", slog.Any("error", err)) + } + } +} diff --git a/agent/policymgr/manager_test.go b/agent/policymgr/manager_test.go new file mode 100644 index 00000000..8e464eec --- /dev/null +++ b/agent/policymgr/manager_test.go @@ -0,0 +1,679 @@ +package policymgr_test + +import ( + "context" + "errors" + "fmt" + "log/slog" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/netboxlabs/orb-agent/agent/backend" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/policies" + "github.com/netboxlabs/orb-agent/agent/policymgr" +) + +// Mock for the backend.Backend interface +type mockBackend struct { + mock.Mock + name string +} + +func (m *mockBackend) GetName() string { + return m.name +} + +func (m *mockBackend) Configure(logger *slog.Logger, repo policies.PolicyRepo, cfg map[string]any, commons config.BackendCommons) error { + args := m.Called(logger, repo, cfg, commons) + return args.Error(0) +} + +func (m *mockBackend) Version() (string, error) { + args := m.Called() + return args.String(0), args.Error(1) +} + +func (m *mockBackend) Start(ctx context.Context, cancelFunc context.CancelFunc) error { + args := m.Called(ctx, cancelFunc) + return args.Error(0) +} + +func (m *mockBackend) Stop(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func (m *mockBackend) FullReset(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func (m *mockBackend) GetStartTime() time.Time { + args := m.Called() + return args.Get(0).(time.Time) +} + +func (m *mockBackend) GetCapabilities() (map[string]any, error) { + args := m.Called() + return args.Get(0).(map[string]any), args.Error(1) +} + +func (m *mockBackend) GetRunningStatus() (backend.RunningStatus, string, error) { + args := m.Called() + return args.Get(0).(backend.RunningStatus), args.String(1), args.Error(2) +} + +func (m *mockBackend) GetInitialState() backend.RunningStatus { + args := m.Called() + return args.Get(0).(backend.RunningStatus) +} + +func (m *mockBackend) ApplyPolicy(policy policies.PolicyData, updatePolicy bool) error { + args := m.Called(policy, updatePolicy) + return args.Error(0) +} + +func (m *mockBackend) RemovePolicy(policy policies.PolicyData) error { + args := m.Called(policy) + return args.Error(0) +} + +// Mock for the secretsmgr.Manager interface +type mockSecretsManager struct { + mock.Mock + callbacks []func(map[string]bool) +} + +func (m *mockSecretsManager) SolvePolicySecrets(payload config.PolicyPayload) (config.PolicyPayload, error) { + args := m.Called(payload) + return args.Get(0).(config.PolicyPayload), args.Error(1) +} + +func (m *mockSecretsManager) SolveConfigSecrets(backends map[string]any, configManager config.ManagerConfig) (map[string]any, config.ManagerConfig, error) { + args := m.Called(backends, configManager) + return args.Get(0).(map[string]any), args.Get(1).(config.ManagerConfig), args.Error(2) +} + +func (m *mockSecretsManager) RegisterUpdatePoliciesCallback(callback func(map[string]bool)) { + m.callbacks = append(m.callbacks, callback) +} + +func (m *mockSecretsManager) Start(context.Context) error { + args := m.Called() + return args.Error(0) +} + +func (m *mockSecretsManager) TriggerCallbacks(policiesIDs map[string]bool) { + for _, callback := range m.callbacks { + callback(policiesIDs) + } +} + +func TestNew(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + require.NotNil(t, mgr) + + // Verify the repo was properly initialized + repo := mgr.GetRepo() + require.NotNil(t, repo) +} + +func TestManagePolicy_ManageAction_NewPolicy(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + // Configure mock backend + mockBe := &mockBackend{name: "testbackend"} + backend.Register("testbackend", mockBe) + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + // Set up expectations + policyData := map[string]any{"key": "value"} + payload := config.PolicyPayload{ + Action: "manage", + ID: "policy1", + Name: "Test Policy", + Backend: "testbackend", + Version: 1, + Data: policyData, + DatasetID: "dataset1", + AgentGroupID: "group1", + } + + solvedPayload := payload + secretsMgr.On("SolvePolicySecrets", payload).Return(solvedPayload, nil) + + expectedPolicy := policies.PolicyData{ + ID: "policy1", + Name: "Test Policy", + Backend: "testbackend", + Version: 1, + Data: policyData, + State: policies.Running, + Datasets: map[string]bool{"dataset1": true}, + GroupIDs: map[string]bool{"group1": true}, + } + + mockBe.On("ApplyPolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == expectedPolicy.ID && pd.Name == expectedPolicy.Name + }), false).Return(nil) + + // Execute + mgr.ManagePolicy(payload) + + // Validate + mockBe.AssertExpectations(t) + secretsMgr.AssertExpectations(t) + + // Verify policy is in repo with correct state + state, err := mgr.GetPolicyState() + require.NoError(t, err) + require.Len(t, state, 1) + assert.Equal(t, expectedPolicy.ID, state[0].ID) + assert.Equal(t, expectedPolicy.Name, state[0].Name) + assert.Equal(t, policies.Running, state[0].State) + assert.Equal(t, expectedPolicy.Datasets, state[0].Datasets) + assert.Equal(t, expectedPolicy.GroupIDs, state[0].GroupIDs) +} + +func TestManagePolicy_ManageAction_UpdatePolicy(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + // Configure mock backend + mockBe := &mockBackend{name: "testbackend"} + backend.Register("testbackend", mockBe) + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + // First add a policy + initialPayload := config.PolicyPayload{ + Action: "manage", + ID: "policy1", + Name: "Test Policy", + Backend: "testbackend", + Version: 1, + Data: map[string]any{"key": "value"}, + DatasetID: "dataset1", + } + + secretsMgr.On("SolvePolicySecrets", initialPayload).Return(initialPayload, nil) + mockBe.On("ApplyPolicy", mock.Anything, false).Return(nil) + + // Initial setup + mgr.ManagePolicy(initialPayload) + + // Now update the policy with a higher version + updatePayload := config.PolicyPayload{ + Action: "manage", + ID: "policy1", + Name: "Updated Policy", // Changed name + Backend: "testbackend", + Version: 2, // Increased version + Data: map[string]any{"key": "updated"}, + DatasetID: "dataset2", // Additional dataset + } + + secretsMgr.On("SolvePolicySecrets", updatePayload).Return(updatePayload, nil) + mockBe.On("ApplyPolicy", mock.Anything, true).Return(nil) + + // Execute the update + mgr.ManagePolicy(updatePayload) + + // Validate state after update + state, err := mgr.GetPolicyState() + require.NoError(t, err) + require.Len(t, state, 1) + + // Check updated properties + assert.Equal(t, "policy1", state[0].ID) + assert.Equal(t, "Updated Policy", state[0].Name) + assert.Equal(t, int32(2), state[0].Version) + assert.Equal(t, policies.Running, state[0].State) + + // Check that both datasets are there + assert.True(t, state[0].Datasets["dataset1"]) + assert.True(t, state[0].Datasets["dataset2"]) + + // Check for PreviousPolicyData with original name + require.NotNil(t, state[0].PreviousPolicyData) + assert.Equal(t, "Test Policy", state[0].PreviousPolicyData.Name) +} + +func TestManagePolicy_ManageAction_BackendUnavailable(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + // Don't register any backends + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + payload := config.PolicyPayload{ + Action: "manage", + ID: "policy1", + Name: "Test Policy", + Backend: "unavailablebackend", // This backend doesn't exist + Version: 1, + Data: map[string]any{"key": "value"}, + DatasetID: "dataset1", + } + + // Execute + mgr.ManagePolicy(payload) + + // Validate state - policy should be saved but marked as failed + state, err := mgr.GetPolicyState() + require.NoError(t, err) + require.Len(t, state, 1) + assert.Equal(t, "policy1", state[0].ID) + assert.Equal(t, policies.FailedToApply, state[0].State) + assert.Equal(t, "backend not available", state[0].BackendErr) +} + +func TestManagePolicy_RemoveAction(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + mockBe := &mockBackend{name: "testbackend"} + backend.Register("testbackend", mockBe) + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + // First add a policy + initialPayload := config.PolicyPayload{ + Action: "manage", + ID: "policy1", + Name: "Test Policy", + Backend: "testbackend", + Version: 1, + Data: map[string]any{"key": "value"}, + DatasetID: "dataset1", + } + + secretsMgr.On("SolvePolicySecrets", initialPayload).Return(initialPayload, nil) + mockBe.On("ApplyPolicy", mock.Anything, false).Return(nil) + + // Initial setup + mgr.ManagePolicy(initialPayload) + + // Now remove the policy + removePayload := config.PolicyPayload{ + Action: "remove", + ID: "policy1", + Name: "Test Policy", + Backend: "testbackend", + } + + mockBe.On("RemovePolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == "policy1" && pd.Name == "Test Policy" + })).Return(nil) + + // Execute remove + mgr.ManagePolicy(removePayload) + + // Validate - policy should be removed + state, err := mgr.GetPolicyState() + require.NoError(t, err) + assert.Empty(t, state) +} + +func TestRemovePolicy(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + mockBe := &mockBackend{name: "testbackend"} + backend.Register("testbackend", mockBe) + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + // Add a policy to the repo first (we need to use ManagePolicy) + addPayload := config.PolicyPayload{ + Action: "manage", + ID: "policy1", + Name: "Test Policy", + Backend: "testbackend", + Version: 1, + Data: map[string]any{}, + DatasetID: "dataset1", + } + secretsMgr.On("SolvePolicySecrets", addPayload).Return(addPayload, nil) + mockBe.On("ApplyPolicy", mock.Anything, false).Return(nil) + mgr.ManagePolicy(addPayload) + + // Set up expectations for removal + mockBe.On("RemovePolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == "policy1" && pd.Name == "Test Policy" + })).Return(nil) + + // Execute removal + err = mgr.RemovePolicy("policy1", "Test Policy", "testbackend") + require.NoError(t, err) + + // Verify policy is gone + state, err := mgr.GetPolicyState() + require.NoError(t, err) + assert.Empty(t, state) +} + +func TestRemoveBackendPolicies(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + mockBe := &mockBackend{name: "testbackend"} + backend.Register("testbackend", mockBe) + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + // Add multiple policies + for i := 1; i <= 3; i++ { + payload := config.PolicyPayload{ + Action: "manage", + ID: fmt.Sprintf("policy%d", i), + Name: fmt.Sprintf("Test Policy %d", i), + Backend: "testbackend", + Version: int32(i), + Data: map[string]any{}, + DatasetID: fmt.Sprintf("dataset%d", i), + } + secretsMgr.On("SolvePolicySecrets", payload).Return(payload, nil) + mockBe.On("ApplyPolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == payload.ID + }), false).Return(nil) + + mgr.ManagePolicy(payload) + } + + // Verify we have 3 policies + state, err := mgr.GetPolicyState() + require.NoError(t, err) + assert.Len(t, state, 3) + + // Set up expectations for removal - all policies should be removed + mockBe.On("RemovePolicy", mock.Anything).Return(nil).Times(3) + + // Test non-permanent removal (only marks policies as unknown) + err = mgr.RemoveBackendPolicies(mockBe, false) + require.NoError(t, err) + + // Verify policies are still there but with state Unknown + state, err = mgr.GetPolicyState() + require.NoError(t, err) + assert.Len(t, state, 3) + for _, policy := range state { + assert.Equal(t, policies.Unknown, policy.State) + } + + // Test permanent removal + mockBe.On("RemovePolicy", mock.Anything).Return(nil).Times(3) + err = mgr.RemoveBackendPolicies(mockBe, true) + require.NoError(t, err) + + // Verify policies are gone + state, err = mgr.GetPolicyState() + require.NoError(t, err) + assert.Empty(t, state) +} + +func TestApplyBackendPolicies(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + mockBe := &mockBackend{name: "testbackend"} + backend.Register("testbackend", mockBe) + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + // First add policies but mark them as unknown (simulating a backend restart) + repo := mgr.GetRepo() + for i := 1; i <= 3; i++ { + policy := policies.PolicyData{ + ID: fmt.Sprintf("policy%d", i), + Name: fmt.Sprintf("Test Policy %d", i), + Backend: "testbackend", + Version: int32(i), + Data: map[string]any{}, + State: policies.Unknown, + Datasets: map[string]bool{fmt.Sprintf("dataset%d", i): true}, + } + err := repo.Update(policy) + require.NoError(t, err) + } + + // Set up expectations for applying policies + mockBe.On("ApplyPolicy", mock.Anything, false).Return(nil).Times(3) + + // Execute + err = mgr.ApplyBackendPolicies(mockBe) + require.NoError(t, err) + + // Verify policies are now running + state, err := mgr.GetPolicyState() + require.NoError(t, err) + assert.Len(t, state, 3) + for _, policy := range state { + assert.Equal(t, policies.Running, policy.State) + assert.Empty(t, policy.BackendErr) + } + + // Test error case - one policy fails to apply + repo = mgr.GetRepo() // Get fresh repo + for i := 1; i <= 3; i++ { + policy := policies.PolicyData{ + ID: fmt.Sprintf("policy%d", i), + Name: fmt.Sprintf("Test Policy %d", i), + Backend: "testbackend", + Version: int32(i), + Data: map[string]any{}, + State: policies.Unknown, + Datasets: map[string]bool{fmt.Sprintf("dataset%d", i): true}, + } + err := repo.Update(policy) + require.NoError(t, err) + } + + // Make policy2 fail + mockBe.ExpectedCalls = nil + mockBe.On("ApplyPolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == "policy1" || pd.ID == "policy3" + }), false).Return(nil).Times(2) + mockBe.On("ApplyPolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == "policy2" + }), false).Return(errors.New("failed to apply")).Once() + + // Execute + err = mgr.ApplyBackendPolicies(mockBe) + require.NoError(t, err) // Function should not return error even if some policies fail + + // Verify status + state, err = mgr.GetPolicyState() + require.NoError(t, err) + assert.Len(t, state, 3) + + for _, policy := range state { + if policy.ID == "policy2" { + assert.Equal(t, policies.FailedToApply, policy.State) + assert.Equal(t, "failed to apply", policy.BackendErr) + } else { + assert.Equal(t, policies.Running, policy.State) + assert.Empty(t, policy.BackendErr) + } + } +} + +func TestPoliciesChanged(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + mockBe := &mockBackend{name: "testbackend"} + backend.Register("testbackend", mockBe) + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + // First add some policies + for i := 1; i <= 3; i++ { + payload := config.PolicyPayload{ + Action: "manage", + ID: fmt.Sprintf("policy%d", i), + Name: fmt.Sprintf("Test Policy %d", i), + Backend: "testbackend", + Version: int32(i), + Data: map[string]any{"original": "data"}, + DatasetID: fmt.Sprintf("dataset%d", i), + } + secretsMgr.On("SolvePolicySecrets", mock.MatchedBy(func(pd config.PolicyPayload) bool { + return pd.ID == payload.ID + })).Return(payload, nil) + mockBe.On("ApplyPolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == payload.ID + }), mock.Anything).Return(nil) + + mgr.ManagePolicy(payload) + } + + // Set up for policy update callback + // Scenario: policy1 unchanged, policy2 updated, policy3 removed + policiesStatus := map[string]bool{ + "policy1": true, // Valid - no change + "policy2": true, // Valid - will be updated + "policy3": false, // Invalid - will be removed + } + // policy3 not included, meaning it should be removed + + // Setup expectations for policy2 update + solvedPayload := config.PolicyPayload{ + ID: "policy2", + Name: "Test Policy 2", + Data: map[string]any{"updated": "data"}, + } + secretsMgr.On("SolvePolicySecrets", mock.MatchedBy(func(payload config.PolicyPayload) bool { + return payload.ID == "policy2" + })).Return(solvedPayload, nil) + + mockBe.On("ApplyPolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == "policy2" && pd.Data.(map[string]any)["updated"] == "data" + }), true).Return(nil) + + // Setup expectations for policy3 removal + mockBe.On("RemovePolicy", mock.MatchedBy(func(pd policies.PolicyData) bool { + return pd.ID == "policy3" + })).Return(nil) + + // Trigger the secret update callback + secretsMgr.TriggerCallbacks(policiesStatus) + + // Verify final state + state, err := mgr.GetPolicyState() + require.NoError(t, err) + + // We should have 2 policies left + stateMap := make(map[string]policies.PolicyData) + for _, p := range state { + stateMap[p.ID] = p + } + + assert.Len(t, stateMap, 2) + assert.Contains(t, stateMap, "policy1") + assert.Contains(t, stateMap, "policy2") + assert.NotContains(t, stateMap, "policy3") + + // Verify policy2 was not updated in DB + updatedPolicy := stateMap["policy2"] + updatedData := updatedPolicy.Data.(map[string]any) + assert.NotEqual(t, "data", updatedData["updated"]) +} + +func TestRemovePolicyDataset(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + secretsMgr := new(mockSecretsManager) + cfg := config.Config{} + + mockBe := &mockBackend{name: "testbackend"} + backend.Register("testbackend", mockBe) + + mgr, err := policymgr.New(logger, secretsMgr, cfg) + require.NoError(t, err) + + // First add a policy with multiple datasets + payload := config.PolicyPayload{ + Action: "manage", + ID: "policy1", + Name: "Test Policy", + Backend: "testbackend", + Version: 1, + Data: map[string]any{}, + DatasetID: "dataset1", + } + secretsMgr.On("SolvePolicySecrets", payload).Return(payload, nil) + mockBe.On("ApplyPolicy", mock.Anything, false).Return(nil) + mgr.ManagePolicy(payload) + + // Add another dataset to the policy + payload2 := config.PolicyPayload{ + Action: "manage", + ID: "policy1", // Same policy ID + Name: "Test Policy", + Backend: "testbackend", + Version: 1, + Data: map[string]any{}, + DatasetID: "dataset2", // Different dataset + } + secretsMgr.On("SolvePolicySecrets", payload2).Return(payload2, nil) + mockBe.On("ApplyPolicy", mock.Anything, true).Return(nil) + mgr.ManagePolicy(payload2) + + // Verify policy has both datasets + state, err := mgr.GetPolicyState() + require.NoError(t, err) + require.Len(t, state, 1) + assert.Len(t, state[0].Datasets, 2) + assert.True(t, state[0].Datasets["dataset1"]) + assert.True(t, state[0].Datasets["dataset2"]) + + // Test removing one dataset + mgr.RemovePolicyDataset("policy1", "dataset1", mockBe) + + // Verify policy still exists but with only one dataset + state, err = mgr.GetPolicyState() + require.NoError(t, err) + require.Len(t, state, 1) + assert.Len(t, state[0].Datasets, 1) + assert.False(t, state[0].Datasets["dataset1"]) + assert.True(t, state[0].Datasets["dataset2"]) + + // Test removing the last dataset - should remove the policy + mockBe.On("RemovePolicy", mock.Anything).Return(nil) + mgr.RemovePolicyDataset("policy1", "dataset2", mockBe) + + // Verify policy is gone + state, err = mgr.GetPolicyState() + require.NoError(t, err) + assert.Empty(t, state) +} diff --git a/agent/secretsmgr/manager.go b/agent/secretsmgr/manager.go new file mode 100644 index 00000000..ce55ea5a --- /dev/null +++ b/agent/secretsmgr/manager.go @@ -0,0 +1,46 @@ +package secretsmgr + +import ( + "context" + "log/slog" + + "github.com/netboxlabs/orb-agent/agent/config" +) + +// Manager is an interface for managing secrets +type Manager interface { + Start(ctx context.Context) error + RegisterUpdatePoliciesCallback(callback func(map[string]bool)) + SolvePolicySecrets(payload config.PolicyPayload) (config.PolicyPayload, error) + SolveConfigSecrets(backends map[string]any, configManager config.ManagerConfig) (map[string]any, config.ManagerConfig, error) +} + +// New creates a new instance of ConfigManager based on the configuration +func New(logger *slog.Logger, c config.ManagerSecrets) Manager { + switch c.Active { + case "vault": + return &vaultManager{logger: logger, config: c.Sources.Vault} + default: + logger.Info("no secrets manager specified or invalid type, skipping") + return &dummyManager{} + } +} + +var _ Manager = (*dummyManager)(nil) + +type dummyManager struct{} + +func (v *dummyManager) Start(_ context.Context) error { + return nil +} + +func (v *dummyManager) RegisterUpdatePoliciesCallback(_ func(map[string]bool)) { +} + +func (v *dummyManager) SolvePolicySecrets(payload config.PolicyPayload) (config.PolicyPayload, error) { + return payload, nil +} + +func (v *dummyManager) SolveConfigSecrets(backends map[string]any, configManager config.ManagerConfig) (map[string]any, config.ManagerConfig, error) { + return backends, configManager, nil +} diff --git a/agent/secretsmgr/vault.go b/agent/secretsmgr/vault.go new file mode 100644 index 00000000..f15bceda --- /dev/null +++ b/agent/secretsmgr/vault.go @@ -0,0 +1,349 @@ +package secretsmgr + +import ( + "context" + "fmt" + "log/slog" + "regexp" + "strings" + "time" + + "github.com/go-co-op/gocron/v2" + vault "github.com/hashicorp/vault/api" + "gopkg.in/yaml.v3" + + "github.com/netboxlabs/orb-agent/agent/config" +) + +var _ Manager = (*vaultManager)(nil) + +type vaultManager struct { + logger *slog.Logger + config config.VaultManager + ctx context.Context + client *vault.Client + usedVars map[string]cachedSecret + callback func(map[string]bool) + auth authMethod + token *vault.Secret + scheduler gocron.Scheduler +} + +type cachedSecret struct { + Value string // The actual secret value + policyIDs map[string]bool // The IDs of policies that have used this secret +} + +func (v *vaultManager) Start(ctx context.Context) error { + v.ctx = ctx + v.usedVars = make(map[string]cachedSecret) + + config := vault.DefaultConfig() + + config.Address = v.config.Address + if v.config.Timeout == nil || *v.config.Timeout == 0 { + config.Timeout = 60 * time.Second + } else { + config.Timeout = time.Duration(*v.config.Timeout) * time.Second + } + + if v.config.Auth == "" { + return fmt.Errorf("no auth method specified") + } + + var err error + v.client, err = vault.NewClient(config) + if err != nil { + return err + } + + if v.config.Namespace != "" { + v.client.SetNamespace(v.config.Namespace) + } + + v.auth, err = newAuthentication(v.config.Auth, v.config.AuthArgs) + if err != nil { + return err + } + + v.token, err = v.auth.vaultAuthenticate(ctx, v.client) + if err != nil { + return err + } + + if v.config.Schedule != nil { + s, err := gocron.NewScheduler() + if err != nil { + return fmt.Errorf("failed to create scheduler: %w", err) + } + + v.scheduler = s + task := gocron.NewTask(v.pollSecrets) + + if _, err = v.scheduler.NewJob(gocron.CronJob(*v.config.Schedule, false), task, + gocron.WithSingletonMode(gocron.LimitModeReschedule)); err != nil { + return fmt.Errorf("failed to create polling job: %w", err) + } + + v.logger.Info("Starting vault secret polling", slog.String("cron interval", *v.config.Schedule)) + v.scheduler.Start() + } + + if err = v.addTokenLifecycleWatcher(); err != nil { + return err + } + + return nil +} + +// RegisterUpdatePoliciesCallback registers a callback function to be called when secrets are updated +func (v *vaultManager) RegisterUpdatePoliciesCallback(callback func(map[string]bool)) { + v.callback = callback +} + +// SolvePolicySecrets processes a policy payload and replaces vault references with environment variables +func (v *vaultManager) SolvePolicySecrets(payload config.PolicyPayload) (config.PolicyPayload, error) { + // Create a copy of the payload + newPayload := payload + + // Process the Data field + processedData, err := v.processValue(payload.Data, payload.ID) + if err != nil { + return payload, err + } + + newPayload.Data = processedData + return newPayload, nil +} + +func (v *vaultManager) pollSecrets() { + if len(v.usedVars) == 0 || v.callback == nil { + return + } + + v.logger.Debug("Polling vault secrets for changes", slog.Int("secretCount", len(v.usedVars))) + changedPolicyIDs := make(map[string]bool) + + // Check each cached secret + for path, cachedSecret := range v.usedVars { + currentValue, err := v.getSecret(path) + if err != nil { + v.logger.Error("Failed to retrieve secret during polling", slog.String("path", path), slog.Any("error", err)) + for id := range cachedSecret.policyIDs { + changedPolicyIDs[id] = false + } + continue + } + + if currentValue != cachedSecret.Value { + v.logger.Info("Detected changed secret", slog.String("path", path)) + cachedSecret.Value = currentValue + v.usedVars[path] = cachedSecret + for id := range cachedSecret.policyIDs { + changedPolicyIDs[id] = true + } + } + } + + if len(changedPolicyIDs) > 0 { + v.logger.Info("Calling update callback for changed secrets", slog.Int("policyCount", len(changedPolicyIDs))) + v.callback(changedPolicyIDs) + } +} + +// SolveConfigSecrets processes the configuration secrets and replaces vault references with environment variables +func (v *vaultManager) SolveConfigSecrets(backends map[string]any, configManager config.ManagerConfig) (map[string]any, config.ManagerConfig, error) { + // Create a copy of the backends + newBackends := backends + processedBackends, err := v.processValue(newBackends, "_backends") + if err != nil { + return backends, configManager, fmt.Errorf("failed to process backends: %w", err) + } + newBackends, ok := processedBackends.(map[string]any) + if !ok { + return backends, configManager, fmt.Errorf("failed to cast processed backends to map[string]any") + } + + // Convert configManager to map[string]any + configManagerMap, err := structToMap(configManager) + if err != nil { + return backends, configManager, fmt.Errorf("failed to convert config manager to map: %w", err) + } + // Process the config manager map + processedConfigManagerMap, err := v.processValue(configManagerMap, "_config_manager") + if err != nil { + return backends, configManager, fmt.Errorf("failed to process config manager: %w", err) + } + newConfigManager, err := mapToStruct[config.ManagerConfig](processedConfigManagerMap) + if err != nil { + return backends, configManager, fmt.Errorf("failed to convert processed map to config manager: %w", err) + } + + // Do not track updates on config vars for now + v.usedVars = make(map[string]cachedSecret) + + // Process the backends and config manager + return newBackends, newConfigManager, nil +} + +// structToMap converts a struct to a map[string]any +func structToMap(input any) (map[string]any, error) { + data, err := yaml.Marshal(input) + if err != nil { + return nil, err + } + var result map[string]any + if err := yaml.Unmarshal(data, &result); err != nil { + return nil, err + } + return result, nil +} + +// mapToStruct converts a map[string]any to a struct of type T +func mapToStruct[T any](input any) (T, error) { + data, err := yaml.Marshal(input) + if err != nil { + return *new(T), err + } + var result T + if err := yaml.Unmarshal(data, &result); err != nil { + return *new(T), err + } + return result, nil +} + +func (v *vaultManager) addTokenLifecycleWatcher() error { + if v.token == nil || v.token.Auth == nil || + !v.token.Auth.Renewable || v.token.Auth.LeaseDuration == 0 { + return nil + } + + lw, err := v.client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{ + Secret: v.token, + RenewBehavior: vault.RenewBehaviorIgnoreErrors, + }) + if err != nil { + return err + } + + go lw.Start() + + go func() { + for { + select { + case <-v.ctx.Done(): + lw.Stop() + return + + case err := <-lw.DoneCh(): + if err != nil { + v.logger.Error("Token renewal failed", slog.Any("error", err)) + } + case output := <-lw.RenewCh(): + v.logger.Info("Token renewed", slog.Time("renewedAt", output.RenewedAt)) + } + } + }() + + return nil +} + +func (v *vaultManager) processValue(value any, id string) (any, error) { + switch val := value.(type) { + case string: + return v.processString(val, id) + case map[string]any: + return v.processMap(val, id) + case []any: + return v.processSlice(val, id) + default: + return val, nil + } +} + +// processString processes a string and replaces vault references +func (v *vaultManager) processString(s string, id string) (string, error) { + re := regexp.MustCompile(`\${vault://([^}]+)}`) + if !re.MatchString(s) { + return s, nil + } + + match := re.FindStringSubmatchIndex(s) + if len(match) < 4 { + return "", fmt.Errorf("failed to find vault reference in string: %s", s) + } + + vaultPath := s[match[2]:match[3]] + + if secrets, exists := v.usedVars[vaultPath]; exists { + secrets.policyIDs[id] = true + v.usedVars[vaultPath] = secrets + return secrets.Value, nil + } + + secret, err := v.getSecret(vaultPath) + if err != nil { + return "", err + } + + v.usedVars[vaultPath] = cachedSecret{ + Value: secret, + policyIDs: map[string]bool{id: true}, + } + + return secret, nil +} + +// processMap processes a map recursively and replaces vault references in its values +func (v *vaultManager) processMap(m map[string]any, id string) (map[string]any, error) { + result := make(map[string]any) + for key, val := range m { + processedVal, err := v.processValue(val, id) + if err != nil { + return nil, fmt.Errorf("failed to process value for key %s: %w", key, err) + } + result[key] = processedVal + } + return result, nil +} + +// processSlice processes a slice recursively and replaces vault references in its elements +func (v *vaultManager) processSlice(s []any, id string) ([]any, error) { + result := make([]any, len(s)) + for i, val := range s { + processedVal, err := v.processValue(val, id) + if err != nil { + return nil, fmt.Errorf("failed to process value at index %d: %w", i, err) + } + result[i] = processedVal + } + return result, nil +} + +// getSecret retrieves a secret from the vault +func (v *vaultManager) getSecret(path string) (string, error) { + // Split the path by forward slashes + parts := strings.Split(path, "/") + if len(parts) < 3 { + return "", fmt.Errorf("invalid vault path format: %s", path) + } + secret, err := v.client.KVv2(parts[0]).Get(v.ctx, strings.Join(parts[1:len(parts)-1], "/")) + if err != nil { + return "", fmt.Errorf("failed to get secret path %s: %w", path, err) + } + if secret == nil || secret.Data == nil { + return "", fmt.Errorf("secret not found: %s", path) + } + value, ok := secret.Data[parts[len(parts)-1]] + if !ok { + return "", fmt.Errorf("secret not found: %s", path) + } + strValue, ok := value.(string) + if !ok { + return "", fmt.Errorf("secret is not a string: %s", path) + } + if strValue == "" { + return "", fmt.Errorf("secret is empty: %s", path) + } + return strValue, nil +} diff --git a/agent/secretsmgr/vault_auth.go b/agent/secretsmgr/vault_auth.go new file mode 100644 index 00000000..7254eccb --- /dev/null +++ b/agent/secretsmgr/vault_auth.go @@ -0,0 +1,262 @@ +package secretsmgr + +import ( + "context" + "fmt" + + vault "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/api/auth/approle" + "github.com/hashicorp/vault/api/auth/kubernetes" + "github.com/hashicorp/vault/api/auth/ldap" + "github.com/hashicorp/vault/api/auth/userpass" + "gopkg.in/yaml.v3" + + "github.com/netboxlabs/orb-agent/agent/config" +) + +type authMethod interface { + vaultAuthenticate(context.Context, *vault.Client) (*vault.Secret, error) +} + +func newAuthentication(auth string, authArgs map[string]any) (authMethod, error) { + var authObj authMethod + + switch auth { + case "token": + authObj = &AuthToken{} + case "approle": + authObj = &AuthAppRole{} + case "userpass": + authObj = &AuthUserPass{} + case "kubernetes": + authObj = &AuthKubernetes{} + case "ldap": + authObj = &AuthLDAP{} + case "aws", "azure", "gcp": + return nil, fmt.Errorf("auth method %s is not currently implemented", auth) + default: + return nil, fmt.Errorf("unsupported auth method: %s", auth) + } + + if err := config.ResolveEnvInMap(authArgs); err != nil { + return nil, fmt.Errorf("failed to resolve env in auth_args: %w", err) + } + + // Convert the map to YAML + yamlData, err := yaml.Marshal(authArgs) + if err != nil { + return nil, fmt.Errorf("failed to marshal auth_args: %w", err) + } + + // Unmarshal YAML into the auth structure + if err := yaml.Unmarshal(yamlData, authObj); err != nil { + return nil, fmt.Errorf("failed to unmarshal '%s' auth_args: %w", auth, err) + } + + return authObj, nil +} + +// AuthToken authenticates against Vault with a token. +type AuthToken struct { + Token string `yaml:"token"` +} + +// UnmarshalYAML for AuthToken validates required fields after unmarshaling +func (a *AuthToken) UnmarshalYAML(value *yaml.Node) error { + type tempAuthToken AuthToken + temp := tempAuthToken{} + if err := value.Decode(&temp); err != nil { + return err + } + *a = AuthToken(temp) + if a.Token == "" { + return fmt.Errorf("missing required field 'token'") + } + return nil +} + +func (a *AuthToken) vaultAuthenticate(_ context.Context, cli *vault.Client) (*vault.Secret, error) { + cli.SetToken(a.Token) + secret, err := cli.Auth().Token().LookupSelf() + if err != nil { + return nil, err + } + return secret, nil +} + +// AuthAppRole authenticates against Vault with AppRole. +type AuthAppRole struct { + RoleID string `yaml:"role_id"` + SecretID string `yaml:"secret_id"` + WrappingToken bool `yaml:"wrapping_token,ommitempty"` + MountPath *string `yaml:"mount_path,ommitempty"` +} + +// UnmarshalYAML for AuthAppRole validates required fields after unmarshaling +func (a *AuthAppRole) UnmarshalYAML(value *yaml.Node) error { + type tempAuthAppRole AuthAppRole + temp := tempAuthAppRole{} + if err := value.Decode(&temp); err != nil { + return err + } + *a = AuthAppRole(temp) + if a.RoleID == "" { + return fmt.Errorf("missing required field 'role_id'") + } + if a.SecretID == "" { + return fmt.Errorf("missing required field 'secret_id'") + } + return nil +} + +func (a *AuthAppRole) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) { + secret := &approle.SecretID{FromString: string(a.SecretID)} + + var opts []approle.LoginOption + if a.WrappingToken { + opts = append(opts, approle.WithWrappingToken()) + } + if a.MountPath != nil && *a.MountPath != "" { + opts = append(opts, approle.WithMountPath(*a.MountPath)) + } + + auth, err := approle.NewAppRoleAuth(a.RoleID, secret, opts...) + if err != nil { + return nil, fmt.Errorf("auth.approle: %w", err) + } + s, err := cli.Auth().Login(ctx, auth) + if err != nil { + return nil, fmt.Errorf("auth.approle: %w", err) + } + return s, nil +} + +// AuthUserPass authenticates against Vault with Userpass. +type AuthUserPass struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + MountPath string `yaml:"mount_path"` +} + +// UnmarshalYAML for AuthUserPass validates required fields after unmarshaling +func (a *AuthUserPass) UnmarshalYAML(value *yaml.Node) error { + type tempAuthUserPass AuthUserPass + temp := tempAuthUserPass{} + if err := value.Decode(&temp); err != nil { + return err + } + *a = AuthUserPass(temp) + if a.Username == "" { + return fmt.Errorf("missing required field 'username'") + } + if a.Password == "" { + return fmt.Errorf("missing required field 'password'") + } + return nil +} + +func (a *AuthUserPass) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) { + secret := &userpass.Password{FromString: string(a.Password)} + + var opts []userpass.LoginOption + + if a.MountPath != "" { + opts = append(opts, userpass.WithMountPath(a.MountPath)) + } + + auth, err := userpass.NewUserpassAuth(a.Username, secret, opts...) + if err != nil { + return nil, fmt.Errorf("auth.userpass: %w", err) + } + s, err := cli.Auth().Login(ctx, auth) + if err != nil { + return nil, fmt.Errorf("auth.userpass: %w", err) + } + return s, nil +} + +// AuthKubernetes authenticates against Vault with Kubernetes. +type AuthKubernetes struct { + Role string `yaml:"role"` + ServiceAccountTokenFile string `yaml:"service_account_file"` + MountPath string `yaml:"mount_path"` +} + +// UnmarshalYAML for AuthKubernetes validates required fields after unmarshaling +func (a *AuthKubernetes) UnmarshalYAML(value *yaml.Node) error { + type tempAuthKubernetes AuthKubernetes + temp := tempAuthKubernetes{} + if err := value.Decode(&temp); err != nil { + return err + } + *a = AuthKubernetes(temp) + if a.Role == "" { + return fmt.Errorf("missing required field 'role'") + } + return nil +} + +func (a *AuthKubernetes) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) { + var opts []kubernetes.LoginOption + + if a.ServiceAccountTokenFile != "" { + opts = append(opts, kubernetes.WithServiceAccountTokenPath(a.ServiceAccountTokenFile)) + } + if a.MountPath != "" { + opts = append(opts, kubernetes.WithMountPath(a.MountPath)) + } + + auth, err := kubernetes.NewKubernetesAuth(a.Role, opts...) + if err != nil { + return nil, fmt.Errorf("auth.kubernetes: %w", err) + } + s, err := cli.Auth().Login(ctx, auth) + if err != nil { + return nil, fmt.Errorf("auth.kubernetes: %w", err) + } + return s, nil +} + +// AuthLDAP authenticates against Vault with LDAP. +type AuthLDAP struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + MountPath string `yaml:"mount_path"` +} + +// UnmarshalYAML for AuthLDAP validates required fields after unmarshaling +func (a *AuthLDAP) UnmarshalYAML(value *yaml.Node) error { + type tempAuthLDAP AuthLDAP + temp := tempAuthLDAP{} + if err := value.Decode(&temp); err != nil { + return err + } + *a = AuthLDAP(temp) + if a.Username == "" { + return fmt.Errorf("missing required field 'username'") + } + if a.Password == "" { + return fmt.Errorf("missing required field 'password'") + } + return nil +} + +func (a *AuthLDAP) vaultAuthenticate(ctx context.Context, cli *vault.Client) (*vault.Secret, error) { + secret := &ldap.Password{FromString: string(a.Password)} + + var opts []ldap.LoginOption + + if a.MountPath != "" { + opts = append(opts, ldap.WithMountPath(a.MountPath)) + } + + auth, err := ldap.NewLDAPAuth(a.Username, secret, opts...) + if err != nil { + return nil, fmt.Errorf("auth.ldap: %w", err) + } + s, err := cli.Auth().Login(ctx, auth) + if err != nil { + return nil, fmt.Errorf("auth.ldap: %w", err) + } + return s, nil +} diff --git a/agent/secretsmgr/vault_auth_test.go b/agent/secretsmgr/vault_auth_test.go new file mode 100644 index 00000000..0a9ceed9 --- /dev/null +++ b/agent/secretsmgr/vault_auth_test.go @@ -0,0 +1,71 @@ +package secretsmgr + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewAuthentication(t *testing.T) { + tests := []struct { + name string + method string + args map[string]any + expectErr bool + }{ + { + name: "token auth with valid args", + method: "token", + args: map[string]any{"token": "test-token"}, + expectErr: false, + }, + { + name: "token auth without token", + method: "token", + args: map[string]any{}, + expectErr: true, + }, + { + name: "unsupported auth method", + method: "unsupported", + args: map[string]any{}, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + auth, err := newAuthentication(tt.method, tt.args) + + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, auth) + } else { + assert.NoError(t, err) + assert.NotNil(t, auth) + } + }) + } +} + +func TestTokenAuth_Authenticate(t *testing.T) { + // Create test vault server + ln, client := createTestVault(t) + defer func() { + if err := ln.Close(); err != nil { + assert.NoError(t, err, "Failed to close test vault listener") + } + }() + + ctx := context.Background() + + // Test with invalid token + auth := &AuthToken{Token: "invalid-token"} + client.SetToken("invalid-token") + + // This should fail + secret, err := auth.vaultAuthenticate(ctx, client) + assert.Error(t, err) + assert.Nil(t, secret) +} diff --git a/agent/secretsmgr/vault_test.go b/agent/secretsmgr/vault_test.go new file mode 100644 index 00000000..880ef1f8 --- /dev/null +++ b/agent/secretsmgr/vault_test.go @@ -0,0 +1,768 @@ +package secretsmgr + +import ( + "context" + "fmt" + "log/slog" + "net" + "os" + "strings" + "testing" + "time" + + vault "github.com/hashicorp/vault/api" + vaulthttp "github.com/hashicorp/vault/http" + vaultsrv "github.com/hashicorp/vault/vault" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netboxlabs/orb-agent/agent/config" +) + +func TestVaultManager_getSecret(t *testing.T) { + // Create test vault server + ln, client := createTestVault(t) + defer func() { + if err := ln.Close(); err != nil { + assert.NoError(t, err, "Failed to close test vault listener") + } + }() + + // Create the vault manager with the test client + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + vm := &vaultManager{ + logger: logger, + config: config.VaultManager{}, + ctx: ctx, + client: client, + } + + tests := []struct { + name string + path string + expectedValue string + expectedError string + }{ + { + name: "valid path and secret", + path: "testsecret/app/credentials/password", + expectedValue: "secretvalue", + expectedError: "", + }, + { + name: "invalid path format", + path: "testsecret/password", + expectedValue: "", + expectedError: "invalid vault path format: testsecret/password", + }, + { + name: "secret not found", + path: "testsecret/nonexistent/path/key", + expectedValue: "", + expectedError: "failed to get secret path testsecret/nonexistent/path/key:", + }, + { + name: "key not found in data", + path: "testsecret/app/credentials/nonexistentkey", + expectedValue: "", + expectedError: "secret not found: testsecret/app/credentials/nonexistentkey", + }, + { + name: "non-string value", + path: "testsecret/app/credentials/numeric", + expectedValue: "", + expectedError: "secret is not a string: testsecret/app/credentials/numeric", + }, + { + name: "empty string value", + path: "testsecret/app/credentials/empty", + expectedValue: "", + expectedError: "secret is empty: testsecret/app/credentials/empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Call the method under test + value, err := vm.getSecret(tt.path) + + // Assertions + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, value) + } + }) + } +} + +func createTestVault(t *testing.T) (net.Listener, *vault.Client) { + t.Helper() + + // Create an in-memory, unsealed core + core, keyShares, rootToken := vaultsrv.TestCoreUnsealed(t) + _ = keyShares + + // Start an HTTP server for the core + ln, addr := vaulthttp.TestServer(t, core) + + // Create a client that talks to the server + conf := vault.DefaultConfig() + conf.Address = addr + + client, err := vault.NewClient(conf) + if err != nil { + t.Fatal(err) + } + client.SetToken(rootToken) + + // Enable KV v2 secret engine + mountInput := &vault.MountInput{ + Type: "kv", + Options: map[string]string{"version": "2"}, + } + err = client.Sys().Mount("testsecret", mountInput) + if err != nil { + t.Fatal(err) + } + + // Wait for KV v2 to become available + time.Sleep(100 * time.Millisecond) + + // Setup various test secrets + secrets := map[string]map[string]any{ + "app/credentials": { + "password": "secretvalue", + "numeric": 12345, + "empty": "", + }, + } + + for path, data := range secrets { + _, err = client.KVv2("testsecret").Put(context.Background(), path, data) + require.NoError(t, err, "Failed to set up secret at %s", path) + } + + return ln, client +} + +func TestVaultManager_processString(t *testing.T) { + // Create test vault server + ln, client := createTestVault(t) + defer func() { + if err := ln.Close(); err != nil { + assert.NoError(t, err, "Failed to close test vault listener") + } + }() + // Create the vault manager with the test client + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + vm := &vaultManager{ + logger: logger, + config: config.VaultManager{}, + ctx: ctx, + client: client, + usedVars: make(map[string]cachedSecret), + } + + tests := []struct { + name string + input string + policyID string + expectedValue string + expectError bool + }{ + { + name: "no vault reference", + input: "plain text", + policyID: "policy1", + expectedValue: "plain text", + expectError: false, + }, + { + name: "valid vault reference", + input: "${vault://testsecret/app/credentials/password}", + policyID: "policy1", + expectedValue: "secretvalue", + expectError: false, + }, + { + name: "invalid vault path", + input: "${vault://testsecret/nonexistent/key}", + policyID: "policy1", + expectedValue: "", + expectError: true, + }, + { + name: "malformed reference", + input: "${vault:/malformed}", + policyID: "policy1", + expectedValue: "${vault:/malformed}", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := vm.processString(tt.input, tt.policyID) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + + // For valid vault references, verify they're cached properly + if strings.Contains(tt.input, "${vault://") && !tt.expectError { + path := strings.TrimPrefix(strings.TrimSuffix(tt.input, "}"), "${vault://") + cached, exists := vm.usedVars[path] + assert.True(t, exists, "Secret should be cached") + assert.Equal(t, tt.expectedValue, cached.Value) + assert.True(t, cached.policyIDs[tt.policyID]) + } + } + }) + } +} + +func TestVaultManager_processMap(t *testing.T) { + // Create test vault server + ln, client := createTestVault(t) + defer func() { + if err := ln.Close(); err != nil { + assert.NoError(t, err, "Failed to close test vault listener") + } + }() + + // Create the vault manager with the test client + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + vm := &vaultManager{ + logger: logger, + config: config.VaultManager{}, + ctx: ctx, + client: client, + usedVars: make(map[string]cachedSecret), + } + + tests := []struct { + name string + input map[string]any + policyID string + expected map[string]any + expectError bool + }{ + { + name: "map with no vault references", + input: map[string]any{ + "key1": "value1", + "key2": "value2", + }, + policyID: "policy1", + expected: map[string]any{ + "key1": "value1", + "key2": "value2", + }, + expectError: false, + }, + { + name: "map with vault references", + input: map[string]any{ + "key1": "value1", + "key2": "${vault://testsecret/app/credentials/password}", + }, + policyID: "policy1", + expected: map[string]any{ + "key1": "value1", + "key2": "secretvalue", + }, + expectError: false, + }, + { + name: "map with invalid vault reference", + input: map[string]any{ + "key1": "value1", + "key2": "${vault://testsecret/nonexistent/key}", + }, + policyID: "policy1", + expected: nil, + expectError: true, + }, + { + name: "nested map", + input: map[string]any{ + "key1": "value1", + "nested": map[string]any{ + "subkey": "${vault://testsecret/app/credentials/password}", + }, + }, + policyID: "policy1", + expected: map[string]any{ + "key1": "value1", + "nested": map[string]any{ + "subkey": "secretvalue", + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := vm.processMap(tt.input, tt.policyID) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestVaultManager_processSlice(t *testing.T) { + // Create test vault server + ln, client := createTestVault(t) + defer func() { + if err := ln.Close(); err != nil { + assert.NoError(t, err, "Failed to close test vault listener") + } + }() + + // Create the vault manager with the test client + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + vm := &vaultManager{ + logger: logger, + config: config.VaultManager{}, + ctx: ctx, + client: client, + usedVars: make(map[string]cachedSecret), + } + + tests := []struct { + name string + input []any + policyID string + expected []any + expectError bool + }{ + { + name: "slice with no vault references", + input: []any{"value1", "value2", 123}, + policyID: "policy1", + expected: []any{"value1", "value2", 123}, + }, + { + name: "slice with vault references", + input: []any{"value1", "${vault://testsecret/app/credentials/password}", 123}, + policyID: "policy1", + expected: []any{"value1", "secretvalue", 123}, + }, + { + name: "slice with invalid vault reference", + input: []any{"value1", "${vault://testsecret/nonexistent/key}"}, + policyID: "policy1", + expectError: true, + }, + { + name: "slice with nested map", + input: []any{ + "value1", + map[string]any{ + "subkey": "${vault://testsecret/app/credentials/password}", + }, + }, + policyID: "policy1", + expected: []any{ + "value1", + map[string]any{ + "subkey": "secretvalue", + }, + }, + }, + { + name: "slice with nested slice", + input: []any{ + "value1", + []any{"nested1", "${vault://testsecret/app/credentials/password}"}, + }, + policyID: "policy1", + expected: []any{ + "value1", + []any{"nested1", "secretvalue"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := vm.processSlice(tt.input, tt.policyID) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestVaultManager_SolvePolicySecrets(t *testing.T) { + // Create test vault server + ln, client := createTestVault(t) + defer func() { + if err := ln.Close(); err != nil { + assert.NoError(t, err, "Failed to close test vault listener") + } + }() + + // Create the vault manager with the test client + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + vm := &vaultManager{ + logger: logger, + config: config.VaultManager{}, + ctx: ctx, + client: client, + usedVars: make(map[string]cachedSecret), + } + + tests := []struct { + name string + payload config.PolicyPayload + expected config.PolicyPayload + expectError bool + }{ + { + name: "payload with no secrets", + payload: config.PolicyPayload{ + ID: "policy1", + Name: "test-policy", + Data: map[string]any{ + "key1": "value1", + "key2": "value2", + }, + }, + expected: config.PolicyPayload{ + ID: "policy1", + Name: "test-policy", + Data: map[string]any{ + "key1": "value1", + "key2": "value2", + }, + }, + expectError: false, + }, + { + name: "payload with vault references", + payload: config.PolicyPayload{ + ID: "policy2", + Name: "test-policy-secrets", + Data: map[string]any{ + "key1": "value1", + "key2": "${vault://testsecret/app/credentials/password}", + "nested": map[string]any{ + "subkey": "${vault://testsecret/app/credentials/password}", + }, + }, + }, + expected: config.PolicyPayload{ + ID: "policy2", + Name: "test-policy-secrets", + Data: map[string]any{ + "key1": "value1", + "key2": "secretvalue", + "nested": map[string]any{ + "subkey": "secretvalue", + }, + }, + }, + expectError: false, + }, + { + name: "payload with invalid vault reference", + payload: config.PolicyPayload{ + ID: "policy3", + Name: "test-policy-invalid", + Data: map[string]any{ + "key1": "value1", + "key2": "${vault://testsecret/nonexistent/key}", + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := vm.SolvePolicySecrets(tt.payload) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestVaultManager_RegisterUpdatePoliciesCallback(t *testing.T) { + // Create the vault manager + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + vm := &vaultManager{ + logger: logger, + config: config.VaultManager{}, + } + + // Test registering a callback + called := false + callback := func(_ map[string]bool) { + called = true + } + + vm.RegisterUpdatePoliciesCallback(callback) + assert.NotNil(t, vm.callback) + + // Manually call the callback to verify it works + vm.callback(map[string]bool{"policy1": true}) + assert.True(t, called) +} + +func TestVaultManager_pollSecrets(t *testing.T) { + // Create test vault server + ln, client := createTestVault(t) + defer func() { + if err := ln.Close(); err != nil { + assert.NoError(t, err, "Failed to close test vault listener") + } + }() + + // Create the vault manager with the test client + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var callbackCalled bool + var callbackPolicyIDs map[string]bool + + callback := func(policyIDs map[string]bool) { + callbackCalled = true + callbackPolicyIDs = policyIDs + } + + vm := &vaultManager{ + logger: logger, + config: config.VaultManager{}, + ctx: ctx, + client: client, + usedVars: make(map[string]cachedSecret), + callback: callback, + } + + // Setup initial secret state + vm.usedVars["testsecret/app/credentials/password"] = cachedSecret{ + Value: "secretvalue", + policyIDs: map[string]bool{"policy1": true, "policy2": true}, + } + + // First poll with no changes + callbackCalled = false + callbackPolicyIDs = nil + vm.pollSecrets() + assert.False(t, callbackCalled) + + // Update the secret in vault + _, err := client.KVv2("testsecret").Put(ctx, "app/credentials", map[string]any{ + "password": "newsecretvalue", + "numeric": 12345, + "empty": "", + }) + require.NoError(t, err) + + // Poll again - should detect the change + callbackCalled = false + callbackPolicyIDs = nil + vm.pollSecrets() + + assert.True(t, callbackCalled) + assert.NotNil(t, callbackPolicyIDs) + assert.Contains(t, callbackPolicyIDs, "policy1") + assert.Contains(t, callbackPolicyIDs, "policy2") + assert.True(t, callbackPolicyIDs["policy1"]) + assert.True(t, callbackPolicyIDs["policy2"]) + + // Verify the cached value was updated + assert.Equal(t, "newsecretvalue", vm.usedVars["testsecret/app/credentials/password"].Value) +} + +func TestVaultManager_Start(t *testing.T) { + // Create test vault server + ln, client := createTestVault(t) + defer func() { + if err := ln.Close(); err != nil { + assert.NoError(t, err, "Failed to close test vault listener") + } + }() + + addr := ln.Addr().String() + token := client.Token() + + tests := []struct { + name string + config config.VaultManager + expectError bool + }{ + { + name: "valid config with token auth", + config: config.VaultManager{ + Address: "http://" + addr, + Auth: "token", + AuthArgs: map[string]any{ + "token": token, + }, + }, + expectError: false, + }, + { + name: "missing auth method", + config: config.VaultManager{ + Address: "http://" + addr, + }, + expectError: true, + }, + { + name: "invalid auth method", + config: config.VaultManager{ + Address: "http://" + addr, + Auth: "invalid-method", + AuthArgs: map[string]any{ + "token": token, + }, + }, + expectError: true, + }, + { + name: "with timeout", + config: config.VaultManager{ + Address: "http://" + addr, + Auth: "token", + AuthArgs: map[string]any{ + "token": token, + }, + Timeout: func() *int { i := 10; return &i }(), + }, + expectError: false, + }, + { + name: "with namespace", + config: config.VaultManager{ + Address: "http://" + addr, + Auth: "token", + Namespace: "test-namespace", + AuthArgs: map[string]any{ + "token": token, + }, + }, + expectError: false, + }, + { + name: "with schedule", + config: config.VaultManager{ + Address: "http://" + addr, + Auth: "token", + AuthArgs: map[string]any{ + "token": token, + }, + Schedule: func() *string { s := "*/5 * * * *"; return &s }(), + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + vm := &vaultManager{ + logger: logger, + config: tt.config, + } + + ctx, cancel := context.WithCancel(context.Background()) + err := vm.Start(ctx) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, vm.client) + assert.NotNil(t, vm.token) + assert.NotNil(t, vm.usedVars) + + if tt.config.Schedule != nil { + assert.NotNil(t, vm.scheduler) + err = vm.scheduler.Shutdown() + assert.NoError(t, err) + } + } + + cancel() + }) + } +} + +func TestNew(t *testing.T) { + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + + tests := []struct { + name string + config config.ManagerSecrets + expectedType string + }{ + { + name: "vault manager", + config: config.ManagerSecrets{ + Active: "vault", + Sources: config.SecretsSources{ + Vault: config.VaultManager{ + Address: "http://localhost:8200", + }, + }, + }, + expectedType: "*secretsmgr.vaultManager", + }, + { + name: "dummy manager when active is empty", + config: config.ManagerSecrets{ + Active: "", + }, + expectedType: "*secretsmgr.dummyManager", + }, + { + name: "dummy manager when active is invalid", + config: config.ManagerSecrets{ + Active: "invalid", + }, + expectedType: "*secretsmgr.dummyManager", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := New(logger, tt.config) + assert.NotNil(t, manager) + assert.Equal(t, tt.expectedType, fmt.Sprintf("%T", manager)) + }) + } +} diff --git a/cmd/agent.example.yaml b/cmd/agent.example.yaml deleted file mode 100644 index c0d36321..00000000 --- a/cmd/agent.example.yaml +++ /dev/null @@ -1,44 +0,0 @@ -version: "1.0" - -# this section is used by pktvisor -# see https://github.com/orb-community/pktvisor/blob/develop/RFCs/2021-04-16-75-taps.md -visor: - taps: - default_pcap: - input_type: pcap - config: - iface: "auto" - -# this section is used orb-agent -# most sections and keys are optional -orb: - # these are arbitrary key value pairs used for dynamically define a group of agents by matching against agent group labels - labels: - region: EU - pop: ams02 - node_type: dns - cloud: - config: - # optionally specify an agent name to use during auto provisioning - # hostname will be used if it's not specified here - agent_name: my-agent1 - auto_provision: true - api: - address: https://api.orb.live - # if auto provisioning, specify API token here (or pass on the command line) - token: TOKEN - mqtt: - address: tls://agents.orb.live:8883 - # if not auto provisioning, specify agent connection details here -# id: "f420a133-7651-412d-852a-6141fafeaea5" -# key: "14ae65ae-092f-4fdc-be6a-0cfb378119dc" -# channel_id: "9610b0a4-b05f-46e5-a32d-000d8a2ec1fd" -# tls: -# verify: true -# db: -# file: "/usr/local/orb/orb-agent.db" - backends: - pktvisor: - binary: "/usr/local/sbin/pktvisord" - # this example assumes the file is saved as agent.yaml. If your file has another name, you must replace it with the proper name - config_file: "/opt/orb/agent.yaml" diff --git a/cmd/e2e_agent_test.go b/cmd/e2e_agent_test.go deleted file mode 100644 index c835c174..00000000 --- a/cmd/e2e_agent_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/signal" - "syscall" - "testing" - "time" - - "github.com/pkg/profile" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - - "github.com/netboxlabs/orb-agent/agent" - "github.com/netboxlabs/orb-agent/agent/config" -) - -func Test_e2e_orbAgent_ConfigFile(t *testing.T) { - t.Skip("local run only, skip in CICD") - defer profile.Start().Stop() - rootCmd := &cobra.Command{ - Use: "orb-agent", - } - - runCmd := &cobra.Command{ - Use: "run", - Short: "Run orb-agent and connect to Orb control plane", - Long: `Run orb-agent and connect to Orb control plane`, - Run: Run, - } - - runCmd.Flags().StringSliceVarP(&cfgFiles, "config", "c", []string{}, "Path to config files (may be specified multiple times)") - runCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Enable verbose (debug level) output") - - rootCmd.AddCommand(runCmd) - rootCmd.SetArgs([]string{"run", "-d", "-c", "/home/lpegoraro/workspace/orb/localconfig/config.yaml"}) - ctx, cancelF := context.WithTimeout(context.Background(), 2*time.Minute) - err := rootCmd.ExecuteContext(ctx) - if err != nil { - t.Fail() - } - - <-ctx.Done() - cancelF() -} - -func Test_main(t *testing.T) { - t.Skip("local run only, skip in CICD") - - mergeOrError("/home/lpegoraro/workspace/orb/localconfig/config.yaml") - - // configuration - var cfg config.Config - err := viper.Unmarshal(&cfg) - if err != nil { - cobra.CheckErr(fmt.Errorf("agent start up error (config): %w", err)) - os.Exit(1) - } - - cfg.OrbAgent.Debug.Enable = true - - // logger - var logger *zap.Logger - atomicLevel := zap.NewAtomicLevel() - if debug { - atomicLevel.SetLevel(zap.DebugLevel) - } else { - atomicLevel.SetLevel(zap.InfoLevel) - } - encoderCfg := zap.NewProductionEncoderConfig() - encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder - core := zapcore.NewCore( - zapcore.NewJSONEncoder(encoderCfg), - os.Stdout, - atomicLevel, - ) - logger = zap.New(core, zap.AddCaller()) - defer func(logger *zap.Logger) { - _ = logger.Sync() - }(logger) - - // new agent - a, err := agent.New(logger, cfg) - if err != nil { - logger.Error("agent start up error", zap.Error(err)) - os.Exit(1) - } - - // handle signals - done := make(chan bool, 1) - rootCtx, cancelFunc := context.WithTimeout(context.WithValue(context.Background(), routineKey, "mainRoutine"), 15*time.Minute) - - go func() { - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - select { - case <-sigs: - logger.Warn("stop signal received stopping agent") - a.Stop(rootCtx) - cancelFunc() - case <-rootCtx.Done(): - logger.Warn("mainRoutine context cancelled") - done <- true - return - } - }() - - // start agent - err = a.Start(rootCtx, cancelFunc) - if err != nil { - logger.Error("agent startup error", zap.Error(err)) - os.Exit(1) - } - - <-done -} diff --git a/cmd/main.go b/cmd/main.go index edabe9b1..d1554a54 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,15 +3,13 @@ package main import ( "context" "fmt" + "log/slog" "os" "os/signal" - "strings" "syscall" "github.com/spf13/cobra" - "github.com/spf13/viper" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" + "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent" "github.com/netboxlabs/orb-agent/agent/backend/devicediscovery" @@ -47,39 +45,60 @@ func Version(_ *cobra.Command, _ []string) { os.Exit(0) } -// Run starts the agent -func Run(_ *cobra.Command, _ []string) { - initConfig() +func loadConfig(path string, configData *config.Config) error { + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("failed to open config file: %w", err) + } + defer func() { + if err := file.Close(); err != nil { + cobra.CheckErr(fmt.Errorf("failed to close config file: %w", err)) + } + }() - // configuration + decoder := yaml.NewDecoder(file) + if err := decoder.Decode(configData); err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + return nil +} + +func initConfig() config.Config { var configData config.Config - err := viper.Unmarshal(&configData) - if err != nil { - cobra.CheckErr(fmt.Errorf("agent start up error (configData): %w", err)) - os.Exit(1) + + // Load default config + if _, err := os.Stat(defaultConfig); err == nil { + if err := loadConfig(defaultConfig, &configData); err != nil { + cobra.CheckErr(fmt.Errorf("error loading default config: %w", err)) + } + } + + // Override with user-provided config files + for _, conf := range cfgFiles { + if err := loadConfig(conf, &configData); err != nil { + cobra.CheckErr(fmt.Errorf("error loading config file %s: %w", conf, err)) + } } + return configData +} + +// Run starts the agent +func Run(_ *cobra.Command, _ []string) { + configData := initConfig() + // logger - var logger *zap.Logger - atomicLevel := zap.NewAtomicLevel() + var l slog.Level if debug { - atomicLevel.SetLevel(zap.DebugLevel) + l = slog.LevelDebug } else { - atomicLevel.SetLevel(zap.InfoLevel) + l = slog.LevelInfo } - encoderCfg := zap.NewProductionEncoderConfig() - encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder - core := zapcore.NewCore( - zapcore.NewJSONEncoder(encoderCfg), - os.Stdout, - atomicLevel, - ) - logger = zap.New(core, zap.AddCaller()) - defer func(logger *zap.Logger) { - _ = logger.Sync() - }(logger) - - logger.Info("backends loaded", zap.Any("backends", configData.OrbAgent.Backends)) + h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: l, AddSource: false}) + logger := slog.New(h) + + logger.Info("backends loaded", slog.Any("backends", configData.OrbAgent.Backends)) configData.OrbAgent.ConfigFile = defaultConfig if len(cfgFiles) > 0 { @@ -89,7 +108,7 @@ func Run(_ *cobra.Command, _ []string) { // new agent a, err := agent.New(logger, configData) if err != nil { - logger.Error("agent start up error", zap.Error(err)) + logger.Error("agent start up error", slog.Any("error", err)) os.Exit(1) } @@ -115,79 +134,13 @@ func Run(_ *cobra.Command, _ []string) { // start agent err = a.Start(rootCtx, cancelFunc) if err != nil { - logger.Error("agent startup error", zap.Error(err)) + logger.Error("agent startup error", slog.Any("error", err)) os.Exit(1) } <-done } -func mergeOrError(path string) { - v := viper.New() - if len(path) > 0 { - v.SetConfigFile(path) - v.SetConfigType("yaml") - } - - v.AutomaticEnv() - replacer := strings.NewReplacer(".", "_") - v.SetEnvKeyReplacer(replacer) - - if len(path) > 0 { - cobra.CheckErr(v.ReadInConfig()) - } - - var fZero float64 - - // check that version of config files are all matched up - if versionNumber1 := viper.GetFloat64("version"); versionNumber1 != fZero { - versionNumber2 := v.GetFloat64("version") - if versionNumber2 == fZero { - cobra.CheckErr("Failed to parse config version in: " + path) - } - if versionNumber2 != versionNumber1 { - cobra.CheckErr("Config file version mismatch in: " + path) - } - } - - // load backend static functions for setting up default values - backendVarsFunction := make(map[string]func(*viper.Viper)) - backendVarsFunction["pktvisor"] = pktvisor.RegisterBackendSpecificVariables - backendVarsFunction["otel"] = otel.RegisterBackendSpecificVariables - backendVarsFunction["device_discovery"] = devicediscovery.RegisterBackendSpecificVariables - backendVarsFunction["network_discovery"] = networkdiscovery.RegisterBackendSpecificVariables - backendVarsFunction["worker"] = worker.RegisterBackendSpecificVariables - - // check if backends are configured - // if not then add pktvisor as default - if len(path) > 0 && len(v.GetStringMap("orb.backends")) == 0 { - pktvisor.RegisterBackendSpecificVariables(v) - } else { - for backendName := range v.GetStringMap("orb.backends") { - if backend := v.GetStringMap("orb.backends." + backendName); backend != nil && backendName != "common" { - backendVarsFunction[backendName](v) - } - } - } - - cobra.CheckErr(viper.MergeConfigMap(v.AllSettings())) -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - // set defaults first - mergeOrError("") - if len(cfgFiles) == 0 { - if _, err := os.Stat(defaultConfig); !os.IsNotExist(err) { - mergeOrError(defaultConfig) - } - } else { - for _, conf := range cfgFiles { - mergeOrError(conf) - } - } -} - func main() { rootCmd := &cobra.Command{ Use: "orb-agent", diff --git a/docs/backends/device_discovery.md b/docs/backends/device_discovery.md index da6a7fcf..7992a5a7 100644 --- a/docs/backends/device_discovery.md +++ b/docs/backends/device_discovery.md @@ -1,6 +1,21 @@ # Device Discovery The device discovery backend leverages [NAPALM](https://napalm.readthedocs.io/en/latest/index.html) to connect to network devices and collect network information. +## Diode Entities +The device discovery backend uses [Diode Python SDK](https://github.com/netboxlabs/diode-sdk-python) to ingest the following entities: + +* [Device](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#device) +* [Interface](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#interface) +* [Device Type](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#device-type) +* [Platform](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#platform) +* [Manufacturer](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#manufacturer) +* [Site](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#site) +* [Role](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#role) +* [IP Address](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#ip-address) +* [Prefix](https://github.com/netboxlabs/diode-sdk-python/blob/develop/docs/entities.md#prefix) +* [VLAN](https://github.com/netboxlabs/diode-sdk-python/blob/develop/README.md#supported-entities-object-types) + +Interfaces are attached to the device and ip addresses will be attached to the interfaces. Prefixes are added to the same interface site that it belongs to. ## Configuration The `device_discovery` backend does not require any special configuration, though overriding `host` and `port` values can be specified. The backend will use the `diode` settings specified in the `common` subsection to forward discovery results. @@ -12,7 +27,8 @@ orb: common: diode: target: grpc://192.168.0.100:8080/diode - api_key: ${DIODE_API_KEY} + client_id: ${DIODE_CLIENT_ID} + client_secret: ${DIODE_CLIENT_SECRET} agent_name: agent01 device_discovery: host: 192.168.5.11 # default 0.0.0.0 @@ -34,9 +50,47 @@ Config defines data for the whole scope and is optional overall. #### Defaults Current supported defaults: -| Key | Description | -|:-----:|:-------------:| -| site | NetBox Site Name | +| Key | Type | Description | +|:-----:|:----:|:-------------:| +| site | str | NetBox Site Name (defaults to 'undefined' if not specified) | +| role | str | Device role (e.g., switch) (defaults to 'undefined' if not specified) | +| if_type | str | Interface Type (defaults to 'other' if not specified) | +| description | str | General description | +| comments | str | General comments | +| tags | list | List of tags | + +##### Nested Defaults + +| Key | Type | Description | +|-------------|------|---------------------------------| +| device | map | Device-specific defaults | +| ├─ description | str | Device description | +| ├─ comments | str | Device comments | +| ├─ tags | list | Device tags | +| interface | map | Interface-specific defaults | +| ├─ description | str | Interface description | +| ├─ tags | list | Interface tags | +| ipaddress | map | IP address-specific defaults | +| ├─ role | str | IP address role | +| ├─ tenant | str | IP address tenant | +| ├─ vrf | str | IP address vrf | +| ├─ description | str | IP address description | +| ├─ comments | str | IP address comments | +| ├─ tags | list | IP address tags | +| prefix | map | Prefix-specific defaults | +| ├─ role | str | Prefix role | +| ├─ tenant | str | Prefix tenant | +| ├─ vrf | str | Prefix vrf | +| ├─ description | str | Prefix description | +| ├─ comments | str | Prefix comments | +| ├─ tags | list | Prefix tags | +| vlan | map | VLAN-specific defaults | +| ├─ group | str | VLAN group | +| ├─ tenant | str | VLAN tenant | +| ├─ role | str | VLAN role | +| ├─ description | str | VLAN description | +| ├─ comments | str | VLAN comments | +| ├─ tags | list | VLAN tags | ### Scope The scope defines a list of devices that can be accessed and pulled data. @@ -63,6 +117,27 @@ orb: schedule: "* * * * *" defaults: site: New York NY + role: switch + description: for all + comments: comment all + tags: [tag1, tag2] + device: + description: device description + comments: this device + tags: [tag3, tag4] + interface: + description: interface description + tags: [tag5] + ipaddress: + description: my ip + comments: my comment + tags: [tag6] + prefix: + description: + comments: + tags: [tag7] + vlan: + role: role scope: - driver: ios hostname: 192.168.0.5 diff --git a/docs/backends/network_discovery.md b/docs/backends/network_discovery.md index b4e301b2..b112bd73 100644 --- a/docs/backends/network_discovery.md +++ b/docs/backends/network_discovery.md @@ -1,6 +1,8 @@ # Network Discovery The network discovery backend leverages [NMAP](https://nmap.org/) to scan networks and discover IP information. +## Diode Entities +The network discovery backend uses [Diode Go SDK](https://github.com/netboxlabs/diode-sdk-go) to ingest discover IP Address entities with Global VRF and allows defining Description, Comments and Tags for them. ## Configuration The `network_discovery` backend does not require any special configuration, though overriding `host` and `port` values can be specified. The backend will use the `diode` settings specified in the `common` subsection to forward discovery results. @@ -12,7 +14,8 @@ orb: common: diode: target: grpc://192.168.0.100:8080/diode - api_key: ${DIODE_API_KEY} + client_id: ${DIODE_CLIENT_ID} + client_secret: ${DIODE_CLIENT_SECRET} agent_name: agent01 network_discovery: host: 192.168.5.11 # default 0.0.0.0 @@ -37,10 +40,11 @@ Config defines data for the whole scope and is optional overall. #### Defaults Current supported defaults: -| Key | Description | -|:-----:|:-------------:| -| comments | NetBox Comments information to be added to discovered IP | -| description | NetBox Description data to be added to discovered IP | +| Key | Type | Description | +|:-----:|:----:|:-------------:| +| comments | str | NetBox Comments information to be added to discovered IP | +| description | str | NetBox Description data to be added to discovered IP | +| tags | list | NetBox Tags to be added to discovered IP | ### Scope The scope defines a list of targets to be scanned. @@ -48,9 +52,14 @@ The scope defines a list of targets to be scanned. | Parameter | Type | Required | Description | |:---------:|:----:|:--------:|:-----------:| | targets | list | yes | The targets that NMAP will scan. These can be specified as IP addresses (192.168.1.1), IP ranges (192.168.1.10-20), IP subnets with mask (192.168.1.0/24) or resolvable domain names. | - - - +| fast_mode | bool | no | Fast mode - Scan fewer ports than the default scan (-F). | +| timing | int | no | Set timing template, higher is faster (-T<0-5>). | +| ports | list | no | Only scan specified ports (-p). Sample: [22,161,162,443,500-600,8080]. | +| exclude_ports | list | no | Exclude the specified ports from scanning. Sample: [23, 9000-12000]. | +| ping_scan | bool | no | Ping Scan (-sn) - disable port scan. If `scan_types` is defined, `ping_scan` will be ignored. | +| top_ports | int | no | Scan most common ports (--top-ports). | +| max_retries | int | no | Caps number of port scan probe retransmissions (--max-retries). | +| scan_types | list | no | Scan technique to be used by NMAP. Supports [udp,connect,syn,ack,window,null,fin,xmas,maimon,sctp_init,sctp_cookie_echo,ip_protocol]. If more than one TCP scan type (`connect,syn,ack,window,null,fin,xmas,maimon`) is defined, only the fist one will be applied. | ### Sample A sample policy including all parameters supported by the network discovery backend. @@ -66,11 +75,21 @@ orb: defaults: comments: none description: IP discovered by network discovery + tags: [net-discovery, orb-agent] scope: targets: - 192.168.7.32 - 192.168.7.30-40 # IP range - 192.168.7.0/24 # IP subnet - google.com # dns lookup + fast_mode: True + max_retries: 0 ``` +### ⚠️ Warning +Be **AWARE** that executing a policy with only targets defined is equivalent to running `nmap `, which in turn is the same as executing `nmap -sS -p1-1000 --open -T3 `: + +- `-sS` → SYN scan (stealth scan, requires root privileges) +- `-p1-1000` → Scans the top 1000 most common ports +- `--open` → Only shows open ports +- `-T3` → Uses the default timing template (T3 is the standard speed) \ No newline at end of file diff --git a/docs/backends/worker.md b/docs/backends/worker.md index 67548b8a..37432afd 100644 --- a/docs/backends/worker.md +++ b/docs/backends/worker.md @@ -1,17 +1,20 @@ # Worker The worker backend allows to run custom implementation as part of Orb Agent. +## Diode Entities +The worker backend can ingest any [supported entity](https://github.com/netboxlabs/diode-sdk-python?tab=readme-ov-file#supported-entities-object-types) of Diode Python SDK. + ## Configuration The `worker` backend does not require any special configuration, though overriding `host` and `port` values can be specified. The backend will use the `diode` settings specified in the `common` subsection to forward discovery results. - ```yaml orb: backends: common: diode: target: grpc://192.168.0.100:8080/diode - api_key: ${DIODE_API_KEY} + client_id: ${DIODE_CLIENT_ID} + client_secret: ${DIODE_CLIENT_SECRET} agent_name: agent01 worker: host: 192.168.5.11 # default 0.0.0.0 @@ -32,8 +35,7 @@ Config defines data for the whole scope and is optional overall. ### Scope -The scope can be defined - +The scope can be defined as either a `list` or a `map`, allowing the user to parse it according to their preference. ### Sample A sample policy including all parameters supported by the device discovery backend. diff --git a/docs/config_samples.md b/docs/config_samples.md index bba378d6..6037d18a 100644 --- a/docs/config_samples.md +++ b/docs/config_samples.md @@ -13,7 +13,8 @@ orb: common: diode: target: grpc://192.168.0.100:8080/diode - api_key: ${DIODE_API_KEY} + client_id: ${DIODE_CLIENT_ID} + client_secret: ${DIODE_CLIENT_SECRET} agent_name: agent01 policies: device_discovery: @@ -79,7 +80,8 @@ orb: common: diode: target: grpc://192.168.31.114:8080/diode - api_key: ${DIODE_API_KEY} + client_id: ${DIODE_CLIENT_ID} + client_secret: ${DIODE_CLIENT_SECRET} agent_name: agent02 policies: network_discovery: @@ -108,7 +110,8 @@ orb: common: diode: target: grpc://192.168.31.114:8080/diode - api_key: ${DIODE_API_KEY} + client_id: ${DIODE_CLIENT_ID} + client_secret: ${DIODE_CLIENT_SECRET} agent_name: agent02 policies: worker: diff --git a/docs/configs/git.md b/docs/configs/git.md index 3cc25365..37b754b1 100644 --- a/docs/configs/git.md +++ b/docs/configs/git.md @@ -1,6 +1,8 @@ # Git The Git configuration manager outlines a policy management system where an agent fetches policies from a Git repository. +**Important**: The `config_manager` and `backends` sections must still be passed directly to the agent via the config file at startup time. These components are not yet dynamically reconfigurable, so ensure the relevant settings are correctly defined before launching the agent. + ### Config The following sample of a git configuration ```yaml @@ -19,6 +21,10 @@ orb: username: "username" password: ${PASSWORD|TOKEN} private_key: path/to/certificate.pem + backends: + network_discovery: + device_discovery: + ... ``` | Parameter | Type | Required | Description | @@ -82,4 +88,21 @@ agent_selector_matches_all: selector: policies: path: folder4/policy4.yaml +``` + +### policy.yaml +Each policy file should explicitly declare the backend it applies to within the policy data itself. For example, a `policy.yaml` that targets the `device_discovery` backend might look like this: + +```yaml +device_discovery: + discovery_1: + config: + schedule: "* * * * *" + defaults: + site: New York NY + scope: + - driver: ios + hostname: 192.168.0.5 + username: admin + password: ${PASS} ``` \ No newline at end of file diff --git a/docs/secretsmgr/vault.md b/docs/secretsmgr/vault.md new file mode 100644 index 00000000..1269ec88 --- /dev/null +++ b/docs/secretsmgr/vault.md @@ -0,0 +1,127 @@ +# Vault Secrets Manager + +The Orb Agent can integrate with HashiCorp Vault to securely manage sensitive information such as passwords and API keys. This feature allows you to reference secrets stored in Vault directly in your policy configurations without hardcoding sensitive values. + +## Configuration + +The Vault secrets manager is configured in the `secrets_manager` section of your Orb Agent configuration file: + +```yaml +orb: + secrets_manager: + active: vault + sources: + vault: + address: "https://vault.example.com:8200" + namespace: "my-namespace" # Optional + timeout: 60 # Optional, in seconds + auth: "token" # Required, see authentication methods below + auth_args: # Required, depends on the auth method + token: "${VAULT_TOKEN}" + schedule: "*/5 * * * *" # Optional, cron format for polling interval +``` + +### Configuration Options + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `address` | string | Yes | The URL of your Vault server | +| `namespace` | string | No | Vault Enterprise namespace | +| `timeout` | int | No | Request timeout in seconds (default: 60) | +| `auth` | string | Yes | Authentication method (see below) | +| `auth_args` | map | Yes | Authentication method arguments | +| `schedule` | string | No | Cron expression for secret polling interval | + +## Authentication Methods + +The Vault secrets manager supports several authentication methods: + +### Token Authentication + +```yaml +auth: "token" +auth_args: + token: "s.abcdefghijklmnopqrstuvwxyz" +``` + +### AppRole Authentication + +```yaml +auth: "approle" +auth_args: + role_id: "12345678-abcd-efgh-ijkl-123456789012" + secret_id: "98765432-zyxw-vusr-qpon-987654321098" + wrapping_token: false # Optional + mount_path: "approle" # Optional +``` + +### UserPass Authentication + +```yaml +auth: "userpass" +auth_args: + username: "myuser" + password: "mypassword" + mount_path: "userpass" # Optional +``` + +### Kubernetes Authentication + +```yaml +auth: "kubernetes" +auth_args: + role: "orb-agent" + service_account_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" # Optional + mount_path: "kubernetes" # Optional +``` + +### LDAP Authentication + +```yaml +auth: "ldap" +auth_args: + username: "myuser" + password: "mypassword" + mount_path: "ldap" # Optional +``` + +## Usage + +To use a secret from Vault in your policy configuration, use the following format: + +``` +${vault://engine/path/to/secret/key} +``` + +For example, if you have a KV v2 secret engine mounted at `kv` with a secret at `path/credentials` that has a key `password` with value `secretvalue`, you would reference it as: + +``` +${vault://kv/path/credentials/password} +``` + +### Example + +Here's an example of using Vault secrets in a device discovery policy: + +```yaml +orb: + policies: + device_discovery: + discovery_1: + schedule: "0 * * * *" # Run hourly + defaults: + site: NY + scope: + - driver: ios + hostname: 10.1.2.24 + username: admin + password: "${vault://secret/cisco/v8000/password}" +``` + +The Orb Agent will resolve the Vault reference and use the actual secret value from Vault when the policy is applied. + +## Secret Polling + +If you configure the `schedule` parameter, the Orb Agent will periodically check for changes to referenced secrets. If a secret value changes, the related policies are automatically updated with the new values. + +This is useful for credential rotation scenarios, where you want to update credentials in Vault without restarting the Orb Agent or manually updating policies. diff --git a/go.mod b/go.mod index b37596ba..1b07193c 100644 --- a/go.mod +++ b/go.mod @@ -1,67 +1,326 @@ module github.com/netboxlabs/orb-agent -go 1.23.2 +go 1.23.3 + +toolchain go1.23.7 require ( github.com/go-cmd/cmd v1.4.3 github.com/go-co-op/gocron/v2 v2.15.0 github.com/go-git/go-git/v5 v5.13.2 github.com/google/uuid v1.6.0 - github.com/jmoiron/sqlx v1.4.0 - github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 - github.com/pkg/profile v1.7.0 - github.com/rubenv/sql-migrate v1.7.1 + github.com/hashicorp/vault v1.19.0 + github.com/hashicorp/vault/api v1.16.0 + github.com/hashicorp/vault/api/auth/approle v0.9.0 + github.com/hashicorp/vault/api/auth/kubernetes v0.9.0 + github.com/hashicorp/vault/api/auth/ldap v0.9.0 + github.com/hashicorp/vault/api/auth/userpass v0.9.0 github.com/spf13/cobra v1.8.1 - github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - dario.cat/mergo v1.0.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect + cloud.google.com/go v0.116.0 // indirect + cloud.google.com/go/auth v0.14.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/cloudsqlconn v1.4.3 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.2.2 // indirect + cloud.google.com/go/kms v1.20.1 // indirect + cloud.google.com/go/longrunning v0.6.2 // indirect + cloud.google.com/go/monitoring v1.21.2 // indirect + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.29 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect + github.com/DataDog/datadog-go v3.2.0+incompatible // indirect + github.com/Jeffail/gabs/v2 v2.1.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.63.84 // indirect + github.com/armon/go-metrics v0.4.1 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go v1.55.6 // indirect + github.com/axiomhq/hyperloglog v0.0.0-20220105174342-98591331716a // indirect + github.com/benbjohnson/immutable v0.4.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/speakeasy v0.2.0 // indirect + github.com/boltdb/bolt v1.3.1 // indirect + github.com/boombuler/barcode v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect + github.com/circonus-labs/circonusllhist v0.1.3 // indirect + github.com/cloudflare/circl v1.5.0 // indirect + github.com/coreos/etcd v3.3.27+incompatible // indirect + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect + github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba // indirect + github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect + github.com/digitalocean/godo v1.7.5 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.2.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/felixge/fgprof v0.9.3 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gammazero/deque v0.2.1 // indirect + github.com/gammazero/workerpool v1.1.3 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/loads v0.22.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/godbus/dbus v4.1.0+incompatible // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/certificate-transparency-go v1.3.1 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-metrics-stackdriver v0.2.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/gophercloud/gophercloud v0.1.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/hashicorp/cli v1.1.6 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/eventlogger v0.2.10 // indirect + github.com/hashicorp/go-bexpr v0.1.12 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-discover v0.0.0-20210818145131-c573d69da192 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1 // indirect + github.com/hashicorp/go-kms-wrapping/v2 v2.0.18 // indirect + github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2 v2.0.10 // indirect + github.com/hashicorp/go-kms-wrapping/wrappers/alicloudkms/v2 v2.0.4 // indirect + github.com/hashicorp/go-kms-wrapping/wrappers/awskms/v2 v2.0.11 // indirect + github.com/hashicorp/go-kms-wrapping/wrappers/azurekeyvault/v2 v2.0.14 // indirect + github.com/hashicorp/go-kms-wrapping/wrappers/gcpckms/v2 v2.0.13 // indirect + github.com/hashicorp/go-kms-wrapping/wrappers/ocikms/v2 v2.0.9 // indirect + github.com/hashicorp/go-kms-wrapping/wrappers/transit/v2 v2.0.13 // indirect + github.com/hashicorp/go-memdb v1.3.4 // indirect + github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.1 // indirect + github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 // indirect + github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 // indirect + github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect + github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0 // indirect + github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1 // indirect + github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/go-syslog v1.0.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.1-vault-5 // indirect + github.com/hashicorp/hcp-sdk-go v0.101.0 // indirect + github.com/hashicorp/mdns v1.0.4 // indirect + github.com/hashicorp/raft v1.7.1 // indirect + github.com/hashicorp/raft-autopilot v0.3.0 // indirect + github.com/hashicorp/raft-boltdb/v2 v2.3.0 // indirect + github.com/hashicorp/raft-snapshot v1.0.4 // indirect + github.com/hashicorp/raft-wal v0.4.0 // indirect + github.com/hashicorp/vault-plugin-secrets-kv v0.21.0 // indirect + github.com/hashicorp/vault/sdk v0.15.2 // indirect + github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.14.3 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgtype v1.14.3 // indirect + github.com/jackc/pgx/v4 v4.18.3 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f // indirect + github.com/jefferai/jsonx v1.0.1 // indirect + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect + github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/magiconair/properties v1.8.9 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx v1.2.29 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/linode/linodego v0.7.1 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/miekg/dns v1.1.50 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect + github.com/mitchellh/pointerstructure v1.2.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/okta/okta-sdk-golang/v5 v5.0.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/oracle/oci-go-sdk/v60 v60.0.0 // indirect + github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/pires/go-proxyproto v0.8.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rboyer/safeio v0.2.1 // indirect + github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/sagikazarmark/locafero v0.6.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/segmentio/fasthash v1.0.3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sethvargo/go-limiter v0.7.1 // indirect + github.com/shirou/gopsutil/v3 v3.22.6 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect + github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d // indirect + github.com/sony/gobreaker v0.5.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tencentcloud/tencentcloud-sdk-go v1.0.162 // indirect + github.com/tink-crypto/tink-go/v2 v2.2.0 // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.4.0 // indirect + github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c // indirect + github.com/vmware/govmomi v0.18.0 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.32.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + go.etcd.io/bbolt v1.4.0-beta.0 // indirect + go.mongodb.org/mongo-driver v1.17.2 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.10.0 // indirect golang.org/x/tools v0.29.0 // indirect + google.golang.org/api v0.221.0 // indirect + google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/resty.v1 v1.12.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/apimachinery v0.32.1 // indirect + k8s.io/client-go v0.32.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 66d7b373..c2b6516d 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,366 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= +cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/cloudsqlconn v1.4.3 h1:/WYFbB1NtMtoMxCbqpzzTFPDkxxlLTPme390KEGaEPc= +cloud.google.com/go/cloudsqlconn v1.4.3/go.mod h1:QL3tuStVOO70txb3rs4G8j5uMfo5ztZii8K3oGD3VYA= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= +cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/kms v1.20.1 h1:og29Wv59uf2FVaZlesaiDAqHFzHaoUyHI3HYp9VUHVg= +cloud.google.com/go/kms v1.20.1/go.mod h1:LywpNiVCvzYNJWS9JUcGJSVTNSwPwi0vBAotzDqn2nc= +cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= +cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU= +cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/Azure/azure-sdk-for-go v44.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 h1:m/sWOGCREuSBqg2htVQTBY8nOZpyajYztF0vUvSZTuM= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0/go.mod h1:Pu5Zksi2KrU7LPbZbNINx6fuVrUp/ffvpxdDj+i8LeE= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1 h1:UPeCRD+XY7QlaGQte2EVI2iOcWvUYA2XY8w5T/8v0NQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.2.1/go.mod h1:oGV6NlB0cvi1ZbYRR2UN44QHxWFyGk+iylgD0qaMXjA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0 h1:z4YeiSXxnUI+PqB46Yj6MZA3nwb1CcJIkEMDrzUd8Cs= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0/go.mod h1:rko9SzMxcMk0NJsNAxALEGaTYyy79bNRwxgJfrH0Spw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.11.0/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= +github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= +github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.0/go.mod h1:QRTvSZQpxqm8mSErhnbI+tANIBAKP7B+UIE2z4ypUO0= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.0/go.mod h1:JljT387FplPzBA31vUcvsetLKF3pec5bdAxjVU4kI2s= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Jeffail/gabs/v2 v2.1.0 h1:6dV9GGOjoQgzWTQEltZPXlJdFloxvIq7DwqgxMCbq30= +github.com/Jeffail/gabs/v2 v2.1.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/SAP/go-hdb v1.10.1 h1:c9dGT5xHZNDwPL3NQcRpnNISn3MchwYaGoMZpCAllUs= +github.com/SAP/go-hdb v1.10.1/go.mod h1:vxYDca44L2eRudZv5JAI6T+IygOfxb7vOCFh/Kj0pug= +github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14= +github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +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/aliyun/alibaba-cloud-sdk-go v1.63.84 h1:8IpC2i1mtsuUt13cbZtVCtQRSjzuMvLiDrbOJcaS+Z4= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.84/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apache/arrow/go/v16 v16.0.0 h1:qRLbJRPj4zaseZrjbDHa7mUoZDDIU+4pu+mE2Lucs5g= +github.com/apache/arrow/go/v16 v16.0.0/go.mod h1:9wnc9mn6vEDTRIm4+27pEjQpRKuTvBaessPoEXQzxWA= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= +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 v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15 h1:7Zwtt/lP3KNRkeZre7soMELMGNoBrutx8nobg1jKWmo= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15/go.mod h1:436h2adoHb57yd+8W+gYPrrA9U/R/SuAuOO42Ushzhw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/axiomhq/hyperloglog v0.0.0-20220105174342-98591331716a h1:eqjiAL3qooftPm8b9C1GsSSRcmlw7iOva8vdBTmV2PY= +github.com/axiomhq/hyperloglog v0.0.0-20220105174342-98591331716a/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s= +github.com/benbjohnson/immutable v0.4.0 h1:CTqXbEerYso8YzVPxmWxh2gnoRQbbB9X1quUC8+vGZA= +github.com/benbjohnson/immutable v0.4.0/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM= +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.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= +github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bufbuild/protocompile v0.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBOT5ll9dM= +github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +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/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0 h1:CWU8piLyqoi9qXEUwzOh5KFKGgmSU5ZhktJyYcq6ryQ= +github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0/go.mod h1:5d8DqS60xkj9k3aXfL3+mXBH0DPYO0FQjcKosxl+b/Q= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/cjlapao/common-go v0.0.39 h1:bAAUrj2B9v0kMzbAOhzjSmiyDy+rd56r2sy7oEiQLlA= +github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9l4UWaddwOA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= +github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudfoundry-community/go-cfclient v0.0.0-20220930021109-9c4e6c59ccf1 h1:ef0OsiQjSQggHrLFAMDRiu6DfkVSElA5jfG1/Nkyu6c= +github.com/cloudfoundry-community/go-cfclient v0.0.0-20220930021109-9c4e6c59ccf1/go.mod h1:sgaEj3tRn0hwe7GPdEUwxrdOqjBzyjyvyOCGf1OQyZY= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.27+incompatible h1:QIudLb9KeBsE5zyYxd1mjzRSkzLg9Wf9QlRwFgd6oTA= +github.com/coreos/etcd v3.3.27+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf h1:GOPo6vn/vTN+3IwZBvXX0y5doJfSC7My0cdzelyOCsQ= +github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/couchbase/gocb/v2 v2.9.3 h1:rp0rQNbmdHL96uz+EBKrj6vboEjHwgV5zNoNDwL/dtU= +github.com/couchbase/gocb/v2 v2.9.3/go.mod h1:zsjLP1qp2I62SpYiEB71dtELDFKIYZkmJz2I9Dyar80= +github.com/couchbase/gocbcore/v10 v10.5.3 h1:jGIMVLnr0c19UQfMfoCHCdJ3BkFEe2OB0ZMXZ+YPGNw= +github.com/couchbase/gocbcore/v10 v10.5.3/go.mod h1:rulbgUK70EuyRUiLQ0LhQAfSI/Rl+jWws8tTbHzvB6M= +github.com/couchbase/gocbcoreps v0.1.3 h1:fILaKGCjxFIeCgAUG8FGmRDSpdrRggohOMKEgO9CUpg= +github.com/couchbase/gocbcoreps v0.1.3/go.mod h1:hBFpDNPnRno6HH5cRXExhqXYRmTsFJlFHQx7vztcXPk= +github.com/couchbase/goprotostellar v1.0.2 h1:yoPbAL9sCtcyZ5e/DcU5PRMOEFaJrF9awXYu3VPfGls= +github.com/couchbase/goprotostellar v1.0.2/go.mod h1:5/yqVnZlW2/NSbAWu1hPJCFBEwjxgpe0PFFOlRixnp4= +github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20240607131231-fb385523de28 h1:lhGOw8rNG6RAadmmaJAF3PJ7MNt7rFuWG7BHCYMgnGE= +github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20240607131231-fb385523de28/go.mod h1:o7T431UOfFVHDNvMBUmUxpHnhivwv7BziUao/nMl81E= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= 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/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/digitalocean/godo v1.7.5 h1:JOQbAO6QT1GGjor0doT0mXefX2FgUDPOpYh2RaXA+ko= +github.com/digitalocean/godo v1.7.5/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70= +github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 h1:2MIhn2R6oXQbgW5yHfS+d6YqyMfXiu2L55rFZC4UD/M= +github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= -github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= +github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= +github.com/gammazero/workerpool v1.1.3 h1:WixN4xzukFoN0XSeXF6puqEqFTl2mECI9S6W44HWy9Q= +github.com/gammazero/workerpool v1.1.3/go.mod h1:wPjyBLDbyKnUn2XwwyD3EEwo9dHutia9/fwNmSHWACc= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-cmd/cmd v1.4.3 h1:6y3G+3UqPerXvPcXvj+5QNPHT02BUw7p6PsqRxLNA7Y= github.com/go-cmd/cmd v1.4.3/go.mod h1:u3hxg/ry+D5kwh8WvUkHLAMe2zQCaXd00t35WfQaOFk= github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo= github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= @@ -47,150 +369,1315 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= -github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= -github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= +github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= +github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 h1:sfz1YppV05y4sYaW7kXZtrocU/+vimnIWt4cxAYh7+o= +github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3/go.mod h1:ZXFhGda43Z2TVbfGZefXyMJzsDHhCh0go3bZUcwTx7o= +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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c= +github.com/gocql/gocql v1.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= +github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= +github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= +github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= +github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= +github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso= -github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-metrics-stackdriver v0.2.0 h1:rbs2sxHAPn2OtUj9JdR/Gij1YKGl0BTVD0augB+HEjE= +github.com/google/go-metrics-stackdriver v0.2.0/go.mod h1:KLcPyp3dWJAFD+yHisGlJSZktIsTjb50eB72U2YZ9K0= +github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/cap v0.8.0 h1:NBC0bxy0l/BUerFfJmtJV3hWwygZfj7+strn3YyWutQ= +github.com/hashicorp/cap v0.8.0/go.mod h1:2VlBggzEqBOU3VuP2TDSrRLjKYZ/2eLeqLbKfoBYmY4= +github.com/hashicorp/cap/ldap v0.0.0-20250106213447-9047b8b3240f h1:iixO0KNqHfSMImUgaHnMHTzmu0FVLwk7VzIZf6++wak= +github.com/hashicorp/cap/ldap v0.0.0-20250106213447-9047b8b3240f/go.mod h1:vGqAhHKOR5gadKWjwhoWp3RKto/tmhVOtH8gcD0c8ss= +github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8= +github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4= +github.com/hashicorp/consul/api v1.29.1 h1:UEwOjYJrd3lG1x5w7HxDRMGiAUPrb3f103EoeKuuEcc= +github.com/hashicorp/consul/api v1.29.1/go.mod h1:lumfRkY/coLuqMICkI7Fh3ylMG31mQSRZyef2c5YvJI= +github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= +github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= +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/eventlogger v0.2.10 h1:Dddth3KVSribGE1rInGToM30tRNblvL0G1OG6N+i2pk= +github.com/hashicorp/eventlogger v0.2.10/go.mod h1:imHMTfJH4qfb8Knh9nZw4iLfL9J1bX6TJKEurSB4t+U= +github.com/hashicorp/go-bexpr v0.1.12 h1:XrdVhmwu+9iYxIUWxsGVG7NQwrhzJZ0vR6nbN5bLgrA= +github.com/hashicorp/go-bexpr v0.1.12/go.mod h1:ACktpcSySkFNpcxWSClFrut7wicd9WzisnvHuw+g9K8= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/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-discover v0.0.0-20210818145131-c573d69da192 h1:eje2KOX8Sf7aYPiAsLnpWdAIrGRMcpFjN/Go/Exb7Zo= +github.com/hashicorp/go-discover v0.0.0-20210818145131-c573d69da192/go.mod h1:3/4dzY4lR1Hzt9bBqMhBzG7lngZ0GKx/nL6G/ad62wE= +github.com/hashicorp/go-gcp-common v0.9.1 h1:ZzAJNAz6OwpNUutnnUVnFERtR2fI1oZT5Z2i1vOly/s= +github.com/hashicorp/go-gcp-common v0.9.1/go.mod h1:JJ5Zvnsmrn1GkBg64+oDfSK/gJtnGyX5x2nFuSdulLw= +github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +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-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 h1:kBoJV4Xl5FLtBfnBjDvBxeNSy2IRITSGs73HQsFUEjY= +github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.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-kms-wrapping/entropy/v2 v2.0.1 h1:KIge4FHZEDb2/xjaWgmBheCTgRL6HV4sgTfDsH876L8= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1/go.mod h1:aHO1EoFD0kBYLBedqxXgalfFT8lrWfP7kpuSoaqGjH0= +github.com/hashicorp/go-kms-wrapping/v2 v2.0.18 h1:DLfC677GfKEpSAFpEWvl1vXsGpEcSHmbhBaPLrdDQHc= +github.com/hashicorp/go-kms-wrapping/v2 v2.0.18/go.mod h1:t/eaR/mi2mw3klfl1WEAuiLKrlZ/Q8cosmsT+RIPLu0= +github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2 v2.0.10 h1:am7ai27sEGpfOefHhUShbWAOa6EvkBaiMpB7zZ/PUyo= +github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2 v2.0.10/go.mod h1:sYX07HI7wMCFe9+FmxMOCwJ7q5CD4aq3VI+KoB8FYZY= +github.com/hashicorp/go-kms-wrapping/wrappers/alicloudkms/v2 v2.0.4 h1:8XgCt3ZDfE0MPBLJsUE4ZnPkFAF4K13Zxqyjx1lA22A= +github.com/hashicorp/go-kms-wrapping/wrappers/alicloudkms/v2 v2.0.4/go.mod h1:Inx0DLGr58Un5TerS8je0SGZwKKihotqaxqoAKHpSmk= +github.com/hashicorp/go-kms-wrapping/wrappers/awskms/v2 v2.0.11 h1:J9zGa9SlcOHT3SQTj0Vv3shHo0anWbs58weURGCgChI= +github.com/hashicorp/go-kms-wrapping/wrappers/awskms/v2 v2.0.11/go.mod h1:iAOCu7/lG5eugg8+k7NVvQt0IpWT8s2Q9wnMtC/guM4= +github.com/hashicorp/go-kms-wrapping/wrappers/azurekeyvault/v2 v2.0.14 h1:oK4OQ5EPbx/66dAvitksV+OdrQ86SZEj3B6VSZrbdEY= +github.com/hashicorp/go-kms-wrapping/wrappers/azurekeyvault/v2 v2.0.14/go.mod h1:fWxrv9YkAMqtsISde5mcutoMvuiH4kyg1AlDzzmqRh8= +github.com/hashicorp/go-kms-wrapping/wrappers/gcpckms/v2 v2.0.13 h1:NGBZnF+yPRZ3gjFl69Y2m58/U0iyB2oH9HaznL9tekA= +github.com/hashicorp/go-kms-wrapping/wrappers/gcpckms/v2 v2.0.13/go.mod h1:4Xb+6d8VPeDcUNuh4toPqJlDpkajeJyIQeg36TtWhKw= +github.com/hashicorp/go-kms-wrapping/wrappers/ocikms/v2 v2.0.9 h1:rlKOPHzZ41QeV/H6UIX2wVkPhLzVK+nKhLRIbIAZ0Yc= +github.com/hashicorp/go-kms-wrapping/wrappers/ocikms/v2 v2.0.9/go.mod h1:pHJfTvq97FAKCWxIJOHZWQmVfRXmUN6tmgEcgj3nC+M= +github.com/hashicorp/go-kms-wrapping/wrappers/transit/v2 v2.0.13 h1:UuDeq3nr0e+H9CrZM3dvpDGkWFSJYTtuTqVekn2za2k= +github.com/hashicorp/go-kms-wrapping/wrappers/transit/v2 v2.0.13/go.mod h1:E2dYgXYNkvKe84PIxD9eJqqhFRA4guCTDweJR4i0gds= +github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= +github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= +github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= +github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0= +github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +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-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a h1:FmnBDwGwlTgugDGbVxwV8UavqSMACbGrUpfc98yFLR4= +github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a/go.mod h1:xbXnmKqX9/+RhPkJ4zrEx4738HacP72aaUPlT2RZ4sU= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 h1:I8bynUKMh9I7JdwtW9voJ0xmHvBpxQtLjrMFDYmhOxY= +github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0/go.mod h1:oKHSQs4ivIfZ3fbXGQOop1XuDfdSb8RIsWTGaAanSfg= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= +github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 h1:VaLXp47MqD1Y2K6QVrA9RooQiPyCgAbnfeJg44wKuJk= +github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1/go.mod h1:hH8rgXHh9fPSDPerG6WzABHsHF+9ZpLhRI1LPk4JZ8c= +github.com/hashicorp/go-secure-stdlib/fileutil v0.1.0 h1:f2mwVgMJjXuX/+eWD6ZW30+oIRgCofL+XMWknFkB1WM= +github.com/hashicorp/go-secure-stdlib/fileutil v0.1.0/go.mod h1:uwcr2oga9pN5+OkHZyTN5MDk3+1YHOuMukhpnPaQAoI= +github.com/hashicorp/go-secure-stdlib/httputil v0.1.0 h1:0cT/LmCfurGE6/MOq8ig3meKYS32YDh0sTE9g86ANgg= +github.com/hashicorp/go-secure-stdlib/httputil v0.1.0/go.mod h1:Md+jfeLf7CjGjTmgBWzFyc4vznsIb8yEiX7/CGAJvkI= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 h1:kH3Rhiht36xhAfhuHyWJDgdXXEx9IIZhDGRk24CDhzg= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.3/go.mod h1:ov1Q0oEDjC3+A4BwsG2YdKltrmEw8sf9Pau4V9JQ4Vo= +github.com/hashicorp/go-secure-stdlib/nonceutil v0.1.0 h1:iJG9Q3iUme12yH+wzBMGYrw/Am4CfX3sDcA8m5OGfhQ= +github.com/hashicorp/go-secure-stdlib/nonceutil v0.1.0/go.mod h1:s28ohJ0kU6tersf0it/WsBCyZSdziPlP+G1FRA3ar28= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 h1:FW0YttEnUNDJ2WL9XcrrfteS1xW8u+sh4ggM8pN5isQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/password v0.1.1 h1:6JzmBqXprakgFEHwBgdchsjaA9x3GyjdI568bXKxa60= +github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= +github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0 h1:U6y5MXGiDVOOtkWJ6o/tu1TxABnI0yKTQWJr7z6BpNk= +github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0/go.mod h1:ecDb3o+8D4xtP0nTCufJaAVawHavy5M2eZ64Nq/8/LM= +github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1 h1:JY+zGg8gOmslwif1fiCqT5Hu1SikLZQcHkmQhCoA9gY= +github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1/go.mod h1:jW3KCTvdPyAdVecOUwiiO2XaYgUJ/isigt++ISkszkY= +github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 h1:SMGUnbpAcat8rIKHkBPjfv81yC46a8eCNZ2hsR2l1EI= +github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1/go.mod h1:Ch/bf00Qnx77MZd49JRgHYqHQjtEmTgGU2faufpVZb0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3 h1:xbrxd0U9XQW8qL1BAz2XrAjAF/P2vcqUTAues9c24B8= +github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.3/go.mod h1:LWq2Sy8UoKKuK4lFuCNWSjJj57MhNNf2zzBWMtkAIX4= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-tfe v1.74.1 h1:I/8fOwSYox17IZV7SULIQH0ZRPNL2g/biW6hHWnOTVY= +github.com/hashicorp/go-tfe v1.74.1/go.mod h1:kGHWMZ3HHjitgqON8nBZ4kPVJ3cLbzM4JMgmNVMs9aQ= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= +github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/hcp-sdk-go v0.101.0 h1:jRphqVzYCw3d/M0CyVe5FIMbS/FFv5Dq36mepIkqI7g= +github.com/hashicorp/hcp-sdk-go v0.101.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs= +github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/nomad/api v0.0.0-20240213164230-c364cb57298d h1:nvfutImOr3GgkMSMjfNdTil9e54vtyQxxyHZ+NHII3Y= +github.com/hashicorp/nomad/api v0.0.0-20240213164230-c364cb57298d/go.mod h1:ijDwa6o1uG1jFSq6kERiX2PamKGpZzTmo0XOFNeFZgw= +github.com/hashicorp/raft v1.0.1/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI= +github.com/hashicorp/raft v1.1.2-0.20191002163536-9c6bd3e3eb17/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= +github.com/hashicorp/raft v1.7.1 h1:ytxsNx4baHsRZrhUcbt3+79zc4ly8qm7pi0393pSchY= +github.com/hashicorp/raft v1.7.1/go.mod h1:hUeiEwQQR/Nk2iKDD0dkEhklSsu3jcAcqvPzPoZSAEM= +github.com/hashicorp/raft-autopilot v0.3.0 h1:KhXCecBFqAMpC0i77qVfuYd937cl2dNatSA/sSNs+2s= +github.com/hashicorp/raft-autopilot v0.3.0/go.mod h1:pUBzcE8bXIm/NcFZ/xKz7O3aNOU/4T4Zkv11YqdxpUc= +github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk= +github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702 h1:RLKEcCuKcZ+qp2VlaaZsYZfLOmIiuJNpEi48Rl8u9cQ= +github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702/go.mod h1:nTakvJ4XYq45UXtn0DbwR4aU9ZdjlnIenpbs6Cd+FM0= +github.com/hashicorp/raft-boltdb/v2 v2.3.0 h1:fPpQR1iGEVYjZ2OELvUHX600VAK5qmdnDEv3eXOwZUA= +github.com/hashicorp/raft-boltdb/v2 v2.3.0/go.mod h1:YHukhB04ChJsLHLJEUD6vjFyLX2L3dsX3wPBZcX4tmc= +github.com/hashicorp/raft-snapshot v1.0.4 h1:EuDuayAJPdiDmVk1ygTDnG2zDzrs0/6/yBuma1IYSow= +github.com/hashicorp/raft-snapshot v1.0.4/go.mod h1:5sL9eUn72lH5DzsFIJ9jaysITbHksSSszImWSOTC8Ic= +github.com/hashicorp/raft-wal v0.4.0 h1:oHCQLPa3gBTrfuBVHaDg2b/TVXpU0RIyeH/mU9ovk3Y= +github.com/hashicorp/raft-wal v0.4.0/go.mod h1:A6vP5o8hGOs1LHfC1Okh9xPwWDcmb6Vvuz/QyqUXlOE= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hashicorp/vault v1.19.0 h1:f4rP2nhGGguXl85BL7iWAXfZU41rOh8WTrdDty4Z4xg= +github.com/hashicorp/vault v1.19.0/go.mod h1:OjoMLEMS/EKOGrD/Jk79vO6660UY6JspRIYB+iQ/RXY= +github.com/hashicorp/vault-plugin-auth-alicloud v0.20.0 h1:yw96/zWrNPFTH8yTqTvVtraJ3EWk9vewvx1H7X6lekI= +github.com/hashicorp/vault-plugin-auth-alicloud v0.20.0/go.mod h1:aAE14G1n1/Qw5/Vj+P0eaEuo8m6op2/3RhR4gN3q5AI= +github.com/hashicorp/vault-plugin-auth-azure v0.20.1 h1:oKbzERNhIMJvECQaouT8BKnn6kfrtA7yeI4l4pdSGcc= +github.com/hashicorp/vault-plugin-auth-azure v0.20.1/go.mod h1:9SFxR96yYv7vxDvZZady3K0YKguSNU53d8WSrshHjlQ= +github.com/hashicorp/vault-plugin-auth-cf v0.20.0 h1:KOdNy0uSffjw0sOU9zg9JgdCkuRPcqOjOIxyV2NZLjg= +github.com/hashicorp/vault-plugin-auth-cf v0.20.0/go.mod h1:SO35/C2iS12kIqIoux4AB2QSQ2IO+hbZ4/UQrQDyawo= +github.com/hashicorp/vault-plugin-auth-gcp v0.20.1 h1:FlucOwK3h67+ThgAf5B2wHYxoI96ryAgyWThe5piE4g= +github.com/hashicorp/vault-plugin-auth-gcp v0.20.1/go.mod h1:XVsmYHrJ0dByHIRXW7crHnXwV+QwZEOjaZBO1JlMQ5I= +github.com/hashicorp/vault-plugin-auth-jwt v0.23.0 h1:LErXihivT7I8ZWB7jzQlp/IA54PdMWCaXNdkjmB0Z3c= +github.com/hashicorp/vault-plugin-auth-jwt v0.23.0/go.mod h1:a/PUlLU88uUe1GtUTdSDkp/0HVXM7p9CY2vcwEh7NeU= +github.com/hashicorp/vault-plugin-auth-kerberos v0.14.0 h1:kJGBKDk8lJXftM8PVG9ars3NWHOdylJaeutTsI/7E0w= +github.com/hashicorp/vault-plugin-auth-kerberos v0.14.0/go.mod h1:JV+qr3M+OIiquVjSHD0AN6p6kTEoEo2GvSdq6Ih5zvo= +github.com/hashicorp/vault-plugin-auth-kubernetes v0.21.0 h1:YmwthfYgEjv+2rir+DY/ADznVtHQ43qPUo6XT6I3Fxs= +github.com/hashicorp/vault-plugin-auth-kubernetes v0.21.0/go.mod h1:V36/cDsl8tjdr8xQ35uLzvlDLclpPCOdLheAk42ks6E= +github.com/hashicorp/vault-plugin-auth-oci v0.18.0 h1:/9H4bv8mCRpO3oHDRuFv7lyWRXLQtdjSEDpCtCfPQOw= +github.com/hashicorp/vault-plugin-auth-oci v0.18.0/go.mod h1:NKc8QPi5Tou5Mmf4xKEBhMSL3pQviePq91d087oQOdo= +github.com/hashicorp/vault-plugin-database-couchbase v0.13.0 h1:ql81GB+20ggmRCS/qKnsRwvYdUYW8Dhv1uhH5lY9mns= +github.com/hashicorp/vault-plugin-database-couchbase v0.13.0/go.mod h1:lfCapwEocUz/lno6ABwxa/OXEHGdsAUejcI0Mlod6TE= +github.com/hashicorp/vault-plugin-database-elasticsearch v0.17.0 h1:GPZFTWjSv85GWDtrxZ6uYXsxNu4utgr+m2+lP7KCOwU= +github.com/hashicorp/vault-plugin-database-elasticsearch v0.17.0/go.mod h1:I2MnIah+ltWlJtmXFA2+KUaaW68TR0QM9JOPmq+Tbdo= +github.com/hashicorp/vault-plugin-database-mongodbatlas v0.14.0 h1:llor1sH0gXaLbQFOidLSbSXKPPFBs16OpyKeAqbBdyI= +github.com/hashicorp/vault-plugin-database-mongodbatlas v0.14.0/go.mod h1:hxuULBD5X4N1hi6PvMdbUdVTE94I/6dPWtT2+rRTTTM= +github.com/hashicorp/vault-plugin-database-redis v0.5.0 h1:p0vLmwUs6Dyiwki6ibtizQ7b4rtx+y78J8kSkr4rsiQ= +github.com/hashicorp/vault-plugin-database-redis v0.5.0/go.mod h1:RaY5jao0wibpMeH/1Jz0QwpH9GQ+vRay2wz5of08QsY= +github.com/hashicorp/vault-plugin-database-redis-elasticache v0.6.0 h1:IkP+Ilb/jC2FLU4KAH9+ai0aaCP+GVmGdmIl981ZFs0= +github.com/hashicorp/vault-plugin-database-redis-elasticache v0.6.0/go.mod h1:AxZkp+gtaDtQL5aKB4/NrTTOMpwefSIy3GTbIRQtxoM= +github.com/hashicorp/vault-plugin-database-snowflake v0.13.0 h1:Q+q7y5yHrLZgsecQkdaf2/+FusXtQnE7jBjCdW+p5R0= +github.com/hashicorp/vault-plugin-database-snowflake v0.13.0/go.mod h1:Do6Tlsbjps5PURr9yzwmYBDOGB02s3dQBRKMj+blthg= +github.com/hashicorp/vault-plugin-mock v0.16.1 h1:5QQvSUHxDjEEbrd2REOeacqyJnCLPD51IQzy71hx8P0= +github.com/hashicorp/vault-plugin-mock v0.16.1/go.mod h1:83G4JKlOwUtxVourn5euQfze3ZWyXcUiLj2wqrKSDIM= +github.com/hashicorp/vault-plugin-secrets-ad v0.20.1 h1:EC+ZwWP54cehgXk5BC8WKX8/pMYVOo7CDLDh4RPQ6TM= +github.com/hashicorp/vault-plugin-secrets-ad v0.20.1/go.mod h1:QeGpxzcU1EEZazRUiJNi4cIJ98f3JbgefhrFGkwI+h4= +github.com/hashicorp/vault-plugin-secrets-alicloud v0.19.0 h1:XH1typO/R5RlyyW5cm65+DDAnYmiA7xEdoRGGrB9xu0= +github.com/hashicorp/vault-plugin-secrets-alicloud v0.19.0/go.mod h1:MxfMowH1VenMCtixd/mDqq9z10CBobzOMZJOXRLi0TA= +github.com/hashicorp/vault-plugin-secrets-azure v0.21.1 h1:+9iHg7P71vVZ+1muAkao28tl6bf9qWw3VRWZmKSTswI= +github.com/hashicorp/vault-plugin-secrets-azure v0.21.1/go.mod h1:IwfPcNyzaigu3LFy8yp+f1TVgBNdtJtTMERWhtDrXqw= +github.com/hashicorp/vault-plugin-secrets-gcp v0.21.1 h1:xEXWqoreWz4ZeZOaLbGpkUHDnJdsO7YADfARxoecsJg= +github.com/hashicorp/vault-plugin-secrets-gcp v0.21.1/go.mod h1:PJwUCF6t95OSpkM15YJZJO8eSPYf/SjvEsrBJZrkzk8= +github.com/hashicorp/vault-plugin-secrets-gcpkms v0.20.0 h1:gFPxVPaFJjyPUF3GE7LwgGkVkQ+BA7BE775IfdznZ5M= +github.com/hashicorp/vault-plugin-secrets-gcpkms v0.20.0/go.mod h1:SgKyMgD4+Jj4jDRgFOactHENY7Vov6Hi0UdYWVO9NGY= +github.com/hashicorp/vault-plugin-secrets-kubernetes v0.10.0 h1:Fw7s2f1WNW1GZgd3jb+7mkx6jPH528AFwWMHg9LarCQ= +github.com/hashicorp/vault-plugin-secrets-kubernetes v0.10.0/go.mod h1:MHNHjEfrXPzWB2J/xmgzojb76wsw0/oUd8z3QLDzzbM= +github.com/hashicorp/vault-plugin-secrets-kv v0.21.0 h1:P8WPzAkttLnhZyhTmY15bKlGkGcLjYsQUAIFi+tu5vA= +github.com/hashicorp/vault-plugin-secrets-kv v0.21.0/go.mod h1:CY63j85kYvO+GjDvMmZlhyJixoQhEf+SenY/OwNSrpI= +github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.14.0 h1:N7zUrgQqvDVUsOZW4x49Cbx6WcjEU5Qwe8hrr4lYvV8= +github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.14.0/go.mod h1:nRcr6W9rb3vDMLDGb/ZovsFhrEM8Q1WLNUKDGRaDplM= +github.com/hashicorp/vault-plugin-secrets-openldap v0.15.1 h1:OE2IZXpfcT1Ft1DpQGFwDdbJlFyrqBnhhd/KTIX1GpE= +github.com/hashicorp/vault-plugin-secrets-openldap v0.15.1/go.mod h1:jztXgadwFYKkmozTgLhUdCNFjJmLhOr88kjbKrJCXEw= +github.com/hashicorp/vault-plugin-secrets-terraform v0.11.0 h1:dIOJ7VKyYU8o9xH1DuD61Fsfl6uSPHy6OrYdEjp4Ku0= +github.com/hashicorp/vault-plugin-secrets-terraform v0.11.0/go.mod h1:6FNbBAQvISpPqLXdvhV8MvxXKWG9iS+D+spzIGU2WuI= +github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/hashicorp/vault/api/auth/approle v0.9.0 h1:FdpspwGVWnGiWmAxd5L1Yd+T+fX2kYnyAIvI5oGdvNs= +github.com/hashicorp/vault/api/auth/approle v0.9.0/go.mod h1:fvtJhBs3AYMs2fXk4U5+u+7unhUGuboiKzFpLPpIazw= +github.com/hashicorp/vault/api/auth/kubernetes v0.9.0 h1:xV3xXMtSV8tq5iefueAw3OOdhhXyjnyhrQkIFM5fh54= +github.com/hashicorp/vault/api/auth/kubernetes v0.9.0/go.mod h1:3K6uEUKZLBQ3d+eXAa4Ubp4UocswU90zY4QP5Az3Vw8= +github.com/hashicorp/vault/api/auth/ldap v0.9.0 h1:XPibaiy0e+Dzv46m2q+D/VhzbrsVMgTe9tH2veu83Vk= +github.com/hashicorp/vault/api/auth/ldap v0.9.0/go.mod h1:rZEy3AP7R0GTAJ867+GRIxPjkkizpcR3Bh21Mpe5zCs= +github.com/hashicorp/vault/api/auth/userpass v0.9.0 h1:tdIY+xe9O0SAcNY1CK7Wk0ENWNKmmGzJ9+iqZfFBW4I= +github.com/hashicorp/vault/api/auth/userpass v0.9.0/go.mod h1:W2Cb0z6MjAHkDjGG95mmRGzCETX5Y+O++UBXX/ZlNGc= +github.com/hashicorp/vault/sdk v0.15.2 h1:Rp5Yp4lyBhlWgq24ZVb2n/YN47RKOAvmx/jlMfS9ku4= +github.com/hashicorp/vault/sdk v0.15.2/go.mod h1:2Wj2tHIgfz0gNWgEPWBbCXFIiPrq96E8FTjPNV9J1Bc= +github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyuqg7yuAWUg/jMZR1/0QTzTRdNR6Uw= +github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= +github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= +github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus= +github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= +github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= +github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= +github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc= +github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= -github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jeffchao/backoff v0.0.0-20140404060208-9d7fd7aa17f2 h1:mex1izRBCD+7WjieGgRdy7e651vD/lvB1bD9vNE/3K4= +github.com/jeffchao/backoff v0.0.0-20140404060208-9d7fd7aa17f2/go.mod h1:xkfESuHriIekR+4RoV+fu91j/CfnYM29Zi2tMFw5iD4= +github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f h1:E87tDTVS5W65euzixn7clSzK66puSt1H4I5SC0EmHH4= +github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00= +github.com/jefferai/jsonx v1.0.1 h1:GvWkLWihoLqDG0BSP45TUQJH9qsINX50PVrFULgpc/I= +github.com/jefferai/jsonx v1.0.1/go.mod h1:yFo3l2fcm7cZVHGq3HKLXE+Pd4RWuRjNBDHksM7XekQ= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg= +github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4= +github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg= +github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY= +github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0= +github.com/joyent/triton-go v0.0.0-20180628001255-830d2b111e62/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA= +github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f h1:ENpDacvnr8faw5ugQmEF1QYk+f/Y9lXFvuYmRxykago= +github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f/go.mod h1:KDSfL7qe5ZfQqvlDMkVjCztbmcpp/c8M77vhQP8ZPvk= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/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.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.29 h1:QT0utmUJ4/12rmsVQrJ3u55bycPkKqGYuGT4tyRhxSQ= +github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= -github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/linode/linodego v0.7.1 h1:4WZmMpSA2NRwlPZcc0+4Gyn7rr99Evk9bnr0B3gXRKE= +github.com/linode/linodego v0.7.1/go.mod h1:ga11n3ivecUrPCHN0rANxKmfWBJVkOXfLMZinAbj2sY= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +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.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +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-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mediocregopher/radix/v4 v4.1.4 h1:Uze6DEbEAvL+VHXUEu/EDBTkUk5CLct5h3nVSGpc6Ts= +github.com/mediocregopher/radix/v4 v4.1.4/go.mod h1:ajchozX/6ELmydxWeWM6xCFHVpZ4+67LXHOTOVR0nCE= +github.com/michaelklishin/rabbit-hole/v2 v2.12.0 h1:946p6jOYFcVJdtBBX8MwXvuBkpPjwm1Nm2Qg8oX+uFk= +github.com/michaelklishin/rabbit-hole/v2 v2.12.0/go.mod h1:AN/3zyz7d++OHf+4WUo/LR0+Q5nlPHMaXasIsG/mPY0= +github.com/microsoft/go-mssqldb v1.5.0 h1:CgENxkwtOBNj3Jg6T1X209y2blCfTTcwuOlznd2k9fk= +github.com/microsoft/go-mssqldb v1.5.0/go.mod h1:lmWsjHD8XX/Txr0f8ZqgbEZSC+BZjmEQy/Ms+rLrvho= +github.com/microsoft/kiota-abstractions-go v1.8.1 h1:0gtK3KERmbKYm5AxJLZ8WPlNR9eACUGWuofFIa01PnA= +github.com/microsoft/kiota-abstractions-go v1.8.1/go.mod h1:YO2QCJyNM9wzvlgGLepw6s9XrPgNHODOYGVDCqQWdLI= +github.com/microsoft/kiota-authentication-azure-go v1.1.0 h1:HudH57Enel9zFQ4TEaJw6lMiyZ5RbBdrRHwdU0NP2RY= +github.com/microsoft/kiota-authentication-azure-go v1.1.0/go.mod h1:zfPFOiLdEqM77Hua5B/2vpcXrVaGqSWjHSRzlvAWEgc= +github.com/microsoft/kiota-http-go v1.4.4 h1:HM0KT/Q7o+JsGatFkkbTIqJL24Jzo5eMI5NNe9N4TQ4= +github.com/microsoft/kiota-http-go v1.4.4/go.mod h1:Kup5nMDD3a9sjdgRKHCqZWqtrv3FbprjcPaGjLR6FzM= +github.com/microsoft/kiota-serialization-form-go v1.0.0 h1:UNdrkMnLFqUCccQZerKjblsyVgifS11b3WCx+eFEsAI= +github.com/microsoft/kiota-serialization-form-go v1.0.0/go.mod h1:h4mQOO6KVTNciMF6azi1J9QB19ujSw3ULKcSNyXXOMA= +github.com/microsoft/kiota-serialization-json-go v1.0.9 h1:lJivec0G0tI6T8McBTnucyyYXczXytwcu1pt0UjWSBY= +github.com/microsoft/kiota-serialization-json-go v1.0.9/go.mod h1:AxrS/Gbmr8y/hIp2pJcpTup/2wCE8ED+VEXkf/9xKb4= +github.com/microsoft/kiota-serialization-multipart-go v1.0.0 h1:3O5sb5Zj+moLBiJympbXNaeV07K0d46IfuEd5v9+pBs= +github.com/microsoft/kiota-serialization-multipart-go v1.0.0/go.mod h1:yauLeBTpANk4L03XD985akNysG24SnRJGaveZf+p4so= +github.com/microsoft/kiota-serialization-text-go v1.0.0 h1:XOaRhAXy+g8ZVpcq7x7a0jlETWnWrEum0RhmbYrTFnA= +github.com/microsoft/kiota-serialization-text-go v1.0.0/go.mod h1:sM1/C6ecnQ7IquQOGUrUldaO5wj+9+v7G2W3sQ3fy6M= +github.com/microsoftgraph/msgraph-sdk-go v1.61.0 h1:/dqZXI7VAHTAeddhEfEPLf1EXszaDAkLcC9dkOARPxI= +github.com/microsoftgraph/msgraph-sdk-go v1.61.0/go.mod h1:2dZJTO/7S+UmfwKlPQg2vczpW7awcIEj1ZCgfZIf0V4= +github.com/microsoftgraph/msgraph-sdk-go-core v1.2.1 h1:P1wpmn3xxfPMFJHg+PJPcusErfRkl63h6OdAnpDbkS8= +github.com/microsoftgraph/msgraph-sdk-go-core v1.2.1/go.mod h1:vFmWQGWyLlhxCESNLv61vlE4qesBU+eWmEVH7DJSESA= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +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-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw= +github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo= +github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +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/mongodb-forks/digest v1.1.0 h1:7eUdsR1BtqLv0mdNm4OXs6ddWvR4X2/OsLwdKksrOoc= +github.com/mongodb-forks/digest v1.1.0/go.mod h1:rb+EX8zotClD5Dj4NdgxnJXG9nwrlx3NWKJ8xttz1Dg= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 h1:BQ1HW7hr4IVovMwWg0E0PYcyW8CzqDcVmaew9cujU4s= +github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2/go.mod h1:TLb2Sg7HQcgGdloNxkrmtgDNR9uVYF3lfdFIN4Ro6Sk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/okta/okta-sdk-golang/v5 v5.0.2 h1:eecvycE/XDX56IWTsOVhqfj5txCgqryTXzKy7wKEq78= +github.com/okta/okta-sdk-golang/v5 v5.0.2/go.mod h1:T/vmECtJX33YPZSVD+sorebd8LLhe38Bi/VrFTjgVX0= +github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.2.0-rc.1 h1:SMjop2pxxYRTfKdsigna/8xRoaoCfIQfD2cVuOb64/o= +github.com/opencontainers/runc v1.2.0-rc.1/go.mod h1:m9JwxfHzXz5YTTXBQr7EY9KTuazFAGPyMQx2nRR3vTw= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU= +github.com/oracle/oci-go-sdk/v59 v59.0.0 h1:+zTvWfj9ZK0OwLRyXjUkZ8dPN3WvkQSRd3iooaOxNVs= +github.com/oracle/oci-go-sdk/v59 v59.0.0/go.mod h1:PWyWRn+xkQxwwmLq/oO03X3tN1tk2vEIE2tFaJmldHM= +github.com/oracle/oci-go-sdk/v60 v60.0.0 h1:EJAWjEi4SY5Raha6iUzq4LTQ0uM5YFw/wat/L1ehIEM= +github.com/oracle/oci-go-sdk/v60 v60.0.0/go.mod h1:krz+2gkSzlSL/L4PvP0Z9pZpag9HYLNtsMd1PmxlA2w= +github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= +github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c h1:vwpFWvAO8DeIZfFeqASzZfsxuWPno9ncAebBEP0N3uE= +github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c/go.mod h1:otzZQXgoO96RTzDB/Hycg0qZcXZsWJGJRSXbmEIJ+4M= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= +github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= -github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= 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/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= -github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d h1:PinQItctnaL2LtkaSM678+ZLLy5TajwOeXzWvYC7tII= +github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +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.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +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.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rboyer/safeio v0.2.1 h1:05xhhdRNAdS3apYm7JRjOqngf4xruaW959jmRxGDuSU= +github.com/rboyer/safeio v0.2.1/go.mod h1:Cq/cEPK+YXFn622lsQ0K4KsPZSPtaptHHEldsy7Fmig= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 h1:Wdi9nwnhFNAlseAOekn6B5G/+GMtks9UKbvRU/CMM/o= +github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03/go.mod h1:gRAiPF5C5Nd0eyyRdqIu9qTiFSoZzpTq727b5B8fkkU= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= -github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.4.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -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/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/conswriter v0.0.0-20180208195008-f5ae3917a627/go.mod h1:7zjs06qF79/FKAJpBvFx3P8Ww4UTIMAe+lpNXDHziac= +github.com/sean-/pager v0.0.0-20180208200047-666be9bf53b5/go.mod h1:BeybITEsBEg6qbIiqJ6/Bqeq25bCLbL7YFmpaFfJDuM= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= +github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sethvargo/go-limiter v0.7.1 h1:wWNhTj0pxjyJ7wuJHpRJpYwJn+bUnjYfw2a85eu5w9U= +github.com/sethvargo/go-limiter v0.7.1/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU= +github.com/shirou/gopsutil/v3 v3.22.6 h1:FnHOFOh+cYAM0C30P+zysPISzlknLC5Z1G4EAElznfQ= +github.com/shirou/gopsutil/v3 v3.22.6/go.mod h1:EdIubSnZhbAvBS1yJ7Xi+AShB/hxwLHOMz4MCYz7yMs= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= -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= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/snowflakedb/gosnowflake v1.13.0 h1:NQoy4mnHUmBuruJhzAGVRO9YLpFxayYTCLf+dxvG7bk= +github.com/snowflakedb/gosnowflake v1.13.0/go.mod h1:nwiPNHaS3EGxnW1rr10ascVYFLA4EKrqMX2TxPt0+N4= +github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d h1:bVQRCxQvfjNUeRqaY/uT0tFuvuFY0ulgnczuR684Xic= +github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d/go.mod h1:Cw4GTlQccdRGSEf6KiMju767x0NEHE0YIVPJSaXjlsw= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.2-0.20210216022020-dd874f9dd33b/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.1 h1:/m2cTZHpqgofDsrwPqsASI6fSNMNhb+9EmUYtHEV2Uk= +github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.1/go.mod h1:Z5KcoM0YLC7INlNhEezeIZ0TZNYf7WSNO0Lvah4DSeQ= 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.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tencentcloud/tencentcloud-sdk-go v1.0.162 h1:8fDzz4GuVg4skjY2B0nMN7h6uN61EDVkuLyI2+qGHhI= +github.com/tencentcloud/tencentcloud-sdk-go v1.0.162/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI= +github.com/tilinna/clock v1.1.0 h1:6IQQQCo6KoBxVudv6gwtY8o4eDfhHo8ojA5dP0MfhSs= +github.com/tilinna/clock v1.1.0/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao= +github.com/tink-crypto/tink-go/v2 v2.2.0 h1:L2Da0F2Udh2agtKztdr69mV/KpnY3/lGTkMgLTVIXlA= +github.com/tink-crypto/tink-go/v2 v2.2.0/go.mod h1:JJ6PomeNPF3cJpfWC0lgyTES6zpJILkAX0cJNwlS3xU= +github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= +github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= +github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= +github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/vmware/govmomi v0.18.0 h1:f7QxSmP7meCtoAmiKZogvVbLInT+CZx6Px6K5rYsJZo= +github.com/vmware/govmomi v0.18.0/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.4.0-beta.0 h1:U7Y9yH6ZojEo5/BDFMXDXD1RNx9L7iKxudzqR68jLaM= +go.etcd.io/bbolt v1.4.0-beta.0/go.mod h1:Qv5yHB6jkQESXT/uVfxJgUPMqgAyhL0GLxcQaz9bSec= +go.mongodb.org/atlas v0.37.0 h1:zQnO1o5+bVP9IotpAYpres4UjMD2F4nwNEFTZhNL4ck= +go.mongodb.org/atlas v0.37.0/go.mod h1:DJYtM+vsEpPEMSkQzJnFHrT0sP7ev6cseZc/GGjJYG8= +go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= +go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 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/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 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/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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-20220128215802-99c3d69c2c27/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= +golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.221.0 h1:qzaJfLhDsbMeFee8zBRdt/Nc+xmOuafD/dbdgGfutOU= +google.golang.org/api v0.221.0/go.mod h1:7sOU2+TL4TxUTdbi0gWgAIg7tH5qBXxoyhtL+9x3biQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +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-20180628173108-788fd7840127/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-20200227125254-8fa46927fb4f/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 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/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI= +gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +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.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +layeh.com/radius v0.0.0-20231213012653-1006025d24f8 h1:orYXpi6BJZdvgytfHH4ybOe4wHnLbbS71Cmd8mWdZjs= +layeh.com/radius v0.0.0-20231213012653-1006025d24f8/go.mod h1:QRf+8aRqXc019kHkpcs/CTgyWXFzf+bxlsyuo2nAl1o= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=