diff --git a/.github/workflows/develop.yaml b/.github/workflows/develop.yaml index ce2fd4e..d201a6c 100644 --- a/.github/workflows/develop.yaml +++ b/.github/workflows/develop.yaml @@ -24,10 +24,10 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf #v3.2.0 + uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 #v3.4.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 #v3.8.0 + uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca #v3.9.0 - name: Login to Docker Hub uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 #v3.3.0 @@ -35,6 +35,11 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Verify QEMU installation + run: | + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + docker run --rm --platform linux/arm64 debian:latest uname -m + - name: Set build info run: | echo ${GITHUB_SHA::7} > ./agent/version/BUILD_COMMIT.txt @@ -42,7 +47,7 @@ jobs: echo $LATEST_RELEASE > ./agent/version/BUILD_VERSION.txt - name: Build image and push - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 #v6.10.0 + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 #v6.14.0 with: context: . file: agent/docker/Dockerfile diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7e21471..ebeedc9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -132,10 +132,10 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf #v3.2.0 + uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 #v3.4.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 #v3.8.0 + uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca #v3.9.0 - name: Login to Docker Hub uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 #v3.3.0 @@ -143,13 +143,18 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Verify QEMU installation + run: | + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + docker run --rm --platform linux/arm64 debian:latest uname -m + - name: Set build info run: | echo $BUILD_COMMIT > ./agent/version/BUILD_COMMIT.txt echo $BUILD_VERSION > ./agent/version/BUILD_VERSION.txt - name: Build image and push - uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 #v6.10.0 + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 #v6.14.0 with: context: . file: agent/docker/Dockerfile diff --git a/README.md b/README.md index 7828584..f48d1da 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ orb: ... ``` -Currently, only the `local` manager is supported, which retrieves policies from the local configuration file passed to the agent. +Currently, only the `local` and `git` sources are supported for config manager. +- [Local](./docs/configs/local.md) +- [Git](./docs/configs/git.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. diff --git a/agent/agent.go b/agent/agent.go index b1e97c9..884da51 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -7,15 +7,14 @@ import ( "runtime" "time" - mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/google/uuid" "github.com/mitchellh/mapstructure" - "github.com/orb-community/orb/fleet" "go.uber.org/zap" "github.com/netboxlabs/orb-agent/agent/backend" "github.com/netboxlabs/orb-agent/agent/config" - manager "github.com/netboxlabs/orb-agent/agent/policyMgr" + "github.com/netboxlabs/orb-agent/agent/configmgr" + "github.com/netboxlabs/orb-agent/agent/policymgr" "github.com/netboxlabs/orb-agent/agent/version" ) @@ -32,8 +31,6 @@ type Agent interface { type orbAgent struct { logger *zap.Logger config config.Config - client mqtt.Client - agentID string backends map[string]backend.Backend backendState map[string]*backend.State backendsCommon config.BackendCommons @@ -42,15 +39,9 @@ type orbAgent struct { asyncContext context.Context - hbTicker *time.Ticker heartbeatCtx context.Context heartbeatCancel context.CancelFunc - // Agent RPC channel, configured from command line - baseTopic string - rpcFromCoreTopic string - heartbeatsTopic string - // Retry Mechanism to ensure the Request is received groupRequestSucceeded context.CancelFunc policyRequestSucceeded context.CancelFunc @@ -58,8 +49,8 @@ type orbAgent struct { // AgentGroup channels sent from core groupsInfos map[string]groupInfo - policyManager manager.PolicyManager - configManager config.Manager + policyManager policymgr.PolicyManager + configManager configmgr.Manager } type groupInfo struct { @@ -71,7 +62,7 @@ var _ Agent = (*orbAgent)(nil) // New creates a new agent func New(logger *zap.Logger, c config.Config) (Agent, error) { - pm, err := manager.New(logger, c) + pm, err := policymgr.New(logger, c) if err != nil { logger.Error("error during create policy manager, exiting", zap.Error(err)) return nil, err @@ -80,31 +71,11 @@ func New(logger *zap.Logger, c config.Config) (Agent, error) { logger.Error("policy manager failed to get repository", zap.Error(err)) return nil, err } - cm := config.New(logger, c.OrbAgent.ConfigManager) + cm := configmgr.New(logger, pm, c.OrbAgent.ConfigManager) return &orbAgent{logger: logger, config: c, policyManager: pm, configManager: cm, groupsInfos: make(map[string]groupInfo)}, nil } -func (a *orbAgent) managePolicies() error { - if a.config.OrbAgent.Policies == nil { - return errors.New("no policies specified") - } - - for beName, policy := range a.config.OrbAgent.Policies { - _, ok := a.backends[beName] - if !ok { - return errors.New("backend not found: " + beName) - } - for pName, data := range policy { - id := uuid.NewString() - payload := fleet.AgentPolicyRPCPayload{Action: "manage", Name: pName, DatasetID: id, Backend: beName, Version: 1, Data: data} - a.policyManager.ManagePolicy(payload) - } - - } - return 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)) @@ -120,7 +91,7 @@ func (a *orbAgent) startBackends(agentCtx context.Context) error { return fmt.Errorf("failed to decode common backend config: %w", err) } } - commonConfig.Otel.AgentTags = a.config.OrbAgent.Tags + commonConfig.Otel.AgentLabels = a.config.OrbAgent.Labels a.backendsCommon = commonConfig delete(a.config.OrbAgent.Backends, "common") @@ -171,19 +142,12 @@ func (a *orbAgent) Start(ctx context.Context, cancelFunc context.CancelFunc) err a.rpcFromCancelFunc = cancelAllAsync a.cancelFunction = cancelFunc a.logger.Info("agent started", zap.String("version", version.GetBuildVersion()), zap.Any("routine", agentCtx.Value(routineKey))) - mqtt.CRITICAL = &agentLoggerCritical{a: a} - mqtt.ERROR = &agentLoggerError{a: a} - - if a.config.OrbAgent.Debug.Enable { - a.logger.Info("debug logging enabled") - mqtt.DEBUG = &agentLoggerDebug{a: a} - } if err := a.startBackends(ctx); err != nil { return err } - if err := a.managePolicies(); err != nil { + if err := a.configManager.Start(a.config, a.backends); err != nil { return err } @@ -193,9 +157,7 @@ func (a *orbAgent) Start(ctx context.Context, cancelFunc context.CancelFunc) err } func (a *orbAgent) logonWithHeartbeat() { - a.hbTicker = time.NewTicker(HeartbeatFreq) a.heartbeatCtx, a.heartbeatCancel = a.extendContext("heartbeat") - go a.sendHeartbeats(a.heartbeatCtx, a.heartbeatCancel) a.logger.Info("heartbeat routine started") } @@ -204,11 +166,6 @@ func (a *orbAgent) logoffWithHeartbeat(ctx context.Context) { if a.heartbeatCtx != nil { a.heartbeatCancel() } - if a.client != nil && a.client.IsConnected() { - if token := a.client.Unsubscribe(a.rpcFromCoreTopic); token.Wait() && token.Error() != nil { - a.logger.Warn("failed to unsubscribe to RPC channel", zap.Error(token.Error())) - } - } } func (a *orbAgent) Stop(ctx context.Context) { @@ -225,9 +182,6 @@ func (a *orbAgent) Stop(ctx context.Context) { } } a.logoffWithHeartbeat(ctx) - if a.client != nil && a.client.IsConnected() { - a.client.Disconnect(0) - } a.logger.Debug("stopping agent with number of go routines and go calls", zap.Int("goroutines", runtime.NumGoroutine()), zap.Int64("gocalls", runtime.NumCgoCall())) if a.policyRequestSucceeded != nil { a.policyRequestSucceeded() @@ -261,7 +215,6 @@ func (a *orbAgent) RestartBackend(ctx context.Context, name string, reason strin 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)) } - be.SetCommsClient(a.agentID, &a.client, fmt.Sprintf("%s/?/%s", a.baseTopic, name)) return nil } diff --git a/agent/backend/backend.go b/agent/backend/backend.go index c85c277..7de278d 100644 --- a/agent/backend/backend.go +++ b/agent/backend/backend.go @@ -4,7 +4,6 @@ import ( "context" "time" - mqtt "github.com/eclipse/paho.mqtt.golang" "go.uber.org/zap" "github.com/netboxlabs/orb-agent/agent/config" @@ -49,7 +48,6 @@ 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 - SetCommsClient(string, *mqtt.Client, string) Version() (string, error) Start(ctx context.Context, cancelFunc context.CancelFunc) error Stop(ctx context.Context) error diff --git a/agent/backend/devicediscovery/device_discovery.go b/agent/backend/devicediscovery/device_discovery.go index e8f6478..0fffa44 100644 --- a/agent/backend/devicediscovery/device_discovery.go +++ b/agent/backend/devicediscovery/device_discovery.go @@ -8,7 +8,6 @@ import ( "net/http" "time" - mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/go-cmd/cmd" "go.uber.org/zap" "gopkg.in/yaml.v3" @@ -50,8 +49,6 @@ type deviceDiscoveryBackend struct { statusChan <-chan cmd.Status cancelFunc context.CancelFunc ctx context.Context - - mqttClient *mqtt.Client } type info struct { @@ -87,10 +84,6 @@ func (d *deviceDiscoveryBackend) Configure(logger *zap.Logger, repo policies.Pol return nil } -func (d *deviceDiscoveryBackend) SetCommsClient(_ string, client *mqtt.Client, _ string) { - d.mqttClient = client -} - func (d *deviceDiscoveryBackend) Version() (string, error) { var info info err := d.request("status", &info, http.MethodGet, http.NoBody, "application/json", versionTimeout) diff --git a/agent/backend/networkdiscovery/network_discovery.go b/agent/backend/networkdiscovery/network_discovery.go index 4fcd453..d0581bf 100644 --- a/agent/backend/networkdiscovery/network_discovery.go +++ b/agent/backend/networkdiscovery/network_discovery.go @@ -8,7 +8,6 @@ import ( "net/http" "time" - mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/go-cmd/cmd" "go.uber.org/zap" "gopkg.in/yaml.v3" @@ -50,8 +49,6 @@ type networkDiscoveryBackend struct { statusChan <-chan cmd.Status cancelFunc context.CancelFunc ctx context.Context - - mqttClient *mqtt.Client } type info struct { @@ -87,10 +84,6 @@ func (d *networkDiscoveryBackend) Configure(logger *zap.Logger, repo policies.Po return nil } -func (d *networkDiscoveryBackend) SetCommsClient(_ string, client *mqtt.Client, _ string) { - d.mqttClient = client -} - func (d *networkDiscoveryBackend) Version() (string, error) { var info info err := d.request("status", &info, http.MethodGet, http.NoBody, "application/json", versionTimeout) diff --git a/agent/backend/otel/comms.go b/agent/backend/otel/comms.go deleted file mode 100644 index 31bf30b..0000000 --- a/agent/backend/otel/comms.go +++ /dev/null @@ -1,16 +0,0 @@ -package otel - -import ( - "fmt" - "strings" - - mqtt "github.com/eclipse/paho.mqtt.golang" -) - -func (o *openTelemetryBackend) SetCommsClient(agentID string, client *mqtt.Client, baseTopic string) { - o.mqttClient = client - otelBaseTopic := strings.Replace(baseTopic, "?", "otlp", 1) - o.otlpMetricsTopic = fmt.Sprintf("%s/m/%c", otelBaseTopic, agentID[0]) - o.otlpTracesTopic = fmt.Sprintf("%s/t/%c", otelBaseTopic, agentID[0]) - o.otlpLogsTopic = fmt.Sprintf("%s/l/%c", otelBaseTopic, agentID[0]) -} diff --git a/agent/backend/otel/exporter_builder.go b/agent/backend/otel/exporter_builder.go deleted file mode 100644 index 60991dc..0000000 --- a/agent/backend/otel/exporter_builder.go +++ /dev/null @@ -1,134 +0,0 @@ -package otel - -import ( - "strconv" - - "go.uber.org/zap" - "gopkg.in/yaml.v3" -) - -// ExporterBuilder is an interface that defines the methods to build an exporter -type ExporterBuilder interface { - GetStructFromYaml(yamlString string) (openTelemetryConfig, error) - MergeDefaultValueWithPolicy(config openTelemetryConfig, policyName string) (openTelemetryConfig, error) -} - -type openTelemetryConfig struct { - Receivers map[string]interface{} `yaml:"receivers"` - Processors map[string]interface{} `yaml:"processors,omitempty"` - Extensions map[string]interface{} `yaml:"extensions,omitempty"` - Exporters map[string]interface{} `yaml:"exporters"` - Service *service `yaml:"service"` -} - -type defaultOtlpExporter struct { - Endpoint string `yaml:"endpoint"` - TLS *tls `yaml:"tls"` -} - -type tls struct { - Insecure bool `yaml:"insecure"` -} - -type service struct { - Pipelines *pipelines `yaml:"pipelines"` - Telemetry *telemetry `yaml:"telemetry,omitempty"` -} - -type telemetry struct { - Metrics *metrics `yaml:"metrics,omitempty"` - Logs *logs `yaml:"logs,omitempty"` - Traces *traces `yaml:"traces,omitempty"` -} - -type metrics struct { - Level string `yaml:"level,omitempty"` - Address string `yaml:"address,omitempty"` -} - -type traces struct { - Enabled bool `yaml:"enabled"` -} - -type logs struct { - Enabled bool `yaml:"enabled"` -} - -type pipelines struct { - Metrics *pipeline `yaml:"metrics,omitempty"` - Traces *pipeline `yaml:"traces,omitempty"` - Logs *pipeline `yaml:"logs,omitempty"` -} - -type pipeline struct { - Exporters []string `yaml:"exporters,omitempty"` - Receivers []string `yaml:"receivers,omitempty"` - Processors []string `yaml:"processors,omitempty"` -} - -func getExporterBuilder(logger *zap.Logger, host string, port int) *exporterBuilder { - return &exporterBuilder{logger: logger, host: host, port: port} -} - -type exporterBuilder struct { - logger *zap.Logger - host string - port int -} - -func (e *exporterBuilder) GetStructFromYaml(yamlString string) (openTelemetryConfig, error) { - var config openTelemetryConfig - if err := yaml.Unmarshal([]byte(yamlString), &config); err != nil { - e.logger.Error("failed to unmarshal yaml string", zap.Error(err)) - return config, err - } - return config, nil -} - -func (e *exporterBuilder) MergeDefaultValueWithPolicy(config openTelemetryConfig, policyID string, policyName string) (openTelemetryConfig, error) { - endpoint := e.host + ":" + strconv.Itoa(e.port) - defaultOtlpExporter := defaultOtlpExporter{ - Endpoint: endpoint, - TLS: &tls{ - Insecure: true, - }, - } - - // Override any openTelemetry exporter that may come, to connect to agent's otlp receiver - config.Exporters = map[string]interface{}{ - "otlp": &defaultOtlpExporter, - } - if config.Processors == nil { - config.Processors = make(map[string]interface{}) - } - config.Processors["transform/policy_data"] = map[string]interface{}{ - "metric_statements": map[string]interface{}{ - "context": "scope", - "statements": []string{ - `set(attributes["policy_id"], "` + policyID + `")`, - `set(attributes["policy_name"], "` + policyName + `")`, - }, - }, - } - if config.Extensions == nil { - config.Extensions = make(map[string]interface{}) - } - tel := &telemetry{ - Metrics: &metrics{Level: "none"}, - } - config.Service.Telemetry = tel - // Override metrics exporter and append attributes/policy_data processor - if config.Service.Pipelines.Metrics != nil { - config.Service.Pipelines.Metrics.Exporters = []string{"otlp"} - config.Service.Pipelines.Metrics.Processors = append(config.Service.Pipelines.Metrics.Processors, "transform/policy_data") - } - if config.Service.Pipelines.Traces != nil { - config.Service.Pipelines.Traces.Exporters = []string{"otlp"} - config.Service.Pipelines.Traces.Processors = append(config.Service.Pipelines.Traces.Processors, "transform/policy_data") - } - if config.Service.Pipelines.Logs != nil { - config.Service.Pipelines.Logs.Exporters = []string{"otlp"} - config.Service.Pipelines.Logs.Processors = append(config.Service.Pipelines.Logs.Processors, "transform/policy_data") - } - return config, nil -} diff --git a/agent/backend/otel/exporter_builder_test.go b/agent/backend/otel/exporter_builder_test.go deleted file mode 100644 index cbcdd50..0000000 --- a/agent/backend/otel/exporter_builder_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package otel - -import ( - "testing" - - "go.uber.org/zap" -) - -func TestBuildDefaultPolicy(t *testing.T) { - testCases := []struct { - caseName string - inputString string - policyID string - policyName string - expectedStruct openTelemetryConfig - processedString string - wantErr error - }{ - { - caseName: "success default policy test", - inputString: ` ---- -receivers: - httpcheck: - targets: - - endpoint: http://orb.live - method: GET - - endpoint: http://orb.community - method: GET - collection_interval: 60s -exporters: - otlp: - endpoint: localhost:4317 - tls: - insecure: true - logging: - verbosity: detailed - sampling_initial: 5 -service: - pipelines: - metrics: - exporters: - - otlp - receivers: - - httpcheck -`, - policyID: "test-policy-id", - policyName: "test-policy", - }, - } - for _, testCase := range testCases { - t.Run(testCase.caseName, func(t *testing.T) { - logger := zap.NewNop() - exporterBuilder := getExporterBuilder(logger, "localhost", 4317) - gotOtelConfig, err := exporterBuilder.GetStructFromYaml(testCase.inputString) - if err != nil { - t.Errorf("failed to merge default value with policy: %v", err) - } - expectedStruct, err := exporterBuilder.MergeDefaultValueWithPolicy(gotOtelConfig, testCase.policyID, testCase.policyName) - if err != nil { - t.Errorf("failed to merge default value with policy: %v", err) - } - if _, ok := expectedStruct.Processors["transform/policy_data"]; !ok { - t.Error("missing required attributes/policy_data processor", err) - } - }) - } -} diff --git a/agent/backend/otel/otel.go b/agent/backend/otel/otel.go index 7d2f740..8ddac7e 100644 --- a/agent/backend/otel/otel.go +++ b/agent/backend/otel/otel.go @@ -1,16 +1,16 @@ package otel import ( + "bytes" "context" + "errors" "fmt" - "os" - "os/exec" - "strconv" + "net/http" "time" - mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/go-cmd/cmd" "go.uber.org/zap" + "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" "github.com/netboxlabs/orb-agent/agent/config" @@ -20,36 +20,47 @@ import ( var _ backend.Backend = (*openTelemetryBackend)(nil) const ( - defaultPath = "otelcol-contrib" - defaultHost = "localhost" - defaultPort = 4317 + defaultExec = "otlpinf" + defaultAPIHost = "localhost" + defaultAPIPort = "10222" + versionTimeout = 5 + capabilitiesTimeout = 5 + readinessBackoff = 10 + readinessTimeout = 10 + applyPolicyTimeout = 10 + removePolicyTimeout = 20 ) type openTelemetryBackend struct { - logger *zap.Logger - startTime time.Time - - // policies - policyRepo policies.PolicyRepo - policyConfigDirectory string - agentTags map[string]string - - // Context for controlling the context cancellation - mainContext context.Context - runningCollectors map[string]runningPolicy - mainCancelFunction context.CancelFunc - - mqttClient *mqtt.Client - - otlpMetricsTopic string - otlpTracesTopic string - otlpLogsTopic string - otelReceiverTaps []string - otelCurrVersion string - - otelReceiverHost string - otelReceiverPort int - otelExecutablePath string + logger *zap.Logger + policyRepo policies.PolicyRepo + exec string + + apiHost string + apiPort string + apiProtocol string + + startTime time.Time + proc *cmd.Cmd + agentLabels map[string]string + statusChan <-chan cmd.Status + cancelFunc context.CancelFunc + ctx context.Context +} + +type info struct { + StartTime string `json:"start_time"` + Version string `json:"version"` + UpTime float64 `json:"up_time"` +} + +// Register registers otel backend +func Register() bool { + backend.Register("otel", &openTelemetryBackend{ + apiProtocol: "http", + exec: defaultExec, + }) + return true } // Configure initializes the backend with the given configuration @@ -57,157 +68,226 @@ func (o *openTelemetryBackend) Configure(logger *zap.Logger, repo policies.Polic config map[string]interface{}, common config.BackendCommons, ) error { o.logger = logger - o.logger.Info("configuring OpenTelemetry backend") o.policyRepo = repo - var err error - o.otelReceiverTaps = []string{"otelcol-contrib", "receivers", "processors", "extensions"} - o.policyConfigDirectory, err = os.MkdirTemp("", "otel-policies") - if err != nil { - o.logger.Error("failed to create temporary directory for policy configs", zap.Error(err)) - return err - } - if path, ok := config["binary"].(string); ok { - o.otelExecutablePath = path - } else { - o.otelExecutablePath = defaultPath - } - _, err = exec.LookPath(o.otelExecutablePath) - if err != nil { - o.logger.Error("otelcol-contrib: binary not found", zap.Error(err)) - return err - } - if err != nil { - o.logger.Error("failed to create temporary directory for policy configs", zap.Error(err)) - return err - } - o.agentTags = common.Otel.AgentTags - if otelPort, ok := config["otlp_port"]; ok { - o.otelReceiverPort, err = strconv.Atoi(otelPort.(string)) - if err != nil { - o.logger.Error("failed to parse otlp port using default", zap.Error(err)) - o.otelReceiverPort = defaultPort - } - } else { - o.otelReceiverPort = defaultPort + var prs bool + if o.apiHost, prs = config["host"].(string); !prs { + o.apiHost = defaultAPIHost } - if otelHost, ok := config["otlp_host"].(string); ok { - o.otelReceiverHost = otelHost - } else { - o.otelReceiverHost = defaultHost + if o.apiPort, prs = config["port"].(string); !prs { + o.apiPort = defaultAPIPort } + o.agentLabels = common.Otel.AgentLabels + return nil } func (o *openTelemetryBackend) GetInitialState() backend.RunningStatus { - return backend.Waiting + return backend.Unknown } func (o *openTelemetryBackend) Version() (string, error) { - if o.otelCurrVersion != "" { - return o.otelCurrVersion, nil - } - ctx, cancel := context.WithTimeout(o.mainContext, 60*time.Second) - var versionOutput string - command := cmd.NewCmd(o.otelExecutablePath, "--version") - status := command.Start() - select { - case finalStatus := <-status: - if finalStatus.Error != nil { - o.logger.Error("error during call of otelcol-contrib version", zap.Error(finalStatus.Error)) - cancel() - return "", finalStatus.Error - } else { - output := finalStatus.Stdout - o.otelCurrVersion = output[0] - versionOutput = output[0] - } - case <-ctx.Done(): - o.logger.Error("timeout during getting version", zap.Error(ctx.Err())) + var info info + err := o.request("status", &info, http.MethodGet, http.NoBody, "application/json", versionTimeout) + if err != nil { + return "", err } - - cancel() - o.logger.Info("running opentelemetry-contrib version", zap.String("version", versionOutput)) - - return versionOutput, nil + return info.Version, nil } func (o *openTelemetryBackend) Start(ctx context.Context, cancelFunc context.CancelFunc) (err error) { - o.runningCollectors = make(map[string]runningPolicy) - o.mainCancelFunction = cancelFunc - o.mainContext = ctx o.startTime = time.Now() - currentWd, err := os.Getwd() - if err != nil { - o.otelExecutablePath = currentWd + "/otelcol-contrib" + o.cancelFunc = cancelFunc + o.ctx = ctx + + pvOptions := []string{ + "run", + "--server_host", o.apiHost, + "--server_port", o.apiPort, } - currentVersion, err := o.Version() - if err != nil { - cancelFunc() - o.logger.Error("error during getting current version", zap.Error(err)) - return err + + o.logger.Info("opentelemetry infinity startup", zap.Strings("arguments", pvOptions)) + + o.proc = cmd.NewCmdOptions(cmd.Options{ + Buffered: false, + Streaming: true, + }, o.exec, pvOptions...) + o.statusChan = o.proc.Start() + + // log STDOUT and STDERR lines streaming from Cmd + doneChan := make(chan struct{}) + go func() { + defer func() { + if doneChan != nil { + close(doneChan) + } + }() + for o.proc.Stdout != nil || o.proc.Stderr != nil { + select { + case line, open := <-o.proc.Stdout: + if !open { + o.proc.Stdout = nil + continue + } + o.logger.Info("opentelemetry infinity stdout", zap.String("log", line)) + case line, open := <-o.proc.Stderr: + if !open { + o.proc.Stderr = nil + continue + } + o.logger.Info("opentelemetry infinity stderr", zap.String("log", line)) + } + } + }() + + // wait for simple startup errors + time.Sleep(time.Second) + + status := o.proc.Status() + + if status.Error != nil { + o.logger.Error("opentelemetry infinity startup error", zap.Error(status.Error)) + return status.Error } - o.logger.Info("starting open-telemetry backend using version", zap.String("version", currentVersion)) - policiesData, err := o.policyRepo.GetAll() - if err != nil { - cancelFunc() - o.logger.Error("failed to start otel backend, policies are absent") - return err + + if status.Complete { + err := o.proc.Stop() + if err != nil { + o.logger.Error("proc.Stop error", zap.Error(err)) + } + return errors.New("opentelemetry infinity startup error, check log") } - for _, policyData := range policiesData { - if err := o.ApplyPolicy(policyData, true); err != nil { - o.logger.Error("failed to start otel backend, failed to apply policy", zap.Error(err)) - cancelFunc() - return err + + o.logger.Info("opentelemetry infinity process started", zap.Int("pid", status.PID)) + + var readinessErr error + for backoff := 0; backoff < readinessBackoff; backoff++ { + version, readinessErr := o.Version() + if readinessErr == nil { + o.logger.Info("opentelemetry infinity readiness ok, got version ", zap.String("device_discovery_version", version)) + break } - o.logger.Info("policy applied successfully", zap.String("policy_id", policyData.ID)) + backoffDuration := time.Duration(backoff) * time.Second + o.logger.Info("opentelemetry infinity is not ready, trying again with backoff", zap.String("backoff backoffDuration", backoffDuration.String())) + time.Sleep(backoffDuration) + } + + if readinessErr != nil { + o.logger.Error("opentelemetry infinity error on readiness", zap.Error(readinessErr)) + err := o.proc.Stop() + if err != nil { + o.logger.Error("proc.Stop error", zap.Error(err)) + } + return readinessErr } return nil } -func (o *openTelemetryBackend) Stop(_ context.Context) error { - o.logger.Info("stopping all running policies") - o.mainCancelFunction() - for policyID, policyEntry := range o.runningCollectors { - o.logger.Debug("stopping policy context", zap.String("policy_id", policyID)) - policyEntry.ctx.Done() +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")))) + 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.Info("opentelemetry infinity process stopped", zap.Int("pid", finalStatus.PID), zap.Int("exit_code", finalStatus.Exit)) return nil } func (o *openTelemetryBackend) FullReset(ctx context.Context) error { - o.logger.Info("restarting otel backend", zap.Int("running policies", len(o.runningCollectors))) - backendCtx, cancelFunc := context.WithCancel(context.WithValue(ctx, config.ContextKey("routine"), "otel")) + // force a stop, which stops scrape as well. if proc is dead, it no ops. + if state, _, _ := o.getProcRunningStatus(); state == backend.Running { + if err := o.Stop(ctx); err != nil { + o.logger.Error("failed to stop backend on restart procedure", zap.Error(err)) + return err + } + } + // for each policy, restart the scraper + 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)) return err } return nil } -// Register registers otel backend -func Register() bool { - backend.Register("otel", &openTelemetryBackend{}) - return true -} - func (o *openTelemetryBackend) GetStartTime() time.Time { return o.startTime } // GetCapabilities this will only print a default backend config -func (o *openTelemetryBackend) GetCapabilities() (capabilities map[string]interface{}, err error) { - capabilities = make(map[string]interface{}) - capabilities["taps"] = o.otelReceiverTaps - return +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) + if err != nil { + return nil, err + } + return caps, nil } // GetRunningStatus returns cross-reference the Processes using the os, with the policies and contexts func (o *openTelemetryBackend) GetRunningStatus() (backend.RunningStatus, string, error) { - amountCollectors := len(o.runningCollectors) - if amountCollectors > 0 { - return backend.Running, fmt.Sprintf("opentelemetry backend running with %d policies", amountCollectors), nil + // first check process status + runningStatus, errMsg, err := o.getProcRunningStatus() + // if it's not running, we're done + if runningStatus != backend.Running { + return runningStatus, errMsg, err + } + // if it's running, check REST API availability too + if _, aiErr := o.Version(); aiErr != nil { + // process is running, but REST API is not accessible + return backend.BackendError, "process running, REST API unavailable", aiErr + } + return runningStatus, "", nil +} + +func (o *openTelemetryBackend) 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 := 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.Debug("opentelemetry infinity policy apply", zap.String("policy_id", data.ID), zap.Any("data", data.Data)) + + fullPolicy := map[string]interface{}{ + 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)) + return err + } + + var resp map[string]interface{} + err = o.request("policies", &resp, http.MethodPost, bytes.NewBuffer(policyYaml), "application/x-yaml", applyPolicyTimeout) + if err != nil { + o.logger.Warn("policy application failure", zap.String("policy_id", data.ID), zap.ByteString("policy", policyYaml)) + return err } - return backend.Waiting, "opentelemetry backend is waiting for policy to come to start running", nil + + return nil +} + +func (o *openTelemetryBackend) RemovePolicy(data policies.PolicyData) error { + o.logger.Debug("opentelemetry 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 := o.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/otel/policy.go b/agent/backend/otel/policy.go deleted file mode 100644 index de8f4a1..0000000 --- a/agent/backend/otel/policy.go +++ /dev/null @@ -1,178 +0,0 @@ -package otel - -import ( - "context" - "errors" - "fmt" - "os" - - "github.com/go-cmd/cmd" - "go.uber.org/zap" - "golang.org/x/exp/slices" - "gopkg.in/yaml.v3" - - "github.com/netboxlabs/orb-agent/agent/config" - "github.com/netboxlabs/orb-agent/agent/policies" -) - -const tempFileNamePattern = "otel-%s-config.yml" - -type runningPolicy struct { - ctx context.Context - cancel context.CancelFunc - policyID string - policyData policies.PolicyData - statusChan *cmd.Status -} - -func (o *openTelemetryBackend) ApplyPolicy(newPolicyData policies.PolicyData, updatePolicy bool) error { - o.logger.Debug("applying policy", zap.String("policy_id", newPolicyData.ID)) - policyYaml, err := yaml.Marshal(newPolicyData.Data) - if err != nil { - o.logger.Warn("yaml policy marshal failure", zap.String("policy_id", newPolicyData.ID), zap.Any("policy", newPolicyData.Data)) - return err - } - builder := getExporterBuilder(o.logger, o.otelReceiverHost, o.otelReceiverPort) - otelConfig, err := builder.GetStructFromYaml(string(policyYaml)) - if err != nil { - return err - } - if err = o.ValidatePolicy(otelConfig); err != nil { - return err - } - otelConfig, err = builder.MergeDefaultValueWithPolicy(otelConfig, newPolicyData.ID, newPolicyData.Name) - if err != nil { - return err - } - newPolicyYaml, err := yaml.Marshal(otelConfig) - if err != nil { - return err - } - if !updatePolicy || !o.policyRepo.Exists(newPolicyData.ID) { - newPolicyPath := fmt.Sprintf("%s/%s", o.policyConfigDirectory, fmt.Sprintf(tempFileNamePattern, newPolicyData.ID)) - o.logger.Info("received new policy", - zap.String("policy_id", newPolicyData.ID), - zap.Int32("version", newPolicyData.Version), - zap.String("policy_path", newPolicyPath)) - if err := os.WriteFile(newPolicyPath, newPolicyYaml, os.ModeTemporary); err != nil { - return err - } - if err = o.addRunner(newPolicyData, newPolicyPath); err != nil { - return err - } - } else { - currentPolicyData, err := o.policyRepo.Get(newPolicyData.ID) - if err != nil { - return err - } - if currentPolicyData.Version <= newPolicyData.Version { - currentPolicyPath := fmt.Sprintf("%s/%s", o.policyConfigDirectory, fmt.Sprintf(tempFileNamePattern, currentPolicyData.ID)) - o.logger.Info("received new policy version", - zap.String("policy_id", newPolicyData.ID), - zap.Int32("version", newPolicyData.Version), - zap.String("policy_path", currentPolicyPath)) - - o.removePolicyControl(currentPolicyData.ID) - - if err := os.WriteFile(currentPolicyPath, newPolicyYaml, os.ModeTemporary); err != nil { - return err - } - if err := o.addRunner(newPolicyData, currentPolicyPath); err != nil { - return err - } - if err := o.policyRepo.Update(newPolicyData); err != nil { - return err - } - } else { - o.logger.Info("current policy version is newer than the one being applied, skipping", - zap.String("policy_id", newPolicyData.ID), - zap.Int32("current_version", currentPolicyData.Version), - zap.Int32("incoming_version", newPolicyData.Version)) - } - } - - return nil -} - -func (o *openTelemetryBackend) addRunner(policyData policies.PolicyData, policyFilePath string) error { - policyContext, policyCancel := context.WithCancel(context.WithValue(o.mainContext, config.ContextKey("policy_id"), policyData.ID)) - command := cmd.NewCmdOptions(cmd.Options{Buffered: false, Streaming: true}, o.otelExecutablePath, "--config", policyFilePath) - go func(ctx context.Context, logger *zap.Logger) { - status := command.Start() - o.logger.Info("starting otel policy", zap.String("policy_id", policyData.ID), - zap.Any("status", command.Status()), zap.Int("process id", command.Status().PID)) - for !command.Status().Complete { - select { - case v := <-ctx.Done(): - err := command.Stop() - if err != nil && !slices.Contains([]string{"command not running", "no such process"}, err.Error()) { - logger.Error("failed to stop otel", zap.String("policy_id", policyData.ID), - zap.Any("value", v), zap.Error(err)) - } - return - case line := <-command.Stdout: - if line != "" { - logger.Info("otel stdout", zap.String("policy_id", policyData.ID), zap.String("line", line)) - } - case line := <-command.Stderr: - if line != "" { - logger.Warn("otel stderr", zap.String("policy_id", policyData.ID), zap.String("line", line)) - } - case finalStatus := <-status: - logger.Info("otel finished", zap.String("policy_id", policyData.ID), zap.Any("status", finalStatus)) - } - } - }(policyContext, o.logger) - status := command.Status() - policyEntry := runningPolicy{ - ctx: policyContext, - cancel: policyCancel, - policyID: policyData.ID, - policyData: policyData, - statusChan: &status, - } - o.addPolicyControl(policyEntry, policyData.ID) - - return nil -} - -func (o *openTelemetryBackend) addPolicyControl(policyEntry runningPolicy, policyID string) { - o.runningCollectors[policyID] = policyEntry -} - -func (o *openTelemetryBackend) removePolicyControl(policyID string) { - policy, ok := o.runningCollectors[policyID] - if !ok { - o.logger.Error("did not find a running collector for policy id", zap.String("policy_id", policyID)) - return - } - policy.cancel() -} - -func (o *openTelemetryBackend) RemovePolicy(data policies.PolicyData) error { - if o.policyRepo.Exists(data.ID) { - o.removePolicyControl(data.ID) - policyPath := fmt.Sprintf("%s/%s", o.policyConfigDirectory, fmt.Sprintf(tempFileNamePattern, data.ID)) - o.logger.Info("removing policy", zap.String("policy_id", data.ID), zap.String("policy_path", policyPath)) - // This is a temp file, if it fails to remove, it will be erased once the container is restarted - if err := os.Remove(policyPath); err != nil { - o.logger.Warn("failed to remove policy file, this won't fail policy removal", zap.String("policy_id", data.ID), zap.Error(err)) - } - return nil - } - o.logger.Warn("no policy was removed, policy not found", zap.String("policy_id", data.ID)) - return nil -} - -func (o *openTelemetryBackend) ValidatePolicy(otelConfig openTelemetryConfig) error { - if otelConfig.Service.Pipelines.Logs == nil && - otelConfig.Service.Pipelines.Metrics == nil && - otelConfig.Service.Pipelines.Traces == nil { - return errors.New("no pipelines defined") - } - if len(otelConfig.Receivers) == 0 { - return errors.New("no receivers defined") - } - - return nil -} diff --git a/agent/backend/otel/utils.go b/agent/backend/otel/utils.go new file mode 100644 index 0000000..d061d85 --- /dev/null +++ b/agent/backend/otel/utils.go @@ -0,0 +1,109 @@ +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 index 5611485..b62ab39 100644 --- a/agent/backend/otel/vars.go +++ b/agent/backend/otel/vars.go @@ -4,5 +4,5 @@ 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.otlp_port", "4316") + v.SetDefault("orb.backends.otel.server_port", "10222") } diff --git a/agent/backend/pktvisor/pktvisor.go b/agent/backend/pktvisor/pktvisor.go index 38b68a6..9548be5 100644 --- a/agent/backend/pktvisor/pktvisor.go +++ b/agent/backend/pktvisor/pktvisor.go @@ -4,16 +4,15 @@ import ( "context" "errors" "fmt" - "net" "net/http" + "os" "os/exec" "strconv" - "strings" "time" - mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/go-cmd/cmd" "go.uber.org/zap" + "gopkg.in/yaml.v3" "github.com/netboxlabs/orb-agent/agent/backend" "github.com/netboxlabs/orb-agent/agent/config" @@ -31,7 +30,6 @@ const ( versionTimeout = 2 scrapeTimeout = 5 tapsTimeout = 5 - defaultConfigPath = "/opt/orb/agent.yaml" defaultAPIHost = "localhost" defaultAPIPort = "10853" ) @@ -54,41 +52,20 @@ type pktvisorBackend struct { startTime time.Time cancelFunc context.CancelFunc ctx context.Context - - mqttClient *mqtt.Client - metricsTopic string - otlpMetricsTopic string - policyRepo policies.PolicyRepo + policyRepo policies.PolicyRepo adminAPIHost string adminAPIPort string adminAPIProtocol string // added for Strings - agentTags map[string]string + agentLabels map[string]string // OpenTelemetry management otelReceiverHost string otelReceiverPort int } -func (p *pktvisorBackend) getFreePort() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") - if err != nil { - return 0, err - } - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return 0, err - } - defer func() { - if err := l.Close(); err != nil { - p.logger.Error("failed to close socket", zap.Error(err)) - } - }() - return l.Addr().(*net.TCPAddr).Port, nil -} - func (p *pktvisorBackend) GetStartTime() time.Time { return p.startTime } @@ -97,14 +74,6 @@ func (p *pktvisorBackend) GetInitialState() backend.RunningStatus { return backend.Unknown } -func (p *pktvisorBackend) SetCommsClient(agentID string, client *mqtt.Client, baseTopic string) { - p.mqttClient = client - metricsTopic := strings.Replace(baseTopic, "?", "be", 1) - otelMetricsTopic := strings.Replace(baseTopic, "?", "otlp", 1) - p.metricsTopic = fmt.Sprintf("%s/m/%c", metricsTopic, agentID[0]) - p.otlpMetricsTopic = fmt.Sprintf("%s/m/%c", otelMetricsTopic, agentID[0]) -} - func (p *pktvisorBackend) GetRunningStatus() (backend.RunningStatus, string, error) { // first check process status runningStatus, errMsg, err := p.getProcRunningStatus() @@ -143,27 +112,16 @@ func (p *pktvisorBackend) Start(ctx context.Context, cancelFunc context.CancelFu return err } - pvOptions := []string{ - "--admin-api", - "-l", - p.adminAPIHost, - "-p", - p.adminAPIPort, - } + pvOptions := []string{"--admin-api"} if len(p.configFile) > 0 { pvOptions = append(pvOptions, "--config", p.configFile) } - pvOptions = append(pvOptions, "--otel") - pvOptions = append(pvOptions, "--otel-host", p.otelReceiverHost) - if p.otelReceiverPort == 0 { - p.otelReceiverPort, err = p.getFreePort() - if err != nil { - p.logger.Error("pktvisor otlp startup error", zap.Error(err)) - return err - } + if p.otelReceiverHost != "" && p.otelReceiverPort != 0 { + pvOptions = append(pvOptions, "--otel") + pvOptions = append(pvOptions, "--otel-host", p.otelReceiverHost) + pvOptions = append(pvOptions, "--otel-port", strconv.Itoa(p.otelReceiverPort)) } - pvOptions = append(pvOptions, "--otel-port", strconv.Itoa(p.otelReceiverPort)) // the macros should be properly configured to enable crashpad // pvOptions = append(pvOptions, "--cp-token", PKTVISOR_CP_TOKEN) @@ -273,32 +231,71 @@ func (p *pktvisorBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo p.logger = logger p.policyRepo = repo - var prs bool - if p.binary, prs = config["binary"].(string); !prs { - p.binary = defaultBinary + p.binary = defaultBinary + p.adminAPIHost = defaultAPIHost + p.adminAPIPort = defaultAPIPort + p.agentLabels = common.Otel.AgentLabels + + // Create temp config file + tmpDir := os.TempDir() + tmpFile, err := os.CreateTemp(tmpDir, "pktvisor-*.yaml") + if err != nil { + return fmt.Errorf("failed to create pktvisor temp config file: %w", err) } - if p.configFile, prs = config["config_file"].(string); !prs { - p.configFile = defaultConfigPath + + // Prepare the visor configuration structure + visorConfig := make(map[string]interface{}) + configSection := make(map[string]interface{}) + + // Process all config entries + for key, value := range config { + switch key { + case "host": + if v, ok := value.(string); ok { + p.adminAPIHost = v + } + configSection[key] = value + case "port": + if v, ok := value.(string); ok { + p.adminAPIPort = v + } + configSection[key] = value + case "taps": + visorConfig["taps"] = value + default: + configSection[key] = value + } } - if p.adminAPIHost, prs = config["api_host"].(string); !prs { - p.adminAPIHost = defaultAPIHost + + if len(configSection) > 0 { + visorConfig["config"] = configSection } - if p.adminAPIPort, prs = config["api_port"].(string); !prs { - p.adminAPIPort = defaultAPIPort + + fullConfig := map[string]any{ + "visor": visorConfig, } - p.agentTags = common.Otel.AgentTags - - p.otelReceiverHost = common.Otel.Host - p.otelReceiverPort = common.Otel.Port - if p.otelReceiverPort == 0 { - var err error - if p.otelReceiverPort, err = p.getFreePort(); err != nil { - p.logger.Error("pktvisor otlp startup error", zap.Error(err)) - return err + + 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)) } + 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)) + } + return fmt.Errorf("failed to write config file: %w", err) + } + + p.configFile = tmpFile.Name() - p.logger.Info("configured otel receiver host", zap.String("host", p.otelReceiverHost), zap.Int("port", p.otelReceiverPort)) + 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)) + } return nil } diff --git a/agent/backend/pktvisor/vars.go b/agent/backend/pktvisor/vars.go index df3db3d..5616c36 100644 --- a/agent/backend/pktvisor/vars.go +++ b/agent/backend/pktvisor/vars.go @@ -6,8 +6,7 @@ import ( // RegisterBackendSpecificVariables registers the backend specific variables for the pktvisor backend func RegisterBackendSpecificVariables(v *viper.Viper) { - v.SetDefault("orb.backends.pktvisor.binary", "/usr/local/sbin/pktvisord") - v.SetDefault("orb.backends.pktvisor.config_file", "/opt/orb/agent.yaml") - v.SetDefault("orb.backends.pktvisor.api_host", "localhost") - v.SetDefault("orb.backends.pktvisor.api_port", "10853") + 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/worker/worker.go b/agent/backend/worker/worker.go index bece373..47e68ba 100644 --- a/agent/backend/worker/worker.go +++ b/agent/backend/worker/worker.go @@ -8,7 +8,6 @@ import ( "net/http" "time" - mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/go-cmd/cmd" "go.uber.org/zap" "gopkg.in/yaml.v3" @@ -50,8 +49,6 @@ type workerBackend struct { statusChan <-chan cmd.Status cancelFunc context.CancelFunc ctx context.Context - - mqttClient *mqtt.Client } type info struct { @@ -87,10 +84,6 @@ func (d *workerBackend) Configure(logger *zap.Logger, repo policies.PolicyRepo, return nil } -func (d *workerBackend) SetCommsClient(_ string, client *mqtt.Client, _ string) { - d.mqttClient = client -} - func (d *workerBackend) Version() (string, error) { var info info err := d.request("status", &info, http.MethodGet, http.NoBody, "application/json", versionTimeout) diff --git a/agent/config/local.go b/agent/config/local.go deleted file mode 100644 index 8f0b2d7..0000000 --- a/agent/config/local.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -import ( - "context" - - "go.uber.org/zap" -) - -var _ Manager = (*localConfigManager)(nil) - -type localConfigManager struct { - logger *zap.Logger - config Local -} - -func (oc *localConfigManager) GetConfig() (MQTTConfig, error) { - return MQTTConfig{Connect: false}, nil -} - -func (oc *localConfigManager) GetContext(ctx context.Context) context.Context { - return ctx -} diff --git a/agent/config/manager.go b/agent/config/manager.go deleted file mode 100644 index 4989835..0000000 --- a/agent/config/manager.go +++ /dev/null @@ -1,25 +0,0 @@ -package config - -import ( - "context" - - "go.uber.org/zap" -) - -// Manager is the interface for configuration manager -type Manager interface { - GetConfig() (MQTTConfig, error) - GetContext(ctx context.Context) context.Context -} - -// New creates a new instance of ConfigManager based on the configuration -func New(logger *zap.Logger, c ManagerConfig) Manager { - switch c.Active { - case "local": - return &localConfigManager{logger: logger, config: c.Backends.Local} - case "cloud": - return &cloudConfigManager{logger: logger, config: c.Backends.Cloud} - default: - return &localConfigManager{logger: logger, config: c.Backends.Local} - } -} diff --git a/agent/config/types.go b/agent/config/types.go index 87a10a0..db52d57 100644 --- a/agent/config/types.go +++ b/agent/config/types.go @@ -3,6 +3,19 @@ package config // ContextKey represents the key for the context 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"` @@ -18,49 +31,58 @@ type MQTTConfig struct { ChannelID string `mapstructure:"channel_id"` } -// CloudConfig represents the configuration for the cloud agent -type CloudConfig struct { - AgentName string `mapstructure:"agent_name"` - AutoProvision bool `mapstructure:"auto_provision"` -} - -// Cloud represents the cloud ConfigManager configuration -type Cloud struct { - Config CloudConfig `mapstructure:"config"` - API APIConfig `mapstructure:"api"` - MQTT MQTTConfig `mapstructure:"mqtt"` - TLS struct { +// 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"` - Tags map[string]string `mapstructure:"tags"` + Labels map[string]string `mapstructure:"labels"` } -// Local represents the local ConfigManager configuration. -type Local struct { +// LocalManager represents the local ConfigManager configuration. +type LocalManager struct { Config string `mapstructure:"config"` } -// ManagerBackends represents the configuration for manager backends, including cloud and local. -type ManagerBackends struct { - Cloud Cloud `mapstructure:"orbcloud"` - Local Local `mapstructure:"local"` +// 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"` +} + +// 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"` } // ManagerConfig represents the configuration for the Config Manager type ManagerConfig struct { - Active string `mapstructure:"active"` - Backends ManagerBackends `mapstructure:"backends"` + Active string `mapstructure:"active"` + Sources ManagerSources `mapstructure:"sources"` } // BackendCommons represents common configuration for backends type BackendCommons struct { Otel struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - AgentTags map[string]string `mapstructure:"agent_tags"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + AgentLabels map[string]string `mapstructure:"agent_labels"` } `mapstructure:"otel"` Diode struct { Target string `mapstructure:"target"` @@ -73,7 +95,7 @@ type BackendCommons struct { type OrbAgent struct { Backends map[string]map[string]interface{} `mapstructure:"backends"` Policies map[string]map[string]interface{} `mapstructure:"policies"` - Tags map[string]string `mapstructure:"tags"` + Labels map[string]string `mapstructure:"labels"` ConfigManager ManagerConfig `mapstructure:"config_manager"` Debug struct { Enable bool `mapstructure:"enable"` diff --git a/agent/config/cloud.go b/agent/configmgr/cloud.go similarity index 81% rename from agent/config/cloud.go rename to agent/configmgr/cloud.go index 064b63e..916652a 100644 --- a/agent/config/cloud.go +++ b/agent/configmgr/cloud.go @@ -1,4 +1,4 @@ -package config +package configmgr import ( "bytes" @@ -17,13 +17,18 @@ import ( "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 - config Cloud + pMgr policymgr.PolicyManager + config config.CloudManager db *sqlx.DB } @@ -106,7 +111,7 @@ func (cc *cloudConfigManager) request(address string, token string, response int return nil } -func (cc *cloudConfigManager) autoProvision(apiAddress string, token string) (MQTTConfig, error) { +func (cc *cloudConfigManager) autoProvision(apiAddress string, token string) (config.MQTTConfig, error) { type AgentRes struct { ID string `json:"id"` Key string `json:"key"` @@ -114,23 +119,23 @@ func (cc *cloudConfigManager) autoProvision(apiAddress string, token string) (MQ } type AgentReq struct { - Name string `json:"name"` - AgentTags map[string]string `json:"agent_tags"` + 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 MQTTConfig{}, err + return config.MQTTConfig{}, err } aname = hostname } - agentReq := AgentReq{Name: strings.Replace(aname, ".", "-", -1), AgentTags: cc.config.Tags} + agentReq := AgentReq{Name: strings.Replace(aname, ".", "-", -1), AgentLabels: cc.config.Labels} body, err := json.Marshal(agentReq) if err != nil { - return MQTTConfig{}, err + return config.MQTTConfig{}, err } cc.logger.Info("attempting auto provision", zap.String("address", apiAddress)) @@ -138,28 +143,28 @@ func (cc *cloudConfigManager) autoProvision(apiAddress string, token string) (MQ var result AgentRes err = cc.request(apiAddress, token, &result, http.MethodPost, body) if err != nil { - return MQTTConfig{}, err + 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 MQTTConfig{}, err + return config.MQTTConfig{}, err } - return MQTTConfig{ + return config.MQTTConfig{ ID: result.ID, Key: result.Key, ChannelID: result.ChannelID, }, nil } -func (cc *cloudConfigManager) GetConfig() (MQTTConfig, error) { +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 MQTTConfig{}, err + return err } cc.db = db @@ -171,30 +176,25 @@ func (cc *cloudConfigManager) GetConfig() (MQTTConfig, error) { cc.logger.Info("using explicitly specified cloud configuration", zap.String("address", mqtt.Address), zap.String("id", mqtt.ID)) - return MQTTConfig{ - Address: mqtt.Address, - ID: mqtt.ID, - Key: mqtt.Key, - ChannelID: mqtt.ChannelID, - }, nil + return nil } // if full config is not available, possibly attempt auto provision configuration if !cc.config.Config.AutoProvision { - return MQTTConfig{}, errors.New("valid cloud MQTT config was not specified, and auto_provision was disabled") + return errors.New("valid cloud MQTT config was not specified, and auto_provision was disabled") } err = cc.migrateDB() if err != nil { - return MQTTConfig{}, err + 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 := MQTTConfig{} + dba := config.MQTTConfig{} if err := cc.db.QueryRowx(q).Scan(&dba.ID, &dba.Key, &dba.ChannelID); err != nil { if err != sql.ErrNoRows { - return MQTTConfig{}, err + return err } } else { // successfully loaded previous auto provision @@ -202,18 +202,18 @@ func (cc *cloudConfigManager) GetConfig() (MQTTConfig, error) { cc.logger.Info("using previous auto provisioned cloud configuration loaded from local storage", zap.String("address", mqtt.Address), zap.String("id", dba.ID)) - return dba, nil + return nil } // attempt a live auto provision apiConfig := cc.config.API if len(apiConfig.Token) == 0 { - return MQTTConfig{}, errors.New("wanted to auto provision, but no API token was available") + 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 MQTTConfig{}, err + return err } result.Address = mqtt.Address cc.logger.Info("using auto provisioned cloud configuration", @@ -221,14 +221,14 @@ func (cc *cloudConfigManager) GetConfig() (MQTTConfig, error) { zap.String("id", result.ID)) result.Connect = true - return result, nil + return nil } func (cc *cloudConfigManager) GetContext(ctx context.Context) context.Context { if cc.config.MQTT.ID != "" { - ctx = context.WithValue(ctx, ContextKey("agent_id"), cc.config.MQTT.ID) + ctx = context.WithValue(ctx, config.ContextKey("agent_id"), cc.config.MQTT.ID) } else { - ctx = context.WithValue(ctx, ContextKey("agent_id"), "auto-provisioning-without-id") + 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 new file mode 100644 index 0000000..de7fa1e --- /dev/null +++ b/agent/configmgr/git.go @@ -0,0 +1,520 @@ +package configmgr + +import ( + "context" + "errors" + "io" + "os" + "slices" + "strings" + + "github.com/go-co-op/gocron/v2" + gitv5 "github.com/go-git/go-git/v5" + gitconfig "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "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" + "github.com/netboxlabs/orb-agent/agent/config" + "github.com/netboxlabs/orb-agent/agent/policymgr" +) + +var _ Manager = (*gitConfigManager)(nil) + +type gitConfigManager struct { + logger *zap.Logger + pMgr policymgr.PolicyManager + config config.GitManager + scheduler gocron.Scheduler + repo *gitv5.Repository + lastRef plumbing.Hash + authMethod transport.AuthMethod + version int32 + matchPolicyPaths []string + namespace uuid.UUID +} + +type ( + policyPath string + policyData map[string]map[string]any + policyKey struct { + Backend string + Name string + } +) + +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) + for _, path := range matchingPolicies { + // Try to get the exact file from the tree + file, err := tree.File(path) + if err != nil { + return nil, errors.New("policy file not found: " + path) + } + + reader, err := file.Reader() + if err != nil { + return nil, err + } + defer func() { + if err := reader.Close(); err != nil { + gc.logger.Error("failed to close file", zap.Error(err)) + } + }() + + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + var policies map[string]map[string]any + if err = yaml.Unmarshal(data, &policies); err != nil { + return nil, err + } + + for beName, policy := range policies { + if _, ok := allPolicies[beName]; !ok { + allPolicies[beName] = make(map[string]any) + } + for pName, data := range policy { + if _, ok := allPolicies[beName][pName]; ok { + return nil, errors.New("policy already exists for backend: " + pName) + } + allPolicies[beName][pName] = data + } + } + policiesByPath[policyPath(path)] = policies + } + return policiesByPath, nil +} + +func (gc *gitConfigManager) removePolicies(policiesByPath map[policyPath]policyData) { + // Build a lookup map from policiesByPath + definedPolicies := make(map[policyKey]struct{}) + for _, policies := range policiesByPath { + for beName, newPolicy := range policies { + for pName := range newPolicy { + key := policyKey{Backend: beName, Name: pName} + definedPolicies[key] = struct{}{} + } + } + } + + appliedPolicies, err := gc.pMgr.GetRepo().GetAll() + if err != nil { + gc.logger.Error("failed to get applied policies", zap.Error(err)) + return + } + + // Remove policies that are not in the matching policies + for _, policy := range appliedPolicies { + 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)) + } + } + } +} + +func (gc *gitConfigManager) applyPolicies(policies policyData, backends map[string]backend.Backend) error { + for beName, policy := range policies { + if _, ok := backends[beName]; !ok { + return errors.New("backend not found: " + beName) + } + for pName, data := range policy { + policyID := uuid.NewSHA1(gc.namespace, []byte(pName+beName)).String() + payload := config.PolicyPayload{ + ID: policyID, + Action: "manage", + Name: pName, + DatasetID: uuid.NewString(), + Backend: beName, + Version: gc.version, + Data: data, + } + gc.pMgr.ManagePolicy(payload) + } + } + + return nil +} + +// processSelector reads and matches the root selector.yaml against the agent's metadata +func (gc *gitConfigManager) processSelector(file *object.File, cfg config.Config) ([]string, error) { + reader, err := file.Reader() + if err != nil { + return nil, err + } + defer func() { + if err := reader.Close(); err != nil { + gc.logger.Error("failed to close file", zap.Error(err)) + } + }() + + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + var selectors map[string]struct { + Selector map[string]string `yaml:"selector"` + Policies map[string]struct { + Enabled *bool `yaml:"enabled"` + Path string `yaml:"path"` + } `yaml:"policies"` + } + if err = yaml.Unmarshal(data, &selectors); err != nil { + return nil, err + } + + // Use a set (map) to store unique policy paths + policyPathsSet := make(map[string]struct{}) + + // Iterate through all selectors and collect all matching ones + for selectorName, entry := range selectors { + matches := true + for key, value := range entry.Selector { + if cfgValue, exists := cfg.OrbAgent.Labels[key]; !exists || cfgValue != value { + matches = false + break + } + } + if matches { + gc.logger.Info("Selector matched", zap.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)) + } + policyPathsSet[policy.Path] = struct{}{} + } + } + } + + // Convert map keys to a slice + var policyPaths []string + for path := range policyPathsSet { + policyPaths = append(policyPaths, path) + } + + return policyPaths, nil +} + +func (gc *gitConfigManager) schedule(cfg config.Config, backends map[string]backend.Backend) { + // Fetch latest updates from remote + err := gc.repo.Fetch(&gitv5.FetchOptions{ + RemoteName: "origin", + Auth: gc.authMethod, + RefSpecs: []gitconfig.RefSpec{"refs/heads/*:refs/heads/*"}, + }) + if err != nil && err != gitv5.NoErrAlreadyUpToDate { + gc.logger.Error("Failed to fetch latest changes", zap.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)) + return + } + + // Check if HEAD has changed + if gc.lastRef == ref.Hash() { + gc.logger.Debug("No changes detected in remote repository") + return + } + + // 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)) + return + } + + tree, err := commit.Tree() + if err != nil { + gc.logger.Error("Failed to get commit tree", zap.Error(err)) + return + } + + selectorFile, err := tree.File("selector.yaml") + if err != nil { + gc.logger.Warn("selector.yaml not found in latest commit") + gc.removePolicies(make(map[policyPath]policyData)) + gc.matchPolicyPaths = make([]string, 0) + gc.lastRef = ref.Hash() + return + } + + // 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)) + return + } + + oldTree, err := oldCommit.Tree() + if err != nil { + gc.logger.Error("Failed to get old commit tree", zap.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)) + return + } + + // Update last seen commit hash + gc.lastRef = ref.Hash() + + matchingPolicies, err := gc.processSelector(selectorFile, cfg) + if err != nil { + gc.logger.Error("Failed to process selector", zap.Error(err)) + return + } + + // Check if selector.yaml or policies has changed + changed := false + for _, change := range changes { + if change.To.Name == "selector.yaml" || slices.Contains(matchingPolicies, change.To.Name) { + changed = true + break + } + } + if !changed { + gc.logger.Info("No changes in selector.yaml or policies") + return + } + + policiesByPath, err := gc.readPolicies(tree, matchingPolicies) + if err != nil { + gc.logger.Error("Failed to read policies", zap.Error(err)) + return + } + + // Remove policies that are not in the matching policies + gc.removePolicies(policiesByPath) + + // Apply new policies + for _, policy := range matchingPolicies { + if slices.Contains(gc.matchPolicyPaths, policy) { + continue + } + policies := policiesByPath[policyPath(policy)] + if err = gc.applyPolicies(policies, backends); err != nil { + gc.logger.Error("failed to apply policies", zap.Error(err)) + } + } + + gc.matchPolicyPaths = matchingPolicies + + // Apply only changed policies + gc.version++ + for _, change := range changes { + 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)) + } + } + } + } +} + +func (gc *gitConfigManager) Start(cfg config.Config, backends map[string]backend.Backend) error { + var err error + gc.version = 1 + + 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 { + return err + } + gc.authMethod = &http.BasicAuth{ + Username: gc.config.Username, + Password: gc.config.Password, + } + } else if gc.config.Auth == "ssh" { + if gc.config.PrivateKey != "" { + if gc.config.Password, err = resolveEnv(gc.config.Password); err != nil { + return err + } + gc.authMethod, err = ssh.NewPublicKeysFromFile("git", gc.config.PrivateKey, gc.config.Password) + } else { + gc.authMethod, err = ssh.NewSSHAgentAuth("git") + } + } + + if err != nil { + return err + } + + // Open an in-memory repository + repo, err := gitv5.Init(memory.NewStorage(), nil) + if err != nil { + return err + } + + // Add the remote + if _, err = repo.CreateRemote(&gitconfig.RemoteConfig{ + Name: "origin", + URLs: []string{gc.config.URL}, + }); err != nil { + return err + } + + // Fetch all branches and references + if err = repo.Fetch(&gitv5.FetchOptions{ + RemoteName: "origin", + Auth: gc.authMethod, + }); err != nil && err != gitv5.NoErrAlreadyUpToDate { + return err + } + + // Get the remote reference list + remote, err := repo.Remote("origin") + if err != nil { + return err + } + + refs, err := remote.List(&gitv5.ListOptions{Auth: gc.authMethod}) + if err != nil { + return err + } + + // Find the default branch + branchName := gc.config.Branch + if branchName == "" { + for _, ref := range refs { + if ref.Name().IsBranch() { + branchName = ref.Name().Short() + gc.logger.Info("detected default branch", zap.String("branch", branchName)) + break + } + } + + if branchName == "" { + return errors.New("failed to detect default branch, repository might be empty") + } + } + + gc.logger.Info("cloning repository", zap.String("url", gc.config.URL), zap.String("branch", branchName)) + + // Now clone the repository with the determined branch + gc.repo, err = gitv5.Clone(memory.NewStorage(), nil, &gitv5.CloneOptions{ + Auth: gc.authMethod, + URL: gc.config.URL, + ReferenceName: plumbing.NewBranchReferenceName(branchName), + SingleBranch: true, + }) + if err != nil { + return err + } + + gc.config.Branch = branchName + gc.namespace = uuid.NewSHA1(uuid.Nil, []byte(gc.config.URL)) + + ref, err := gc.repo.Head() + if err != nil { + return err + } + + // Get the latest commit + gc.lastRef = ref.Hash() + commit, err := gc.repo.CommitObject(gc.lastRef) + if err != nil { + return err + } + + // Get the tree (file structure) of the commit + tree, err := commit.Tree() + if err != nil { + return err + } + + // Locate the selector.yaml file in the root directory + selectorFile, err := tree.File("selector.yaml") + if err != nil { + return errors.New("selector.yaml not found in repository root") + } + + // Process the selector file + matchingPolicies, err := gc.processSelector(selectorFile, cfg) + if err != nil { + return err + } + if matchingPolicies == nil { + gc.logger.Info("No matching selector found. No policies will be applied.") + return nil + } + + // Read all policies + policiesByPath, err := gc.readPolicies(tree, matchingPolicies) + if err != nil { + return err + } + + // Apply the matched policies + for path, policies := range policiesByPath { + gc.logger.Info("Applying policies from " + string(path)) + if err = gc.applyPolicies(policies, backends); err != nil { + return err + } + } + + gc.matchPolicyPaths = matchingPolicies + + // start scheduler + if gc.config.Schedule != nil { + s, err := gocron.NewScheduler() + if err != nil { + return err + } + 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 { + return err + } + gc.scheduler.Start() + } + + return nil +} + +func (gc *gitConfigManager) GetContext(ctx context.Context) context.Context { + return ctx +} diff --git a/agent/configmgr/local.go b/agent/configmgr/local.go new file mode 100644 index 0000000..8cca951 --- /dev/null +++ b/agent/configmgr/local.go @@ -0,0 +1,49 @@ +package configmgr + +import ( + "context" + "errors" + + "github.com/google/uuid" + "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 = (*localConfigManager)(nil) + +type localConfigManager struct { + logger *zap.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 { + policyID := uuid.NewSHA1(uuid.Nil, []byte(pName+beName)).String() + id := uuid.NewString() + payload := config.PolicyPayload{ + ID: policyID, Action: "manage", + Name: pName, DatasetID: id, Backend: beName, Version: 1, Data: data, + } + lc.pMgr.ManagePolicy(payload) + } + + } + return nil +} + +func (lc *localConfigManager) GetContext(ctx context.Context) context.Context { + return ctx +} diff --git a/agent/configmgr/manager.go b/agent/configmgr/manager.go new file mode 100644 index 0000000..cef3461 --- /dev/null +++ b/agent/configmgr/manager.go @@ -0,0 +1,31 @@ +package configmgr + +import ( + "context" + + "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" +) + +// Manager is the interface for configuration manager +type Manager interface { + Start(cfg config.Config, backends map[string]backend.Backend) error + GetContext(ctx context.Context) context.Context +} + +// 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 { + case "local": + return &localConfigManager{logger: logger, pMgr: mgr, config: c.Sources.Local} + case "cloud": + return &cloudConfigManager{logger: logger, pMgr: mgr, config: c.Sources.Cloud} + case "git": + return &gitConfigManager{logger: logger, pMgr: mgr, config: c.Sources.Git} + default: + return &localConfigManager{logger: logger, pMgr: mgr, config: c.Sources.Local} + } +} diff --git a/agent/docker/Dockerfile b/agent/docker/Dockerfile index 4dd6528..e591cec 100644 --- a/agent/docker/Dockerfile +++ b/agent/docker/Dockerfile @@ -1,5 +1,5 @@ ARG PKTVISOR_TAG=develop -ARG OTEL_TAG=0.111.0 +ARG OTEL_TAG=develop ARG NETWORK_DISCOVERY_TAG=latest ARG GO_VERSION=1.23 @@ -19,7 +19,7 @@ RUN --mount=target=. \ CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /build/orb-agent ./cmd/main.go -FROM otel/opentelemetry-collector-contrib:${OTEL_TAG} AS otelcol-contrib +FROM netboxlabs/opentelemetry-infinity:${OTEL_TAG} AS otlpinf FROM netboxlabs/network-discovery:${NETWORK_DISCOVERY_TAG} AS network-discovery @@ -32,26 +32,19 @@ RUN \ apt install --yes --force-yes --no-install-recommends nmap openssh-client && \ rm -rf /var/lib/apt -RUN addgroup --system netdev && useradd -m --shell /bin/bash -G netdev appuser && echo "appuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers - -# Create necessary directories and set ownership to appuser -RUN mkdir -p /opt/orb && chown appuser:appuser /opt/orb \ - && chown appuser:appuser /usr/local/bin \ - && chown appuser:appuser /var/run +RUN mkdir -p /opt/orb COPY --from=builder /build/orb-agent /usr/local/bin/orb-agent COPY --from=builder /src/orb-agent/agent/docker/agent_default.yaml /opt/orb/agent_default.yaml COPY --from=builder /src/orb-agent/agent/docker/orb-agent-entry.sh /usr/local/bin/orb-agent-entry.sh COPY --from=builder /src/orb-agent/agent/docker/run-agent.sh /run-agent.sh -COPY --from=pktvisor /usr/local/sbin/pktvisord /usr/local/sbin/pktvisord -COPY --from=pktvisor /usr/local/sbin/crashpad_handler /usr/local/sbin/crashpad_handler +COPY --from=pktvisor /usr/local/sbin/pktvisord /usr/local/bin/pktvisord +COPY --from=pktvisor /usr/local/sbin/crashpad_handler /usr/local/bin/crashpad_handler COPY --from=pktvisor /geo-db /geo-db COPY --from=pktvisor /iana /iana -RUN chown appuser:appuser /geo-db - -COPY --from=otelcol-contrib /otelcol-contrib /usr/local/bin/otelcol-contrib +COPY --from=otlpinf /exe /usr/local/bin/otlpinf COPY --from=network-discovery /usr/local/bin/network-discovery /usr/local/bin/network-discovery @@ -59,6 +52,4 @@ RUN pip3 install netboxlabs-device-discovery netboxlabs-orb-worker RUN chmod a+x /run-agent.sh -USER appuser - ENTRYPOINT [ "/usr/local/bin/orb-agent-entry.sh" ] diff --git a/agent/heartbeats.go b/agent/heartbeats.go deleted file mode 100644 index a055dff..0000000 --- a/agent/heartbeats.go +++ /dev/null @@ -1,152 +0,0 @@ -package agent - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/orb-community/orb/fleet" - "go.uber.org/zap" - - "github.com/netboxlabs/orb-agent/agent/backend" - "github.com/netboxlabs/orb-agent/agent/policies" -) - -// HeartbeatFreq how often to heartbeat -const HeartbeatFreq = 50 * time.Second - -// RestartTime minimum time to wait between restarts -const RestartTime = 5 * time.Minute - -func (a *orbAgent) sendSingleHeartbeat(ctx context.Context, t time.Time, agentsState fleet.State) { - if a.heartbeatsTopic == "" { - a.logger.Debug("heartbeat topic not yet set, skipping") - return - } - - a.logger.Debug("heartbeat", zap.String("state", agentsState.String())) - - bes := make(map[string]fleet.BackendStateInfo) - for name, be := range a.backends { - if agentsState == fleet.Offline { - bes[name] = fleet.BackendStateInfo{State: backend.Offline.String()} - continue - } - besi := fleet.BackendStateInfo{} - backendStatus, errMsg, err := be.GetRunningStatus() - a.backendState[name].Status = backendStatus - besi.State = backendStatus.String() - if backendStatus != backend.Running { - a.logger.Error("backend not ready", zap.String("backend", name), zap.String("status", backendStatus.String()), zap.String("errMsg", errMsg), zap.Error(err)) - if err != nil { - a.backendState[name].LastError = fmt.Sprintf("failed to retrieve backend status: %v", err) - } else if errMsg != "" { - a.backendState[name].LastError = errMsg - } - // status is not running so we have a current error - besi.Error = a.backendState[name].LastError - if time.Since(be.GetStartTime()) >= RestartTime { - a.logger.Info("attempting backend restart due to failed status during heartbeat") - ctx = a.configManager.GetContext(ctx) - err := a.RestartBackend(ctx, name, "failed during heartbeat") - if err != nil { - a.logger.Error("failed to restart backend", zap.Error(err), zap.String("backend", name)) - } - } else { - a.logger.Info("waiting to attempt backend restart due to failed status", zap.Duration("remaining_secs", RestartTime-(time.Since(be.GetStartTime())))) - } - } else { - // status is Running so no current error - besi.Error = "" - } - if a.backendState[name].LastError != "" { - besi.LastError = a.backendState[name].LastError - } - if !a.backendState[name].LastRestartTS.IsZero() { - besi.LastRestartTS = a.backendState[name].LastRestartTS - } - if a.backendState[name].RestartCount > 0 { - besi.RestartCount = a.backendState[name].RestartCount - } - if a.backendState[name].LastRestartReason != "" { - besi.LastRestartReason = a.backendState[name].LastRestartReason - } - bes[name] = besi - } - - ps := make(map[string]fleet.PolicyStateInfo) - pdata, err := a.policyManager.GetPolicyState() - if err == nil { - for _, pd := range pdata { - pstate := policies.Offline.String() - // if agent is not offline, default to status that policy manager believes we should be in - if agentsState != fleet.Offline { - pstate = pd.State.String() - } - // but if the policy backend is not running, policy isn't either - if bestate, ok := a.backendState[pd.Backend]; ok && bestate.Status != backend.Running { - pstate = policies.Unknown.String() - pd.BackendErr = "backend is unreachable" - } - ps[pd.ID] = fleet.PolicyStateInfo{ - Name: pd.Name, - Version: pd.Version, - State: pstate, - Error: pd.BackendErr, - Datasets: pd.GetDatasetIDs(), - LastScrapeTS: pd.LastScrapeTS, - LastScrapeBytes: pd.LastScrapeBytes, - Backend: pd.Backend, - } - } - } else { - a.logger.Error("unable to retrieved policy state", zap.Error(err)) - } - - ag := make(map[string]fleet.GroupStateInfo) - for id, groupInfo := range a.groupsInfos { - ag[id] = fleet.GroupStateInfo{ - GroupName: groupInfo.Name, - GroupChannel: groupInfo.ChannelID, - } - } - - hbData := fleet.Heartbeat{ - SchemaVersion: fleet.CurrentHeartbeatSchemaVersion, - State: agentsState, - TimeStamp: t, - BackendState: bes, - PolicyState: ps, - GroupState: ag, - } - - body, err := json.Marshal(hbData) - if err != nil { - a.logger.Error("error marshalling heartbeat", zap.Error(err)) - return - } - - if token := a.client.Publish(a.heartbeatsTopic, 1, false, body); token.Wait() && token.Error() != nil { - a.logger.Error("error sending heartbeat", zap.Error(token.Error())) - } -} - -func (a *orbAgent) sendHeartbeats(ctx context.Context, cancelFunc context.CancelFunc) { - a.logger.Debug("start heartbeats routine", zap.Any("routine", ctx.Value(routineKey))) - a.sendSingleHeartbeat(ctx, time.Now(), fleet.Online) - defer func() { - cancelFunc() - }() - for { - select { - case <-ctx.Done(): - a.logger.Debug("context done, stopping heartbeats routine") - a.sendSingleHeartbeat(ctx, time.Now(), fleet.Offline) - a.heartbeatCtx = nil - return - case t := <-a.hbTicker.C: - a.sendSingleHeartbeat(ctx, t, fleet.Online) - } - } -} diff --git a/agent/logging.go b/agent/logging.go deleted file mode 100644 index f751583..0000000 --- a/agent/logging.go +++ /dev/null @@ -1,58 +0,0 @@ -package agent - -import ( - mqtt "github.com/eclipse/paho.mqtt.golang" - "go.uber.org/zap" -) - -type agentLoggerDebug struct { - a *orbAgent -} -type agentLoggerWarn struct { - a *orbAgent -} -type agentLoggerCritical struct { - a *orbAgent -} -type agentLoggerError struct { - a *orbAgent -} - -var ( - _ mqtt.Logger = (*agentLoggerDebug)(nil) - _ mqtt.Logger = (*agentLoggerWarn)(nil) - _ mqtt.Logger = (*agentLoggerCritical)(nil) - _ mqtt.Logger = (*agentLoggerError)(nil) -) - -func (a *agentLoggerWarn) Println(v ...interface{}) { - a.a.logger.Warn("WARN mqtt log", zap.Any("payload", v)) -} - -func (a *agentLoggerWarn) Printf(_ string, v ...interface{}) { - a.a.logger.Warn("WARN mqtt log", zap.Any("payload", v)) -} - -func (a *agentLoggerDebug) Println(v ...interface{}) { - a.a.logger.Debug("DEBUG mqtt log", zap.Any("payload", v)) -} - -func (a *agentLoggerDebug) Printf(_ string, v ...interface{}) { - a.a.logger.Debug("DEBUG mqtt log", zap.Any("payload", v)) -} - -func (a *agentLoggerCritical) Println(v ...interface{}) { - a.a.logger.Error("CRITICAL mqtt log", zap.Any("payload", v)) -} - -func (a *agentLoggerCritical) Printf(_ string, v ...interface{}) { - a.a.logger.Error("CRITICAL mqtt log", zap.Any("payload", v)) -} - -func (a *agentLoggerError) Println(v ...interface{}) { - a.a.logger.Error("ERROR mqtt log", zap.Any("payload", v)) -} - -func (a *agentLoggerError) Printf(_ string, v ...interface{}) { - a.a.logger.Error("ERROR mqtt log", zap.Any("payload", v)) -} diff --git a/agent/policyMgr/manager.go b/agent/policymgr/manager.go similarity index 96% rename from agent/policyMgr/manager.go rename to agent/policymgr/manager.go index 4715aa2..a022275 100644 --- a/agent/policyMgr/manager.go +++ b/agent/policymgr/manager.go @@ -1,11 +1,10 @@ -package manager +package policymgr import ( "errors" "fmt" "strings" - "github.com/orb-community/orb/fleet" "go.uber.org/zap" "github.com/netboxlabs/orb-agent/agent/backend" @@ -15,7 +14,7 @@ import ( // PolicyManager is the interface for managing policies type PolicyManager interface { - ManagePolicy(payload fleet.AgentPolicyRPCPayload) + ManagePolicy(payload config.PolicyPayload) RemovePolicyDataset(policyID string, datasetID string, be backend.Backend) GetPolicyState() ([]policies.PolicyData, error) GetRepo() policies.PolicyRepo @@ -50,7 +49,7 @@ func (a *policyManager) GetPolicyState() ([]policies.PolicyData, error) { return a.repo.GetAll() } -func (a *policyManager) ManagePolicy(payload fleet.AgentPolicyRPCPayload) { +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), @@ -191,7 +190,7 @@ func (a *policyManager) RemovePolicyDataset(policyID string, datasetID string, b } } -func (a *policyManager) applyPolicy(payload fleet.AgentPolicyRPCPayload, be backend.Backend, pd *policies.PolicyData, updatePolicy bool) { +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)) diff --git a/cmd/agent.example.yaml b/cmd/agent.example.yaml index 4ee5ebb..c0d3632 100644 --- a/cmd/agent.example.yaml +++ b/cmd/agent.example.yaml @@ -12,8 +12,8 @@ visor: # 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 tags - tags: + # 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 diff --git a/docs/backends/worker.md b/docs/backends/worker.md index e40fc6c..67548b8 100644 --- a/docs/backends/worker.md +++ b/docs/backends/worker.md @@ -1,6 +1,5 @@ # Worker -The worker backend TBD. - +The worker backend allows to run custom implementation as part of Orb Agent. ## 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. diff --git a/docs/configs/git.md b/docs/configs/git.md new file mode 100644 index 0000000..3cc2536 --- /dev/null +++ b/docs/configs/git.md @@ -0,0 +1,85 @@ +# Git +The Git configuration manager outlines a policy management system where an agent fetches policies from a Git repository. + +### Config +The following sample of a git configuration +```yaml +orb: + labels: + region: EU + pop: ams02 + config_manager: + active: git + sources: + git: + url: "https://github.com/myorg/policyrepo" + schedule: "* * * * *" + branch: develop + auth: "basic" + username: "username" + password: ${PASSWORD|TOKEN} + private_key: path/to/certificate.pem +``` + +| Parameter | Type | Required | Description | +|:---------:|:----:|:--------:|:-----------:| +| url | string | yes | the url of the repository that contain agent policies | +| schedule | cron format | no | If defined, it will execute fetch remote changes on cron schedule time. If not defined, it will execute the match and apply policies only once | +| branch | string | no | the git branch that should be used by the agent. If not specified, the default branch will be used | +| auth | string | no | it can be either 'basic' or 'ssh'. The basic authentication supports both password or token. If not specified, no auth will be used (public repository) | +| username | string | no | username used for authentication | +| password | string | no | the password used for authentication. If the auth method is 'basic' it should cointains the password or auth token. If the method is 'ssh' it should contains the password for the ssh certificate file | +| private_key | string | no | the path for the ssh certificate file | + +## Git Repository Structure + +The Orb Agent requires the Git repository containing its policies to have the following structure: +- A `selector.yaml` file in the root folder of the repository +- Policy files that define agent policies + +### Sample Structure +``` +. +├── .git +├── selector.yaml +├── policy1.yaml +├── folder2 +│   ├── policy2.yaml +│   └── folder3 +│   └── policy3.yaml +└── folder4 + └── policy4.yaml +``` + +### selector.yaml +The `selector.yaml` file must include the `selector` and `policies` sections: + - `selector`: Defines key-value pairs that identify agents based on their labels. If the selector is empty, it matches all agents. + - `policies`: Specifies policy file paths and their enabled or disabled state. If the `enabled` field is not provided, the policy is enabled by default + + +```yaml +agent_selector_1: + selector: + region: EU + pop: ams02 + policies: + policy1: + path: policy1.yaml + policy2: + enabled: false + path: folder2/policy2.yaml +agent_selector_2: + selector: + region: US + pop: nyc02 + policies: + policy1: + enabled: true + path: policy1.yaml + policy3: + path: folder2/folder3/policy3.yaml +agent_selector_matches_all: + selector: + policies: + path: folder4/policy4.yaml +``` \ No newline at end of file diff --git a/docs/configs/local.md b/docs/configs/local.md new file mode 100644 index 0000000..c5962bf --- /dev/null +++ b/docs/configs/local.md @@ -0,0 +1,11 @@ +# Local +Local config manager retrieves policies from the local configuration file passed to the agent. +It does not require any specific configuration, it just needs to be activated and config file passed. + +```yaml +orb: + config_manager: + active: local + ... +``` + diff --git a/go.mod b/go.mod index 6e0d2ff..b37596b 100644 --- a/go.mod +++ b/go.mod @@ -3,66 +3,65 @@ module github.com/netboxlabs/orb-agent go 1.23.2 require ( - github.com/eclipse/paho.mqtt.golang v1.5.0 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/orb-community/orb v0.30.0 github.com/pkg/profile v1.7.0 github.com/rubenv/sql-migrate v1.7.1 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 gopkg.in/yaml.v3 v3.0.1 ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/fxamacker/cbor/v2 v2.7.0 // 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-kit/kit v0.13.0 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-zoo/bone v1.3.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/magiconair/properties v1.8.9 // indirect - github.com/mainflux/mainflux v0.0.0-20220415135135-92d8fb99bf82 // indirect - github.com/mainflux/senml v1.5.0 // indirect - github.com/nats-io/nats.go v1.38.0 // indirect - github.com/nats-io/nkeys v0.4.9 // indirect - github.com/nats-io/nuid v1.0.1 // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.10.0 // 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/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // 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/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.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 + 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 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect - google.golang.org/grpc v1.69.2 // indirect - google.golang.org/protobuf v1.36.2 // indirect + golang.org/x/tools v0.29.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index a9d8fde..66d7b37 100644 --- a/go.sum +++ b/go.sum @@ -2,147 +2,100 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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/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/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/benbjohnson/immutable v0.4.3 h1:GYHcksoJ9K6HyAUpGxwZURrbTkXA0Dh4otXGqbhdrjA= -github.com/benbjohnson/immutable v0.4.3/go.mod h1:qJIKKSmdqz1tVzNtst1DZzvaqOU1onk1rc03IeM3Owk= -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/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -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/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= +github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +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/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/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= -github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/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/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU= -github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY= -github.com/docker/docker v27.1.2+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/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= -github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= +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/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/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/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -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-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-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= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +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-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= -github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -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-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-zoo/bone v1.3.0 h1:PY6sHq37FnQhj+4ZyqFIzJQHvrrGx0GEc3vTZZC/OsI= -github.com/go-zoo/bone v1.3.0/go.mod h1:HI3Lhb7G3UQcAwEhOJ2WyNcsFtQX1WYHa0Hl4OBbhW8= -github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= -github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +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/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/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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -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/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/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/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/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/mainflux/mainflux v0.0.0-20220415135135-92d8fb99bf82 h1:UWQLBZ7ychamG9uuBtCwVmt1tBQxPQuJ1VszC9zYFS8= -github.com/mainflux/mainflux v0.0.0-20220415135135-92d8fb99bf82/go.mod h1:YPGCoouBMT7gP6u4Hnj7vafJqRzT5yiuKtBNMC/DUIE= -github.com/mainflux/senml v1.5.0 h1:GAd1y1eMohfa6sVYcr2iQfVfkkh9l/q7B1TWF5L68xs= -github.com/mainflux/senml v1.5.0/go.mod h1:SMX76mM5yenjLVjZOM27+njCGkP+AA64O46nRQiBRlE= 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/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/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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA= -github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw= -github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= -github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= -github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= -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-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= -github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/orb-community/orb v0.30.0 h1:JHOEAwq0UU8DrLlZhyHv43lVMglwbhS1VBz4B8SZvTM= -github.com/orb-community/orb v0.30.0/go.mod h1:XlE5fdjlS4HJZfwDwl/zaoqmPNgpnsoOveiwTJsApM0= -github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= -github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= -github.com/ory/keto/proto/ory/keto/acl/v1alpha1 v0.0.0-20210616104402-80e043246cf9 h1:gP86NkMkUlqMOTjFQ8lt8T1HbHtCJGGeeeh/6c+nla0= -github.com/ory/keto/proto/ory/keto/acl/v1alpha1 v0.0.0-20210616104402-80e043246cf9/go.mod h1:8IoeBQqIRKWU5L6dTKQTlTwVhlUawpqSBJZWfLLN4FM= +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/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 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= @@ -152,16 +105,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI 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/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -169,8 +116,11 @@ github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +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= @@ -185,97 +135,61 @@ 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +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= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.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/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/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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-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.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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/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.2.2/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=