Skip to content

Commit b17c25b

Browse files
t-kikuckhanhtc1202
andauthored
[ECS] Support Plan Preview for ECS (#4881)
* Impl ECS's Diff func of taskDef and serviceDef Signed-off-by: t-kikuc <[email protected]> * Add ECSManifest's cache Signed-off-by: t-kikuc <[email protected]> * Impl ECSDiff for planpreview Signed-off-by: t-kikuc <[email protected]> * Enable ECS PlanPreview Signed-off-by: t-kikuc <[email protected]> * Draft: diff test Signed-off-by: t-kikuc <[email protected]> * Add and fix tests of diff with TODO of lowerCamelCase Signed-off-by: t-kikuc <[email protected]> * fix tests: add params to servicedef Signed-off-by: t-kikuc <[email protected]> * Remove wip comments Signed-off-by: t-kikuc <[email protected]> * Skip summarizing if no changes were detected Signed-off-by: t-kikuc <[email protected]> * Add tags to test data Signed-off-by: t-kikuc <[email protected]> * removed an unnecessary test case Signed-off-by: t-kikuc <[email protected]> * Clean comments, removing TODO Signed-off-by: t-kikuc <[email protected]> * remove an unnecessary blank line in the test data Signed-off-by: t-kikuc <[email protected]> * Modify ApiVersion and Kind to apparent dummy Signed-off-by: t-kikuc <[email protected]> * Add DiffStructs() for comparing non-k8s manifests Signed-off-by: t-kikuc <[email protected]> * removed unstructured.Unstructured from ECS Signed-off-by: t-kikuc <[email protected]> * Separate DiffByCommand() to diff package Signed-off-by: t-kikuc <[email protected]> * Fix diff render output: split ServiceDef and TaskDef sections Signed-off-by: t-kikuc <[email protected]> * Add a testcase to planpreview_test for ECS Signed-off-by: t-kikuc <[email protected]> * Combine unnecessary DiffBytesByCommand() to DiffByCommand() Signed-off-by: t-kikuc <[email protected]> * Remove IgnorePath from comparing ECS Manifests Signed-off-by: t-kikuc <[email protected]> * rename func to DiffStructureds() Signed-off-by: t-kikuc <[email protected]> * add comment of which func to use Signed-off-by: t-kikuc <[email protected]> * Removed LoadECSManifest() func Signed-off-by: t-kikuc <[email protected]> * Rename test func Signed-off-by: t-kikuc <[email protected]> * Add tests of diffbycommand Signed-off-by: t-kikuc <[email protected]> * Removed duplicated tests Signed-off-by: t-kikuc <[email protected]> * Rename ECSManifest to ECSManifests Signed-off-by: t-kikuc <[email protected]> * Removed an unnecessary variable Signed-off-by: t-kikuc <[email protected]> * Renamed func to 'renderByCommand' Signed-off-by: t-kikuc <[email protected]> * Add check of existence of the command Signed-off-by: t-kikuc <[email protected]> * Rename func to 'RenderByCommand' Signed-off-by: t-kikuc <[email protected]> * Moved func 'RenderByCommand()' to renderer.go Signed-off-by: t-kikuc <[email protected]> * Fix nits: removed unnecessary if-state Signed-off-by: t-kikuc <[email protected]> --------- Signed-off-by: t-kikuc <[email protected]> Co-authored-by: Khanh Tran <[email protected]>
1 parent 03e5a6c commit b17c25b

File tree

15 files changed

+870
-1
lines changed

15 files changed

+870
-1
lines changed

pkg/app/pipectl/cmd/planpreview/planpreview_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ NOTE: An error occurred while building plan-preview for applications of the foll
165165
ApplicationKind: model.ApplicationKind_CLOUDRUN,
166166
Error: "missing key",
167167
},
168+
{
169+
ApplicationId: "app-5",
170+
ApplicationName: "app-5",
171+
ApplicationUrl: "https://pipecd.dev/app-5",
172+
ApplicationKind: model.ApplicationKind_ECS,
173+
Error: "wrong application configuration",
174+
},
168175
},
169176
},
170177
{
@@ -196,14 +203,17 @@ changes-1
196203
changes-2
197204
---DETAILS_END---
198205
199-
NOTE: An error occurred while building plan-preview for the following 2 applications:
206+
NOTE: An error occurred while building plan-preview for the following 3 applications:
200207
201208
1. app: app-3, env: env-3, kind: TERRAFORM
202209
reason: wrong application configuration
203210
204211
2. app: app-4, kind: CLOUDRUN
205212
reason: missing key
206213
214+
3. app: app-5, kind: ECS
215+
reason: wrong application configuration
216+
207217
NOTE: An error occurred while building plan-preview for applications of the following 2 Pipeds:
208218
209219
1. piped: piped-name-1 (piped-1)

pkg/app/piped/planpreview/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ func (b *builder) buildApp(ctx context.Context, worker int, command string, app
249249
dr, err = b.terraformDiff(ctx, app, targetDSP, &buf)
250250
case model.ApplicationKind_CLOUDRUN:
251251
dr, err = b.cloudrundiff(ctx, app, targetDSP, preCommit, &buf)
252+
case model.ApplicationKind_ECS:
253+
dr, err = b.ecsdiff(ctx, app, targetDSP, preCommit, &buf)
252254
default:
253255
// TODO: Calculating planpreview's diff for other application kinds.
254256
dr = &diffResult{

pkg/app/piped/planpreview/ecsdiff.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2024 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+
"io"
22+
23+
"github.com/pipe-cd/pipecd/pkg/app/piped/deploysource"
24+
provider "github.com/pipe-cd/pipecd/pkg/app/piped/platformprovider/ecs"
25+
"github.com/pipe-cd/pipecd/pkg/diff"
26+
"github.com/pipe-cd/pipecd/pkg/model"
27+
)
28+
29+
func (b *builder) ecsdiff(
30+
ctx context.Context,
31+
app *model.Application,
32+
targetDSP deploysource.Provider,
33+
lastCommit string,
34+
buf *bytes.Buffer,
35+
) (*diffResult, error) {
36+
var (
37+
oldManifests, newManifests provider.ECSManifests
38+
err error
39+
)
40+
41+
newManifests, err = b.loadECSManifests(ctx, *app, targetDSP)
42+
if err != nil {
43+
fmt.Fprintf(buf, "failed to load ecs manifests at the head commit (%v)\n", err)
44+
return nil, err
45+
}
46+
47+
if lastCommit == "" {
48+
fmt.Fprintf(buf, "failed to find the commit of the last successful deployment")
49+
return nil, fmt.Errorf("cannot get the old manifests without the last successful deployment")
50+
}
51+
52+
runningDSP := deploysource.NewProvider(
53+
b.workingDir,
54+
deploysource.NewGitSourceCloner(b.gitClient, b.repoCfg, "running", lastCommit),
55+
*app.GitPath,
56+
b.secretDecrypter,
57+
)
58+
59+
oldManifests, err = b.loadECSManifests(ctx, *app, runningDSP)
60+
if err != nil {
61+
fmt.Fprintf(buf, "failed to load ecs manifests at the running commit (%v)\n", err)
62+
return nil, err
63+
}
64+
65+
result, err := provider.Diff(
66+
oldManifests,
67+
newManifests,
68+
diff.WithEquateEmpty(),
69+
diff.WithCompareNumberAndNumericString(),
70+
)
71+
if err != nil {
72+
fmt.Fprintf(buf, "failed to compare manifests (%v)\n", err)
73+
return nil, err
74+
}
75+
76+
if result.NoChange() {
77+
fmt.Fprintln(buf, "No changes were detected")
78+
return &diffResult{
79+
summary: "No changes were detected",
80+
noChange: true,
81+
}, nil
82+
}
83+
84+
details := result.Render(provider.DiffRenderOptions{
85+
UseDiffCommand: true,
86+
})
87+
fmt.Fprintf(buf, "--- Last Deploy\n+++ Head Commit\n\n%s\n", details)
88+
89+
return &diffResult{
90+
summary: fmt.Sprintf("%d changes were detected", len(result.Diff.Nodes())),
91+
}, nil
92+
}
93+
94+
func (b *builder) loadECSManifests(ctx context.Context, app model.Application, dsp deploysource.Provider) (provider.ECSManifests, error) {
95+
commit := dsp.Revision()
96+
cache := provider.ECSManifestsCache{
97+
AppID: app.Id,
98+
Cache: b.appManifestsCache,
99+
Logger: b.logger,
100+
}
101+
102+
manifests, ok := cache.Get(commit)
103+
if ok {
104+
return manifests, nil
105+
}
106+
107+
ds, err := dsp.Get(ctx, io.Discard)
108+
if err != nil {
109+
return provider.ECSManifests{}, err
110+
}
111+
112+
appCfg := ds.ApplicationConfig.ECSApplicationSpec
113+
if appCfg == nil {
114+
return provider.ECSManifests{}, fmt.Errorf("malformed application configuration file")
115+
}
116+
117+
taskDef, err := provider.LoadTaskDefinition(ds.AppDir, appCfg.Input.TaskDefinitionFile)
118+
if err != nil {
119+
return provider.ECSManifests{}, err
120+
}
121+
serviceDef, err := provider.LoadServiceDefinition(ds.AppDir, appCfg.Input.ServiceDefinitionFile)
122+
if err != nil {
123+
return provider.ECSManifests{}, err
124+
}
125+
126+
manifests = provider.ECSManifests{
127+
TaskDefinition: &taskDef,
128+
ServiceDefinition: &serviceDef,
129+
}
130+
131+
cache.Put(commit, manifests)
132+
return manifests, nil
133+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2024 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 ecs
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
21+
"go.uber.org/zap"
22+
23+
"github.com/pipe-cd/pipecd/pkg/cache"
24+
)
25+
26+
type ECSManifestsCache struct {
27+
AppID string
28+
Cache cache.Cache
29+
Logger *zap.Logger
30+
}
31+
32+
func (c ECSManifestsCache) Get(commit string) (ECSManifests, bool) {
33+
key := ecsManifestsCacheKey(c.AppID, commit)
34+
item, err := c.Cache.Get(key)
35+
if err == nil {
36+
return item.(ECSManifests), true
37+
}
38+
39+
if errors.Is(err, cache.ErrNotFound) {
40+
c.Logger.Info("ecs manifests were not found in cache",
41+
zap.String("app-id", c.AppID),
42+
zap.String("commit-hash", commit),
43+
)
44+
return ECSManifests{}, false
45+
}
46+
47+
c.Logger.Error("failed while retrieving ecs manifests from cache",
48+
zap.String("app-id", c.AppID),
49+
zap.String("commit-hash", commit),
50+
zap.Error(err),
51+
)
52+
return ECSManifests{}, false
53+
}
54+
55+
func (c ECSManifestsCache) Put(commit string, sm ECSManifests) {
56+
key := ecsManifestsCacheKey(c.AppID, commit)
57+
if err := c.Cache.Put(key, sm); err != nil {
58+
c.Logger.Error("failed while putting ecs manifests from cache",
59+
zap.String("app-id", c.AppID),
60+
zap.String("commit-hash", commit),
61+
zap.Error(err),
62+
)
63+
}
64+
}
65+
66+
func ecsManifestsCacheKey(appID, commit string) string {
67+
return fmt.Sprintf("%s/%s", appID, commit)
68+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2024 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 ecs
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"strings"
21+
22+
"github.com/pipe-cd/pipecd/pkg/diff"
23+
)
24+
25+
const (
26+
diffCommand = "diff"
27+
)
28+
29+
type DiffResult struct {
30+
Diff *diff.Result
31+
Old ECSManifests
32+
New ECSManifests
33+
}
34+
35+
func (d *DiffResult) NoChange() bool {
36+
return len(d.Diff.Nodes()) == 0
37+
}
38+
39+
func Diff(old, new ECSManifests, opts ...diff.Option) (*DiffResult, error) {
40+
d, err := diff.DiffStructureds(old, new, opts...)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
if !d.HasDiff() {
46+
return &DiffResult{Diff: d}, nil
47+
}
48+
49+
ret := &DiffResult{
50+
Old: old,
51+
New: new,
52+
Diff: d,
53+
}
54+
return ret, nil
55+
}
56+
57+
type DiffRenderOptions struct {
58+
// If true, use "diff" command to render.
59+
UseDiffCommand bool
60+
}
61+
62+
func (d *DiffResult) Render(opt DiffRenderOptions) string {
63+
var b strings.Builder
64+
opts := []diff.RenderOption{
65+
diff.WithLeftPadding(1),
66+
}
67+
renderer := diff.NewRenderer(opts...)
68+
if !opt.UseDiffCommand {
69+
b.WriteString(renderer.Render(d.Diff.Nodes()))
70+
} else {
71+
d, err := renderByCommand(diffCommand, d.Old, d.New)
72+
if err != nil {
73+
b.WriteString(fmt.Sprintf("An error occurred while rendering diff (%v)", err))
74+
} else {
75+
b.Write(d)
76+
}
77+
}
78+
b.WriteString("\n")
79+
80+
return b.String()
81+
}
82+
83+
func renderByCommand(command string, old, new ECSManifests) ([]byte, error) {
84+
taskDiff, err := diff.RenderByCommand(command, old.TaskDefinition, new.TaskDefinition)
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
serviceDiff, err := diff.RenderByCommand(command, old.ServiceDefinition, new.ServiceDefinition)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
return bytes.Join([][]byte{
95+
[]byte("# 1. ServiceDefinition"),
96+
serviceDiff,
97+
[]byte("\n# 2. TaskDefinition"),
98+
taskDiff,
99+
}, []byte("\n")), nil
100+
}

0 commit comments

Comments
 (0)