mirror of https://github.com/databricks/cli.git
interation on `.tfstate` sync stability
* read `serial` from `.tfstate`. * defer local `.tfstate` handle closing on method exits. * document private methods not to forget their intentions.
This commit is contained in:
parent
06db8376bc
commit
515a6930f8
1
go.mod
1
go.mod
|
@ -9,6 +9,7 @@ require (
|
||||||
github.com/hashicorp/go-version v1.5.0 // MPL 2.0
|
github.com/hashicorp/go-version v1.5.0 // MPL 2.0
|
||||||
github.com/hashicorp/hc-install v0.3.2 // MPL 2.0
|
github.com/hashicorp/hc-install v0.3.2 // MPL 2.0
|
||||||
github.com/hashicorp/terraform-exec v0.16.1 // MPL 2.0
|
github.com/hashicorp/terraform-exec v0.16.1 // MPL 2.0
|
||||||
|
github.com/hashicorp/terraform-json v0.13.0 // MPL 2.0
|
||||||
github.com/manifoldco/promptui v0.9.0 // BSD-3-Clause license
|
github.com/manifoldco/promptui v0.9.0 // BSD-3-Clause license
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // MIT
|
github.com/mitchellh/go-homedir v1.1.0 // MIT
|
||||||
github.com/spf13/cobra v1.4.0 // Apache 2.0
|
github.com/spf13/cobra v1.4.0 // Apache 2.0
|
||||||
|
|
|
@ -44,6 +44,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/databricks/bricks/project"
|
"github.com/databricks/bricks/project"
|
||||||
|
@ -78,56 +79,76 @@ func (d *TerraformDeployer) Init(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns location of terraform state on DBFS based on project's deployment isolation level.
|
||||||
func (d *TerraformDeployer) remoteTfstateLoc() string {
|
func (d *TerraformDeployer) remoteTfstateLoc() string {
|
||||||
prefix := project.Current.DeploymentIsolationPrefix()
|
prefix := project.Current.DeploymentIsolationPrefix()
|
||||||
return fmt.Sprintf("%s/%s/terraform.tfstate", DeploymentStateRemoteLocation, prefix)
|
return fmt.Sprintf("%s/%s/terraform.tfstate", DeploymentStateRemoteLocation, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TerraformDeployer) remoteState(ctx context.Context) (*tfjson.State, error) {
|
// returns structured representation of terraform state on DBFS.
|
||||||
|
func (d *TerraformDeployer) remoteState(ctx context.Context) (*tfjson.State, int, error) {
|
||||||
dbfs := storage.NewDbfsAPI(ctx, project.Current.Client())
|
dbfs := storage.NewDbfsAPI(ctx, project.Current.Client())
|
||||||
raw, err := dbfs.Read(d.remoteTfstateLoc())
|
raw, err := dbfs.Read(d.remoteTfstateLoc())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return d.tfstateFromReader(bytes.NewBuffer(raw))
|
return d.tfstateFromReader(bytes.NewBuffer(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// opens file handle for local-backend terraform state, that has to be closed in the calling
|
||||||
|
// methods. this file alone is not the authoritative state of deployment and has to properly
|
||||||
|
// be synced with remote counterpart.
|
||||||
func (d *TerraformDeployer) openLocalState() (*os.File, error) {
|
func (d *TerraformDeployer) openLocalState() (*os.File, error) {
|
||||||
return os.Open(fmt.Sprintf("%s/terraform.tfstate", d.WorkDir))
|
return os.Open(fmt.Sprintf("%s/terraform.tfstate", d.WorkDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TerraformDeployer) localState() (*tfjson.State, error) {
|
// returns structured representation of terraform state on local machine. as part of
|
||||||
raw, err := d.openLocalState()
|
// the optimistic concurrency control, please make sure to always compare the serial
|
||||||
|
// number of local and remote states before proceeding with deployment.
|
||||||
|
func (d *TerraformDeployer) localState() (*tfjson.State, int, error) {
|
||||||
|
local, err := d.openLocalState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return d.tfstateFromReader(raw)
|
defer local.Close()
|
||||||
|
return d.tfstateFromReader(local)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *TerraformDeployer) tfstateFromReader(reader io.Reader) (*tfjson.State, error) {
|
// converts input stream into structured representation of terraform state and deployment
|
||||||
|
// serial number, that helps controlling versioning and synchronisation via optimistic locking.
|
||||||
|
func (d *TerraformDeployer) tfstateFromReader(reader io.Reader) (*tfjson.State, int, error) {
|
||||||
var state tfjson.State
|
var state tfjson.State
|
||||||
state.UseJSONNumber(true)
|
state.UseJSONNumber(true)
|
||||||
decoder := json.NewDecoder(reader)
|
decoder := json.NewDecoder(reader)
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
err := decoder.Decode(&state)
|
err := decoder.Decode(&state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
err = state.Validate()
|
err = state.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return &state, nil
|
var serialWrapper struct {
|
||||||
|
Serial int `json:"serial,omitempty"`
|
||||||
|
}
|
||||||
|
// TODO: use byte buffer if this decoder fails on double reading
|
||||||
|
err = decoder.Decode(&serialWrapper)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return &state, serialWrapper.Serial, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uploads terraform state from local directory to designated DBFS location.
|
||||||
func (d *TerraformDeployer) uploadTfstate(ctx context.Context) error {
|
func (d *TerraformDeployer) uploadTfstate(ctx context.Context) error {
|
||||||
// scripts/azcli-integration/terraform.tfstate
|
|
||||||
dbfs := storage.NewDbfsAPI(ctx, project.Current.Client())
|
dbfs := storage.NewDbfsAPI(ctx, project.Current.Client())
|
||||||
f, err := d.openLocalState()
|
local, err := d.openLocalState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
raw, err := io.ReadAll(f)
|
defer local.Close()
|
||||||
|
raw, err := io.ReadAll(local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -135,15 +156,18 @@ func (d *TerraformDeployer) uploadTfstate(ctx context.Context) error {
|
||||||
return dbfs.Create(d.remoteTfstateLoc(), raw, true)
|
return dbfs.Create(d.remoteTfstateLoc(), raw, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downloads terraform state from DBFS to local working directory.
|
||||||
func (d *TerraformDeployer) downloadTfstate(ctx context.Context) error {
|
func (d *TerraformDeployer) downloadTfstate(ctx context.Context) error {
|
||||||
remote, err := d.remoteState(ctx)
|
remote, serialDeployed, err := d.remoteState(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Printf("[DEBUG] remote serial is %d", serialDeployed)
|
||||||
local, err := d.openLocalState()
|
local, err := d.openLocalState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer local.Close()
|
||||||
raw, err := json.Marshal(remote)
|
raw, err := json.Marshal(remote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
Loading…
Reference in New Issue