diff --git a/cmd/packer-plugin-check/main.go b/cmd/packer-plugin-check/main.go new file mode 100644 index 000000000..f94f0ba3c --- /dev/null +++ b/cmd/packer-plugin-check/main.go @@ -0,0 +1,177 @@ +// packer-plugin-check is a command used by plugins to validate compatibility and basic configuration +// to work with Packer. +package main + +import ( + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + + packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" + "github.com/hashicorp/packer/packer/plugin" +) + +const packerPluginCheck = "packer-plugin-check" + +var ( + docs = flag.Bool("docs", false, "flag to indicate that documentation files should be checked.") + load = flag.String("load", "", "flag to check if plugin can be loaded by Packer and is compatible with HCL2.") +) + +// Usage is a replacement usage function for the flags package. +func Usage() { + fmt.Fprintf(os.Stderr, "Usage of "+packerPluginCheck+":\n") + fmt.Fprintf(os.Stderr, "\t"+packerPluginCheck+" [flags]\n") + fmt.Fprintf(os.Stderr, "Flags:\n") + flag.PrintDefaults() +} + +func main() { + log.SetFlags(0) + log.SetPrefix(packerPluginCheck + ": ") + flag.Usage = Usage + flag.Parse() + + if flag.NFlag() == 0 { + flag.Usage() + os.Exit(2) + } + + if *docs { + if err := checkDocumentation(); err != nil { + fmt.Printf(err.Error()) + os.Exit(2) + } + fmt.Printf("Plugin succesfully passed docs check.\n") + } + + if len(*load) > 0 { + if err := checkPluginName(*load); err != nil { + fmt.Printf(err.Error()) + os.Exit(2) + } + if err := discoverAndLoad(); err != nil { + fmt.Printf(err.Error()) + os.Exit(2) + } + fmt.Printf("Plugin succesfully passed compatibility check.\n") + } +} + +// checkDocumentation looks for the presence of a docs folder with mdx files inside. +// It is not possible to predict the number of mdx files for a given plugin. +// Because of that, finding one file inside the folder is enough to validate the docs existence. +func checkDocumentation() error { + // TODO: this should be updated once we have defined what's going to be for plguin's docs + wd, err := os.Getwd() + if err != nil { + return err + } + + docsDir := wd + "/docs" + stat, err := os.Stat(docsDir) + if err != nil { + return fmt.Errorf("could not find docs folter: %s", err.Error()) + } + if !stat.IsDir() { + return fmt.Errorf("expecting docs do be a directory of mdx files") + } + + var mdxFound bool + _ = filepath.Walk(docsDir, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() && filepath.Ext(path) == ".mdx" { + mdxFound = true + return io.EOF + } + return nil + }) + + if mdxFound { + fmt.Printf("a mdx file was found inside the docs folder\n") + return nil + } + return fmt.Errorf("no docs files found, make sure to have the docs in place before releasing") +} + +// checkPluginName checks for the possible valid names for a plugin, packer-plugin-* or packer-[builder|provisioner|post-processor]-*. +// If the name is prefixed with `packer-[builder|provisioner|post-processor]-`, packer won't be able to install it, +// therefore a WARNING will be shown. +func checkPluginName(name string) error { + if strings.HasPrefix(name, "packer-plugin-") { + return nil + } + if strings.HasPrefix(name, "packer-builder-") || + strings.HasPrefix(name, "packer-provisioner-") || + strings.HasPrefix(name, "packer-post-processor-") { + fmt.Printf("\n[WARNING] Plugin is named with old prefix `packer-[builder|provisioner|post-processor]-{name})`. " + + "These will be detected but Packer cannot install them automatically. " + + "The plugin must be a multi-plugin named packer-plugin-{name} to be installable through the `packer init` command.\n") + return nil + } + return fmt.Errorf("plugin's name is not valid") +} + +// discoverAndLoad will discover the plugin binary from the current directory and load any builder/provisioner/post-processor +// in the plugin configuration. At least one builder, provisioner, or post-processor should be found to validate the plugin's +// compatibility with Packer. +func discoverAndLoad() error { + config := plugin.Config{ + PluginMinPort: 10000, + PluginMaxPort: 25000, + } + err := config.Discover() + if err != nil { + return err + } + + // TODO: validate correctness of plugins loaded by checking them against the output of the `describe` command. + builders, provisioners, postProcessors := config.GetPlugins() + if len(builders) == 0 && + len(provisioners) == 0 && + len(postProcessors) == 0 { + return fmt.Errorf("couldn't load any Builder/Provisioner/Post-Processor from the plugin binary") + } + + return checkHCL2ConfigSpec(builders, provisioners, postProcessors) +} + +// checkHCL2ConfigSpec checks if the hcl2spec config is present for the given plugins by validating that ConfigSpec() does not +// return an empty map of specs. +func checkHCL2ConfigSpec(builders packersdk.MapOfBuilder, provisioners packersdk.MapOfProvisioner, postProcessors packersdk.MapOfPostProcessor) error { + var errs *packersdk.MultiError + for _, b := range builders.List() { + builder, err := builders.Start(b) + if err != nil { + return packersdk.MultiErrorAppend(err, errs) + } + if len(builder.ConfigSpec()) == 0 { + errs = packersdk.MultiErrorAppend(fmt.Errorf("builder %q does not contain the required hcl2spec configuration", b), errs) + } + } + for _, p := range provisioners.List() { + provisioner, err := provisioners.Start(p) + if err != nil { + return packersdk.MultiErrorAppend(err, errs) + } + if len(provisioner.ConfigSpec()) == 0 { + errs = packersdk.MultiErrorAppend(fmt.Errorf("provisioner %q does not contain the required hcl2spec configuration", p), errs) + } + } + for _, pp := range postProcessors.List() { + postProcessor, err := postProcessors.Start(pp) + if err != nil { + return packersdk.MultiErrorAppend(err, errs) + } + if len(postProcessor.ConfigSpec()) == 0 { + errs = packersdk.MultiErrorAppend(fmt.Errorf("post-processor %q does not contain the required hcl2spec configuration", pp), errs) + } + } + if errs != nil && len(errs.Errors) > 0 { + return errs + } + return nil +}