## 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 -->
202 lines
6.2 KiB
Go
202 lines
6.2 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/fleetdm/fleet/v4/tools/dibble/pkg/seed"
|
|
"github.com/fleetdm/fleet/v4/tools/dibble/pkg/themes"
|
|
)
|
|
|
|
// runAll executes every seeder in dependency order with the given counts.
|
|
// Used by both `dibble all` and the wizard's "everything" preset.
|
|
type allCounts struct {
|
|
Users int
|
|
Teams int
|
|
Policies int
|
|
Reports int
|
|
Labels int
|
|
Scripts int
|
|
Profiles int
|
|
Software int
|
|
CAs int
|
|
|
|
// EnrollSecrets is per-team and binary (one per team or none). True to
|
|
// rotate every team's secret; false to leave them alone.
|
|
EnrollSecrets bool
|
|
|
|
// ActivityBatches drives the direct-MySQL activity seeder. Zero skips
|
|
// it entirely — activities need a DSN, not just an API token, so we
|
|
// don't run them by default.
|
|
ActivityBatches int
|
|
ActivityDSN string
|
|
ActivityHostID uint
|
|
|
|
// IDPUserCount / IDPHostCount drive the direct-MySQL IDP seeder. Zero
|
|
// for either skips it entirely (it needs a DSN like activities).
|
|
IDPUserCount int
|
|
IDPHostCount int
|
|
IDPDSN string
|
|
|
|
// CAsDSN is the MySQL DSN used by the CA seeder. CAs > 0 also writes
|
|
// directly to MySQL, bypassing the service layer's URL validation.
|
|
CAsDSN string
|
|
}
|
|
|
|
func defaultAllCounts() allCounts {
|
|
return allCounts{
|
|
Users: 5,
|
|
Teams: 3,
|
|
Policies: 5,
|
|
Reports: 5,
|
|
Labels: 5,
|
|
Scripts: 3,
|
|
Profiles: 4,
|
|
Software: 5,
|
|
CAs: 0,
|
|
EnrollSecrets: true,
|
|
ActivityBatches: 0,
|
|
ActivityDSN: "fleet:insecure@tcp(localhost:3306)/fleet",
|
|
ActivityHostID: 1,
|
|
IDPUserCount: 0,
|
|
IDPHostCount: 0,
|
|
IDPDSN: "fleet:insecure@tcp(localhost:3306)/fleet",
|
|
CAsDSN: "fleet:insecure@tcp(localhost:3306)/fleet",
|
|
}
|
|
}
|
|
|
|
func runAll(c *Client, theme themes.Theme, counts allCounts) error {
|
|
start := time.Now()
|
|
log := seederLogger{}
|
|
|
|
// Collect every seeder's errors so `dibble all` exits non-zero when any
|
|
// step failed. Without this, CI / scripted runs treat a half-broken run
|
|
// as success.
|
|
var allErrs []error
|
|
report := func(res seed.Result) {
|
|
printf("%s", res.Summary())
|
|
allErrs = append(allErrs, res.Errors...)
|
|
}
|
|
|
|
// Teams first — many other seeders want a team list.
|
|
teams, tRes := seed.Teams(c, log, theme, counts.Teams)
|
|
report(tRes)
|
|
|
|
report(seed.Users(c, log, theme, counts.Users))
|
|
|
|
if counts.EnrollSecrets {
|
|
report(seed.EnrollSecrets(c, log, teams))
|
|
}
|
|
|
|
report(seed.Labels(c, log, theme, counts.Labels))
|
|
report(seed.Policies(c, log, theme, teams, counts.Policies))
|
|
report(seed.Reports(c, log, theme, teams, counts.Reports))
|
|
report(seed.Scripts(c, log, theme, teams, counts.Scripts))
|
|
report(seed.Profiles(c, log, theme, teams, counts.Profiles))
|
|
|
|
if counts.Software > 0 {
|
|
// `dibble all` lands software under the first existing team if any,
|
|
// otherwise no team. Use `dibble software` directly for team
|
|
// overrides — this path is intentionally simple.
|
|
swOpt := seed.SoftwareOptions{MaintainedAppCount: counts.Software}
|
|
if len(teams) > 0 {
|
|
swOpt.TeamID = teams[0].ID
|
|
}
|
|
report(seed.SoftwareCustom(c, log, swOpt))
|
|
report(seed.SoftwareMaintained(c, log, swOpt))
|
|
}
|
|
|
|
if counts.CAs > 0 {
|
|
report(seed.CAs(context.Background(), log, seed.CAOptions{
|
|
DSN: counts.CAsDSN,
|
|
Count: counts.CAs,
|
|
}))
|
|
}
|
|
|
|
if counts.ActivityBatches > 0 {
|
|
report(seed.Activities(context.Background(), log, seed.ActivitiesOptions{
|
|
DSN: counts.ActivityDSN,
|
|
HostID: counts.ActivityHostID,
|
|
Batches: counts.ActivityBatches,
|
|
}))
|
|
}
|
|
|
|
if counts.IDPUserCount > 0 {
|
|
report(seed.IDP(context.Background(), c, log, seed.IDPOptions{
|
|
DSN: counts.IDPDSN,
|
|
UserCount: counts.IDPUserCount,
|
|
HostCount: counts.IDPHostCount,
|
|
}))
|
|
}
|
|
|
|
elapsed := time.Since(start).Round(time.Millisecond)
|
|
fmt.Fprintf(stdoutish, "\n🌱 dibble done in %s (theme: %s).\n", elapsed, theme.Name)
|
|
return reportErrors(allErrs)
|
|
}
|
|
|
|
func newAllCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "all",
|
|
Short: "Seed every entity with sensible defaults (idempotent)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := requireConfig(); err != nil {
|
|
return err
|
|
}
|
|
c, err := newClientFromViper()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
theme, err := currentTheme()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
counts := defaultAllCounts()
|
|
if v, _ := cmd.Flags().GetInt("users"); v > 0 {
|
|
counts.Users = v
|
|
}
|
|
if v, _ := cmd.Flags().GetInt("teams"); v > 0 {
|
|
counts.Teams = v
|
|
}
|
|
if v, _ := cmd.Flags().GetInt("activity-batches"); v > 0 {
|
|
counts.ActivityBatches = v
|
|
}
|
|
if v, _ := cmd.Flags().GetString("activity-dsn"); v != "" {
|
|
counts.ActivityDSN = v
|
|
}
|
|
if v, _ := cmd.Flags().GetUint("activity-host-id"); v > 0 {
|
|
counts.ActivityHostID = v
|
|
}
|
|
if v, _ := cmd.Flags().GetInt("idp-user-count"); v > 0 {
|
|
counts.IDPUserCount = v
|
|
}
|
|
if v, _ := cmd.Flags().GetInt("idp-host-count"); v >= 0 && cmd.Flags().Changed("idp-host-count") {
|
|
counts.IDPHostCount = v
|
|
}
|
|
if v, _ := cmd.Flags().GetString("idp-dsn"); v != "" {
|
|
counts.IDPDSN = v
|
|
}
|
|
if v, _ := cmd.Flags().GetInt("cas"); v > 0 {
|
|
counts.CAs = v
|
|
}
|
|
if v, _ := cmd.Flags().GetString("cas-dsn"); v != "" {
|
|
counts.CAsDSN = v
|
|
}
|
|
return runAll(c, theme, counts)
|
|
},
|
|
}
|
|
cmd.Flags().Int("users", 0, "Override user count (0 = default)")
|
|
cmd.Flags().Int("teams", 0, "Override team count (0 = default)")
|
|
cmd.Flags().Int("activity-batches", 0, "Seed N batches of fake activities via direct MySQL (0 = skip)")
|
|
cmd.Flags().String("activity-dsn", "", "MySQL DSN for the activity seeder (default fleet:insecure@tcp(localhost:3306)/fleet)")
|
|
cmd.Flags().Uint("activity-host-id", 0, "host_id used by the activity seeder for host-scoped rows (default 1)")
|
|
cmd.Flags().Int("idp-user-count", 0, "Seed N users with IDP accounts via direct MySQL (0 = skip)")
|
|
cmd.Flags().Int("idp-host-count", 5, "How many hosts to assign IDP accounts to (round-robin); only used when --idp-user-count > 0")
|
|
cmd.Flags().String("idp-dsn", "", "MySQL DSN for the IDP seeder (default fleet:insecure@tcp(localhost:3306)/fleet)")
|
|
cmd.Flags().Int("cas", 0, "Seed N batches of fake certificate authorities via direct MySQL (0 = skip)")
|
|
cmd.Flags().String("cas-dsn", "", "MySQL DSN for the CA seeder (default fleet:insecure@tcp(localhost:3306)/fleet)")
|
|
return cmd
|
|
}
|