/* How to simplify terraform configuration for the project? --- Solve the following adoption slowers: - remove the need for `required_providers` block - authenticate Databricks provider with the same DatabricksClient - skip downloading and locking Databricks provider every time (few seconds) - users won't have to copy-paste these into their configs: ```hcl terraform { required_providers { databricks = { source = "databrickslabs/databricks" } } } provider "databricks" { } ``` Terraform Plugin SDK v2 is using similar techniques for testing providers. One may find details in github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource/plugin.go. In short: - init provider isntance - start terraform plugin GRPC server - "reattach" providers and specify the `tfexec.Reattach` options, which essentially forward GRPC address to terraform subprocess. - this can be done by either adding a source depenency on Databricks provider or adding a special launch mode to it. For now --- Let's see how far we can get without GRPC magic. */ package terraform const DeploymentStateRemoteLocation = "dbfs:/FileStore/deployment-state" // type TerraformDeployer struct { // WorkDir string // CopyTfs bool // tf *tfexec.Terraform // } // func (d *TerraformDeployer) Init(ctx context.Context) error { // if d.CopyTfs { // panic("copying tf configuration files to a temporary dir not yet implemented") // } // // TODO: most likely merge the methods // exec, err := newTerraform(ctx, d.WorkDir, map[string]string{}) // if err != nil { // return err // } // d.tf = exec // return nil // } // // returns location of terraform state on DBFS based on project's deployment isolation level. // func (d *TerraformDeployer) remoteTfstateLoc() string { // prefix := project.Current.DeploymentIsolationPrefix() // return fmt.Sprintf("%s/%s/terraform.tfstate", DeploymentStateRemoteLocation, prefix) // } // // returns structured representation of terraform state on DBFS. // func (d *TerraformDeployer) remoteState(ctx context.Context) (*tfjson.State, int, error) { // raw, err := utilities.ReadDbfsFile(ctx, // project.Current.WorkspacesClient(), // d.remoteTfstateLoc(), // ) // if err != nil { // return nil, 0, err // } // 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) { // return os.Open(fmt.Sprintf("%s/terraform.tfstate", d.WorkDir)) // } // // returns structured representation of terraform state on local machine. as part of // // 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 { // return nil, 0, err // } // defer local.Close() // return d.tfstateFromReader(local) // } // // 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 // state.UseJSONNumber(true) // decoder := json.NewDecoder(reader) // decoder.UseNumber() // err := decoder.Decode(&state) // if err != nil { // return nil, 0, err // } // err = state.Validate() // if err != nil { // return nil, 0, err // } // 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 { // local, err := d.openLocalState() // if err != nil { // return err // } // defer local.Close() // raw, err := io.ReadAll(local) // if err != nil { // return err // } // // TODO: make sure that deployment locks are implemented // return utilities.CreateDbfsFile(ctx, // project.Current.WorkspacesClient(), // d.remoteTfstateLoc(), // raw, // true, // ) // } // // downloads terraform state from DBFS to local working directory. // func (d *TerraformDeployer) downloadTfstate(ctx context.Context) error { // remote, serialDeployed, err := d.remoteState(ctx) // if err != nil { // return err // } // log.Debugf(ctx, "remote serial is %d", serialDeployed) // local, err := d.openLocalState() // if err != nil { // return err // } // defer local.Close() // raw, err := json.Marshal(remote) // if err != nil { // return err // } // _, err = io.Copy(local, bytes.NewBuffer(raw)) // return err // } // // installs terraform to a temporary directory (for now) // func installTerraform(ctx context.Context) (string, error) { // // TODO: let configuration and/or environment variable specify // // terraform binary. Or detect if terraform is installed in the $PATH // installer := &releases.ExactVersion{ // Product: product.Terraform, // Version: version.Must(version.NewVersion("1.1.0")), // } // return installer.Install(ctx) // } // func newTerraform(ctx context.Context, workDir string, env map[string]string) (*tfexec.Terraform, error) { // execPath, err := installTerraform(ctx) // if err != nil { // return nil, err // } // // TODO: figure out how to cleanup/skip `.terraform*` files and dirs, not to confuse users // // one of the options: take entire working directory with *.tf files and move them to tmpdir. // // make it optional, of course, otherwise debugging may become super hard. // tf, err := tfexec.NewTerraform(workDir, execPath) // if err != nil { // return nil, err // } // err = tf.SetEnv(env) // if err != nil { // return nil, err // } // return tf, err // }