-
Notifications
You must be signed in to change notification settings - Fork 208
Lambda cloud provider #1347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lambda cloud provider #1347
Changes from all commits
cf52a91
55d5884
9d52491
3cdaad6
da4a21a
3840f54
b03ba2a
0d5f659
f67aa3f
d92a7cd
2584986
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
Large diffs are not rendered by default.
| 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", | ||
| ], | ||
| ) |
| 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: | ||
khanhtc1202 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fallthrough | ||
| case lambda.ErrCodeResourceNotReadyException: | ||
| return fmt.Errorf("resource error occurred: %w", err) | ||
| } | ||
| } | ||
| return fmt.Errorf("unknown error given: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
| 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"` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be more appropriate to replace it with
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that name may confuse ( overlap with #1358
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add some tests for this function?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry I forgot to commit that 🙏 actually, I wrote tests for the |
||
| 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 | ||
| } | ||
| 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) | ||
| }) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.