Skip to content

Commit

Permalink
Support Plan Preview for Lambda (#5046)
Browse files Browse the repository at this point in the history
  • Loading branch information
t-kikuc authored Aug 6, 2024
1 parent 65c96e0 commit 34363a8
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 2 deletions.
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 @@ -172,6 +172,13 @@ NOTE: An error occurred while building plan-preview for applications of the foll
ApplicationKind: model.ApplicationKind_ECS,
Error: "wrong application configuration",
},
{
ApplicationId: "app-6",
ApplicationName: "app-6",
ApplicationUrl: "https://pipecd.dev/app-6",
ApplicationKind: model.ApplicationKind_LAMBDA,
Error: "wrong application configuration",
},
},
},
{
Expand Down Expand Up @@ -203,7 +210,7 @@ changes-1
changes-2
---DETAILS_END---
NOTE: An error occurred while building plan-preview for the following 3 applications:
NOTE: An error occurred while building plan-preview for the following 4 applications:
1. app: app-3, env: env-3, kind: TERRAFORM
reason: wrong application configuration
Expand All @@ -214,6 +221,9 @@ NOTE: An error occurred while building plan-preview for the following 3 applicat
3. app: app-5, kind: ECS
reason: wrong application configuration
4. app: app-6, kind: LAMBDA
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
3 changes: 2 additions & 1 deletion pkg/app/piped/planpreview/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,9 @@ func (b *builder) buildApp(ctx context.Context, worker int, command string, app
dr, err = b.cloudrundiff(ctx, app, targetDSP, preCommit, &buf)
case model.ApplicationKind_ECS:
dr, err = b.ecsdiff(ctx, app, targetDSP, preCommit, &buf)
case model.ApplicationKind_LAMBDA:
dr, err = b.lambdadiff(ctx, app, targetDSP, preCommit, &buf)
default:
// TODO: Calculating planpreview's diff for other application kinds.
dr = &diffResult{
summary: fmt.Sprintf("%s application is not implemented yet (coming soon)", app.Kind.String()),
}
Expand Down
124 changes: 124 additions & 0 deletions pkg/app/piped/planpreview/lambdadiff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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/lambda"
"github.com/pipe-cd/pipecd/pkg/diff"
"github.com/pipe-cd/pipecd/pkg/model"
)

func (b *builder) lambdadiff(
ctx context.Context,
app *model.Application,
targetDSP deploysource.Provider,
lastCommit string,
buf *bytes.Buffer,
) (*diffResult, error) {
var (
oldManifest, newManifest provider.FunctionManifest
err error
)

newManifest, err = b.loadFunctionManifest(ctx, *app, targetDSP)
if err != nil {
fmt.Fprintf(buf, "failed to load lambda manifest 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 manifest without the last successful deployment")
}

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

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

result, err := provider.Diff(
oldManifest,
newManifest,
diff.WithEquateEmpty(),
diff.WithCompareNumberAndNumericString(),
)
if err != nil {
fmt.Fprintf(buf, "failed to compare manifest (%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) loadFunctionManifest(ctx context.Context, app model.Application, dsp deploysource.Provider) (provider.FunctionManifest, error) {
commit := dsp.Revision()
cache := provider.FunctionManifestCache{
AppID: app.Id,
Cache: b.appManifestsCache,
Logger: b.logger,
}

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

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

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

manifest, err = provider.LoadFunctionManifest(ds.AppDir, appCfg.Input.FunctionManifestFile)
if err != nil {
return provider.FunctionManifest{}, err
}

cache.Put(commit, manifest)
return manifest, nil
}
68 changes: 68 additions & 0 deletions pkg/app/piped/platformprovider/lambda/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 lambda

import (
"errors"
"fmt"

"go.uber.org/zap"

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

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

func (c FunctionManifestCache) Get(commit string) (FunctionManifest, bool) {
key := manifestCacheKey(c.AppID, commit)
item, err := c.Cache.Get(key)
if err == nil {
return item.(FunctionManifest), true
}

if errors.Is(err, cache.ErrNotFound) {
c.Logger.Info("function manifest wes not found in cache",
zap.String("app-id", c.AppID),
zap.String("commit-hash", commit),
)
return FunctionManifest{}, false
}

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

func (c FunctionManifestCache) Put(commit string, sm FunctionManifest) {
key := manifestCacheKey(c.AppID, commit)
if err := c.Cache.Put(key, sm); err != nil {
c.Logger.Error("failed while putting function manifest into cache",
zap.String("app-id", c.AppID),
zap.String("commit-hash", commit),
zap.Error(err),
)
}
}

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

import (
"fmt"
"strings"

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

const (
diffCommand = "diff"
)

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

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

func Diff(old, new FunctionManifest, 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 := diff.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()
}
Loading

0 comments on commit 34363a8

Please sign in to comment.