Files
puppet-catalog-tools/internal/impact/paths_test.go
T
unkin-agent 8093933500
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
add the catalog-impact binary
Work out which hosts a puppet-prod change can affect, so a catalog test can
fan out over exactly those hosts instead of the whole estate.

- add cmd/catalog-impact and internal/impact
- map hieradata paths onto fact selectors from the hiera hierarchy
- trace manifest classes back to role classes through include, contain,
  class {} and hiera_include, to a fixed point
- fetch the estate fact table from pdbmux in one query via FactsByName
- widen to all hosts, loudly, for any path that matches no rule
- ship the binary and its completions in the RPM
2026-09-12 16:15:00 +10:00

133 lines
5.0 KiB
Go

package impact
import (
"reflect"
"testing"
)
func TestHieradataSelector(t *testing.T) {
tests := []struct {
name string
path string
rule string
want Selector
}{
{"common yaml", "common.yaml", RuleCommon, Selector{All: true}},
{"common eyaml", "common.eyaml", RuleCommon, Selector{All: true}},
{"node", "nodes/web01.example.net.yaml", RuleNode, Selector{Certname: "web01.example.net"}},
{"virtual", "virtual/physical.yaml", RuleVirtual, Selector{Virtual: "physical"}},
{"role tier1", "roles/infra.yaml", RuleRole, Selector{RoleTiers: []string{"infra"}}},
{"role tier2", "roles/infra/puppet.yaml", RuleRole, Selector{RoleTiers: []string{"infra", "puppet"}}},
{"role tier3", "roles/infra/puppet/master.yaml", RuleRole,
Selector{RoleTiers: []string{"infra", "puppet", "master"}}},
{"role eyaml", "roles/apps/media.eyaml", RuleRole, Selector{RoleTiers: []string{"apps", "media"}}},
{"country", "country/au.yaml", RuleCountry, Selector{Country: "au"}},
{"country eyaml", "country/au.eyaml", RuleCountry, Selector{Country: "au"}},
{"country region", "country/au/region/syd1.yaml", RuleCountryRegion,
Selector{Country: "au", Region: "syd1"}},
{"country region tier1", "country/au/region/syd1/infra.yaml", RuleCountryRegRole,
Selector{Country: "au", Region: "syd1", RoleTiers: []string{"infra"}}},
{"country region tier3", "country/au/region/syd1/infra/puppet/master.eyaml", RuleCountryRegRole,
Selector{Country: "au", Region: "syd1", RoleTiers: []string{"infra", "puppet", "master"}}},
{"country roles tier1", "country/au/roles/infra.yaml", RuleCountryRole,
Selector{Country: "au", RoleTiers: []string{"infra"}}},
{"country roles tier3", "country/au/roles/infra/puppet/master.yaml", RuleCountryRole,
Selector{Country: "au", RoleTiers: []string{"infra", "puppet", "master"}}},
{"os all releases", "os/AlmaLinux/all_releases.yaml", RuleOS, Selector{OSName: "AlmaLinux"}},
{"os major", "os/AlmaLinux/AlmaLinux9.yaml", RuleOS, Selector{OSName: "AlmaLinux", OSMajor: "9"}},
{"os major minor", "os/AlmaLinux/AlmaLinux9.7.yaml", RuleOS,
Selector{OSName: "AlmaLinux", OSMajor: "9", OSMinor: "7"}},
{"os debian major", "os/Debian/Debian11.yaml", RuleOS, Selector{OSName: "Debian", OSMajor: "11"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, rule, ok := hieradataSelector(tc.path)
if !ok {
t.Fatalf("hieradataSelector(%q) not recognised", tc.path)
}
if rule != tc.rule {
t.Errorf("rule = %q, want %q", rule, tc.rule)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("selector = %#v, want %#v", got, tc.want)
}
})
}
}
func TestHieradataSelectorRejects(t *testing.T) {
for _, p := range []string{
"README.md",
"roles/a/b/c/d.yaml",
"country/au/region/syd1/a/b/c/d.yaml",
"os/AlmaLinux/AlmaLinux9.json",
"whatever/thing.yaml",
} {
if _, _, ok := hieradataSelector(p); ok {
t.Errorf("hieradataSelector(%q) unexpectedly recognised", p)
}
}
}
func TestManifestClasses(t *testing.T) {
tests := []struct {
module string
rest []string
want []string
}{
{"profiles", []string{"base.pp"}, []string{"profiles::base"}},
{"profiles", []string{"base", "motd.pp"}, []string{"profiles::base::motd"}},
{"roles", []string{"infra", "k8s", "node.pp"}, []string{"roles::infra::k8s::node"}},
{"jellyfin", []string{"init.pp"}, []string{"jellyfin", "jellyfin::init"}},
{"profiles", []string{"cloudinit", "init.pp"}, []string{"profiles::cloudinit", "profiles::cloudinit::init"}},
}
for _, tc := range tests {
if got := ManifestClasses(tc.module, tc.rest); !reflect.DeepEqual(got, tc.want) {
t.Errorf("ManifestClasses(%q, %v) = %v, want %v", tc.module, tc.rest, got, tc.want)
}
}
}
func TestClassifyPathRepoGlobal(t *testing.T) {
for _, p := range []string{"hiera.yaml", "environment.conf", "Puppetfile", "manifests/site.pp"} {
m := classifyPath(p, nil)
if len(m.targets) != 1 || !m.targets[0].sel.All {
t.Fatalf("classifyPath(%q) = %#v, want an all-hosts target", p, m)
}
if m.targets[0].rule != RuleRepoGlobal {
t.Errorf("classifyPath(%q) rule = %q", p, m.targets[0].rule)
}
}
}
func TestClassifyPathIgnoredAndUnknown(t *testing.T) {
for _, p := range []string{".woodpecker/test.yaml", "Gemfile", "doc/notes.md", "vendor/bundle/x.rb", "README.md"} {
if m := classifyPath(p, nil); !m.ignored {
t.Errorf("classifyPath(%q) should be ignored, got %#v", p, m)
}
}
for _, p := range []string{"random/thing.txt", "hieradata/nope/deep/deeper/x.yaml"} {
if m := classifyPath(p, nil); !m.unknown {
t.Errorf("classifyPath(%q) should be unknown, got %#v", p, m)
}
}
}
func TestClassifyPathPluginsync(t *testing.T) {
m := classifyPath("modules/libs/lib/facter/netbox.rb", nil)
if len(m.targets) != 1 || !m.targets[0].sel.All || m.targets[0].rule != RulePluginsync {
t.Fatalf("pluginsync path = %#v, want an all-hosts pluginsync target", m)
}
}
func TestClassifyPathManifest(t *testing.T) {
m := classifyPath("site/profiles/manifests/base/motd.pp", nil)
if len(m.classes) != 1 || m.classes[0].name != "profiles::base::motd" {
t.Fatalf("classes = %#v", m.classes)
}
}