Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,42 @@ stability contract and the GitHub release notes for the full feature list.
5. Verify `kubectl logs deploy/fleetsweeper | grep migrate` shows no errors
and `/readyz` returns 200.

## v0.16.0

No schema migration.

- **A bare scan scans every context.** `fleetsweeper scan`, `watch`, and the
`serve` scheduler with no `--contexts` or `--group` now scan every context in
the kubeconfig. `--all-contexts` is removed from all three and from the
`init` scaffold. Drop it from scripts and config files, since an unknown flag
is an error. A kubeconfig with no contexts exits with code 3 and names the
file.

- **The scan API takes an empty request.** `POST /api/scan` with `{}` scans
every context. The `all_contexts` field is removed and a request that sends
it gets 400. A ClusterScan with no `contexts` and no `group` also scans every
context, where before it scanned nothing.

- **The chart's scheduler renders contexts.** `scheduler.allContexts` is
removed. `scheduler.contexts` now renders one `--contexts` argument per
entry. Leave it empty to scan every context.

- **Scans are stored by default.** With no `--db`, every scan is saved to
`$XDG_STATE_HOME/fleetsweeper/fleet.db` when `XDG_STATE_HOME` is an absolute
path, else `~/.local/state/fleetsweeper/fleet.db` (the user config directory
on Windows). The directory is created 0700 and the file 0600, and the path is
printed once on first use. `--db` still overrides it. Commands that needed
`--db` (`group`, `history`, `compare`, `location`, `apikey`, and `scan
--group`) now use the default store. Exit code 4 now means no store path
could be resolved, i.e. no `--db` and no home directory. `watch` still stores
scans only when `--db` is set.

- **The terminal summary leads with coverage.** After a scan, stderr shows
coverage first (scanner runs observed, then any cluster with no complete
run), then the score or "not graded", then the top three fixes, then cohorts
and incidents. Scripts should read the JSON report on stdout, not this
summary.

## v0.15.0

No schema migration.
Expand Down
5 changes: 3 additions & 2 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ func TestContract(t *testing.T) {
okOrAPIError(t, "contexts", func() error { _, err := c.Contexts(ctx); return err }())
})

// An empty request scans every kubeconfig context, and this server's kubeconfig holds none.
t.Run("trigger scan empty", func(t *testing.T) {
_, err := c.TriggerScan(ctx, client.TriggerScanRequest{})
var apiErr *client.APIError
Expand All @@ -281,8 +282,8 @@ func TestContract(t *testing.T) {
if apiErr.StatusCode != http.StatusBadRequest {
t.Errorf("status = %d, want %d", apiErr.StatusCode, http.StatusBadRequest)
}
if apiErr.Message != "no contexts specified" {
t.Errorf("message = %q, want %q", apiErr.Message, "no contexts specified")
if apiErr.Message != "no contexts in kubeconfig" {
t.Errorf("message = %q, want %q", apiErr.Message, "no contexts in kubeconfig")
}
})

Expand Down
4 changes: 1 addition & 3 deletions client/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,8 @@ type TimelineResponse struct {

// TriggerScanRequest is the body for POST /api/scans.
type TriggerScanRequest struct {
// Contexts names the kubeconfig contexts to scan.
// Contexts names the kubeconfig contexts to scan. Empty with no Group scans every context.
Contexts []string `json:"contexts,omitempty"`
// AllContexts scans every kubeconfig context when true.
AllContexts bool `json:"all_contexts,omitempty"`
// Group scans the clusters in a named group.
Group string `json:"group,omitempty"`
}
Expand Down
8 changes: 0 additions & 8 deletions cmd/cmd_apikey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -140,10 +139,3 @@ func TestAPIKeyCreateValidation(t *testing.T) {
t.Error("expected error for invalid role")
}
}

// TestAPIKeyRequiresDB verifies list without --db returns the sentinel.
func TestAPIKeyRequiresDB(t *testing.T) {
if _, err := runAPIKey(t, "", "list"); !errors.Is(err, ErrNoDatabase) {
t.Errorf("want ErrNoDatabase, got %v", err)
}
}
7 changes: 0 additions & 7 deletions cmd/cmd_compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,3 @@ func TestCompareFormats(t *testing.T) {
}
}
}

// TestCompareRequiresDB verifies the missing-db path surfaces an error.
func TestCompareRequiresDB(t *testing.T) {
if _, err := execCompare(t, "", "a", "b", "--format=text"); err == nil {
t.Error("expected error without --db")
}
}
22 changes: 15 additions & 7 deletions cmd/cmd_doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,11 @@ func runCheck(parent context.Context, name string, timeout time.Duration, fn fun
return res
}

// doctorCheckDatabase opens the configured --db and pings it.
// doctorCheckDatabase opens --db, or the default state store when one exists, and pings it.
func doctorCheckDatabase(ctx context.Context, cmd *cobra.Command) CheckResult {
dbPath, _ := cmd.Flags().GetString("db")
if dbPath == "" {
return CheckResult{Status: StatusSkip, Detail: "no --db configured"}
dbPath, ok := existingDBPath(cmd)
if !ok {
return CheckResult{Status: StatusSkip, Detail: noStoreDetail(dbPath)}
}
driver, _ := cmd.Flags().GetString("db-driver")
if driver == "" {
Expand Down Expand Up @@ -403,14 +403,22 @@ func doctorCheckAdmissionWebhook(
return CheckResult{Status: StatusOK, Detail: fmt.Sprintf("%d webhook(s) registered", len(cfg.Webhooks))}
}

// noStoreDetail explains a skipped store check: no store yet at the default path, or no default.
func noStoreDetail(defaultPath string) string {
if defaultPath == "" {
return "no --db and no default state directory"
}
return "no store yet at " + defaultPath + "; the first scan creates it"
}

// doctorCheckScanFreshness reads the most recent scan from the
// configured store and warns when its timestamp is older than
// --scan-freshness. Distinguishes "no scans yet" (warn) from "stale
// scans" (fail) so first-install behavior isn't reported as broken.
func doctorCheckScanFreshness(ctx context.Context, cmd *cobra.Command) CheckResult {
dbPath, _ := cmd.Flags().GetString("db")
if dbPath == "" {
return CheckResult{Status: StatusSkip, Detail: "no --db configured"}
dbPath, ok := existingDBPath(cmd)
if !ok {
return CheckResult{Status: StatusSkip, Detail: noStoreDetail(dbPath)}
}
maxAge, _ := cmd.Flags().GetDuration("scan-freshness")
driver, _ := cmd.Flags().GetString("db-driver")
Expand Down
9 changes: 0 additions & 9 deletions cmd/cmd_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"context"
"errors"
"path/filepath"
"testing"

Expand Down Expand Up @@ -69,14 +68,6 @@ func TestGroupLifecycle(t *testing.T) {
}
}

// TestGroupRequiresDB verifies the missing-db sentinel from a group run.
func TestGroupRequiresDB(t *testing.T) {
t.Parallel()
if err := runGroupList(newGroupCmd(""), nil); !errors.Is(err, ErrNoDatabase) {
t.Errorf("want ErrNoDatabase, got %v", err)
}
}

// groupMembers reopens the store and returns the named group's cluster list.
func groupMembers(t *testing.T, dbPath, name string) []string {
t.Helper()
Expand Down
7 changes: 0 additions & 7 deletions cmd/cmd_history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,6 @@ func TestHistoryTrend(t *testing.T) {
}
}

// TestHistoryRequiresDB verifies the missing-db sentinel surfaces.
func TestHistoryRequiresDB(t *testing.T) {
if err := runHistory(t, "", "list"); !errors.Is(err, ErrNoDatabase) {
t.Errorf("want ErrNoDatabase, got %v", err)
}
}

// countScans reopens the store and returns the number of persisted scans.
func countScans(t *testing.T, dbPath string) int {
t.Helper()
Expand Down
2 changes: 0 additions & 2 deletions cmd/cmd_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ cors:

scheduler:
interval: 30m
allContexts: true

persistence:
enabled: true
Expand Down Expand Up @@ -156,7 +155,6 @@ admin-addr: "127.0.0.1:8081"
db: "/data/fleet.db"
controller: true
scan-interval: "30m"
all-contexts: true
audit-retention: "720h"
`

Expand Down
9 changes: 0 additions & 9 deletions cmd/cmd_location_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"context"
"errors"
"path/filepath"
"testing"

Expand Down Expand Up @@ -74,11 +73,3 @@ func TestLocationSetValidation(t *testing.T) {
t.Error("expected error for longitude > 180")
}
}

// TestLocationRequiresDB verifies the missing-db sentinel from a location run.
func TestLocationRequiresDB(t *testing.T) {
t.Parallel()
if err := runLocationList(newLocationCmd("", 0, 0, "", ""), nil); !errors.Is(err, ErrNoDatabase) {
t.Errorf("want ErrNoDatabase, got %v", err)
}
}
3 changes: 2 additions & 1 deletion cmd/cmd_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ func init() {
rootCmd.PersistentFlags().String("kubeconfig", defaultKubeconfig(), "Path to the kubeconfig file.")
rootCmd.PersistentFlags().Bool("pretty", false, "Pretty-print JSON output.")
rootCmd.PersistentFlags().String("log-level", "warn", "Log level (debug, info, warn, error).")
rootCmd.PersistentFlags().String("db", "", "Database DSN. SQLite: a filesystem path. Postgres: a postgres:// URL.")
rootCmd.PersistentFlags().String("db", "", "Database DSN. SQLite: a filesystem path. Postgres: a postgres:// URL. "+
"Empty uses $XDG_STATE_HOME/fleetsweeper/fleet.db, else ~/.local/state/fleetsweeper/fleet.db.")
rootCmd.PersistentFlags().String("db-driver", "", "Backend driver. One of sqlite, postgres. Empty auto-detects from the DSN prefix.")

rootCmd.AddCommand(scanCmd)
Expand Down
Loading
Loading