Skip to content

Commit e6c3aab

Browse files
authored
Merge pull request #33018 from hashicorp/tf-5529-sro-tfe-version-check
2 parents 0e0db2d + 300a60f commit e6c3aab

File tree

7 files changed

+204
-18
lines changed

7 files changed

+204
-18
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ require (
3939
github.com/hashicorp/go-multierror v1.1.1
4040
github.com/hashicorp/go-plugin v1.4.3
4141
github.com/hashicorp/go-retryablehttp v0.7.2
42-
github.com/hashicorp/go-tfe v1.18.0
42+
github.com/hashicorp/go-tfe v1.21.0
4343
github.com/hashicorp/go-uuid v1.0.3
4444
github.com/hashicorp/go-version v1.6.0
4545
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
@@ -147,7 +147,7 @@ require (
147147
github.com/hashicorp/go-msgpack v0.5.4 // indirect
148148
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
149149
github.com/hashicorp/go-safetemp v1.0.0 // indirect
150-
github.com/hashicorp/go-slug v0.10.1 // indirect
150+
github.com/hashicorp/go-slug v0.11.0 // indirect
151151
github.com/hashicorp/golang-lru v0.5.1 // indirect
152152
github.com/hashicorp/serf v0.9.5 // indirect
153153
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect

go.sum

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -537,13 +537,13 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
537537
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
538538
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
539539
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
540-
github.com/hashicorp/go-slug v0.10.1 h1:05SCRWCBpCxOeP7stQHvMgOz0raCBCekaytu8Rg/RZ4=
541-
github.com/hashicorp/go-slug v0.10.1/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41RKLH301v4=
540+
github.com/hashicorp/go-slug v0.11.0 h1:l7cHWiBk8cnnskjheloW9h8PwXhihvwXbQiiFw2KqkY=
541+
github.com/hashicorp/go-slug v0.11.0/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41RKLH301v4=
542542
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
543543
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
544544
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
545-
github.com/hashicorp/go-tfe v1.18.0 h1:AjyZe2KSAyGHD1kbGYlY64PVYQPnJJON24qr97IjIqA=
546-
github.com/hashicorp/go-tfe v1.18.0/go.mod h1:T76X7dHKNEPEugPCZI3gDdaDdxUU4P4sqMZO60W57cQ=
545+
github.com/hashicorp/go-tfe v1.21.0 h1:sTZXf/MaC/iQ8HxKwYSL0xJSEVDwY+h4ngh/+na8vdk=
546+
github.com/hashicorp/go-tfe v1.21.0/go.mod h1:jedlLiHHiDeBKKpON4aIpTdsKbc2OaVbklEPI7XEHiY=
547547
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
548548
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
549549
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@@ -762,7 +762,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
762762
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
763763
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
764764
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
765-
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
765+
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
766766
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
767767
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 h1:DYtBXB7sVc3EOW5horg8j55cLZynhsLYhHrvQ/jXKKM=
768768
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=

internal/cloud/backend_plan.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"log"
1212
"os"
1313
"path/filepath"
14+
"strconv"
1415
"strings"
1516
"syscall"
1617
"time"
@@ -428,10 +429,10 @@ func (b *Cloud) renderPlanLogs(ctx context.Context, op *backend.Operation, run *
428429
return nil
429430
}
430431

431-
// Only call the renderer if the remote workspace has structured run output
432-
// enabled. The plan output will have already been rendered when the logs
433-
// were read if this wasn't the case.
434-
if run.Workspace.StructuredRunOutputEnabled && b.renderer != nil {
432+
// Determine whether we should call the renderer to generate the plan output
433+
// in human readable format. Otherwise we risk duplicate plan output since
434+
// plan output may be contained in the streamed log file.
435+
if ok, err := b.shouldRenderStructuredRunOutput(run); ok {
435436
// Fetch the redacted plan.
436437
redacted, err := readRedactedPlan(ctx, b.client.BaseURL(), b.token, run.Plan.ID)
437438
if err != nil {
@@ -440,11 +441,53 @@ func (b *Cloud) renderPlanLogs(ctx context.Context, op *backend.Operation, run *
440441

441442
// Render plan output.
442443
b.renderer.RenderHumanPlan(*redacted, op.PlanMode)
444+
} else if err != nil {
445+
return err
443446
}
444447

445448
return nil
446449
}
447450

451+
// shouldRenderStructuredRunOutput ensures the remote workspace has structured
452+
// run output enabled and, if using Terraform Enterprise, ensures it is a release
453+
// that supports enabling SRO for CLI-driven runs. The plan output will have
454+
// already been rendered when the logs were read if this wasn't the case.
455+
func (b *Cloud) shouldRenderStructuredRunOutput(run *tfe.Run) (bool, error) {
456+
if b.renderer == nil || !run.Workspace.StructuredRunOutputEnabled {
457+
return false, nil
458+
}
459+
460+
// If the cloud backend is configured against TFC, we only require that
461+
// the workspace has structured run output enabled.
462+
if b.client.IsCloud() && run.Workspace.StructuredRunOutputEnabled {
463+
return true, nil
464+
}
465+
466+
// If the cloud backend is configured against TFE, ensure the release version
467+
// supports enabling SRO for CLI runs.
468+
if b.client.IsEnterprise() {
469+
tfeVersion := b.client.RemoteTFEVersion()
470+
if tfeVersion != "" {
471+
v := strings.Split(tfeVersion[1:], "-")
472+
releaseDate, err := strconv.Atoi(v[0])
473+
if err != nil {
474+
return false, err
475+
}
476+
477+
// Any release older than 202302-1 will not support enabling SRO for
478+
// CLI-driven runs
479+
if releaseDate < 202302 {
480+
return false, nil
481+
} else if run.Workspace.StructuredRunOutputEnabled {
482+
return true, nil
483+
}
484+
}
485+
}
486+
487+
// Version of TFE is unknowable
488+
return false, nil
489+
}
490+
448491
const planDefaultHeader = `
449492
[reset][yellow]Running plan in Terraform Cloud. Output will stream here. Pressing Ctrl-C
450493
will stop streaming the logs, but will not stop the plan running remotely.[reset]

internal/cloud/backend_plan_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cloud
22

33
import (
44
"context"
5+
"net/http"
56
"os"
67
"os/signal"
78
"strings"
@@ -1250,3 +1251,120 @@ func TestCloud_planOtherError(t *testing.T) {
12501251
t.Fatalf("expected error message, got: %s", err.Error())
12511252
}
12521253
}
1254+
1255+
func TestCloud_planShouldRenderSRO(t *testing.T) {
1256+
t.Run("when instance is TFC", func(t *testing.T) {
1257+
handlers := map[string]func(http.ResponseWriter, *http.Request){
1258+
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
1259+
w.Header().Set("Content-Type", "application/json")
1260+
w.Header().Set("TFP-API-Version", "2.5")
1261+
w.Header().Set("TFP-AppName", "Terraform Cloud")
1262+
},
1263+
}
1264+
b, bCleanup := testBackendWithHandlers(t, handlers)
1265+
t.Cleanup(bCleanup)
1266+
b.renderer = &jsonformat.Renderer{}
1267+
1268+
t.Run("and SRO is enabled", func(t *testing.T) {
1269+
r := &tfe.Run{
1270+
Workspace: &tfe.Workspace{
1271+
StructuredRunOutputEnabled: true,
1272+
},
1273+
}
1274+
assertSRORendered(t, b, r, true)
1275+
})
1276+
1277+
t.Run("and SRO is not enabled", func(t *testing.T) {
1278+
r := &tfe.Run{
1279+
Workspace: &tfe.Workspace{
1280+
StructuredRunOutputEnabled: false,
1281+
},
1282+
}
1283+
assertSRORendered(t, b, r, false)
1284+
})
1285+
1286+
})
1287+
1288+
t.Run("when instance is TFE and version supports CLI SRO", func(t *testing.T) {
1289+
handlers := map[string]func(http.ResponseWriter, *http.Request){
1290+
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
1291+
w.Header().Set("Content-Type", "application/json")
1292+
w.Header().Set("TFP-API-Version", "2.5")
1293+
w.Header().Set("TFP-AppName", "Terraform Enterprise")
1294+
w.Header().Set("X-TFE-Version", "v202303-1")
1295+
},
1296+
}
1297+
b, bCleanup := testBackendWithHandlers(t, handlers)
1298+
t.Cleanup(bCleanup)
1299+
b.renderer = &jsonformat.Renderer{}
1300+
1301+
t.Run("and SRO is enabled", func(t *testing.T) {
1302+
r := &tfe.Run{
1303+
Workspace: &tfe.Workspace{
1304+
StructuredRunOutputEnabled: true,
1305+
},
1306+
}
1307+
assertSRORendered(t, b, r, true)
1308+
})
1309+
1310+
t.Run("and SRO is not enabled", func(t *testing.T) {
1311+
r := &tfe.Run{
1312+
Workspace: &tfe.Workspace{
1313+
StructuredRunOutputEnabled: false,
1314+
},
1315+
}
1316+
assertSRORendered(t, b, r, false)
1317+
})
1318+
})
1319+
1320+
t.Run("when instance is a known unsupported TFE release", func(t *testing.T) {
1321+
handlers := map[string]func(http.ResponseWriter, *http.Request){
1322+
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
1323+
w.Header().Set("Content-Type", "application/json")
1324+
w.Header().Set("TFP-API-Version", "2.5")
1325+
w.Header().Set("TFP-AppName", "Terraform Enterprise")
1326+
w.Header().Set("X-TFE-Version", "v202208-1")
1327+
},
1328+
}
1329+
b, bCleanup := testBackendWithHandlers(t, handlers)
1330+
t.Cleanup(bCleanup)
1331+
b.renderer = &jsonformat.Renderer{}
1332+
1333+
r := &tfe.Run{
1334+
Workspace: &tfe.Workspace{
1335+
StructuredRunOutputEnabled: true,
1336+
},
1337+
}
1338+
assertSRORendered(t, b, r, false)
1339+
})
1340+
1341+
t.Run("when instance is an unknown TFE release", func(t *testing.T) {
1342+
handlers := map[string]func(http.ResponseWriter, *http.Request){
1343+
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
1344+
w.Header().Set("Content-Type", "application/json")
1345+
w.Header().Set("TFP-API-Version", "2.5")
1346+
},
1347+
}
1348+
b, bCleanup := testBackendWithHandlers(t, handlers)
1349+
t.Cleanup(bCleanup)
1350+
b.renderer = &jsonformat.Renderer{}
1351+
1352+
r := &tfe.Run{
1353+
Workspace: &tfe.Workspace{
1354+
StructuredRunOutputEnabled: true,
1355+
},
1356+
}
1357+
assertSRORendered(t, b, r, false)
1358+
})
1359+
1360+
}
1361+
1362+
func assertSRORendered(t *testing.T, b *Cloud, r *tfe.Run, shouldRender bool) {
1363+
got, err := b.shouldRenderStructuredRunOutput(r)
1364+
if err != nil {
1365+
t.Fatalf("expected no error: %v", err)
1366+
}
1367+
if shouldRender != got {
1368+
t.Fatalf("expected SRO to be rendered: %t, got %t", shouldRender, got)
1369+
}
1370+
}

internal/cloud/backend_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ func TestCloud_setUnavailableTerraformVersion(t *testing.T) {
647647
}),
648648
})
649649

650-
b, bCleanup := testBackend(t, config)
650+
b, bCleanup := testBackend(t, config, nil)
651651
defer bCleanup()
652652

653653
// Make sure the workspace doesn't exist yet -- otherwise, we can't test what

internal/cloud/testing.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ var (
4343
tfeHost: {"token": testCred},
4444
})
4545
testBackendSingleWorkspaceName = "app-prod"
46+
defaultTFCPing = map[string]func(http.ResponseWriter, *http.Request){
47+
"/api/v2/ping": func(w http.ResponseWriter, r *http.Request) {
48+
w.Header().Set("Content-Type", "application/json")
49+
w.Header().Set("TFP-API-Version", "2.5")
50+
w.Header().Set("TFP-AppName", "Terraform Cloud")
51+
},
52+
}
4653
)
4754

4855
// mockInput is a mock implementation of terraform.UIInput.
@@ -79,7 +86,7 @@ func testBackendWithName(t *testing.T) (*Cloud, func()) {
7986
"tags": cty.NullVal(cty.Set(cty.String)),
8087
}),
8188
})
82-
return testBackend(t, obj)
89+
return testBackend(t, obj, defaultTFCPing)
8390
}
8491

8592
func testBackendWithTags(t *testing.T) (*Cloud, func()) {
@@ -96,7 +103,7 @@ func testBackendWithTags(t *testing.T) (*Cloud, func()) {
96103
),
97104
}),
98105
})
99-
return testBackend(t, obj)
106+
return testBackend(t, obj, nil)
100107
}
101108

102109
func testBackendNoOperations(t *testing.T) (*Cloud, func()) {
@@ -109,7 +116,20 @@ func testBackendNoOperations(t *testing.T) (*Cloud, func()) {
109116
"tags": cty.NullVal(cty.Set(cty.String)),
110117
}),
111118
})
112-
return testBackend(t, obj)
119+
return testBackend(t, obj, nil)
120+
}
121+
122+
func testBackendWithHandlers(t *testing.T, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, func()) {
123+
obj := cty.ObjectVal(map[string]cty.Value{
124+
"hostname": cty.NullVal(cty.String),
125+
"organization": cty.StringVal("hashicorp"),
126+
"token": cty.NullVal(cty.String),
127+
"workspaces": cty.ObjectVal(map[string]cty.Value{
128+
"name": cty.StringVal(testBackendSingleWorkspaceName),
129+
"tags": cty.NullVal(cty.Set(cty.String)),
130+
}),
131+
})
132+
return testBackend(t, obj, handlers)
113133
}
114134

115135
func testCloudState(t *testing.T) *State {
@@ -186,8 +206,13 @@ func testBackendWithOutputs(t *testing.T) (*Cloud, func()) {
186206
return b, cleanup
187207
}
188208

189-
func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) {
190-
s := testServer(t)
209+
func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, func()) {
210+
var s *httptest.Server
211+
if handlers != nil {
212+
s = testServerWithHandlers(handlers)
213+
} else {
214+
s = testServer(t)
215+
}
191216
b := New(testDisco(s))
192217

193218
// Configure the backend so the client is created.

internal/command/jsonformat/renderer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...Pla
5858
// version differences. This should work for alpha testing in the meantime.
5959
if plan.PlanFormatVersion != jsonplan.FormatVersion || plan.ProviderFormatVersion != jsonprovider.FormatVersion {
6060
renderer.Streams.Println(format.WordWrap(
61-
renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This plan was generated using a different version of Terraform, the diff presented here maybe missing representations of recent features."),
61+
renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This plan was generated using a different version of Terraform, the diff presented here may be missing representations of recent features."),
6262
renderer.Streams.Stdout.Columns()))
6363
}
6464

0 commit comments

Comments
 (0)