Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions tables/puppet/puppet_facts.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func PuppetFactsColumns() []table.ColumnDefinition {
func PuppetFactsGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string

facts, err := getPuppetFacts()
facts, err := getPuppetFactsFunc()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -66,6 +66,8 @@ func PuppetFactsGenerate(ctx context.Context, queryContext table.QueryContext) (
return results, nil
}

var getPuppetFactsFunc = getPuppetFacts

func getPuppetFacts() (*puppetFacts, error) {
// check if puppet command exists
execPath, err := getPuppetExecPath()
Expand All @@ -74,8 +76,7 @@ func getPuppetFacts() (*puppetFacts, error) {
}

// execute command
cmd := exec.Command(execPath, "facts", "--render-as", "json")
out, err := cmd.Output()
out, err := runPuppetFactsCmd(execPath)
if err != nil {
return nil, errors.Wrap(err, "calling puppet facts to get puppet facts")
}
Expand All @@ -88,6 +89,11 @@ func getPuppetFacts() (*puppetFacts, error) {
return &facts, nil
}

var runPuppetFactsCmd = func(execPath string) ([]byte, error) {
cmd := exec.Command(execPath, "facts", "--render-as", "json")
return cmd.Output()
}

func getPuppetExecPath() (string, error) {
// if puppet command not in the path, try to use the predefined path
if execPath, ok := puppetPath[runtime.GOOS]; ok {
Expand Down
4 changes: 2 additions & 2 deletions tables/puppet/puppet_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func PuppetInfoColumns() []table.ColumnDefinition {
// plugins is flat it will return a single row.
func PuppetInfoGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string
runData, err := getPuppetYaml()
runData, err := getPuppetYamlFunc()
if err != nil {
return results, err
}
Expand All @@ -77,7 +77,7 @@ func PuppetInfoGenerate(ctx context.Context, queryContext table.QueryContext) ([
"status": runData.Status,
"time": runData.Time,
"transaction_completed": runData.TransactionCompleted,
"transaction_uuid": runData.TransactionCompleted,
"transaction_uuid": runData.TransactionUUID,
})

return results, nil
Expand Down
2 changes: 1 addition & 1 deletion tables/puppet/puppet_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func PuppetLogsColumns() []table.ColumnDefinition {
// plugins is flat it will return a single row.
func PuppetLogsGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string
runData, err := getPuppetYaml()
runData, err := getPuppetYamlFunc()
if err != nil {
return results, err
}
Expand Down
2 changes: 1 addition & 1 deletion tables/puppet/puppet_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func PuppetStateColumns() []table.ColumnDefinition {

func PuppetStateGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string
runData, err := getPuppetYaml()
runData, err := getPuppetYamlFunc()
if err != nil {
return results, err
}
Expand Down
246 changes: 246 additions & 0 deletions tables/puppet/puppet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package puppet

import (
"context"
"errors"
"os"
"path/filepath"
"testing"

"github.com/osquery/osquery-go/plugin/table"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func samplePuppetInfo() *PuppetInfo {
return &PuppetInfo{
CachedCatalogStatus: "not_used",
CatalogUUID: "catalog-uuid",
CodeID: "code-id",
ConfigurationVersion: "123",
CorrectiveChange: "false",
Environment: "production",
Host: "example.local",
Kind: "apply",
MasterUsed: "puppet.example.com",
Noop: "false",
NoopPending: "false",
PuppetVersion: "8.0.0",
ReportFormat: "12",
Status: "changed",
Time: "2026-01-01T00:00:00Z",
TransactionCompleted: "true",
TransactionUUID: "transaction-uuid",
Logs: []Log{{
Level: "notice",
Message: "configured",
Source: "Puppet",
Time: "2026-01-01T00:00:00Z",
File: "/tmp/manifest.pp",
Line: "1",
}},
ResourceStatuses: map[string]ResourceStatus{
"File[/tmp/example]": {
Title: "/tmp/example",
File: "/tmp/manifest.pp",
Line: "1",
Resource: "File[/tmp/example]",
ResourceType: "File",
EvaulationTime: "0.1",
Failed: "false",
Changed: "true",
OutOfSync: "true",
Skipped: "false",
ChangeCount: "1",
OutOfSyncCount: "1",
CorrectiveChange: "false",
},
},
}
}

func withPuppetYAML(t *testing.T, info *PuppetInfo, err error) {
t.Helper()
original := getPuppetYamlFunc
getPuppetYamlFunc = func() (*PuppetInfo, error) {
return info, err
}
t.Cleanup(func() {
getPuppetYamlFunc = original
})
}

func withPuppetFacts(t *testing.T, facts *puppetFacts, err error) {
t.Helper()
original := getPuppetFactsFunc
getPuppetFactsFunc = func() (*puppetFacts, error) {
return facts, err
}
t.Cleanup(func() {
getPuppetFactsFunc = original
})
}

func withPuppetYAMLPath(t *testing.T, path string) {
t.Helper()
original := puppetYAMLPath
puppetYAMLPath = func() string {
return path
}
t.Cleanup(func() {
puppetYAMLPath = original
})
}

func withRunPuppetFactsCmd(t *testing.T, fn func(string) ([]byte, error)) {
t.Helper()
original := runPuppetFactsCmd
runPuppetFactsCmd = fn
t.Cleanup(func() {
runPuppetFactsCmd = original
})
}

func TestPuppetColumns(t *testing.T) {
assert.Contains(t, PuppetInfoColumns(), table.TextColumn("transaction_uuid"))
assert.Equal(t, []table.ColumnDefinition{
table.TextColumn("level"),
table.TextColumn("message"),
table.TextColumn("source"),
table.TextColumn("time"),
table.TextColumn("file"),
table.TextColumn("line"),
}, PuppetLogsColumns())
assert.Contains(t, PuppetStateColumns(), table.TextColumn("resource_type"))
assert.Equal(t, []table.ColumnDefinition{
table.TextColumn("node"),
table.TextColumn("fact"),
table.TextColumn("value"),
}, PuppetFactsColumns())
}

func TestPuppetInfoGenerate(t *testing.T) {
withPuppetYAML(t, samplePuppetInfo(), nil)

rows, err := PuppetInfoGenerate(context.Background(), table.QueryContext{})
require.NoError(t, err)
assert.Equal(t, "transaction-uuid", rows[0]["transaction_uuid"])
assert.Equal(t, "changed", rows[0]["status"])
}

func TestPuppetLogsGenerate(t *testing.T) {
withPuppetYAML(t, samplePuppetInfo(), nil)

rows, err := PuppetLogsGenerate(context.Background(), table.QueryContext{})
require.NoError(t, err)
assert.Equal(t, []map[string]string{{
"level": "notice",
"message": "configured",
"source": "Puppet",
"time": "2026-01-01T00:00:00Z",
"file": "/tmp/manifest.pp",
"line": "1",
}}, rows)
}

func TestPuppetStateGenerate(t *testing.T) {
withPuppetYAML(t, samplePuppetInfo(), nil)

rows, err := PuppetStateGenerate(context.Background(), table.QueryContext{})
require.NoError(t, err)
require.Len(t, rows, 1)
assert.Equal(t, "File[/tmp/example]", rows[0]["resource"])
assert.Equal(t, "File", rows[0]["resource_type"])
}

func TestPuppetGeneratorsReturnYAMLErrors(t *testing.T) {
withPuppetYAML(t, nil, errors.New("yaml failed"))

_, err := PuppetInfoGenerate(context.Background(), table.QueryContext{})
assert.Error(t, err)
_, err = PuppetLogsGenerate(context.Background(), table.QueryContext{})
assert.Error(t, err)
_, err = PuppetStateGenerate(context.Background(), table.QueryContext{})
assert.Error(t, err)
}

func TestPuppetFactsGenerate(t *testing.T) {
withPuppetFacts(t, &puppetFacts{
Name: "example.local",
Values: map[string]interface{}{
"os": map[string]interface{}{"family": "Darwin"},
"uptime": "1 day",
},
}, nil)

rows, err := PuppetFactsGenerate(context.Background(), table.QueryContext{})
require.NoError(t, err)
assert.ElementsMatch(t, []map[string]string{
{"node": "example.local", "fact": "os", "value": `{"family":"Darwin"}`},
{"node": "example.local", "fact": "uptime", "value": "1 day"},
}, rows)
}

func TestPuppetFactsGenerateError(t *testing.T) {
withPuppetFacts(t, nil, errors.New("facts failed"))

rows, err := PuppetFactsGenerate(context.Background(), table.QueryContext{})
assert.Error(t, err)
assert.Nil(t, rows)
}

func TestGetPuppetYaml(t *testing.T) {
path := filepath.Join(t.TempDir(), "last_run_report.yaml")
require.NoError(t, os.WriteFile(path, []byte("host: example.local\rstatus: changed\rtransaction_uuid: transaction-uuid\r"), 0600))
withPuppetYAMLPath(t, path)

info, err := getPuppetYaml()
require.NoError(t, err)
assert.Equal(t, "example.local", info.Host)
assert.Equal(t, "changed", info.Status)
assert.Equal(t, "transaction-uuid", info.TransactionUUID)
}

func TestGetPuppetYamlErrors(t *testing.T) {
withPuppetYAMLPath(t, filepath.Join(t.TempDir(), "missing.yaml"))
info, err := getPuppetYaml()
assert.Error(t, err)
assert.NotNil(t, info)

path := filepath.Join(t.TempDir(), "last_run_report.yaml")
require.NoError(t, os.WriteFile(path, []byte(":\n"), 0600))
withPuppetYAMLPath(t, path)
info, err = getPuppetYaml()
assert.Error(t, err)
assert.NotNil(t, info)
}

func TestGetPuppetFacts(t *testing.T) {
t.Setenv("PUPPET_PATH", "/tmp/puppet")
withRunPuppetFactsCmd(t, func(execPath string) ([]byte, error) {
assert.Equal(t, "/tmp/puppet", execPath)
return []byte(`{"Name":"example.local","Values":{"uptime":"1 day"}}`), nil
})

facts, err := getPuppetFacts()
require.NoError(t, err)
assert.Equal(t, "example.local", facts.Name)
assert.Equal(t, "1 day", facts.Values["uptime"])
}

func TestGetPuppetFactsErrors(t *testing.T) {
t.Setenv("PUPPET_PATH", "/tmp/puppet")
withRunPuppetFactsCmd(t, func(execPath string) ([]byte, error) {
return nil, errors.New("puppet failed")
})
facts, err := getPuppetFacts()
assert.Error(t, err)
assert.Nil(t, facts)

withRunPuppetFactsCmd(t, func(execPath string) ([]byte, error) {
return []byte("not json"), nil
})
facts, err = getPuppetFacts()
assert.Error(t, err)
assert.Nil(t, facts)
}
6 changes: 5 additions & 1 deletion tables/puppet/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ func yamlPath() string {
return "/opt/puppetlabs/puppet/cache/state/last_run_report.yaml"
}

var getPuppetYamlFunc = getPuppetYaml

var puppetYAMLPath = yamlPath

func getPuppetYaml() (*PuppetInfo, error) {
var yamlData PuppetInfo

yamlFile, err := os.Open(yamlPath())
yamlFile, err := os.Open(puppetYAMLPath())
if err != nil {
log.Print(err)
return &yamlData, err
Expand Down
Loading