diff --git a/cmd/collect.go b/cmd/collect.go index 7c12682..4a5f3e4 100644 --- a/cmd/collect.go +++ b/cmd/collect.go @@ -24,7 +24,7 @@ output them in the format specified`, Run: func(cmd *cobra.Command, args []string) { shipshape.FactsOnly = true runCmd.Run(cmd, args) - for _, f := range fact.Facts { + for _, f := range fact.Manager().GetPlugins() { if shouldSkipFact(f) { continue } @@ -32,10 +32,10 @@ output them in the format specified`, log.WithFields(log.Fields{ "fact": f.GetName(), "format": f.GetFormat(), - "data": f.GetData(), }).Debug("printing collected fact") fmt.Printf("%s:", f.GetName()) switch f.GetFormat() { + case data.FormatMapListString: loadedData := data.AsMapListString(f.GetData()) for k, vList := range loadedData { @@ -44,6 +44,7 @@ output them in the format specified`, fmt.Printf(" - %s\n", v) } } + case data.FormatMapString: loadedData := data.AsMapString(f.GetData()) fmt.Println() @@ -55,6 +56,7 @@ output them in the format specified`, fmt.Printf(" %s: %s\n", k, v) } } + case data.FormatMapNestedString: loadedData := data.AsMapNestedString(f.GetData()) fmt.Println() @@ -64,10 +66,17 @@ output them in the format specified`, fmt.Printf(" %s: %s\n", k2, v) } } + case data.FormatString: fmt.Printf(" %s\n", f.GetData()) + + case data.FormatRaw: + fmt.Println("\n", + output.TabbedMultiline(" ", fmt.Sprintf("%s", f.GetData()))) + default: - fmt.Println(" collect not yet implemented for", f.GetFormat()) + log.WithField("data", fmt.Sprintf("%s", f.GetData())).Warn("collect not yet implemented for this format") + fmt.Println(" collect not yet implemented for", f.GetFormat()) } fmt.Println() } diff --git a/cmd/config.go b/cmd/config.go index 2d7c8f2..873b07b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -45,17 +45,17 @@ var configListPluginsCmd = &cobra.Command{ Long: `List all available plugins that can be used in shipshape`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Connection plugins:") - for _, p := range connection.RegistryKeys() { + for _, p := range connection.Manager().GetFactoriesKeys() { fmt.Println(" - " + p) } fmt.Println("\nFact plugins:") - for _, p := range fact.RegistryKeys() { + for _, p := range fact.Manager().GetFactoriesKeys() { fmt.Println(" - " + p) } fmt.Println("\nAnalyse plugins:") - for _, p := range analyse.RegistryKeys() { + for _, p := range analyse.Manager().GetFactoriesKeys() { fmt.Println(" - " + p) } diff --git a/cmd/gen.go b/cmd/gen.go index 9860b1a..21ba53c 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -40,20 +40,10 @@ func main() { } gen.BreachType(breachTypes) break - case "connection-plugin": - if len(plugins) == 0 { - log.Fatal("connection-plugin missing flags; plugin is required") - } - gen.ConnectionPlugin(plugins) - break case "fact-plugin": - if len(plugins) == 0 { - log.Fatal("fact-plugin missing flags; plugin is required") - } if pkg == "" { log.Fatal("fact-plugin missing flags; package is required") } - gen.FactPlugin(plugins, pkg, enableEnvResolver) gen.FactRegistry(pkg) break case "analyse-plugin": diff --git a/cmd/gen/analyseplugin.go b/cmd/gen/analyseplugin.go index 9ed5573..2d8097a 100644 --- a/cmd/gen/analyseplugin.go +++ b/cmd/gen/analyseplugin.go @@ -11,7 +11,6 @@ func AnalysePlugin(plugins []string) { log.Println("Generating analyse plugin funcs -", strings.Join(plugins, ",")) tmplPath := filepath.Join("..", "..", "pkg", "analyse", "templates", "analyseplugin.go.tmpl") - tmplTestPath := filepath.Join("..", "..", "pkg", "analyse", "templates", "analyseplugin_test.go.tmpl") for _, p := range plugins { pluginFile := strings.ToLower(p) + "_gen.go" @@ -21,14 +20,5 @@ func AnalysePlugin(plugins []string) { } templateToFile(tmplPath, struct{ Plugin string }{p}, pluginFullFilePath) - - // Test file. - pluginTestFile := strings.ToLower(p) + "_gen_test.go" - pluginFullTestFilePath := filepath.Join(getScriptPath(), "..", "..", "pkg", "analyse", pluginTestFile) - if err := os.Remove(pluginTestFile); err != nil && !os.IsNotExist(err) { - log.Fatalln(err) - } - - templateToFile(tmplTestPath, struct{ Plugin string }{p}, pluginFullTestFilePath) } } diff --git a/cmd/gen/connectionplugin.go b/cmd/gen/connectionplugin.go deleted file mode 100644 index 007a1bd..0000000 --- a/cmd/gen/connectionplugin.go +++ /dev/null @@ -1,46 +0,0 @@ -package gen - -import ( - "bytes" - "fmt" - "log" - "os" - "path/filepath" - "strings" - "text/template" -) - -func ConnectionPlugin(plugins []string) { - log.Println("Generating connection plugin funcs -", strings.Join(plugins, ",")) - - for _, p := range plugins { - pluginFile := strings.ToLower(p) + "_gen.go" - pluginFullFilePath := filepath.Join(getScriptPath(), "..", "..", "pkg", "connection", pluginFile) - if err := os.Remove(pluginFullFilePath); err != nil && !os.IsNotExist(err) { - log.Fatalln(err) - } - createFileWithString(pluginFullFilePath, - fmt.Sprintf("package connection\n\n"+ - "// Code generated by connection-plugin --plugin=%s; DO NOT EDIT.\n", p)) - appendFileContent(pluginFullFilePath, connectionPluginFuncs(p)) - } -} - -func connectionPluginFuncs(p string) string { - tmplStr := ` -func (p *{{.Plugin}}) GetName() string { - return p.Name -} -` - tmpl, err := template.New("connectionPluginFuncs").Parse(tmplStr) - if err != nil { - log.Fatalln(err) - } - - buf := &bytes.Buffer{} - err = tmpl.Execute(buf, struct{ Plugin string }{p}) - if err != nil { - log.Fatalln(err) - } - return buf.String() -} diff --git a/cmd/gen/factplugin.go b/cmd/gen/factplugin.go index aa8f62f..076573b 100644 --- a/cmd/gen/factplugin.go +++ b/cmd/gen/factplugin.go @@ -3,35 +3,8 @@ package gen import ( "fmt" "log" - "os" - "path/filepath" - "strings" ) -func FactPlugin(plugins []string, pkg string, envResolver bool) { - log.Printf("Generating fact plugin funcs for package %s: %s\n", pkg, strings.Join(plugins, ",")) - - tmplPath := filepath.Join("..", "..", "pkg", "fact", "templates", "factplugin.go.tmpl") - - for _, p := range plugins { - pluginFile := strings.ToLower(p) + "_gen.go" - pluginDir := filepath.Join(getScriptPath(), "..", "..", "pkg", "fact") - if pkg != "fact" { - pluginDir = filepath.Join(pluginDir, pkg) - } - pluginFullFilePath := filepath.Join(pluginDir, pluginFile) - if err := os.Remove(pluginFullFilePath); err != nil && !os.IsNotExist(err) { - log.Fatalln(err) - } - - templateToFile(tmplPath, struct { - Package string - Plugin string - EnvResolver bool - }{Package: pkg, Plugin: p, EnvResolver: envResolver}, pluginFullFilePath) - } -} - // FactRegistry adds the Facters for a package to the registry. func FactRegistry(pkg string) { log.Println("Updating Fact plugins registry - adding", pkg) diff --git a/examples/regex-not-match.yml b/examples/regex-not-match.yml index 272eb82..5246665 100644 --- a/examples/regex-not-match.yml +++ b/examples/regex-not-match.yml @@ -7,10 +7,11 @@ collect: yaml:key: input: extension-file path: module.lagoon_logs + ignore-not-found: true analyse: lagoon-logs-check: regex:not-match: description: Lagoon logs module is not enabled input: module-lagoon_logs - pattern: "^1$" + pattern: "^0$" diff --git a/pkg/analyse/allowedlist.go b/pkg/analyse/allowedlist.go index 26fc231..f79bfc0 100644 --- a/pkg/analyse/allowedlist.go +++ b/pkg/analyse/allowedlist.go @@ -7,23 +7,11 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/breach" "github.com/salsadigitalauorg/shipshape/pkg/data" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" "github.com/salsadigitalauorg/shipshape/pkg/utils" ) type AllowedList struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - Severity string `yaml:"severity"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - Remediation interface{} `yaml:"remediation"` - input fact.Facter - - // Plugin fields. + BaseAnalyser `yaml:",inline"` PackageMatch string `yaml:"package-match"` pkgRegex *regexp.Regexp Allowed []string `yaml:"allowed"` @@ -36,10 +24,12 @@ type AllowedList struct { //go:generate go run ../../cmd/gen.go analyse-plugin --plugin=AllowedList --package=analyse func init() { - Registry["allowed:list"] = func(id string) Analyser { return NewAllowedList(id) } + Manager().RegisterFactory("allowed:list", func(id string) Analyser { + return NewAllowedList(id) + }) } -func (p *AllowedList) PluginName() string { +func (p *AllowedList) GetName() string { return "allowed:list" } diff --git a/pkg/analyse/allowedlist_test.go b/pkg/analyse/allowedlist_test.go index 77a17e7..588e8c5 100644 --- a/pkg/analyse/allowedlist_test.go +++ b/pkg/analyse/allowedlist_test.go @@ -12,13 +12,14 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" "github.com/salsadigitalauorg/shipshape/pkg/fact/testdata" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) func TestAllowedListInit(t *testing.T) { assert := assert.New(t) // Test that the plugin is registered. - plugin := Registry["allowed:list"]("testAllowedList") + plugin := Manager().GetFactories()["allowed:list"]("testAllowedList") assert.NotNil(plugin) analyser, ok := plugin.(*AllowedList) assert.True(ok) @@ -26,8 +27,8 @@ func TestAllowedListInit(t *testing.T) { } func TestAllowedListPluginName(t *testing.T) { - instance := AllowedList{Id: "testAllowedList"} - assert.Equal(t, "allowed:list", instance.PluginName()) + instance := NewAllowedList("testAllowedList") + assert.Equal(t, "allowed:list", instance.GetName()) } func TestAllowedListAnalyse(t *testing.T) { @@ -44,32 +45,32 @@ func TestAllowedListAnalyse(t *testing.T) { // List of strings. { name: "listString/NoBreaches", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatListString, - TestInputData: []interface{}{"value1", "value2"}, - }, + input: testdata.New( + "testFacter", + data.FormatListString, + []interface{}{"value1", "value2"}, + ), allowed: []string{"value1", "value2"}, expectedBreaches: []breach.Breach{}, }, { name: "listString/Ignored", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatListString, - TestInputData: []interface{}{"value1", "value2", "value3"}, - }, + input: testdata.New( + "testFacter", + data.FormatListString, + []interface{}{"value1", "value2", "value3"}, + ), allowed: []string{"value1", "value2"}, ignore: []string{"value3"}, expectedBreaches: []breach.Breach{}, }, { name: "listString/NotAllowed", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatListString, - TestInputData: []interface{}{"value1", "value2", "value3"}, - }, + input: testdata.New( + "testFacter", + data.FormatListString, + []interface{}{"value1", "value2", "value3"}, + ), allowed: []string{"value1", "value2"}, expectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -82,11 +83,11 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "listString/Deprecated", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatListString, - TestInputData: []interface{}{"value1", "value2", "value3"}, - }, + input: testdata.New( + "testFacter", + data.FormatListString, + []interface{}{"value1", "value2", "value3"}, + ), allowed: []string{"value1", "value2"}, deprecated: []string{"value3"}, expectedBreaches: []breach.Breach{ @@ -100,11 +101,11 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "listString/Required", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatListString, - TestInputData: []interface{}{"value1", "value2"}, - }, + input: testdata.New( + "testFacter", + data.FormatListString, + []interface{}{"value1", "value2"}, + ), allowed: []string{"value1", "value2"}, required: []string{"value3"}, expectedBreaches: []breach.Breach{ @@ -120,29 +121,29 @@ func TestAllowedListAnalyse(t *testing.T) { // String map. { name: "mapStringNoBreaches", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{ + input: testdata.New( + "testFacter", + data.FormatMapString, + map[string]interface{}{ "key1": "value1", "key2": "value2", }, - }, + ), allowed: []string{"value1", "value2"}, expectedBreaches: []breach.Breach{}, }, { name: "mapStringExcludedIgnored", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{ + input: testdata.New( + "testFacter", + data.FormatMapString, + map[string]interface{}{ "key1": "value1", "key2": "value2", "key3": "value3", "key4": "value4", }, - }, + ), allowed: []string{"value1", "value2"}, excludeKeys: []string{"key3"}, ignore: []string{"value4"}, @@ -150,15 +151,15 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "mapStringNotAllowed", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{ + input: testdata.New( + "testFacter", + data.FormatMapString, + map[string]interface{}{ "key1": "value1", "key2": "value2", "key3": "value3", }, - }, + ), allowed: []string{"value1", "value2"}, expectedBreaches: []breach.Breach{ &breach.KeyValueBreach{ @@ -173,15 +174,15 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "mapStringDeprecated", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{ + input: testdata.New( + "testFacter", + data.FormatMapString, + map[string]interface{}{ "key1": "value1", "key2": "value2", "key3": "value3", }, - }, + ), allowed: []string{"value1", "value2"}, deprecated: []string{"value3"}, expectedBreaches: []breach.Breach{ @@ -197,15 +198,15 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "mapString/Required", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{ + input: testdata.New( + "testFacter", + data.FormatMapString, + map[string]interface{}{ "key1": "value1", "key2": "value2", "key3": "value3", }, - }, + ), required: []string{"value4", "value5"}, expectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -226,29 +227,29 @@ func TestAllowedListAnalyse(t *testing.T) { // Test data with map of list of strings. { name: "mapListStringNoBreaches", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapListString, - TestInputData: map[string][]string{ + input: testdata.New( + "testFacter", + data.FormatMapListString, + map[string][]string{ "key1": {"value1"}, "key2": {"value2"}, }, - }, + ), allowed: []string{"value1", "value2"}, expectedBreaches: []breach.Breach{}, }, { name: "mapListStringExcludedIgnored", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapListString, - TestInputData: map[string][]string{ + input: testdata.New( + "testFacter", + data.FormatMapListString, + map[string][]string{ "key1": {"value1"}, "key2": {"value2"}, "key3": {"value3"}, "key4": {"value4"}, }, - }, + ), allowed: []string{"value1", "value2"}, excludeKeys: []string{"key3"}, ignore: []string{"value4"}, @@ -256,15 +257,15 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "mapListStringSomeIgnored", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapListString, - TestInputData: map[string][]string{ + input: testdata.New( + "testFacter", + data.FormatMapListString, + map[string][]string{ "key1": {"value1"}, "key2": {"value2"}, "key3": {"value3", "value4"}, }, - }, + ), allowed: []string{"value1", "value2"}, ignore: []string{"value4"}, expectedBreaches: []breach.Breach{ @@ -280,14 +281,14 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "mapListStringDeprecated", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapListString, - TestInputData: map[string][]string{ + input: testdata.New( + "testFacter", + data.FormatMapListString, + map[string][]string{ "key1": {"value1"}, "key2": {"value2", "value4"}, }, - }, + ), allowed: []string{"value1", "value2"}, deprecated: []string{"value4"}, expectedBreaches: []breach.Breach{ @@ -303,14 +304,14 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "mapListStringDisallowed", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapListString, - TestInputData: map[string][]string{ + input: testdata.New( + "testFacter", + data.FormatMapListString, + map[string][]string{ "key1": {"value1", "value3"}, "key2": {"value2", "value4"}, }, - }, + ), allowed: []string{"value1", "value2"}, expectedBreaches: []breach.Breach{ &breach.KeyValueBreach{ @@ -333,14 +334,14 @@ func TestAllowedListAnalyse(t *testing.T) { }, { name: "mapListString/Required", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapListString, - TestInputData: map[string][]string{ + input: testdata.New( + "testFacter", + data.FormatMapListString, + map[string][]string{ "key1": {"value1", "value3", "value5"}, "key2": {"value2", "value4"}, }, - }, + ), required: []string{"value5", "value6"}, expectedBreaches: []breach.Breach{ &breach.KeyValueBreach{ @@ -369,15 +370,18 @@ func TestAllowedListAnalyse(t *testing.T) { } for _, tc := range tt { - assert := assert.New(t) - currLogOut := logrus.StandardLogger().Out defer logrus.SetOutput(currLogOut) logrus.SetOutput(io.Discard) t.Run(tc.name, func(t *testing.T) { + assert := assert.New(t) analyser := AllowedList{ - Id: "testAllowedList", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "testAllowedList", + }, + }, Allowed: tc.allowed, Required: tc.required, Deprecated: tc.deprecated, diff --git a/pkg/analyse/analyse.go b/pkg/analyse/analyse.go deleted file mode 100644 index 1448338..0000000 --- a/pkg/analyse/analyse.go +++ /dev/null @@ -1,75 +0,0 @@ -package analyse - -import ( - "sort" - - log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" - - "github.com/salsadigitalauorg/shipshape/pkg/result" -) - -var Registry = map[string]func(string) Analyser{} -var Analysers = map[string]Analyser{} -var Errors = []error{} - -func RegistryKeys() []string { - keys := []string{} - for k := range Registry { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// ParseConfig parses the raw config and creates the analysers. -func ParseConfig(raw map[string]map[string]interface{}) { - count := 0 - log.WithField("registry", RegistryKeys()).Debug("analysers") - for name, pluginConf := range raw { - for pluginName, pluginMap := range pluginConf { - f, ok := Registry[pluginName] - if !ok { - continue - } - - // Convert the map to yaml, then parse it into the plugin. - // Not catching any errors here since the yaml content is known. - pluginYaml, _ := yaml.Marshal(pluginMap) - p := f(name) - yaml.Unmarshal(pluginYaml, p) - - log.WithFields(log.Fields{ - "check": name, - "plugin": pluginName, - "description": p.GetDescription(), - "input": p.GetInputName(), - }).Debug("parsed analyser") - Analysers[name] = p - count++ - } - } - log.Infof("parsed %d analysers", count) -} - -func ValidateInputs() { - for _, p := range Analysers { - if err := p.ValidateInput(); err != nil { - Errors = append(Errors, err) - } - } -} - -func AnalyseAll() map[string]result.Result { - results := map[string]result.Result{} - for _, p := range Analysers { - if p.PreProcessInput() { - p.Analyse() - } - result := p.GetResult() - results[p.GetId()] = result - log.WithField("analyser", p.GetId()).WithFields(result.LogFields()). - Debug("analysed result") - } - return results -} diff --git a/pkg/analyse/base.go b/pkg/analyse/base.go new file mode 100644 index 0000000..a5a0a9e --- /dev/null +++ b/pkg/analyse/base.go @@ -0,0 +1,94 @@ +package analyse + +import ( + "github.com/salsadigitalauorg/shipshape/pkg/breach" + "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" + "github.com/salsadigitalauorg/shipshape/pkg/result" + log "github.com/sirupsen/logrus" +) + +// BaseAnalyser provides common fields and functionality for analyse plugins. +type BaseAnalyser struct { + plugin.BasePlugin `yaml:",inline"` + Description string `yaml:"description"` + InputName string `yaml:"input"` + Severity string `yaml:"severity"` + breach.BreachTemplate `yaml:"breach-format"` + Result result.Result + Remediation interface{} `yaml:"remediation"` + input fact.Facter +} + +func (p *BaseAnalyser) GetDescription() string { + return p.Description +} + +func (p *BaseAnalyser) GetInputName() string { + return p.InputName +} + +func (p *BaseAnalyser) GetBreachTemplate() breach.BreachTemplate { + return p.BreachTemplate +} + +func (p *BaseAnalyser) GetResult() result.Result { + if p.Description != "" && p.Result.Name != p.Description { + p.Result.Name = p.Description + } + return p.Result +} + +func (p *BaseAnalyser) SetInput(input fact.Facter) { + p.input = input +} + +func (p *BaseAnalyser) GetInput() fact.Facter { + return p.input +} + +func (p *BaseAnalyser) AddBreach(b breach.Breach) { + b.SetCommonValues("", p.GetId(), p.Severity) + p.Result.Breaches = append(p.Result.Breaches, b) +} + +// Default implementations +func (p *BaseAnalyser) ValidateInput() error { + log.WithFields(log.Fields{ + "analyser": p.Id, + }).Debug("validating input") + + inPlugin := fact.Manager().FindPlugin(p.InputName) + if inPlugin == nil { + return &plugin.ErrSupportNotFound{ + Plugin: p.GetId(), SupportType: "input", SupportPlugin: p.InputName} + } + + p.input = inPlugin + return nil +} + +func (p *BaseAnalyser) PreProcessInput() bool { + if p.input == nil { + p.AddBreach(&breach.ValueBreach{ + Value: "no input available to analyse", + }) + return false + } + + if len(p.input.GetErrors()) > 0 { + errs := []string{} + for _, e := range p.input.GetErrors() { + errs = append(errs, e.Error()) + } + p.AddBreach(&breach.KeyValuesBreach{ + Key: "input failure", + Values: errs, + }) + return false + } + + return true +} + +func (p *BaseAnalyser) Analyse() {} diff --git a/pkg/analyse/equals.go b/pkg/analyse/equals.go index c51f5d7..9ab9ccb 100644 --- a/pkg/analyse/equals.go +++ b/pkg/analyse/equals.go @@ -7,44 +7,32 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/breach" "github.com/salsadigitalauorg/shipshape/pkg/data" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" ) // Equals is an analyser that checks if a fact is equal to a value. // If a map is provided as input, the key is used to look up the value. type Equals struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - Severity string `yaml:"severity"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - Remediation interface{} `yaml:"remediation"` - input fact.Facter - - // Plugin fields. - Value string `yaml:"value"` - Key string `yaml:"key"` + BaseAnalyser `yaml:",inline"` + Value string `yaml:"value"` + Key string `yaml:"key"` } //go:generate go run ../../cmd/gen.go analyse-plugin --plugin=Equals --package=analyse func init() { - Registry["equals"] = func(id string) Analyser { return NewEquals(id) } + Manager().RegisterFactory("equals", func(id string) Analyser { return NewEquals(id) }) } -func (p *Equals) PluginName() string { +func (p *Equals) GetName() string { return "equals" } func (p *Equals) Analyse() { log.WithFields(log.Fields{ - "plugin": p.PluginName(), - "id": p.Id, - "input": p.InputName, - "input-format": p.input.GetFormat(), + "plugin": p.GetName(), + "id": p.GetId(), + "input": p.GetInputName(), + "input-format": p.GetInput().GetFormat(), }).Debug("analysing") switch p.input.GetFormat() { diff --git a/pkg/analyse/equals_test.go b/pkg/analyse/equals_test.go index 34072a1..5f708b3 100644 --- a/pkg/analyse/equals_test.go +++ b/pkg/analyse/equals_test.go @@ -10,13 +10,14 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact/testdata" "github.com/salsadigitalauorg/shipshape/pkg/internal" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) func TestEqualsInit(t *testing.T) { assert := assert.New(t) // Test that the plugin is registered. - plugin := Registry["equals"]("TestEquals") + plugin := Manager().GetFactories()["equals"]("TestEquals") assert.NotNil(plugin) analyser, ok := plugin.(*Equals) assert.True(ok) @@ -24,8 +25,8 @@ func TestEqualsInit(t *testing.T) { } func TestEqualsPluginName(t *testing.T) { - instance := Equals{Id: "TestEquals"} - assert.Equal(t, "equals", instance.PluginName()) + instance := NewEquals("TestEquals") + assert.Equal(t, "equals", instance.GetName()) } func TestEqualsAnalyse(t *testing.T) { @@ -33,15 +34,19 @@ func TestEqualsAnalyse(t *testing.T) { // String. { Name: "string", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatString, - TestInputData: "foo", - }, + Input: testdata.New( + "testFact", + data.FormatString, + "foo", + ), Analyser: &Equals{ - InputName: "testFact", - Id: "TestEquals", - Value: "foo", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestEquals", + }, + InputName: "testFact", + }, + Value: "foo", }, ExpectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -53,15 +58,19 @@ func TestEqualsAnalyse(t *testing.T) { }, { Name: "stringNotEqual", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatString, - TestInputData: "bar", - }, + Input: testdata.New( + "testFact", + data.FormatString, + "bar", + ), Analyser: &Equals{ - InputName: "testFact", - Id: "TestEquals", - Value: "foo", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestEquals", + }, + InputName: "testFact", + }, + Value: "foo", }, ExpectedBreaches: []breach.Breach{}, }, @@ -69,18 +78,22 @@ func TestEqualsAnalyse(t *testing.T) { // Map of string. { Name: "mapString", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{ + Input: testdata.New( + "testFact", + data.FormatMapString, + map[string]interface{}{ "foo": "bar", }, - }, + ), Analyser: &Equals{ - InputName: "testFact", - Id: "TestEquals", - Key: "foo", - Value: "bar", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestEquals", + }, + InputName: "testFact", + }, + Key: "foo", + Value: "bar", }, ExpectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -92,16 +105,20 @@ func TestEqualsAnalyse(t *testing.T) { }, { Name: "mapStringNotEqual", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{"foo": "zoom"}, - }, + Input: testdata.New( + "testFact", + data.FormatMapString, + map[string]interface{}{"foo": "zoom"}, + ), Analyser: &Equals{ - InputName: "testFact", - Id: "TestEquals", - Key: "foo", - Value: "bar", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestEquals", + }, + InputName: "testFact", + }, + Key: "foo", + Value: "bar", }, ExpectedBreaches: []breach.Breach{}, }, @@ -109,15 +126,19 @@ func TestEqualsAnalyse(t *testing.T) { // Unsupported. { Name: "unsupported", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatListString, - TestInputData: []interface{}{"foo", "bar"}, - }, + Input: testdata.New( + "testFact", + data.FormatListString, + []interface{}{"foo", "bar"}, + ), Analyser: &Equals{ - InputName: "testFact", - Id: "TestEquals", - Value: "foo", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestEquals", + }, + InputName: "testFact", + }, + Value: "foo", }, ExpectedBreaches: []breach.Breach{}, }, diff --git a/pkg/analyse/manager.go b/pkg/analyse/manager.go new file mode 100644 index 0000000..38222b2 --- /dev/null +++ b/pkg/analyse/manager.go @@ -0,0 +1,90 @@ +package analyse + +import ( + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + "github.com/salsadigitalauorg/shipshape/pkg/plugin" + "github.com/salsadigitalauorg/shipshape/pkg/pluginmanager" + "github.com/salsadigitalauorg/shipshape/pkg/result" +) + +// manager handles analyser plugin registration and lifecycle. +type manager struct { + *pluginmanager.Manager[Analyser] +} + +var m *manager + +// Manager returns the analyser manager. +func Manager() *manager { + if m == nil { + m = &manager{ + Manager: pluginmanager.NewManager[Analyser](), + } + } + return m +} + +func (m *manager) GetFactoriesKeys() []string { + return plugin.GetFactoriesKeys[Analyser](m.GetFactories()) +} + +// ParseConfig parses the raw config and creates the analysers. +func (m *manager) ParseConfig(raw map[string]map[string]interface{}) error { + log.WithField("registry", m.ListPlugins()).Debug("analysers") + count := 0 + for id, pluginConf := range raw { + for pluginName, pluginIf := range pluginConf { + plugin, err := m.GetPlugin(pluginName, id) + if err != nil { + return err + } + + // Convert the map to yaml, then parse it into the plugin. + // Not catching any errors when marshalling since the yaml content is known. + pluginYaml, _ := yaml.Marshal(pluginIf) + err = yaml.Unmarshal(pluginYaml, plugin) + if err != nil { + return err + } + + log.WithFields(log.Fields{ + "id": id, + "plugin": plugin.GetName(), + "description": plugin.GetDescription(), + "input": plugin.GetInputName(), + }).Debug("parsed analyser") + count++ + } + } + log.Infof("parsed %d analysers", count) + return nil +} + +func (m *manager) ValidateInputs() { + for _, plugin := range m.GetPlugins() { + if err := plugin.ValidateInput(); err != nil { + m.AddErrors(err) + } + } +} + +// AnalyseAll runs all registered analysers and returns their results. +func (m *manager) AnalyseAll() map[string]result.Result { + results := make(map[string]result.Result) + for _, plugin := range m.GetPlugins() { + if plugin.PreProcessInput() { + plugin.Analyse() + } + + result := plugin.GetResult() + results[plugin.GetId()] = result + + log.WithField("analyser", plugin.GetId()). + WithFields(result.LogFields()). + Debug("analysed result") + } + + return results +} diff --git a/pkg/analyse/analyse_test.go b/pkg/analyse/manager_test.go similarity index 72% rename from pkg/analyse/analyse_test.go rename to pkg/analyse/manager_test.go index 4c2f403..069ffd6 100644 --- a/pkg/analyse/analyse_test.go +++ b/pkg/analyse/manager_test.go @@ -10,6 +10,7 @@ import ( . "github.com/salsadigitalauorg/shipshape/pkg/analyse" "github.com/salsadigitalauorg/shipshape/pkg/analyse/testdata" "github.com/salsadigitalauorg/shipshape/pkg/breach" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" "github.com/salsadigitalauorg/shipshape/pkg/result" ) @@ -45,24 +46,28 @@ func TestParseConfig(t *testing.T) { } for _, tc := range tt { - assert := assert.New(t) - currLogOut := logrus.StandardLogger().Out defer logrus.SetOutput(currLogOut) logrus.SetOutput(io.Discard) t.Run(tc.name, func(t *testing.T) { - assert.Len(Analysers, 0) - registryBackup := Registry + assert := assert.New(t) + assert.Len(Manager().GetPlugins(), 0) + factoriesBackup := Manager().GetFactories() if tc.registry != nil { - Registry = tc.registry + Manager().Reset() + for k, v := range tc.registry { + Manager().RegisterFactory(k, v) + } } - ParseConfig(tc.config) + Manager().ParseConfig(tc.config) defer func() { - Registry = registryBackup - Analysers = map[string]Analyser{} + Manager().Reset() + for k, v := range factoriesBackup { + Manager().RegisterFactory(k, v) + } }() - assert.Len(Analysers, tc.expectAnalyserCount) + assert.Len(Manager().GetPlugins(), tc.expectAnalyserCount) }) } } @@ -95,21 +100,25 @@ func TestValidateInputs(t *testing.T) { } for _, tc := range tt { - assert := assert.New(t) - currLogOut := logrus.StandardLogger().Out defer logrus.SetOutput(currLogOut) logrus.SetOutput(io.Discard) t.Run(tc.name, func(t *testing.T) { - assert.Len(Errors, 0) - Analysers = tc.analysers - ValidateInputs() + assert := assert.New(t) + assert.Len(Manager().GetErrors(), 0) + + if len(tc.analysers) > 0 { + Manager().SetPlugins(tc.analysers) + } + + Manager().ValidateInputs() + defer func() { - Analysers = map[string]Analyser{} - Errors = []error{} + Manager().ResetPlugins() + Manager().ResetErrors() }() - assert.Len(Errors, tc.expectErrorCount) + assert.Len(Manager().GetErrors(), tc.expectErrorCount) }) } } @@ -128,7 +137,13 @@ func TestAnalyseAll(t *testing.T) { { name: "analyserWithPreProcessInputFail", analysers: map[string]Analyser{ - "test": &testdata.TestAnalyserPreprocessInputFail{Id: "test"}, + "test": &testdata.TestAnalyserPreprocessInputFail{ + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "test", + }, + }, + }, }, expectResults: map[string]result.Result{ "test": { @@ -144,7 +159,13 @@ func TestAnalyseAll(t *testing.T) { { name: "analyserPass", analysers: map[string]Analyser{ - "test": &testdata.TestAnalyserPass{Id: "test"}, + "test": &testdata.TestAnalyserPass{ + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "test", + }, + }, + }, }, expectResults: map[string]result.Result{ "test": { @@ -160,21 +181,21 @@ func TestAnalyseAll(t *testing.T) { } for _, tc := range tt { - assert := assert.New(t) - currLogOut := logrus.StandardLogger().Out defer logrus.SetOutput(currLogOut) logrus.SetOutput(io.Discard) t.Run(tc.name, func(t *testing.T) { + assert := assert.New(t) + defer func() { - Analysers = map[string]Analyser{} - Errors = []error{} + Manager().ResetPlugins() + Manager().ResetErrors() }() - assert.Len(Errors, 0) - Analysers = tc.analysers - results := AnalyseAll() + assert.Len(Manager().GetErrors(), 0) + Manager().SetPlugins(tc.analysers) + results := Manager().AnalyseAll() assert.Equal(tc.expectResults, results) }) } diff --git a/pkg/analyse/notempty.go b/pkg/analyse/notempty.go index 6fdbac0..5f8f7f7 100644 --- a/pkg/analyse/notempty.go +++ b/pkg/analyse/notempty.go @@ -3,30 +3,20 @@ package analyse import ( "github.com/salsadigitalauorg/shipshape/pkg/breach" "github.com/salsadigitalauorg/shipshape/pkg/data" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" log "github.com/sirupsen/logrus" ) type NotEmpty struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - Severity string `yaml:"severity"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - Remediation interface{} `yaml:"remediation"` - input fact.Facter + BaseAnalyser `yaml:",inline"` } //go:generate go run ../../cmd/gen.go analyse-plugin --plugin=NotEmpty --package=analyse func init() { - Registry["not:empty"] = func(id string) Analyser { return NewNotEmpty(id) } + Manager().RegisterFactory("not:empty", func(id string) Analyser { return NewNotEmpty(id) }) } -func (p *NotEmpty) PluginName() string { +func (p *NotEmpty) GetName() string { return "not:empty" } diff --git a/pkg/analyse/notempty_test.go b/pkg/analyse/notempty_test.go index f4df669..0d22819 100644 --- a/pkg/analyse/notempty_test.go +++ b/pkg/analyse/notempty_test.go @@ -18,7 +18,7 @@ func TestNotEmptyInit(t *testing.T) { assert := assert.New(t) // Test that the plugin is registered. - plugin := Registry["not:empty"]("testNotEmpty") + plugin := Manager().GetFactories()["not:empty"]("testNotEmpty") assert.NotNil(plugin) analyser, ok := plugin.(*NotEmpty) assert.True(ok) @@ -26,8 +26,8 @@ func TestNotEmptyInit(t *testing.T) { } func TestNotEmptyPluginName(t *testing.T) { - instance := NotEmpty{Id: "testNotEmpty"} - assert.Equal(t, "not:empty", instance.PluginName()) + instance := NewNotEmpty("testNotEmpty") + assert.Equal(t, "not:empty", instance.GetName()) } func TestNotEmptyAnalyse(t *testing.T) { @@ -38,31 +38,31 @@ func TestNotEmptyAnalyse(t *testing.T) { }{ { name: "mapNestedStringNil", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string(nil), - }, + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string(nil), + ), expectedBreaches: []breach.Breach{}, }, { name: "mapNestedStringEmpty", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{}, - }, + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{}, + ), expectedBreaches: []breach.Breach{}, }, { name: "mapNestedStringNotEmpty", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{ + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{ "key1": {"subKey1": "value1"}, }, - }, + ), expectedBreaches: []breach.Breach{ &breach.KeyValueBreach{ BreachType: "key-value", @@ -83,7 +83,7 @@ func TestNotEmptyAnalyse(t *testing.T) { logrus.SetOutput(io.Discard) t.Run(tc.name, func(t *testing.T) { - analyser := NotEmpty{Id: tc.name} + analyser := NewNotEmpty(tc.name) tc.input.Collect() analyser.SetInput(tc.input) diff --git a/pkg/analyse/notequals.go b/pkg/analyse/notequals.go index ecb603a..7d52d70 100644 --- a/pkg/analyse/notequals.go +++ b/pkg/analyse/notequals.go @@ -7,44 +7,32 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/breach" "github.com/salsadigitalauorg/shipshape/pkg/data" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" ) // NotEquals is an analyser that checks if a fact is not equal to a value. // If a map is provided as input, the key is used to look up the value. type NotEquals struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - Severity string `yaml:"severity"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - Remediation interface{} `yaml:"remediation"` - input fact.Facter - - // Plugin fields. - Value string `yaml:"value"` - Key string `yaml:"key"` + BaseAnalyser `yaml:",inline"` + Value string `yaml:"value"` + Key string `yaml:"key"` } //go:generate go run ../../cmd/gen.go analyse-plugin --plugin=NotEquals --package=analyse func init() { - Registry["not:equals"] = func(id string) Analyser { return NewNotEquals(id) } + Manager().RegisterFactory("not:equals", func(id string) Analyser { return NewNotEquals(id) }) } -func (p *NotEquals) PluginName() string { +func (p *NotEquals) GetName() string { return "not:equals" } func (p *NotEquals) Analyse() { log.WithFields(log.Fields{ - "plugin": p.PluginName(), - "id": p.Id, - "input": p.InputName, - "input-format": p.input.GetFormat(), + "plugin": p.GetName(), + "id": p.GetId(), + "input": p.GetInputName(), + "input-format": p.GetInput().GetFormat(), }).Debug("analysing") switch p.input.GetFormat() { diff --git a/pkg/analyse/notequals_test.go b/pkg/analyse/notequals_test.go index d199c3f..e5a1bae 100644 --- a/pkg/analyse/notequals_test.go +++ b/pkg/analyse/notequals_test.go @@ -10,13 +10,14 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact/testdata" "github.com/salsadigitalauorg/shipshape/pkg/internal" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) func TestNotEqualsInit(t *testing.T) { assert := assert.New(t) // Test that the plugin is registered. - plugin := Registry["not:equals"]("TestNotEquals") + plugin := Manager().GetFactories()["not:equals"]("TestNotEquals") assert.NotNil(plugin) analyser, ok := plugin.(*NotEquals) assert.True(ok) @@ -24,8 +25,8 @@ func TestNotEqualsInit(t *testing.T) { } func TestNotEqualsPluginName(t *testing.T) { - instance := NotEquals{Id: "TestNotEquals"} - assert.Equal(t, "not:equals", instance.PluginName()) + instance := NewNotEquals("TestNotEquals") + assert.Equal(t, "not:equals", instance.GetName()) } func TestNotEqualsAnalyse(t *testing.T) { @@ -33,15 +34,19 @@ func TestNotEqualsAnalyse(t *testing.T) { // String. { Name: "string", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatString, - TestInputData: "bar", - }, + Input: testdata.New( + "testFact", + data.FormatString, + "bar", + ), Analyser: &NotEquals{ - InputName: "testFact", - Id: "TestNotEquals", - Value: "foo", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestNotEquals", + }, + InputName: "testFact", + }, + Value: "foo", }, ExpectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -53,15 +58,19 @@ func TestNotEqualsAnalyse(t *testing.T) { }, { Name: "stringEqual", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatString, - TestInputData: "foo", - }, + Input: testdata.New( + "testFact", + data.FormatString, + "foo", + ), Analyser: &NotEquals{ - InputName: "testFact", - Id: "TestNotEquals", - Value: "foo", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestNotEquals", + }, + InputName: "testFact", + }, + Value: "foo", }, ExpectedBreaches: []breach.Breach{}, }, @@ -69,18 +78,22 @@ func TestNotEqualsAnalyse(t *testing.T) { // Map of string. { Name: "mapString", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{ + Input: testdata.New( + "testFact", + data.FormatMapString, + map[string]interface{}{ "foo": "baz", }, - }, + ), Analyser: &NotEquals{ - InputName: "testFact", - Id: "TestNotEquals", - Key: "foo", - Value: "bar", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestNotEquals", + }, + InputName: "testFact", + }, + Key: "foo", + Value: "bar", }, ExpectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -92,16 +105,20 @@ func TestNotEqualsAnalyse(t *testing.T) { }, { Name: "mapStringEqual", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatMapString, - TestInputData: map[string]interface{}{"foo": "bar"}, - }, + Input: testdata.New( + "testFact", + data.FormatMapString, + map[string]interface{}{"foo": "bar"}, + ), Analyser: &NotEquals{ - InputName: "testFact", - Id: "TestNotEquals", - Key: "foo", - Value: "bar", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestNotEquals", + }, + InputName: "testFact", + }, + Key: "foo", + Value: "bar", }, ExpectedBreaches: []breach.Breach{}, }, @@ -109,15 +126,19 @@ func TestNotEqualsAnalyse(t *testing.T) { // Unsupported. { Name: "unsupported", - Input: &testdata.TestFacter{ - Name: "testFact", - TestInputDataFormat: data.FormatListString, - TestInputData: []interface{}{"foo", "bar"}, - }, + Input: testdata.New( + "testFact", + data.FormatListString, + []interface{}{"foo", "bar"}, + ), Analyser: &NotEquals{ - InputName: "testFact", - Id: "TestNotEquals", - Value: "foo", + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: "TestNotEquals", + }, + InputName: "testFact", + }, + Value: "foo", }, ExpectedBreaches: []breach.Breach{}, }, diff --git a/pkg/analyse/regexmatch.go b/pkg/analyse/regexmatch.go index a58dc74..66d5b86 100644 --- a/pkg/analyse/regexmatch.go +++ b/pkg/analyse/regexmatch.go @@ -8,45 +8,45 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/breach" "github.com/salsadigitalauorg/shipshape/pkg/data" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" ) type RegexMatch struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - Severity string `yaml:"severity"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - Remediation interface{} `yaml:"remediation"` - input fact.Facter - - // Plugin fields. - Pattern string `yaml:"pattern"` + BaseAnalyser `yaml:",inline"` + Pattern string `yaml:"pattern"` } //go:generate go run ../../cmd/gen.go analyse-plugin --plugin=RegexMatch --package=analyse func init() { - Registry["regex:match"] = func(id string) Analyser { return NewRegexMatch(id) } + Manager().RegisterFactory("regex:match", func(id string) Analyser { + return NewRegexMatch(id) + }) } -func (p *RegexMatch) PluginName() string { +func (p *RegexMatch) GetName() string { return "regex:match" } func (p *RegexMatch) Analyse() { - switch p.input.GetFormat() { + input := p.GetInput() + if input == nil { + return + } + + re, err := regexp.Compile(p.Pattern) + if err != nil { + p.AddErrors(err) + return + } + + switch input.GetFormat() { case data.FormatNil: return case data.FormatMapNestedString: - inputData := data.AsMapNestedString(p.input.GetData()) + inputData := data.AsMapNestedString(input.GetData()) for k, kvs := range inputData { for k2, v := range kvs { - match, _ := regexp.MatchString(p.Pattern, v) - if match { + if re.MatchString(v) { breach.EvaluateTemplate(p, &breach.KeyValueBreach{ Key: k, ValueLabel: k2, @@ -57,17 +57,16 @@ func (p *RegexMatch) Analyse() { } } case data.FormatString: - inputData := data.AsString(p.input.GetData()) - match, _ := regexp.MatchString(p.Pattern, inputData) - if match { + inputData := data.AsString(input.GetData()) + if re.MatchString(inputData) { breach.EvaluateTemplate(p, &breach.ValueBreach{ Value: fmt.Sprintf("%s equals '%s'", p.InputName, inputData), }, p.Remediation) } default: - log.WithField("input-format", p.input.GetFormat()).Debug("unsupported input format") + log.WithField("input-format", input.GetFormat()).Debug("unsupported input format") breach.EvaluateTemplate(p, &breach.ValueBreach{ - Value: fmt.Sprintf("unsupported input format %s", p.input.GetFormat()), + Value: fmt.Sprintf("unsupported input format %s", input.GetFormat()), }, nil) } } diff --git a/pkg/analyse/regexmatch_test.go b/pkg/analyse/regexmatch_test.go index c89617b..ca20b39 100644 --- a/pkg/analyse/regexmatch_test.go +++ b/pkg/analyse/regexmatch_test.go @@ -14,11 +14,13 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/fact/testdata" ) +const DataFormatUnsupported data.DataFormat = "nosupport" + func TestRegexMatchInit(t *testing.T) { assert := assert.New(t) // Test that the plugin is registered. - plugin := Registry["regex:match"]("testRegexMatch") + plugin := Manager().GetFactories()["regex:match"]("testRegexMatch") assert.NotNil(plugin) analyser, ok := plugin.(*RegexMatch) assert.True(ok) @@ -26,8 +28,8 @@ func TestRegexMatchInit(t *testing.T) { } func TestRegexMatchPluginName(t *testing.T) { - instance := RegexMatch{Id: "testRegexMatch"} - assert.Equal(t, "regex:match", instance.PluginName()) + instance := NewRegexMatch("testRegexMatch") + assert.Equal(t, "regex:match", instance.GetName()) } func TestRegexMatchAnalyse(t *testing.T) { @@ -39,49 +41,49 @@ func TestRegexMatchAnalyse(t *testing.T) { }{ { name: "nil", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatNil, - TestInputData: nil, - }, + input: testdata.New( + "testFacter", + data.FormatNil, + nil, + ), }, // Nested string map. { name: "mapNestedStringEmpty", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{}, - }, + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{}, + ), }, { name: "mapNestedStringNoMatch", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{ + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{ "key1": { "subkey1": "value1", "subkey2": "value2", }, }, - }, + ), pattern: ".*value3.*", expectedBreaches: []breach.Breach{}, }, { name: "mapNestedString1Match", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{ + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{ "key1": { "subkey1": "value1", "subkey2": "value2", }, }, - }, + ), pattern: ".*value2.*", expectedBreaches: []breach.Breach{ &breach.KeyValueBreach{ @@ -95,10 +97,10 @@ func TestRegexMatchAnalyse(t *testing.T) { }, { name: "mapNestedStringMultipleMatches", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{ + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{ "key1": { "subkey1": "value1", "subkey2": "value2", @@ -110,7 +112,7 @@ func TestRegexMatchAnalyse(t *testing.T) { "subkey3": "value3", }, }, - }, + ), pattern: ".*value(1|3|5).*", expectedBreaches: []breach.Breach{ &breach.KeyValueBreach{ @@ -140,11 +142,11 @@ func TestRegexMatchAnalyse(t *testing.T) { // String. { name: "stringEmpty/match/digit/single/match", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatString, - TestInputData: "0", - }, + input: testdata.New( + "testFacter", + data.FormatString, + "0", + ), pattern: "^0$", expectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -156,11 +158,11 @@ func TestRegexMatchAnalyse(t *testing.T) { }, { name: "stringEmpty/match/digit/single/nomatch", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatString, - TestInputData: "010", - }, + input: testdata.New( + "testFacter", + data.FormatString, + "010", + ), pattern: "^0$", expectedBreaches: []breach.Breach{}, }, @@ -168,10 +170,11 @@ func TestRegexMatchAnalyse(t *testing.T) { // Unsupported. { name: "unsupported", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: "nosupport", - }, + input: testdata.New( + "testFacter", + DataFormatUnsupported, + nil, + ), pattern: ".*", expectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -184,17 +187,15 @@ func TestRegexMatchAnalyse(t *testing.T) { } for _, tc := range tt { - assert := assert.New(t) - currLogOut := logrus.StandardLogger().Out defer logrus.SetOutput(currLogOut) logrus.SetOutput(io.Discard) t.Run(tc.name, func(t *testing.T) { - analyser := RegexMatch{ - Id: tc.name, - Pattern: tc.pattern, - } + assert := assert.New(t) + + analyser := NewRegexMatch(tc.name) + analyser.Pattern = tc.pattern tc.input.Collect() analyser.SetInput(tc.input) diff --git a/pkg/analyse/regexnotmatch.go b/pkg/analyse/regexnotmatch.go index 9469b8a..54f35f6 100644 --- a/pkg/analyse/regexnotmatch.go +++ b/pkg/analyse/regexnotmatch.go @@ -8,39 +8,42 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/breach" "github.com/salsadigitalauorg/shipshape/pkg/data" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" ) type RegexNotMatch struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - Severity string `yaml:"severity"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - Remediation interface{} `yaml:"remediation"` - input fact.Facter - - // Plugin fields. - Pattern string `yaml:"pattern"` + BaseAnalyser `yaml:",inline"` + Pattern string `yaml:"pattern"` } //go:generate go run ../../cmd/gen.go analyse-plugin --plugin=RegexNotMatch --package=analyse func init() { - Registry["regex:not-match"] = func(id string) Analyser { return NewRegexNotMatch(id) } + Manager().RegisterFactory("regex:not-match", func(id string) Analyser { return NewRegexNotMatch(id) }) } -func (p *RegexNotMatch) PluginName() string { +func (p *RegexNotMatch) GetName() string { return "regex:not-match" } func (p *RegexNotMatch) Analyse() { + contextLogger := log.WithFields(log.Fields{ + "plugin": p.GetName(), + "id": p.GetId(), + }) + + contextLogger.WithFields(log.Fields{ + "input": p.GetInputName(), + "input-format": p.GetInput().GetFormat(), + }).Debug("analysing") + switch p.input.GetFormat() { + case data.FormatNil: + breach.EvaluateTemplate(p, &breach.ValueBreach{ + Value: fmt.Sprintf("%s is nil", p.GetInputName()), + }, nil) return + case data.FormatMapNestedString: inputData := data.AsMapNestedString(p.input.GetData()) for k, kvs := range inputData { @@ -56,6 +59,7 @@ func (p *RegexNotMatch) Analyse() { } } } + case data.FormatString: inputData := data.AsString(p.input.GetData()) match, _ := regexp.MatchString(p.Pattern, inputData) @@ -64,6 +68,7 @@ func (p *RegexNotMatch) Analyse() { Value: fmt.Sprintf("%s equals '%s'", p.InputName, inputData), }, p.Remediation) } + default: log.WithField("input-format", p.input.GetFormat()).Debug("unsupported input format") breach.EvaluateTemplate(p, &breach.ValueBreach{ diff --git a/pkg/analyse/regexnotmatch_test.go b/pkg/analyse/regexnotmatch_test.go index 5aa7074..a0ddd9c 100644 --- a/pkg/analyse/regexnotmatch_test.go +++ b/pkg/analyse/regexnotmatch_test.go @@ -18,7 +18,7 @@ func TestRegexNotMatchInit(t *testing.T) { assert := assert.New(t) // Test that the plugin is registered. - plugin := Registry["regex:not-match"]("testRegexNotMatch") + plugin := Manager().GetFactories()["regex:not-match"]("testRegexNotMatch") assert.NotNil(plugin) analyser, ok := plugin.(*RegexNotMatch) assert.True(ok) @@ -26,62 +26,71 @@ func TestRegexNotMatchInit(t *testing.T) { } func TestRegexNotMatchPluginName(t *testing.T) { - instance := RegexNotMatch{Id: "testRegexNotMatch"} - assert.Equal(t, "regex:not-match", instance.PluginName()) + instance := NewRegexNotMatch("testRegexNotMatch") + assert.Equal(t, "regex:not-match", instance.GetName()) } func TestRegexNotMatchAnalyse(t *testing.T) { tt := []struct { name string + inputName string input fact.Facter pattern string expectedBreaches []breach.Breach }{ { name: "nil", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatNil, - TestInputData: nil, + input: testdata.New( + "testFacter", + data.FormatNil, + nil, + ), + inputName: "testFacter", + expectedBreaches: []breach.Breach{ + &breach.ValueBreach{ + BreachType: "value", + CheckName: "nil", + Value: "testFacter is nil", + }, }, }, // Nested string map. { name: "mapNestedStringEmpty", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{}, - }, + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{}, + ), }, { name: "mapNestedStringAllMatch", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{ + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{ "key1": { "subkey1": "value1", "subkey2": "value2", }, }, - }, + ), pattern: "value[12]", expectedBreaches: []breach.Breach{}, }, { name: "mapNestedString1NotMatch", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{ + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{ "key1": { "subkey1": "value1", "subkey2": "other2", }, }, - }, + ), pattern: "value.*", expectedBreaches: []breach.Breach{ &breach.KeyValueBreach{ @@ -95,10 +104,10 @@ func TestRegexNotMatchAnalyse(t *testing.T) { }, { name: "mapNestedStringMultipleNotMatches", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatMapNestedString, - TestInputData: map[string]map[string]string{ + input: testdata.New( + "testFacter", + data.FormatMapNestedString, + map[string]map[string]string{ "key1": { "subkey1": "other1", "subkey2": "value2", @@ -110,7 +119,7 @@ func TestRegexNotMatchAnalyse(t *testing.T) { "subkey3": "other3", }, }, - }, + ), pattern: "value.*", expectedBreaches: []breach.Breach{ &breach.KeyValueBreach{ @@ -147,11 +156,11 @@ func TestRegexNotMatchAnalyse(t *testing.T) { // String. { name: "string/notmatch/digit/single", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatString, - TestInputData: "1", - }, + input: testdata.New( + "testFacter", + data.FormatString, + "1", + ), pattern: "^0$", expectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -163,11 +172,11 @@ func TestRegexNotMatchAnalyse(t *testing.T) { }, { name: "string/match/digit/single", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatString, - TestInputData: "0", - }, + input: testdata.New( + "testFacter", + data.FormatString, + "0", + ), pattern: "^0$", expectedBreaches: []breach.Breach{}, }, @@ -175,10 +184,11 @@ func TestRegexNotMatchAnalyse(t *testing.T) { // Unsupported. { name: "unsupported", - input: &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: "nosupport", - }, + input: testdata.New( + "testFacter", + DataFormatUnsupported, + nil, + ), pattern: ".*", expectedBreaches: []breach.Breach{ &breach.ValueBreach{ @@ -191,17 +201,16 @@ func TestRegexNotMatchAnalyse(t *testing.T) { } for _, tc := range tt { - assert := assert.New(t) - currLogOut := logrus.StandardLogger().Out defer logrus.SetOutput(currLogOut) logrus.SetOutput(io.Discard) t.Run(tc.name, func(t *testing.T) { - analyser := RegexNotMatch{ - Id: tc.name, - Pattern: tc.pattern, - } + assert := assert.New(t) + + analyser := NewRegexNotMatch(tc.name) + analyser.InputName = tc.inputName + analyser.Pattern = tc.pattern tc.input.Collect() analyser.SetInput(tc.input) diff --git a/pkg/analyse/templates/analyseplugin.go.tmpl b/pkg/analyse/templates/analyseplugin.go.tmpl index a58ed07..986b4a1 100644 --- a/pkg/analyse/templates/analyseplugin.go.tmpl +++ b/pkg/analyse/templates/analyseplugin.go.tmpl @@ -1,10 +1,7 @@ package analyse import ( - log "github.com/sirupsen/logrus" - - "github.com/salsadigitalauorg/shipshape/pkg/breach" - "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" "github.com/salsadigitalauorg/shipshape/pkg/result" ) @@ -12,81 +9,14 @@ import ( func New{{ .Plugin }}(id string) *{{ .Plugin }} { return &{{ .Plugin }}{ - Id: id, - Result: result.Result{Name: id, Severity: "normal"}, - } -} - -func (p *{{ .Plugin }}) SetInput(input fact.Facter) { - p.input = input -} - -func (p *{{ .Plugin }}) GetId() string { - return p.Id -} - -func (p *{{ .Plugin }}) GetDescription() string { - return p.Description -} - -func (p *{{ .Plugin }}) GetInputName() string { - return p.InputName -} - -func (p *{{ .Plugin }}) GetBreachTemplate() breach.BreachTemplate { - return p.BreachTemplate -} - -func (p *{{ .Plugin }}) GetResult() result.Result { - if p.Description != "" && p.Result.Name != p.Description { - p.Result.Name = p.Description + BaseAnalyser: BaseAnalyser{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + Result: result.Result{ + Name: id, + Severity: "normal", + }, + }, } - return p.Result -} - -func (p *{{ .Plugin }}) ValidateInput() error { - log.WithFields(log.Fields{ - "analyser": p.Id, - }).Debug("validating input") - - plugin := fact.GetInstance(p.InputName) - if plugin == nil { - return &fact.ErrSupportNotFound{ - Plugin: p.GetId(), SupportType: "input", SupportPlugin: p.InputName} - } - - p.input = plugin - return nil -} - -func (p *{{ .Plugin }}) PreProcessInput() bool { - if p.input == nil { - p.AddBreach(&breach.ValueBreach{ - Value: "no input available to analyse", - }) - return false - } - - if len(p.input.GetErrors()) > 0 { - errs := []string{} - for _, e := range p.input.GetErrors() { - errs = append(errs, e.Error()) - } - p.AddBreach(&breach.KeyValuesBreach{ - Key: "input failure", - Values: errs, - }) - return false - } - - return true -} - -// AddBreach appends a Breach to the Result. -func (p *{{ .Plugin }}) AddBreach(b breach.Breach) { - b.SetCommonValues("", p.Id, p.Severity) - p.Result.Breaches = append( - p.Result.Breaches, - b, - ) } diff --git a/pkg/analyse/templates/analyseplugin_test.go.tmpl b/pkg/analyse/templates/analyseplugin_test.go.tmpl deleted file mode 100644 index 46d5392..0000000 --- a/pkg/analyse/templates/analyseplugin_test.go.tmpl +++ /dev/null @@ -1,103 +0,0 @@ -package analyse_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - . "github.com/salsadigitalauorg/shipshape/pkg/analyse" - "github.com/salsadigitalauorg/shipshape/pkg/breach" - "github.com/salsadigitalauorg/shipshape/pkg/data" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/fact/testdata" - "github.com/salsadigitalauorg/shipshape/pkg/result" -) - -// Code generated by analyse-plugin --plugin={{ .Plugin }}; DO NOT EDIT. -// The proper way to update this file is to -// - modify one of the pkg/analyse/*_gen_test.go files as needed, -// - copy the changes to pkg/analyse/templates/analyseplugin_test.go.tmpl, -// - replace the plugin struct name with {{ .Plugin }} wherever needed. - -func Test{{ .Plugin }}Getters(t *testing.T) { - instance := {{ .Plugin }}{ - Id: "test{{ .Plugin }}", - Description: "description of the test {{ .Plugin }} analyser", - InputName: "testInputName", - } - assert.Equal(t, "test{{ .Plugin }}", instance.GetId()) - assert.Equal(t, "description of the test {{ .Plugin }} analyser", instance.GetDescription()) - assert.Equal(t, "testInputName", instance.GetInputName()) -} - -func Test{{ .Plugin }}GetResult(t *testing.T) { - t.Run("noDescription", func(t *testing.T) { - instance := {{ .Plugin }}{ - Result: result.Result{Name: "test{{ .Plugin }}ResultName"}, - } - assert.Equal(t, "test{{ .Plugin }}ResultName", instance.GetResult().Name) - }) - - t.Run("description", func(t *testing.T) { - instance := {{ .Plugin }}{ - Result: result.Result{Name: "test{{ .Plugin }}ResultName"}, - Description: "description of the test {{ .Plugin }} analyser", - } - assert.Equal(t, "description of the test {{ .Plugin }} analyser", instance.GetResult().Name) - }) -} - -func Test{{ .Plugin }}ValidateInput(t *testing.T) { - t.Run("noInput", func(t *testing.T) { - instance := {{ .Plugin }}{} - err := instance.ValidateInput() - assert.Equal(t, &fact.ErrSupportNotFound{ - Plugin: "", SupportType: "input", SupportPlugin: ""}, err) - }) - - t.Run("input", func(t *testing.T) { - origFacts := fact.Facts - defer func() { fact.Facts = origFacts }() - - testFacter := &testdata.TestFacter{ - Name: "testFacter", - TestInputDataFormat: data.FormatListString, - TestInputData: nil, - } - - fact.Facts = map[string]fact.Facter{"testInputName": testFacter} - instance := {{ .Plugin }}{InputName: "testInputName"} - instance.SetInput(testFacter) - err := instance.ValidateInput() - assert.Nil(t, err) - }) -} - -func Test{{ .Plugin }}PreProcessInput(t *testing.T) { - t.Run("noInput", func(t *testing.T) { - instance := {{ .Plugin }}{} - assert.False(t, instance.PreProcessInput()) - assert.ElementsMatch(t, []breach.Breach{&breach.ValueBreach{ - BreachType: "value", - Value: "no input available to analyse"}}, instance.GetResult().Breaches) - }) - - t.Run("inputNoError", func(t *testing.T) { - instance := {{ .Plugin }}{} - instance.SetInput(&testdata.TestFacter{Name: "testFacter"}) - assert.True(t, instance.PreProcessInput()) - }) - - t.Run("inputWithError", func(t *testing.T) { - instance := {{ .Plugin }}{} - testInput := testdata.TestFacter{Name: "testFacter"} - testInput.AddError(errors.New("test error")) - instance.SetInput(&testInput) - assert.False(t, instance.PreProcessInput()) - assert.ElementsMatch(t, []breach.Breach{&breach.KeyValuesBreach{ - BreachType: "key-values", - Key: "input failure", - Values: []string{"test error"}}}, instance.GetResult().Breaches) - }) -} diff --git a/pkg/analyse/testdata/testanalyser.go b/pkg/analyse/testdata/testanalyser.go index 6cc5cfb..2e8f7e5 100644 --- a/pkg/analyse/testdata/testanalyser.go +++ b/pkg/analyse/testdata/testanalyser.go @@ -1,42 +1,11 @@ package testdata -import ( - "github.com/salsadigitalauorg/shipshape/pkg/breach" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" -) +import "github.com/salsadigitalauorg/shipshape/pkg/analyse" type TestAnalyser struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - input fact.Facter + analyse.BaseAnalyser } -// Common plugin methods. -func (p *TestAnalyser) PluginName() string { return "test-analyser" } - -func (p *TestAnalyser) GetId() string { return p.Id } - -// Analyse methods. - -func (p *TestAnalyser) SetInput(input fact.Facter) { p.input = input } - -func (p *TestAnalyser) GetDescription() string { return p.Description } - -func (p *TestAnalyser) GetInputName() string { return p.InputName } - -func (p *TestAnalyser) GetBreachTemplate() breach.BreachTemplate { return p.BreachTemplate } - -func (p *TestAnalyser) GetResult() result.Result { return p.Result } - -func (p *TestAnalyser) ValidateInput() error { return nil } - +func (p *TestAnalyser) GetName() string { return "test-analyser" } +func (p *TestAnalyser) ValidateInput() error { return nil } func (p *TestAnalyser) PreProcessInput() bool { return true } - -func (p *TestAnalyser) Analyse() {} - -func (p *TestAnalyser) AddBreach(b breach.Breach) {} diff --git a/pkg/analyse/testdata/testanalyserinputerror.go b/pkg/analyse/testdata/testanalyserinputerror.go index 0d9cc22..904d7be 100644 --- a/pkg/analyse/testdata/testanalyserinputerror.go +++ b/pkg/analyse/testdata/testanalyserinputerror.go @@ -3,42 +3,13 @@ package testdata import ( "errors" - "github.com/salsadigitalauorg/shipshape/pkg/breach" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" + "github.com/salsadigitalauorg/shipshape/pkg/analyse" ) type TestAnalyserInputError struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - input fact.Facter + analyse.BaseAnalyser } -// Common plugin methods. -func (p *TestAnalyserInputError) PluginName() string { return "test-analyser" } - -func (p *TestAnalyserInputError) GetId() string { return p.Id } - -// Analyse methods. - -func (p *TestAnalyserInputError) SetInput(input fact.Facter) { p.input = input } - -func (p *TestAnalyserInputError) GetDescription() string { return p.Description } - -func (p *TestAnalyserInputError) GetInputName() string { return p.InputName } - -func (p *TestAnalyserInputError) GetBreachTemplate() breach.BreachTemplate { return p.BreachTemplate } - -func (p *TestAnalyserInputError) GetResult() result.Result { return p.Result } - -func (p *TestAnalyserInputError) ValidateInput() error { return errors.New("input error") } - +func (p *TestAnalyserInputError) GetName() string { return "test-analyser" } +func (p *TestAnalyserInputError) ValidateInput() error { return errors.New("input error") } func (p *TestAnalyserInputError) PreProcessInput() bool { return true } - -func (p *TestAnalyserInputError) Analyse() {} - -func (p *TestAnalyserInputError) AddBreach(b breach.Breach) {} diff --git a/pkg/analyse/testdata/testanalyserpass.go b/pkg/analyse/testdata/testanalyserpass.go index 472c856..7760c26 100644 --- a/pkg/analyse/testdata/testanalyserpass.go +++ b/pkg/analyse/testdata/testanalyserpass.go @@ -1,37 +1,15 @@ package testdata import ( + "github.com/salsadigitalauorg/shipshape/pkg/analyse" "github.com/salsadigitalauorg/shipshape/pkg/breach" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" ) type TestAnalyserPass struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - Severity string `yaml:"severity"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - input fact.Facter + analyse.BaseAnalyser } -func (p *TestAnalyserPass) PluginName() string { return "test-analyser" } - -func (p *TestAnalyserPass) SetInput(input fact.Facter) { p.input = input } - -func (p *TestAnalyserPass) GetId() string { return p.Id } - -func (p *TestAnalyserPass) GetDescription() string { return p.Description } - -func (p *TestAnalyserPass) GetInputName() string { return p.InputName } - -func (p *TestAnalyserPass) GetBreachTemplate() breach.BreachTemplate { - return p.BreachTemplate -} - -func (p *TestAnalyserPass) GetResult() result.Result { return p.Result } +func (p *TestAnalyserPass) GetName() string { return "test-analyser" } func (p *TestAnalyserPass) ValidateInput() error { return nil } @@ -43,11 +21,3 @@ func (p *TestAnalyserPass) Analyse() { Values: []string{"more details would be here"}, }) } - -func (p *TestAnalyserPass) AddBreach(b breach.Breach) { - b.SetCommonValues("", p.Id, p.Severity) - p.Result.Breaches = append( - p.Result.Breaches, - b, - ) -} diff --git a/pkg/analyse/testdata/testanalyserpreprocessinputfail.go b/pkg/analyse/testdata/testanalyserpreprocessinputfail.go index b56d604..790ce88 100644 --- a/pkg/analyse/testdata/testanalyserpreprocessinputfail.go +++ b/pkg/analyse/testdata/testanalyserpreprocessinputfail.go @@ -3,37 +3,15 @@ package testdata import ( "errors" + "github.com/salsadigitalauorg/shipshape/pkg/analyse" "github.com/salsadigitalauorg/shipshape/pkg/breach" - "github.com/salsadigitalauorg/shipshape/pkg/fact" - "github.com/salsadigitalauorg/shipshape/pkg/result" ) type TestAnalyserPreprocessInputFail struct { - // Common fields. - Id string `yaml:"name"` - Description string `yaml:"description"` - InputName string `yaml:"input"` - Severity string `yaml:"severity"` - breach.BreachTemplate `yaml:"breach-format"` - Result result.Result - input fact.Facter + analyse.BaseAnalyser } -func (p *TestAnalyserPreprocessInputFail) PluginName() string { return "test-analyser" } - -func (p *TestAnalyserPreprocessInputFail) SetInput(input fact.Facter) { p.input = input } - -func (p *TestAnalyserPreprocessInputFail) GetId() string { return p.Id } - -func (p *TestAnalyserPreprocessInputFail) GetDescription() string { return p.Description } - -func (p *TestAnalyserPreprocessInputFail) GetInputName() string { return p.InputName } - -func (p *TestAnalyserPreprocessInputFail) GetBreachTemplate() breach.BreachTemplate { - return p.BreachTemplate -} - -func (p *TestAnalyserPreprocessInputFail) GetResult() result.Result { return p.Result } +func (p *TestAnalyserPreprocessInputFail) GetName() string { return "test-analyser" } func (p *TestAnalyserPreprocessInputFail) ValidateInput() error { return errors.New("input error") } diff --git a/pkg/analyse/types.go b/pkg/analyse/types.go index fd06b6f..e74c0e8 100644 --- a/pkg/analyse/types.go +++ b/pkg/analyse/types.go @@ -3,22 +3,24 @@ package analyse import ( "github.com/salsadigitalauorg/shipshape/pkg/breach" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" "github.com/salsadigitalauorg/shipshape/pkg/result" ) type Analyser interface { - // Common plugin methods. - PluginName() string - GetId() string + plugin.Plugin - // Analyse methods. + // Input methods SetInput(input fact.Facter) - GetDescription() string + GetInput() fact.Facter GetInputName() string - GetBreachTemplate() breach.BreachTemplate - GetResult() result.Result ValidateInput() error PreProcessInput() bool + + // Analysis methods + GetDescription() string + GetBreachTemplate() breach.BreachTemplate + GetResult() result.Result Analyse() AddBreach(b breach.Breach) } diff --git a/pkg/connection/base.go b/pkg/connection/base.go new file mode 100644 index 0000000..2da3622 --- /dev/null +++ b/pkg/connection/base.go @@ -0,0 +1,25 @@ +package connection + +import ( + "github.com/salsadigitalauorg/shipshape/pkg/plugin" +) + +// BaseConnection provides common fields and functionality for connection plugins. +type BaseConnection struct { + plugin.BasePlugin `yaml:",inline"` + Name string `yaml:"name"` + errors []error + data []byte +} + +func (p *BaseConnection) GetName() string { + return p.Name +} + +func (p *BaseConnection) GetErrors() []error { + return p.errors +} + +func (p *BaseConnection) AddErrors(errs ...error) { + p.errors = append(p.errors, errs...) +} diff --git a/pkg/connection/connection.go b/pkg/connection/connection.go deleted file mode 100644 index aa2cf2c..0000000 --- a/pkg/connection/connection.go +++ /dev/null @@ -1,59 +0,0 @@ -package connection - -import ( - "fmt" - "sort" - - log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" -) - -var Registry = map[string]func(string) Connectioner{} -var Connections = map[string]Connectioner{} -var Errors = []error{} - -func RegistryKeys() []string { - keys := []string{} - for k := range Registry { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -func ParseConfig(raw map[string]map[string]interface{}) { - count := 0 - log.WithField("registry", RegistryKeys()).Debug("available connections") - for name, pluginConf := range raw { - for pluginName, pluginMap := range pluginConf { - f, ok := Registry[pluginName] - if !ok { - continue - } - - p := f(name) - pluginYaml, err := yaml.Marshal(pluginMap) - if err != nil { - panic(err) - } - - err = yaml.Unmarshal(pluginYaml, p) - if err != nil { - panic(err) - } - - log.WithField("connection", fmt.Sprintf("%#v", p)).Debug("parsed connection") - Connections[name] = p - count++ - } - } - log.Infof("parsed %d connections", count) -} - -func GetInstance(name string) Connectioner { - if c, ok := Connections[name]; !ok { - return nil - } else { - return c - } -} diff --git a/pkg/connection/dockerexec.go b/pkg/connection/dockerexec.go index c2e7a17..a7b6dcf 100644 --- a/pkg/connection/dockerexec.go +++ b/pkg/connection/dockerexec.go @@ -1,25 +1,33 @@ package connection -import "github.com/salsadigitalauorg/shipshape/pkg/command" +import ( + "github.com/salsadigitalauorg/shipshape/pkg/command" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" +) type DockerExec struct { - // Common fields. - Name string `yaml:"name"` - errors []error - data []byte - - // Plugin fields. - Container string `yaml:"container"` - Command []string `yaml:"command"` + BaseConnection `yaml:",inline"` + Container string `yaml:"container"` + Command []string `yaml:"command"` } -//go:generate go run ../../cmd/gen.go connection-plugin --plugin=DockerExec - func init() { - Registry["docker:exec"] = func(n string) Connectioner { return &DockerExec{Name: n} } + Manager().RegisterFactory("docker:exec", func(n string) Connectioner { + return NewDockerExec(n) + }) +} + +func NewDockerExec(id string) *DockerExec { + return &DockerExec{ + BaseConnection: BaseConnection{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + }, + } } -func (p *DockerExec) PluginName() string { +func (p *DockerExec) GetName() string { return "docker:exec" } diff --git a/pkg/connection/manager.go b/pkg/connection/manager.go new file mode 100644 index 0000000..4bd0f26 --- /dev/null +++ b/pkg/connection/manager.go @@ -0,0 +1,61 @@ +package connection + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + "github.com/salsadigitalauorg/shipshape/pkg/plugin" + "github.com/salsadigitalauorg/shipshape/pkg/pluginmanager" +) + +// manager handles connection plugin registration and lifecycle. +type manager struct { + *pluginmanager.Manager[Connectioner] +} + +var m *manager + +// Manager returns the connection manager. +func Manager() *manager { + if m == nil { + m = &manager{ + Manager: pluginmanager.NewManager[Connectioner](), + } + } + return m +} + +func (m *manager) GetFactoriesKeys() []string { + return plugin.GetFactoriesKeys[Connectioner](m.GetFactories()) +} + +// ParseConfig parses the raw config and creates the connections. +func (m *manager) ParseConfig(raw map[string]map[string]interface{}) error { + count := 0 + log.WithField("registry", m.GetFactoriesKeys()).Debug("available connections") + for id, pluginConf := range raw { + for pluginName, pluginIf := range pluginConf { + plugin, err := m.GetPlugin(pluginName, id) + if err != nil { + return err + } + + // Convert the map to yaml, then parse it into the plugin. + pluginYaml, _ := yaml.Marshal(pluginIf) + err = yaml.Unmarshal(pluginYaml, plugin) + if err != nil { + return err + } + + log.WithFields(log.Fields{ + "id": id, + "plugin": fmt.Sprintf("%#v", plugin), + }).Debug("parsed connection") + count++ + } + } + log.Infof("parsed %d connections", count) + return nil +} diff --git a/pkg/connection/mysql.go b/pkg/connection/mysql.go index a87e470..301d6a6 100644 --- a/pkg/connection/mysql.go +++ b/pkg/connection/mysql.go @@ -7,28 +7,36 @@ import ( "github.com/doug-martin/goqu/v9" _ "github.com/doug-martin/goqu/v9/dialect/mysql" "github.com/go-sql-driver/mysql" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) type Mysql struct { - // Common fields. - Name string `yaml:"name"` - - // Plugin fields. - Host string `yaml:"host"` - Port string `yaml:"port"` - User string `yaml:"user"` - Password string `yaml:"password"` - Database string `yaml:"database"` - Db *goqu.Database + BaseConnection `yaml:",inline"` + Host string `yaml:"host"` + Port string `yaml:"port"` + User string `yaml:"user"` + Password string `yaml:"password"` + Database string `yaml:"database"` + Db *goqu.Database } -//go:generate go run ../../cmd/gen.go connection-plugin --plugin=Mysql - func init() { - Registry["mysql"] = func(n string) Connectioner { return &Mysql{Name: n} } + Manager().RegisterFactory("mysql", func(n string) Connectioner { + return NewMysql(n) + }) +} + +func NewMysql(id string) *Mysql { + return &Mysql{ + BaseConnection: BaseConnection{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + }, + } } -func (p *Mysql) PluginName() string { +func (p *Mysql) GetName() string { return "mysql" } diff --git a/pkg/connection/types.go b/pkg/connection/types.go index f0e008d..9d08f00 100644 --- a/pkg/connection/types.go +++ b/pkg/connection/types.go @@ -1,7 +1,8 @@ package connection +import "github.com/salsadigitalauorg/shipshape/pkg/plugin" + type Connectioner interface { - PluginName() string - GetName() string + plugin.Plugin Run() ([]byte, error) } diff --git a/pkg/env/envresolver.go b/pkg/env/envresolver.go index 2863b7d..00d1542 100644 --- a/pkg/env/envresolver.go +++ b/pkg/env/envresolver.go @@ -16,12 +16,25 @@ type EnvResolver interface { GetEnvMap() (map[string]string, error) } -func ReadEnvFile(er EnvResolver) (map[string]string, error) { - if !er.ShouldResolveEnv() { +type BaseEnvResolver struct { + ResolveEnv bool `yaml:"resolve-env"` + EnvFile string `yaml:"env-file"` +} + +func (e *BaseEnvResolver) ShouldResolveEnv() bool { + return e.ResolveEnv +} + +func (e *BaseEnvResolver) GetEnvFile() string { + return e.EnvFile +} + +func (e *BaseEnvResolver) GetEnvMap() (map[string]string, error) { + if !e.ShouldResolveEnv() { return nil, nil } - envFile := er.GetEnvFile() + envFile := e.GetEnvFile() if envFile == "" { envFile = filepath.Join(config.ProjectDir, ".env") } diff --git a/pkg/fact/base.go b/pkg/fact/base.go new file mode 100644 index 0000000..fd259ad --- /dev/null +++ b/pkg/fact/base.go @@ -0,0 +1,233 @@ +package fact + +import ( + log "github.com/sirupsen/logrus" + + "github.com/salsadigitalauorg/shipshape/pkg/connection" + "github.com/salsadigitalauorg/shipshape/pkg/data" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" +) + +// BaseFact provides common fields and functionality for fact plugins. +type BaseFact struct { + plugin.BasePlugin `yaml:",inline"` + Format data.DataFormat `yaml:"format"` + ConnectionName string `yaml:"connection"` + InputName string `yaml:"input"` + AdditionalInputNames []string `yaml:"additional-inputs"` + + connection connection.Connectioner + input Facter + additionalInputs []Facter + data interface{} +} + +func (p *BaseFact) GetFormat() data.DataFormat { + return p.Format +} + +func (p *BaseFact) GetConnectionName() string { + return p.ConnectionName +} + +func (p *BaseFact) GetConnection() connection.Connectioner { + return p.connection +} + +func (p *BaseFact) GetInputName() string { + return p.InputName +} + +func (p *BaseFact) GetInput() Facter { + return p.input +} + +func (p *BaseFact) GetAdditionalInputNames() []string { + return p.AdditionalInputNames +} + +func (p *BaseFact) GetAdditionalInputs() []Facter { + return p.additionalInputs +} + +func (p *BaseFact) GetErrors() []error { + if p.input != nil { + p.AddErrors(p.input.GetErrors()...) + return p.BasePlugin.GetErrors() + } + return p.BasePlugin.GetErrors() +} + +func (p *BaseFact) GetData() interface{} { + return p.data +} + +func (p *BaseFact) SetConnection(conn connection.Connectioner) { + p.connection = conn +} + +func (p *BaseFact) SetInputName(name string) { + p.InputName = name +} + +func (p *BaseFact) SetInput(inP Facter) { + p.input = inP +} + +func (p *BaseFact) SetData(data interface{}) { + p.data = data +} + +func (p *BaseFact) SetAdditionalInputs(plugins []Facter) { + p.additionalInputs = plugins +} + +// Default implementations for support methods +func (p *BaseFact) SupportedConnections() (plugin.SupportLevel, []string) { + return plugin.SupportNone, []string{} +} + +func (p *BaseFact) SupportedInputFormats() (plugin.SupportLevel, []data.DataFormat) { + return plugin.SupportNone, []data.DataFormat{} +} + +func ValidateConnection(p Facter) error { + connectionSupport, supportedConnections := p.SupportedConnections() + + log.WithFields(log.Fields{ + "fact": p.GetId(), + "connection-support": connectionSupport, + "supported-connections": supportedConnections, + }).Debug("validating connection") + + if (connectionSupport == plugin.SupportOptional || + connectionSupport == plugin.SupportNone) && + len(supportedConnections) == 0 && p.GetConnectionName() == "" { + return nil + } + + if connectionSupport == plugin.SupportRequired && p.GetConnectionName() == "" { + return &plugin.ErrSupportRequired{ + Plugin: p.GetName(), SupportType: "connection"} + } + + connPlug := connection.Manager().FindPlugin(p.GetConnectionName()) + if connPlug == nil { + return &plugin.ErrSupportNotFound{ + Plugin: p.GetName(), + SupportType: "connection", + SupportPlugin: p.GetConnectionName()} + } + + for _, s := range supportedConnections { + if connPlug.GetName() == s { + p.SetConnection(connPlug) + return nil + } + } + return &plugin.ErrSupportNone{ + Plugin: p.GetName(), + SupportType: "connection", + SupportPlugin: connPlug.GetName()} +} + +func ValidateInput(p Facter) error { + inputFormatSupport, supportedInputFormats := p.SupportedInputFormats() + log.WithFields(log.Fields{ + "fact": p.GetName(), + "inputFormatSupport": inputFormatSupport, + "supportedInputFormats": supportedInputFormats, + "inputName": p.GetInputName(), + }).Debug("validating input") + + if (inputFormatSupport == plugin.SupportOptional || + inputFormatSupport == plugin.SupportNone) && + len(supportedInputFormats) == 0 && p.GetInputName() == "" { + return nil + } + + if inputFormatSupport == plugin.SupportRequired && p.GetInputName() == "" { + return &plugin.ErrSupportRequired{Plugin: p.GetName(), SupportType: "inputFormat"} + } + + if p.GetInputName() != "" { + inPlug := Manager().FindPlugin(p.GetInputName()) + if inPlug == nil { + return &plugin.ErrSupportNotFound{ + Plugin: p.GetName(), + SupportType: "input", + SupportPlugin: p.GetInputName()} + } + + log.WithFields(log.Fields{ + "fact": p.GetName(), + "input-plugin": inPlug.GetName(), + "input-plugin-format": inPlug.GetFormat(), + }).Debug("found input plugin") + + if inPlug.GetFormat() == "" { + return &plugin.ErrSupportRequired{ + Plugin: inPlug.GetName(), SupportType: "inputFormat"} + } + + for _, s := range supportedInputFormats { + if inPlug.GetFormat() == s { + p.SetInput(inPlug) + return nil + } + } + + return &plugin.ErrSupportNone{ + SupportType: "inputFormat", + SupportPlugin: string(inPlug.GetFormat()), + Plugin: p.GetName(), + } + } + + return &plugin.ErrSupportNotFound{ + SupportType: "input", + Plugin: p.GetName(), + SupportPlugin: p.GetInputName()} +} + +func LoadAdditionalInputs(p Facter) []error { + log.WithFields(log.Fields{"fact": p.GetId()}). + Debug("loading additional inputs") + + if len(p.GetAdditionalInputNames()) == 0 { + return nil + } + + plugins := []Facter{} + errs := []error{} + for _, n := range p.GetAdditionalInputNames() { + inPlug := Manager().FindPlugin(n) + if inPlug == nil { + errs = append(errs, &plugin.ErrSupportNotFound{ + Plugin: p.GetName(), + SupportType: "input", + SupportPlugin: n, + }) + continue + } + + if inPlug.GetFormat() == "" { + errs = append(errs, &plugin.ErrSupportRequired{ + Plugin: inPlug.GetName(), + SupportType: "inputFormat"}) + continue + } + + plugins = append(plugins, inPlug) + } + + if len(errs) > 0 { + return errs + } + + p.SetAdditionalInputs(plugins) + return nil +} + +// Collect is the main method for collecting data from the fact. +func (p *BaseFact) Collect() {} diff --git a/pkg/fact/command/command.go b/pkg/fact/command/command.go index 77e389d..7d90525 100644 --- a/pkg/fact/command/command.go +++ b/pkg/fact/command/command.go @@ -8,56 +8,48 @@ import ( log "github.com/sirupsen/logrus" "github.com/salsadigitalauorg/shipshape/pkg/command" - "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) // Command is a representation of a shell command. type Command struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - - connection connection.Connectioner - input fact.Facter - additionalInputs []fact.Facter - errors []error - data interface{} - - // Plugin fields. + fact.BaseFact `yaml:",inline"` + + // Plugin-specific fields Cmd string `yaml:"cmd"` Args []string `yaml:"args"` IgnoreError bool `yaml:"ignore-error"` } -//go:generate go run ../../../cmd/gen.go fact-plugin --plugin=Command --package=command +//go:generate go run ../../../cmd/gen.go fact-plugin --package=command func init() { - fact.Registry["command"] = func(n string) fact.Facter { - return &Command{Name: n, Format: data.FormatMapString} - } -} - -func (p *Command) PluginName() string { - return "command" + fact.Manager().RegisterFactory("command", func(n string) fact.Facter { + return New(n) + }) } -func (p *Command) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func New(id string) *Command { + return &Command{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + Format: data.FormatMapString, + }, + } } -func (p *Command) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func (p *Command) GetName() string { + return "command" } func (p *Command) Collect() { contextLogger := log.WithFields(log.Fields{ - "fact-plugin": p.PluginName(), - "fact": p.Name, + "fact-plugin": p.GetName(), + "fact": p.GetId(), }) contextLogger.WithFields(log.Fields{ @@ -87,9 +79,9 @@ func (p *Command) Collect() { WithField("stdout", res["stdout"]). WithField("stderr", res["stderr"]). WithError(err).Error("command failed") - p.errors = append(p.errors, err) + p.AddErrors(err) } } - p.data = res + p.SetData(res) } diff --git a/pkg/fact/command/command_test.go b/pkg/fact/command/command_test.go index 57deb46..eeb41cc 100644 --- a/pkg/fact/command/command_test.go +++ b/pkg/fact/command/command_test.go @@ -6,70 +6,90 @@ import ( "github.com/stretchr/testify/assert" + "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" . "github.com/salsadigitalauorg/shipshape/pkg/fact/command" "github.com/salsadigitalauorg/shipshape/pkg/internal" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) func TestCommandInit(t *testing.T) { assert := assert.New(t) // Test that the command plugin is registered. - factPlugin := fact.Registry["command"]("TestCommand") + factPlugin := fact.Manager().GetFactories()["command"]("TestCommand") assert.NotNil(factPlugin) keyFacter, ok := factPlugin.(*Command) assert.True(ok) - assert.Equal("TestCommand", keyFacter.Name) + assert.Equal("TestCommand", keyFacter.Id) } func TestCommandPluginName(t *testing.T) { - commandF := Command{Name: "TestCommand"} - assert.Equal(t, "command", commandF.PluginName()) + commandF := New("TestCommand") + assert.Equal(t, "command", commandF.GetName()) } func TestCommandSupportedConnections(t *testing.T) { - commandF := Command{Name: "TestCommand"} + commandF := New("TestCommand") supportLevel, connections := commandF.SupportedConnections() - assert.Equal(t, fact.SupportNone, supportLevel) + assert.Equal(t, plugin.SupportNone, supportLevel) assert.Empty(t, connections) } func TestCommandSupportedInputs(t *testing.T) { - commandF := Command{Name: "TestCommand"} - supportLevel, inputs := commandF.SupportedInputs() - assert.Equal(t, fact.SupportNone, supportLevel) + commandF := New("TestCommand") + supportLevel, inputs := commandF.SupportedInputFormats() + assert.Equal(t, plugin.SupportNone, supportLevel) assert.ElementsMatch(t, []string{}, inputs) } func TestCommandCollect(t *testing.T) { tests := []internal.FactCollectTest{ { - Name: "emptyCommand", - Facter: &Command{Name: "TestCommand"}, + Name: "emptyCommand", + Facter: New("TestCommand"), + ExpectedFormat: data.FormatMapString, ExpectedData: map[string]string{ "code": "1", "stderr": "exec: no command", "stdout": "", }, ExpectedErrors: []error{errors.New("exec: no command")}, }, { - Name: "emptyCommand/ignoreError", - Facter: &Command{Name: "TestCommand", IgnoreError: true}, + Name: "emptyCommand/ignoreError", + FactFn: func() fact.Facter { + f := New("TestCommand") + f.IgnoreError = true + return f + }, + ExpectedFormat: data.FormatMapString, ExpectedData: map[string]string{ "code": "1", "stderr": "exec: no command", "stdout": "", }, }, { - Name: "echo", - Facter: &Command{Name: "TestCommand", Cmd: "echo", Args: []string{"hello"}}, + Name: "echo", + FactFn: func() fact.Facter { + f := New("TestCommand") + f.Cmd = "echo" + f.Args = []string{"hello"} + return f + }, + ExpectedFormat: data.FormatMapString, ExpectedData: map[string]string{ "code": "0", "stderr": "", "stdout": "hello", }, }, { - Name: "multiline", - Facter: &Command{Name: "TestCommand", Cmd: "ls", Args: []string{"-A1"}}, + Name: "multiline", + FactFn: func() fact.Facter { + f := New("TestCommand") + f.Cmd = "ls" + f.Args = []string{"-1"} + return f + }, + ExpectedFormat: data.FormatMapString, ExpectedData: map[string]string{ - "code": "0", "stderr": "", "stdout": "command.go\ncommand_gen.go\ncommand_test.go", + "code": "0", "stderr": "", "stdout": "command.go\ncommand_test.go", }, }, } diff --git a/pkg/fact/database/search.go b/pkg/fact/database/search.go index cabf48e..fed2de3 100644 --- a/pkg/fact/database/search.go +++ b/pkg/fact/database/search.go @@ -12,21 +12,12 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) // Search searches the provided text from all tables of a database. type Search struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - connection connection.Connectioner - input fact.Facter - additionalInputs []fact.Facter - errors []error - data interface{} + fact.BaseFact `yaml:",inline"` // Plugin fields. Tables map[string][]string `yaml:"tables"` @@ -34,33 +25,40 @@ type Search struct { IdField string `yaml:"id-field"` } -//go:generate go run ../../../cmd/gen.go fact-plugin --plugin=Search --package=database +//go:generate go run ../../../cmd/gen.go fact-plugin --package=database func init() { - fact.Registry["database:search"] = func(n string) fact.Facter { - return &Search{Name: n, Format: data.FormatMapNestedString} - } + fact.Manager().RegisterFactory("database:search", func(n string) fact.Facter { + return New(n) + }) } -func (p *Search) PluginName() string { - return "database:search" +func New(id string) *Search { + return &Search{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + Format: data.FormatMapNestedString, + }, + } } -func (p *Search) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportRequired, []string{"mysql"} +func (p *Search) GetName() string { + return "database:search" } -func (p *Search) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func (p *Search) SupportedConnections() (plugin.SupportLevel, []string) { + return plugin.SupportRequired, []string{"mysql"} } func (p *Search) Collect() { if p.IdField == "" { - p.errors = append(p.errors, fmt.Errorf("id-field is required")) + p.AddErrors(fmt.Errorf("id-field is required")) return } - conn := p.connection.(*connection.Mysql) + conn := p.GetConnection().(*connection.Mysql) log.WithField("mysqlConn", conn).Debug("collecting data") if len(p.Tables) == 0 { @@ -73,7 +71,7 @@ func (p *Search) Collect() { // Execute the connection to get the db instance. if _, err := conn.Run(); err != nil { - p.errors = append(p.errors, err) + p.AddErrors(err) return } @@ -91,7 +89,7 @@ func (p *Search) Collect() { goqu.C(col).Like(p.Search)).ScanVals(&ids); err != nil { log.WithField("err", fmt.Sprintf("%#v", err)).Trace("failed to search") if mErr, ok := err.(*mysql.MySQLError); !ok || mErr.Message != unknownColMsg { - p.errors = append(p.errors, err) + p.AddErrors(err) continue } } @@ -106,7 +104,7 @@ func (p *Search) Collect() { } log.WithField("occurrences", fmt.Sprintf("%+v", occurrences)).Trace("search done") - p.data = occurrences + p.SetData(occurrences) } // fetchTablesColumns fetches list of tables and columns from the information_schema db. @@ -122,7 +120,7 @@ func (p *Search) fetchTablesColumns(conn connection.Mysql) error { // Execute the connection to get the db instance. if _, err := conn.Run(); err != nil { - p.errors = append(p.errors, err) + p.AddErrors(err) return err } @@ -131,7 +129,7 @@ func (p *Search) fetchTablesColumns(conn connection.Mysql) error { goqu.C("table_schema").Eq(origDb), goqu.C("data_type").In([]string{"char", "varchar", "longtext", "longblob"}), )).ScanStructs(&tablesCols); err != nil { - p.errors = append(p.errors, err) + p.AddErrors(err) return err } diff --git a/pkg/fact/docker/dockercommand.go b/pkg/fact/docker/dockercommand.go index 59396bc..f09b8e3 100644 --- a/pkg/fact/docker/dockercommand.go +++ b/pkg/fact/docker/dockercommand.go @@ -9,54 +9,53 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" "github.com/salsadigitalauorg/shipshape/pkg/utils" ) type DockerCommand struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - connection connection.Connectioner - input fact.Facter - additionalInputs []fact.Facter - errors []error - data interface{} + fact.BaseFact `yaml:",inline"` // Plugin fields. Command []string `yaml:"command"` AsList bool `yaml:"as-list"` } -//go:generate go run ../../../cmd/gen.go fact-plugin --plugin=DockerCommand --package=docker +//go:generate go run ../../../cmd/gen.go fact-plugin --package=docker func init() { - fact.Registry["docker:command"] = func(n string) fact.Facter { return &DockerCommand{Name: n} } + fact.Manager().RegisterFactory("docker:command", func(n string) fact.Facter { + return NewDockerCommand(n) + }) } -func (p *DockerCommand) PluginName() string { - return "docker:command" +func NewDockerCommand(id string) *DockerCommand { + return &DockerCommand{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + }, + } } -func (p *DockerCommand) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportRequired, []string{"docker:exec"} +func (p *DockerCommand) GetName() string { + return "docker:command" } -func (p *DockerCommand) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func (p *DockerCommand) SupportedConnections() (plugin.SupportLevel, []string) { + return plugin.SupportRequired, []string{"docker:exec"} } func (p *DockerCommand) Collect() { log.WithFields(log.Fields{ - "fact-plugin": p.PluginName(), - "fact": p.Name, + "fact-plugin": p.GetName(), + "fact": p.GetId(), "connection": p.GetConnectionName(), - "connection-plugin": p.connection.PluginName(), + "connection-plugin": p.GetConnection().GetName(), }).Debug("collecting data") - dockerConn := p.connection.(*connection.DockerExec) + dockerConn := p.GetConnection().(*connection.DockerExec) dockerConn.Command = p.Command rawData, err := dockerConn.Run() if err != nil { @@ -68,16 +67,16 @@ func (p *DockerCommand) Collect() { } err = errors.New(errMsg) log.WithError(err).Error("docker command failed") - p.errors = append(p.errors, err) + p.AddErrors(err) return } if !p.AsList { p.Format = data.FormatRaw - p.data = rawData + p.SetData(rawData) return } p.Format = data.FormatListString - p.data = utils.MultilineOutputToSlice(rawData) + p.SetData(utils.MultilineOutputToSlice(rawData)) } diff --git a/pkg/fact/docker/images.go b/pkg/fact/docker/images.go index 4a80fcb..9d98dc5 100644 --- a/pkg/fact/docker/images.go +++ b/pkg/fact/docker/images.go @@ -5,25 +5,15 @@ import ( log "github.com/sirupsen/logrus" - "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/docker" "github.com/salsadigitalauorg/shipshape/pkg/env" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) type Images struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - connection connection.Connectioner - input fact.Facter - additionalInputs []fact.Facter - errors []error - data interface{} + fact.BaseFact `yaml:",inline"` // Plugin fields. NoTag bool `yaml:"no-tag"` @@ -34,26 +24,28 @@ type Images struct { Ignore []string `yaml:"ignore"` } -//go:generate go run ../../../cmd/gen.go fact-plugin --plugin=Images --package=docker - func init() { - fact.Registry["docker:images"] = func(n string) fact.Facter { return &Images{Name: n} } + fact.Manager().RegisterFactory("docker:images", func(n string) fact.Facter { + return NewImages(n) + }) } -func (p *Images) PluginName() string { - return "docker:images" +func NewImages(id string) *Images { + return &Images{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + }, + } } -func (p *Images) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func (p *Images) GetName() string { + return "docker:images" } -func (p *Images) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportRequired, []string{ - "file:read", - "file:lookup", - "file:read:multiple", - } +func (p *Images) SupportedInputFormats() (plugin.SupportLevel, []data.DataFormat) { + return plugin.SupportRequired, []data.DataFormat{data.FormatMapBytes} } func (p *Images) resolveIgnore(envMapMap map[string]map[string]string) { @@ -70,11 +62,11 @@ func (p *Images) resolveIgnore(envMapMap map[string]map[string]string) { resI, err = env.ResolveValue(envMap, i) if err != nil { log.WithFields(log.Fields{ - "fact-plugin": p.PluginName(), - "fact": p.Name, + "fact-plugin": p.GetName(), + "fact": p.GetId(), "error": err, }).Error("could not resolve ignore value") - p.errors = append(p.errors, err) + p.AddErrors(err) return } if resI != i { @@ -87,28 +79,34 @@ func (p *Images) resolveIgnore(envMapMap map[string]map[string]string) { } func (p *Images) Collect() { - log.WithFields(log.Fields{ - "fact-plugin": p.PluginName(), - "fact": p.Name, - "input": p.GetInputName(), - "input-plugin": p.input.PluginName(), + contextLogger := log.WithFields(log.Fields{ + "fact-plugin": p.GetName(), + "fact": p.GetId(), + }) + + contextLogger.WithFields(log.Fields{ + "input": p.GetInputName(), + "input-plugin": p.GetInput().GetName(), + "additional-inputs": p.GetAdditionalInputNames(), + "no-tag": p.NoTag, + "ignore": p.Ignore, }).Debug("collecting data") var fileBytesMap map[string][]byte - switch p.input.GetFormat() { + switch p.GetInput().GetFormat() { case data.FormatMapBytes: - inputData := data.AsMapBytes(p.input.GetData()) + inputData := data.AsMapBytes(p.GetInput().GetData()) if inputData == nil { return } fileBytesMap = inputData default: - p.errors = append(p.errors, &fact.ErrSupportNone{ - Plugin: p.Name, - SupportType: "input data format", - SupportPlugin: string(p.input.GetFormat())}) + p.AddErrors(&plugin.ErrSupportNone{ + Plugin: p.GetName(), + SupportType: "inputFormat", + SupportPlugin: string(p.GetInput().GetFormat())}) } if fileBytesMap == nil { @@ -117,20 +115,22 @@ func (p *Images) Collect() { envMap := map[string]map[string]string{} if p.ArgsFrom != "" { - if p.additionalInputs == nil { - p.errors = append(p.errors, &fact.ErrSupportRequired{ - Plugin: p.Name, SupportType: "additional inputs"}) + contextLogger.WithField("argsFrom", p.ArgsFrom).Debug("resolving env") + if len(p.GetAdditionalInputNames()) == 0 { + p.AddErrors(&plugin.ErrSupportRequired{ + Plugin: p.GetName(), SupportType: "additionalInputs"}) return } - for _, i := range p.additionalInputs { - if i.GetName() == p.ArgsFrom { + for _, i := range p.GetAdditionalInputs() { + if i.GetId() == p.ArgsFrom { envMap = data.AsMapNestedString(i.GetData()) break } } - } + contextLogger.WithField("envMap", envMap).Debug("resolved env") + } p.resolveIgnore(envMap) baseImagesMap := map[string][]string{} @@ -138,7 +138,7 @@ func (p *Images) Collect() { baseImages, err := docker.Parse(fBytes, envMap[fn], p.NoTag, p.Ignore) if err != nil { log.WithField("error", err).Error("could not parse Dockerfile") - p.errors = append(p.errors, err) + p.AddErrors(err) return } @@ -148,9 +148,9 @@ func (p *Images) Collect() { baseImagesMap[fn] = append(baseImagesMap[fn], bi.String()) } - p.data = baseImagesMap + p.SetData(baseImagesMap) log.WithFields(log.Fields{ - "fact": p.Name, + "fact": p.GetId(), "baseImages": fmt.Sprintf("%+v", baseImagesMap), }).Debug("parsed Dockerfile") } diff --git a/pkg/fact/docker/images_test.go b/pkg/fact/docker/images_test.go index 81ce814..bed4c80 100644 --- a/pkg/fact/docker/images_test.go +++ b/pkg/fact/docker/images_test.go @@ -7,27 +7,40 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/fact" . "github.com/salsadigitalauorg/shipshape/pkg/fact/docker" "github.com/salsadigitalauorg/shipshape/pkg/internal" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) func TestImagesCollect(t *testing.T) { tests := []internal.FactCollectTest{ { - Name: "noInput", - Facter: &Images{Name: "base-images", InputName: "test-input"}, - ExpectedInputError: &fact.ErrSupportRequired{SupportType: "input"}, + Name: "noInput", + FactFn: func() fact.Facter { + f := NewImages("base-images") + f.SetInputName("test-input") + return f + }, + ExpectedInputError: &plugin.ErrSupportRequired{SupportType: "input"}, }, { - Name: "inputFormatUnsupported", - Facter: &Images{Name: "base-images", InputName: "test-input"}, + Name: "inputFormatUnsupported", + FactFn: func() fact.Facter { + f := NewImages("base-images") + f.SetInputName("test-input") + return f + }, TestInput: internal.FactInputTest{DataFormat: data.FormatRaw, Data: []byte("foo")}, - ExpectedErrors: []error{&fact.ErrSupportNone{ + ExpectedInputError: &plugin.ErrSupportNone{ Plugin: "base-images", SupportType: "input data format", - SupportPlugin: "raw"}}, + SupportPlugin: "raw"}, }, { - Name: "bogusData", - Facter: &Images{Name: "base-images", InputName: "test-input"}, + Name: "bogusData", + FactFn: func() fact.Facter { + f := NewImages("base-images") + f.SetInputName("test-input") + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"foo": []byte("bar")}}, @@ -35,8 +48,12 @@ func TestImagesCollect(t *testing.T) { ExpectedData: map[string][]string{"foo": {}}, }, { - Name: "dockerfile/simple", - Facter: &Images{Name: "base-images", InputName: "test-input"}, + Name: "dockerfile/simple", + FactFn: func() fact.Facter { + f := NewImages("base-images") + f.SetInputName("test-input") + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"Dockerfile": []byte("FROM scratch\n")}, @@ -45,8 +62,12 @@ func TestImagesCollect(t *testing.T) { ExpectedData: map[string][]string{"Dockerfile": {"scratch:latest"}}, }, { - Name: "dockerfile/withArgs", - Facter: &Images{Name: "base-images", InputName: "test-input"}, + Name: "dockerfile/withArgs", + FactFn: func() fact.Facter { + f := NewImages("base-images") + f.SetInputName("test-input") + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"php": []byte(`ARG CLI_IMAGE @@ -59,12 +80,13 @@ FROM php:${PHP_IMAGE_VERSION} ExpectedData: map[string][]string{"php": {":latest", "php:8.3"}}, }, { - Name: "dockerfile/withArgsWithArgsInput/NoDataFormat", - Facter: &Images{ - Name: "base-images", - InputName: "test-input", - ArgsFrom: "args-input", - AdditionalInputNames: []string{"args-input"}, + Name: "dockerfile/withArgs/WithAdditionalInput/NoDataFormat", + FactFn: func() fact.Facter { + f := NewImages("base-images") + f.SetInputName("test-input") + f.ArgsFrom = "args-input" + f.AdditionalInputNames = []string{"args-input"} + return f }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, @@ -80,17 +102,18 @@ FROM php:${PHP_IMAGE_VERSION} Data: map[string]map[string]string{"php": {"CLI_IMAGE": "myapp"}}, }, }, - ExpectedAdditionalInputsErrs: []error{&fact.ErrSupportRequired{ - Plugin: "args-input", - SupportType: "additional input data format"}}, + ExpectedAdditionalInputsErrs: []error{&plugin.ErrSupportRequired{ + Plugin: "testdata:testfacter", + SupportType: "inputFormat"}}, }, { - Name: "dockerfile/withArgsWithArgsInput", - Facter: &Images{ - Name: "base-images", - InputName: "test-input", - ArgsFrom: "args-input", - AdditionalInputNames: []string{"args-input"}, + Name: "dockerfile/withArgs/WithAdditionalInput", + FactFn: func() fact.Facter { + f := NewImages("base-images") + f.SetInputName("test-input") + f.ArgsFrom = "args-input" + f.AdditionalInputNames = []string{"args-input"} + return f }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, diff --git a/pkg/fact/facter.go b/pkg/fact/facter.go deleted file mode 100644 index b50ef73..0000000 --- a/pkg/fact/facter.go +++ /dev/null @@ -1,136 +0,0 @@ -package fact - -import ( - log "github.com/sirupsen/logrus" - - "github.com/salsadigitalauorg/shipshape/pkg/connection" -) - -func ValidatePluginConnection(p Facter) (connection.Connectioner, error) { - connectionSupport, supportedConnections := p.SupportedConnections() - log.WithFields(log.Fields{ - "fact": p.GetName(), - "connection-support": connectionSupport, - "supported-connections": supportedConnections, - }).Debug("validating connection") - - if (connectionSupport == SupportOptional || - connectionSupport == SupportNone) && - len(supportedConnections) == 0 && p.GetConnectionName() == "" { - return nil, nil - } - - if connectionSupport == SupportRequired && p.GetConnectionName() == "" { - return nil, &ErrSupportRequired{ - Plugin: p.GetName(), SupportType: "connection"} - } - - plugin := connection.GetInstance(p.GetConnectionName()) - if plugin == nil { - return nil, &ErrSupportNotFound{ - Plugin: p.GetName(), - SupportType: "connection", - SupportPlugin: p.GetConnectionName()} - } - - for _, s := range supportedConnections { - if plugin.PluginName() == s { - return plugin, nil - } - } - return nil, &ErrSupportNone{ - Plugin: p.PluginName(), - SupportType: "connection", - SupportPlugin: plugin.PluginName()} -} - -func ValidatePluginInput(p Facter) (Facter, error) { - inputSupport, supportedInputs := p.SupportedInputs() - log.WithFields(log.Fields{ - "fact": p.GetName(), - "input-support": inputSupport, - "supported-inputs": supportedInputs, - }).Debug("validating input") - - if (inputSupport == SupportOptional || - inputSupport == SupportNone) && - len(supportedInputs) == 0 && p.GetInputName() == "" { - return nil, nil - } - - if inputSupport == SupportRequired && p.GetInputName() == "" { - return nil, &ErrSupportRequired{Plugin: p.GetName(), SupportType: "input"} - } - - if p.GetInputName() != "" { - plugin := GetInstance(p.GetInputName()) - if plugin == nil { - return nil, &ErrSupportNotFound{ - Plugin: p.GetName(), - SupportType: "input", - SupportPlugin: p.GetInputName()} - } - - log.WithFields(log.Fields{ - "fact": p.GetName(), - "input-plugin": plugin.GetName(), - "input-plugin-format": plugin.GetFormat(), - }).Debug("found input plugin") - - if plugin.GetFormat() == "" { - return nil, &ErrSupportRequired{ - Plugin: plugin.GetName(), SupportType: "input data format"} - } - - for _, s := range supportedInputs { - if plugin.PluginName() == s { - return plugin, nil - } - } - - return nil, &ErrSupportNone{ - SupportType: "input", - SupportPlugin: plugin.PluginName(), - Plugin: p.PluginName(), - } - } - - return nil, &ErrSupportNotFound{ - SupportType: "input", - Plugin: p.GetName(), - SupportPlugin: p.GetInputName()} -} - -func LoadPluginAdditionalInputs(p Facter) ([]Facter, []error) { - log.WithFields(log.Fields{"fact": p.GetName()}). - Debug("loading additional inputs") - - if len(p.GetAdditionalInputNames()) == 0 { - return nil, nil - } - - plugins := []Facter{} - errs := []error{} - for _, n := range p.GetAdditionalInputNames() { - plugin := GetInstance(n) - if plugin == nil { - errs = append(errs, &ErrSupportNotFound{ - Plugin: p.GetName(), - SupportType: "additional input", - SupportPlugin: n, - }) - continue - } - - if plugin.GetFormat() == "" { - errs = append(errs, &ErrSupportRequired{ - Plugin: plugin.GetName(), - SupportType: "additional input data format"}) - continue - } - - plugins = append(plugins, plugin) - } - - return plugins, errs -} diff --git a/pkg/fact/facts.go b/pkg/fact/facts.go deleted file mode 100644 index 8936f7d..0000000 --- a/pkg/fact/facts.go +++ /dev/null @@ -1,157 +0,0 @@ -package fact - -import ( - "sort" - - log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" - - "github.com/salsadigitalauorg/shipshape/pkg/breach" - "github.com/salsadigitalauorg/shipshape/pkg/utils" -) - -var Registry = map[string]func(string) Facter{} - -// OnlyFactNames is a list of fact names to collect. -// If empty, all facts are collected. -var OnlyFactNames = []string{} - -var Facts = map[string]Facter{} -var Errors = []error{} -var collected = []string{} - -func init() { - breach.TemplateFuncs["lookupFactAsStringMap"] = func(inputName string, key string) string { - input := GetInstance(inputName) - if input == nil { - return "" - } - ifcMap := input.GetData().(map[string]interface{}) - val, ok := ifcMap[key] - if !ok { - return "" - } - return val.(string) - } -} - -func GetInstance(name string) Facter { - if p, ok := Facts[name]; !ok { - return nil - } else { - return p - } -} - -func RegistryKeys() []string { - keys := []string{} - for k := range Registry { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -func ParseConfig(raw map[string]map[string]interface{}) { - count := 0 - log.WithField("registry", RegistryKeys()).Debug("available fact plugins") - for name, pluginConf := range raw { - for pluginName, pluginMap := range pluginConf { - f, ok := Registry[pluginName] - if !ok { - continue - } - - p := f(name) - pluginYaml, err := yaml.Marshal(pluginMap) - if err != nil { - panic(err) - } - - err = yaml.Unmarshal(pluginYaml, p) - if err != nil { - panic(err) - } - - log.WithFields(log.Fields{ - "fact": p.GetName(), - "plugin": pluginName, - }).Debug("parsed fact config") - Facts[name] = p - count++ - } - } - log.Infof("parsed %d facts", count) -} - -func CollectAllFacts() { - for name, p := range Facts { - if len(OnlyFactNames) > 0 && - !utils.StringSliceContains(OnlyFactNames, name) { - continue - } - CollectFact(name, p) - } -} - -func CollectFact(name string, f Facter) { - log.WithField("fact", name).Debug("starting CollectFact process") - var inputF Facter - if f.GetInputName() != "" { - log.WithField("fact", name). - WithField("inputName", f.GetInputName()). - Debug("collect input") - inputF = GetInstance(f.GetInputName()) - CollectFact(f.GetInputName(), inputF) - } - - if len(f.GetAdditionalInputNames()) > 0 { - for _, n := range f.GetAdditionalInputNames() { - log.WithField("fact", name). - WithField("additionalInputName", n). - Debug("collect additional input") - CollectFact(n, GetInstance(n)) - } - } - - if inputF != nil && len(inputF.GetErrors()) > 0 { - return - } - - if utils.StringSliceContains(collected, name) { - return - } - - if err := f.ValidateConnection(); err != nil { - Errors = append(Errors, err) - log.WithField("fact", name).WithError(err). - Error("failed to validate connection") - return - } - - if err := f.ValidateInput(); err != nil { - Errors = append(Errors, err) - log.WithField("fact", name).WithError(err). - Error("failed to validate input") - return - } - - if errs := f.LoadAdditionalInputs(); len(errs) != 0 { - Errors = append(Errors, errs...) - log.WithField("fact", name).WithField("errors", errs). - Error("failed to load additional input") - return - } - - log.WithField("fact", name).Info("collecting fact") - f.Collect() - if len(f.GetErrors()) > 0 { - Errors = append(Errors, f.GetErrors()...) - } - - log.WithFields(log.Fields{ - "fact": name, - "data": f.GetData(), - }).Trace("collected fact") - collected = append(collected, name) -} diff --git a/pkg/fact/file/lookup.go b/pkg/fact/file/lookup.go index 68efaee..98184de 100644 --- a/pkg/fact/file/lookup.go +++ b/pkg/fact/file/lookup.go @@ -7,24 +7,14 @@ import ( log "github.com/sirupsen/logrus" "github.com/salsadigitalauorg/shipshape/pkg/config" - "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" "github.com/salsadigitalauorg/shipshape/pkg/utils" ) type Lookup struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - connection connection.Connectioner - input fact.Facter - additionalInputs []fact.Facter - errors []error - data interface{} + fact.BaseFact `yaml:",inline"` // Plugin fields. Path string `yaml:"path"` @@ -34,42 +24,51 @@ type Lookup struct { SkipDirs []string `yaml:"skip-dirs"` } -//go:generate go run ../../../cmd/gen.go fact-plugin --plugin=Lookup --package=file +//go:generate go run ../../../cmd/gen.go fact-plugin --package=file func init() { - fact.Registry["file:lookup"] = func(n string) fact.Facter { - return &Lookup{Name: n, FileNamesOnly: true} - } -} - -func (p *Lookup) PluginName() string { - return "file:lookup" + fact.Manager().RegisterFactory("file:lookup", func(n string) fact.Facter { + return NewLookup(n) + }) } -func (p *Lookup) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func NewLookup(id string) *Lookup { + return &Lookup{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + }, + FileNamesOnly: true, + } } -func (p *Lookup) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func (p *Lookup) GetName() string { + return "file:lookup" } func (p *Lookup) Collect() { - log.WithFields(log.Fields{ - "fact": p.Name, + contextLogger := log.WithFields(log.Fields{ + "fact-plugin": p.GetName(), + "fact": p.GetId(), + }) + + contextLogger.WithFields(log.Fields{ "project-dir": config.ProjectDir, "path": p.Path, "pattern": p.Pattern, - }).Info("looking up files") + }).Debug("looking up files") + files, err := utils.FindFiles(filepath.Join(config.ProjectDir, p.Path), p.Pattern, p.ExcludePattern, p.SkipDirs) if err != nil { - p.errors = append(p.errors, err) + contextLogger.WithError(err).Error("error looking up files") + p.AddErrors(err) return } if p.FileNamesOnly { p.Format = data.FormatListString - p.data = files + p.SetData(files) return } @@ -77,11 +76,12 @@ func (p *Lookup) Collect() { for _, f := range files { fData, err := os.ReadFile(f) if err != nil { - p.errors = append(p.errors, err) + contextLogger.WithError(err).Error("error reading file") + p.AddErrors(err) continue } filesDataMap[f] = fData } p.Format = data.FormatMapBytes - p.data = filesDataMap + p.SetData(filesDataMap) } diff --git a/pkg/fact/file/read.go b/pkg/fact/file/read.go index b76a87a..3e8a2c6 100644 --- a/pkg/fact/file/read.go +++ b/pkg/fact/file/read.go @@ -8,65 +8,62 @@ import ( log "github.com/sirupsen/logrus" "github.com/salsadigitalauorg/shipshape/pkg/config" - "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) type Read struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - connection connection.Connectioner - input fact.Facter - additionalInputs []fact.Facter - errors []error - data interface{} + fact.BaseFact `yaml:",inline"` // Plugin fields. Path string `yaml:"path"` } -//go:generate go run ../../../cmd/gen.go fact-plugin --plugin=Read --package=file - func init() { - fact.Registry["file:read"] = func(n string) fact.Facter { - return &Read{Name: n, Format: data.FormatRaw} - } -} - -func (p *Read) PluginName() string { - return "file:read" + fact.Manager().RegisterFactory("file:read", func(n string) fact.Facter { + return NewRead(n) + }) } -func (p *Read) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportNone, nil +func NewRead(id string) *Read { + return &Read{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + Format: data.FormatRaw, + }, + } } -func (p *Read) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportNone, nil +func (p *Read) GetName() string { + return "file:read" } func (p *Read) Collect() { - log.WithFields(log.Fields{ - "fact": p.Name, + contextLogger := log.WithFields(log.Fields{ + "fact-plugin": p.GetName(), + "fact": p.GetId(), + }) + + contextLogger.WithFields(log.Fields{ "project-dir": config.ProjectDir, "path": p.Path, - }).Info("verifying file existence") + }).Debug("verifying file existence") fullpath := filepath.Join(config.ProjectDir, p.Path) if _, err := os.Stat(fullpath); errors.Is(err, os.ErrNotExist) { - p.errors = append(p.errors, err) + contextLogger.WithError(err).Debug("file does not exist") + p.AddErrors(err) return } fData, err := os.ReadFile(fullpath) if err != nil { - p.errors = append(p.errors, err) + contextLogger.WithError(err).Debug("error reading file") + p.AddErrors(err) return } - p.data = fData + p.SetData(fData) } diff --git a/pkg/fact/file/readmultiple.go b/pkg/fact/file/readmultiple.go index 5ea51f9..635f4e8 100644 --- a/pkg/fact/file/readmultiple.go +++ b/pkg/fact/file/readmultiple.go @@ -8,23 +8,13 @@ import ( log "github.com/sirupsen/logrus" "github.com/salsadigitalauorg/shipshape/pkg/config" - "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) type ReadMultiple struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - connection connection.Connectioner - input fact.Facter - additionalInputs []fact.Facter - errors []error - data interface{} + fact.BaseFact `yaml:",inline"` // Plugin fields. Files []string `yaml:"files"` @@ -33,54 +23,66 @@ type ReadMultiple struct { //go:generate go run ../../../cmd/gen.go fact-plugin --plugin=ReadMultiple --package=file func init() { - fact.Registry["file:read:multiple"] = func(n string) fact.Facter { - return &ReadMultiple{Name: n, Format: data.FormatMapBytes} - } + fact.Manager().RegisterFactory("file:read:multiple", func(n string) fact.Facter { + return NewReadMultiple(n) + }) } -func (p *ReadMultiple) PluginName() string { - return "file:read:multiple" +func NewReadMultiple(id string) *ReadMultiple { + return &ReadMultiple{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + }, + } } -func (p *ReadMultiple) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportNone, nil +func (p *ReadMultiple) GetName() string { + return "file:read:multiple" } -func (p *ReadMultiple) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportOptional, []string{"yaml:key"} +func (p *ReadMultiple) SupportedInputFormats() (plugin.SupportLevel, []data.DataFormat) { + return plugin.SupportOptional, []data.DataFormat{data.FormatMapString} } func (p *ReadMultiple) Collect() { - log.WithFields(log.Fields{ - "fact": p.Name, - "project-dir": config.ProjectDir, - }).Info("collecting files data") + contextLogger := log.WithFields(log.Fields{ + "fact-plugin": p.GetName(), + "fact": p.GetId(), + }) + + contextLogger.WithFields(log.Fields{"project-dir": config.ProjectDir}). + Debug("collecting files data") - if p.input == nil && len(p.Files) == 0 { - p.errors = append(p.errors, errors.New("no files specified")) + if p.GetInput() == nil && len(p.Files) == 0 { + contextLogger.Error("no files specified") + p.AddErrors(errors.New("no files specified")) return } - if p.input != nil { - switch p.input.GetFormat() { + if p.GetInput() != nil { + switch p.GetInput().GetFormat() { case data.FormatMapString: p.Format = data.FormatMapBytes res := map[string][]byte{} - filenameMap := data.AsMapString(p.input.GetData()) + filenameMap := data.AsMapString(p.GetInput().GetData()) for k, filename := range filenameMap { fullpath := filepath.Join(config.ProjectDir, filename) if _, err := os.Stat(fullpath); errors.Is(err, os.ErrNotExist) { - p.errors = append(p.errors, err) + contextLogger.WithError(err).Debug("file does not exist") + p.AddErrors(err) continue } fData, err := os.ReadFile(fullpath) if err != nil { - p.errors = append(p.errors, err) + contextLogger.WithError(err).Debug("error reading file") + p.AddErrors(err) continue } res[k] = fData } - p.data = res + p.SetData(res) return } } @@ -90,16 +92,18 @@ func (p *ReadMultiple) Collect() { for _, filename := range p.Files { fullpath := filepath.Join(config.ProjectDir, filename) if _, err := os.Stat(fullpath); errors.Is(err, os.ErrNotExist) { - p.errors = append(p.errors, err) + contextLogger.WithError(err).Debug("file does not exist") + p.AddErrors(err) continue } fData, err := os.ReadFile(fullpath) if err != nil { - p.errors = append(p.errors, err) + contextLogger.WithError(err).Debug("error reading file") + p.AddErrors(err) continue } res[filename] = fData } - p.data = res + p.SetData(res) } diff --git a/pkg/fact/manager.go b/pkg/fact/manager.go new file mode 100644 index 0000000..eac4b60 --- /dev/null +++ b/pkg/fact/manager.go @@ -0,0 +1,162 @@ +package fact + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + "github.com/salsadigitalauorg/shipshape/pkg/breach" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" + "github.com/salsadigitalauorg/shipshape/pkg/pluginmanager" + "github.com/salsadigitalauorg/shipshape/pkg/utils" +) + +// manager handles fact plugin registration and lifecycle. +type manager struct { + *pluginmanager.Manager[Facter] + // collected is a list of fact names that have already been collected. + collected []string +} + +var m *manager + +// OnlyFactNames is a list of fact names to collect. +// If empty, all facts are collected. +var OnlyFactNames = []string{} + +// Manager returns the fact manager. +func Manager() *manager { + if m == nil { + // Add a template function to lookup a fact as a string map. + breach.TemplateFuncs["lookupFactAsStringMap"] = func(inputName string, key string) string { + input := Manager().FindPlugin(inputName) + if input == nil { + return "" + } + ifcMap := input.GetData().(map[string]interface{}) + val, ok := ifcMap[key] + if !ok { + return "" + } + return val.(string) + } + + m = &manager{ + Manager: pluginmanager.NewManager[Facter](), + } + } + return m +} + +func (m *manager) GetFactoriesKeys() []string { + return plugin.GetFactoriesKeys[Facter](m.GetFactories()) +} + +// ParseConfig parses the raw config and creates the facts. +func (m *manager) ParseConfig(raw map[string]map[string]interface{}) error { + count := 0 + log.WithField("registry", m.GetFactoriesKeys()).Debug("available fact plugins") + for id, pluginConf := range raw { + for pluginName, pluginIf := range pluginConf { + log.WithField("pluginIf", pluginIf).Trace("parsing fact config") + p, err := Manager().GetPlugin(pluginName, id) + if err != nil { + return err + } + + // Convert the map to yaml, then parse it into the plugin. + // Not catching any errors when marshalling since the yaml content is known. + pluginYaml, _ := yaml.Marshal(pluginIf) + err = yaml.Unmarshal(pluginYaml, p) + if err != nil { + return err + } + + log.WithFields(log.Fields{ + "id": p.GetId(), + "plugin": pluginName, + }).Debug("parsed fact") + + log.WithField("fact", fmt.Sprintf("%#v", p)).Trace("parsed fact") + count++ + } + } + log.Infof("parsed %d facts", count) + return nil +} + +// CollectAllFacts collects all facts. +func (m *manager) CollectAllFacts() { + for name, p := range m.GetPlugins() { + if len(OnlyFactNames) > 0 && + !utils.StringSliceContains(OnlyFactNames, name) { + continue + } + m.CollectFact(name, p) + } +} + +// CollectFact collects a fact. +func (m *manager) CollectFact(name string, f Facter) { + log.WithField("fact", name).Debug("starting CollectFact process") + var inputF Facter + if f.GetInputName() != "" { + log.WithField("fact", name). + WithField("inputName", f.GetInputName()). + Debug("collect input") + inputF = m.FindPlugin(f.GetInputName()) + m.CollectFact(f.GetInputName(), inputF) + } + + if len(f.GetAdditionalInputNames()) > 0 { + for _, n := range f.GetAdditionalInputNames() { + log.WithField("fact", name). + WithField("additionalInputName", n). + Debug("collect additional input") + inputF = m.FindPlugin(n) + m.CollectFact(n, inputF) + } + } + + if inputF != nil && len(inputF.GetErrors()) > 0 { + return + } + + if utils.StringSliceContains(m.collected, name) { + return + } + + if err := ValidateConnection(f); err != nil { + m.AddErrors(err) + log.WithField("fact", name).WithError(err). + Error("failed to validate connection") + return + } + + if err := ValidateInput(f); err != nil { + m.AddErrors(err) + log.WithField("fact", name).WithError(err). + Error("failed to validate input") + return + } + + if errs := LoadAdditionalInputs(f); len(errs) != 0 { + m.AddErrors(errs...) + log.WithField("fact", name).WithField("errors", errs). + Error("failed to load additional input") + return + } + + log.WithField("fact", name).Info("collecting fact") + f.Collect() + if len(f.GetErrors()) > 0 { + m.AddErrors(f.GetErrors()...) + } + + log.WithFields(log.Fields{ + "fact": name, + "data": f.GetData(), + }).Trace("collected fact") + m.collected = append(m.collected, name) +} diff --git a/pkg/fact/templates/factplugin.go.tmpl b/pkg/fact/templates/factplugin.go.tmpl deleted file mode 100644 index dabde51..0000000 --- a/pkg/fact/templates/factplugin.go.tmpl +++ /dev/null @@ -1,85 +0,0 @@ -package {{ .Package }} - -import ( - "github.com/salsadigitalauorg/shipshape/pkg/data" - {{ if .EnvResolver -}} - "github.com/salsadigitalauorg/shipshape/pkg/env" - {{ end -}} - "github.com/salsadigitalauorg/shipshape/pkg/fact" -) - -// Code generated by fact-plugin --plugin={{ .Plugin }} --package={{ .Package }}; DO NOT EDIT. - -func (p *{{ .Plugin }}) GetName() string { - return p.Name -} - -func (p *{{ .Plugin }}) GetData() interface{} { - return p.data -} - -func (p *{{ .Plugin }}) GetFormat() data.DataFormat { - return p.Format -} - -func (p *{{ .Plugin }}) GetConnectionName() string { - return p.ConnectionName -} - -func (p *{{ .Plugin }}) GetInputName() string { - return p.InputName -} - -func (p *{{ .Plugin }}) GetAdditionalInputNames() []string { - return p.AdditionalInputNames -} - -func (p *{{ .Plugin }}) GetErrors() []error { - if p.input != nil { - return append(p.errors, p.input.GetErrors()...) - } - return p.errors -} - -func (p *{{ .Plugin }}) ValidateConnection() error { - connPlugin, err := fact.ValidatePluginConnection(p) - if err != nil { - return err - } - p.connection = connPlugin - return nil -} - -func (p *{{ .Plugin }}) ValidateInput() error { - inputPlugin, err := fact.ValidatePluginInput(p) - if err != nil { - return err - } - - p.input = inputPlugin - return nil -} - -func (p *{{ .Plugin }}) LoadAdditionalInputs() []error { - additionalInputs, errs := fact.LoadPluginAdditionalInputs(p) - if len(errs) > 0 { - return errs - } - - p.additionalInputs = additionalInputs - return nil -} - -{{ if .EnvResolver -}} -func (p *{{ .Plugin }}) ShouldResolveEnv() bool { - return p.ResolveEnv -} - -func (p *{{ .Plugin }}) GetEnvFile() string { - return p.EnvFile -} - -func (p *{{ .Plugin }}) GetEnvMap() (map[string]string, error) { - return env.ReadEnvFile(p) -} -{{ end -}} diff --git a/pkg/fact/testdata/testfacter.go b/pkg/fact/testdata/testfacter.go index 85fba60..fe781c5 100644 --- a/pkg/fact/testdata/testfacter.go +++ b/pkg/fact/testdata/testfacter.go @@ -3,17 +3,11 @@ package testdata import ( "github.com/salsadigitalauorg/shipshape/pkg/data" "github.com/salsadigitalauorg/shipshape/pkg/fact" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) type TestFacter struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - errors []error - data interface{} + fact.BaseFact // Plugin fields. TestInputDataFormat data.DataFormat @@ -21,67 +15,28 @@ type TestFacter struct { } func init() { - fact.Registry["file:read"] = func(n string) fact.Facter { return &TestFacter{Name: n} } + fact.Manager().RegisterFactory("testdata:testfacter", func(n string) fact.Facter { + return New(n, data.FormatNil, nil) + }) } -func (p *TestFacter) PluginName() string { - return "file:read" +func New(id string, dataFormat data.DataFormat, data any) *TestFacter { + return &TestFacter{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + }, + TestInputDataFormat: dataFormat, + TestInputData: data, + } } -func (p *TestFacter) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} -} - -func (p *TestFacter) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func (p *TestFacter) GetName() string { + return "testdata:testfacter" } func (p *TestFacter) Collect() { p.Format = p.TestInputDataFormat - p.data = p.TestInputData -} - -// Generated methods. -func (p *TestFacter) GetName() string { - return p.Name -} - -func (p *TestFacter) GetData() interface{} { - return p.data -} - -func (p *TestFacter) GetFormat() data.DataFormat { - return p.Format -} - -func (p *TestFacter) GetConnectionName() string { - return p.ConnectionName -} - -func (p *TestFacter) GetInputName() string { - return p.InputName -} - -func (p *TestFacter) GetAdditionalInputNames() []string { - return p.AdditionalInputNames -} - -func (p *TestFacter) GetErrors() []error { - return p.errors -} - -func (p *TestFacter) ValidateConnection() error { - return &fact.ErrSupportNone{SupportType: "connection"} -} - -func (p *TestFacter) ValidateInput() error { - return &fact.ErrSupportNone{SupportType: "input"} -} - -func (p *TestFacter) LoadAdditionalInputs() []error { - return []error{} -} - -func (p *TestFacter) AddError(err error) { - p.errors = append(p.errors, err) + p.SetData(p.TestInputData) } diff --git a/pkg/fact/types.go b/pkg/fact/types.go index 7fae411..56d0c07 100644 --- a/pkg/fact/types.go +++ b/pkg/fact/types.go @@ -1,69 +1,35 @@ package fact import ( - "fmt" - + "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) +// Facter defines the interface for fact plugins type Facter interface { - // Common plugin methods. - PluginName() string - GetName() string + plugin.Plugin - // Fact methods. - GetErrors() []error - GetConnectionName() string - GetInputName() string - GetAdditionalInputNames() []string + // Data methods GetData() interface{} GetFormat() data.DataFormat - SupportedConnections() (SupportLevel, []string) - ValidateConnection() error - SupportedInputs() (SupportLevel, []string) - ValidateInput() error - LoadAdditionalInputs() []error - Collect() -} - -type SupportLevel string - -const ( - SupportRequired SupportLevel = "required" - SupportOptional SupportLevel = "optional" - SupportNone SupportLevel = "not-supported" -) - -type ErrSupportRequired struct { - Plugin string - SupportType string -} -func (m *ErrSupportRequired) Error() string { - return fmt.Sprintf("%s required for '%s'", m.SupportType, m.Plugin) -} - -type ErrSupportNotFound struct { - Plugin string - SupportType string - SupportPlugin string -} - -func (m *ErrSupportNotFound) Error() string { - return fmt.Sprintf("%s '%s' not found for '%s'", - m.SupportType, m.SupportPlugin, m.Plugin) -} + // Connection methods + GetConnectionName() string + GetConnection() connection.Connectioner + SetConnection(connection.Connectioner) + SupportedConnections() (plugin.SupportLevel, []string) -type ErrSupportNone struct { - Plugin string - SupportType string - SupportPlugin string -} + // Input methods + GetInputName() string + GetInput() Facter + GetAdditionalInputNames() []string + GetAdditionalInputs() []Facter + SetInputName(name string) + SetInput(Facter) + SupportedInputFormats() (plugin.SupportLevel, []data.DataFormat) + SetAdditionalInputs([]Facter) -func (m *ErrSupportNone) Error() string { - if m.SupportPlugin == "" { - return fmt.Sprintf("%s not supported for '%s'", m.SupportType, m.Plugin) - } - return fmt.Sprintf("%s '%s' not supported for '%s'", - m.SupportType, m.SupportPlugin, m.Plugin) + // Collection + Collect() } diff --git a/pkg/fact/yaml/key.go b/pkg/fact/yaml/key.go index 86a8da3..332920e 100644 --- a/pkg/fact/yaml/key.go +++ b/pkg/fact/yaml/key.go @@ -4,31 +4,19 @@ import ( "errors" log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" - "github.com/salsadigitalauorg/shipshape/pkg/connection" "github.com/salsadigitalauorg/shipshape/pkg/data" + "github.com/salsadigitalauorg/shipshape/pkg/env" "github.com/salsadigitalauorg/shipshape/pkg/fact" - "gopkg.in/yaml.v3" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) // Key looks up a key in a YAML file using the file:lookup or // yaml:key input plugins. type Key struct { - // Common fields. - Name string `yaml:"name"` - Format data.DataFormat `yaml:"format"` - ConnectionName string `yaml:"connection"` - InputName string `yaml:"input"` - AdditionalInputNames []string `yaml:"additional-inputs"` - connection connection.Connectioner - input fact.Facter - additionalInputs []fact.Facter - errors []error - data interface{} - - // Resolve env vars. - ResolveEnv bool `yaml:"resolve-env"` - EnvFile string `yaml:"env-file"` + fact.BaseFact `yaml:",inline"` + env.BaseEnvResolver `yaml:",inline"` // Plugin fields. Path string `yaml:"path"` @@ -40,26 +28,35 @@ type Key struct { IgnoreNotFound bool `yaml:"ignore-not-found"` } -//go:generate go run ../../../cmd/gen.go fact-plugin --plugin=Key --package=yaml --envresolver +//go:generate go run ../../../cmd/gen.go fact-plugin --package=yaml func init() { - fact.Registry["yaml:key"] = func(n string) fact.Facter { return &Key{Name: n} } + fact.Manager().RegisterFactory("yaml:key", func(n string) fact.Facter { + return New(n) + }) } -func (p *Key) PluginName() string { - return "yaml:key" +func New(id string) *Key { + return &Key{ + BaseFact: fact.BaseFact{ + BasePlugin: plugin.BasePlugin{ + Id: id, + }, + }, + } } -func (p *Key) SupportedConnections() (fact.SupportLevel, []string) { - return fact.SupportNone, []string{} +func (p *Key) GetName() string { + return "yaml:key" } -func (p *Key) SupportedInputs() (fact.SupportLevel, []string) { - return fact.SupportRequired, []string{ - "docker:command", - "file:read", - "file:lookup", - "yaml:key"} +func (p *Key) SupportedInputFormats() (plugin.SupportLevel, []data.DataFormat) { + return plugin.SupportRequired, []data.DataFormat{ + data.FormatRaw, + data.FormatMapBytes, + FormatYamlNodes, + FormatMapYamlNodes, + } } func (p *Key) Collect() { @@ -67,19 +64,22 @@ func (p *Key) Collect() { var lookupMap *MapYamlLookup var nestedLookupMap map[string]*MapYamlLookup - log.WithFields(log.Fields{ - "fact-plugin": p.PluginName(), - "fact": p.Name, + contextLogger := log.WithFields(log.Fields{ + "fact-plugin": p.GetName(), + "fact": p.GetId(), + }) + + contextLogger.WithFields(log.Fields{ "input": p.GetInputName(), - "input-plugin": p.input.PluginName(), - "input-format": p.input.GetFormat(), + "input-plugin": p.GetInput().GetName(), + "input-format": p.GetInput().GetFormat(), }).Debug("collecting data") - switch p.input.GetFormat() { + switch p.GetInput().GetFormat() { // The file:read plugin is used to read the file content. case data.FormatRaw: - inputData := data.AsBytes(p.input.GetData()) + inputData := data.AsBytes(p.GetInput().GetData()) if inputData == nil { return } @@ -91,13 +91,14 @@ func (p *Key) Collect() { p.Format = data.FormatNil return } - p.errors = append(p.errors, err) + contextLogger.WithError(err).Error("error looking up yaml path") + p.AddErrors(err) return } // The file:lookup plugin is used to lookup files. case data.FormatMapBytes: - inputData := data.AsMapBytes(p.input.GetData()) + inputData := data.AsMapBytes(p.GetInput().GetData()) if inputData == nil { return } @@ -118,36 +119,46 @@ func (p *Key) Collect() { return } } - p.errors = append(p.errors, errs...) + for _, err := range errs { + contextLogger.WithError(err).Error("error looking up yaml path") + } + p.AddErrors(errs...) return } // The yaml:key plugin is used to lookup keys in a single YAML file. case FormatYamlNodes: - yamlNodes := DataAsYamlNodes(p.input.GetData()) + yamlNodes := DataAsYamlNodes(p.GetInput().GetData()) var errs []error lookupMap, errs = NewMapYamlLookupFromNodes(yamlNodes, p.Path) if len(errs) > 0 { - p.errors = append(p.errors, errs...) + for _, err := range errs { + contextLogger.WithError(err).Error("error looking up yaml path") + } + p.AddErrors(errs...) return } // The yaml.lookup plugin is used to lookup keys in multiple YAML files. case FormatMapYamlNodes: - mapYamlNodes := DataAsMapYamlNodes(p.input.GetData()) + mapYamlNodes := DataAsMapYamlNodes(p.GetInput().GetData()) nestedLookupMap = map[string]*MapYamlLookup{} for f, nodes := range mapYamlNodes { lookupMap, errs := NewMapYamlLookupFromNodes(nodes, p.Path) if len(errs) > 0 { - p.errors = append(p.errors, errs...) + for _, err := range errs { + contextLogger.WithError(err).Error("error looking up yaml path") + } + p.AddErrors(errs...) return } nestedLookupMap[f] = lookupMap } default: - log.WithField("input-format", p.input.GetFormat()).Error("unsupported input format") + contextLogger.WithField("input-format", p.GetInput().GetFormat()). + Error("unsupported input format") } if lookup == nil && lookupMap == nil && nestedLookupMap == nil { @@ -157,10 +168,10 @@ func (p *Key) Collect() { if p.NodesOnly { if lookup != nil { p.Format = FormatYamlNodes - p.data = lookup.Nodes + p.SetData(lookup.Nodes) } else if lookupMap != nil { p.Format = FormatMapYamlNodes - p.data = lookupMap.GetMapNodes() + p.SetData(lookupMap.GetMapNodes()) } return } @@ -168,7 +179,8 @@ func (p *Key) Collect() { if p.KeysOnly { if lookup != nil { if lookup.Kind != yaml.MappingNode { - p.errors = append(p.errors, errors.New("keys-only lookup only supports a single mapping node")) + contextLogger.Error("keys-only lookup only supports a single mapping node") + p.AddErrors(errors.New("keys-only lookup only supports a single mapping node")) return } mappedData := MappingNodeToKeyedMap(lookup.Nodes[0]) @@ -177,32 +189,30 @@ func (p *Key) Collect() { keys = append(keys, k) } p.Format = data.FormatListString - p.data = keys + p.SetData(keys) } else { - p.errors = append(p.errors, errors.New("yaml-nodes-map unsupported format for keys-only lookup")) + contextLogger.Error("yaml-nodes-map unsupported format for keys-only lookup") + p.AddErrors(errors.New("yaml-nodes-map unsupported format for keys-only lookup")) } return } envMap, err := p.GetEnvMap() if err != nil { - log.WithFields(log.Fields{ - "fact-plugin": p.PluginName(), - "fact": p.Name, - "input": p.GetInputName(), - }).WithError(err).Error("unable to read env file") - p.errors = append(p.errors, err) + contextLogger.WithField("input", p.GetInputName()). + WithError(err).Error("unable to read env file") + p.AddErrors(err) return } if lookup != nil { lookup.ProcessNodes(envMap) p.Format = lookup.Format - p.data = lookup.Data + p.SetData(lookup.Data) } else if lookupMap != nil { lookupMap.ProcessMap(envMap) p.Format = lookupMap.Format - p.data = lookupMap.DataMap + p.SetData(lookupMap.DataMap) } else { res := map[string]map[string]string{} for f, m := range nestedLookupMap { @@ -216,11 +226,13 @@ func (p *Key) Collect() { case data.FormatMapString: p.Format = data.FormatMapNestedString default: - p.errors = append(p.errors, errors.New("unsupported format for nested lookup")) + contextLogger.WithField("format", m.Format). + Error("unsupported format for nested lookup") + p.AddErrors(errors.New("unsupported format " + string(m.Format) + " for nested lookup")) return } } } - p.data = res + p.SetData(res) } } diff --git a/pkg/fact/yaml/key_test.go b/pkg/fact/yaml/key_test.go index f75ad04..8f5c93c 100644 --- a/pkg/fact/yaml/key_test.go +++ b/pkg/fact/yaml/key_test.go @@ -11,81 +11,105 @@ import ( "github.com/salsadigitalauorg/shipshape/pkg/fact" . "github.com/salsadigitalauorg/shipshape/pkg/fact/yaml" "github.com/salsadigitalauorg/shipshape/pkg/internal" + "github.com/salsadigitalauorg/shipshape/pkg/plugin" ) func TestKeyInit(t *testing.T) { assert := assert.New(t) // Test that the yaml:key plugin is registered. - factPlugin := fact.Registry["yaml:key"]("testKeyYaml") + factPlugin := fact.Manager().GetFactories()["yaml:key"]("testKeyYaml") assert.NotNil(factPlugin) keyFacter, ok := factPlugin.(*Key) assert.True(ok) - assert.Equal("testKeyYaml", keyFacter.Name) + assert.Equal("testKeyYaml", keyFacter.GetId()) } func TestKeyPluginName(t *testing.T) { - key := Key{Name: "testKeyYaml"} - assert.Equal(t, "yaml:key", key.PluginName()) + key := New("testKeyYaml") + assert.Equal(t, "yaml:key", key.GetName()) } func TestKeySupportedConnections(t *testing.T) { - key := Key{Name: "testKeyYaml"} + key := New("testKeyYaml") supportLevel, connections := key.SupportedConnections() - assert.Equal(t, fact.SupportNone, supportLevel) + assert.Equal(t, plugin.SupportNone, supportLevel) assert.Empty(t, connections) } -func TestKeySupportedInputs(t *testing.T) { - key := Key{Name: "testKeyYaml"} - supportLevel, inputs := key.SupportedInputs() - assert.Equal(t, fact.SupportRequired, supportLevel) - assert.ElementsMatch(t, []string{ - "docker:command", - "file:read", - "file:lookup", - "yaml:key"}, inputs) +func TestKeySupportedInputFormats(t *testing.T) { + key := New("testKeyYaml") + supportLevel, inputFormats := key.SupportedInputFormats() + assert.Equal(t, plugin.SupportRequired, supportLevel) + assert.ElementsMatch(t, []data.DataFormat{ + data.FormatRaw, + data.FormatMapBytes, + FormatYamlNodes, + FormatMapYamlNodes}, inputFormats) } func TestKeyCollect(t *testing.T) { tests := []internal.FactCollectTest{ { Name: "noInput", - Facter: &Key{Name: "base-images"}, - ExpectedInputError: &fact.ErrSupportRequired{SupportType: "input"}, + Facter: New("base-images"), + ExpectedInputError: &plugin.ErrSupportRequired{SupportType: "input"}, }, { - Name: "noInput/nameProvided", - Facter: &Key{Name: "base-images", InputName: "test-input"}, - ExpectedInputError: &fact.ErrSupportRequired{SupportType: "input"}, + Name: "noInput/nameProvided", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + return f + }, + ExpectedInputError: &plugin.ErrSupportRequired{SupportType: "input"}, }, { - Name: "inputFormat/Empty", - Facter: &Key{Name: "base-images", InputName: "test-input"}, + Name: "inputFormat/Empty", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + return f + }, TestInput: internal.FactInputTest{Data: []byte("")}, - ExpectedInputError: &fact.ErrSupportRequired{SupportType: "input data format"}, + ExpectedInputError: &plugin.ErrSupportRequired{SupportType: "input data format"}, }, // Raw data format (data.FormatRaw) cases. { - Name: "inputFormat/Raw/NotFound", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo"}, + Name: "inputFormat/Raw/NotFound", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatRaw, Data: []byte("bar: baz")}, ExpectedErrors: []error{errors.New("yaml path not found")}, }, { Name: "inputFormat/Raw/NotFound/Ignored", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo", - IgnoreNotFound: true}, + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + f.IgnoreNotFound = true + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatRaw, Data: []byte("bar: baz")}, ExpectedFormat: data.FormatNil, ExpectedData: nil, }, { - Name: "inputFormat/Raw", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo"}, + Name: "inputFormat/Raw", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatRaw, Data: []byte("foo: bar")}, ExpectedFormat: data.FormatString, @@ -93,8 +117,13 @@ func TestKeyCollect(t *testing.T) { }, { Name: "inputFormat/Raw/NodesOnly", - Facter: &Key{Name: "base-images", InputName: "test-input", - Path: "foo", NodesOnly: true}, + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + f.NodesOnly = true + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatRaw, Data: []byte("foo: bar")}, ExpectedFormat: FormatYamlNodes, @@ -104,8 +133,13 @@ func TestKeyCollect(t *testing.T) { }, { Name: "inputFormat/Raw/KeysOnly", - Facter: &Key{Name: "base-images", InputName: "test-input", - Path: "foo", KeysOnly: true}, + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + f.KeysOnly = true + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatRaw, Data: []byte(`foo: bar: baz @@ -117,8 +151,13 @@ func TestKeyCollect(t *testing.T) { // Map of Raw data (data.FormatMapBytes) format cases. { - Name: "inputFormat/MapBytes/NotFound", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo"}, + Name: "inputFormat/MapBytes/NotFound", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"file1": []byte("bar: baz")}, @@ -127,8 +166,13 @@ func TestKeyCollect(t *testing.T) { }, { Name: "inputFormat/MapBytes/NotFound/Ignored", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo", - IgnoreNotFound: true}, + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + f.IgnoreNotFound = true + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"file1": []byte("bar: baz")}, @@ -137,8 +181,13 @@ func TestKeyCollect(t *testing.T) { ExpectedData: nil, }, { - Name: "inputFormat/MapBytes/scalar", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo"}, + Name: "inputFormat/MapBytes/scalar", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"file1": []byte("foo: bar")}, @@ -147,8 +196,13 @@ func TestKeyCollect(t *testing.T) { ExpectedData: map[string]any{"file1": "bar"}, }, { - Name: "inputFormat/MapBytes/list", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo"}, + Name: "inputFormat/MapBytes/list", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"file1": []byte("foo: [bar, baz]")}, @@ -157,8 +211,13 @@ func TestKeyCollect(t *testing.T) { ExpectedData: map[string]any{"file1": []string{"bar", "baz"}}, }, { - Name: "inputFormat/MapBytes/mapOfString", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo"}, + Name: "inputFormat/MapBytes/mapOfString", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"file1": []byte(`foo: @@ -170,8 +229,13 @@ func TestKeyCollect(t *testing.T) { ExpectedData: map[string]any{"file1": map[string]string{"bar": "baz", "zoo": "bar"}}, }, { - Name: "inputFormat/MapBytes/mapOfList", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "foo"}, + Name: "inputFormat/MapBytes/mapOfList", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "foo" + return f + }, TestInput: internal.FactInputTest{ DataFormat: data.FormatMapBytes, Data: map[string][]byte{"file1": []byte(`foo: @@ -185,8 +249,13 @@ func TestKeyCollect(t *testing.T) { // List of Yaml nodes (FormatYamlNodes) format cases. { - Name: "inputFormat/YamlNodes/scalar", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "baz"}, + Name: "inputFormat/YamlNodes/scalar", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "baz" + return f + }, TestInput: internal.FactInputTest{ DataFormat: FormatYamlNodes, DataFn: func() any { @@ -201,8 +270,13 @@ func TestKeyCollect(t *testing.T) { ExpectedData: map[string]any{"bar": "zap", "foo": "zoom"}, }, { - Name: "inputFormat/YamlNodes/list", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "baz"}, + Name: "inputFormat/YamlNodes/list", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "baz" + return f + }, TestInput: internal.FactInputTest{ DataFormat: FormatYamlNodes, DataFn: func() any { @@ -219,8 +293,13 @@ func TestKeyCollect(t *testing.T) { "bar": []string{"whoop", "pop"}}, }, { - Name: "inputFormat/YamlNodes/mapOfString", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "baz"}, + Name: "inputFormat/YamlNodes/mapOfString", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "baz" + return f + }, TestInput: internal.FactInputTest{ DataFormat: FormatYamlNodes, DataFn: func() any { @@ -243,8 +322,13 @@ func TestKeyCollect(t *testing.T) { "bar": map[string]string{"whoop": "pop"}}, }, { - Name: "inputFormat/YamlNodes/mapOfList", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "baz"}, + Name: "inputFormat/YamlNodes/mapOfList", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "baz" + return f + }, TestInput: internal.FactInputTest{ DataFormat: FormatYamlNodes, DataFn: func() any { @@ -269,8 +353,13 @@ func TestKeyCollect(t *testing.T) { // Map of Yaml nodes (FormatMapYamlNodes) format cases. { - Name: "inputFormat/MapYamlNodes/scalar", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "baz"}, + Name: "inputFormat/MapYamlNodes/scalar", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "baz" + return f + }, TestInput: internal.FactInputTest{ DataFormat: FormatMapYamlNodes, DataFn: func() any { @@ -294,8 +383,13 @@ func TestKeyCollect(t *testing.T) { "key2": {"zoom": "paf", "whoop": "blo"}}, }, { - Name: "inputFormat/MapYamlNodes/list", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "baz"}, + Name: "inputFormat/MapYamlNodes/list", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "baz" + return f + }, TestInput: internal.FactInputTest{ DataFormat: FormatMapYamlNodes, DataFn: func() any { @@ -314,11 +408,16 @@ func TestKeyCollect(t *testing.T) { }, }, ExpectedErrors: []error{ - errors.New("unsupported format for nested lookup")}, + errors.New("unsupported format map-list-string for nested lookup")}, }, { - Name: "inputFormat/MapYamlNodes/map", - Facter: &Key{Name: "base-images", InputName: "test-input", Path: "baz"}, + Name: "inputFormat/MapYamlNodes/map", + FactFn: func() fact.Facter { + f := New("base-images") + f.SetInputName("test-input") + f.Path = "baz" + return f + }, TestInput: internal.FactInputTest{ DataFormat: FormatMapYamlNodes, DataFn: func() any { @@ -337,7 +436,7 @@ func TestKeyCollect(t *testing.T) { }, }, ExpectedErrors: []error{ - errors.New("unsupported format for nested lookup")}, + errors.New("unsupported format map-nested-string for nested lookup")}, }, } diff --git a/pkg/internal/testutils_factcollect.go b/pkg/internal/testutils_factcollect.go index e0eb88c..ba65ad7 100644 --- a/pkg/internal/testutils_factcollect.go +++ b/pkg/internal/testutils_factcollect.go @@ -22,6 +22,7 @@ type FactInputTest struct { type FactCollectTest struct { Name string Facter fact.Facter + FactFn func() fact.Facter TestInput FactInputTest ExpectedInputError error @@ -44,7 +45,11 @@ func TestFactCollect(t *testing.T, fct FactCollectTest) { defer logrus.SetOutput(currLogOut) logrus.SetOutput(io.Discard) - fact.Facts = map[string]fact.Facter{} + fact.Manager().ResetPlugins() + + if fct.FactFn != nil { + fct.Facter = fct.FactFn() + } // Load input plugin. if fct.TestInput.DataFn != nil { @@ -52,16 +57,18 @@ func TestFactCollect(t *testing.T, fct FactCollectTest) { } if fct.TestInput.Data != nil { - testP := testdata.TestFacter{ - Name: "test-input", - TestInputDataFormat: fct.TestInput.DataFormat, - TestInputData: fct.TestInput.Data, + p, err := fact.Manager().GetPlugin("testdata:testfacter", "test-input") + if err != nil { + t.Fatalf("failed to get test input plugin: %s", err) } + + testP := p.(*testdata.TestFacter) + testP.TestInputDataFormat = fct.TestInput.DataFormat + testP.TestInputData = fct.TestInput.Data testP.Collect() - fact.Facts["test-input"] = &testP } - err := fct.Facter.ValidateInput() + err := fact.ValidateInput(fct.Facter) if fct.ExpectedInputError != nil { assert.Error(err, fct.ExpectedInputError) return @@ -72,16 +79,18 @@ func TestFactCollect(t *testing.T, fct FactCollectTest) { // Load additional inputs. if len(fct.TestAdditionalInputs) > 0 { for name, testInput := range fct.TestAdditionalInputs { - testP := testdata.TestFacter{ - Name: name, - TestInputDataFormat: testInput.DataFormat, - TestInputData: testInput.Data, + p, err := fact.Manager().GetPlugin("testdata:testfacter", name) + if err != nil { + t.Fatalf("failed to get test input plugin: %s", err) } + + testP := p.(*testdata.TestFacter) + testP.TestInputDataFormat = testInput.DataFormat + testP.TestInputData = testInput.Data testP.Collect() - fact.Facts[name] = &testP } - errs := fct.Facter.LoadAdditionalInputs() + errs := fact.LoadAdditionalInputs(fct.Facter) if len(fct.ExpectedAdditionalInputsErrs) > 0 { assert.ElementsMatch(fct.ExpectedAdditionalInputsErrs, errs) return diff --git a/pkg/plugin/base.go b/pkg/plugin/base.go new file mode 100644 index 0000000..642ac42 --- /dev/null +++ b/pkg/plugin/base.go @@ -0,0 +1,24 @@ +package plugin + +// BasePlugin provides common fields and functionality for all plugins. +type BasePlugin struct { + // Common fields found across plugins + Id string `yaml:"-"` + + // Internal fields + errors []error +} + +// Base getter methods +func (p *BasePlugin) GetId() string { + return p.Id +} + +func (p *BasePlugin) GetErrors() []error { + return p.errors +} + +// AddError adds an error to the plugin's error list +func (p *BasePlugin) AddErrors(errs ...error) { + p.errors = append(p.errors, errs...) +} diff --git a/pkg/plugin/factory.go b/pkg/plugin/factory.go new file mode 100644 index 0000000..53f11cb --- /dev/null +++ b/pkg/plugin/factory.go @@ -0,0 +1,40 @@ +package plugin + +import "sort" + +// GetFactoriesKeys returns a sorted list of keys from a plugin factory registry. +func GetFactoriesKeys[T Plugin](factories interface{}) []string { + var keys []string + + switch r := factories.(type) { + case Factories[T]: + for k := range r { + keys = append(keys, k) + } + case FactoriesNoId[T]: + for k := range r { + keys = append(keys, k) + } + } + + sort.Strings(keys) + return keys +} + +// GetFactory returns a plugin factory from a registry by name. +func GetFactory[T Plugin](factories Factories[T], name string) func(string) T { + if factory, ok := factories[name]; ok { + return factory + } + var zero func(string) T + return zero +} + +// GetFactoryNoId returns a plugin factory from a registry. +func GetFactoryNoId[T Plugin](factories FactoriesNoId[T], name string) func() T { + if factory, ok := factories[name]; ok { + return factory + } + var zero func() T + return zero +} diff --git a/pkg/plugin/types.go b/pkg/plugin/types.go new file mode 100644 index 0000000..fa95f8f --- /dev/null +++ b/pkg/plugin/types.go @@ -0,0 +1,69 @@ +// Package plugin provides the base interfaces and types for all Shipshape plugins. +package plugin + +import "fmt" + +// Plugin is the base interface that all plugins must implement. +type Plugin interface { + // GetName returns the unique identifier for this plugin type. + GetName() string + // GetId returns the unique identifier for this plugin instance. + GetId() string + // GetErrors returns the errors for this plugin. + GetErrors() []error + // AddErrors adds an error to the plugin's error list. + AddErrors(errs ...error) +} + +// Factories represents a generic plugin factory registry. +type Factories[T Plugin] map[string]func(string) T + +// FactoriesNoId represents a plugin factory registry for +// plugins that don't require ids. +type FactoriesNoId[T Plugin] map[string]func() T + +// SupportLevel defines the level of support for plugin dependencies. +type SupportLevel string + +const ( + SupportRequired SupportLevel = "required" + SupportOptional SupportLevel = "optional" + SupportNone SupportLevel = "not-supported" +) + +// ErrSupportRequired is returned when a required plugin dependency is missing. +type ErrSupportRequired struct { + Plugin string + SupportType string +} + +func (m *ErrSupportRequired) Error() string { + return fmt.Sprintf("%s required for '%s'", m.SupportType, m.Plugin) +} + +// ErrSupportNotFound is returned when a plugin dependency cannot be found. +type ErrSupportNotFound struct { + Plugin string + SupportType string + SupportPlugin string +} + +func (m *ErrSupportNotFound) Error() string { + return fmt.Sprintf("%s '%s' not found for '%s'", + m.SupportType, m.SupportPlugin, m.Plugin) +} + +// ErrSupportNone is returned when a plugin dependency is not supported. +type ErrSupportNone struct { + Plugin string + SupportType string + SupportPlugin string +} + +func (m *ErrSupportNone) Error() string { + if m.SupportPlugin == "" { + return fmt.Sprintf("%s not supported for '%s'", m.SupportType, m.Plugin) + } + return fmt.Sprintf("%s '%s' not supported for '%s'", + m.SupportType, m.SupportPlugin, m.Plugin) +} diff --git a/pkg/pluginmanager/manager.go b/pkg/pluginmanager/manager.go new file mode 100644 index 0000000..deaa717 --- /dev/null +++ b/pkg/pluginmanager/manager.go @@ -0,0 +1,157 @@ +package pluginmanager + +import ( + "fmt" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/salsadigitalauorg/shipshape/pkg/plugin" +) + +// Manager handles plugin registration, validation, and lifecycle management. +type Manager[T plugin.Plugin] struct { + mu sync.RWMutex + factories plugin.Factories[T] + plugins map[string]T + errors []error +} + +// NewManager creates a new plugin manager instance. +func NewManager[T plugin.Plugin]() *Manager[T] { + return &Manager[T]{ + factories: make(plugin.Factories[T]), + plugins: make(map[string]T), + } +} + +// RegisterFactory adds a new plugin factory to the registry. +func (m *Manager[T]) RegisterFactory(name string, factory func(string) T) error { + m.mu.Lock() + defer m.mu.Unlock() + + if _, exists := m.factories[name]; exists { + return fmt.Errorf("plugin factory '%s' is already registered", name) + } + + log.WithField("plugin", name).Debug("registering plugin factory") + m.factories[name] = factory + return nil +} + +// GetFactories returns the plugin factories. +func (m *Manager[T]) GetFactories() plugin.Factories[T] { + m.mu.RLock() + defer m.mu.RUnlock() + return m.factories +} + +// FindPlugin returns a plugin instance by name. +func (m *Manager[T]) FindPlugin(name string) T { + m.mu.RLock() + defer m.mu.RUnlock() + return m.plugins[name] +} + +// GetPlugin returns a plugin instance by plugin name and id, +// creating it if it doesn't exist. +func (m *Manager[T]) GetPlugin(name string, id string) (T, error) { + m.mu.RLock() + if plugin, exists := m.plugins[name]; exists { + m.mu.RUnlock() + return plugin, nil + } + m.mu.RUnlock() + + m.mu.Lock() + defer m.mu.Unlock() + + // Double-check after acquiring write lock + if plugin, exists := m.plugins[name]; exists { + return plugin, nil + } + + factory, exists := m.factories[name] + if !exists { + var zero T + return zero, fmt.Errorf("plugin factory '%s' not found in registry", name) + } + + plugin := factory(id) + m.plugins[id] = plugin + return plugin, nil +} + +// GetPlugins returns the plugin instances. +func (m *Manager[T]) GetPlugins() map[string]T { + m.mu.RLock() + defer m.mu.RUnlock() + return m.plugins +} + +// SetPlugins sets the plugin instances. +func (m *Manager[T]) SetPlugins(plugins map[string]T) { + m.mu.Lock() + defer m.mu.Unlock() + m.plugins = plugins +} + +// ListPlugins returns a sorted list of registered plugin names. +func (m *Manager[T]) ListPlugins() []string { + return plugin.GetFactoriesKeys[T](m.factories) +} + +// ResetPlugins resets the plugin instances. +func (m *Manager[T]) ResetPlugins() { + m.mu.Lock() + defer m.mu.Unlock() + m.plugins = make(map[string]T) +} + +// AddErrors adds errors to the manager. +func (m *Manager[T]) AddErrors(errs ...error) { + m.mu.Lock() + defer m.mu.Unlock() + m.errors = append(m.errors, errs...) +} + +// GetErrors returns the errors. +func (m *Manager[T]) GetErrors() []error { + m.mu.RLock() + defer m.mu.RUnlock() + return m.errors +} + +// ResetErrors resets the errors. +func (m *Manager[T]) ResetErrors() { + m.mu.Lock() + defer m.mu.Unlock() + m.errors = []error{} +} + +// Reset clears all registered plugins and instances. +func (m *Manager[T]) Reset() { + m.mu.Lock() + defer m.mu.Unlock() + m.factories = make(plugin.Factories[T]) + m.plugins = make(map[string]T) + m.errors = []error{} +} + +// You can use it like this: +// Create a manager for fact plugins +// factManager := pluginmanager.NewManager[fact.Facter]() + +// // Register a plugin +// factManager.Register("command", func(name string) fact.Facter { +// return &command.Command{Name: name} +// }) + +// // Get a plugin instance +// cmdPlugin, err := factManager.GetPlugin("command") +// if err != nil { +// log.Fatal(err) +// } + +// // List all registered plugins +// plugins := factManager.ListPlugins() diff --git a/pkg/pluginmanager/manager_test.go b/pkg/pluginmanager/manager_test.go new file mode 100644 index 0000000..4aa7284 --- /dev/null +++ b/pkg/pluginmanager/manager_test.go @@ -0,0 +1,61 @@ +package pluginmanager + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/salsadigitalauorg/shipshape/pkg/plugin" +) + +// TestPlugin implements the Plugin interface for testing. +type TestPlugin struct { + plugin.BasePlugin +} + +func (p *TestPlugin) GetName() string { + return "test-plugin" +} + +func (p *TestPlugin) GetId() string { + return p.Id +} + +func TestManager(t *testing.T) { + manager := NewManager[*TestPlugin]() + assert := assert.New(t) + + // Test registration + err := manager.RegisterFactory("testFactory", func(name string) *TestPlugin { + return &TestPlugin{BasePlugin: plugin.BasePlugin{Id: name}} + }) + assert.NoError(err) + + // Test duplicate registration + err = manager.RegisterFactory("testFactory", func(name string) *TestPlugin { + return &TestPlugin{BasePlugin: plugin.BasePlugin{Id: name}} + }) + assert.Error(err) + assert.Contains(err.Error(), "already registered") + + // Test getting plugin + plugin, err := manager.GetPlugin("testFactory", "test") + assert.NoError(err) + assert.NotNil(plugin) + assert.Equal("test-plugin", plugin.GetName()) + + // Test getting non-existent plugin + plugin, err = manager.GetPlugin("non-existent", "test") + assert.Error(err) + assert.Contains(err.Error(), "not found in registry") + assert.Nil(plugin) + + // Test listing plugins + plugins := manager.ListPlugins() + assert.Equal(1, len(plugins)) + assert.Equal("testFactory", plugins[0]) + + // Test reset + manager.Reset() + assert.Empty(manager.ListPlugins()) +} diff --git a/pkg/shipshape/shipshape.go b/pkg/shipshape/shipshape.go index 1857647..cceb9f9 100644 --- a/pkg/shipshape/shipshape.go +++ b/pkg/shipshape/shipshape.go @@ -141,19 +141,25 @@ func RunV2() { log.WithField("config", fmt.Sprintf("%+v", RunConfigV2)).Trace("running v2") log.Print("parsing connections config") - connection.ParseConfig(RunConfigV2.Connections) + if err := connection.Manager().ParseConfig(RunConfigV2.Connections); err != nil { + log.Fatal(err) + } log.Print("parsing facts config") - fact.ParseConfig(RunConfigV2.Collect) + if err := fact.Manager().ParseConfig(RunConfigV2.Collect); err != nil { + log.Fatal(err) + } log.Print("parsing analysers config") - analyse.ParseConfig(RunConfigV2.Analyse) + if err := analyse.Manager().ParseConfig(RunConfigV2.Analyse); err != nil { + log.Fatal(err) + } RunResultList = result.NewResultList(Remediate) log.Print("parsing output config") output.ParseConfig(RunConfigV2.Output, &RunResultList) log.Print("collecting facts") - fact.CollectAllFacts() - if len(fact.Errors) > 0 { + fact.Manager().CollectAllFacts() + if len(fact.Manager().GetErrors()) > 0 { log.Fatal("failed to collect facts") } @@ -162,14 +168,14 @@ func RunV2() { } log.Print("validating analyser inputs") - analyse.ValidateInputs() - if len(analyse.Errors) > 0 { - log.WithField("errors", analyse.Errors). + analyse.Manager().ValidateInputs() + if len(analyse.Manager().GetErrors()) > 0 { + log.WithField("errors", analyse.Manager().GetErrors()). Fatal("failed to validate analyser inputs") } log.Print("analysing facts") - results := analyse.AnalyseAll() + results := analyse.Manager().AnalyseAll() if Remediate { log.Print("starting remediation")