add upgrade tests (#6596)

* add upgrade tests

* fix lint issues

go.mod

* remove req.cnf

* revert unrelated changes

* make version configurable in test

* fix golangci-lint ruleguard issue

Related to https://github.com/go-critic/go-critic/issues/1152
Need to have github.com/quasilyte/go-ruleguard/dsl

* fix lint issues

* fix

* clean up docker-compose.yml

* fix http request

* add readme

* fix lint issues

* address feedback

* fix

* add platform

* address feedback

* run go fmt
This commit is contained in:
Michal Nicpon 2022-07-19 15:11:51 -06:00 committed by GitHub
parent 51e505c3f6
commit d4be5ad2a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 684 additions and 14 deletions

View file

@ -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

View file

@ -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)
}

10
go.mod
View file

@ -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

11
go.sum
View file

@ -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=

View file

@ -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) {

View file

@ -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 {

16
test/upgrade/README.md Normal file
View file

@ -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
```

View file

@ -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

28
test/upgrade/fleet.crt Normal file
View file

@ -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-----

52
test/upgrade/fleet.key Normal file
View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
RTtLhMIZYvzUXRhi7Hwm2KCjG3kVxg==
-----END PRIVATE KEY-----

369
test/upgrade/fleet_test.go Normal file
View file

@ -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
}

10
test/upgrade/generate_cert.sh Executable file
View file

@ -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"

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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)
}