## Overview `dibble` is a one-stop CLI for seeding a Fleet server with test data — users, teams, policies, reports, labels, scripts, MDM profiles, software, secrets, CAs, and vulns — replacing ~8 ad-hoc seeding tools with a single binary. It makes it easy to: - **Spin up a populated dev/test server in one command** — `dibble all` plants everything with sensible, idempotent defaults. - **Skip the flag-memorization** — running `dibble` with no args launches an interactive wizard that prompts for Fleet URL, API token, theme, and which entities to seed, and offers to save the config to `~/.dibble.yaml`. - **Seed individual entity types** — `dibble users`, `dibble teams`, `dibble policies`, etc., when you only need one slice. - **Get themed, recognizable test data** — pick a theme (hitchhikers, tng, lotr, ghibli, parksrec, …) so seeded names are easy to eyeball in the UI. Hosts are intentionally out of scope — `cmd/osquery-perf` still owns that. `dibble hosts` is a thin convenience wrapper that picks a fleet, fetches its enroll secret, and prints/runs the osquery-perf invocation for you. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Dibble: a CLI tool to seed realistic test data, including an interactive wizard and subcommands for teams/users/software/policies/scripts/reports/profiles/labels/activities/enroll-secrets/hosts/vulns, plus theme-driven “cas” and “ping”. * Theme system: multiple curated themes to generate consistent seeded identities, policies, software, labels, and scripts. * **Chores** * Ignored the built dibble binary and added a Makefile build target to compile the dibble tool. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
package seed
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/fleetdm/fleet/v4/tools/dibble/pkg/themes"
|
|
)
|
|
|
|
// Team is the minimal team representation we need to chain seeders. Many
|
|
// downstream seeders (policies, profiles, scripts) need the integer ID.
|
|
type Team struct {
|
|
ID uint
|
|
Name string
|
|
}
|
|
|
|
type teamCreateResp struct {
|
|
Team struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
} `json:"team"`
|
|
// The newer API renames "team" → "fleet" in some responses.
|
|
Fleet struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
} `json:"fleet"`
|
|
}
|
|
|
|
// Teams creates `count` teams (aka fleets) and returns the resulting list.
|
|
// Existing teams with the same name are looked up so callers always get an ID.
|
|
func Teams(c Client, log Logger, theme themes.Theme, count int) ([]Team, Result) {
|
|
res := Result{Entity: "teams"}
|
|
teams := make([]Team, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
name := themes.TeamName(theme, i)
|
|
body := map[string]any{"name": name}
|
|
var resp teamCreateResp
|
|
err := c.Post("/api/latest/fleet/fleets", body, &resp)
|
|
id := resp.Team.ID
|
|
if id == 0 {
|
|
id = resp.Fleet.ID
|
|
}
|
|
switch {
|
|
case err == nil:
|
|
res.Created++
|
|
teams = append(teams, Team{ID: id, Name: name})
|
|
log.Printf("team %s (id=%d)", name, id)
|
|
case IsAlreadyExists(err):
|
|
res.Skipped++
|
|
// Look up the existing team so downstream seeders can scope by
|
|
// team id. A genuine lookup failure (network, auth) is a hard
|
|
// error; "not found" is silently ignored so a renamed/deleted
|
|
// team doesn't block the rest of the run.
|
|
got, lookupErr := findTeamByName(c, name)
|
|
switch {
|
|
case lookupErr == nil:
|
|
teams = append(teams, got)
|
|
case errors.As(lookupErr, new(errTeamNotFound)):
|
|
// Team exists by name conflict but not in the list — odd,
|
|
// but treat as a no-op rather than failing the run.
|
|
default:
|
|
res.Errors = append(res.Errors,
|
|
fmt.Errorf("lookup existing team %q: %w", name, lookupErr))
|
|
}
|
|
default:
|
|
res.Errors = append(res.Errors, err)
|
|
}
|
|
}
|
|
return teams, res
|
|
}
|
|
|
|
type listTeamsResp struct {
|
|
Teams []struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
} `json:"teams"`
|
|
Fleets []struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
} `json:"fleets"`
|
|
}
|
|
|
|
func findTeamByName(c Client, name string) (Team, error) {
|
|
var resp listTeamsResp
|
|
if err := c.Get("/api/latest/fleet/fleets?per_page=500", &resp); err != nil {
|
|
return Team{}, err
|
|
}
|
|
list := resp.Teams
|
|
if len(list) == 0 {
|
|
for _, f := range resp.Fleets {
|
|
list = append(list, struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
}{ID: f.ID, Name: f.Name})
|
|
}
|
|
}
|
|
for _, t := range list {
|
|
if t.Name == name {
|
|
return Team{ID: t.ID, Name: t.Name}, nil
|
|
}
|
|
}
|
|
return Team{}, errTeamNotFound{name: name}
|
|
}
|
|
|
|
type errTeamNotFound struct{ name string }
|
|
|
|
func (e errTeamNotFound) Error() string { return "team not found: " + e.name }
|