diff --git a/pkg/app/pipedv1/plugin/terraform/go.mod b/pkg/app/pipedv1/plugin/terraform/go.mod index 0084d063d5..a6654703d8 100644 --- a/pkg/app/pipedv1/plugin/terraform/go.mod +++ b/pkg/app/pipedv1/plugin/terraform/go.mod @@ -4,8 +4,9 @@ go 1.24.2 require ( github.com/hashicorp/hcl/v2 v2.0.0 - github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250702080240-3ef0619b560c + github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250822060248-d10a3b690599 github.com/stretchr/testify v1.10.0 + go.uber.org/zap v1.19.1 ) require ( @@ -35,7 +36,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect - github.com/pipe-cd/pipecd v0.52.0 // indirect + github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -51,10 +52,10 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.19.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect @@ -65,7 +66,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/grpc v1.64.1 // indirect google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/yaml v1.5.0 // indirect ) diff --git a/pkg/app/pipedv1/plugin/terraform/go.sum b/pkg/app/pipedv1/plugin/terraform/go.sum index a44d561f41..dea52a7746 100644 --- a/pkg/app/pipedv1/plugin/terraform/go.sum +++ b/pkg/app/pipedv1/plugin/terraform/go.sum @@ -224,10 +224,10 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pipe-cd/pipecd v0.52.0 h1:/WRzHs4hqeYRJBvu0ask6UAO7qBlvPgN1ulBdA1VjgE= -github.com/pipe-cd/pipecd v0.52.0/go.mod h1:Hi4d3mndTeY+hPB4YbN9aIgvP00EBV0CM+NQgyEwn98= -github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250702080240-3ef0619b560c h1:SIB/5S/kpoq4ymBlZwMaky/fnyDHYNN7MdtWC6GgB7Q= -github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250702080240-3ef0619b560c/go.mod h1:WpVRto2ZLgFRJ4VOk8gtTChHNCrGa4UjRhGN81TCl2E= +github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5 h1:1VM6ZkE2YfXqROq3lU8xrOV21MdJ257p19VX71E/nsU= +github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5/go.mod h1:5H0ydj0eUpGnJOesA2GPU3mTVlZEZDb8cNP7/lvNPTU= +github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250822060248-d10a3b690599 h1:fvEUqZHeGqzUYyejNK4oR5UGXw0MM8v+uZdNQekPztQ= +github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250822060248-d10a3b690599/go.mod h1:JjOYv2tMx72fvLpe88KG8cvrlHiI5XKYeZBvdDO3g80= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -320,6 +320,10 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= diff --git a/pkg/app/pipedv1/plugin/terraform/main.go b/pkg/app/pipedv1/plugin/terraform/main.go index b2e0f9fba2..6a649eb64e 100644 --- a/pkg/app/pipedv1/plugin/terraform/main.go +++ b/pkg/app/pipedv1/plugin/terraform/main.go @@ -21,6 +21,7 @@ import ( "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/deployment" "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/livestate" + "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/planpreview" ) func main() { @@ -28,6 +29,7 @@ func main() { "0.0.1", sdk.WithDeploymentPlugin(&deployment.Plugin{}), sdk.WithLivestatePlugin(&livestate.Plugin{}), + sdk.WithPlanPreviewPlugin(&planpreview.Plugin{}), ) if err != nil { log.Fatalln(err) diff --git a/pkg/app/pipedv1/plugin/terraform/planpreview/plugin.go b/pkg/app/pipedv1/plugin/terraform/planpreview/plugin.go new file mode 100644 index 0000000000..6f57da766a --- /dev/null +++ b/pkg/app/pipedv1/plugin/terraform/planpreview/plugin.go @@ -0,0 +1,84 @@ +// Copyright 2025 The PipeCD Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package planpreview + +import ( + "bytes" + "context" + "fmt" + + "go.uber.org/zap" + + "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/config" + "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/provider" + + sdk "github.com/pipe-cd/piped-plugin-sdk-go" +) + +var ( + _ sdk.PlanPreviewPlugin[sdk.ConfigNone, config.DeployTargetConfig, config.ApplicationConfigSpec] = (*Plugin)(nil) +) + +type Plugin struct{} + +// GetPlanPreview implements sdk.PlanPreviewPlugin. +func (p *Plugin) GetPlanPreview(ctx context.Context, _ *sdk.ConfigNone, dts []*sdk.DeployTarget[config.DeployTargetConfig], input *sdk.GetPlanPreviewInput[config.ApplicationConfigSpec]) (*sdk.GetPlanPreviewResponse, error) { + if len(dts) != 1 { + return nil, fmt.Errorf("only 1 deploy target is allowed but got %d", len(dts)) + } + dt := dts[0] + + cmd, err := provider.NewTerraformCommand(ctx, input.Client, input.Request.TargetDeploymentSource, dt) + if err != nil { + input.Logger.Error("Failed to initialize Terraform command", zap.Error(err)) + return nil, err + } + + buf := &bytes.Buffer{} + planResult, err := cmd.Plan(ctx, buf) + if err != nil { + input.Logger.Error("Failed to execute plan", zap.Error(err)) + return nil, err + } + + return toResponse(planResult, buf, dt.Name), nil +} + +func toResponse(planResult provider.PlanResult, planBuf *bytes.Buffer, deployTarget string) *sdk.GetPlanPreviewResponse { + if planResult.NoChanges() { + return &sdk.GetPlanPreviewResponse{ + Results: []sdk.PlanPreviewResult{ + { + DeployTarget: deployTarget, + NoChange: true, + Summary: "No changes were detected", + DiffLanguage: "hcl", + }, + }, + } + } + + return &sdk.GetPlanPreviewResponse{ + Results: []sdk.PlanPreviewResult{ + { + DeployTarget: deployTarget, + NoChange: false, + Summary: fmt.Sprintf("%d to import, %d to add, %d to change, %d to destroy", planResult.Imports, planResult.Adds, planResult.Changes, planResult.Destroys), + DiffLanguage: "hcl", + Details: planBuf.Bytes(), + }, + }, + } +} diff --git a/pkg/app/pipedv1/plugin/terraform/planpreview/plugin_test.go b/pkg/app/pipedv1/plugin/terraform/planpreview/plugin_test.go new file mode 100644 index 0000000000..eac5764b5f --- /dev/null +++ b/pkg/app/pipedv1/plugin/terraform/planpreview/plugin_test.go @@ -0,0 +1,98 @@ +// Copyright 2025 The PipeCD Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package planpreview + +import ( + "bytes" + "testing" + + sdk "github.com/pipe-cd/piped-plugin-sdk-go" + "github.com/stretchr/testify/assert" + + "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/terraform/provider" +) + +func TestToResponse(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + planResult provider.PlanResult + planBuf *bytes.Buffer + want *sdk.GetPlanPreviewResponse + }{ + { + name: "no changes", + planResult: provider.PlanResult{ + Imports: 0, + Adds: 0, + Changes: 0, + Destroys: 0, + }, + planBuf: bytes.NewBuffer([]byte("No changes.")), + want: &sdk.GetPlanPreviewResponse{ + Results: []sdk.PlanPreviewResult{ + { + DeployTarget: "dt-1", + NoChange: true, + Summary: "No changes were detected", + DiffLanguage: "hcl", + }, + }, + }, + }, + { + name: "with changes", + planResult: provider.PlanResult{ + Imports: 1, + Adds: 2, + Changes: 3, + Destroys: 4, + }, + planBuf: bytes.NewBuffer([]byte(` +Terraform will perform the following actions: + +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +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.`)), + want: &sdk.GetPlanPreviewResponse{ + Results: []sdk.PlanPreviewResult{ + { + DeployTarget: "dt-1", + NoChange: false, + Summary: "1 to import, 2 to add, 3 to change, 4 to destroy", + DiffLanguage: "hcl", + Details: []byte(` +Terraform will perform the following actions: + +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +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.`), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := toResponse(tt.planResult, tt.planBuf, "dt-1") + + assert.Equal(t, tt.want, got) + }) + } +}