/plugin/plugin.go

https://github.com/instrumenta/conftest · Go · 207 lines · 136 code · 39 blank · 32 comment · 34 complexity · 7c288ee7925e7623541e07fffef18187 MD5 · raw file

  1. package plugin
  2. import (
  3. "context"
  4. "fmt"
  5. "io/ioutil"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "runtime"
  10. "strings"
  11. "syscall"
  12. "github.com/ghodss/yaml"
  13. )
  14. // Plugin represents a plugin.
  15. type Plugin struct {
  16. Name string `yaml:"name"`
  17. Version string `yaml:"version"`
  18. Usage string `yaml:"usage"`
  19. Description string `yaml:"description"`
  20. Command string `yaml:"command"`
  21. }
  22. // Load loads a plugin given the name of the plugin.
  23. // The name of the plugin is defined in the plugin
  24. // configuration and is stored in a folder with the name
  25. // of the plugin.
  26. func Load(name string) (*Plugin, error) {
  27. plugin := Plugin{
  28. Name: name,
  29. }
  30. loadedPlugin, err := FromDirectory(plugin.Directory())
  31. if err != nil {
  32. return nil, fmt.Errorf("from directory: %w", err)
  33. }
  34. return loadedPlugin, nil
  35. }
  36. // FindAll finds all of the plugins available on the
  37. // local file system.
  38. func FindAll() ([]*Plugin, error) {
  39. if _, err := os.Stat(CacheDirectory()); os.IsNotExist(err) {
  40. return []*Plugin{}, nil
  41. }
  42. files, err := ioutil.ReadDir(CacheDirectory())
  43. if err != nil {
  44. return nil, fmt.Errorf("read plugin cache: %w", err)
  45. }
  46. var plugins []*Plugin
  47. for _, file := range files {
  48. plugin := Plugin{
  49. Name: file.Name(),
  50. }
  51. // While it should not be possible for invalid plugins to be added to
  52. // the cache, if it does occur, remove the plugin from the cache so it
  53. // does not prevent valid plugins from being loaded.
  54. foundPlugin, err := FromDirectory(plugin.Directory())
  55. if err != nil {
  56. os.RemoveAll(plugin.Directory())
  57. continue
  58. }
  59. plugins = append(plugins, foundPlugin)
  60. }
  61. return plugins, nil
  62. }
  63. // Exec executes the command defined by the plugin along with any
  64. // arguments.
  65. //
  66. // Arguments that are passed into Exec will be added after
  67. // any arguments that are defined in the plugins configuration.
  68. func (p *Plugin) Exec(ctx context.Context, args []string) error {
  69. // Plugin configurations reference the CONFTEST_PLUGIN_DIR
  70. // environment to be able to call the plugin.
  71. os.Setenv("CONFTEST_PLUGIN_DIR", p.Directory())
  72. expandedCommand := os.ExpandEnv(string(p.Command))
  73. var command string
  74. var arguments []string
  75. var err error
  76. if runtime.GOOS == "windows" {
  77. command, arguments, err = parseWindowsCommand(expandedCommand, args)
  78. } else {
  79. command, arguments, err = parseCommand(expandedCommand, args)
  80. }
  81. if err != nil {
  82. return fmt.Errorf("parse command: %w", err)
  83. }
  84. cmd := exec.CommandContext(ctx, command, arguments...)
  85. cmd.Stdin = os.Stdin
  86. cmd.Stdout = os.Stdout
  87. cmd.Stderr = os.Stderr
  88. cmd.Env = os.Environ()
  89. // If an error is found during the execution of the plugin, figure
  90. // out if the error was from not being able to execute the plugin or
  91. // an error set by the plugin itself.
  92. if err := cmd.Run(); err != nil {
  93. exiterr, ok := err.(*exec.ExitError)
  94. if !ok {
  95. return fmt.Errorf("exit: %w", err)
  96. }
  97. status, ok := exiterr.Sys().(syscall.WaitStatus)
  98. if !ok {
  99. return fmt.Errorf("status: %w", err)
  100. }
  101. // Conftest can either return 1 or 2 for an error. If Conftest
  102. // returns an error, let it handle its own error.
  103. if status.ExitStatus() == 1 || status.ExitStatus() == 2 {
  104. return nil
  105. }
  106. return fmt.Errorf("plugin exec: %w", err)
  107. }
  108. return nil
  109. }
  110. // Directory returns the full path of the directory where the
  111. // plugin is stored in the plugin cache.
  112. func (p *Plugin) Directory() string {
  113. return filepath.Join(CacheDirectory(), p.Name)
  114. }
  115. // CacheDirectory returns the full path to the
  116. // cache directory where all of the plugins are stored.
  117. func CacheDirectory() string {
  118. const cacheDir = ".conftest/plugins"
  119. homeDir, _ := os.UserHomeDir()
  120. directory := filepath.Join(homeDir, cacheDir)
  121. directory = filepath.ToSlash(directory)
  122. return directory
  123. }
  124. // FromDirectory returns a plugin from a specific directory.
  125. //
  126. // The given directory must contain a plugin configuration file
  127. // in order to return successfully.
  128. func FromDirectory(directory string) (*Plugin, error) {
  129. const configurationFileName = "plugin.yaml"
  130. configPath := filepath.Join(directory, configurationFileName)
  131. data, err := ioutil.ReadFile(configPath)
  132. if err != nil {
  133. return nil, fmt.Errorf("read config: %w", err)
  134. }
  135. var plugin Plugin
  136. if err := yaml.Unmarshal(data, &plugin); err != nil {
  137. return nil, fmt.Errorf("unmarshal plugin: %w", err)
  138. }
  139. return &plugin, nil
  140. }
  141. func parseCommand(command string, extraArgs []string) (string, []string, error) {
  142. args := strings.Split(command, " ")
  143. if len(args) == 0 || args[0] == "" {
  144. return "", nil, fmt.Errorf("prepare plugin command: no command found")
  145. }
  146. executable := args[0]
  147. var configArguments []string
  148. if len(args) > 1 {
  149. configArguments = args[1:]
  150. }
  151. if len(extraArgs) > 0 {
  152. configArguments = append(configArguments, extraArgs...)
  153. }
  154. return executable, configArguments, nil
  155. }
  156. func parseWindowsCommand(command string, extraArgs []string) (string, []string, error) {
  157. executable, arguments, err := parseCommand(command, extraArgs)
  158. if err != nil {
  159. return "", nil, fmt.Errorf("parse command: %w", err)
  160. }
  161. // When executing shell scripts on Windows, the sh
  162. // program needs to be used to run the script.
  163. if strings.HasSuffix(executable, ".sh") {
  164. arguments = append([]string{executable}, arguments...)
  165. return "sh", arguments, nil
  166. }
  167. return executable, arguments, nil
  168. }