Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype of Initial Resource State #3060

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions builtin/providers/aws/resource_aws_iam_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ func resourceAwsIamGroup() *schema.Resource {
//Update: resourceAwsIamGroupUpdate,
Delete: resourceAwsIamGroupDelete,

SetInitialState: func(d *schema.ResourceData, meta interface {}) error {
d.SetId(d.Get("name").(string))
return nil
},

Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
Type: schema.TypeString,
Expand Down
8 changes: 8 additions & 0 deletions builtin/providers/aws/resource_aws_iam_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ func resourceAwsIamRole() *schema.Resource {
//Update: resourceAwsIamRoleUpdate,
Delete: resourceAwsIamRoleDelete,

SetInitialState: func(d *schema.ResourceData, meta interface {}) error {
d.SetId(d.Get("name").(string))
return nil
},

Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
Type: schema.TypeString,
Expand Down Expand Up @@ -110,6 +115,9 @@ func resourceAwsIamRoleReadResult(d *schema.ResourceData, role *iam.Role) error
if err := d.Set("unique_id", role.RoleId); err != nil {
return err
}
if err := d.Set("assume_role_policy", role.AssumeRolePolicyDocument); err != nil {
return err
}
return nil
}

Expand Down
5 changes: 5 additions & 0 deletions builtin/providers/aws/resource_aws_iam_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ func resourceAwsIamUser() *schema.Resource {
//Update: resourceAwsIamUserUpdate,
Delete: resourceAwsIamUserDelete,

SetInitialState: func(d *schema.ResourceData, meta interface {}) error {
d.SetId(d.Get("name").(string))
return nil
},

Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
Type: schema.TypeString,
Expand Down
16 changes: 16 additions & 0 deletions builtin/providers/aws/resource_aws_key_pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ func resourceAwsKeyPair() *schema.Resource {
Update: nil,
Delete: resourceAwsKeyPairDelete,

SetInitialState: func(d *schema.ResourceData, meta interface {}) error {
// If the configuration is forcing a particular key name
// then we'll prime our initial state with it so that
// we can determine if we're going to collide with an
// existing key.
keyName := d.Get("key_name").(string)
if keyName != "" {
d.SetId(keyName)

// Don't set the public_key since we don't actually know
// what the remote state says, and Read can't fetch it.
d.Set("public_key", "")
}
return nil
},

Schema: map[string]*schema.Schema{
"key_name": &schema.Schema{
Type: schema.TypeString,
Expand Down
5 changes: 5 additions & 0 deletions builtin/providers/aws/resource_aws_s3_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ func resourceAwsS3Bucket() *schema.Resource {
Update: resourceAwsS3BucketUpdate,
Delete: resourceAwsS3BucketDelete,

SetInitialState: func(d *schema.ResourceData, meta interface {}) error {
d.SetId(d.Get("bucket").(string))
return nil
},

Schema: map[string]*schema.Schema{
"bucket": &schema.Schema{
Type: schema.TypeString,
Expand Down
14 changes: 9 additions & 5 deletions builtin/providers/terraform/resource_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ import (

func resourceRemoteState() *schema.Resource {
return &schema.Resource{
Create: resourceRemoteStateCreate,
Create: resourceRemoteStateRead,
Read: resourceRemoteStateRead,
Delete: resourceRemoteStateDelete,

SetInitialState: func(d *schema.ResourceData, meta interface {}) error {
// Just need to set the id to *something* non-empty, and then
// we'll get an opportunity to fill the initial state for real
// when the "Read" function is called.
d.SetId(time.Now().UTC().String())
return nil
},

Schema: map[string]*schema.Schema{
"backend": &schema.Schema{
Type: schema.TypeString,
Expand All @@ -35,10 +43,6 @@ func resourceRemoteState() *schema.Resource {
}
}

func resourceRemoteStateCreate(d *schema.ResourceData, meta interface{}) error {
return resourceRemoteStateRead(d, meta)
}

func resourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
backend := d.Get("backend").(string)
config := make(map[string]string)
Expand Down
28 changes: 10 additions & 18 deletions command/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,31 +48,23 @@ func (c *RefreshCommand) Run(args []string) int {
return 1
}

// Verify that the state path exists. The "ContextArg" function below
// Verify that the state path can be read. The "ContextArg" function below
// will actually do this, but we want to provide a richer error message
// if possible.
// It is okay to refresh without a state since the configuration may
// contain logical resources or resources that do not explicitly need to
// be created first.
if !state.State().IsRemote() {
if _, err := os.Stat(c.Meta.statePath); err != nil {
if os.IsNotExist(err) {
if !os.IsNotExist(err) {
c.Ui.Error(fmt.Sprintf(
"The Terraform state file for your infrastructure does not\n"+
"exist. The 'refresh' command only works and only makes sense\n"+
"when there is existing state that Terraform is managing. Please\n"+
"double-check the value given below and try again. If you\n"+
"haven't created infrastructure with Terraform yet, use the\n"+
"'terraform apply' command.\n\n"+
"Path: %s",
c.Meta.statePath))
return 1
}

c.Ui.Error(fmt.Sprintf(
"There was an error reading the Terraform state that is needed\n"+
"There was an error reading the Terraform state that is needed\n"+
"for refreshing. The path and error are shown below.\n\n"+
"Path: %s\n\nError: %s",
c.Meta.statePath,
err))
return 1
c.Meta.statePath,
err))
return 1
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions helper/schema/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,18 @@ func (p *Provider) Diff(
return r.Diff(s, c)
}

// InitialState implementation of terraform.ResourceProvider interface.
func (p *Provider) InitialState(
info *terraform.InstanceInfo,
c *terraform.ResourceConfig) (*terraform.InstanceState, error) {
r, ok := p.ResourcesMap[info.Type]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", info.Type)
}

return r.InitialState(c, p.meta)
}

// Refresh implementation of terraform.ResourceProvider interface.
func (p *Provider) Refresh(
info *terraform.InstanceInfo,
Expand Down
56 changes: 56 additions & 0 deletions helper/schema/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@ type Resource struct {
// needs to make any remote API calls.
MigrateState StateMigrateFunc

// SetInitialState is an optional function to pre-set the state for a
// new resource instance. It is provided a ResourceData for the
// configuration and it may mutate that ResourceData; if an Id is set
// when this function returns, the resource data will be written into
// the state so that it can be considered when refreshing and planning.
//
// The function must not make any network requests. It may only
// read the configuration and set any initial state that is needed
// to retrieve the *true* remote state once "Read" is called.
//
// Use this, for example, if the resource configuration includes a
// unique id that might already be in use in the remote system. By
// pre-setting the Id, Terraform can check if there is an existing
// resource to update, and in that case use the "Update" function
// rather than "Create" when "terraform apply" is run.
//
// You don't need to use this if the id for your resource is a computed
// value issued by the target system when the resource is first created.
//
// The "Read" function may subsequently set the Id back to "" to indicate
// that the resource doesn't exist after all, in which case
// "terraform apply" will try to create it as normal.
SetInitialState InitialStateFunc

// The functions below are the CRUD operations for this resource.
//
// The only optional operation is Update. If Update is not implemented,
Expand Down Expand Up @@ -95,6 +119,9 @@ type DeleteFunc func(*ResourceData, interface{}) error
// See Resource documentation.
type ExistsFunc func(*ResourceData, interface{}) (bool, error)

// See Resource documentation.
type InitialStateFunc func(*ResourceData, interface{}) error

// See Resource documentation.
type StateMigrateFunc func(
int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error)
Expand Down Expand Up @@ -167,6 +194,35 @@ func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return schemaMap(r.Schema).Validate(c)
}

// InitialState uses the configuration to potentially determine an initial
// state for a new instance.
func (r *Resource) InitialState(
c *terraform.ResourceConfig,
meta interface{}) (*terraform.InstanceState, error) {

if r.SetInitialState == nil {
return nil, nil
}

data, err := schemaMap(r.Schema).ConfigData(c)
if err != nil {
return nil, err
}

err = r.SetInitialState(data, meta)
if err != nil {
return nil, err
}

// Resource implementation will set the id in order to signal that it wants
// to prime the initial state with whatever's in the resource data.
if data.Id() != "" {
return data.State(), nil
} else {
return nil, nil
}
}

// Refresh refreshes the state of the resource.
func (r *Resource) Refresh(
s *terraform.InstanceState,
Expand Down
9 changes: 9 additions & 0 deletions helper/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ func (m schemaMap) Data(
}, nil
}

// ConfigData returns a ResourceData for the given config.
func (m schemaMap) ConfigData(
c *terraform.ResourceConfig) (*ResourceData, error) {
return &ResourceData{
schema: m,
config: c,
}, nil
}

// Diff returns the diff for a resource given the schema map,
// state, and configuration.
func (m schemaMap) Diff(
Expand Down
41 changes: 41 additions & 0 deletions rpc/resource_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,26 @@ func (p *ResourceProvider) Diff(
return resp.Diff, err
}

func (p *ResourceProvider) InitialState(
info *terraform.InstanceInfo,
c *terraform.ResourceConfig) (*terraform.InstanceState, error) {

var resp ResourceProviderInitialStateResponse
args := &ResourceProviderInitialStateArgs{
Info: info,
Config: c,
}
err := p.Client.Call(p.Name+".InitialState", args, &resp)
if err != nil {
return nil, err
}
if resp.Error != nil {
return nil, resp.Error
}

return resp.State, nil
}

func (p *ResourceProvider) Refresh(
info *terraform.InstanceInfo,
s *terraform.InstanceState) (*terraform.InstanceState, error) {
Expand Down Expand Up @@ -221,6 +241,16 @@ type ResourceProviderDiffResponse struct {
Error *BasicError
}

type ResourceProviderInitialStateArgs struct {
Info *terraform.InstanceInfo
Config *terraform.ResourceConfig
}

type ResourceProviderInitialStateResponse struct {
State *terraform.InstanceState
Error *BasicError
}

type ResourceProviderRefreshArgs struct {
Info *terraform.InstanceInfo
State *terraform.InstanceState
Expand Down Expand Up @@ -339,6 +369,17 @@ func (s *ResourceProviderServer) Diff(
return nil
}

func (s *ResourceProviderServer) InitialState(
args *ResourceProviderInitialStateArgs,
result *ResourceProviderInitialStateResponse) error {
state, err := s.Provider.InitialState(args.Info, args.Config)
*result = ResourceProviderInitialStateResponse{
State: state,
Error: NewBasicError(err),
}
return nil
}

func (s *ResourceProviderServer) Refresh(
args *ResourceProviderRefreshArgs,
result *ResourceProviderRefreshResponse) error {
Expand Down
57 changes: 57 additions & 0 deletions terraform/eval_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,60 @@ func (n *EvalUndeposeState) Eval(ctx EvalContext) (interface{}, error) {

return nil, nil
}

// EvalSetInitialId is an EvalNode implementation that will try to prime a
// resource's state with an id from the configuration if it doesn't already
// have an id set.
//
// If executed before EvalRefresh, this allows a resource to be considered as
// already-existing and skip the "create" step if a resource with the
// configured id already exists, or if the resource is a read-only
// data-collection resource.
type EvalSetInitialState struct {
Name string
ResourceType string
Info *InstanceInfo
Provider *ResourceProvider
ProviderName string
Config **ResourceConfig
State **InstanceState
Dependencies []string
Output **InstanceState
}

// TODO: test
func (n *EvalSetInitialState) Eval(ctx EvalContext) (interface{}, error) {
provider := *n.Provider
state := *n.State
config := *n.Config

// If we already have a state then we have nothing to do; this applies
// only to new resources that haven't yet been initialized.
if state != nil {
return nil, nil
}

// Ask the provider for a configured state.
newState, err := provider.InitialState(n.Info, config)
if err != nil {
return nil, fmt.Errorf("%s: %s", n.Info.Id, err.Error())
}

if newState != nil {
newState, err = writeInstanceToState(ctx, n.Name, n.ResourceType, n.ProviderName, n.Dependencies,
func(rs *ResourceState) error {
rs.Primary = newState
return nil
},
)
if err != nil {
return nil, fmt.Errorf("%s: %s", n.Info.Id, err.Error())
}
}

if n.Output != nil {
*n.Output = newState
}

return nil, nil
}
Loading