Implement partial Read

Some time ago, we introduced the concept of the initialization error to
Pulumi (i.e., an error where the resource was successfully created but
failed to fully initialize). This was originally implemented in `Create`
and `Update`  methods of the resource provider interface; when we
detected an initialization failure, we'd pack the live version of the
object into the error, and return that to the engine.

Omitted from this initial implementation was a similar semantics for
`Read`. There are many implications of this, but one of them is that a
`pulumi refresh` will erase any initialization errors that had
previously been observed, even if the initialization errors still exist
in the resource.

This commit will introduce the initialization error semantics to `Read`,
fixing this issue.
This commit is contained in:
Alex Clemmer 2018-08-07 00:40:43 -07:00
parent a09d9ba035
commit a172f1a048
13 changed files with 184 additions and 48 deletions

View file

@ -1029,3 +1029,96 @@ func TestExternalRefresh(t *testing.T) {
assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA") assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
assert.True(t, snap.Resources[1].External) assert.True(t, snap.Resources[1].External)
} }
func TestRefreshInitFailure(t *testing.T) {
//
// Refresh will persist any initialization errors that are returned by `Read`. This provider
// will error out or not based on the value of `refreshShouldFail`.
//
refreshShouldFail := false
//
// Set up test environment to use `readFailProvider` as the underlying resource provider.
//
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
ReadF: func(
urn resource.URN, id resource.ID, props resource.PropertyMap,
) (resource.PropertyMap, resource.Status, error) {
if refreshShouldFail {
err := &plugin.InitError{
Reasons: []string{"Refresh reports continued to fail to initialize"},
}
return resource.PropertyMap{}, resource.StatusPartialFailure, err
}
return resource.PropertyMap{}, resource.StatusOK, nil
},
}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, "", false, nil, "",
resource.PropertyMap{})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{host: host},
}
provURN := p.NewProviderURN("pkgA", "default", "")
resURN := p.NewURN("pkgA:m:typA", "resA", "")
//
// Create an old snapshot with a single initialization failure.
//
old := &deploy.Snapshot{
Resources: []*resource.State{{
Type: resURN.Type(),
URN: resURN,
Custom: true,
ID: "0",
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
InitErrors: []string{"Resource failed to initialize"},
}},
}
//
// Refresh DOES NOT fail, causing the initialization error to disappear.
//
p.Steps = []TestStep{{Op: Refresh}}
snap := p.Run(t, old)
for _, resource := range snap.Resources {
switch urn := resource.URN; urn {
case provURN:
// break
case resURN:
assert.Equal(t, []string{}, resource.InitErrors)
default:
t.Fatalf("unexpected resource %v", urn)
}
}
//
// Refresh DOES fail, causing the new initialization error to appear.
//
refreshShouldFail = true
p.Steps = []TestStep{{Op: Refresh}}
snap = p.Run(t, old)
for _, resource := range snap.Resources {
switch urn := resource.URN; urn {
case provURN:
// break
case resURN:
assert.Equal(t, []string{"Refresh reports continued to fail to initialize"}, resource.InitErrors)
default:
t.Fatalf("unexpected resource %v", urn)
}
}
}

View file

@ -45,7 +45,8 @@ type Provider struct {
olds, news resource.PropertyMap) (resource.PropertyMap, resource.Status, error) olds, news resource.PropertyMap) (resource.PropertyMap, resource.Status, error)
DeleteF func(urn resource.URN, id resource.ID, olds resource.PropertyMap) (resource.Status, error) DeleteF func(urn resource.URN, id resource.ID, olds resource.PropertyMap) (resource.Status, error)
ReadF func(urn resource.URN, id resource.ID, props resource.PropertyMap) (resource.PropertyMap, error) ReadF func(urn resource.URN, id resource.ID,
props resource.PropertyMap) (resource.PropertyMap, resource.Status, error)
InvokeF func(tok tokens.ModuleMember, InvokeF func(tok tokens.ModuleMember,
inputs resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) inputs resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error)
} }
@ -129,9 +130,9 @@ func (prov *Provider) Delete(urn resource.URN,
} }
func (prov *Provider) Read(urn resource.URN, id resource.ID, func (prov *Provider) Read(urn resource.URN, id resource.ID,
props resource.PropertyMap) (resource.PropertyMap, error) { props resource.PropertyMap) (resource.PropertyMap, resource.Status, error) {
if prov.ReadF == nil { if prov.ReadF == nil {
return resource.PropertyMap{}, nil return resource.PropertyMap{}, resource.StatusUnknown, nil
} }
return prov.ReadF(urn, id, props) return prov.ReadF(urn, id, props)
} }

View file

@ -339,8 +339,8 @@ func (r *Registry) Delete(urn resource.URN, id resource.ID, props resource.Prope
} }
func (r *Registry) Read(urn resource.URN, id resource.ID, func (r *Registry) Read(urn resource.URN, id resource.ID,
props resource.PropertyMap) (resource.PropertyMap, error) { props resource.PropertyMap) (resource.PropertyMap, resource.Status, error) {
return nil, errors.New("provider resources may not be read") return nil, resource.StatusUnknown, errors.New("provider resources may not be read")
} }
func (r *Registry) Invoke(tok tokens.ModuleMember, func (r *Registry) Invoke(tok tokens.ModuleMember,

View file

@ -111,8 +111,8 @@ func (prov *testProvider) Create(urn resource.URN, props resource.PropertyMap) (
return "", nil, resource.StatusOK, errors.New("unsupported") return "", nil, resource.StatusOK, errors.New("unsupported")
} }
func (prov *testProvider) Read(urn resource.URN, id resource.ID, func (prov *testProvider) Read(urn resource.URN, id resource.ID,
props resource.PropertyMap) (resource.PropertyMap, error) { props resource.PropertyMap) (resource.PropertyMap, resource.Status, error) {
return nil, errors.New("unsupported") return nil, resource.StatusUnknown, errors.New("unsupported")
} }
func (prov *testProvider) Diff(urn resource.URN, id resource.ID, func (prov *testProvider) Diff(urn resource.URN, id resource.ID,
olds resource.PropertyMap, news resource.PropertyMap, _ bool) (plugin.DiffResult, error) { olds resource.PropertyMap, news resource.PropertyMap, _ bool) (plugin.DiffResult, error) {

View file

@ -257,7 +257,7 @@ func (d *defaultProviders) newRegisterDefaultProviderEvent(
// Create the result channel and the event. // Create the result channel and the event.
done := make(chan *RegisterResult) done := make(chan *RegisterResult)
event := &registerResourceEvent{ event := &registerResourceEvent{
goal: resource.NewGoal(providers.MakeProviderType(pkg), "default", true, inputs, "", false, nil, ""), goal: resource.NewGoal(providers.MakeProviderType(pkg), "default", true, inputs, "", false, nil, "", nil),
done: done, done: done,
} }
return event, done, nil return event, done, nil
@ -601,7 +601,7 @@ func (rm *resmon) RegisterResource(ctx context.Context,
// Send the goal state to the engine. // Send the goal state to the engine.
step := &registerResourceEvent{ step := &registerResourceEvent{
goal: resource.NewGoal(t, name, custom, props, parent, protect, dependencies, provider), goal: resource.NewGoal(t, name, custom, props, parent, protect, dependencies, provider, nil),
done: make(chan *RegisterResult), done: make(chan *RegisterResult),
} }

View file

@ -142,16 +142,16 @@ func TestRegisterNoDefaultProviders(t *testing.T) {
// Register a component resource. // Register a component resource.
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal(componentURN.Type(), componentURN.Name(), false, resource.PropertyMap{}, "", false, goal: resource.NewGoal(componentURN.Type(), componentURN.Name(), false, resource.PropertyMap{}, "", false,
nil, ""), nil, "", []string{}),
}, },
// Register a couple resources using provider A. // Register a couple resources using provider A.
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal("pkgA:index:typA", "res1", true, resource.PropertyMap{}, componentURN, false, nil, goal: resource.NewGoal("pkgA:index:typA", "res1", true, resource.PropertyMap{}, componentURN, false, nil,
providerARef.String()), providerARef.String(), []string{}),
}, },
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal("pkgA:index:typA", "res2", true, resource.PropertyMap{}, componentURN, false, nil, goal: resource.NewGoal("pkgA:index:typA", "res2", true, resource.PropertyMap{}, componentURN, false, nil,
providerARef.String()), providerARef.String(), []string{}),
}, },
// Register two more providers. // Register two more providers.
newProviderEvent("pkgA", "providerB", nil, ""), newProviderEvent("pkgA", "providerB", nil, ""),
@ -159,11 +159,11 @@ func TestRegisterNoDefaultProviders(t *testing.T) {
// Register a few resources that use the new providers. // Register a few resources that use the new providers.
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal("pkgB:index:typB", "res3", true, resource.PropertyMap{}, "", false, nil, goal: resource.NewGoal("pkgB:index:typB", "res3", true, resource.PropertyMap{}, "", false, nil,
providerBRef.String()), providerBRef.String(), []string{}),
}, },
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal("pkgB:index:typC", "res4", true, resource.PropertyMap{}, "", false, nil, goal: resource.NewGoal("pkgB:index:typC", "res4", true, resource.PropertyMap{}, "", false, nil,
providerCRef.String()), providerCRef.String(), []string{}),
}, },
} }
@ -225,21 +225,25 @@ func TestRegisterDefaultProviders(t *testing.T) {
// Register a component resource. // Register a component resource.
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal(componentURN.Type(), componentURN.Name(), false, resource.PropertyMap{}, "", false, goal: resource.NewGoal(componentURN.Type(), componentURN.Name(), false, resource.PropertyMap{}, "", false,
nil, ""), nil, "", []string{}),
}, },
// Register a couple resources from package A. // Register a couple resources from package A.
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal("pkgA:m:typA", "res1", true, resource.PropertyMap{}, componentURN, false, nil, ""), goal: resource.NewGoal("pkgA:m:typA", "res1", true, resource.PropertyMap{},
componentURN, false, nil, "", []string{}),
}, },
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal("pkgA:m:typA", "res2", true, resource.PropertyMap{}, componentURN, false, nil, ""), goal: resource.NewGoal("pkgA:m:typA", "res2", true, resource.PropertyMap{},
componentURN, false, nil, "", []string{}),
}, },
// Register a few resources from other packages. // Register a few resources from other packages.
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal("pkgB:m:typB", "res3", true, resource.PropertyMap{}, "", false, nil, ""), goal: resource.NewGoal("pkgB:m:typB", "res3", true, resource.PropertyMap{}, "", false,
nil, "", []string{}),
}, },
&testRegEvent{ &testRegEvent{
goal: resource.NewGoal("pkgB:m:typC", "res4", true, resource.PropertyMap{}, "", false, nil, ""), goal: resource.NewGoal("pkgB:m:typC", "res4", true, resource.PropertyMap{}, "", false,
nil, "", []string{}),
}, },
} }

View file

@ -18,7 +18,6 @@ import (
"context" "context"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/resource" "github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy/providers" "github.com/pulumi/pulumi/pkg/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/resource/plugin" "github.com/pulumi/pulumi/pkg/resource/plugin"
@ -152,20 +151,29 @@ func (iter *refreshSourceIterator) newRefreshGoal(s *resource.State) (*resource.
if !ok { if !ok {
return nil, errors.Errorf("unknown provider '%v' for resource '%v'", s.Provider, s.URN) return nil, errors.Errorf("unknown provider '%v' for resource '%v'", s.Provider, s.URN)
} }
refreshed, err := provider.Read(s.URN, s.ID, s.Outputs)
initErrorReasons := []string{}
refreshed, resourceStatus, err := provider.Read(s.URN, s.ID, s.Outputs)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "refreshing %s's state", s.URN) if resourceStatus != resource.StatusPartialFailure {
return nil, errors.Wrapf(err, "refreshing %s's state", s.URN)
}
// Else it's a `StatusPartialError`.
if initErr, isInitErr := err.(*plugin.InitError); isInitErr {
initErrorReasons = initErr.Reasons
}
} else if refreshed == nil { } else if refreshed == nil {
return nil, nil // the resource was deleted. return nil, nil // the resource was deleted.
} }
s = resource.NewState( s = resource.NewState(
s.Type, s.URN, s.Custom, s.Delete, s.ID, s.Inputs, refreshed, s.Type, s.URN, s.Custom, s.Delete, s.ID, s.Inputs, refreshed,
s.Parent, s.Protect, s.External, s.Dependencies, s.InitErrors, s.Provider) s.Parent, s.Protect, s.External, s.Dependencies, initErrorReasons, s.Provider)
} }
// Now just return the actual state as the goal state. // Now just return the actual state as the goal state.
return resource.NewGoal(s.Type, s.URN.Name(), s.Custom, s.Outputs, s.Parent, s.Protect, s.Dependencies, return resource.NewGoal(s.Type, s.URN.Name(), s.Custom, s.Outputs, s.Parent, s.Protect,
s.Provider), nil s.Dependencies, s.Provider, s.InitErrors), nil
} }
type refreshSourceEvent struct { type refreshSourceEvent struct {

View file

@ -81,9 +81,9 @@ func TestRefresh(t *testing.T) {
reads := int32(0) reads := int32(0)
noopProvider := &deploytest.Provider{ noopProvider := &deploytest.Provider{
ReadF: func(resource.URN, resource.ID, resource.PropertyMap) (resource.PropertyMap, error) { ReadF: func(resource.URN, resource.ID, resource.PropertyMap) (resource.PropertyMap, resource.Status, error) {
atomic.AddInt32(&reads, 1) atomic.AddInt32(&reads, 1)
return resource.PropertyMap{}, nil return resource.PropertyMap{}, resource.StatusUnknown, nil
}, },
} }

View file

@ -496,6 +496,8 @@ func (s *ReadStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error
urn := s.new.URN urn := s.new.URN
id := s.new.ID id := s.new.ID
var resourceError error
resourceStatus := resource.StatusOK
// Unlike most steps, Read steps run during previews. The only time // Unlike most steps, Read steps run during previews. The only time
// we can't run is if the ID we are given is unknown. // we can't run is if the ID we are given is unknown.
if id == "" || id == plugin.UnknownStringValue { if id == "" || id == plugin.UnknownStringValue {
@ -506,9 +508,18 @@ func (s *ReadStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error
return resource.StatusOK, nil, err return resource.StatusOK, nil, err
} }
result, err := prov.Read(urn, id, s.new.Inputs) result, rst, err := prov.Read(urn, id, s.new.Inputs)
if err != nil { if err != nil {
return resource.StatusUnknown, nil, err if rst != resource.StatusPartialFailure {
return rst, nil, err
}
resourceError = err
resourceStatus = rst
if initErr, isInitErr := err.(*plugin.InitError); isInitErr {
s.new.InitErrors = initErr.Reasons
}
} }
s.new.Outputs = result s.new.Outputs = result
@ -521,7 +532,10 @@ func (s *ReadStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error
} }
complete := func() { s.event.Done(&ReadResult{State: s.new}) } complete := func() { s.event.Done(&ReadResult{State: s.new}) }
return resource.StatusOK, complete, nil if resourceError == nil {
return resourceStatus, complete, nil
}
return resourceStatus, complete, resourceError
} }
// StepOp represents the kind of operation performed by a step. It evaluates to its string label. // StepOp represents the kind of operation performed by a step. It evaluates to its string label.

View file

@ -502,7 +502,7 @@ func (sg *stepGenerator) getResourcePropertyStates(urn resource.URN, goal *resou
} }
return props, inputs, outputs, return props, inputs, outputs,
resource.NewState(goal.Type, urn, goal.Custom, false, "", resource.NewState(goal.Type, urn, goal.Custom, false, "",
inputs, outputs, goal.Parent, goal.Protect, false, goal.Dependencies, nil, goal.Provider) inputs, outputs, goal.Parent, goal.Protect, false, goal.Dependencies, goal.InitErrors, goal.Provider)
} }
// issueCheckErrors prints any check errors to the diagnostics sink. // issueCheckErrors prints any check errors to the diagnostics sink.

View file

@ -57,7 +57,8 @@ type Provider interface {
// Read the current live state associated with a resource. Enough state must be include in the inputs to uniquely // Read the current live state associated with a resource. Enough state must be include in the inputs to uniquely
// identify the resource; this is typically just the resource ID, but may also include some properties. If the // identify the resource; this is typically just the resource ID, but may also include some properties. If the
// resource is missing (for instance, because it has been deleted), the resulting property map will be nil. // resource is missing (for instance, because it has been deleted), the resulting property map will be nil.
Read(urn resource.URN, id resource.ID, props resource.PropertyMap) (resource.PropertyMap, error) Read(urn resource.URN, id resource.ID,
props resource.PropertyMap) (resource.PropertyMap, resource.Status, error)
// Update updates an existing resource with new values. // Update updates an existing resource with new values.
Update(urn resource.URN, id resource.ID, Update(urn resource.URN, id resource.ID,
olds resource.PropertyMap, news resource.PropertyMap) (resource.PropertyMap, resource.Status, error) olds resource.PropertyMap, news resource.PropertyMap) (resource.PropertyMap, resource.Status, error)

View file

@ -365,7 +365,7 @@ func (p *provider) Create(urn resource.URN, props resource.PropertyMap) (resourc
resourceStatus, id, liveObject, resourceError = parseError(err) resourceStatus, id, liveObject, resourceError = parseError(err)
logging.V(7).Infof("%s failed: %v", label, resourceError) logging.V(7).Infof("%s failed: %v", label, resourceError)
if resourceStatus == resource.StatusUnknown { if resourceStatus != resource.StatusPartialFailure {
return "", nil, resourceStatus, resourceError return "", nil, resourceStatus, resourceError
} }
// Else it's a `StatusPartialFailure`. // Else it's a `StatusPartialFailure`.
@ -394,7 +394,9 @@ func (p *provider) Create(urn resource.URN, props resource.PropertyMap) (resourc
// read the current live state associated with a resource. enough state must be include in the inputs to uniquely // read the current live state associated with a resource. enough state must be include in the inputs to uniquely
// identify the resource; this is typically just the resource id, but may also include some properties. // identify the resource; this is typically just the resource id, but may also include some properties.
func (p *provider) Read(urn resource.URN, id resource.ID, props resource.PropertyMap) (resource.PropertyMap, error) { func (p *provider) Read(
urn resource.URN, id resource.ID, props resource.PropertyMap,
) (resource.PropertyMap, resource.Status, error) {
contract.Assert(urn != "") contract.Assert(urn != "")
contract.Assert(id != "") contract.Assert(id != "")
@ -404,49 +406,60 @@ func (p *provider) Read(urn resource.URN, id resource.ID, props resource.Propert
// Get the RPC client and ensure it's configured. // Get the RPC client and ensure it's configured.
client, err := p.getClient() client, err := p.getClient()
if err != nil { if err != nil {
return nil, err return nil, resource.StatusUnknown, err
} }
// If the provider is not fully configured, return an empty bag. // If the provider is not fully configured, return an empty bag.
if !p.cfgknown { if !p.cfgknown {
return resource.PropertyMap{}, nil return resource.PropertyMap{}, resource.StatusUnknown, nil
} }
// Marshal the input state so we can perform the RPC. // Marshal the input state so we can perform the RPC.
marshaled, err := MarshalProperties(props, MarshalOptions{Label: label, ElideAssetContents: true}) marshaled, err := MarshalProperties(props, MarshalOptions{Label: label, ElideAssetContents: true})
if err != nil { if err != nil {
return nil, err return nil, resource.StatusUnknown, err
} }
// Now issue the read request over RPC, blocking until it finished. // Now issue the read request over RPC, blocking until it finished.
var readID resource.ID
var liveObject *_struct.Struct
var resourceError error
var resourceStatus = resource.StatusOK
resp, err := client.Read(p.ctx.Request(), &pulumirpc.ReadRequest{ resp, err := client.Read(p.ctx.Request(), &pulumirpc.ReadRequest{
Id: string(id), Id: string(id),
Urn: string(urn), Urn: string(urn),
Properties: marshaled, Properties: marshaled,
}) })
if err != nil { if err != nil {
resourceStatus, readID, liveObject, resourceError = parseError(err)
logging.V(7).Infof("%s failed: %v", label, err) logging.V(7).Infof("%s failed: %v", label, err)
return nil, err
if resourceStatus != resource.StatusPartialFailure {
return nil, resourceStatus, resourceError
}
// Else it's a `StatusPartialFailure`.
} else {
id = resource.ID(resp.GetId())
liveObject = resp.GetProperties()
} }
// If the resource was missing, simply return a nil property map. // If the resource was missing, simply return a nil property map.
readID := resp.GetId() if string(readID) == "" {
if readID == "" { return nil, resourceStatus, nil
return nil, nil } else if readID != id {
} else if readID != string(id) { return nil, resourceStatus, errors.Errorf(
return nil, errors.Errorf(
"reading resource %s yielded an unexpected ID; expected %s, got %s", urn, id, readID) "reading resource %s yielded an unexpected ID; expected %s, got %s", urn, id, readID)
} }
// Finally, unmarshal the resulting state properties and return them. // Finally, unmarshal the resulting state properties and return them.
results, err := UnmarshalProperties(resp.GetProperties(), MarshalOptions{ results, err := UnmarshalProperties(liveObject, MarshalOptions{
Label: fmt.Sprintf("%s.outputs", label), RejectUnknowns: true}) Label: fmt.Sprintf("%s.outputs", label), RejectUnknowns: true})
if err != nil { if err != nil {
return nil, err return nil, resourceStatus, err
} }
logging.V(7).Infof("%s success; #outs=%d", label, len(results)) logging.V(7).Infof("%s success; #outs=%d", label, len(results))
return results, nil return results, resourceStatus, resourceError
} }
// Update updates an existing resource with new values. // Update updates an existing resource with new values.
@ -492,7 +505,7 @@ func (p *provider) Update(urn resource.URN, id resource.ID,
resourceStatus, _, liveObject, resourceError = parseError(err) resourceStatus, _, liveObject, resourceError = parseError(err)
logging.V(7).Infof("%s failed: %v", label, resourceError) logging.V(7).Infof("%s failed: %v", label, resourceError)
if resourceStatus == resource.StatusUnknown { if resourceStatus != resource.StatusPartialFailure {
return nil, resourceStatus, resourceError return nil, resourceStatus, resourceError
} }
// Else it's a `StatusPartialFailure`. // Else it's a `StatusPartialFailure`.

View file

@ -29,11 +29,12 @@ type Goal struct {
Protect bool // true to protect this resource from deletion. Protect bool // true to protect this resource from deletion.
Dependencies []URN // dependencies of this resource object. Dependencies []URN // dependencies of this resource object.
Provider string // the provider to use for this resource. Provider string // the provider to use for this resource.
InitErrors []string // errors encountered as we attempted to initialize the resource.
} }
// NewGoal allocates a new resource goal state. // NewGoal allocates a new resource goal state.
func NewGoal(t tokens.Type, name tokens.QName, custom bool, props PropertyMap, func NewGoal(t tokens.Type, name tokens.QName, custom bool, props PropertyMap,
parent URN, protect bool, dependencies []URN, provider string) *Goal { parent URN, protect bool, dependencies []URN, provider string, initErrors []string) *Goal {
return &Goal{ return &Goal{
Type: t, Type: t,
Name: name, Name: name,
@ -43,5 +44,6 @@ func NewGoal(t tokens.Type, name tokens.QName, custom bool, props PropertyMap,
Protect: protect, Protect: protect,
Dependencies: dependencies, Dependencies: dependencies,
Provider: provider, Provider: provider,
InitErrors: initErrors,
} }
} }