1
1
Fork 0
mirror of https://github.com/mcuadros/ascode synced 2024-05-08 16:46:18 +02:00

starlark/types: Computed renamed to Attribute, added documentation

This commit is contained in:
Máximo Cuadros 2020-03-26 07:13:24 +01:00
parent be125c9f03
commit c3cd97a5fc
No known key found for this signature in database
GPG Key ID: 17A5DFEDC735AE4B
16 changed files with 290 additions and 214 deletions

View File

@ -63,7 +63,7 @@ func NewRuntime(pm *terraform.PluginManager) *Runtime {
"provisioner": types.BuiltinProvisioner(),
"backend": types.BuiltinBackend(),
"hcl": types.BuiltinHCL(),
"fn": types.BuiltinFunctionComputed(),
"fn": types.BuiltinFunctionAttribute(),
"evaluate": types.BuiltinEvaluate(),
"struct": starlark.NewBuiltin("struct", starlarkstruct.Make),
"module": starlark.NewBuiltin("module", starlarkstruct.MakeModule),

205
starlark/types/attribute.go Normal file
View File

@ -0,0 +1,205 @@
package types
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"go.starlark.net/starlark"
)
// sTring alias required to avoid name collision with the method String.
type sString = starlark.String
// Attribute is a reference to an argument of a Resource. Used mainly
// for Computed arguments of Resources.
//
// outline: types
// types:
// Attribute
// Attribute is a reference to an argument of a Resource. Used mainly
// for Computed arguments of Resources.
//
// Attribute behaves as type of the argument represented, this means
// that their can be assigned to other resource arguments of the same
// type. And, if the type is a list are indexables.
//
// examples:
// attribute.star
//
// fields:
// __resource__ Resource
// Resource of the attribute.
// __type__ string
// Type of the attribute. Eg.: `string`
type Attribute struct {
r *Resource
t cty.Type
name string
path string
sString
}
var _ starlark.Value = &Attribute{}
var _ starlark.HasAttrs = &Attribute{}
var _ starlark.Indexable = &Attribute{}
var _ starlark.Comparable = &Attribute{}
// NewAttribute returns a new Attribute for a given value or block of a Resource.
func NewAttribute(r *Resource, t cty.Type, name string) *Attribute {
var parts []string
var path string
child := r
for {
if child.parent.kind == ProviderKind {
if child.kind == ResourceKind {
path = fmt.Sprintf("%s.%s", child.typ, child.Name())
} else {
path = fmt.Sprintf("%s.%s.%s", child.kind, child.typ, child.Name())
}
break
}
parts = append(parts, child.typ)
child = child.parent
}
for i := len(parts) - 1; i >= 0; i-- {
path += "." + parts[i]
}
// handling of MaxItems equals 1
block, ok := r.parent.block.BlockTypes[r.typ]
if ok && block.MaxItems == 1 {
name = "0." + name
}
return NewAttributeWithPath(r, t, name, path+"."+name)
}
func NewAttributeWithPath(r *Resource, t cty.Type, name, path string) *Attribute {
return &Attribute{
r: r,
t: t,
name: name,
path: path,
sString: starlark.String(fmt.Sprintf("${%s}", path)),
}
}
// Type honors the starlark.Value interface.
func (c *Attribute) Type() string {
return fmt.Sprintf("Attribute<%s>", MustTypeFromCty(c.t).Starlark())
}
func (c *Attribute) InnerType() *Type {
t, _ := NewTypeFromCty(c.t)
return t
}
// Attr honors the starlark.HasAttrs interface.
func (c *Attribute) Attr(name string) (starlark.Value, error) {
switch name {
case "__resource__":
return c.r, nil
case "__type__":
return starlark.String(MustTypeFromCty(c.t).Starlark()), nil
}
if !c.t.IsObjectType() {
return nil, fmt.Errorf("%s it's not a object", c.Type())
}
if !c.t.HasAttribute(name) {
errmsg := fmt.Sprintf("%s has no .%s field", c.Type(), name)
return nil, starlark.NoSuchAttrError(errmsg)
}
path := fmt.Sprintf("%s.%s", c.path, name)
return NewAttributeWithPath(c.r, c.t.AttributeType(name), name, path), nil
}
// AttrNames honors the starlark.HasAttrs interface.
func (c *Attribute) AttrNames() []string {
return []string{"__resource__", "__type__"}
}
func (c *Attribute) doNested(name, path string, t cty.Type, index int) *Attribute {
return &Attribute{
r: c.r,
t: t,
name: c.name,
}
}
// Index honors the starlark.Indexable interface.
func (c *Attribute) Index(i int) starlark.Value {
path := fmt.Sprintf("%s.%d", c.path, i)
if c.t.IsSetType() {
return NewAttributeWithPath(c.r, *c.t.SetElementType(), c.name, path)
}
if c.t.IsListType() {
return NewAttributeWithPath(c.r, *c.t.ListElementType(), c.name, path)
}
return starlark.None
}
// Len honors the starlark.Indexable interface.
func (c *Attribute) Len() int {
if !c.t.IsSetType() && !c.t.IsListType() {
return 0
}
return 1024
}
// BuiltinFunctionAttribute returns a built-in function that wraps Attributes
// in HCL functions.
//
// outline: types
// functions:
// fn(name, target) Attribute
// Fn wraps an Attribute in a HCL function. Since the Attributes value
// are only available in the `apply` phase of Terraform, the only method
// to manipulate this values is using the Terraform
// [HCL functions](https://www.terraform.io/docs/configuration/functions.html).
//
//
// params:
// name string
// Name of the HCL function to be applied. Eg.: `base64encode`
// target Attribute
// Target Attribute of the HCL function.
//
func BuiltinFunctionAttribute() starlark.Value {
// TODO(mcuadros): implement multiple arguments support.
return starlark.NewBuiltin("fn", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var function starlark.String
var computed *Attribute
switch len(args) {
case 2:
var ok bool
function, ok = args.Index(0).(starlark.String)
if !ok {
return nil, fmt.Errorf("expected string, got %s", args.Index(0).Type())
}
computed, ok = args.Index(1).(*Attribute)
if !ok {
return nil, fmt.Errorf("expected Attribute, got %s", args.Index(1).Type())
}
default:
return nil, fmt.Errorf("unexpected positional arguments count")
}
path := fmt.Sprintf("%s(%s)", function.GoString(), computed.path)
return NewAttributeWithPath(computed.r, computed.t, computed.name, path), nil
})
}

View File

@ -0,0 +1,9 @@
package types
import (
"testing"
)
func TestAttribute(t *testing.T) {
doTest(t, "testdata/attribute.star")
}

View File

@ -68,6 +68,7 @@ var _ starlark.HasAttrs = &ResourceCollection{}
var _ starlark.Callable = &ResourceCollection{}
var _ starlark.Comparable = &ResourceCollection{}
// NewResourceCollection returns a new ResourceCollection for the given values.
func NewResourceCollection(
typ string, k Kind, block *configschema.Block, provider *Provider, parent *Resource,
) *ResourceCollection {
@ -271,6 +272,7 @@ var _ starlark.HasAttrs = &ProviderCollection{}
var _ starlark.Callable = &ProviderCollection{}
var _ starlark.Comparable = &ProviderCollection{}
// NewProviderCollection returns a new ProviderCollection.
func NewProviderCollection(pm *terraform.PluginManager) *ProviderCollection {
return &ProviderCollection{
pm: pm,

View File

@ -1,162 +0,0 @@
package types
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"go.starlark.net/starlark"
)
type sString = starlark.String
type Computed struct {
r *Resource
t cty.Type
name string
path string
sString
}
var _ starlark.Value = &Computed{}
var _ starlark.HasAttrs = &Computed{}
var _ starlark.Indexable = &Computed{}
var _ starlark.Comparable = &Computed{}
func NewComputed(r *Resource, t cty.Type, name string) *Computed {
var parts []string
var path string
child := r
for {
if child.parent.kind == ProviderKind {
if child.kind == ResourceKind {
path = fmt.Sprintf("%s.%s", child.typ, child.Name())
} else {
path = fmt.Sprintf("%s.%s.%s", child.kind, child.typ, child.Name())
}
break
}
parts = append(parts, child.typ)
child = child.parent
}
for i := len(parts) - 1; i >= 0; i-- {
path += "." + parts[i]
}
// handling of MaxItems equals 1
block, ok := r.parent.block.BlockTypes[r.typ]
if ok && block.MaxItems == 1 {
name = "0." + name
}
return NewComputedWithPath(r, t, name, path+"."+name)
}
func NewComputedWithPath(r *Resource, t cty.Type, name, path string) *Computed {
return &Computed{
r: r,
t: t,
name: name,
path: path,
sString: starlark.String(fmt.Sprintf("${%s}", path)),
}
}
// Type honors the starlark.Value interface.
func (c *Computed) Type() string {
return fmt.Sprintf("Computed<%s>", MustTypeFromCty(c.t).Starlark())
}
func (c *Computed) InnerType() *Type {
t, _ := NewTypeFromCty(c.t)
return t
}
// Attr honors the starlark.HasAttrs interface.
func (c *Computed) Attr(name string) (starlark.Value, error) {
switch name {
case "__resource__":
return c.r, nil
case "__type__":
return starlark.String(MustTypeFromCty(c.t).Starlark()), nil
}
if !c.t.IsObjectType() {
return nil, nil
}
if !c.t.HasAttribute(name) {
return nil, nil
}
path := fmt.Sprintf("%s.%s", c.path, name)
return NewComputedWithPath(c.r, c.t.AttributeType(name), name, path), nil
}
// AttrNames honors the starlark.HasAttrs interface.
func (c *Computed) AttrNames() []string {
return []string{"__resource__", "__type__"}
}
func (c *Computed) doNested(name, path string, t cty.Type, index int) *Computed {
return &Computed{
r: c.r,
t: t,
name: c.name,
}
}
// Index honors the starlark.Indexable interface.
func (c *Computed) Index(i int) starlark.Value {
path := fmt.Sprintf("%s.%d", c.path, i)
if c.t.IsSetType() {
return NewComputedWithPath(c.r, *c.t.SetElementType(), c.name, path)
}
if c.t.IsListType() {
return NewComputedWithPath(c.r, *c.t.ListElementType(), c.name, path)
}
return starlark.None
}
// Len honors the starlark.Indexable interface.
func (c *Computed) Len() int {
if !c.t.IsSetType() && !c.t.IsListType() {
return 0
}
return 1024
}
func BuiltinFunctionComputed() starlark.Value {
return starlark.NewBuiltin("fn", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var function starlark.String
var computed *Computed
switch len(args) {
case 2:
var ok bool
function, ok = args.Index(0).(starlark.String)
if !ok {
return nil, fmt.Errorf("expected string, got %s", args.Index(0).Type())
}
computed, ok = args.Index(1).(*Computed)
if !ok {
return nil, fmt.Errorf("expected Computed, got %s", args.Index(1).Type())
}
default:
return nil, fmt.Errorf("unexpected positional arguments count")
}
path := fmt.Sprintf("%s(%s)", function.GoString(), computed.path)
return NewComputedWithPath(computed.r, computed.t, computed.name, path), nil
})
}

View File

@ -1,9 +0,0 @@
package types
import (
"testing"
)
func TestComputed(t *testing.T) {
doTest(t, "testdata/computed.star")
}

View File

@ -11,6 +11,7 @@ import (
"go.starlark.net/starlark"
)
// HCLCompatible defines if the struct is suitable of by encoded in HCL.
type HCLCompatible interface {
ToHCL(b *hclwrite.Body)
}
@ -44,6 +45,7 @@ func BuiltinHCL() starlark.Value {
})
}
// ToHCL honors the HCLCompatible interface.
func (s *Terraform) ToHCL(b *hclwrite.Body) {
if s.b != nil {
s.b.ToHCL(b)
@ -52,6 +54,7 @@ func (s *Terraform) ToHCL(b *hclwrite.Body) {
s.p.ToHCL(b)
}
// ToHCL honors the HCLCompatible interface.
func (s *Dict) ToHCL(b *hclwrite.Body) {
for _, v := range s.Keys() {
p, _, _ := s.Get(v)
@ -64,6 +67,7 @@ func (s *Dict) ToHCL(b *hclwrite.Body) {
}
}
// ToHCL honors the HCLCompatible interface.
func (s *Provider) ToHCL(b *hclwrite.Body) {
block := b.AppendNewBlock("provider", []string{s.typ})
@ -76,11 +80,13 @@ func (s *Provider) ToHCL(b *hclwrite.Body) {
b.AppendNewline()
}
// ToHCL honors the HCLCompatible interface.
func (s *Provisioner) ToHCL(b *hclwrite.Body) {
block := b.AppendNewBlock("provisioner", []string{s.typ})
s.Resource.doToHCLAttributes(block.Body())
}
// ToHCL honors the HCLCompatible interface.
func (s *Backend) ToHCL(b *hclwrite.Body) {
parent := b.AppendNewBlock("terraform", nil)
@ -89,6 +95,7 @@ func (s *Backend) ToHCL(b *hclwrite.Body) {
b.AppendNewline()
}
// ToHCL honors the HCLCompatible interface.
func (t *ResourceCollectionGroup) ToHCL(b *hclwrite.Body) {
names := make(sort.StringSlice, len(t.collections))
var i int
@ -103,12 +110,14 @@ func (t *ResourceCollectionGroup) ToHCL(b *hclwrite.Body) {
}
}
// ToHCL honors the HCLCompatible interface.
func (c *ResourceCollection) ToHCL(b *hclwrite.Body) {
for i := 0; i < c.Len(); i++ {
c.Index(i).(*Resource).ToHCL(b)
}
}
// ToHCL honors the HCLCompatible interface.
func (r *Resource) ToHCL(b *hclwrite.Body) {
if len(b.Blocks()) != 0 || len(b.Attributes()) != 0 {
b.AppendNewline()
@ -142,7 +151,7 @@ func (r *Resource) doToHCLAttributes(body *hclwrite.Body) {
return nil
}
if c, ok := v.v.(*Computed); ok {
if c, ok := v.v.(*Attribute); ok {
body.SetAttributeTraversal(v.Name, hcl.Traversal{
hcl.TraverseRoot{Name: c.String()},
})
@ -168,7 +177,7 @@ func (r *Resource) doToHCLAttributes(body *hclwrite.Body) {
}
func (r *Resource) doToHCLDependencies(body *hclwrite.Body) {
if len(r.dependenies) == 0 {
if len(r.dependencies) == 0 {
return
}
@ -184,8 +193,8 @@ func (r *Resource) doToHCLDependencies(body *hclwrite.Body) {
Type: hclsyntax.TokenOBrack, Bytes: []byte{'['},
})
l := len(r.dependenies)
for i, dep := range r.dependenies {
l := len(r.dependencies)
for i, dep := range r.dependencies {
name := fmt.Sprintf("%s.%s", dep.typ, dep.Name())
toks = append(toks, &hclwrite.Token{
Type: hclsyntax.TokenIdent, Bytes: []byte(name),

View File

@ -14,6 +14,7 @@ import (
)
const (
// PluginManagerLocal is the key of the terraform.PluginManager in the thread.
PluginManagerLocal = "plugin_manager"
)
@ -206,15 +207,15 @@ func (p *Provider) AttrNames() []string {
}
// CompareSameType honors starlark.Comparable interface.
func (x *Provider) CompareSameType(op syntax.Token, y_ starlark.Value, depth int) (bool, error) {
y := y_.(*Provider)
func (p *Provider) CompareSameType(op syntax.Token, yv starlark.Value, depth int) (bool, error) {
y := yv.(*Provider)
switch op {
case syntax.EQL:
return x == y, nil
return p == y, nil
case syntax.NEQ:
return x != y, nil
return p != y, nil
default:
return false, fmt.Errorf("%s %s %s not implemented", x.Type(), op, y.Type())
return false, fmt.Errorf("%s %s %s not implemented", p.Type(), op, y.Type())
}
}

View File

@ -72,7 +72,7 @@ func doTestPrint(t *testing.T, filename string, print func(*starlark.Thread, str
"provisioner": BuiltinProvisioner(),
"backend": BuiltinBackend(),
"hcl": BuiltinHCL(),
"fn": BuiltinFunctionComputed(),
"fn": BuiltinFunctionAttribute(),
"evaluate": BuiltinEvaluate(),
"tf": NewTerraform(pm),
}

View File

@ -41,6 +41,7 @@ func (k Kind) IsProviderRelated() bool {
return false
}
// Resource Kind constants.
const (
ProviderKind Kind = "provider"
ProvisionerKind Kind = "provisioner"
@ -188,7 +189,7 @@ type Resource struct {
provider *Provider
parent *Resource
dependenies []*Resource
dependencies []*Resource
provisioners []*Provisioner
}
@ -329,7 +330,7 @@ func (r *Resource) attrBlock(name string, b *configschema.NestedBlock) (starlark
func (r *Resource) attrValue(name string, attr *configschema.Attribute) (starlark.Value, error) {
if attr.Computed {
if !r.values.Has(name) {
return NewComputed(r, attr.Type, name), nil
return NewAttribute(r, attr.Type, name), nil
}
}
@ -468,7 +469,7 @@ func (r *Resource) dependsOn(_ *starlark.Thread, _ *starlark.Builtin, args starl
resources[i] = resource
}
r.dependenies = append(r.dependenies, resources...)
r.dependencies = append(r.dependencies, resources...)
return starlark.None, nil
}
@ -488,30 +489,30 @@ func (r *Resource) addProvisioner(_ *starlark.Thread, _ *starlark.Builtin, args
}
// CompareSameType honors starlark.Comparable interface.
func (x *Resource) CompareSameType(op syntax.Token, y_ starlark.Value, depth int) (bool, error) {
y := y_.(*Resource)
func (r *Resource) CompareSameType(op syntax.Token, yv starlark.Value, depth int) (bool, error) {
y := yv.(*Resource)
switch op {
case syntax.EQL:
ok, err := x.doCompareSameType(y, depth)
ok, err := r.doCompareSameType(y, depth)
return ok, err
case syntax.NEQ:
ok, err := x.doCompareSameType(y, depth)
ok, err := r.doCompareSameType(y, depth)
return !ok, err
default:
return false, fmt.Errorf("%s %s %s not implemented", x.Type(), op, y.Type())
return false, fmt.Errorf("%s %s %s not implemented", r.Type(), op, y.Type())
}
}
func (x *Resource) doCompareSameType(y *Resource, depth int) (bool, error) {
if x.typ != y.typ {
func (r *Resource) doCompareSameType(y *Resource, depth int) (bool, error) {
if r.typ != y.typ {
return false, nil
}
if x.values.Len() != y.values.Len() {
if r.values.Len() != y.values.Len() {
return false, nil
}
for _, xval := range x.values.List() {
for _, xval := range r.values.List() {
yval := y.values.Get(xval.Name)
if yval == nil {
return false, nil

View File

@ -4,10 +4,10 @@ aws = tf.provider("aws", "2.13.0")
ami = aws.data.ami()
# compute of scalar
# attribute of scalar
web = aws.resource.instance()
web.ami = ami.id
assert.eq(type(web.ami), "Computed<string>")
assert.eq(type(web.ami), "Attribute<string>")
assert.eq(str(web.ami), '"${data.aws_ami.id_2.id}"')
assert.eq(web.ami.__resource__, ami)
assert.eq(web.ami.__type__, "string")
@ -16,33 +16,37 @@ assert.eq(web.ami.__type__, "string")
assert.eq("__resource__" in dir(web.ami), True)
assert.eq("__type__" in dir(web.ami), True)
# compute of set
# attribute of set
table = aws.data.dynamodb_table()
assert.eq(str(table.ttl), '"${data.aws_dynamodb_table.id_4.ttl}"')
assert.eq(str(table.ttl[0]), '"${data.aws_dynamodb_table.id_4.ttl.0}"')
assert.eq(str(table.ttl[0].attribute_name), '"${data.aws_dynamodb_table.id_4.ttl.0.attribute_name}"')
# compute of list
# attribute of list
instance = aws.data.instance()
assert.eq(str(instance.credit_specification), '"${data.aws_instance.id_5.credit_specification}"')
assert.eq(str(instance.credit_specification[0]), '"${data.aws_instance.id_5.credit_specification.0}"')
assert.eq(str(instance.credit_specification[0].cpu_credits), '"${data.aws_instance.id_5.credit_specification.0.cpu_credits}"')
# compute of map
computed = str(aws.resource.instance().root_block_device.volume_size)
assert.eq(computed, '"${aws_instance.id_6.root_block_device.0.volume_size}"')
# attribute of block
attribute = str(aws.resource.instance().root_block_device.volume_size)
assert.eq(attribute, '"${aws_instance.id_6.root_block_device.0.volume_size}"')
# compute on data source
# attribute on data source
assert.eq(str(aws.resource.instance().id), '"${aws_instance.id_7.id}"')
# compute on resource
# attribute on resource
assert.eq(str(aws.data.ami().id), '"${data.aws_ami.id_8.id}"')
gcp = tf.provider("google", "3.13.0")
# computed on list with MaxItem:1
# attribute on list with MaxItem:1
cluster = gcp.resource.container_cluster("foo")
assert.eq(str(cluster.master_auth.client_certificate), '"${google_container_cluster.foo.master_auth.0.client_certificate}"')
# attr non-object
assert.fails(lambda: web.ami.foo, "Attribute<string> it's not a object")
# fn wrapping
assert.eq(str(fn("base64encode", web.ami)), '"${base64encode(data.aws_ami.id_2.id)}"')

View File

@ -0,0 +1,14 @@
# When a Resource has an Attribute means that the value it's only available
# during the `apply` phase of Terraform. So in, AsCode an attribute behaves
# like a poor-man pointer.
aws = tf.provider("aws")
ami = aws.resource.ami("ubuntu")
instance = aws.resource.instance("foo")
instance.ami = ami.id
print(instance.ami)
# Output:
# "${aws_ami.ubuntu.id}"

View File

@ -31,7 +31,7 @@ assert.eq(qux.name, None)
assert.fails(lambda: qux.foo, "Resource<data> has no .foo field or method")
# attr id
assert.eq(type(qux.id), "Computed<string>")
assert.eq(type(qux.id), "Attribute<string>")
assert.eq(str(qux.id), '"${data.ignition_user.id_2.id}"')
aws = tf.provider("aws", "2.13.0")

View File

@ -46,7 +46,7 @@ func NewTypeFromStarlark(typ string) (*Type, error) {
t.cty = cty.List(cty.NilType)
case "dict", "Resource":
t.cty = cty.Map(cty.NilType)
case "Computed":
case "Attribute":
t.cty = cty.String
default:
return nil, fmt.Errorf("unexpected %q type", typ)
@ -108,7 +108,7 @@ func (t *Type) Cty() cty.Type {
return t.cty
}
// Validate validates a value againts the type.
// Validate validates a value against the type.
func (t *Type) Validate(v starlark.Value) error {
switch v.(type) {
case starlark.String:
@ -123,12 +123,12 @@ func (t *Type) Validate(v starlark.Value) error {
if t.cty == cty.Bool {
return nil
}
case *Computed:
if t.cty == v.(*Computed).t {
case *Attribute:
if t.cty == v.(*Attribute).t {
return nil
}
vt := v.(*Computed).InnerType().Starlark()
vt := v.(*Attribute).InnerType().Starlark()
return fmt.Errorf("expected %s, got %s", t.typ, vt)
case *starlark.List:
if t.cty.IsListType() || t.cty.IsSetType() {

View File

@ -78,8 +78,8 @@ func (v *Value) Cty() cty.Value {
}
return cty.MapVal(values)
case "Computed":
return cty.StringVal(v.v.(*Computed).GoString())
case "Attribute":
return cty.StringVal(v.v.(*Attribute).GoString())
default:
return cty.StringVal(fmt.Sprintf("unhandled: %s", v.t.typ))
}
@ -264,10 +264,12 @@ func (a Values) Cty(schema *configschema.Block) cty.Value {
return cty.ObjectVal(values)
}
// Dict is a starlark.Dict HCLCompatible.
type Dict struct {
*starlark.Dict
}
// NewDict returns a new empty Dict.
func NewDict() *Dict {
return &Dict{starlark.NewDict(0)}
}

View File

@ -14,7 +14,7 @@ import (
"github.com/mitchellh/cli"
)
// PluginManager is a wrapper arround the terraform tools to download and execute
// PluginManager is a wrapper around the terraform tools to download and execute
// terraform plugins, like providers and provisioners.
type PluginManager struct {
Path string
@ -28,7 +28,7 @@ func (m *PluginManager) Provider(provider, version string, forceLocal bool) (*pl
meta, ok := m.getLocal("provider", provider, version)
if !ok && !forceLocal {
var err error
meta, ok, err = m.getProviderRemote(provider, version)
meta, _, err = m.getProviderRemote(provider, version)
if err != nil {
return nil, discovery.PluginMeta{}, err
}