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

145 lines
5.0 KiB
Go

package command
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/fleetdm/fleet/v4/tools/dibble/pkg/seed"
)
// newSoftwareCmd wires `dibble software` and its subcommands. The seeder
// uploads the curated installer fixtures and registers a few Fleet-maintained
// apps. Both paths target one team — the first existing team by default, or
// whichever team --team-id / --team-name selects.
func newSoftwareCmd() *cobra.Command {
root := &cobra.Command{
Use: "software",
Short: "Seed software (custom installers + Fleet-maintained apps)",
Long: `Seed real software into Fleet for testing. Pick a subcommand:
dibble software all — upload custom installers AND add maintained apps
dibble software custom — upload curated installers (2-3 per extension)
dibble software maintained — add Fleet-maintained apps from the server's catalog
Custom uploads cover .pkg / .deb / .msi / .exe / .rpm / .tar.gz / .ipa fixtures bundled
into the dibble binary. Maintained-app entries are read from
/api/latest/fleet/software/fleet_maintained_apps and POSTed in catalog order.
All work scopes to a single team. By default dibble picks the first team Fleet
returns; override with --team-id or --team-name. Use --team-id 0 for the
no-team / global scope.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() },
}
addCommonFlags := func(cmd *cobra.Command) {
cmd.Flags().Int("team-id", -1, "Team to upload software to. -1 (default) = first team Fleet returns. 0 = no team / global.")
cmd.Flags().String("team-name", "", "Team to upload software to, by name. Overrides --team-id if set.")
cmd.Flags().Int("maintained-count", 3, "How many Fleet-maintained apps to add (used by `all` and `maintained`)")
}
mkSub := func(use, short string, runFn func(client *Client, opt seed.SoftwareOptions) error) *cobra.Command {
sub := &cobra.Command{
Use: use,
Short: short,
RunE: func(cmd *cobra.Command, args []string) error {
if err := requireConfig(); err != nil {
return err
}
c, err := newClientFromViper()
if err != nil {
return err
}
teamID, err := resolveSoftwareTeamID(cmd, c)
if err != nil {
return err
}
maintained, _ := cmd.Flags().GetInt("maintained-count")
return runFn(c, seed.SoftwareOptions{
TeamID: teamID,
MaintainedAppCount: maintained,
})
},
}
addCommonFlags(sub)
return sub
}
root.AddCommand(mkSub("custom", "Upload curated installer fixtures (2-3 per extension)",
func(c *Client, opt seed.SoftwareOptions) error {
res := seed.SoftwareCustom(c, seederLogger{}, opt)
printf("%s", res.Summary())
return reportErrors(res.Errors)
}))
root.AddCommand(mkSub("maintained", "Add Fleet-maintained apps from the server's catalog",
func(c *Client, opt seed.SoftwareOptions) error {
if opt.MaintainedAppCount <= 0 {
opt.MaintainedAppCount = 3
}
res := seed.SoftwareMaintained(c, seederLogger{}, opt)
printf("%s", res.Summary())
return reportErrors(res.Errors)
}))
root.AddCommand(mkSub("all", "Upload custom installers AND add maintained apps",
func(c *Client, opt seed.SoftwareOptions) error {
if opt.MaintainedAppCount <= 0 {
opt.MaintainedAppCount = 3
}
cRes := seed.SoftwareCustom(c, seederLogger{}, opt)
printf("%s", cRes.Summary())
mRes := seed.SoftwareMaintained(c, seederLogger{}, opt)
printf("%s", mRes.Summary())
combined := append([]error{}, cRes.Errors...)
combined = append(combined, mRes.Errors...)
return reportErrors(combined)
}))
return root
}
// resolveSoftwareTeamID determines which team uploads land under. Precedence:
//
// 1. --team-name (looked up against Fleet)
// 2. --team-id (0 means global, -1 means "auto-pick")
// 3. Auto-pick: first team Fleet returns. If none exist, prompt
// interactively when a TTY is attached; otherwise default to 0 / global.
func resolveSoftwareTeamID(cmd *cobra.Command, c *Client) (uint, error) {
if name, _ := cmd.Flags().GetString("team-name"); name != "" {
teams, err := listExistingTeams(c)
if err != nil {
return 0, fmt.Errorf("list teams to resolve --team-name=%q: %w", name, err)
}
for _, t := range teams {
if strings.EqualFold(t.Name, name) {
return t.ID, nil
}
}
return 0, fmt.Errorf("no team named %q (try --team-id or run `dibble teams` first)", name)
}
explicit, _ := cmd.Flags().GetInt("team-id")
if explicit == 0 {
return 0, nil
}
if explicit > 0 {
return uint(explicit), nil
}
// Auto-pick: first team Fleet returns. The wizard's software step (or
// --team-id / --team-name) is the path for picking a specific team.
teams, err := listExistingTeams(c)
if err != nil {
return 0, fmt.Errorf("list teams for auto-pick: %w", err)
}
if len(teams) == 0 {
printf("no teams found — falling back to no-team / global scope")
return 0, nil
}
printf("software: auto-picked team %q (id=%d) — override with --team-id or --team-name", teams[0].Name, teams[0].ID)
return teams[0].ID, nil
}