diff --git a/.golangci.yml b/.golangci.yml index c0e90e3c5c..c18c447b07 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,7 +33,7 @@ linters-settings: settings: ruleguard: rules: "./tools/ci/rules.go" - failOn: all # NEW + failOn: all gofmt: # simplify code: gofmt with `-s` option, true by default diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 7943a81f4e..33ff59c4a6 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -977,14 +977,14 @@ func getSoftwareCommand() *cli.Command { return errors.New("Can't specify both yaml and json flags.") } - var teamID *uint + query := url.Values{} - teamIDFlag := c.Uint(teamFlagName) - if teamIDFlag != 0 { - teamID = &teamIDFlag + teamID := c.Uint(teamFlagName) + if teamID != 0 { + query.Set("team_id", strconv.FormatUint(uint64(teamID), 10)) } - software, err := client.ListSoftware(teamID) + software, err := client.ListSoftware(query.Encode()) if err != nil { return fmt.Errorf("could not list software: %w", err) } diff --git a/go.mod b/go.mod index b5439eca0d..c0ee46dfc9 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,11 @@ require ( github.com/aws/aws-sdk-go v1.43.16 github.com/beevik/etree v1.1.0 github.com/briandowns/spinner v1.13.0 + github.com/cenkalti/backoff v2.1.1+incompatible github.com/cenkalti/backoff/v4 v4.1.3 github.com/davecgh/go-spew v1.1.1 github.com/dgraph-io/badger/v2 v2.2007.2 + github.com/docker/docker v20.10.17+incompatible github.com/doug-martin/goqu/v9 v9.18.0 github.com/e-dard/netbug v0.0.0-20151029172837-e64d308a0b20 github.com/elazarl/go-bindata-assetfs v1.0.0 @@ -150,7 +152,6 @@ require ( github.com/caarlos0/env/v6 v6.7.0 // indirect github.com/caarlos0/go-shellwords v1.0.12 // indirect github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect - github.com/cenkalti/backoff v2.1.1+incompatible // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect @@ -160,6 +161,9 @@ require ( github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/elastic/go-licenser v0.3.1 // indirect github.com/elastic/go-sysinfo v1.1.1 // indirect @@ -185,6 +189,7 @@ require ( github.com/go-stack/stack v1.8.1 // indirect github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -230,6 +235,8 @@ require ( github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect github.com/oschwald/maxminddb-golang v1.8.0 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/pelletier/go-toml v1.9.3 // indirect @@ -246,6 +253,7 @@ require ( github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect github.com/secure-systems-lab/go-securesystemslib v0.3.1 // indirect github.com/sergi/go-diff v1.2.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect github.com/slack-go/slack v0.9.4 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/go.sum b/go.sum index a4be1ae5fe..4502d7c134 100644 --- a/go.sum +++ b/go.sum @@ -92,6 +92,7 @@ github.com/Azure/go-amqp v0.13.11/go.mod h1:D5ZrjQqB1dyp1A+G73xeL/kNn7D5qHJIIsNN github.com/Azure/go-amqp v0.13.12/go.mod h1:D5ZrjQqB1dyp1A+G73xeL/kNn7D5qHJIIsNNps7YNmk= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +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/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= @@ -567,18 +568,22 @@ github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= github.com/docker/go v1.5.1-1/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= @@ -1293,12 +1298,14 @@ github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= @@ -1363,11 +1370,13 @@ github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1 github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +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.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= @@ -2488,8 +2497,10 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/server/service/client_hosts.go b/server/service/client_hosts.go index b5ae525671..2e8fa245e2 100644 --- a/server/service/client_hosts.go +++ b/server/service/client_hosts.go @@ -1,6 +1,8 @@ package service import ( + "fmt" + "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" ) @@ -13,6 +15,13 @@ func (c *Client) GetHosts(query string) ([]HostResponse, error) { return responseBody.Hosts, err } +func (c *Client) GetHost(id uint) (*HostDetailResponse, error) { + verb, path := "GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", id) + var responseBody getHostResponse + err := c.authenticatedRequest(nil, verb, path, &responseBody) + return responseBody.Host, err +} + // HostByIdentifier retrieves a host by the uuid, osquery_host_id, hostname, or // node_key. func (c *Client) HostByIdentifier(identifier string) (*HostDetailResponse, error) { diff --git a/server/service/client_software.go b/server/service/client_software.go index f8af1b6efa..e7a0fca8d9 100644 --- a/server/service/client_software.go +++ b/server/service/client_software.go @@ -1,18 +1,12 @@ package service import ( - "fmt" - "github.com/fleetdm/fleet/v4/server/fleet" ) // ListSoftware retrieves the software running across hosts. -func (c *Client) ListSoftware(teamID *uint) ([]fleet.Software, error) { +func (c *Client) ListSoftware(query string) ([]fleet.Software, error) { verb, path := "GET", "/api/latest/fleet/software" - query := "" - if teamID != nil { - query = fmt.Sprintf("team_id=%d", *teamID) - } var responseBody listSoftwareResponse err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query) if err != nil { diff --git a/test/upgrade/README.md b/test/upgrade/README.md new file mode 100644 index 0000000000..c3561c0cd6 --- /dev/null +++ b/test/upgrade/README.md @@ -0,0 +1,16 @@ +# Upgrade Tests + +The tests located in `test/upgrade` are intended to test fleet upgrades with online migrations as proposed in [#6376](https://github.com/fleetdm/fleet/pull/6376). +To run the tests, you need to specify the from and to versions. For example + +``` +$ FLEET_VERSION_A=v4.16.0 FLEET_VERSION_B=v4.17.0 go test ./test/upgrade +``` + +Ensure that Docker is installed with Compose V2. +To check if you have the correct version, run the following command + +``` +$ docker compose version +Docker Compose version v2.6.0 +``` diff --git a/test/upgrade/docker-compose.yaml b/test/upgrade/docker-compose.yaml new file mode 100644 index 0000000000..693c333ad7 --- /dev/null +++ b/test/upgrade/docker-compose.yaml @@ -0,0 +1,67 @@ +services: + mysql: + platform: ${FLEET_MYSQL_PLATFORM:-linux/x86_64} + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: toor + MYSQL_DATABASE: fleet + MYSQL_USER: fleet + MYSQL_PASSWORD: fleet + ports: + - "3306" + + redis: + image: redis:6 + + # reverse proxy and tls termination for fleet-a and fleet-b + fleet: + image: nginx + volumes: + # don't mount the config. These will be copied manually so that + # we can reload nginx without recreating containers and getting a new public port each time. + # - ./nginx/fleet-a.conf:/etc/nginx/conf.d/default.conf + - ./fleet.crt:/etc/nginx/fleet.crt + - ./fleet.key:/etc/nginx/fleet.key + ports: + - "443" + + fleet-a: &default-fleet + image: fleetdm/fleet:${FLEET_VERSION_A:-latest} + environment: + FLEET_MYSQL_ADDRESS: mysql:3306 + FLEET_MYSQL_DATABASE: fleet + FLEET_MYSQL_USERNAME: fleet + FLEET_MYSQL_PASSWORD: fleet + FLEET_REDIS_ADDRESS: redis:6379 + FLEET_SERVER_ADDRESS: 0.0.0.0:8080 + FLEET_SERVER_TLS: 'false' + FLEET_LOGGING_JSON: 'true' + FLEET_BETA_SOFTWARE_INVENTORY: 1 + FLEET_LICENSE_KEY: ${FLEET_LICENSE_KEY} + FLEET_OSQUERY_LABEL_UPDATE_INTERVAL: 1m + FLEET_VULNERABILITIES_CURRENT_INSTANCE_CHECKS: "yes" + FLEET_VULNERABILITIES_DATABASES_PATH: /fleet/vulndb + FLEET_VULNERABILITIES_PERIODICITY: 5m + FLEET_LOGGING_DEBUG: 'true' + # This can be configured for testing purposes but otherwise uses the + # typical default of provided. + FLEET_OSQUERY_HOST_IDENTIFIER: ${FLEET_OSQUERY_HOST_IDENTIFIER:-provided} + ports: + - "8080" + depends_on: + - mysql + - redis + + # Uses a different version than fleet-a + fleet-b: + <<: *default-fleet + image: fleetdm/fleet:${FLEET_VERSION_B:-latest} + + osquery: + image: "osquery/osquery:4.6.0-ubuntu20.04" + volumes: + - ./fleet.crt:/etc/osquery/fleet.crt + - ./osquery.flags:/etc/osquery/osquery.flags + environment: + ENROLL_SECRET: "${ENROLL_SECRET}" + command: osqueryd --flagfile=/etc/osquery/osquery.flags diff --git a/test/upgrade/fleet.crt b/test/upgrade/fleet.crt new file mode 100644 index 0000000000..21b641a8fe --- /dev/null +++ b/test/upgrade/fleet.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEyDCCArCgAwIBAgIJALXDyAqDa1hCMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV +BAMMBWZsZWV0MB4XDTIyMDcwNDE4MTUwN1oXDTMyMDcwMTE4MTUwN1owEDEOMAwG +A1UEAwwFZmxlZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCt7sQJ +XtNFuP9URxv2mSYYb9ePvPHNQENGkEDvpJUT3BRXmPHrdaCAbi2kDM+/f+M0smMn +TQdVn9/PWRrIFQjGpW+AKtnvhR6O6MgfflJonIewRYertfyjwpPWzGGre12jB9Og +3T4fzlGy+GnYE9dKvVUaS+Tm+DH0yCle0YRfjJFEaMrmqTAIKxkz9rUDqp6iF3mr +vkonhwI1D9VsMDcECvgw8MG5A3N0Yte1MV4OrFD9f88xrmx8b8F5HaozQVLBT0eA +0hQ5RUFZC9mGD5hs0B6qvvfOKHO8RJjdydHMTD/w7RpyU+PzTE2qT3ofrc1Ecaow +CAAPwSwlPc00qPw9Wfdyw/GzkMCkTgf6XmkdSAahdc69YfjA1OpR/nlUziQTM60Y +MjKn96lNuOP87AbgxEPyG8rTkuNUXSEjQeGNmntE9yLmYKLVQcbetzviDgJXM4Xw +YlDRMwCsqwJYJmKrY+XYC3nqdVoyJSgWIMPh5SCUTzq5gUoT3jl1I7J4rCZmM3jy +BMITbDElv3p9HTKv/RIUVMr9N3i/NyRf00SgO6guIqz1vNjB2YhmtVpQiDw+wLxa +wfW5p9vrB7XQNM5/CVPDh2ScVq2ZZam6dQEdltSwooLqHbL2dv0iIGTo4mX31PQz +LUmC9RbB3k6WkplpvH+oq31mC/Hmst1BvQc4XwIDAQABoyUwIzAhBgNVHREEGjAY +ggdmbGVldC1hggdmbGVldC1ihwQKAAABMA0GCSqGSIb3DQEBCwUAA4ICAQBijPir +B3ePlqiWvyOJo4OyxURLG8mBzH/xNabpUY3BFBwgauEKGm/LzAgeQzbt5B5g9V9q +waVt7meRKEC+HJi+vEjVLXx7rk40z9m67GEfhC/DvSiGeWty3pQFzNEHlyHZmi9f +Z1zR6AVgPSdTCooPPWgi5cBWjq/kG4gxJT7Wvrw9IuWzlgwlkkux1gwVuWZ6GLil +pq5T5PGdsBg0W3S3ssxayQ3Nl2qYeKqRiN/0ynR+RIf4VqZGySXwIECHROC4fpsA +UoJxmABYdBjjvfIgLJyM+bbLVXRLAM6I7wabgaICDPsFc4Ax+TjOdfao/SkTRDUk +zsSuc5SpfVUEQxcpVxwpyRpRlO4YGrCrgozzx7Sal/PGp+Hs3L2uC/JguUaVRUGS +yZhaiFQjLNXeND9ARwtEQkpMWrjAnICVwX80mlcwfx5JK2zFQe5yJusMynu+Ecv4 +PuJRl6LMd+m7hof1oWWMHYKbzj7VaefM0C0GvFCoX2YeBHpr7FmqaEOVtehDgizK +Nrz2c92VUFn77vmIphNeR8cnJou5AB1L9pSelNYO0wcABhLktsyOaQyftjN3aCA/ +cUn73RXyud8jg63W4dcsT2O5eaANc0rlZefjy57l+PDg8joIdFDIN4Ul2B6f7tfY +yuYXq7cV8AZFRKVEOcpmLUOexq1AtEvslZqC3Q== +-----END CERTIFICATE----- diff --git a/test/upgrade/fleet.key b/test/upgrade/fleet.key new file mode 100644 index 0000000000..0c2b1c2ac8 --- /dev/null +++ b/test/upgrade/fleet.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +RTtLhMIZYvzUXRhi7Hwm2KCjG3kVxg== +-----END PRIVATE KEY----- diff --git a/test/upgrade/fleet_test.go b/test/upgrade/fleet_test.go new file mode 100644 index 0000000000..7c6cb978cf --- /dev/null +++ b/test/upgrade/fleet_test.go @@ -0,0 +1,369 @@ +package upgrade + +import ( + "bytes" + "context" + "errors" + "fmt" + "math/rand" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/cenkalti/backoff" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" + "github.com/fleetdm/fleet/v4/server/service" + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" +) + +// Slots correspond to docker-compose fleet services, either fleet-a or fleet-b +const ( + slotA = "a" + slotB = "b" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +// Fleet represents the fleet server and its dependencies used for testing. +type Fleet struct { + // ProjectName is the docker compose project name + ProjectName string + // FilePath is the path to the docker-compose.yml + FilePath string + // Version is the active fleet version. + Version string + // Token is the fleet token used for authentication + Token string + + dockerClient client.ContainerAPIClient +} + +// NewFleet starts fleet and it's dependencies with the specified version. +func NewFleet(t *testing.T, version string) *Fleet { + // don't use test name because it will be normalized + //nolint:gosec // does not need to be secure for tests + projectName := "fleet-test-" + strconv.FormatUint(rand.Uint64(), 16) + + dockerClient, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + t.Fatalf("create docker client: %v", err) + } + + f := &Fleet{ + ProjectName: projectName, + FilePath: "docker-compose.yaml", + Version: version, + dockerClient: dockerClient, + } + + t.Cleanup(f.cleanup) + + if err := f.Start(); err != nil { + t.Fatalf("start fleet: %v", err) + } + + return f +} + +func (f *Fleet) Start() error { + env := map[string]string{ + "FLEET_VERSION_A": f.Version, + } + _, err := f.execCompose(env, "pull", "--parallel") + if err != nil { + return err + } + + // start mysql and wait until ready + _, err = f.execCompose(env, "up", "-d", "mysql") + if err != nil { + return err + } + if err := f.waitMYSQL(); err != nil { + return err + } + + // run the migrations using the fleet-a service + _, err = f.execCompose(env, "run", "-T", "fleet-a", "fleet", "prepare", "db", "--no-prompt") + if err != nil { + return err + } + + // start fleet-a + _, err = f.execCompose(env, "up", "-d", "fleet-a", "fleet") + if err != nil { + return err + } + + // copy the nginx conf and reload nginx without creating a new container + srcPath := filepath.Join("nginx", "fleet-a.conf") + _, err = f.execCompose(env, "cp", srcPath, "fleet:/etc/nginx/conf.d/default.conf") + if err != nil { + return err + } + + _, err = f.execCompose(env, "exec", "-T", "fleet", "nginx", "-s", "reload") + if err != nil { + return err + } + + if err := f.waitFleet(slotA); err != nil { + return err + } + + if err := f.setupFleet(); err != nil { + return err + } + + return nil +} + +// Client returns a fleet client that uses the fleet API. +func (f *Fleet) Client() (*service.Client, error) { + port, err := f.getPublicPort("fleet", 443) + if err != nil { + return nil, fmt.Errorf("get fleet port: %v", err) + } + + address := fmt.Sprintf("https://localhost:%d", port) + client, err := service.NewClient(address, true, "", "") + if err != nil { + return nil, err + } + + client.SetToken(f.Token) + + return client, nil +} + +func (f *Fleet) setupFleet() error { + client, err := f.Client() + if err != nil { + return err + } + + token, err := client.Setup("admin@example.com", "Admin", "password123#", "Fleet Test") + if err != nil { + return err + } + f.Token = token + + return nil +} + +func (f *Fleet) waitMYSQL() error { + + // get the random mysql host port assigned by docker + port, err := f.getPublicPort("mysql", 3306) + if err != nil { + return err + } + + dsn := fmt.Sprintf("fleet:fleet@tcp(localhost:%d)/fleet", port) + + retryInterval := 5 * time.Second + timeout := 1 * time.Minute + + ticker := time.NewTicker(retryInterval) + defer ticker.Stop() + + timeoutChan := time.After(timeout) + for { + select { + case <-timeoutChan: + return fmt.Errorf("db connection failed after %s", timeout) + case <-ticker.C: + db, err := sqlx.Connect("mysql", dsn) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to connect to db: %v\n", err) + } else { + db.Close() + return nil + } + } + } +} + +func (f *Fleet) getPublicPort(serviceName string, privatePort uint16) (uint16, error) { + containerName := fmt.Sprintf("%s-%s-1", f.ProjectName, serviceName) + + // get the random fleet host port assigned by docker + argsName := filters.Arg("name", containerName) + containers, err := f.dockerClient.ContainerList(context.TODO(), types.ContainerListOptions{Filters: filters.NewArgs(argsName), All: true}) + if err != nil { + return 0, err + } + if len(containers) == 0 { + return 0, errors.New("no containers found") + } + for _, port := range containers[0].Ports { + if port.PrivatePort == privatePort { + return port.PublicPort, nil + } + } + return 0, errors.New("private port not found") +} + +func (f *Fleet) waitFleet(slot string) error { + containerName := fmt.Sprintf("%s-fleet-%s-1", f.ProjectName, slot) + + // get the random fleet host port assigned by docker + argsName := filters.Arg("name", containerName) + containers, err := f.dockerClient.ContainerList(context.TODO(), types.ContainerListOptions{Filters: filters.NewArgs(argsName), All: true}) + if err != nil { + return err + } + if len(containers) == 0 { + return errors.New("no fleet container found") + } + port := containers[0].Ports[0].PublicPort + healthURL := fmt.Sprintf("http://localhost:%d/healthz", port) + + retryStrategy := backoff.NewExponentialBackOff() + retryStrategy.MaxInterval = 1 * time.Second + + if err := backoff.Retry( + func() error { + //nolint:gosec // G107: Ok to trust docker here + resp, err := http.Get(healthURL) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("non-200 status code: %d", resp.StatusCode) + } + return nil + }, + retryStrategy, + ); err != nil { + return fmt.Errorf("check health: %v", err) + } + + return nil +} + +func (f *Fleet) cleanup() { + output, err := f.execCompose(nil, "down", "-v", "--remove-orphans") + if err != nil { + fmt.Fprintf(os.Stderr, "stop fleet: %v %s", err, output) + } +} + +func (f *Fleet) execCompose(env map[string]string, args ...string) (string, error) { + + // docker compose variables via environment eg FLEET_VERSION_A + e := os.Environ() + for k, v := range env { + e = append(e, fmt.Sprintf("%s=%s", k, v)) + } + + // prepend default args + args = append([]string{ + "compose", + "--project-name", f.ProjectName, + "--file", f.FilePath, + }, args...) + + var stdout, stderr bytes.Buffer + + cmd := exec.Command("docker", args...) + cmd.Env = e + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("docker: %v %s", err, stderr.String()) + } + + return stdout.String(), nil +} + +// StartHost starts an osquery host using docker-compose and enrolls it with fleet. +// Returns the container ID which is also the hostname and osquery host ID. +func (f *Fleet) StartHost() (string, error) { + // get the enroll secret + client, err := f.Client() + if err != nil { + return "", err + } + + enrollSecretSpec, err := client.GetEnrollSecretSpec() + if err != nil { + return "", err + } + if len(enrollSecretSpec.Secrets) == 0 { + return "", errors.New("no enroll secret found") + } + + enrollSecret := enrollSecretSpec.Secrets[0].Secret + + env := map[string]string{ + "ENROLL_SECRET": enrollSecret, + } + output, err := f.execCompose(env, "run", "-d", "-T", "osquery") + if err != nil { + return "", err + } + + // get the container id + containerID := output[:len(output)-1] // strip the newline from output + + // inspect the container to get the hostname + containerJSON, err := f.dockerClient.ContainerInspect(context.Background(), containerID) + if err != nil { + return "", fmt.Errorf("inspect container: %v", err) + } + hostname := containerJSON.Config.Hostname + + return hostname, nil +} + +// Upgrade upgrades fleet to a specified version. +func (f *Fleet) Upgrade(toVersion string) error { + env := map[string]string{ + "FLEET_VERSION_B": toVersion, + } + + // run migrations using fleet-b + serviceName := "fleet-b" + _, err := f.execCompose(env, "run", "-T", serviceName, "fleet", "prepare", "db", "--no-prompt") + if err != nil { + return fmt.Errorf("run migrations: %v", err) + } + + // start the service + _, err = f.execCompose(env, "up", "-d", serviceName) + if err != nil { + return fmt.Errorf("start fleet: %v", err) + } + + // wait until healthy + if err := f.waitFleet(slotB); err != nil { + return fmt.Errorf("wait for fleet to be healthy: %v", err) + } + + // copy the nginx conf and reload nginx without creating a new container + srcPath := filepath.Join("nginx", "fleet-b.conf") + _, err = f.execCompose(env, "cp", srcPath, "fleet:/etc/nginx/conf.d/default.conf") + if err != nil { + return err + } + + _, err = f.execCompose(env, "exec", "-T", "fleet", "nginx", "-s", "reload") + if err != nil { + return err + } + + f.Version = toVersion + + return nil +} diff --git a/test/upgrade/generate_cert.sh b/test/upgrade/generate_cert.sh new file mode 100755 index 0000000000..3909fe1243 --- /dev/null +++ b/test/upgrade/generate_cert.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ + -keyout fleet.key -out fleet.crt -extensions san -config \ + <(echo "[req]"; + echo distinguished_name=req; + echo "[san]"; + echo subjectAltName=DNS:fleet-a,DNS:fleet-b + ) \ + -subj "/CN=fleet" diff --git a/test/upgrade/nginx/fleet-a.conf b/test/upgrade/nginx/fleet-a.conf new file mode 100644 index 0000000000..682a15c1bf --- /dev/null +++ b/test/upgrade/nginx/fleet-a.conf @@ -0,0 +1,11 @@ +server { + listen 443 ssl; + server_name fleet; + ssl_certificate fleet.crt; + ssl_certificate_key fleet.key; + + location / { + proxy_pass http://fleet-a:8080; + } +} + diff --git a/test/upgrade/nginx/fleet-b.conf b/test/upgrade/nginx/fleet-b.conf new file mode 100644 index 0000000000..c54967e505 --- /dev/null +++ b/test/upgrade/nginx/fleet-b.conf @@ -0,0 +1,11 @@ +server { + listen 443 ssl; + server_name fleet; + ssl_certificate fleet.crt; + ssl_certificate_key fleet.key; + + location / { + proxy_pass http://fleet-b:8080; + } +} + diff --git a/test/upgrade/osquery.flags b/test/upgrade/osquery.flags new file mode 100644 index 0000000000..a81ffc1367 --- /dev/null +++ b/test/upgrade/osquery.flags @@ -0,0 +1,27 @@ +--verbose=true +--debug + +--tls_hostname=fleet +--tls_server_certs=/etc/osquery/fleet.crt + +--enroll_secret_env=ENROLL_SECRET +--enroll_tls_endpoint=/api/v1/osquery/enroll + +--config_plugin=tls +--config_tls_endpoint=/api/v1/osquery/config +--config_refresh=10 + +--disable_distributed=false +--distributed_plugin=tls +--distributed_interval=10 +--distributed_tls_max_attempts=3 +--distributed_tls_read_endpoint=/api/v1/osquery/distributed/read +--distributed_tls_write_endpoint=/api/v1/osquery/distributed/write + +--logger_plugin=tls +--logger_tls_endpoint=/api/v1/osquery/log +--logger_tls_period=10 + +--disable_carver=false +--carver_start_endpoint=/api/v1/osquery/carve/begin +--carver_continue_endpoint=/api/v1/osquery/carve/block diff --git a/test/upgrade/upgrade_test.go b/test/upgrade/upgrade_test.go new file mode 100644 index 0000000000..4b1b9549ea --- /dev/null +++ b/test/upgrade/upgrade_test.go @@ -0,0 +1,57 @@ +package upgrade + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func enrollHost(t *testing.T, f *Fleet) string { + client, err := f.Client() + require.NoError(t, err) + + // enroll a host + hostname, err := f.StartHost() + require.NoError(t, err) + + // wait until host is enrolled and software is listed + require.Eventually(t, func() bool { + host, err := client.HostByIdentifier(hostname) + if err != nil { + t.Logf("get host: %v", err) + return false + } + + if len(host.Software) == 0 { + return false + } + + return true + }, 5*time.Minute, 5*time.Second) + + return hostname +} + +func TestUpgradeAToB(t *testing.T) { + versionA := os.Getenv("FLEET_VERSION_A") + if versionA == "" { + t.Skip("Missing environment variable FLEET_VERSION_A") + } + + versionB := os.Getenv("FLEET_VERSION_B") + if versionB == "" { + t.Skip("Missing environment variable FLEET_VERSION_B") + } + + f := NewFleet(t, versionA) + + enrollHost(t, f) + + err := f.Upgrade(versionB) + require.NoError(t, err) + + // enroll another host with the new version + enrollHost(t, f) +}