Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
18e60f6
Implement plan preview for kubernetes_multicluster plugin
mohammedfirdouss Apr 17, 2026
39c3869
Merge branch 'master' into feat/plan-preview
mohammedfirdouss Apr 17, 2026
2bf5722
Fix prealloc lint issue in planpreview/plugin.go
mohammedfirdouss Apr 17, 2026
f8cecb7
Merge branch 'master' into feat/plan-preview
mohammedfirdouss Apr 18, 2026
39052c7
Merge branch 'master' into feat/plan-preview
mohammedfirdouss Apr 21, 2026
6fa266d
Merge branch 'master' into feat/plan-preview
mohammedfirdouss Apr 21, 2026
efd344b
Merge branch 'master' into feat/plan-preview
mohammedfirdouss Apr 23, 2026
5c35a9b
Retrigger plan preview
mohammedfirdouss Apr 20, 2026
e1a70bd
Add plan preview tests for kubernetes_multicluster plugin
mohammedfirdouss Apr 25, 2026
5be2869
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 4, 2026
ab6e6fc
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 7, 2026
bf0a647
ci: retrigger checks
mohammedfirdouss May 7, 2026
b27b713
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 9, 2026
1562de5
ci: retrigger checks
mohammedfirdouss May 9, 2026
87668cc
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 17, 2026
42fb94b
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 19, 2026
5e73ce3
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 22, 2026
aca5335
fix(kubernetes_multicluster): correct copyright year to 2026 in planp…
mohammedfirdouss May 22, 2026
d71f97f
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 24, 2026
25986d0
Merge upstream/master into feat/plan-preview and fix planpreview conf…
mohammedfirdouss May 24, 2026
3030785
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 25, 2026
67a1f81
Merge branch 'master' into feat/plan-preview
mohammedfirdouss May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/app/pipedv1/plugin/kubernetes_multicluster/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/config"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/deployment"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/livestate"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/planpreview"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/provider"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/toolregistry"
)
Expand Down Expand Up @@ -72,6 +73,7 @@ func main() {
sdk.WithInitializer[config.KubernetesApplicationSpec](&initializer{}),
sdk.WithDeploymentPlugin(&deployment.Plugin{}),
sdk.WithLivestatePlugin(&livestate.Plugin{}),
sdk.WithPlanPreviewPlugin(&planpreview.Plugin{}),
)
if err != nil {
log.Fatalln(err)
Expand Down
203 changes: 203 additions & 0 deletions pkg/app/pipedv1/plugin/kubernetes_multicluster/planpreview/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright 2026 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 (
"context"
"fmt"

sdk "github.com/pipe-cd/piped-plugin-sdk-go"
"github.com/pipe-cd/piped-plugin-sdk-go/diff"

kubeconfig "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/config"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/provider"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes_multicluster/toolregistry"
)

var (
_ sdk.PlanPreviewPlugin[kubeconfig.KubernetesPluginConfig, kubeconfig.KubernetesDeployTargetConfig, kubeconfig.KubernetesApplicationSpec] = (*Plugin)(nil)
)

// Plugin implements the sdk.PlanPreviewPlugin interface for the kubernetes_multicluster plugin.
type Plugin struct{}

// GetPlanPreview returns the plan preview result showing what will change across all deploy targets.
func (p *Plugin) GetPlanPreview(ctx context.Context, _ *kubeconfig.KubernetesPluginConfig, dts []*sdk.DeployTarget[kubeconfig.KubernetesDeployTargetConfig], input *sdk.GetPlanPreviewInput[kubeconfig.KubernetesApplicationSpec]) (*sdk.GetPlanPreviewResponse, error) {
toolRegistry := toolregistry.NewRegistry(input.Client.ToolRegistry())
loader := provider.NewLoader(toolRegistry)

targetDS := input.Request.TargetDeploymentSource
targetAppCfg, err := targetDS.AppConfig()
if err != nil {
return nil, err
}
targetSpec := targetAppCfg.Spec

runningDS := input.Request.RunningDeploymentSource

multiTargets := targetSpec.Input.MultiTargets

// Single-target fallback: no multiTargets configured — load manifests once and return one result.
if len(multiTargets) == 0 {
newManifests, err := loadManifests(ctx, loader, input, &targetDS, targetSpec, nil)
if err != nil {
return nil, err
}

var oldManifests []provider.Manifest
if runningDS.CommitHash != "" {
runningAppCfg, err := runningDS.AppConfig()
if err != nil {
return nil, err
}
oldManifests, err = loadManifests(ctx, loader, input, &runningDS, runningAppCfg.Spec, nil)
if err != nil {
return nil, err
}
}

result, err := provider.DiffList(
oldManifests,
newManifests,
input.Logger,
diff.WithEquateEmpty(),
diff.WithCompareNumberAndNumericString(),
)
if err != nil {
return nil, err
}

deployTargetName := ""
if len(dts) > 0 {
deployTargetName = dts[0].Name
}
return &sdk.GetPlanPreviewResponse{
Results: []sdk.PlanPreviewResult{toResult(result, deployTargetName)},
}, nil
}

// Multi-target: produce one PlanPreviewResult per deploy target.
results := make([]sdk.PlanPreviewResult, 0, len(dts))
for _, dt := range dts {
// Find the matching KubernetesMultiTarget config for this deploy target.
var mt *kubeconfig.KubernetesMultiTarget
for i := range multiTargets {
if multiTargets[i].Target.Name == dt.Name {
mt = &multiTargets[i]
break
}
}

newManifests, err := loadManifests(ctx, loader, input, &targetDS, targetSpec, mt)
if err != nil {
results = append(results, sdk.PlanPreviewResult{
DeployTarget: dt.Name,
NoChange: false,
Summary: fmt.Sprintf("Failed to load target manifests: %v", err),
DiffLanguage: "diff",
})
continue
}

var oldManifests []provider.Manifest
if runningDS.CommitHash != "" {
runningAppCfg, err := runningDS.AppConfig()
if err != nil {
return nil, err
}
oldManifests, err = loadManifests(ctx, loader, input, &runningDS, runningAppCfg.Spec, mt)
if err != nil {
results = append(results, sdk.PlanPreviewResult{
DeployTarget: dt.Name,
NoChange: false,
Summary: fmt.Sprintf("Failed to load running manifests: %v", err),
DiffLanguage: "diff",
})
continue
}
}

result, err := provider.DiffList(
oldManifests,
newManifests,
input.Logger,
diff.WithEquateEmpty(),
diff.WithCompareNumberAndNumericString(),
)
if err != nil {
results = append(results, sdk.PlanPreviewResult{
DeployTarget: dt.Name,
NoChange: false,
Summary: fmt.Sprintf("Failed to diff manifests: %v", err),
DiffLanguage: "diff",
})
continue
}

results = append(results, toResult(result, dt.Name))
}

return &sdk.GetPlanPreviewResponse{Results: results}, nil
}

// loadManifests loads manifests from the given deployment source, optionally overriding
// the manifest paths from the multiTarget config.
func loadManifests(ctx context.Context, loader *provider.Loader, input *sdk.GetPlanPreviewInput[kubeconfig.KubernetesApplicationSpec], ds *sdk.DeploymentSource[kubeconfig.KubernetesApplicationSpec], spec *kubeconfig.KubernetesApplicationSpec, mt *kubeconfig.KubernetesMultiTarget) ([]provider.Manifest, error) {
manifestPaths := spec.Input.Manifests
if mt != nil && len(mt.Manifests) > 0 {
manifestPaths = mt.Manifests
}

return loader.LoadManifests(ctx, provider.LoaderInput{
PipedID: input.Request.PipedID,
AppID: input.Request.ApplicationID,
CommitHash: ds.CommitHash,
AppName: input.Request.ApplicationName,
AppDir: ds.ApplicationDirectory,
ConfigFilename: ds.ApplicationConfigFilename,
Manifests: manifestPaths,
Namespace: spec.Input.Namespace,
KustomizeVersion: spec.Input.KustomizeVersion,
KustomizeOptions: spec.Input.KustomizeOptions,
HelmVersion: spec.Input.HelmVersion,
HelmChart: spec.Input.HelmChart,
HelmOptions: spec.Input.HelmOptions,
Logger: input.Logger,
})
}

// toResult converts a DiffListResult into a PlanPreviewResult for the given deploy target.
func toResult(result *provider.DiffListResult, deployTarget string) sdk.PlanPreviewResult {
if result.NoChanges() {
return sdk.PlanPreviewResult{
DeployTarget: deployTarget,
NoChange: true,
Summary: "No changes were detected",
DiffLanguage: "diff",
}
}

details := result.Render(provider.DiffRenderOptions{
MaskSecret: true,
})

return sdk.PlanPreviewResult{
DeployTarget: deployTarget,
NoChange: false,
Summary: fmt.Sprintf("%d added manifests, %d changed manifests, %d deleted manifests", len(result.Adds), len(result.Changes), len(result.Deletes)),
DiffLanguage: "diff",
Details: []byte(details),
}
}
Loading
Loading