Skip to content

Commit 802e10c

Browse files
authored
[Terraform Plugin] Implemented PlanPreview (#6150)
* update sdk to latest Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * impl planpreview Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * update main.go Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> * fix lint Signed-off-by: t-kikuc <tkikuchi07f@gmail.com> --------- Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
1 parent f18eed5 commit 802e10c

File tree

5 files changed

+202
-14
lines changed

5 files changed

+202
-14
lines changed

pkg/app/pipedv1/plugin/terraform/go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ go 1.24.2
44

55
require (
66
github.com/hashicorp/hcl/v2 v2.0.0
7-
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250702080240-3ef0619b560c
7+
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250822060248-d10a3b690599
88
github.com/stretchr/testify v1.10.0
9+
go.uber.org/zap v1.19.1
910
)
1011

1112
require (
@@ -35,7 +36,7 @@ require (
3536
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3637
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
3738
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
38-
github.com/pipe-cd/pipecd v0.52.0 // indirect
39+
github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5 // indirect
3940
github.com/pmezard/go-difflib v1.0.0 // indirect
4041
github.com/prometheus/client_golang v1.12.1 // indirect
4142
github.com/prometheus/client_model v0.5.0 // indirect
@@ -51,10 +52,10 @@ require (
5152
go.opentelemetry.io/otel/trace v1.28.0 // indirect
5253
go.uber.org/atomic v1.11.0 // indirect
5354
go.uber.org/multierr v1.6.0 // indirect
54-
go.uber.org/zap v1.19.1 // indirect
55+
go.yaml.in/yaml/v2 v2.4.2 // indirect
5556
golang.org/x/crypto v0.36.0 // indirect
5657
golang.org/x/net v0.38.0 // indirect
57-
golang.org/x/oauth2 v0.21.0 // indirect
58+
golang.org/x/oauth2 v0.27.0 // indirect
5859
golang.org/x/sync v0.12.0 // indirect
5960
golang.org/x/sys v0.31.0 // indirect
6061
golang.org/x/text v0.23.0 // indirect
@@ -65,7 +66,6 @@ require (
6566
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
6667
google.golang.org/grpc v1.64.1 // indirect
6768
google.golang.org/protobuf v1.34.2 // indirect
68-
gopkg.in/yaml.v2 v2.4.0 // indirect
6969
gopkg.in/yaml.v3 v3.0.1 // indirect
70-
sigs.k8s.io/yaml v1.3.0 // indirect
70+
sigs.k8s.io/yaml v1.5.0 // indirect
7171
)

pkg/app/pipedv1/plugin/terraform/go.sum

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,10 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
224224
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
225225
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
226226
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
227-
github.com/pipe-cd/pipecd v0.52.0 h1:/WRzHs4hqeYRJBvu0ask6UAO7qBlvPgN1ulBdA1VjgE=
228-
github.com/pipe-cd/pipecd v0.52.0/go.mod h1:Hi4d3mndTeY+hPB4YbN9aIgvP00EBV0CM+NQgyEwn98=
229-
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250702080240-3ef0619b560c h1:SIB/5S/kpoq4ymBlZwMaky/fnyDHYNN7MdtWC6GgB7Q=
230-
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250702080240-3ef0619b560c/go.mod h1:WpVRto2ZLgFRJ4VOk8gtTChHNCrGa4UjRhGN81TCl2E=
227+
github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5 h1:1VM6ZkE2YfXqROq3lU8xrOV21MdJ257p19VX71E/nsU=
228+
github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5/go.mod h1:5H0ydj0eUpGnJOesA2GPU3mTVlZEZDb8cNP7/lvNPTU=
229+
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250822060248-d10a3b690599 h1:fvEUqZHeGqzUYyejNK4oR5UGXw0MM8v+uZdNQekPztQ=
230+
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250822060248-d10a3b690599/go.mod h1:JjOYv2tMx72fvLpe88KG8cvrlHiI5XKYeZBvdDO3g80=
231231
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
232232
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
233233
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -320,6 +320,10 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
320320
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
321321
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
322322
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
323+
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
324+
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
325+
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
326+
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
323327
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
324328
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
325329
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -400,8 +404,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
400404
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
401405
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
402406
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
403-
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
404-
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
407+
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
408+
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
405409
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
406410
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
407411
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -632,5 +636,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
632636
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
633637
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
634638
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
635-
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
636-
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
639+
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
640+
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=

pkg/app/pipedv1/plugin/terraform/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ import (
2121

2222
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/deployment"
2323
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/livestate"
24+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/planpreview"
2425
)
2526

2627
func main() {
2728
plugin, err := sdk.NewPlugin(
2829
"0.0.1",
2930
sdk.WithDeploymentPlugin(&deployment.Plugin{}),
3031
sdk.WithLivestatePlugin(&livestate.Plugin{}),
32+
sdk.WithPlanPreviewPlugin(&planpreview.Plugin{}),
3133
)
3234
if err != nil {
3335
log.Fatalln(err)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2025 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package planpreview
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"fmt"
21+
22+
"go.uber.org/zap"
23+
24+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/config"
25+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/provider"
26+
27+
sdk "github.com/pipe-cd/piped-plugin-sdk-go"
28+
)
29+
30+
var (
31+
_ sdk.PlanPreviewPlugin[sdk.ConfigNone, config.DeployTargetConfig, config.ApplicationConfigSpec] = (*Plugin)(nil)
32+
)
33+
34+
type Plugin struct{}
35+
36+
// GetPlanPreview implements sdk.PlanPreviewPlugin.
37+
func (p *Plugin) GetPlanPreview(ctx context.Context, _ *sdk.ConfigNone, dts []*sdk.DeployTarget[config.DeployTargetConfig], input *sdk.GetPlanPreviewInput[config.ApplicationConfigSpec]) (*sdk.GetPlanPreviewResponse, error) {
38+
if len(dts) != 1 {
39+
return nil, fmt.Errorf("only 1 deploy target is allowed but got %d", len(dts))
40+
}
41+
dt := dts[0]
42+
43+
cmd, err := provider.NewTerraformCommand(ctx, input.Client, input.Request.TargetDeploymentSource, dt)
44+
if err != nil {
45+
input.Logger.Error("Failed to initialize Terraform command", zap.Error(err))
46+
return nil, err
47+
}
48+
49+
buf := &bytes.Buffer{}
50+
planResult, err := cmd.Plan(ctx, buf)
51+
if err != nil {
52+
input.Logger.Error("Failed to execute plan", zap.Error(err))
53+
return nil, err
54+
}
55+
56+
return toResponse(planResult, buf, dt.Name), nil
57+
}
58+
59+
func toResponse(planResult provider.PlanResult, planBuf *bytes.Buffer, deployTarget string) *sdk.GetPlanPreviewResponse {
60+
if planResult.NoChanges() {
61+
return &sdk.GetPlanPreviewResponse{
62+
Results: []sdk.PlanPreviewResult{
63+
{
64+
DeployTarget: deployTarget,
65+
NoChange: true,
66+
Summary: "No changes were detected",
67+
DiffLanguage: "hcl",
68+
},
69+
},
70+
}
71+
}
72+
73+
return &sdk.GetPlanPreviewResponse{
74+
Results: []sdk.PlanPreviewResult{
75+
{
76+
DeployTarget: deployTarget,
77+
NoChange: false,
78+
Summary: fmt.Sprintf("%d to import, %d to add, %d to change, %d to destroy", planResult.Imports, planResult.Adds, planResult.Changes, planResult.Destroys),
79+
DiffLanguage: "hcl",
80+
Details: planBuf.Bytes(),
81+
},
82+
},
83+
}
84+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2025 The PipeCD Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package planpreview
16+
17+
import (
18+
"bytes"
19+
"testing"
20+
21+
sdk "github.com/pipe-cd/piped-plugin-sdk-go"
22+
"github.com/stretchr/testify/assert"
23+
24+
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/provider"
25+
)
26+
27+
func TestToResponse(t *testing.T) {
28+
t.Parallel()
29+
30+
tests := []struct {
31+
name string
32+
planResult provider.PlanResult
33+
planBuf *bytes.Buffer
34+
want *sdk.GetPlanPreviewResponse
35+
}{
36+
{
37+
name: "no changes",
38+
planResult: provider.PlanResult{
39+
Imports: 0,
40+
Adds: 0,
41+
Changes: 0,
42+
Destroys: 0,
43+
},
44+
planBuf: bytes.NewBuffer([]byte("No changes.")),
45+
want: &sdk.GetPlanPreviewResponse{
46+
Results: []sdk.PlanPreviewResult{
47+
{
48+
DeployTarget: "dt-1",
49+
NoChange: true,
50+
Summary: "No changes were detected",
51+
DiffLanguage: "hcl",
52+
},
53+
},
54+
},
55+
},
56+
{
57+
name: "with changes",
58+
planResult: provider.PlanResult{
59+
Imports: 1,
60+
Adds: 2,
61+
Changes: 3,
62+
Destroys: 4,
63+
},
64+
planBuf: bytes.NewBuffer([]byte(`
65+
Terraform will perform the following actions:
66+
<plan-details>
67+
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
68+
69+
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.`)),
70+
want: &sdk.GetPlanPreviewResponse{
71+
Results: []sdk.PlanPreviewResult{
72+
{
73+
DeployTarget: "dt-1",
74+
NoChange: false,
75+
Summary: "1 to import, 2 to add, 3 to change, 4 to destroy",
76+
DiffLanguage: "hcl",
77+
Details: []byte(`
78+
Terraform will perform the following actions:
79+
<plan-details>
80+
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
81+
82+
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.`),
83+
},
84+
},
85+
},
86+
},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
t.Parallel()
92+
93+
got := toResponse(tt.planResult, tt.planBuf, "dt-1")
94+
95+
assert.Equal(t, tt.want, got)
96+
})
97+
}
98+
}

0 commit comments

Comments
 (0)