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:
parent
fd719d64cd
commit
977e56e03e
7 changed files with 247 additions and 17 deletions
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
195
lib/aws/provider/lambda/permission.go
Normal file
195
lib/aws/provider/lambda/permission.go
Normal 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
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -36,17 +36,24 @@ func ProviderTest(t *testing.T, resources map[string]Resource, steps []Step) {
|
|||
|
||||
p := &providerTest{
|
||||
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)
|
||||
|
@ -98,6 +108,7 @@ func ProviderTestSimple(t *testing.T, provider lumirpc.ResourceProviderServer, t
|
|||
|
||||
type providerTest struct {
|
||||
resources map[string]Resource
|
||||
namesInCreationOrder []string
|
||||
ids map[string]resource.ID
|
||||
props map[string]*structpb.Struct
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue