2022-09-23 19:00:23 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.
Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.
The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.
Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.
This is the old `GetConfig()` interface:
```go
type OrbitConfigFetcher interface {
GetConfig() (*fleet.OrbitConfig, error)
}
```
This is the new `OrbitConfigReceiver` interface:
```go
type OrbitConfigReceiver interface {
Run(*OrbitConfig) error
}
```
To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.
```go
orbitClient.RegisterConfigReceiver(extRunner)
```
Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series, one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain
Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain
This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)
Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet
Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
- The application is about to restart
- We can set a timeout for how long receivers are allowed to take
Closes #12662
---------
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 19:22:56 +00:00
|
|
|
"context"
|
2023-04-27 11:44:39 +00:00
|
|
|
"crypto/tls"
|
2022-09-23 19:00:23 +00:00
|
|
|
"encoding/json"
|
2022-09-26 14:44:09 +00:00
|
|
|
"errors"
|
2022-09-23 19:00:23 +00:00
|
|
|
"fmt"
|
2024-08-21 14:08:16 +00:00
|
|
|
"io"
|
2022-10-03 20:28:19 +00:00
|
|
|
"io/fs"
|
2024-08-21 14:08:16 +00:00
|
|
|
"mime"
|
2024-02-21 18:36:15 +00:00
|
|
|
"net"
|
2022-09-23 19:00:23 +00:00
|
|
|
"net/http"
|
2024-05-09 11:54:11 +00:00
|
|
|
"net/http/httptrace"
|
2024-05-14 20:25:35 +00:00
|
|
|
"net/url"
|
2022-10-03 20:28:19 +00:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2023-01-26 21:51:24 +00:00
|
|
|
"runtime"
|
2022-10-03 20:28:19 +00:00
|
|
|
"sync"
|
|
|
|
|
"time"
|
add headers denoting capabilities between fleet server / desktop / orbit (#7833)
This adds a new mechanism to allow us to handle compatibility issues between Orbit, Fleet Server and Fleet Desktop.
The general idea is to _always_ send a custom header of the form:
```
fleet-capabilities-header = "X-Fleet-Capabilities:" capabilities
capabilities = capability * (,)
capability = string
```
Both from the server to the clients (Orbit, Fleet Desktop) and vice-versa. For an example, see: https://github.com/fleetdm/fleet/commit/8c0bbdd291f54e03e19766bcdfead0fb8067f60c
Also, the following applies:
- Backwards compat: if the header is not present, assume that orbit/fleet doesn't have the capability
- The current capabilities endpoint will be removed
### Motivation
This solution is trying to solve the following problems:
- We have three independent processes communicating with each other (Fleet Desktop, Orbit and Fleet Server). Each process can be updated independently, and therefore we need a way for each process to know what features are supported by its peers.
- We originally implemented a dedicated API endpoint in the server that returned a list of the capabilities (or "features") enabled, we found this, and any other server-only solution (like API versioning) to be insufficient because:
- There are cases in which the server also needs to know which features are supported by its clients
- Clients needed to poll for changes to detect if the capabilities supported by the server change, by sending the capabilities on each request we have a much cleaner way to handling different responses.
- We are also introducing an unauthenticated endpoint to get the server features, this gives us flexibility if we need to implement different authentication mechanisms, and was one of the pitfalls of the first implementation.
Related to https://github.com/fleetdm/fleet/issues/7929
2022-09-26 10:53:53 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
2023-08-25 21:25:07 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/logging"
|
2024-11-21 16:31:03 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/luks"
|
2023-01-26 21:51:24 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
|
2022-10-03 20:28:19 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/pkg/retry"
|
add headers denoting capabilities between fleet server / desktop / orbit (#7833)
This adds a new mechanism to allow us to handle compatibility issues between Orbit, Fleet Server and Fleet Desktop.
The general idea is to _always_ send a custom header of the form:
```
fleet-capabilities-header = "X-Fleet-Capabilities:" capabilities
capabilities = capability * (,)
capability = string
```
Both from the server to the clients (Orbit, Fleet Desktop) and vice-versa. For an example, see: https://github.com/fleetdm/fleet/commit/8c0bbdd291f54e03e19766bcdfead0fb8067f60c
Also, the following applies:
- Backwards compat: if the header is not present, assume that orbit/fleet doesn't have the capability
- The current capabilities endpoint will be removed
### Motivation
This solution is trying to solve the following problems:
- We have three independent processes communicating with each other (Fleet Desktop, Orbit and Fleet Server). Each process can be updated independently, and therefore we need a way for each process to know what features are supported by its peers.
- We originally implemented a dedicated API endpoint in the server that returned a list of the capabilities (or "features") enabled, we found this, and any other server-only solution (like API versioning) to be insufficient because:
- There are cases in which the server also needs to know which features are supported by its clients
- Clients needed to poll for changes to detect if the capabilities supported by the server change, by sending the capabilities on each request we have a much cleaner way to handling different responses.
- We are also introducing an unauthenticated endpoint to get the server features, this gives us flexibility if we need to implement different authentication mechanisms, and was one of the pitfalls of the first implementation.
Related to https://github.com/fleetdm/fleet/issues/7929
2022-09-26 10:53:53 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2025-07-16 18:08:27 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/service/contract"
|
2022-10-03 20:28:19 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2022-09-23 19:00:23 +00:00
|
|
|
)
|
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
// OrbitClient exposes the Orbit API to communicate with the Fleet server.
|
2022-09-23 19:00:23 +00:00
|
|
|
type OrbitClient struct {
|
|
|
|
|
*baseClient
|
2022-10-03 20:28:19 +00:00
|
|
|
nodeKeyFilePath string
|
|
|
|
|
enrollSecret string
|
2023-03-13 21:54:18 +00:00
|
|
|
hostInfo fleet.OrbitHostInfo
|
2022-10-03 20:28:19 +00:00
|
|
|
|
|
|
|
|
enrolledMu sync.Mutex
|
|
|
|
|
enrolled bool
|
|
|
|
|
|
|
|
|
|
lastRecordedErrMu sync.Mutex
|
|
|
|
|
lastRecordedErr error
|
2022-10-28 17:27:21 +00:00
|
|
|
|
2024-02-21 18:36:15 +00:00
|
|
|
configCache configCache
|
2024-02-22 17:24:17 +00:00
|
|
|
onGetConfigErrFns *OnGetConfigErrFuncs
|
2024-02-21 18:36:15 +00:00
|
|
|
lastNetErrOnGetConfigLogged time.Time
|
2023-12-11 13:04:24 +00:00
|
|
|
|
2024-05-09 11:54:11 +00:00
|
|
|
lastIdleConnectionsCleanupMu sync.Mutex
|
|
|
|
|
lastIdleConnectionsCleanup time.Time
|
|
|
|
|
|
2022-10-28 17:27:21 +00:00
|
|
|
// TestNodeKey is used for testing only.
|
|
|
|
|
TestNodeKey string
|
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.
Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.
The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.
Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.
This is the old `GetConfig()` interface:
```go
type OrbitConfigFetcher interface {
GetConfig() (*fleet.OrbitConfig, error)
}
```
This is the new `OrbitConfigReceiver` interface:
```go
type OrbitConfigReceiver interface {
Run(*OrbitConfig) error
}
```
To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.
```go
orbitClient.RegisterConfigReceiver(extRunner)
```
Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series, one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain
Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain
This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)
Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet
Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
- The application is about to restart
- We can set a timeout for how long receivers are allowed to take
Closes #12662
---------
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 19:22:56 +00:00
|
|
|
|
|
|
|
|
// Interfaces that will receive updated configs
|
|
|
|
|
ConfigReceivers []fleet.OrbitConfigReceiver
|
|
|
|
|
// How frequently a new config will be fetched
|
|
|
|
|
ReceiverUpdateInterval time.Duration
|
2024-07-19 15:44:43 +00:00
|
|
|
// receiverUpdateContext used by ExecuteConfigReceivers to cancel the update loop.
|
|
|
|
|
receiverUpdateContext context.Context
|
|
|
|
|
// receiverUpdateCancelFunc is used to cancel receiverUpdateContext.
|
|
|
|
|
receiverUpdateCancelFunc context.CancelFunc
|
2025-07-18 14:31:52 +00:00
|
|
|
|
|
|
|
|
// hostIdentityCertPath is the file path to the host identity certificate issued using SCEP.
|
|
|
|
|
//
|
|
|
|
|
// If set then it will be deleted on HTTP 401 errors from Fleet and it will cause ExecuteConfigReceivers
|
|
|
|
|
// to terminate to trigger a restart.
|
|
|
|
|
hostIdentityCertPath string
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-11 13:04:24 +00:00
|
|
|
// time-to-live for config cache
|
|
|
|
|
const configCacheTTL = 3 * time.Second
|
|
|
|
|
|
|
|
|
|
type configCache struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
lastUpdated time.Time
|
|
|
|
|
config *fleet.OrbitConfig
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-23 19:00:23 +00:00
|
|
|
func (oc *OrbitClient) request(verb string, path string, params interface{}, resp interface{}) error {
|
2025-01-29 16:24:44 +00:00
|
|
|
return oc.requestWithExternal(verb, path, params, resp, false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// requestWithExternal is used to make requests to Fleet or external URLs. If external is true, the pathOrURL
|
|
|
|
|
// is used as the full URL to make the request to.
|
|
|
|
|
func (oc *OrbitClient) requestWithExternal(verb string, pathOrURL string, params interface{}, resp interface{}, external bool) error {
|
2022-09-23 19:00:23 +00:00
|
|
|
var bodyBytes []byte
|
|
|
|
|
var err error
|
|
|
|
|
if params != nil {
|
|
|
|
|
bodyBytes, err = json.Marshal(params)
|
|
|
|
|
if err != nil {
|
2022-10-03 20:28:19 +00:00
|
|
|
return fmt.Errorf("making request json marshalling : %w", err)
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-09 11:54:11 +00:00
|
|
|
oc.closeIdleConnections()
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
if os.Getenv("FLEETD_TEST_HTTPTRACE") == "1" {
|
|
|
|
|
ctx = httptrace.WithClientTrace(ctx, testStdoutHTTPTracer)
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-29 16:24:44 +00:00
|
|
|
var request *http.Request
|
|
|
|
|
if external {
|
|
|
|
|
request, err = http.NewRequestWithContext(
|
|
|
|
|
ctx,
|
|
|
|
|
verb,
|
|
|
|
|
pathOrURL,
|
|
|
|
|
nil,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
parsedURL, err := url.Parse(pathOrURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("parsing URL: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request, err = http.NewRequestWithContext(
|
|
|
|
|
ctx,
|
|
|
|
|
verb,
|
|
|
|
|
oc.url(parsedURL.Path, parsedURL.RawQuery).String(),
|
|
|
|
|
bytes.NewBuffer(bodyBytes),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
oc.setClientCapabilitiesHeader(request)
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
|
response, err := oc.http.Do(request)
|
|
|
|
|
if err != nil {
|
2022-10-03 20:28:19 +00:00
|
|
|
oc.setLastRecordedError(err)
|
2025-01-29 16:24:44 +00:00
|
|
|
return fmt.Errorf("%s %s: %w", verb, pathOrURL, err)
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
|
2025-01-29 16:24:44 +00:00
|
|
|
if err := oc.parseResponse(verb, pathOrURL, response, resp); err != nil {
|
2022-10-03 20:28:19 +00:00
|
|
|
oc.setLastRecordedError(err)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-22 17:24:17 +00:00
|
|
|
// OnGetConfigErrFuncs defines functions to be executed on GetConfig errors.
|
|
|
|
|
type OnGetConfigErrFuncs struct {
|
|
|
|
|
// OnNetErrFunc receives network and 5XX errors on GetConfig requests.
|
|
|
|
|
// These errors are rate limited to once every 5 minutes.
|
|
|
|
|
OnNetErrFunc func(err error)
|
|
|
|
|
// DebugErrFunc receives all errors on GetConfig requests.
|
|
|
|
|
DebugErrFunc func(err error)
|
|
|
|
|
}
|
2024-02-21 18:36:15 +00:00
|
|
|
|
|
|
|
|
var (
|
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.
Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.
The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.
Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.
This is the old `GetConfig()` interface:
```go
type OrbitConfigFetcher interface {
GetConfig() (*fleet.OrbitConfig, error)
}
```
This is the new `OrbitConfigReceiver` interface:
```go
type OrbitConfigReceiver interface {
Run(*OrbitConfig) error
}
```
To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.
```go
orbitClient.RegisterConfigReceiver(extRunner)
```
Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series, one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain
Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain
This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)
Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet
Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
- The application is about to restart
- We can set a timeout for how long receivers are allowed to take
Closes #12662
---------
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 19:22:56 +00:00
|
|
|
netErrInterval = 5 * time.Minute
|
|
|
|
|
configRetryOnNetworkError = 30 * time.Second
|
|
|
|
|
defaultOrbitConfigReceiverInterval = 30 * time.Second
|
2024-02-21 18:36:15 +00:00
|
|
|
)
|
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
// NewOrbitClient creates a new OrbitClient.
|
|
|
|
|
//
|
2023-03-13 21:54:18 +00:00
|
|
|
// - rootDir is the Orbit's root directory, where the Orbit node key is loaded-from/stored.
|
|
|
|
|
// - addr is the address of the Fleet server.
|
|
|
|
|
// - orbitHostInfo is the host system information used for enrolling to Fleet.
|
2024-02-22 17:24:17 +00:00
|
|
|
// - onGetConfigErrFns can be used to handle errors in the GetConfig request.
|
2023-03-13 21:54:18 +00:00
|
|
|
func NewOrbitClient(
|
|
|
|
|
rootDir string,
|
|
|
|
|
addr string,
|
|
|
|
|
rootCA string,
|
|
|
|
|
insecureSkipVerify bool,
|
|
|
|
|
enrollSecret string,
|
2023-04-27 11:44:39 +00:00
|
|
|
fleetClientCert *tls.Certificate,
|
2023-03-13 21:54:18 +00:00
|
|
|
orbitHostInfo fleet.OrbitHostInfo,
|
2024-02-22 17:24:17 +00:00
|
|
|
onGetConfigErrFns *OnGetConfigErrFuncs,
|
2025-07-18 14:31:52 +00:00
|
|
|
httpSignerWrapper func(*http.Client) *http.Client,
|
|
|
|
|
hostIdentityCertPath string,
|
2023-03-13 21:54:18 +00:00
|
|
|
) (*OrbitClient, error) {
|
2024-08-02 19:06:21 +00:00
|
|
|
orbitCapabilities := fleet.GetOrbitClientCapabilities()
|
2025-07-18 14:31:52 +00:00
|
|
|
bc, err := newBaseClient(addr, insecureSkipVerify, rootCA, "", fleetClientCert, orbitCapabilities, httpSignerWrapper)
|
2022-09-23 19:00:23 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
nodeKeyFilePath := filepath.Join(rootDir, constant.OrbitNodeKeyFileName)
|
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.
Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.
The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.
Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.
This is the old `GetConfig()` interface:
```go
type OrbitConfigFetcher interface {
GetConfig() (*fleet.OrbitConfig, error)
}
```
This is the new `OrbitConfigReceiver` interface:
```go
type OrbitConfigReceiver interface {
Run(*OrbitConfig) error
}
```
To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.
```go
orbitClient.RegisterConfigReceiver(extRunner)
```
Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series, one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain
Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain
This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)
Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet
Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
- The application is about to restart
- We can set a timeout for how long receivers are allowed to take
Closes #12662
---------
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 19:22:56 +00:00
|
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
|
|
|
|
2022-09-23 19:00:23 +00:00
|
|
|
return &OrbitClient{
|
2024-05-09 11:54:11 +00:00
|
|
|
nodeKeyFilePath: nodeKeyFilePath,
|
|
|
|
|
baseClient: bc,
|
|
|
|
|
enrollSecret: enrollSecret,
|
|
|
|
|
hostInfo: orbitHostInfo,
|
|
|
|
|
enrolled: false,
|
|
|
|
|
onGetConfigErrFns: onGetConfigErrFns,
|
|
|
|
|
lastIdleConnectionsCleanup: time.Now(),
|
2024-05-15 22:55:02 +00:00
|
|
|
ReceiverUpdateInterval: defaultOrbitConfigReceiverInterval,
|
2024-07-19 15:44:43 +00:00
|
|
|
receiverUpdateContext: ctx,
|
|
|
|
|
receiverUpdateCancelFunc: cancelFunc,
|
2025-07-18 14:31:52 +00:00
|
|
|
hostIdentityCertPath: hostIdentityCertPath,
|
2022-10-03 20:28:19 +00:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-19 15:44:43 +00:00
|
|
|
// TriggerOrbitRestart triggers a orbit process restart.
|
|
|
|
|
func (oc *OrbitClient) TriggerOrbitRestart(reason string) {
|
|
|
|
|
log.Info().Msgf("orbit restart triggered: %s", reason)
|
|
|
|
|
oc.receiverUpdateCancelFunc()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RestartTriggered returns true if any of the config receivers triggered an orbit restart.
|
|
|
|
|
func (oc *OrbitClient) RestartTriggered() bool {
|
|
|
|
|
select {
|
|
|
|
|
case <-oc.receiverUpdateContext.Done():
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-09 11:54:11 +00:00
|
|
|
// closeIdleConnections attempts to close idle connections from the pool
|
|
|
|
|
// every 55 minutes.
|
|
|
|
|
//
|
|
|
|
|
// Some load balancers (e.g. AWS ELB) have a maximum lifetime for a connection
|
|
|
|
|
// (no matter if the connection is active or not) and will forcefully close the
|
|
|
|
|
// connection causing errors in the client (e.g. https://github.com/fleetdm/fleet/issues/18783).
|
|
|
|
|
// To prevent these errors, we will attempt to cleanup idle connections every 55
|
|
|
|
|
// minutes to not let these connection grow too old. (AWS ELB's default value for maximum
|
|
|
|
|
// lifetime of a connection is 3600 seconds.)
|
|
|
|
|
func (oc *OrbitClient) closeIdleConnections() {
|
|
|
|
|
oc.lastIdleConnectionsCleanupMu.Lock()
|
|
|
|
|
defer oc.lastIdleConnectionsCleanupMu.Unlock()
|
|
|
|
|
|
|
|
|
|
if time.Since(oc.lastIdleConnectionsCleanup) < 55*time.Minute {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oc.lastIdleConnectionsCleanup = time.Now()
|
|
|
|
|
|
|
|
|
|
c, ok := oc.baseClient.http.(*http.Client)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
t, ok := c.Transport.(*http.Transport)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.CloseIdleConnections()
|
2022-10-03 20:28:19 +00:00
|
|
|
}
|
|
|
|
|
|
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.
Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.
The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.
Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.
This is the old `GetConfig()` interface:
```go
type OrbitConfigFetcher interface {
GetConfig() (*fleet.OrbitConfig, error)
}
```
This is the new `OrbitConfigReceiver` interface:
```go
type OrbitConfigReceiver interface {
Run(*OrbitConfig) error
}
```
To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.
```go
orbitClient.RegisterConfigReceiver(extRunner)
```
Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series, one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain
Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain
This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)
Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet
Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
- The application is about to restart
- We can set a timeout for how long receivers are allowed to take
Closes #12662
---------
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 19:22:56 +00:00
|
|
|
func (oc *OrbitClient) RunConfigReceivers() error {
|
|
|
|
|
config, err := oc.GetConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("RunConfigReceivers get config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var errs []error
|
|
|
|
|
var errMu sync.Mutex
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
wg.Add(len(oc.ConfigReceivers))
|
|
|
|
|
|
|
|
|
|
for _, receiver := range oc.ConfigReceivers {
|
|
|
|
|
receiver := receiver
|
|
|
|
|
go func() {
|
|
|
|
|
defer func() {
|
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
|
errMu.Lock()
|
|
|
|
|
errs = append(errs, fmt.Errorf("panic occured in receiver: %v", err))
|
|
|
|
|
errMu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
wg.Done()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
err := receiver.Run(config)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errMu.Lock()
|
|
|
|
|
errs = append(errs, err)
|
|
|
|
|
errMu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
|
|
if len(errs) != 0 {
|
|
|
|
|
return errors.Join(errs...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (oc *OrbitClient) RegisterConfigReceiver(cr fleet.OrbitConfigReceiver) {
|
|
|
|
|
oc.ConfigReceivers = append(oc.ConfigReceivers, cr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (oc *OrbitClient) ExecuteConfigReceivers() error {
|
|
|
|
|
ticker := time.NewTicker(oc.ReceiverUpdateInterval)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
2024-07-19 15:44:43 +00:00
|
|
|
case <-oc.receiverUpdateContext.Done():
|
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.
Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.
The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.
Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.
This is the old `GetConfig()` interface:
```go
type OrbitConfigFetcher interface {
GetConfig() (*fleet.OrbitConfig, error)
}
```
This is the new `OrbitConfigReceiver` interface:
```go
type OrbitConfigReceiver interface {
Run(*OrbitConfig) error
}
```
To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.
```go
orbitClient.RegisterConfigReceiver(extRunner)
```
Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series, one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain
Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain
This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)
Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet
Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
- The application is about to restart
- We can set a timeout for how long receivers are allowed to take
Closes #12662
---------
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 19:22:56 +00:00
|
|
|
return nil
|
|
|
|
|
case <-ticker.C:
|
2024-05-14 20:25:35 +00:00
|
|
|
if err := oc.RunConfigReceivers(); err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("running config receivers")
|
|
|
|
|
}
|
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.
Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.
The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.
Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.
This is the old `GetConfig()` interface:
```go
type OrbitConfigFetcher interface {
GetConfig() (*fleet.OrbitConfig, error)
}
```
This is the new `OrbitConfigReceiver` interface:
```go
type OrbitConfigReceiver interface {
Run(*OrbitConfig) error
}
```
To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.
```go
orbitClient.RegisterConfigReceiver(extRunner)
```
Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series, one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain
Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain
This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)
Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet
Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
- The application is about to restart
- We can set a timeout for how long receivers are allowed to take
Closes #12662
---------
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 19:22:56 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (oc *OrbitClient) InterruptConfigReceivers(err error) {
|
2024-07-19 15:44:43 +00:00
|
|
|
oc.receiverUpdateCancelFunc()
|
Orbit config receiver (#18518)
New interface for adding periodic jobs that rely on notifications/config
changes in Orbit.
Previously if we wanted to have recurring checks in Orbit, we would add
them into a chain of `GetConfig` calls. This call chain would be run
periodically by one of the runners registered with the cli application
framework.
The new method to register `OrbitConfigReceivers` with the
`OrbitClient`, and then register the orbit client itself with the
application framework.
Instead of having giving each fetcher an internal reference to the
previous fetcher that it must call, the receiver is registered with the
client and the new config is passed to the receiver.
This is the old `GetConfig()` interface:
```go
type OrbitConfigFetcher interface {
GetConfig() (*fleet.OrbitConfig, error)
}
```
This is the new `OrbitConfigReceiver` interface:
```go
type OrbitConfigReceiver interface {
Run(*OrbitConfig) error
}
```
To register a new receiver, you call the `RegisterConfigReceiver` method
on the client.
```go
orbitClient.RegisterConfigReceiver(extRunner)
```
Downsides of the old method:
- Spaghetti call chain setup
- Cascading failure, of one fails, all after it fail
- Run in series, one long function call holds up the rest
- Anything that wants to restart orbit is added as a Runner to the
application, meaning there could be several timers calling `GetConfig`
and running the chain
Benefits of the new method:
- Clean `RegisterConfigReceiver` api, no call chaining required
- Config receivers can be added at runtime
- Isolated receivers, one failing call don't effect others
- All calls are run in parallel in goroutines, no calls can hold up the
rest
- No more need for multiple runners, using a context cancel, any
receiver can queue a call to restart orbit
- Single point to handle errors and logging for all receivers
- Panic recovery to stop orbit from crashing
- Easier to test, configs are passed in and do not require a call chain
This branch contains a little bit of code from the installer method I
was working on because I branched it off of that. (oops)
Not all code comments surrounding old `GetConfig()` methods have been
fully updated yet
Possible changes:
- Update the interface to take a context, so we can let receivers know
to exit early. I can imagine two cases for this:
- The application is about to restart
- We can set a timeout for how long receivers are allowed to take
Closes #12662
---------
Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
2024-05-09 19:22:56 +00:00
|
|
|
}
|
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
// GetConfig returns the Orbit config fetched from Fleet server for this instance of OrbitClient.
|
2024-02-21 18:36:15 +00:00
|
|
|
// Since this method is called in multiple places, we use a cache with configCacheTTL time-to-live
|
|
|
|
|
// to reduce traffic to the Fleet server.
|
|
|
|
|
// Upon network errors, this method will retry the get config request (every 30 seconds).
|
2023-01-25 20:03:40 +00:00
|
|
|
func (oc *OrbitClient) GetConfig() (*fleet.OrbitConfig, error) {
|
2023-12-11 13:04:24 +00:00
|
|
|
oc.configCache.mu.Lock()
|
|
|
|
|
defer oc.configCache.mu.Unlock()
|
2024-02-21 18:36:15 +00:00
|
|
|
|
2023-12-11 13:04:24 +00:00
|
|
|
// If time-to-live passed, we update the config cache
|
|
|
|
|
now := time.Now()
|
|
|
|
|
if now.After(oc.configCache.lastUpdated.Add(configCacheTTL)) {
|
2024-02-22 17:24:17 +00:00
|
|
|
verb, path := "POST", "/api/fleet/orbit/config"
|
2024-02-21 18:36:15 +00:00
|
|
|
var (
|
|
|
|
|
resp fleet.OrbitConfig
|
|
|
|
|
err error
|
|
|
|
|
)
|
2024-02-22 17:24:17 +00:00
|
|
|
// Retry until we don't get a network error or a 5XX error.
|
2024-02-21 18:36:15 +00:00
|
|
|
_ = retry.Do(func() error {
|
|
|
|
|
err = oc.authenticatedRequest(verb, path, &orbitGetConfigRequest{}, &resp)
|
2024-02-22 17:24:17 +00:00
|
|
|
var (
|
|
|
|
|
netErr net.Error
|
|
|
|
|
statusCodeErr *statusCodeErr
|
|
|
|
|
)
|
|
|
|
|
if err != nil && oc.onGetConfigErrFns != nil && oc.onGetConfigErrFns.DebugErrFunc != nil {
|
|
|
|
|
oc.onGetConfigErrFns.DebugErrFunc(err)
|
|
|
|
|
}
|
|
|
|
|
if errors.As(err, &netErr) || (errors.As(err, &statusCodeErr) && statusCodeErr.code >= 500) {
|
2024-02-21 18:36:15 +00:00
|
|
|
now := time.Now()
|
2024-02-22 17:24:17 +00:00
|
|
|
if oc.onGetConfigErrFns != nil && oc.onGetConfigErrFns.OnNetErrFunc != nil && now.After(oc.lastNetErrOnGetConfigLogged.Add(netErrInterval)) {
|
|
|
|
|
oc.onGetConfigErrFns.OnNetErrFunc(err)
|
2024-02-21 18:36:15 +00:00
|
|
|
oc.lastNetErrOnGetConfigLogged = now
|
|
|
|
|
}
|
2024-02-22 17:24:17 +00:00
|
|
|
return err // retry on network or server 5XX errors
|
2024-02-21 18:36:15 +00:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}, retry.WithInterval(configRetryOnNetworkError))
|
2023-12-11 13:04:24 +00:00
|
|
|
oc.configCache.config = &resp
|
|
|
|
|
oc.configCache.err = err
|
|
|
|
|
oc.configCache.lastUpdated = now
|
2022-10-03 20:28:19 +00:00
|
|
|
}
|
2023-12-11 13:04:24 +00:00
|
|
|
return oc.configCache.config, oc.configCache.err
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
2022-10-10 20:15:35 +00:00
|
|
|
// SetOrUpdateDeviceToken sends a request to the server to set or update the
|
|
|
|
|
// device token with the given value.
|
|
|
|
|
func (oc *OrbitClient) SetOrUpdateDeviceToken(deviceAuthToken string) error {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/device_token"
|
|
|
|
|
params := setOrUpdateDeviceTokenRequest{
|
|
|
|
|
DeviceAuthToken: deviceAuthToken,
|
|
|
|
|
}
|
|
|
|
|
var resp setOrUpdateDeviceTokenResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, ¶ms, &resp); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-21 17:22:59 +00:00
|
|
|
// SetOrUpdateDeviceMappingEmail sends a request to the server to set or update the
|
|
|
|
|
// device mapping email with the given value.
|
|
|
|
|
func (oc *OrbitClient) SetOrUpdateDeviceMappingEmail(email string) error {
|
|
|
|
|
verb, path := "PUT", "/api/fleet/orbit/device_mapping"
|
|
|
|
|
params := orbitPutDeviceMappingRequest{
|
|
|
|
|
Email: email,
|
|
|
|
|
}
|
|
|
|
|
var resp orbitPutDeviceMappingResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, ¶ms, &resp); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-23 22:31:47 +00:00
|
|
|
// GetHostScript returns the script fetched from Fleet server to run on this
|
|
|
|
|
// host.
|
|
|
|
|
func (oc *OrbitClient) GetHostScript(execID string) (*fleet.HostScriptResult, error) {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/scripts/request"
|
|
|
|
|
var resp orbitGetScriptResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitGetScriptRequest{
|
|
|
|
|
ExecutionID: execID,
|
|
|
|
|
}, &resp); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return resp.HostScriptResult, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SaveHostScriptResult saves the result of running the script on this host.
|
|
|
|
|
func (oc *OrbitClient) SaveHostScriptResult(result *fleet.HostScriptResultPayload) error {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/scripts/result"
|
|
|
|
|
var resp orbitPostScriptResultResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitPostScriptResultRequest{
|
|
|
|
|
HostScriptResultPayload: result,
|
|
|
|
|
}, &resp); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-14 20:25:35 +00:00
|
|
|
func (oc *OrbitClient) GetInstallerDetails(installId string) (*fleet.SoftwareInstallDetails, error) {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/software_install/details"
|
|
|
|
|
var resp orbitGetSoftwareInstallResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitGetSoftwareInstallRequest{
|
|
|
|
|
InstallUUID: installId,
|
|
|
|
|
}, &resp); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return resp.SoftwareInstallDetails, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (oc *OrbitClient) SaveInstallerResult(payload *fleet.HostSoftwareInstallResultPayload) error {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/software_install/result"
|
|
|
|
|
var resp orbitPostSoftwareInstallResultResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitPostSoftwareInstallResultRequest{
|
|
|
|
|
HostSoftwareInstallResultPayload: payload,
|
|
|
|
|
}, &resp); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 14:09:57 +00:00
|
|
|
func (oc *OrbitClient) DownloadSoftwareInstaller(installerID uint, downloadDirectory string, progressFunc func(n int)) (string, error) {
|
2024-05-14 20:25:35 +00:00
|
|
|
verb, path := "POST", "/api/fleet/orbit/software_install/package?alt=media"
|
2025-03-20 14:09:57 +00:00
|
|
|
resp := FileResponse{
|
|
|
|
|
DestPath: downloadDirectory,
|
|
|
|
|
ProgressFunc: progressFunc,
|
|
|
|
|
}
|
2024-05-14 20:25:35 +00:00
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitDownloadSoftwareInstallerRequest{
|
|
|
|
|
InstallerID: installerID,
|
|
|
|
|
}, &resp); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return resp.GetFilePath(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 14:09:57 +00:00
|
|
|
func (oc *OrbitClient) DownloadSoftwareInstallerFromURL(url string, filename string, downloadDirectory string, progressFunc func(int)) (string, error) {
|
|
|
|
|
resp := FileResponse{
|
|
|
|
|
DestPath: downloadDirectory,
|
|
|
|
|
DestFile: filename,
|
|
|
|
|
SkipMediaType: true,
|
|
|
|
|
ProgressFunc: progressFunc,
|
|
|
|
|
}
|
2025-01-29 16:24:44 +00:00
|
|
|
if err := oc.requestWithExternal("GET", url, nil, &resp, true); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return resp.GetFilePath(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 19:48:16 +00:00
|
|
|
type NullFileResponse struct{}
|
2024-08-21 14:08:16 +00:00
|
|
|
|
|
|
|
|
func (f *NullFileResponse) Handle(resp *http.Response) error {
|
|
|
|
|
_, _, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("parsing media type from response header: %w", err)
|
|
|
|
|
}
|
|
|
|
|
_, err = io.Copy(io.Discard, resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("copying from http stream to io.Discard: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DownloadAndDiscardSoftwareInstaller downloads the software installer and discards it.
|
|
|
|
|
// This method is used during load testing by osquery-perf.
|
|
|
|
|
func (oc *OrbitClient) DownloadAndDiscardSoftwareInstaller(installerID uint) error {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/software_install/package?alt=media"
|
|
|
|
|
resp := NullFileResponse{}
|
|
|
|
|
return oc.authenticatedRequest(verb, path, &orbitDownloadSoftwareInstallerRequest{
|
|
|
|
|
InstallerID: installerID,
|
|
|
|
|
}, &resp)
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-10 20:15:35 +00:00
|
|
|
// Ping sends a ping request to the orbit/ping endpoint.
|
2022-10-03 20:28:19 +00:00
|
|
|
func (oc *OrbitClient) Ping() error {
|
|
|
|
|
verb, path := "HEAD", "/api/fleet/orbit/ping"
|
|
|
|
|
err := oc.request(verb, path, nil, nil)
|
|
|
|
|
if err == nil || errors.Is(err, notFoundErr{}) {
|
|
|
|
|
// notFound is ok, it means an old server without the capabilities header
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (oc *OrbitClient) enroll() (string, error) {
|
2022-09-23 19:00:23 +00:00
|
|
|
verb, path := "POST", "/api/fleet/orbit/enroll"
|
2025-07-16 18:08:27 +00:00
|
|
|
params := contract.EnrollOrbitRequest{
|
2023-12-15 18:26:32 +00:00
|
|
|
EnrollSecret: oc.enrollSecret,
|
|
|
|
|
HardwareUUID: oc.hostInfo.HardwareUUID,
|
|
|
|
|
HardwareSerial: oc.hostInfo.HardwareSerial,
|
|
|
|
|
Hostname: oc.hostInfo.Hostname,
|
|
|
|
|
Platform: oc.hostInfo.Platform,
|
2025-09-05 19:05:19 +00:00
|
|
|
PlatformLike: oc.hostInfo.PlatformLike,
|
2023-12-15 18:26:32 +00:00
|
|
|
OsqueryIdentifier: oc.hostInfo.OsqueryIdentifier,
|
2024-11-18 21:51:36 +00:00
|
|
|
ComputerName: oc.hostInfo.ComputerName,
|
|
|
|
|
HardwareModel: oc.hostInfo.HardwareModel,
|
2023-03-13 21:54:18 +00:00
|
|
|
}
|
2022-10-28 17:27:21 +00:00
|
|
|
var resp EnrollOrbitResponse
|
2022-09-23 19:00:23 +00:00
|
|
|
err := oc.request(verb, path, params, &resp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return resp.OrbitNodeKey, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
// enrollLock helps protect the enrolling process in case mutliple OrbitClients
|
|
|
|
|
// want to re-enroll at the same time.
|
|
|
|
|
var enrollLock sync.Mutex
|
|
|
|
|
|
|
|
|
|
// getNodeKeyOrEnroll attempts to read the orbit node key if the file exists on disk
|
|
|
|
|
// otherwise it enrolls the host with Fleet and saves the node key to disk
|
|
|
|
|
func (oc *OrbitClient) getNodeKeyOrEnroll() (string, error) {
|
2022-10-28 17:27:21 +00:00
|
|
|
if oc.TestNodeKey != "" {
|
|
|
|
|
return oc.TestNodeKey, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
enrollLock.Lock()
|
|
|
|
|
defer enrollLock.Unlock()
|
|
|
|
|
|
2023-10-27 18:28:54 +00:00
|
|
|
orbitNodeKey, err := os.ReadFile(oc.nodeKeyFilePath)
|
2022-10-03 20:28:19 +00:00
|
|
|
switch {
|
|
|
|
|
case err == nil:
|
|
|
|
|
return string(orbitNodeKey), nil
|
|
|
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
|
|
|
// OK, if there's no orbit node key, proceed to enroll.
|
|
|
|
|
default:
|
|
|
|
|
return "", fmt.Errorf("read orbit node key file: %w", err)
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
2022-10-03 20:28:19 +00:00
|
|
|
var (
|
|
|
|
|
orbitNodeKey_ string
|
|
|
|
|
endpointDoesNotExist bool
|
|
|
|
|
)
|
|
|
|
|
if err := retry.Do(
|
|
|
|
|
func() error {
|
|
|
|
|
var err error
|
|
|
|
|
orbitNodeKey_, err = oc.enrollAndWriteNodeKeyFile()
|
|
|
|
|
switch {
|
|
|
|
|
case err == nil:
|
|
|
|
|
return nil
|
|
|
|
|
case errors.Is(err, notFoundErr{}):
|
|
|
|
|
// Do not retry if the endpoint does not exist.
|
|
|
|
|
endpointDoesNotExist = true
|
|
|
|
|
return nil
|
|
|
|
|
default:
|
2023-08-25 21:25:07 +00:00
|
|
|
logging.LogErrIfEnvNotSet(constant.SilenceEnrollLogErrorEnvVar, err, "enroll failed, retrying")
|
2022-10-03 20:28:19 +00:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-03-13 10:57:00 +00:00
|
|
|
// The below configuration means the following retry intervals (exponential backoff):
|
|
|
|
|
// 10s, 20s, 40s, 80s, 160s and then return the failure (max attempts = 6)
|
|
|
|
|
// thus executing no more than ~6 enroll request failures every ~5 minutes.
|
|
|
|
|
retry.WithInterval(orbitEnrollRetryInterval()),
|
2022-10-03 20:28:19 +00:00
|
|
|
retry.WithMaxAttempts(constant.OrbitEnrollMaxRetries),
|
2024-03-13 10:57:00 +00:00
|
|
|
retry.WithBackoffMultiplier(constant.OrbitEnrollBackoffMultiplier),
|
2022-10-03 20:28:19 +00:00
|
|
|
); err != nil {
|
|
|
|
|
return "", fmt.Errorf("orbit node key enroll failed, attempts=%d", constant.OrbitEnrollMaxRetries)
|
|
|
|
|
}
|
|
|
|
|
if endpointDoesNotExist {
|
|
|
|
|
return "", errors.New("enroll endpoint does not exist")
|
|
|
|
|
}
|
|
|
|
|
return orbitNodeKey_, nil
|
|
|
|
|
}
|
2022-09-23 19:00:23 +00:00
|
|
|
|
2023-08-29 13:50:13 +00:00
|
|
|
// GetNodeKey gets the orbit node key from file.
|
|
|
|
|
func (oc *OrbitClient) GetNodeKey() (string, error) {
|
|
|
|
|
orbitNodeKey, err := os.ReadFile(oc.nodeKeyFilePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return string(orbitNodeKey), nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
func (oc *OrbitClient) enrollAndWriteNodeKeyFile() (string, error) {
|
|
|
|
|
orbitNodeKey, err := oc.enroll()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("enroll request: %w", err)
|
|
|
|
|
}
|
2023-01-26 21:51:24 +00:00
|
|
|
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
|
|
|
|
|
// creating the secret file with empty content
|
|
|
|
|
if err := os.WriteFile(oc.nodeKeyFilePath, nil, constant.DefaultFileMode); err != nil {
|
|
|
|
|
return "", fmt.Errorf("create orbit node key file: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// restricting file access
|
|
|
|
|
if err := platform.ChmodRestrictFile(oc.nodeKeyFilePath); err != nil {
|
|
|
|
|
return "", fmt.Errorf("apply ACLs: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writing raw key material to the acl-ready secret file
|
2022-10-03 20:28:19 +00:00
|
|
|
if err := os.WriteFile(oc.nodeKeyFilePath, []byte(orbitNodeKey), constant.DefaultFileMode); err != nil {
|
|
|
|
|
return "", fmt.Errorf("write orbit node key file: %w", err)
|
|
|
|
|
}
|
2023-01-26 21:51:24 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
return orbitNodeKey, nil
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
2022-09-26 14:44:09 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
func (oc *OrbitClient) authenticatedRequest(verb string, path string, params interface{}, resp interface{}) error {
|
|
|
|
|
nodeKey, err := oc.getNodeKeyOrEnroll()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2022-09-26 14:44:09 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
s := params.(setOrbitNodeKeyer)
|
|
|
|
|
s.setOrbitNodeKey(nodeKey)
|
|
|
|
|
|
2022-11-29 16:54:36 +00:00
|
|
|
err = oc.request(verb, path, params, resp)
|
|
|
|
|
switch {
|
2022-10-03 20:28:19 +00:00
|
|
|
case err == nil:
|
|
|
|
|
oc.setEnrolled(true)
|
2022-09-26 14:44:09 +00:00
|
|
|
return nil
|
2022-10-03 20:28:19 +00:00
|
|
|
case errors.Is(err, ErrUnauthenticated):
|
|
|
|
|
if err := os.Remove(oc.nodeKeyFilePath); err != nil {
|
|
|
|
|
log.Info().Err(err).Msg("remove orbit node key")
|
|
|
|
|
}
|
|
|
|
|
oc.setEnrolled(false)
|
2025-07-18 14:31:52 +00:00
|
|
|
|
|
|
|
|
if oc.hostIdentityCertPath != "" {
|
|
|
|
|
if err := os.Remove(oc.hostIdentityCertPath); err != nil {
|
|
|
|
|
log.Info().Err(err).Msg("remove orbit host identity cert")
|
|
|
|
|
}
|
|
|
|
|
log.Info().Msg("removed orbit host identity cert, triggering a restart")
|
|
|
|
|
oc.receiverUpdateCancelFunc()
|
|
|
|
|
}
|
2022-10-03 20:28:19 +00:00
|
|
|
return err
|
|
|
|
|
default:
|
|
|
|
|
return err
|
2022-09-26 14:44:09 +00:00
|
|
|
}
|
2022-10-03 20:28:19 +00:00
|
|
|
}
|
2022-09-26 14:44:09 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
func (oc *OrbitClient) Enrolled() bool {
|
|
|
|
|
oc.enrolledMu.Lock()
|
|
|
|
|
defer oc.enrolledMu.Unlock()
|
|
|
|
|
|
|
|
|
|
return oc.enrolled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (oc *OrbitClient) setEnrolled(v bool) {
|
|
|
|
|
oc.enrolledMu.Lock()
|
|
|
|
|
defer oc.enrolledMu.Unlock()
|
|
|
|
|
|
|
|
|
|
oc.enrolled = v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (oc *OrbitClient) LastRecordedError() error {
|
|
|
|
|
oc.lastRecordedErrMu.Lock()
|
|
|
|
|
defer oc.lastRecordedErrMu.Unlock()
|
|
|
|
|
|
|
|
|
|
return oc.lastRecordedErr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (oc *OrbitClient) setLastRecordedError(err error) {
|
|
|
|
|
oc.lastRecordedErrMu.Lock()
|
|
|
|
|
defer oc.lastRecordedErrMu.Unlock()
|
|
|
|
|
|
|
|
|
|
oc.lastRecordedErr = fmt.Errorf("%s: %w", time.Now().UTC().Format("2006-01-02T15:04:05Z"), err)
|
2022-09-26 14:44:09 +00:00
|
|
|
}
|
2023-08-04 21:50:03 +00:00
|
|
|
|
2024-03-13 10:57:00 +00:00
|
|
|
func orbitEnrollRetryInterval() time.Duration {
|
2023-08-04 21:50:03 +00:00
|
|
|
interval := os.Getenv("FLEETD_ENROLL_RETRY_INTERVAL")
|
|
|
|
|
if interval != "" {
|
|
|
|
|
d, err := time.ParseDuration(interval)
|
|
|
|
|
if err == nil {
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return constant.OrbitEnrollRetrySleep
|
|
|
|
|
}
|
2023-10-06 22:04:33 +00:00
|
|
|
|
|
|
|
|
// SetOrUpdateDiskEncryptionKey sends a request to the server to set or update the disk
|
|
|
|
|
// encryption keys and result of the encryption process
|
|
|
|
|
func (oc *OrbitClient) SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/disk_encryption_key"
|
|
|
|
|
|
|
|
|
|
var resp orbitPostDiskEncryptionKeyResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitPostDiskEncryptionKeyRequest{
|
|
|
|
|
EncryptionKey: diskEncryptionStatus.EncryptionKey,
|
|
|
|
|
ClientError: diskEncryptionStatus.ClientError,
|
|
|
|
|
}, &resp); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2024-05-09 11:54:11 +00:00
|
|
|
|
|
|
|
|
const httpTraceTimeFormat = "2006-01-02T15:04:05Z"
|
|
|
|
|
|
|
|
|
|
var testStdoutHTTPTracer = &httptrace.ClientTrace{
|
|
|
|
|
ConnectStart: func(network, addr string) {
|
|
|
|
|
fmt.Printf(
|
|
|
|
|
"httptrace: %s: ConnectStart: %s, %s\n",
|
|
|
|
|
time.Now().UTC().Format(httpTraceTimeFormat), network, addr,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
ConnectDone: func(network, addr string, err error) {
|
|
|
|
|
fmt.Printf(
|
|
|
|
|
"httptrace: %s: ConnectDone: %s, %s, err='%s'\n",
|
|
|
|
|
time.Now().UTC().Format(httpTraceTimeFormat), network, addr, err,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
}
|
2024-10-14 21:15:42 +00:00
|
|
|
|
|
|
|
|
// GetSetupExperienceStatus checks the status of the setup experience for this host.
|
|
|
|
|
func (oc *OrbitClient) GetSetupExperienceStatus() (*fleet.SetupExperienceStatusPayload, error) {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/setup_experience/status"
|
|
|
|
|
var resp getOrbitSetupExperienceStatusResponse
|
|
|
|
|
err := oc.authenticatedRequest(verb, path, &getOrbitSetupExperienceStatusRequest{}, &resp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resp.Results, nil
|
|
|
|
|
}
|
2024-11-21 16:31:03 +00:00
|
|
|
|
|
|
|
|
func (oc *OrbitClient) SendLinuxKeyEscrowResponse(lr luks.LuksResponse) error {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/luks_data"
|
|
|
|
|
var resp orbitPostLUKSResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitPostLUKSRequest{
|
|
|
|
|
Passphrase: lr.Passphrase,
|
|
|
|
|
KeySlot: lr.KeySlot,
|
|
|
|
|
Salt: lr.Salt,
|
|
|
|
|
ClientError: lr.Err,
|
|
|
|
|
}, &resp); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-09-05 14:07:03 +00:00
|
|
|
|
|
|
|
|
func (oc *OrbitClient) InitiateSetupExperience() (fleet.SetupExperienceInitResult, error) {
|
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/setup_experience/init"
|
|
|
|
|
var resp orbitSetupExperienceInitResponse
|
|
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitSetupExperienceInitRequest{}, &resp); err != nil {
|
|
|
|
|
return fleet.SetupExperienceInitResult{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resp.Result, nil
|
|
|
|
|
}
|