Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 0 additions & 4 deletions examples/lambda/simple/.pipe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,3 @@
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html
apiVersion: pipecd.dev/v1beta1
kind: LambdaApp
spec:
input:
# Lambda code sourced from the same Git repository.
path: lambdas/helloworld
7 changes: 7 additions & 0 deletions examples/lambda/simple/function.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: pipecd.dev/v1beta1
kind: LambdaFunction
spec:
name: SimpleFunction
image: ecr.ap-northeast-1.amazonaws.com/lambda-test:v0.0.1
tags:
app: simple
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
cloud.google.com/go/firestore v1.2.0
cloud.google.com/go/storage v1.11.0
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46
github.com/aws/aws-sdk-go v1.34.5
github.com/aws/aws-sdk-go v1.36.21
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/envoyproxy/protoc-gen-validate v0.1.0
github.com/fsouza/fake-gcs-server v1.21.0
Expand All @@ -33,7 +33,7 @@ require (
go.uber.org/multierr v1.2.0 // indirect
go.uber.org/zap v1.10.1-0.20190709142728-9a9fa7d4b5f0
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
google.golang.org/api v0.31.0
Expand Down
83 changes: 10 additions & 73 deletions go.sum

Large diffs are not rendered by default.

31 changes: 29 additions & 2 deletions pkg/app/piped/cloudprovider/lambda/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["lambda.go"],
srcs = [
"client.go",
"lambda.go",
"function.go",
],
importpath = "github.com/pipe-cd/pipe/pkg/app/piped/cloudprovider/lambda",
visibility = ["//visibility:public"],
deps = [
"//pkg/config:go_default_library",
"@io_k8s_sigs_yaml//:go_default_library",
"@com_github_aws_aws_sdk_go//aws:go_default_library",
"@com_github_aws_aws_sdk_go//aws/awserr:go_default_library",
"@com_github_aws_aws_sdk_go//aws/credentials:go_default_library",
"@com_github_aws_aws_sdk_go//aws/credentials/ec2rolecreds:go_default_library",
"@com_github_aws_aws_sdk_go//aws/ec2metadata:go_default_library",
"@com_github_aws_aws_sdk_go//aws/session:go_default_library",
"@com_github_aws_aws_sdk_go//service/lambda:go_default_library",
"@org_golang_x_sync//singleflight:go_default_library",
"@org_uber_go_zap//:go_default_library",
],
)

go_test(
name = "go_default_test",
size = "small",
srcs = ["function_test.go"],
embed = [":go_default_library"],
deps = [
"@com_github_stretchr_testify//assert:go_default_library",
],
)
97 changes: 97 additions & 0 deletions pkg/app/piped/cloudprovider/lambda/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2021 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 (
"context"
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/lambda"
"go.uber.org/zap"
)

type client struct {
region string
client *lambda.Lambda
logger *zap.Logger
}

func newClient(region, profile, credentialsFile string, logger *zap.Logger) (*client, error) {
if region == "" {
return nil, fmt.Errorf("region is required field")
}

c := &client{
region: region,
logger: logger.Named("lambda"),
}

sess, err := session.NewSession()
if err != nil {
return nil, fmt.Errorf("failed to create a session: %w", err)
}
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{
Filename: credentialsFile,
Profile: profile,
},
&ec2rolecreds.EC2RoleProvider{
Client: ec2metadata.New(sess),
},
},
)
cfg := aws.NewConfig().WithRegion(c.region).WithCredentials(creds)
c.client = lambda.New(sess, cfg)

return c, nil
}

func (c *client) Apply(ctx context.Context, fm FunctionManifest, role string) error {
if role == "" {
return fmt.Errorf("role arn is required")
}
input := &lambda.CreateFunctionInput{
Code: &lambda.FunctionCode{ImageUri: &fm.Spec.ImageURI},
FunctionName: &fm.Spec.Name,
Role: &role,
}
_, err := c.client.CreateFunctionWithContext(ctx, input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case lambda.ErrCodeInvalidParameterValueException:
return fmt.Errorf("invalid parameter given: %w", err)
case lambda.ErrCodeServiceException:
return fmt.Errorf("aws lambda service encountered an internal error: %w", err)
case lambda.ErrCodeCodeStorageExceededException:
return fmt.Errorf("total code size per account exceeded: %w", err)
case lambda.ErrCodeResourceNotFoundException:
fallthrough
case lambda.ErrCodeResourceNotReadyException:
return fmt.Errorf("resource error occurred: %w", err)
}
}
return fmt.Errorf("unknown error given: %w", err)
}
return nil
}
116 changes: 116 additions & 0 deletions pkg/app/piped/cloudprovider/lambda/function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2021 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"
"io/ioutil"
"strings"

"sigs.k8s.io/yaml"
)

const (
versionV1Beta1 = "pipecd.dev/v1beta1"
functionManifestKind = "LambdaFunction"
)

type FunctionManifest struct {
Kind string `json:"kind"`
APIVersion string `json:"apiVersion,omitempty"`
Spec FunctionManifestSpec `json:"spec"`
}

func (fm *FunctionManifest) validate() error {
if fm.APIVersion != versionV1Beta1 {
return fmt.Errorf("unsupported version: %s", fm.APIVersion)
}
if fm.Kind != functionManifestKind {
return fmt.Errorf("invalid manifest kind given: %s", fm.Kind)
}
if err := fm.Spec.validate(); err != nil {
return err
}
return nil
}

// FunctionManifestSpec contains configuration for LambdaFunction.
type FunctionManifestSpec struct {
Name string `json:"name"`
ImageURI string `json:"image"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be more appropriate to replace it with ImageRef that clearly represents a specific image that has a unique digest. But it's up to you.

Copy link
Member Author

@khanhtc1202 khanhtc1202 Jan 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that name may confuse ( overlap with #1358 ImageRef struct ), besides this name specific exactly what I want to be given from here ( the URI to image on ecr registry ) so I think it's good for me 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, no objection to that 👍

Tags map[string]string `json:"tags,omitempty"`
}

func (fmp FunctionManifestSpec) validate() error {
if len(fmp.Name) == 0 {
return fmt.Errorf("lambda function is missing")
}
if len(fmp.ImageURI) == 0 {
return fmt.Errorf("image uri is missing")
}
return nil
}

func loadFunctionManifest(path string) (FunctionManifest, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some tests for this function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I forgot to commit that 🙏 actually, I wrote tests for the parseFunctionManifest instead 👀

data, err := ioutil.ReadFile(path)
if err != nil {
return FunctionManifest{}, err
}
return parseFunctionManifest(data)
}

func parseFunctionManifest(data []byte) (FunctionManifest, error) {
var obj FunctionManifest
if err := yaml.Unmarshal(data, &obj); err != nil {
return FunctionManifest{}, err
}
if err := obj.validate(); err != nil {
return FunctionManifest{}, err
}
return obj, nil
}

// DecideRevisionName returns revision name to apply.
func DecideRevisionName(fm FunctionManifest, commit string) (string, error) {
tag, err := FindImageTag(fm)
if err != nil {
return "", err
}
tag = strings.ReplaceAll(tag, ".", "")

if len(commit) > 7 {
commit = commit[:7]
}
return fmt.Sprintf("%s-%s-%s", fm.Spec.Name, tag, commit), nil
}

// FindImageTag parses image tag from given LambdaFunction manifest.
func FindImageTag(fm FunctionManifest) (string, error) {
name, tag := parseContainerImage(fm.Spec.ImageURI)
if name == "" {
return "", fmt.Errorf("image name could not be empty")
}
return tag, nil
}

func parseContainerImage(image string) (name, tag string) {
parts := strings.Split(image, ":")
if len(parts) == 2 {
tag = parts[1]
}
paths := strings.Split(parts[0], "/")
name = paths[len(paths)-1]
return
}
68 changes: 68 additions & 0 deletions pkg/app/piped/cloudprovider/lambda/function_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2021 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 (
"testing"

"github.com/stretchr/testify/assert"
)

func TestparseFunctionManifest(t *testing.T) {
testcases := []struct {
name string
data string
wantSpec interface{}
wantErr bool
}{
{
name: "correct config for LambdaFunction",
data: `{
"apiVersion": "pipecd.dev/v1beta1",
"kind": "LambdaFunction",
"spec": {
"name": "SimpleFunction",
"image": "ecr.region.amazonaws.com/lambda-simple-function:v0.0.1"
}
}`,
wantSpec: FunctionManifest{
Kind: "LambdaFunction",
APIVersion: "pipecd.dev/v1beta1",
Spec: FunctionManifestSpec{
Name: "SimpleFunction",
ImageURI: "ecr.region.amazonaws.com/lambda-simple-function:v0.0.1",
},
},
wantErr: false,
},
{
name: "missing required fields",
data: `{
"apiVersion": "pipecd.dev/v1beta1",
"kind": "LambdaFunction",
"spec": {}
}`,
wantSpec: FunctionManifest{},
wantErr: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
fm, err := parseFunctionManifest([]byte(tc.data))
assert.Equal(t, tc.wantErr, err != nil)
assert.Equal(t, tc.wantSpec, fm)
})
}
}
Loading