Files
George Karr a25ae3ddfc Adding initial tool - dibble the tapir for seeding data (#46122)
## 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 -->
2026-06-17 11:07:05 -05:00

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 }