diff --git a/commands.go b/commands.go index 14483381ae..dfec5d0a90 100644 --- a/commands.go +++ b/commands.go @@ -414,6 +414,14 @@ func initCommands( }, } + if meta.AllowExperimentalFeatures { + Commands["cloud"] = func() (cli.Command, error) { + return &command.CloudCommand{ + Meta: meta, + }, nil + } + } + PrimaryCommands = []string{ "init", "validate", diff --git a/internal/command/cloud.go b/internal/command/cloud.go new file mode 100644 index 0000000000..fd998d37c5 --- /dev/null +++ b/internal/command/cloud.go @@ -0,0 +1,102 @@ +package command + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/terraform/internal/cloudplugin" + "github.com/hashicorp/terraform/internal/cloudplugin/cloudplugin1" + "github.com/hashicorp/terraform/internal/logging" +) + +// 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 +} + +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 +) + +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, + } +) + +func (c *CloudCommand) proxy(args []string, stdout, stderr io.Writer) int { + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: Handshake, + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + Cmd: exec.Command("./terraform-cloudplugin"), + Logger: logging.NewCloudLogger(), + VersionedPlugins: map[int]plugin.PluginSet{ + 1: { + "cloud": &cloudplugin1.GRPCCloudPlugin{}, + }, + }, + }) + 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 + cloud1 := raw.(cloudplugin.Cloud1) + return cloud1.Execute(args, stdout, stderr) +} + +// Run runs the cloud command with the given arguments. +func (c *CloudCommand) Run(args []string) int { + args = c.Meta.process(args) + + // TODO: Download and verify the signing of the terraform-cloudplugin + // release that is appropriate for this OS/Arch + if _, err := os.Stat("./terraform-cloudplugin"); err != nil { + c.Ui.Warn("terraform-cloudplugin not found. This plugin does not have an official release, yet.") + return 1 + } + + // TODO: Need to use some type of c.Meta handle here + return c.proxy(args, os.Stdout, os.Stderr) +} + +// Help returns help text for the cloud command. +func (c *CloudCommand) Help() string { + helpText := new(bytes.Buffer) + c.proxy([]string{}, helpText, io.Discard) + + return helpText.String() +} + +// Synopsis returns a short summary of the cloud command. +func (c *CloudCommand) Synopsis() string { + return "Manage Terraform Cloud settings and metadata" +} diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 325b747ecb..92c33048dd 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -30,6 +30,7 @@ const ( // to other loggers, like provisioners and remote-state backends. envLogCore = "TF_LOG_CORE" envLogProvider = "TF_LOG_PROVIDER" + envLogCloud = "TF_LOG_CLOUD" ) var ( @@ -134,6 +135,20 @@ func NewProviderLogger(prefix string) hclog.Logger { return l } +// NewCloudLogger returns a logger for the cloud plugin, possibly with a +// different log level from the global logger. +func NewCloudLogger() hclog.Logger { + l := &logPanicWrapper{ + Logger: logger.Named("cloud"), + } + + level := cloudLogLevel() + logger.Debug("created cloud logger", "level", level) + + l.SetLevel(level) + return l +} + // CurrentLogLevel returns the current log level string based the environment vars func CurrentLogLevel() string { ll, _ := globalLogLevel() @@ -149,6 +164,15 @@ func providerLogLevel() hclog.Level { return parseLogLevel(providerEnvLevel) } +func cloudLogLevel() hclog.Level { + providerEnvLevel := strings.ToUpper(os.Getenv(envLogCloud)) + if providerEnvLevel == "" { + providerEnvLevel = strings.ToUpper(os.Getenv(envLog)) + } + + return parseLogLevel(providerEnvLevel) +} + func globalLogLevel() (hclog.Level, bool) { var json bool envLevel := strings.ToUpper(os.Getenv(envLog))