diff --git a/internal/backend/remote-state/kubernetes/backend.go b/internal/backend/remote-state/kubernetes/backend.go index 85d3037c67..8ba390dfb1 100644 --- a/internal/backend/remote-state/kubernetes/backend.go +++ b/internal/backend/remote-state/kubernetes/backend.go @@ -5,16 +5,13 @@ package kubernetes import ( "bytes" - "context" "fmt" "log" "os" "path/filepath" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" - "github.com/hashicorp/terraform/version" "github.com/mitchellh/go-homedir" + "github.com/zclconf/go-cty/cty" k8sSchema "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" @@ -22,6 +19,12 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" ) // Modified from github.com/terraform-providers/terraform-provider-kubernetes @@ -44,158 +47,191 @@ var ( // New creates a new backend for kubernetes remote state. func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "secret_suffix": { - Type: schema.TypeString, - Required: true, - Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.", - }, - "labels": { - Type: schema.TypeMap, - Optional: true, - Description: "Map of additional labels to be applied to the secret.", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "namespace": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_NAMESPACE", "default"), - Description: "Namespace to store the secret in.", - }, - "in_cluster_config": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_IN_CLUSTER_CONFIG", false), - Description: "Used to authenticate to the cluster from inside a pod.", - }, - "load_config_file": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true), - Description: "Load local kubeconfig.", - }, - "host": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""), - Description: "The hostname (in form of URI) of Kubernetes master.", - }, - "username": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""), - Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", - }, - "password": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""), - Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", - }, - "insecure": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false), - Description: "Whether server should be accessed without verifying the TLS certificate.", - }, - "client_certificate": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""), - Description: "PEM-encoded client certificate for TLS authentication.", - }, - "client_key": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""), - Description: "PEM-encoded client certificate key for TLS authentication.", - }, - "cluster_ca_certificate": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""), - Description: "PEM-encoded root certificates bundle for TLS authentication.", - }, - "config_paths": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.", - }, - "config_path": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", ""), - Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.", - }, - "config_context": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""), - }, - "config_context_auth_info": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""), - Description: "", - }, - "config_context_cluster": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""), - Description: "", - }, - "token": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""), - Description: "Token to authentifcate a service account.", - }, - "proxy_url": { - Type: schema.TypeString, - Optional: true, - Description: "URL to the proxy to be used for all API requests", - DefaultFunc: schema.EnvDefaultFunc("KUBE_PROXY_URL", ""), - }, - "exec": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "api_version": { - Type: schema.TypeString, - Required: true, - }, - "command": { - Type: schema.TypeString, - Required: true, - }, - "env": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "args": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "secret_suffix": { + Type: cty.String, + Required: true, + Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.", + }, + "labels": { + Type: cty.Map(cty.String), + Optional: true, + Description: "Map of additional labels to be applied to the secret.", + }, + "namespace": { + Type: cty.String, + Optional: true, + Description: "Namespace to store the secret in.", + }, + "in_cluster_config": { + Type: cty.Bool, + Optional: true, + Description: "Used to authenticate to the cluster from inside a pod.", + }, + "load_config_file": { + Type: cty.Bool, + Optional: true, + Description: "Load local kubeconfig.", + }, + "host": { + Type: cty.String, + Optional: true, + Description: "The hostname (in form of URI) of Kubernetes master.", + }, + "username": { + Type: cty.String, + Optional: true, + Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", + }, + "password": { + Type: cty.String, + Optional: true, + Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", + }, + "insecure": { + Type: cty.Bool, + Optional: true, + Description: "Whether server should be accessed without verifying the TLS certificate.", + }, + "client_certificate": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded client certificate for TLS authentication.", + }, + "client_key": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded client certificate key for TLS authentication.", + }, + "cluster_ca_certificate": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded root certificates bundle for TLS authentication.", + }, + "config_paths": { + Type: cty.List(cty.String), + Optional: true, + Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.", + }, + "config_path": { + Type: cty.String, + Optional: true, + Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.", + }, + "config_context": { + Type: cty.String, + Optional: true, + }, + "config_context_auth_info": { + Type: cty.String, + Optional: true, + Description: "", + }, + "config_context_cluster": { + Type: cty.String, + Optional: true, + Description: "", + }, + "token": { + Type: cty.String, + Optional: true, + Description: "Token to authentifcate a service account.", + }, + "proxy_url": { + Type: cty.String, + Optional: true, + Description: "URL to the proxy to be used for all API requests", + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "exec": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "api_version": { + Type: cty.String, + Required: true, + }, + "command": { + Type: cty.String, + Required: true, + }, + "env": { + Type: cty.Map(cty.String), + Optional: true, + }, + "args": { + Type: cty.List(cty.String), + Optional: true, + }, + }, }, }, }, - Description: "Use a credential plugin to authenticate.", + }, + SDKLikeDefaults: backendbase.SDKLikeDefaults{ + "namespace": { + EnvVars: []string{"KUBE_NAMESPACE"}, + Fallback: "default", + }, + "in_cluster_config": { + EnvVars: []string{"KUBE_IN_CLUSTER_CONFIG"}, + Fallback: "false", + }, + "load_config_file": { + EnvVars: []string{"KUBE_LOAD_CONFIG_FILE"}, + Fallback: "true", + }, + "host": { + EnvVars: []string{"KUBE_HOST"}, + }, + "username": { + EnvVars: []string{"KUBE_USER"}, + }, + "password": { + EnvVars: []string{"KUBE_PASSWORD"}, + }, + "insecure": { + EnvVars: []string{"KUBE_INSECURE"}, + Fallback: "false", + }, + "client_certificate": { + EnvVars: []string{"KUBE_CLIENT_CERT_DATA"}, + }, + "client_key": { + EnvVars: []string{"KUBE_CLIENT_KEY_DATA"}, + }, + "cluster_ca_certificate": { + EnvVars: []string{"KUBE_CLUSTER_CA_CERT_DATA"}, + }, + "config_path": { + EnvVars: []string{"KUBE_CONFIG_PATH"}, + }, + "config_context": { + EnvVars: []string{"KUBE_CTX"}, + }, + "config_context_auth_info": { + EnvVars: []string{"KUBE_CTX_AUTH_INFO"}, + }, + "config_context_cluster": { + EnvVars: []string{"KUBE_CTX_CLUSTER"}, + }, + "token": { + EnvVars: []string{"KUBE_TOKEN"}, + }, + "proxy_url": { + EnvVars: []string{"KUBE_PROXY_URL"}, + }, }, }, } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result } type Backend struct { - *schema.Backend + backendbase.Base // The fields below are set from configure kubernetesSecretClient dynamic.ResourceInterface @@ -234,68 +270,69 @@ func (b Backend) KubernetesLeaseClient() (coordinationv1.LeaseInterface, error) return b.kubernetesLeaseClient, nil } -func (b *Backend) configure(ctx context.Context) error { +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { if b.config != nil { return nil } - // Grab the resource data - data := schema.FromContextBackendConfig(ctx) + data := backendbase.NewSDKLikeData(configVal) cfg, err := getInitialConfig(data) if err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } // Overriding with static configuration cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", version.String()) - if v, ok := data.GetOk("host"); ok { - cfg.Host = v.(string) - } - if v, ok := data.GetOk("username"); ok { - cfg.Username = v.(string) + if v := data.String("host"); v != "" { + cfg.Host = v } - if v, ok := data.GetOk("password"); ok { - cfg.Password = v.(string) + if v := data.String("username"); v != "" { + cfg.Username = v } - if v, ok := data.GetOk("insecure"); ok { - cfg.Insecure = v.(bool) + if v := data.String("password"); v != "" { + cfg.Password = v } - if v, ok := data.GetOk("cluster_ca_certificate"); ok { - cfg.CAData = bytes.NewBufferString(v.(string)).Bytes() + cfg.Insecure = data.Bool("insecure") + if v := data.String("cluster_ca_certificate"); v != "" { + cfg.CAData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("client_certificate"); ok { - cfg.CertData = bytes.NewBufferString(v.(string)).Bytes() + if v := data.String("client_certificate"); v != "" { + cfg.CertData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("client_key"); ok { - cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes() + if v := data.String("client_key"); v != "" { + cfg.KeyData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("token"); ok { - cfg.BearerToken = v.(string) + if v := data.String("token"); v != "" { + cfg.BearerToken = v } - if v, ok := data.GetOk("labels"); ok { + if v := data.GetAttr("labels", cty.Map(cty.String)); !v.IsNull() { labels := map[string]string{} - for k, vv := range v.(map[string]interface{}) { - labels[k] = vv.(string) + for it := v.ElementIterator(); it.Next(); { + kV, vV := it.Element() + if vV.IsNull() { + vV = cty.StringVal("") + } + labels[kV.AsString()] = vV.AsString() } b.labels = labels } - ns := data.Get("namespace").(string) + ns := data.String("namespace") b.namespace = ns - b.nameSuffix = data.Get("secret_suffix").(string) + b.nameSuffix = data.String("secret_suffix") b.config = cfg return nil } -func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { +func getInitialConfig(data backendbase.SDKLikeData) (*restclient.Config, error) { var cfg *restclient.Config var err error - inCluster := data.Get("in_cluster_config").(bool) + inCluster := data.Bool("in_cluster_config") if inCluster { cfg, err = restclient.InClusterConfig() if err != nil { @@ -314,16 +351,14 @@ func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { return cfg, err } -func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { +func tryLoadingConfigFile(d backendbase.SDKLikeData) (*restclient.Config, error) { loader := &clientcmd.ClientConfigLoadingRules{} configPaths := []string{} - if v, ok := d.Get("config_path").(string); ok && v != "" { + if v := d.String("config_path"); v != "" { configPaths = []string{v} - } else if v, ok := d.Get("config_paths").([]interface{}); ok && len(v) > 0 { - for _, p := range v { - configPaths = append(configPaths, p.(string)) - } + } else if v := d.GetAttr("config_paths", cty.List(cty.String)); !v.IsNull() { + configPaths = append(configPaths, decodeListOfString(v)...) } else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" { configPaths = filepath.SplitList(v) } @@ -348,46 +383,56 @@ func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { overrides := &clientcmd.ConfigOverrides{} ctxSuffix := "; default context" - ctx, ctxOk := d.GetOk("config_context") - authInfo, authInfoOk := d.GetOk("config_context_auth_info") - cluster, clusterOk := d.GetOk("config_context_cluster") - if ctxOk || authInfoOk || clusterOk { + configCtx := d.String("config_context") + authInfo := d.String("config_context_auth_info") + cluster := d.String("config_context_cluster") + if configCtx != "" || authInfo != "" || cluster != "" { ctxSuffix = "; overriden context" - if ctxOk { - overrides.CurrentContext = ctx.(string) + if configCtx != "" { + overrides.CurrentContext = configCtx ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext) log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext) } overrides.Context = clientcmdapi.Context{} - if authInfoOk { - overrides.Context.AuthInfo = authInfo.(string) + if authInfo != "" { + overrides.Context.AuthInfo = authInfo ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo) } - if clusterOk { - overrides.Context.Cluster = cluster.(string) + if cluster != "" { + overrides.Context.Cluster = cluster ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster) } log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context) } - if v, ok := d.GetOk("exec"); ok { - exec := &clientcmdapi.ExecConfig{} - if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok { - exec.APIVersion = spec["api_version"].(string) - exec.Command = spec["command"].(string) - exec.Args = expandStringSlice(spec["args"].([]interface{})) - for kk, vv := range spec["env"].(map[string]interface{}) { - exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)}) + // exec is a nested block with nesting mode NestingSingle, so GetAttr + // will return a value of an object type that will be null if the block + // isn't present at all. + if v := d.GetAttr("exec", cty.DynamicPseudoType); !v.IsNull() { + spec := backendbase.NewSDKLikeData(v) + exec := &clientcmdapi.ExecConfig{ + APIVersion: spec.String("api_version"), + Command: spec.String("command"), + Args: decodeListOfString(spec.GetAttr("args", cty.List(cty.String))), + } + if envMap := spec.GetAttr("env", cty.Map(cty.String)); !envMap.IsNull() { + for it := envMap.ElementIterator(); it.Next(); { + kV, vV := it.Element() + if vV.IsNull() { + vV = cty.StringVal("") + } + exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{ + Name: kV.AsString(), + Value: vV.AsString(), + }) } - } else { - return nil, fmt.Errorf("Failed to parse exec") } overrides.AuthInfo.Exec = exec } - if v, ok := d.GetOk("proxy_url"); ok { - overrides.ClusterDefaults.ProxyURL = v.(string) + if v := d.String("proxy_url"); v != "" { + overrides.ClusterDefaults.ProxyURL = v } cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) @@ -404,15 +449,18 @@ func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { return cfg, nil } -func expandStringSlice(s []interface{}) []string { - result := make([]string, len(s), len(s)) - for k, v := range s { - // Handle the Terraform parser bug which turns empty strings in lists to nil. - if v == nil { - result[k] = "" +func decodeListOfString(v cty.Value) []string { + if v.IsNull() { + return nil + } + ret := make([]string, 0, v.LengthInt()) + for it := v.ElementIterator(); it.Next(); { + _, vV := it.Element() + if vV.IsNull() { + ret = append(ret, "") } else { - result[k] = v.(string) + ret = append(ret, vV.AsString()) } } - return result + return ret } diff --git a/internal/backend/remote-state/kubernetes/go.mod b/internal/backend/remote-state/kubernetes/go.mod index f75f81932b..bb51e40246 100644 --- a/internal/backend/remote-state/kubernetes/go.mod +++ b/internal/backend/remote-state/kubernetes/go.mod @@ -4,8 +4,8 @@ go 1.22.5 require ( github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 - github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 github.com/mitchellh/go-homedir v1.1.0 + github.com/zclconf/go-cty v1.15.0 k8s.io/api v0.25.5 k8s.io/apimachinery v0.25.5 k8s.io/client-go v0.25.5 @@ -49,10 +49,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -60,7 +57,6 @@ require ( github.com/spf13/afero v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.4 // indirect - github.com/zclconf/go-cty v1.15.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.23.0 // indirect @@ -99,6 +95,4 @@ replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 -replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy - replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/kubernetes/go.sum b/internal/backend/remote-state/kubernetes/go.sum index 47c5b4b80e..80d0a20d08 100644 --- a/internal/backend/remote-state/kubernetes/go.sum +++ b/internal/backend/remote-state/kubernetes/go.sum @@ -261,18 +261,12 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=