backport of commit 38f95d30d8

pull/12510/head
Lucas Bajolet 3 years ago
parent 49e7c0988a
commit b2baa32ab4

@ -0,0 +1,181 @@
package plugin
import (
_ "embed"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"github.com/hashicorp/go-multierror"
amazonacc "github.com/hashicorp/packer-plugin-amazon/builder/ebs/acceptance"
"github.com/hashicorp/packer-plugin-sdk/acctest"
"github.com/hashicorp/packer/hcl2template/addrs"
)
//go:embed test-fixtures/basic_amazon_bundled.pkr.hcl
var basicAmazonBundledEbsTemplate string
func TestAccBuildBundledPlugins(t *testing.T) {
plugin := addrs.Plugin{
Hostname: "github.com",
Namespace: "hashicorp",
Type: "amazon",
}
testCase := &acctest.PluginTestCase{
Name: "amazon-ebs_bundled_test",
Setup: func() error {
return cleanupPluginInstallation(plugin)
},
Teardown: func() error {
helper := amazonacc.AMIHelper{
Region: "us-east-1",
Name: "packer-plugin-bundled-amazon-ebs-test",
}
return helper.CleanUpAmi()
},
Template: basicAmazonBundledEbsTemplate,
Type: "amazon-ebs",
Init: false,
Check: func(buildCommand *exec.Cmd, logfile string) error {
if buildCommand.ProcessState != nil {
if buildCommand.ProcessState.ExitCode() != 0 {
return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
}
}
rawLogs, err := os.ReadFile(logfile)
if err != nil {
return fmt.Errorf("failed to read logs: %s", err)
}
var errs error
logs := string(rawLogs)
if !strings.Contains(logs, "Warning: Bundled plugins used") {
errs = multierror.Append(errs, errors.New("expected warning about bundled plugins used, did not find it"))
}
if !strings.Contains(logs, "Then run 'packer init' to manage installation of the plugins") {
errs = multierror.Append(errs, errors.New("expected suggestion about packer init in logs, did not find it."))
}
return errs
},
}
acctest.TestPlugin(t, testCase)
}
//go:embed test-fixtures/basic_amazon_with_required_plugins.pkr.hcl
var basicAmazonRequiredPluginEbsTemplate string
func TestAccBuildBundledPluginsWithRequiredPlugins(t *testing.T) {
plugin := addrs.Plugin{
Hostname: "github.com",
Namespace: "hashicorp",
Type: "amazon",
}
testCase := &acctest.PluginTestCase{
Name: "amazon-ebs_with_required_plugins_test",
Setup: func() error {
return cleanupPluginInstallation(plugin)
},
Teardown: func() error {
helper := amazonacc.AMIHelper{
Region: "us-east-1",
Name: "packer-plugin-required-plugin-amazon-ebs-test",
}
return helper.CleanUpAmi()
},
Template: basicAmazonRequiredPluginEbsTemplate,
Type: "amazon-ebs",
Init: false,
Check: func(buildCommand *exec.Cmd, logfile string) error {
if buildCommand.ProcessState != nil {
if buildCommand.ProcessState.ExitCode() != 1 {
return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
}
}
rawLogs, err := os.ReadFile(logfile)
if err != nil {
return fmt.Errorf("failed to read logs: %s", err)
}
var errs error
logs := string(rawLogs)
if strings.Contains(logs, "Warning: Bundled plugins used") {
errs = multierror.Append(errs, errors.New("did not expect warning about bundled plugins used"))
}
if !strings.Contains(logs, "Missing plugins") {
errs = multierror.Append(errs, errors.New("expected error about plugins required and not installed, did not find it"))
}
return errs
},
}
acctest.TestPlugin(t, testCase)
}
//go:embed test-fixtures/basic_amazon_bundled.json
var basicAmazonBundledEbsTemplateJSON string
func TestAccBuildBundledPluginsJSON(t *testing.T) {
plugin := addrs.Plugin{
Hostname: "github.com",
Namespace: "hashicorp",
Type: "amazon",
}
testCase := &acctest.PluginTestCase{
Name: "amazon-ebs_bundled_test_json",
Setup: func() error {
return cleanupPluginInstallation(plugin)
},
Teardown: func() error {
helper := amazonacc.AMIHelper{
Region: "us-east-1",
Name: "packer-plugin-bundled-amazon-ebs-test-json",
}
return helper.CleanUpAmi()
},
Template: basicAmazonBundledEbsTemplateJSON,
Type: "amazon-ebs",
Init: false,
Check: func(buildCommand *exec.Cmd, logfile string) error {
if buildCommand.ProcessState != nil {
if buildCommand.ProcessState.ExitCode() != 0 {
return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
}
}
rawLogs, err := os.ReadFile(logfile)
if err != nil {
return fmt.Errorf("failed to read logs: %s", err)
}
var errs error
logs := string(rawLogs)
if !strings.Contains(logs, "Warning: Bundled plugins used") {
errs = multierror.Append(errs, errors.New("expected warning about bundled plugins, did not find it."))
}
if !strings.Contains(logs, "plugins with the 'packer plugins install' command") {
errs = multierror.Append(errs, errors.New("expected suggestion about packer plugins install in logs, did not find it."))
}
return errs
},
}
acctest.TestPlugin(t, testCase)
}

@ -0,0 +1,10 @@
{
"builders": [{
"type": "amazon-ebs",
"region": "us-east-1",
"instance_type": "m3.medium",
"source_ami": "ami-76b2a71e",
"ssh_username": "ubuntu",
"ami_name": "packer-plugin-bundled-amazon-ebs-test-json"
}]
}

@ -0,0 +1,11 @@
source "amazon-ebs" "basic-test" {
region = "us-east-1"
instance_type = "m3.medium"
source_ami = "ami-76b2a71e"
ssh_username = "ubuntu"
ami_name = "packer-plugin-bundled-amazon-ebs-test"
}
build {
sources = ["source.amazon-ebs.basic-test"]
}

@ -0,0 +1,20 @@
packer {
required_plugins {
amazon = {
source = "github.com/hashicorp/amazon",
version = "~> 1"
}
}
}
source "amazon-ebs" "basic-test" {
region = "us-east-1"
instance_type = "m3.medium"
source_ami = "ami-76b2a71e"
ssh_username = "ubuntu"
ami_name = "packer-plugin-bundled-amazon-ebs-test"
}
build {
sources = ["source.amazon-ebs.basic-test"]
}

@ -87,7 +87,15 @@ func (c *BuildCommand) RunContext(buildCtx context.Context, cla *BuildArgs) int
return ret
}
diags := packerStarter.Initialize(packer.InitializeOptions{})
diags := packerStarter.DetectPluginBinaries()
ret = writeDiags(c.Ui, nil, diags)
if ret != 0 {
return ret
}
diags = packerStarter.Initialize(packer.InitializeOptions{})
bundledDiags := c.DetectBundledPlugins(packerStarter)
diags = append(bundledDiags, diags...)
ret = writeDiags(c.Ui, nil, diags)
if ret != 0 {
return ret

@ -9,7 +9,9 @@ import (
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template"
@ -179,3 +181,176 @@ func (m *Meta) GetConfigFromJSON(cla *MetaArgs) (packer.Handler, int) {
}
return core, ret
}
func (m *Meta) DetectBundledPlugins(handler packer.Handler) hcl.Diagnostics {
var plugins []string
switch h := handler.(type) {
case *packer.Core:
plugins = m.detectBundledPluginsJSON(h)
case *hcl2template.PackerConfig:
plugins = m.detectBundledPluginsHCL2(handler.(*hcl2template.PackerConfig))
}
if len(plugins) == 0 {
return nil
}
buf := &strings.Builder{}
buf.WriteString("This template relies on the use of plugins bundled into the Packer binary.\n")
buf.WriteString("The practice of bundling external plugins into Packer will be removed in an upcoming version.\n\n")
switch h := handler.(type) {
case *packer.Core:
buf.WriteString("To remove this warning and ensure builds keep working you can install these external plugins with the 'packer plugins install' command\n\n")
for _, plugin := range plugins {
fmt.Fprintf(buf, "* packer plugins install %s\n", plugin)
}
buf.WriteString("\nAlternatively, if you upgrade your templates to HCL2, you can use 'packer init' with a 'required_plugins' block to automatically install external plugins.\n\n")
fmt.Fprintf(buf, "You can try HCL2 by running 'packer hcl2_upgrade %s'", h.Template.Path)
case *hcl2template.PackerConfig:
buf.WriteString("To remove this warning, add the following section to your template:\n")
buf.WriteString(m.fixRequiredPlugins(h))
buf.WriteString("\nThen run 'packer init' to manage installation of the plugins")
}
return hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Bundled plugins used",
Detail: buf.String(),
},
}
}
func (m *Meta) detectBundledPluginsJSON(core *packer.Core) []string {
bundledPlugins := map[string]struct{}{}
tmpl := core.Template
if tmpl == nil {
panic("No template parsed. This is a Packer bug which should be reported, please open an issue on the project's issue tracker.")
}
for _, b := range tmpl.Builders {
builderType := fmt.Sprintf("packer-builder-%s", b.Type)
if bundledStatus[builderType] {
bundledPlugins[builderType] = struct{}{}
}
}
for _, p := range tmpl.Provisioners {
provisionerType := fmt.Sprintf("packer-provisioner-%s", p.Type)
if bundledStatus[provisionerType] {
bundledPlugins[provisionerType] = struct{}{}
}
}
for _, pps := range tmpl.PostProcessors {
for _, pp := range pps {
postProcessorType := fmt.Sprintf("packer-post-processor-%s", pp.Type)
if bundledStatus[postProcessorType] {
bundledPlugins[postProcessorType] = struct{}{}
}
}
}
return compileBundledPluginList(bundledPlugins)
}
var knownPluginPrefixes = map[string]string{
"amazon": "github.com/hashicorp/amazon",
"ansible": "github.com/hashicorp/ansible",
"azure": "github.com/hashicorp/azure",
"docker": "github.com/hashicorp/docker",
"googlecompute": "github.com/hashicorp/googlecompute",
"qemu": "github.com/hashicorp/qemu",
"vagrant": "github.com/hashicorp/vagrant",
"vmware": "github.com/hashicorp/vmware",
"vsphere": "github.com/hashicorp/vsphere",
}
func (m *Meta) fixRequiredPlugins(config *hcl2template.PackerConfig) string {
plugins := map[string]struct{}{}
for _, b := range config.Builds {
for _, b := range b.Sources {
for prefix, plugin := range knownPluginPrefixes {
if strings.HasPrefix(b.Type, prefix) {
plugins[plugin] = struct{}{}
}
}
}
for _, p := range b.ProvisionerBlocks {
for prefix, plugin := range knownPluginPrefixes {
if strings.HasPrefix(p.PType, prefix) {
plugins[plugin] = struct{}{}
}
}
}
for _, pps := range b.PostProcessorsLists {
for _, pp := range pps {
for prefix, plugin := range knownPluginPrefixes {
if strings.HasPrefix(pp.PType, prefix) {
plugins[plugin] = struct{}{}
}
}
}
}
}
for _, ds := range config.Datasources {
for prefix, plugin := range knownPluginPrefixes {
if strings.HasPrefix(ds.Type, prefix) {
plugins[plugin] = struct{}{}
}
}
}
retPlugins := make([]string, 0, len(plugins))
for plugin := range plugins {
retPlugins = append(retPlugins, plugin)
}
return generateRequiredPluginsBlock(retPlugins)
}
func (m *Meta) detectBundledPluginsHCL2(config *hcl2template.PackerConfig) []string {
bundledPlugins := map[string]struct{}{}
for _, b := range config.Builds {
for _, src := range b.Sources {
builderType := fmt.Sprintf("packer-builder-%s", src.Type)
if bundledStatus[builderType] {
bundledPlugins[builderType] = struct{}{}
}
}
for _, p := range b.ProvisionerBlocks {
provisionerType := fmt.Sprintf("packer-provisioner-%s", p.PType)
if bundledStatus[provisionerType] {
bundledPlugins[provisionerType] = struct{}{}
}
}
for _, pps := range b.PostProcessorsLists {
for _, pp := range pps {
postProcessorType := fmt.Sprintf("packer-post-processor-%s", pp.PType)
if bundledStatus[postProcessorType] {
bundledPlugins[postProcessorType] = struct{}{}
}
}
}
}
for _, ds := range config.Datasources {
dsType := fmt.Sprintf("packer-datasource-%s", ds.Type)
if bundledStatus[dsType] {
bundledPlugins[dsType] = struct{}{}
}
}
return compileBundledPluginList(bundledPlugins)
}

@ -65,9 +65,17 @@ func (c *ValidateCommand) RunContext(ctx context.Context, cla *ValidateArgs) int
return 0
}
diags := packerStarter.Initialize(packer.InitializeOptions{
diags := packerStarter.DetectPluginBinaries()
ret = writeDiags(c.Ui, nil, diags)
if ret != 0 {
return ret
}
diags = packerStarter.Initialize(packer.InitializeOptions{
SkipDatasourcesExecution: !cla.EvaluateDatasources,
})
bundledDiags := c.DetectBundledPlugins(packerStarter)
diags = append(bundledDiags, diags...)
ret = writeDiags(c.Ui, nil, diags)
if ret != 0 {
return ret

@ -4,6 +4,10 @@
package command
import (
"fmt"
"log"
"strings"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
// Previously core-bundled components, split into their own plugins but
@ -102,6 +106,170 @@ var VendoredPostProcessors = map[string]packersdk.PostProcessor{
"vsphere": new(vspherepostprocessor.PostProcessor),
}
// bundledStatus is used to know if one of the bundled components is loaded from
// an external plugin, or from the bundled plugins.
//
// We keep track of this to produce a warning if a user relies on one
// such plugin, as they will be removed in a later version of Packer.
var bundledStatus = map[string]bool{
"packer-builder-amazon-ebs": false,
"packer-builder-amazon-chroot": false,
"packer-builder-amazon-ebssurrogate": false,
"packer-builder-amazon-ebsvolume": false,
"packer-builder-amazon-instance": false,
"packer-post-processor-amazon-import": false,
"packer-datasource-amazon-ami": false,
"packer-datasource-amazon-secretsmanager": false,
"packer-provisioner-ansible": false,
"packer-provisioner-ansible-local": false,
"packer-provisioner-azure-dtlartifact": false,
"packer-builder-azure-arm": false,
"packer-builder-azure-chroot": false,
"packer-builder-azure-dtl": false,
"packer-builder-docker": false,
"packer-post-processor-docker-import": false,
"packer-post-processor-docker-push": false,
"packer-post-processor-docker-save": false,
"packer-post-processor-docker-tag": false,
"packer-builder-googlecompute": false,
"packer-post-processor-googlecompute-export": false,
"packer-post-processor-googlecompute-import": false,
"packer-builder-qemu": false,
"packer-builder-vagrant": false,
"packer-post-processor-vagrant": false,
"packer-post-processor-vagrant-cloud": false,
"packer-builder-virtualbox-iso": false,
"packer-builder-virtualbox-ovf": false,
"packer-builder-virtualbox-vm": false,
"packer-builder-vmware-iso": false,
"packer-builder-vmware-vmx": false,
"packer-builder-vsphere-clone": false,
"packer-builder-vsphere-iso": false,
"packer-post-processor-vsphere-template": false,
"packer-post-processor-vsphere": false,
}
// TrackBundledPlugin marks a component as loaded from Packer's bundled plugins
// instead of from an externally loaded plugin.
//
// NOTE: `pluginName' must be in the format `packer-<type>-<component_name>'
func TrackBundledPlugin(pluginName string) {
_, exists := bundledStatus[pluginName]
if !exists {
return
}
bundledStatus[pluginName] = true
}
var componentPluginMap = map[string]string{
"packer-builder-amazon-ebs": "github.com/hashicorp/amazon",
"packer-builder-amazon-chroot": "github.com/hashicorp/amazon",
"packer-builder-amazon-ebssurrogate": "github.com/hashicorp/amazon",
"packer-builder-amazon-ebsvolume": "github.com/hashicorp/amazon",
"packer-builder-amazon-instance": "github.com/hashicorp/amazon",
"packer-post-processor-amazon-import": "github.com/hashicorp/amazon",
"packer-datasource-amazon-ami": "github.com/hashicorp/amazon",
"packer-datasource-amazon-secretsmanager": "github.com/hashicorp/amazon",
"packer-provisioner-ansible": "github.com/hashicorp/ansible",
"packer-provisioner-ansible-local": "github.com/hashicorp/ansible",
"packer-provisioner-azure-dtlartifact": "github.com/hashicorp/azure",
"packer-builder-azure-arm": "github.com/hashicorp/azure",
"packer-builder-azure-chroot": "github.com/hashicorp/azure",
"packer-builder-azure-dtl": "github.com/hashicorp/azure",
"packer-builder-docker": "github.com/hashicorp/docker",
"packer-post-processor-docker-import": "github.com/hashicorp/docker",
"packer-post-processor-docker-push": "github.com/hashicorp/docker",
"packer-post-processor-docker-save": "github.com/hashicorp/docker",
"packer-post-processor-docker-tag": "github.com/hashicorp/docker",
"packer-builder-googlecompute": "github.com/hashicorp/googlecompute",
"packer-post-processor-googlecompute-export": "github.com/hashicorp/googlecompute",
"packer-post-processor-googlecompute-import": "github.com/hashicorp/googlecompute",
"packer-builder-qemu": "github.com/hashicorp/qemu",
"packer-builder-vagrant": "github.com/hashicorp/vagrant",
"packer-post-processor-vagrant": "github.com/hashicorp/vagrant",
"packer-post-processor-vagrant-cloud": "github.com/hashicorp/vagrant",
"packer-builder-virtualbox-iso": "github.com/hashicorp/virtualbox",
"packer-builder-virtualbox-ovf": "github.com/hashicorp/virtualbox",
"packer-builder-virtualbox-vm": "github.com/hashicorp/virtualbox",
"packer-builder-vmware-iso": "github.com/hashicorp/vmware",
"packer-builder-vmware-vmx": "github.com/hashicorp/vmware",
"packer-builder-vsphere-clone": "github.com/hashicorp/vsphere",
"packer-builder-vsphere-iso": "github.com/hashicorp/vsphere",
"packer-post-processor-vsphere-template": "github.com/hashicorp/vsphere",
"packer-post-processor-vsphere": "github.com/hashicorp/vsphere",
}
// compileBundledPluginList returns a list of plugins to import in a config
//
// This only works on bundled plugins and serves as a way to inform users that
// they should not rely on a bundled plugin anymore, but give them recommendations
// on how to manage those plugins instead.
func compileBundledPluginList(componentMap map[string]struct{}) []string {
plugins := map[string]struct{}{}
for component := range componentMap {
plugin, ok := componentPluginMap[component]
if !ok {
log.Printf("Unknown bundled plugin component: %q", component)
continue
}
plugins[plugin] = struct{}{}
}
pluginList := make([]string, 0, len(plugins))
for plugin := range plugins {
pluginList = append(pluginList, plugin)
}
return pluginList
}
func generateRequiredPluginsBlock(plugins []string) string {
if len(plugins) == 0 {
return ""
}
buf := &strings.Builder{}
buf.WriteString(`
packer {
required_plugins {`)
for _, plugin := range plugins {
pluginName := strings.Replace(plugin, "github.com/hashicorp/", "", 1)
fmt.Fprintf(buf, `
%s = {
source = %q
version = "~> 1"
}`, pluginName, plugin)
}
buf.WriteString(`
}
}
`)
return buf.String()
}
// Upon init lets load up any plugins that were vendored manually into the default
// set of plugins.
func init() {

@ -153,6 +153,7 @@ func (c *config) discoverInternalComponents() error {
for builder := range command.Builders {
builder := builder
if !c.Plugins.Builders.Has(builder) {
command.TrackBundledPlugin(fmt.Sprintf("packer-builder-%s", builder))
bin := fmt.Sprintf("%s%splugin%spacker-builder-%s",
packerPath, PACKERSPACE, PACKERSPACE, builder)
c.Plugins.Builders.Set(builder, func() (packersdk.Builder, error) {
@ -164,6 +165,7 @@ func (c *config) discoverInternalComponents() error {
for provisioner := range command.Provisioners {
provisioner := provisioner
if !c.Plugins.Provisioners.Has(provisioner) {
command.TrackBundledPlugin(fmt.Sprintf("packer-provisioner-%s", provisioner))
bin := fmt.Sprintf("%s%splugin%spacker-provisioner-%s",
packerPath, PACKERSPACE, PACKERSPACE, provisioner)
c.Plugins.Provisioners.Set(provisioner, func() (packersdk.Provisioner, error) {
@ -175,6 +177,7 @@ func (c *config) discoverInternalComponents() error {
for postProcessor := range command.PostProcessors {
postProcessor := postProcessor
if !c.Plugins.PostProcessors.Has(postProcessor) {
command.TrackBundledPlugin(fmt.Sprintf("packer-post-processor-%s", postProcessor))
bin := fmt.Sprintf("%s%splugin%spacker-post-processor-%s",
packerPath, PACKERSPACE, PACKERSPACE, postProcessor)
c.Plugins.PostProcessors.Set(postProcessor, func() (packersdk.PostProcessor, error) {
@ -186,6 +189,7 @@ func (c *config) discoverInternalComponents() error {
for dataSource := range command.Datasources {
dataSource := dataSource
if !c.Plugins.DataSources.Has(dataSource) {
command.TrackBundledPlugin(fmt.Sprintf("packer-datasource-%s", dataSource))
bin := fmt.Sprintf("%s%splugin%spacker-datasource-%s",
packerPath, PACKERSPACE, PACKERSPACE, dataSource)
c.Plugins.DataSources.Set(dataSource, func() (packersdk.Datasource, error) {

@ -308,19 +308,8 @@ func filterVarsFromLogs(inputOrLocal Variables) {
}
func (cfg *PackerConfig) Initialize(opts packer.InitializeOptions) hcl.Diagnostics {
var diags hcl.Diagnostics
// enable packer to start plugins requested in required_plugins.
moreDiags := cfg.detectPluginBinaries()
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
return diags
}
moreDiags = cfg.InputVariables.ValidateValues()
diags = append(diags, moreDiags...)
moreDiags = cfg.LocalVariables.ValidateValues()
diags = append(diags, moreDiags...)
diags := cfg.InputVariables.ValidateValues()
diags = append(diags, cfg.LocalVariables.ValidateValues()...)
diags = append(diags, cfg.evaluateDatasources(opts.SkipDatasourcesExecution)...)
diags = append(diags, checkForDuplicateLocalDefinition(cfg.LocalBlocks)...)
diags = append(diags, cfg.evaluateLocalVariables(cfg.LocalBlocks)...)

@ -54,7 +54,7 @@ func (cfg *PackerConfig) PluginRequirements() (plugingetter.Requirements, hcl.Di
return reqs, diags
}
func (cfg *PackerConfig) detectPluginBinaries() hcl.Diagnostics {
func (cfg *PackerConfig) DetectPluginBinaries() hcl.Diagnostics {
opts := plugingetter.ListInstallationsOptions{
FromFolders: cfg.parser.PluginConfig.KnownPluginFolders,
BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{

@ -132,6 +132,12 @@ func NewCore(c *CoreConfig) *Core {
return core
}
// DetectPluginBinaries is used to load required plugins from the template,
// since it is unsupported in JSON, this is essentially a no-op.
func (c *Core) DetectPluginBinaries() hcl.Diagnostics {
return nil
}
func (c *Core) Initialize(_ InitializeOptions) hcl.Diagnostics {
err := c.initialize()
if err != nil {

@ -40,6 +40,12 @@ type InitializeOptions struct {
SkipDatasourcesExecution bool
}
type PluginBinaryDetector interface {
// DetectPluginBinaries is used only for HCL2 templates, and loads required
// plugins if specified.
DetectPluginBinaries() hcl.Diagnostics
}
// The Handler handles all Packer things. This interface reflects the Packer
// commands, ex: init, console ( evaluate ), fix config, inspect config, etc. To
// run a build we will start the builds and then the core of Packer handles
@ -53,6 +59,7 @@ type Handler interface {
BuildGetter
ConfigFixer
ConfigInspector
PluginBinaryDetector
}
//go:generate enumer -type FixConfigMode

Loading…
Cancel
Save