2023-08-16 14:04:52 -04:00
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
2023-06-28 20:23:38 -04:00
package command
import (
"bytes"
"fmt"
"io"
2023-07-28 15:11:34 -04:00
"log"
"net/url"
2023-06-28 20:23:38 -04:00
"os"
"os/exec"
2023-07-28 15:11:34 -04:00
"path"
"runtime"
2023-06-28 20:23:38 -04:00
2023-12-21 14:51:34 -05:00
"google.golang.org/grpc/metadata"
2023-06-28 20:23:38 -04:00
"github.com/hashicorp/go-plugin"
2023-07-28 15:11:34 -04:00
"github.com/hashicorp/terraform/internal/cloud"
2023-06-28 20:23:38 -04:00
"github.com/hashicorp/terraform/internal/cloudplugin/cloudplugin1"
"github.com/hashicorp/terraform/internal/logging"
2025-04-28 12:54:23 -04:00
"github.com/hashicorp/terraform/internal/pluginshared"
2023-07-28 15:11:34 -04:00
"github.com/hashicorp/terraform/internal/tfdiags"
2023-06-28 20:23:38 -04:00
)
// CloudCommand is a Command implementation that interacts with Terraform
// Cloud for operations that are inherently planless. It delegates
// all execution to an internal plugin.
type CloudCommand struct {
Meta
2023-12-21 14:51:34 -05:00
// Path to the plugin server executable
2023-07-28 15:11:34 -04:00
pluginBinary string
2023-12-21 14:51:34 -05:00
// Service URL we can download plugin release binaries from
pluginService * url . URL
// Everything the plugin needs to build a client and Do Things
pluginConfig CloudPluginConfig
2023-06-28 20:23:38 -04:00
}
const (
// DefaultCloudPluginVersion is the implied protocol version, though all
// historical versions are defined explicitly.
DefaultCloudPluginVersion = 1
// ExitRPCError is the exit code that is returned if an plugin
// communication error occurred.
ExitRPCError = 99
2023-07-28 15:11:34 -04:00
// ExitPluginError is the exit code that is returned if the plugin
// cannot be downloaded.
ExitPluginError = 98
2023-12-21 14:51:34 -05:00
2024-04-22 15:21:52 -04:00
// The regular HCP Terraform API service that the go-tfe client relies on.
2023-12-21 14:51:34 -05:00
tfeServiceID = "tfe.v2"
// The cloud plugin release download service that the BinaryManager relies
// on to fetch the plugin.
cloudpluginServiceID = "cloudplugin.v1"
2023-06-28 20:23:38 -04:00
)
var (
// Handshake is used to verify that the plugin is the appropriate plugin for
// the client. This is not a security verification.
Handshake = plugin . HandshakeConfig {
MagicCookieKey : "TF_CLOUDPLUGIN_MAGIC_COOKIE" ,
MagicCookieValue : "721fca41431b780ff3ad2623838faaa178d74c65e1cfdfe19537c31656496bf9f82d6c6707f71d81c8eed0db9043f79e56ab4582d013bc08ead14f57961461dc" ,
ProtocolVersion : DefaultCloudPluginVersion ,
}
2023-07-28 15:11:34 -04:00
// CloudPluginDataDir is the name of the directory within the data directory
CloudPluginDataDir = "cloudplugin"
2023-06-28 20:23:38 -04:00
)
2023-07-28 15:11:34 -04:00
func ( c * CloudCommand ) realRun ( args [ ] string , stdout , stderr io . Writer ) int {
args = c . Meta . process ( args )
diags := c . initPlugin ( )
2023-11-30 19:18:03 -05:00
if diags . HasWarnings ( ) || diags . HasErrors ( ) {
c . View . Diagnostics ( diags )
}
2023-07-28 15:11:34 -04:00
if diags . HasErrors ( ) {
return ExitPluginError
}
2023-06-28 20:23:38 -04:00
client := plugin . NewClient ( & plugin . ClientConfig {
HandshakeConfig : Handshake ,
AllowedProtocols : [ ] plugin . Protocol { plugin . ProtocolGRPC } ,
2023-07-28 15:11:34 -04:00
Cmd : exec . Command ( c . pluginBinary ) ,
2023-06-28 20:23:38 -04:00
Logger : logging . NewCloudLogger ( ) ,
VersionedPlugins : map [ int ] plugin . PluginSet {
1 : {
2023-12-19 22:20:07 -05:00
"cloud" : & cloudplugin1 . GRPCCloudPlugin {
Metadata : c . pluginConfig . ToMetadata ( ) ,
} ,
2023-06-28 20:23:38 -04:00
} ,
} ,
} )
defer client . Kill ( )
// Connect via RPC
rpcClient , err := client . Client ( )
if err != nil {
fmt . Fprintf ( stderr , "Failed to create cloud plugin client: %s" , err )
return ExitRPCError
}
// Request the plugin
raw , err := rpcClient . Dispense ( "cloud" )
if err != nil {
fmt . Fprintf ( stderr , "Failed to request cloud plugin interface: %s" , err )
return ExitRPCError
}
// Proxy the request
2023-07-21 12:15:58 -04:00
// Note: future changes will need to determine the type of raw when
// multiple versions are possible.
2025-04-28 12:54:23 -04:00
cloud1 , ok := raw . ( pluginshared . CustomPluginClient )
2023-07-21 12:15:58 -04:00
if ! ok {
2023-07-25 10:27:26 -04:00
c . Ui . Error ( "If more than one cloudplugin versions are available, they need to be added to the cloud command. This is a bug in Terraform." )
2023-07-21 12:15:58 -04:00
return ExitRPCError
}
2023-06-28 20:23:38 -04:00
return cloud1 . Execute ( args , stdout , stderr )
}
2023-12-21 14:51:34 -05:00
// discoverAndConfigure is an implementation detail of initPlugin. It fills in the
// pluginService and pluginConfig fields on a CloudCommand struct.
func ( c * CloudCommand ) discoverAndConfigure ( ) tfdiags . Diagnostics {
2023-07-28 15:11:34 -04:00
var diags tfdiags . Diagnostics
2023-12-21 14:51:34 -05:00
// First, spin up a Cloud backend. (Why? bc finding the info the plugin
// needs is hard, and the Cloud backend already knows how to do it all.)
backendConfig , bConfigDiags := c . loadBackendConfig ( "." )
diags = diags . Append ( bConfigDiags )
2023-07-28 15:11:34 -04:00
if diags . HasErrors ( ) {
2023-12-21 14:51:34 -05:00
return diags
2023-07-28 15:11:34 -04:00
}
b , backendDiags := c . Backend ( & BackendOpts {
2025-07-11 06:08:56 -04:00
BackendConfig : backendConfig ,
2023-07-28 15:11:34 -04:00
} )
diags = diags . Append ( backendDiags )
2023-12-21 14:51:34 -05:00
if diags . HasErrors ( ) {
return diags
2023-07-28 15:11:34 -04:00
}
2023-12-21 14:51:34 -05:00
cb , ok := b . ( * cloud . Cloud )
2023-07-28 15:11:34 -04:00
if ! ok {
2023-12-21 14:51:34 -05:00
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"No `cloud` block found" ,
"Cloud command requires that a `cloud` block be configured in the working directory" ,
) )
return diags
2023-07-28 15:11:34 -04:00
}
2023-12-21 14:51:34 -05:00
// Ok sweet. First, re-use the cached service discovery info for this TFC
// instance to find our plugin service and TFE API URLs:
pluginService , err := cb . ServicesHost . ServiceURL ( cloudpluginServiceID )
if err != nil {
return diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"Cloud plugin service not found" ,
err . Error ( ) ,
) )
}
c . pluginService = pluginService
2023-07-28 15:11:34 -04:00
2023-12-21 14:51:34 -05:00
tfeService , err := cb . ServicesHost . ServiceURL ( tfeServiceID )
if err != nil {
return diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
2024-04-22 15:21:52 -04:00
"HCP Terraform API service not found" ,
2023-12-21 14:51:34 -05:00
err . Error ( ) ,
) )
}
currentWorkspace , err := c . Workspace ( )
if err != nil {
// The only possible error here is "you set TF_WORKSPACE badly"
return diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"Bad current workspace" ,
err . Error ( ) ,
) )
}
// Now just steal everything we need so we can pass it to the plugin later.
c . pluginConfig = CloudPluginConfig {
Address : tfeService . String ( ) ,
BasePath : tfeService . Path ,
DisplayHostname : cb . Hostname ,
Token : cb . Token ,
Organization : cb . Organization ,
CurrentWorkspace : currentWorkspace ,
WorkspaceName : cb . WorkspaceMapping . Name ,
2024-10-25 14:46:03 -04:00
WorkspaceTags : cb . WorkspaceMapping . TagsAsSet ,
2023-12-21 14:51:34 -05:00
DefaultProjectName : cb . WorkspaceMapping . Project ,
}
return diags
2023-07-28 15:11:34 -04:00
}
func ( c * CloudCommand ) initPlugin ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
var errorSummary = "Cloud plugin initialization error"
// Initialization can be aborted by interruption signals
ctx , done := c . InterruptibleContext ( c . CommandContext ( ) )
defer done ( )
2023-12-21 14:51:34 -05:00
// Discover service URLs, and build out the plugin config
2023-12-27 19:23:20 -05:00
diags = diags . Append ( c . discoverAndConfigure ( ) )
2023-12-21 14:51:34 -05:00
if diags . HasErrors ( ) {
return diags
2023-07-28 15:11:34 -04:00
}
packagesPath , err := c . initPackagesCache ( )
if err != nil {
return diags . Append ( tfdiags . Sourceless ( tfdiags . Error , errorSummary , err . Error ( ) ) )
}
2023-11-30 19:18:03 -05:00
overridePath := os . Getenv ( "TF_CLOUD_PLUGIN_DEV_OVERRIDE" )
2025-04-28 12:54:23 -04:00
bm , err := pluginshared . NewCloudBinaryManager ( ctx , packagesPath , overridePath , c . pluginService , runtime . GOOS , runtime . GOARCH )
2023-07-28 15:11:34 -04:00
if err != nil {
return diags . Append ( tfdiags . Sourceless ( tfdiags . Error , errorSummary , err . Error ( ) ) )
}
2023-08-17 17:00:16 -04:00
version , err := bm . Resolve ( )
2023-07-28 15:11:34 -04:00
if err != nil {
return diags . Append ( tfdiags . Sourceless ( tfdiags . Error , "Cloud plugin download error" , err . Error ( ) ) )
}
var cacheTraceMsg = ""
if version . ResolvedFromCache {
cacheTraceMsg = " (resolved from cache)"
}
2023-11-30 19:18:03 -05:00
if version . ResolvedFromDevOverride {
cacheTraceMsg = " (resolved from dev override)"
detailMsg := fmt . Sprintf ( "Instead of using the current released version, Terraform is loading the cloud plugin from the following location:\n\n - %s\n\nOverriding the cloud plugin location can cause unexpected behavior, and is only intended for use when developing new versions of the plugin." , version . Path )
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Warning ,
"Cloud plugin development overrides are in effect" ,
detailMsg ,
) )
}
2023-08-17 17:00:16 -04:00
log . Printf ( "[TRACE] plugin %q binary located at %q%s" , version . ProductVersion , version . Path , cacheTraceMsg )
c . pluginBinary = version . Path
2023-11-30 19:18:03 -05:00
return diags
2023-07-28 15:11:34 -04:00
}
func ( c * CloudCommand ) initPackagesCache ( ) ( string , error ) {
packagesPath := path . Join ( c . WorkingDir . DataDir ( ) , CloudPluginDataDir )
if info , err := os . Stat ( packagesPath ) ; err != nil || ! info . IsDir ( ) {
log . Printf ( "[TRACE] initialized cloudplugin cache directory at %q" , packagesPath )
err = os . MkdirAll ( packagesPath , 0755 )
if err != nil {
return "" , fmt . Errorf ( "failed to initialize cloudplugin cache directory: %w" , err )
}
} else {
log . Printf ( "[TRACE] cloudplugin cache directory found at %q" , packagesPath )
}
return packagesPath , nil
}
// Run runs the cloud command with the given arguments.
func ( c * CloudCommand ) Run ( args [ ] string ) int {
args = c . Meta . process ( args )
2023-08-18 08:53:25 -04:00
return c . realRun ( args , c . Meta . Streams . Stdout . File , c . Meta . Streams . Stderr . File )
2023-06-28 20:23:38 -04:00
}
// Help returns help text for the cloud command.
func ( c * CloudCommand ) Help ( ) string {
helpText := new ( bytes . Buffer )
2023-07-28 15:11:34 -04:00
if exitCode := c . realRun ( [ ] string { } , helpText , io . Discard ) ; exitCode != 0 {
return ""
}
2023-06-28 20:23:38 -04:00
return helpText . String ( )
}
// Synopsis returns a short summary of the cloud command.
func ( c * CloudCommand ) Synopsis ( ) string {
2024-04-22 15:21:52 -04:00
return "Manage HCP Terraform settings and metadata"
2023-06-28 20:23:38 -04:00
}
2023-12-21 14:51:34 -05:00
2024-01-05 16:58:14 -05:00
// CloudPluginConfig is everything the cloud plugin needs to know to configure a
2024-04-22 15:21:52 -04:00
// client and talk to HCP Terraform.
2023-12-21 14:51:34 -05:00
type CloudPluginConfig struct {
// Maybe someday we can use struct tags to automate grabbing these out of
// the metadata headers! And verify client-side that we're sending the right
// stuff, instead of having it all be a stringly-typed mystery ball! I want
// to believe in that distant shining day! 🌻 Meantime, these struct tags
// serve purely as docs.
2024-01-05 16:58:14 -05:00
Address string ` md:"tfc-address" `
BasePath string ` md:"tfc-base-path" `
DisplayHostname string ` md:"tfc-display-hostname" `
Token string ` md:"tfc-token" `
Organization string ` md:"tfc-organization" `
// The actual selected workspace
2023-12-21 14:51:34 -05:00
CurrentWorkspace string ` md:"tfc-current-workspace" `
2024-01-05 16:58:14 -05:00
// The raw "WorkspaceMapping" attributes, which determine the workspaces
// that could be selected. Generally you want CurrentWorkspace instead, but
// these can potentially be useful for niche use cases.
2023-12-21 14:51:34 -05:00
WorkspaceName string ` md:"tfc-workspace-name" `
WorkspaceTags [ ] string ` md:"tfc-workspace-tags" `
DefaultProjectName string ` md:"tfc-default-project-name" `
}
func ( c CloudPluginConfig ) ToMetadata ( ) metadata . MD {
// First, do everything except tags the easy way
md := metadata . Pairs (
"tfc-address" , c . Address ,
"tfc-base-path" , c . BasePath ,
"tfc-display-hostname" , c . DisplayHostname ,
"tfc-token" , c . Token ,
"tfc-organization" , c . Organization ,
"tfc-current-workspace" , c . CurrentWorkspace ,
"tfc-workspace-name" , c . WorkspaceName ,
"tfc-default-project-name" , c . DefaultProjectName ,
)
// Then the straggler
md [ "tfc-workspace-tags" ] = c . WorkspaceTags
return md
}