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 }