Skip to content

Commit

Permalink
Merge pull request #97 from muvaf/emptystate
Browse files Browse the repository at this point in the history
terraform: make sure that the tfstate is re-produced in case we end up with empty tfstate
  • Loading branch information
muvaf authored Sep 26, 2022
2 parents ae77d3d + 1819387 commit 32f7e34
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 68 deletions.
142 changes: 98 additions & 44 deletions pkg/terraform/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package terraform
import (
"context"
"fmt"
iofs "io/fs"
"path/filepath"
"strings"

Expand All @@ -19,6 +20,20 @@ import (
"github.com/upbound/upjet/pkg/resource/json"
)

const (
errWriteTFStateFile = "cannot write terraform.tfstate file"
errWriteMainTFFile = "cannot write main.tf.json file"
errCheckIfStateEmpty = "cannot check whether the state is empty"
errGetID = "cannot get id"
errMarshalAttributes = "cannot marshal produced state attributes"
errInsertTimeouts = "cannot insert timeouts metadata to private raw"
errReadTFState = "cannot read terraform.tfstate file"
errMarshalState = "cannot marshal state object"
errUnmarshalAttr = "cannot unmarshal state attributes"
errUnmarshalTFState = "cannot unmarshal tfstate file"
errFmtNonString = "cannot work with a non-string id: %s"
)

// FileProducerOption allows you to configure FileProducer
type FileProducerOption func(*FileProducer)

Expand Down Expand Up @@ -76,9 +91,58 @@ type FileProducer struct {
fs afero.Afero
}

// WriteTFState writes the Terraform state that should exist in the filesystem to
// start any Terraform operation.
func (fp *FileProducer) WriteTFState(ctx context.Context) error {
// WriteMainTF writes the content main configuration file that has the desired
// state configuration for Terraform.
func (fp *FileProducer) WriteMainTF() error {
// If the resource is in a deletion process, we need to remove the deletion
// protection.
fp.parameters["lifecycle"] = map[string]bool{
"prevent_destroy": !meta.WasDeleted(fp.Resource),
}

// Add operation timeouts if any timeout configured for the resource
if tp := timeouts(fp.Config.OperationTimeouts).asParameter(); len(tp) != 0 {
fp.parameters["timeouts"] = tp
}

// Note(turkenh): To use third party providers, we need to configure
// provider name in required_providers.
providerSource := strings.Split(fp.Setup.Requirement.Source, "/")
m := map[string]any{
"terraform": map[string]any{
"required_providers": map[string]any{
providerSource[len(providerSource)-1]: map[string]string{
"source": fp.Setup.Requirement.Source,
"version": fp.Setup.Requirement.Version,
},
},
},
"provider": map[string]any{
providerSource[len(providerSource)-1]: fp.Setup.Configuration,
},
"resource": map[string]any{
fp.Resource.GetTerraformResourceType(): map[string]any{
fp.Resource.GetName(): fp.parameters,
},
},
}
rawMainTF, err := json.JSParser.Marshal(m)
if err != nil {
return errors.Wrap(err, "cannot marshal main hcl object")
}
return errors.Wrap(fp.fs.WriteFile(filepath.Join(fp.Dir, "main.tf.json"), rawMainTF, 0600), errWriteMainTFFile)
}

// EnsureTFState writes the Terraform state that should exist in the filesystem
// to start any Terraform operation.
func (fp *FileProducer) EnsureTFState(ctx context.Context) error {
empty, err := fp.isStateEmpty()
if err != nil {
return errors.Wrap(err, errCheckIfStateEmpty)
}
if !empty {
return nil
}
base := make(map[string]any)
// NOTE(muvaf): Since we try to produce the current state, observation
// takes precedence over parameters.
Expand All @@ -90,19 +154,19 @@ func (fp *FileProducer) WriteTFState(ctx context.Context) error {
}
id, err := fp.Config.ExternalName.GetIDFn(ctx, meta.GetExternalName(fp.Resource), fp.parameters, fp.Setup.Map())
if err != nil {
return errors.Wrap(err, "cannot get id")
return errors.Wrap(err, errGetID)
}
base["id"] = id
attr, err := json.JSParser.Marshal(base)
if err != nil {
return errors.Wrap(err, "cannot marshal produced state attributes")
return errors.Wrap(err, errMarshalAttributes)
}
var privateRaw []byte
if pr, ok := fp.Resource.GetAnnotations()[resource.AnnotationKeyPrivateRawAttribute]; ok {
privateRaw = []byte(pr)
}
if privateRaw, err = insertTimeoutsMeta(privateRaw, timeouts(fp.Config.OperationTimeouts)); err != nil {
return errors.Wrap(err, "cannot insert timeouts metadata to private raw")
return errors.Wrap(err, errInsertTimeouts)
}
s := json.NewStateV4()
s.TerraformVersion = fp.Setup.Version
Expand All @@ -127,49 +191,39 @@ func (fp *FileProducer) WriteTFState(ctx context.Context) error {

rawState, err := json.JSParser.Marshal(s)
if err != nil {
return errors.Wrap(err, "cannot marshal state object")
return errors.Wrap(err, errMarshalState)
}
return errors.Wrap(fp.fs.WriteFile(filepath.Join(fp.Dir, "terraform.tfstate"), rawState, 0600), "cannot write tfstate file")
return errors.Wrap(fp.fs.WriteFile(filepath.Join(fp.Dir, "terraform.tfstate"), rawState, 0600), errWriteTFStateFile)
}

// WriteMainTF writes the content main configuration file that has the desired
// state configuration for Terraform.
func (fp *FileProducer) WriteMainTF() error {
// If the resource is in a deletion process, we need to remove the deletion
// protection.
fp.parameters["lifecycle"] = map[string]bool{
"prevent_destroy": !meta.WasDeleted(fp.Resource),
// isStateEmpty returns whether the Terraform state includes a resource or not.
func (fp *FileProducer) isStateEmpty() (bool, error) {
data, err := fp.fs.ReadFile(filepath.Join(fp.Dir, "terraform.tfstate"))
if errors.Is(err, iofs.ErrNotExist) {
return true, nil
}

// Add operation timeouts if any timeout configured for the resource
if tp := timeouts(fp.Config.OperationTimeouts).asParameter(); len(tp) != 0 {
fp.parameters["timeouts"] = tp
if err != nil {
return false, errors.Wrap(err, errReadTFState)
}

// Note(turkenh): To use third party providers, we need to configure
// provider name in required_providers.
providerSource := strings.Split(fp.Setup.Requirement.Source, "/")
m := map[string]any{
"terraform": map[string]any{
"required_providers": map[string]any{
providerSource[len(providerSource)-1]: map[string]string{
"source": fp.Setup.Requirement.Source,
"version": fp.Setup.Requirement.Version,
},
},
},
"provider": map[string]any{
providerSource[len(providerSource)-1]: fp.Setup.Configuration,
},
"resource": map[string]any{
fp.Resource.GetTerraformResourceType(): map[string]any{
fp.Resource.GetName(): fp.parameters,
},
},
s := &json.StateV4{}
if err := json.JSParser.Unmarshal(data, s); err != nil {
return false, errors.Wrap(err, errUnmarshalTFState)
}
rawMainTF, err := json.JSParser.Marshal(m)
if err != nil {
return errors.Wrap(err, "cannot marshal main hcl object")
attrData := s.GetAttributes()
if attrData == nil {
return true, nil
}
attr := map[string]any{}
if err := json.JSParser.Unmarshal(attrData, &attr); err != nil {
return false, errors.Wrap(err, errUnmarshalAttr)
}
id, ok := attr["id"]
if !ok {
return true, nil
}
sid, ok := id.(string)
if !ok {
return false, errors.Errorf(errFmtNonString, fmt.Sprint(id))
}
return errors.Wrap(fp.fs.WriteFile(filepath.Join(fp.Dir, "main.tf.json"), rawMainTF, 0600), "cannot write tfstate file")
return sid == "", nil
}
Loading

0 comments on commit 32f7e34

Please sign in to comment.