Skip to content

Commit

Permalink
[ECS] Support Plan Preview for ECS (#4881)
Browse files Browse the repository at this point in the history
* Impl ECS's Diff func of taskDef and serviceDef

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add ECSManifest's cache

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Impl ECSDiff for planpreview

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Enable ECS PlanPreview

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Draft: diff test

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add and fix tests of diff with TODO of lowerCamelCase

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* fix tests: add params to servicedef

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Remove wip comments

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Skip summarizing if no changes were detected

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add tags to test data

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* removed an unnecessary test case

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Clean comments, removing TODO

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* remove an unnecessary blank line in the test data

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Modify ApiVersion and Kind to apparent dummy

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add DiffStructs() for comparing non-k8s manifests

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* removed unstructured.Unstructured from ECS

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Separate DiffByCommand() to diff package

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Fix diff render output: split ServiceDef and TaskDef sections

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add a testcase to planpreview_test for ECS

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Combine unnecessary DiffBytesByCommand() to DiffByCommand()

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Remove IgnorePath from comparing ECS Manifests

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* rename func to DiffStructureds()

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* add comment of which func to use

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Removed LoadECSManifest() func

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Rename test func

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add tests of diffbycommand

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Removed duplicated tests

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Rename ECSManifest to ECSManifests

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Removed an unnecessary variable

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Renamed func to 'renderByCommand'

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Add check of existence of the command

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Rename func to 'RenderByCommand'

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Moved func 'RenderByCommand()' to renderer.go

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

* Fix nits: removed unnecessary if-state

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>

---------

Signed-off-by: t-kikuc <tkikuchi07f@gmail.com>
Co-authored-by: Khanh Tran <32532742+khanhtc1202@users.noreply.github.com>
  • Loading branch information
t-kikuc and khanhtc1202 authored Apr 30, 2024
1 parent 03e5a6c commit b17c25b
Show file tree
Hide file tree
Showing 15 changed files with 870 additions and 1 deletion.
12 changes: 11 additions & 1 deletion pkg/app/pipectl/cmd/planpreview/planpreview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ NOTE: An error occurred while building plan-preview for applications of the foll
ApplicationKind: model.ApplicationKind_CLOUDRUN,
Error: "missing key",
},
{
ApplicationId: "app-5",
ApplicationName: "app-5",
ApplicationUrl: "https://pipecd.dev/app-5",
ApplicationKind: model.ApplicationKind_ECS,
Error: "wrong application configuration",
},
},
},
{
Expand Down Expand Up @@ -196,14 +203,17 @@ changes-1
changes-2
---DETAILS_END---
NOTE: An error occurred while building plan-preview for the following 2 applications:
NOTE: An error occurred while building plan-preview for the following 3 applications:
1. app: app-3, env: env-3, kind: TERRAFORM
reason: wrong application configuration
2. app: app-4, kind: CLOUDRUN
reason: missing key
3. app: app-5, kind: ECS
reason: wrong application configuration
NOTE: An error occurred while building plan-preview for applications of the following 2 Pipeds:
1. piped: piped-name-1 (piped-1)
Expand Down
2 changes: 2 additions & 0 deletions pkg/app/piped/planpreview/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ func (b *builder) buildApp(ctx context.Context, worker int, command string, app
dr, err = b.terraformDiff(ctx, app, targetDSP, &buf)
case model.ApplicationKind_CLOUDRUN:
dr, err = b.cloudrundiff(ctx, app, targetDSP, preCommit, &buf)
case model.ApplicationKind_ECS:
dr, err = b.ecsdiff(ctx, app, targetDSP, preCommit, &buf)
default:
// TODO: Calculating planpreview's diff for other application kinds.
dr = &diffResult{
Expand Down
133 changes: 133 additions & 0 deletions pkg/app/piped/planpreview/ecsdiff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2024 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"
"io"

"github.com/pipe-cd/pipecd/pkg/app/piped/deploysource"
provider "github.com/pipe-cd/pipecd/pkg/app/piped/platformprovider/ecs"
"github.com/pipe-cd/pipecd/pkg/diff"
"github.com/pipe-cd/pipecd/pkg/model"
)

func (b *builder) ecsdiff(
ctx context.Context,
app *model.Application,
targetDSP deploysource.Provider,
lastCommit string,
buf *bytes.Buffer,
) (*diffResult, error) {
var (
oldManifests, newManifests provider.ECSManifests
err error
)

newManifests, err = b.loadECSManifests(ctx, *app, targetDSP)
if err != nil {
fmt.Fprintf(buf, "failed to load ecs manifests at the head commit (%v)\n", err)
return nil, err
}

if lastCommit == "" {
fmt.Fprintf(buf, "failed to find the commit of the last successful deployment")
return nil, fmt.Errorf("cannot get the old manifests without the last successful deployment")
}

runningDSP := deploysource.NewProvider(
b.workingDir,
deploysource.NewGitSourceCloner(b.gitClient, b.repoCfg, "running", lastCommit),
*app.GitPath,
b.secretDecrypter,
)

oldManifests, err = b.loadECSManifests(ctx, *app, runningDSP)
if err != nil {
fmt.Fprintf(buf, "failed to load ecs manifests at the running commit (%v)\n", err)
return nil, err
}

result, err := provider.Diff(
oldManifests,
newManifests,
diff.WithEquateEmpty(),
diff.WithCompareNumberAndNumericString(),
)
if err != nil {
fmt.Fprintf(buf, "failed to compare manifests (%v)\n", err)
return nil, err
}

if result.NoChange() {
fmt.Fprintln(buf, "No changes were detected")
return &diffResult{
summary: "No changes were detected",
noChange: true,
}, nil
}

details := result.Render(provider.DiffRenderOptions{
UseDiffCommand: true,
})
fmt.Fprintf(buf, "--- Last Deploy\n+++ Head Commit\n\n%s\n", details)

return &diffResult{
summary: fmt.Sprintf("%d changes were detected", len(result.Diff.Nodes())),
}, nil
}

func (b *builder) loadECSManifests(ctx context.Context, app model.Application, dsp deploysource.Provider) (provider.ECSManifests, error) {
commit := dsp.Revision()
cache := provider.ECSManifestsCache{
AppID: app.Id,
Cache: b.appManifestsCache,
Logger: b.logger,
}

manifests, ok := cache.Get(commit)
if ok {
return manifests, nil
}

ds, err := dsp.Get(ctx, io.Discard)
if err != nil {
return provider.ECSManifests{}, err
}

appCfg := ds.ApplicationConfig.ECSApplicationSpec
if appCfg == nil {
return provider.ECSManifests{}, fmt.Errorf("malformed application configuration file")
}

taskDef, err := provider.LoadTaskDefinition(ds.AppDir, appCfg.Input.TaskDefinitionFile)
if err != nil {
return provider.ECSManifests{}, err
}
serviceDef, err := provider.LoadServiceDefinition(ds.AppDir, appCfg.Input.ServiceDefinitionFile)
if err != nil {
return provider.ECSManifests{}, err
}

manifests = provider.ECSManifests{
TaskDefinition: &taskDef,
ServiceDefinition: &serviceDef,
}

cache.Put(commit, manifests)
return manifests, nil
}
68 changes: 68 additions & 0 deletions pkg/app/piped/platformprovider/ecs/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2024 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 ecs

import (
"errors"
"fmt"

"go.uber.org/zap"

"github.com/pipe-cd/pipecd/pkg/cache"
)

type ECSManifestsCache struct {
AppID string
Cache cache.Cache
Logger *zap.Logger
}

func (c ECSManifestsCache) Get(commit string) (ECSManifests, bool) {
key := ecsManifestsCacheKey(c.AppID, commit)
item, err := c.Cache.Get(key)
if err == nil {
return item.(ECSManifests), true
}

if errors.Is(err, cache.ErrNotFound) {
c.Logger.Info("ecs manifests were not found in cache",
zap.String("app-id", c.AppID),
zap.String("commit-hash", commit),
)
return ECSManifests{}, false
}

c.Logger.Error("failed while retrieving ecs manifests from cache",
zap.String("app-id", c.AppID),
zap.String("commit-hash", commit),
zap.Error(err),
)
return ECSManifests{}, false
}

func (c ECSManifestsCache) Put(commit string, sm ECSManifests) {
key := ecsManifestsCacheKey(c.AppID, commit)
if err := c.Cache.Put(key, sm); err != nil {
c.Logger.Error("failed while putting ecs manifests from cache",
zap.String("app-id", c.AppID),
zap.String("commit-hash", commit),
zap.Error(err),
)
}
}

func ecsManifestsCacheKey(appID, commit string) string {
return fmt.Sprintf("%s/%s", appID, commit)
}
100 changes: 100 additions & 0 deletions pkg/app/piped/platformprovider/ecs/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2024 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 ecs

import (
"bytes"
"fmt"
"strings"

"github.com/pipe-cd/pipecd/pkg/diff"
)

const (
diffCommand = "diff"
)

type DiffResult struct {
Diff *diff.Result
Old ECSManifests
New ECSManifests
}

func (d *DiffResult) NoChange() bool {
return len(d.Diff.Nodes()) == 0
}

func Diff(old, new ECSManifests, opts ...diff.Option) (*DiffResult, error) {
d, err := diff.DiffStructureds(old, new, opts...)
if err != nil {
return nil, err
}

if !d.HasDiff() {
return &DiffResult{Diff: d}, nil
}

ret := &DiffResult{
Old: old,
New: new,
Diff: d,
}
return ret, nil
}

type DiffRenderOptions struct {
// If true, use "diff" command to render.
UseDiffCommand bool
}

func (d *DiffResult) Render(opt DiffRenderOptions) string {
var b strings.Builder
opts := []diff.RenderOption{
diff.WithLeftPadding(1),
}
renderer := diff.NewRenderer(opts...)
if !opt.UseDiffCommand {
b.WriteString(renderer.Render(d.Diff.Nodes()))
} else {
d, err := renderByCommand(diffCommand, d.Old, d.New)
if err != nil {
b.WriteString(fmt.Sprintf("An error occurred while rendering diff (%v)", err))
} else {
b.Write(d)
}
}
b.WriteString("\n")

return b.String()
}

func renderByCommand(command string, old, new ECSManifests) ([]byte, error) {
taskDiff, err := diff.RenderByCommand(command, old.TaskDefinition, new.TaskDefinition)
if err != nil {
return nil, err
}

serviceDiff, err := diff.RenderByCommand(command, old.ServiceDefinition, new.ServiceDefinition)
if err != nil {
return nil, err
}

return bytes.Join([][]byte{
[]byte("# 1. ServiceDefinition"),
serviceDiff,
[]byte("\n# 2. TaskDefinition"),
taskDiff,
}, []byte("\n")), nil
}
Loading

0 comments on commit b17c25b

Please sign in to comment.