mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
23a2964eef
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.23+incompatible to 23.0.4+incompatible. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/docker/docker/releases">github.com/docker/docker's releases</a>.</em></p> <blockquote> <h2>v23.0.4</h2> <h2>23.0.4</h2> <p>For a full list of pull requests and changes in this release, refer to the relevant GitHub milestones:</p> <ul> <li><a href="https://github.com/docker/cli/milestone/77?closed=1">docker/cli, 23.0.4 milestone</a></li> <li><a href="https://github.com/moby/moby/milestone/117?closed=1">moby/moby, 23.0.4 milestone</a></li> </ul> <h3>Bug fixes and enhancements</h3> <ul> <li>Fix a performance regression in Docker CLI 23.0.0 <a href="https://redirect.github.com/docker/cli/pull/4141">docker/cli#4141</a>.</li> <li>Fix progress indicator on <code>docker cp</code> not functioning as intended <a href="https://redirect.github.com/docker/cli/pull/4157">docker/cli#4157</a>.</li> <li>Fix shell completion for <code>docker compose --file</code> <a href="https://redirect.github.com/docker/cli/pull/4177">docker/cli#4177</a>.</li> <li>Fix an error caused by incorrect handling of "default-address-pools" in <code>daemon.json</code> <a href="https://redirect.github.com/moby/moby/pull/45246">moby/moby#45246</a>.</li> </ul> <h3>Packaging Updates</h3> <ul> <li>Fix missing packages for CentOS 9 Stream.</li> <li>Upgrade Go to <code>1.19.8</code>. <a href="https://redirect.github.com/docker/docker-ce-packaging/pull/878">docker/docker-ce-packaging#878</a>, <a href="https://redirect.github.com/docker/cli/pull/4164">docker/cli#4164</a>, <a href="https://redirect.github.com/moby/moby/pull/45277">moby/moby#45277</a>, which contains fixes for <a href="https://github.com/advisories/GHSA-fp86-2355-v99r">CVE-2023-24537</a>, <a href="https://github.com/advisories/GHSA-v4m2-x4rp-hv22">CVE-2023-24538</a>, <a href="https://github.com/advisories/GHSA-8v5j-pwr7-w5f8">CVE-2023-24534</a>, and <a href="https://github.com/advisories/GHSA-9f7g-gqwh-jpf5">CVE-2023-24536</a></li> </ul> <h2>v23.0.3</h2> <h2>23.0.3</h2> <blockquote> <p><strong>Note</strong></p> <p>Due to an issue with CentOS 9 Stream's package repositories, packages for CentOS 9 are currently unavailable. Packages for CentOS 9 may be added later, or as part of the next (23.0.4) patch release.</p> </blockquote> <h3>Bug fixes and enhancements</h3> <ul> <li>Fixed a number of issues that can cause Swarm encrypted overlay networks to fail to uphold their guarantees, addressing <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28841">CVE-2023-28841</a>, <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28840">CVE-2023-28840</a>, and <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28842">CVE-2023-28842</a>. <ul> <li>A lack of kernel support for encrypted overlay networks now reports as an error.</li> <li>Encrypted overlay networks are eagerly set up, rather than waiting for multiple nodes to attach.</li> <li>Encrypted overlay networks are now usable on Red Hat Enterprise Linux 9 through the use of the <code>xt_bpf</code> kernel module.</li> <li>Users of Swarm overlay networks should review <a href="https://github.com/moby/moby/security/advisories/GHSA-vwm3-crmr-xfxw">GHSA-vwm3-crmr-xfxw</a> to ensure that unintentional exposure has not occurred.</li> </ul> </li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="cbce331930
"><code>cbce331</code></a> Merge pull request <a href="https://redirect.github.com/docker/docker/issues/45330">#45330</a> from kevingentile/buildkit-3770</li> <li><a href="5f684cb072
"><code>5f684cb</code></a> Merge pull request <a href="https://redirect.github.com/docker/docker/issues/45331">#45331</a> from thaJeztah/23.0_backport_rootless_script_bugs</li> <li><a href="3731ce10d4
"><code>3731ce1</code></a> Fix argument quoting bugs in dockerd-rootless.sh</li> <li><a href="23774ada04
"><code>23774ad</code></a> vendor: github.com/moby/buildkit v0.10.7-0.20230412161310-d52b2d584242</li> <li><a href="90e8a0bbf5
"><code>90e8a0b</code></a> Merge pull request <a href="https://redirect.github.com/docker/docker/issues/45323">#45323</a> from thaJeztah/23.0_backport_vendor_sctp</li> <li><a href="9277e64444
"><code>9277e64</code></a> vendor: github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2</li> <li><a href="cdb6200887
"><code>cdb6200</code></a> Merge pull request <a href="https://redirect.github.com/docker/docker/issues/45293">#45293</a> from AkihiroSuda/backport-45283-23</li> <li><a href="09fbbd5677
"><code>09fbbd5</code></a> docker-rootless-setuptools.sh: improve readability of messages</li> <li><a href="4ca4705bf7
"><code>4ca4705</code></a> Merge pull request <a href="https://redirect.github.com/docker/docker/issues/45277">#45277</a> from thaJeztah/23.0_bump_go1.19.8</li> <li><a href="d3e52936c3
"><code>d3e5293</code></a> [23.0] update go to go1.19.8</li> <li>Additional commits viewable in <a href="https://github.com/docker/docker/compare/v20.10.23...v23.0.4">compare view</a></li> </ul> </details> <br /> [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/docker/docker&package-manager=go_modules&previous-version=20.10.23+incompatible&new-version=23.0.4+incompatible)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) </details> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Zach Wasserman <zach@fleetdm.com>
368 lines
8.7 KiB
Go
368 lines
8.7 KiB
Go
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, client.WithAPIVersionNegotiation())
|
|
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
|
|
}
|