Add aws.lambda.Permission resource (#224)

Adds a Permission resource to enable lambda Functions
to be invoked by event sources.

Per #223, we are continuing to discuss whether these
should really be a seperate resource or just an inline
component of a Function resource.  However, until we
support cycle-breaking, we'll need them to be a separate
resource type.

Progress on #222.
This commit is contained in:
Luke Hoban 2017-06-06 18:42:00 -07:00 committed by GitHub
parent fd719d64cd
commit 977e56e03e
7 changed files with 247 additions and 17 deletions

View file

@ -15,6 +15,7 @@
import { jsonStringify, sha1hash, printf } from "@lumi/lumi/runtime"
import { Deployment, RestAPI, Stage } from "../apigateway"
import { Permission } from "../lambda"
import { Function } from "./function"
import { region } from "../config"
@ -74,6 +75,10 @@ function createPathSpec(lambdaARN: string): SwaggerOperation {
}
}
function createSourceARN(region: string, account: string, apiid: string, functionName: string): string {
return "arn:aws:execute-api:"+region+":"+account+":"+apiid+"/*/*/"+ functionName;
}
// API is a higher level abstraction for working with AWS APIGateway reources.
export class API {
public api: RestAPI
@ -110,6 +115,16 @@ export class API {
default:
throw new Error("Method not supported: " + method);
}
let apiName = "";
if(this.api.apiName !== undefined) {
apiName = this.api.apiName;
}
let invokePermission = new Permission(this.apiName + "_invoke_" + method + path, {
action: "lambda:invokeFunction",
function: lambda.lambda,
principal: "apigateway.amazonaws.com",
sourceARN: createSourceARN("us-east-1", "490047557317", apiName, "webapi-test-func"),
});
// TODO[pulumi/lumi#90]: Once we suport output properties, we can use `lambda.lambda.arn` as input
// to constructing this apigateway lambda invocation uri.
// this.swaggerSpec.paths[path][swaggerMethod] = createPathSpec(lambda.lambda.arn);

View file

@ -103,8 +103,8 @@ func (arn ARN) Parse() (Parts, error) {
if len(ps) > 5 {
parts.Resource = ps[5]
}
if len(ps) > 6 {
parts.Resource = parts.Resource + ":" + ps[6]
for i := 6; i < len(ps); i++ {
parts.Resource = parts.Resource + ":" + ps[i]
}
return parts, nil
}

View file

@ -29,12 +29,10 @@ func Test(t *testing.T) {
cleanupFunctions(ctx)
cleanupRoles(ctx)
functionProvider := NewFunctionProvider(ctx)
roleProvider := iamprovider.NewRoleProvider(ctx)
resources := map[string]testutil.Resource{
"role": {Provider: roleProvider, Token: iam.RoleToken},
"f": {Provider: functionProvider, Token: FunctionToken},
"role": {Provider: iamprovider.NewRoleProvider(ctx), Token: iam.RoleToken},
"f": {Provider: NewFunctionProvider(ctx), Token: FunctionToken},
"permission": {Provider: NewPermissionProvider(ctx), Token: PermissionToken},
}
steps := []testutil.Step{
testutil.Step{
@ -80,6 +78,17 @@ func Test(t *testing.T) {
}
},
},
testutil.ResourceGenerator{
Name: "permission",
Creator: func(ctx testutil.Context) interface{} {
return &lambda.Permission{
Name: aws.String(RESOURCEPREFIX),
Function: ctx.GetResourceID("f"),
Action: "lambda:InvokeFunction",
Principal: "apigateway.amazonaws.com",
}
},
},
},
}

View file

@ -0,0 +1,195 @@
// Licensed to Pulumi Corporation ("Pulumi") under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// Pulumi licenses this file to You 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 (
"crypto/sha1"
"encoding/json"
"fmt"
"strings"
"github.com/aws/aws-sdk-go/aws"
awslambda "github.com/aws/aws-sdk-go/service/lambda"
"github.com/pulumi/lumi/pkg/resource"
"github.com/pulumi/lumi/pkg/util/contract"
"github.com/pulumi/lumi/sdk/go/pkg/lumirpc"
"golang.org/x/net/context"
"github.com/pulumi/lumi/lib/aws/provider/arn"
"github.com/pulumi/lumi/lib/aws/provider/awsctx"
awscommon "github.com/pulumi/lumi/lib/aws/rpc"
"github.com/pulumi/lumi/lib/aws/rpc/lambda"
)
const PermissionToken = lambda.PermissionToken
const (
maxStatementID = 100
)
type policy struct {
Version string
ID string `json:"Id"`
Statement []statement
}
type statement struct {
Sid string
Effect string
Principal principal
Action string
Resource string
Condition map[string]map[string]string
}
type principal struct {
Service string
}
// NewPermissionID returns an AWS APIGateway Deployment ARN ID for the given restAPIID and deploymentID
func NewPermissionID(region, account, functionName, statementID string) resource.ID {
return arn.NewID("lambda", region, account, "function:"+functionName+":policy:"+statementID)
}
// ParsePermissionID parses an AWS APIGateway Deployment ARN ID to extract the restAPIID and deploymentID
func ParsePermissionID(id resource.ID) (string, string, error) {
res, err := arn.ParseResourceName(id)
if err != nil {
return "", "", err
}
parts := strings.Split(res, ":")
if len(parts) != 3 || parts[1] != "policy" {
return "", "", fmt.Errorf("expected Permission ARN of the form %v: %v",
"arn:aws:lambda:region:account:function:function-name:policy:statement-id", id)
}
return parts[0], parts[2], nil
}
// NewPermissionProvider creates a provider that handles Lambda permission operations.
func NewPermissionProvider(ctx *awsctx.Context) lumirpc.ResourceProviderServer {
ops := &permissionProvider{ctx}
return lambda.NewPermissionProvider(ops)
}
type permissionProvider struct {
ctx *awsctx.Context
}
// Check validates that the given property bag is valid for a resource of the given type.
func (p *permissionProvider) Check(ctx context.Context, obj *lambda.Permission) ([]error, error) {
var failures []error
return failures, nil
}
// Create allocates a new instance of the provided resource and returns its unique ID afterwards. (The input ID
// must be blank.) If this call fails, the resource must not have been created (i.e., it is "transacational").
func (p *permissionProvider) Create(ctx context.Context, obj *lambda.Permission) (resource.ID, error) {
// Auto-generate a name in part based on the resource name.
statementID := resource.NewUniqueHex(*obj.Name+"-", maxStatementID, sha1.Size)
functionName, err := arn.ParseResourceName(obj.Function)
if err != nil {
return "", err
}
fmt.Printf("Creating Lambda Permission '%v' with statement ID '%v'\n", obj.Name, statementID)
create := &awslambda.AddPermissionInput{
Action: aws.String(obj.Action),
FunctionName: aws.String(functionName),
Principal: aws.String(obj.Principal),
SourceAccount: obj.SourceAccount,
StatementId: aws.String(statementID),
}
if obj.SourceARN != nil {
create.SourceArn = aws.String(string(*obj.SourceARN))
}
_, err = p.ctx.Lambda().AddPermission(create)
if err != nil {
return "", err
}
return NewPermissionID(p.ctx.Region(), p.ctx.AccountID(), functionName, statementID), nil
}
// Get reads the instance state identified by ID, returning a populated resource object, or an error if not found.
func (p *permissionProvider) Get(ctx context.Context, id resource.ID) (*lambda.Permission, error) {
functionName, statementID, err := ParsePermissionID(id)
if err != nil {
return nil, err
}
resp, err := p.ctx.Lambda().GetPolicy(&awslambda.GetPolicyInput{
FunctionName: aws.String(functionName),
})
if err != nil {
return nil, err
}
contract.Assert(resp != nil)
contract.Assert(resp.Policy != nil)
policy := policy{}
err = json.Unmarshal([]byte(*resp.Policy), &policy)
if err != nil {
return nil, err
}
for _, statement := range policy.Statement {
if statement.Sid == statementID {
permission := &lambda.Permission{
Action: statement.Action,
Function: resource.ID(statement.Resource),
Principal: statement.Principal.Service,
}
if arnLike, ok := statement.Condition["ArnLike"]; ok {
sourceARN := awscommon.ARN(arnLike["AWS:SourceArn"])
permission.SourceARN = &sourceARN
}
if stringEquals, ok := statement.Condition["StringEquals"]; ok {
sourceAccount := stringEquals["AWS:SourceAccount"]
permission.SourceAccount = &sourceAccount
}
return permission, nil
}
}
return nil, fmt.Errorf("No statement found for id '%v'", id)
}
// InspectChange checks what impacts a hypothetical update will have on the resource's properties.
func (p *permissionProvider) InspectChange(ctx context.Context, id resource.ID,
old *lambda.Permission, new *lambda.Permission, diff *resource.ObjectDiff) ([]string, error) {
return nil, nil
}
// Update updates an existing resource with new values. Only those values in the provided property bag are updated
// to new values. The resource ID is returned and may be different if the resource had to be recreated.
func (p *permissionProvider) Update(ctx context.Context, id resource.ID,
old *lambda.Permission, new *lambda.Permission, diff *resource.ObjectDiff) error {
contract.Failf("No properties of Permission resource are updatable.")
return nil
}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
func (p *permissionProvider) Delete(ctx context.Context, id resource.ID) error {
functionName, statementID, err := ParsePermissionID(id)
if err != nil {
return err
}
fmt.Printf("Deleting Lambda Permission '%v'\n", statementID)
_, err = p.ctx.Lambda().RemovePermission(&awslambda.RemovePermissionInput{
FunctionName: aws.String(functionName),
StatementId: aws.String(statementID),
})
if err != nil {
return err
}
return nil
}

View file

@ -56,6 +56,7 @@ func NewProvider() (*Provider, error) {
elasticbeanstalk.ApplicationVersionToken: elasticbeanstalk.NewApplicationVersionProvider(ctx),
elasticbeanstalk.EnvironmentToken: elasticbeanstalk.NewEnvironmentProvider(ctx),
lambda.FunctionToken: lambda.NewFunctionProvider(ctx),
lambda.PermissionToken: lambda.NewPermissionProvider(ctx),
iam.RoleToken: iam.NewRoleProvider(ctx),
s3.BucketToken: s3.NewBucketProvider(ctx),
s3.ObjectToken: s3.NewObjectProvider(ctx),

View file

@ -35,18 +35,25 @@ type Step []ResourceGenerator
func ProviderTest(t *testing.T, resources map[string]Resource, steps []Step) {
p := &providerTest{
resources: resources,
ids: map[string]resource.ID{},
props: map[string]*structpb.Struct{},
resources: resources,
namesInCreationOrder: []string{},
ids: map[string]resource.ID{},
props: map[string]*structpb.Struct{},
}
// For each step, create or update all listed resources
for _, step := range steps {
for _, res := range step {
provider := resources[res.Name].Provider
token := resources[res.Name].Token
currentResource, ok := resources[res.Name]
if !ok {
t.Fatalf("expected resource to have been pre-declared: %v", res.Name)
}
provider := currentResource.Provider
token := currentResource.Token
if id, ok := p.ids[res.Name]; !ok {
id, props := createResource(t, res.Creator(p), provider, token)
p.ids[res.Name] = resource.ID(id)
p.namesInCreationOrder = append(p.namesInCreationOrder, res.Name)
p.props[res.Name] = props
if id == "" {
t.Fatal("expected to succesfully create resource")
@ -61,7 +68,10 @@ func ProviderTest(t *testing.T, resources map[string]Resource, steps []Step) {
}
}
}
for name, id := range p.ids {
// Delete resources in the opposite order they were created
for i := len(p.namesInCreationOrder) - 1; i >= 0; i-- {
name := p.namesInCreationOrder[i]
id := p.ids[name]
provider := resources[name].Provider
token := resources[name].Token
ok := deleteResource(t, string(id), provider, token)
@ -97,9 +107,10 @@ func ProviderTestSimple(t *testing.T, provider lumirpc.ResourceProviderServer, t
}
type providerTest struct {
resources map[string]Resource
ids map[string]resource.ID
props map[string]*structpb.Struct
resources map[string]Resource
namesInCreationOrder []string
ids map[string]resource.ID
props map[string]*structpb.Struct
}
func (p *providerTest) GetResourceID(name string) resource.ID {

View file

@ -109,7 +109,6 @@ export class API {
let restAPI = new aws.apigateway.RestAPI(prefix, { body: body });
let deployment = new aws.apigateway.Deployment(prefix + "-deployment", {
restAPI: restAPI,
stageName: "Stage",
});
let stage = new aws.apigateway.Stage(prefix + "-primary-stage", {
deployment: deployment,