2017-02-22 07:13:46 +01:00
package atlas
import (
"fmt"
"net/url"
"os"
"strings"
"sync"
2018-03-21 02:43:02 +01:00
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
2017-02-22 07:13:46 +01:00
"github.com/hashicorp/terraform/backend"
2018-03-21 02:43:02 +01:00
"github.com/hashicorp/terraform/config/configschema"
2017-02-22 07:13:46 +01:00
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
2018-03-21 02:43:02 +01:00
const EnvVarToken = "ATLAS_TOKEN"
const EnvVarAddress = "ATLAS_ADDRESS"
2017-02-22 07:13:46 +01:00
// Backend is an implementation of EnhancedBackend that performs all operations
// in Atlas. State must currently also be stored in Atlas, although it is worth
// investigating in the future if state storage can be external as well.
type Backend struct {
// CLI and Colorize control the CLI output. If CLI is nil then no CLI
// output will be done. If CLIColor is nil then no coloring will be done.
CLI cli . Ui
CLIColor * colorstring . Colorize
// ContextOpts are the base context options to set when initializing a
// Terraform context. Many of these will be overridden or merged by
// Operation. See Operation for more details.
ContextOpts * terraform . ContextOpts
//---------------------------------------------------------------
// Internal fields, do not set
//---------------------------------------------------------------
// stateClient is the legacy state client, setup in Configure
stateClient * stateClient
// schema is the schema for configuration, set by init
schema * schema . Backend
// opLock locks operations
opLock sync . Mutex
}
2018-03-21 02:43:02 +01:00
func ( b * Backend ) ConfigSchema ( ) * configschema . Block {
return & configschema . Block {
Attributes : map [ string ] * configschema . Attribute {
"name" : {
Type : cty . String ,
2017-02-22 07:13:46 +01:00
Required : true ,
2018-03-21 02:43:02 +01:00
Description : "Full name of the environment in Terraform Enterprise, such as 'myorg/myenv'" ,
2017-02-22 07:13:46 +01:00
} ,
2018-03-21 02:43:02 +01:00
"access_token" : {
Type : cty . String ,
Optional : true ,
Description : "Access token to use to access Terraform Enterprise; the ATLAS_TOKEN environment variable is used if this argument is not set" ,
2017-02-22 07:13:46 +01:00
} ,
2018-03-21 02:43:02 +01:00
"address" : {
Type : cty . String ,
2017-02-22 07:13:46 +01:00
Optional : true ,
2018-03-21 02:43:02 +01:00
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." ,
2017-02-22 07:13:46 +01:00
} ,
} ,
}
}
2018-03-21 02:43:02 +01:00
func ( b * Backend ) ValidateConfig ( obj cty . Value ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
name := obj . GetAttr ( "name" ) . AsString ( )
if ct := strings . Count ( name , "/" ) ; ct != 1 {
diags = diags . Append ( tfdiags . AttributeValue (
tfdiags . Error ,
"Invalid workspace selector" ,
` The "name" argument must be an organization name and a workspace name separated by a slash, such as "acme/network-production". ` ,
cty . Path { cty . GetAttrStep { Name : "name" } } ,
) )
2017-02-22 07:13:46 +01:00
}
2018-03-21 02:43:02 +01:00
if v := obj . GetAttr ( "address" ) ; ! v . IsNull ( ) {
addr := v . AsString ( )
_ , err := url . Parse ( addr )
if err != nil {
diags = diags . Append ( tfdiags . AttributeValue (
tfdiags . Error ,
"Invalid Terraform Enterprise URL" ,
fmt . Sprintf ( ` The "address" argument must be a valid URL: %s. ` , err ) ,
cty . Path { cty . GetAttrStep { Name : "address" } } ,
) )
}
2017-02-22 07:13:46 +01:00
}
2018-03-21 02:43:02 +01:00
return diags
}
func ( b * Backend ) Configure ( obj cty . Value ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
2017-02-22 07:13:46 +01:00
2018-03-21 02:43:02 +01:00
client := & stateClient {
2017-02-22 07:13:46 +01:00
// This is optionally set during Atlas Terraform runs.
RunId : os . Getenv ( "ATLAS_RUN_ID" ) ,
}
2018-03-21 02:43:02 +01:00
name := obj . GetAttr ( "name" ) . AsString ( ) // assumed valid due to ValidateConfig method
slashIdx := strings . Index ( name , "/" )
client . User = name [ : slashIdx ]
client . Name = name [ slashIdx + 1 : ]
if v := obj . GetAttr ( "access_token" ) ; ! v . IsNull ( ) {
client . AccessToken = v . AsString ( )
} else {
client . AccessToken = os . Getenv ( EnvVarToken )
if client . AccessToken == "" {
diags = diags . Append ( tfdiags . AttributeValue (
tfdiags . Error ,
"Missing Terraform Enterprise access token" ,
` The "access_token" argument must be set unless the ATLAS_TOKEN environment variable is set to provide the authentication token for Terraform Enterprise. ` ,
cty . Path { cty . GetAttrStep { Name : "access_token" } } ,
) )
}
}
if v := obj . GetAttr ( "address" ) ; ! v . IsNull ( ) {
addr := v . AsString ( )
addrURL , err := url . Parse ( addr )
if err != nil {
// We already validated the URL in ValidateConfig, so this shouldn't happen
panic ( err )
}
client . Server = addr
client . ServerURL = addrURL
} else {
addr := os . Getenv ( EnvVarAddress )
if addr == "" {
addr = defaultAtlasServer
}
addrURL , err := url . Parse ( addr )
if err != nil {
diags = diags . Append ( tfdiags . AttributeValue (
tfdiags . Error ,
"Invalid Terraform Enterprise URL" ,
fmt . Sprintf ( ` The ATLAS_ADDRESS environment variable must contain a valid URL: %s. ` , err ) ,
cty . Path { cty . GetAttrStep { Name : "address" } } ,
) )
}
client . Server = addr
client . ServerURL = addrURL
}
b . stateClient = client
2017-02-22 07:13:46 +01:00
2018-03-21 02:43:02 +01:00
return diags
2018-07-04 12:11:35 +02:00
}
2018-03-21 02:43:02 +01:00
func ( b * Backend ) States ( ) ( [ ] string , error ) {
return nil , backend . ErrNamedStatesNotSupported
2018-07-04 12:11:35 +02:00
}
2018-03-21 02:43:02 +01:00
func ( b * Backend ) DeleteState ( name string ) error {
return backend . ErrNamedStatesNotSupported
2018-07-04 12:11:35 +02:00
}
func ( b * Backend ) State ( name string ) ( state . State , error ) {
if name != backend . DefaultStateName {
return nil , backend . ErrNamedStatesNotSupported
}
2018-03-21 02:43:02 +01:00
return & remote . State { Client : b . stateClient } , nil
2018-07-04 12:11:35 +02:00
}
// Colorize returns the Colorize structure that can be used for colorizing
// output. This is gauranteed to always return a non-nil value and so is useful
// as a helper to wrap any potentially colored strings.
func ( b * Backend ) Colorize ( ) * colorstring . Colorize {
if b . CLIColor != nil {
return b . CLIColor
}
return & colorstring . Colorize {
Colors : colorstring . DefaultColors ,
Disable : true ,
}
}