github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/atlas/backend.go (about)

     1  package atlas
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  
    10  	"github.com/hashicorp/terraform/tfdiags"
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/hashicorp/terraform/backend"
    14  	"github.com/hashicorp/terraform/configs/configschema"
    15  	"github.com/hashicorp/terraform/state"
    16  	"github.com/hashicorp/terraform/state/remote"
    17  	"github.com/hashicorp/terraform/terraform"
    18  	"github.com/mitchellh/cli"
    19  	"github.com/mitchellh/colorstring"
    20  )
    21  
    22  const EnvVarToken = "ATLAS_TOKEN"
    23  const EnvVarAddress = "ATLAS_ADDRESS"
    24  
    25  // Backend is an implementation of EnhancedBackend that performs all operations
    26  // in Atlas. State must currently also be stored in Atlas, although it is worth
    27  // investigating in the future if state storage can be external as well.
    28  type Backend struct {
    29  	// CLI and Colorize control the CLI output. If CLI is nil then no CLI
    30  	// output will be done. If CLIColor is nil then no coloring will be done.
    31  	CLI      cli.Ui
    32  	CLIColor *colorstring.Colorize
    33  
    34  	// ContextOpts are the base context options to set when initializing a
    35  	// Terraform context. Many of these will be overridden or merged by
    36  	// Operation. See Operation for more details.
    37  	ContextOpts *terraform.ContextOpts
    38  
    39  	//---------------------------------------------------------------
    40  	// Internal fields, do not set
    41  	//---------------------------------------------------------------
    42  	// stateClient is the legacy state client, setup in Configure
    43  	stateClient *stateClient
    44  
    45  	// opLock locks operations
    46  	opLock sync.Mutex
    47  }
    48  
    49  var _ backend.Backend = (*Backend)(nil)
    50  
    51  // New returns a new initialized Atlas backend.
    52  func New() *Backend {
    53  	return &Backend{}
    54  }
    55  
    56  func (b *Backend) ConfigSchema() *configschema.Block {
    57  	return &configschema.Block{
    58  		Attributes: map[string]*configschema.Attribute{
    59  			"name": {
    60  				Type:        cty.String,
    61  				Required:    true,
    62  				Description: "Full name of the environment in Terraform Enterprise, such as 'myorg/myenv'",
    63  			},
    64  			"access_token": {
    65  				Type:        cty.String,
    66  				Optional:    true,
    67  				Description: "Access token to use to access Terraform Enterprise; the ATLAS_TOKEN environment variable is used if this argument is not set",
    68  			},
    69  			"address": {
    70  				Type:        cty.String,
    71  				Optional:    true,
    72  				Description: "Base URL for your Terraform Enterprise installation; the ATLAS_ADDRESS environment variable is used if this argument is not set, finally falling back to a default of 'https://atlas.hashicorp.com/' if neither are set.",
    73  			},
    74  		},
    75  	}
    76  }
    77  
    78  func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
    79  	var diags tfdiags.Diagnostics
    80  
    81  	name := obj.GetAttr("name").AsString()
    82  	if ct := strings.Count(name, "/"); ct != 1 {
    83  		diags = diags.Append(tfdiags.AttributeValue(
    84  			tfdiags.Error,
    85  			"Invalid workspace selector",
    86  			`The "name" argument must be an organization name and a workspace name separated by a slash, such as "acme/network-production".`,
    87  			cty.Path{cty.GetAttrStep{Name: "name"}},
    88  		))
    89  	}
    90  
    91  	if v := obj.GetAttr("address"); !v.IsNull() {
    92  		addr := v.AsString()
    93  		_, err := url.Parse(addr)
    94  		if err != nil {
    95  			diags = diags.Append(tfdiags.AttributeValue(
    96  				tfdiags.Error,
    97  				"Invalid Terraform Enterprise URL",
    98  				fmt.Sprintf(`The "address" argument must be a valid URL: %s.`, err),
    99  				cty.Path{cty.GetAttrStep{Name: "address"}},
   100  			))
   101  		}
   102  	}
   103  
   104  	return obj, diags
   105  }
   106  
   107  func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
   108  	var diags tfdiags.Diagnostics
   109  
   110  	client := &stateClient{
   111  		// This is optionally set during Atlas Terraform runs.
   112  		RunId: os.Getenv("ATLAS_RUN_ID"),
   113  	}
   114  
   115  	name := obj.GetAttr("name").AsString() // assumed valid due to PrepareConfig method
   116  	slashIdx := strings.Index(name, "/")
   117  	client.User = name[:slashIdx]
   118  	client.Name = name[slashIdx+1:]
   119  
   120  	if v := obj.GetAttr("access_token"); !v.IsNull() {
   121  		client.AccessToken = v.AsString()
   122  	} else {
   123  		client.AccessToken = os.Getenv(EnvVarToken)
   124  		if client.AccessToken == "" {
   125  			diags = diags.Append(tfdiags.AttributeValue(
   126  				tfdiags.Error,
   127  				"Missing Terraform Enterprise access token",
   128  				`The "access_token" argument must be set unless the ATLAS_TOKEN environment variable is set to provide the authentication token for Terraform Enterprise.`,
   129  				cty.Path{cty.GetAttrStep{Name: "access_token"}},
   130  			))
   131  		}
   132  	}
   133  
   134  	if v := obj.GetAttr("address"); !v.IsNull() {
   135  		addr := v.AsString()
   136  		addrURL, err := url.Parse(addr)
   137  		if err != nil {
   138  			// We already validated the URL in PrepareConfig, so this shouldn't happen
   139  			panic(err)
   140  		}
   141  		client.Server = addr
   142  		client.ServerURL = addrURL
   143  	} else {
   144  		addr := os.Getenv(EnvVarAddress)
   145  		if addr == "" {
   146  			addr = defaultAtlasServer
   147  		}
   148  		addrURL, err := url.Parse(addr)
   149  		if err != nil {
   150  			diags = diags.Append(tfdiags.AttributeValue(
   151  				tfdiags.Error,
   152  				"Invalid Terraform Enterprise URL",
   153  				fmt.Sprintf(`The ATLAS_ADDRESS environment variable must contain a valid URL: %s.`, err),
   154  				cty.Path{cty.GetAttrStep{Name: "address"}},
   155  			))
   156  		}
   157  		client.Server = addr
   158  		client.ServerURL = addrURL
   159  	}
   160  
   161  	b.stateClient = client
   162  
   163  	return diags
   164  }
   165  
   166  func (b *Backend) Workspaces() ([]string, error) {
   167  	return nil, backend.ErrWorkspacesNotSupported
   168  }
   169  
   170  func (b *Backend) DeleteWorkspace(name string) error {
   171  	return backend.ErrWorkspacesNotSupported
   172  }
   173  
   174  func (b *Backend) StateMgr(name string) (state.State, error) {
   175  	if name != backend.DefaultStateName {
   176  		return nil, backend.ErrWorkspacesNotSupported
   177  	}
   178  
   179  	return &remote.State{Client: b.stateClient}, nil
   180  }
   181  
   182  func (b *Backend) StateMgrWithoutCheckVersion(name string) (state.State, error) {
   183  	return b.StateMgr(name)
   184  }
   185  
   186  // Colorize returns the Colorize structure that can be used for colorizing
   187  // output. This is gauranteed to always return a non-nil value and so is useful
   188  // as a helper to wrap any potentially colored strings.
   189  func (b *Backend) Colorize() *colorstring.Colorize {
   190  	if b.CLIColor != nil {
   191  		return b.CLIColor
   192  	}
   193  
   194  	return &colorstring.Colorize{
   195  		Colors:  colorstring.DefaultColors,
   196  		Disable: true,
   197  	}
   198  }