Merge pull request #199 from pulumi/apigateway

Adds support for AWS API Gateway RestApi, Deployment and Stage resources. Together, these enable publishing an HTTP endpoint from a Lumi script.

Also creates the aws.serverless sub-package for higher level serverless abstractions similar to the AWS Serverless Application Model (SAM). Currently includes higher-level Function and API abstractions.
This commit is contained in:
Luke Hoban 2017-06-04 15:06:06 -07:00 committed by GitHub
commit 5f1e8b7653
32 changed files with 1355 additions and 280 deletions

View file

@ -62,7 +62,7 @@ function createLambda() {
let obj = { x: 42 }
let mus = music
let lambda = new aws.lambda.FunctionX(
let lambda = new aws.serverless.Function(
"mylambda",
[aws.iam.AWSLambdaFullAccess],
(event, context, callback) => {
@ -73,6 +73,13 @@ function createLambda() {
callback(null, "Succeeed with " + context.getRemainingTimeInMillis() + "ms remaining.");
}
);
return lambda;
}
createLambda();
let lambda = createLambda();
let api = new aws.serverless.API("frontend")
api.route("GET", "/bambam", lambda)
api.route("PUT", "/bambam", lambda)
let stage = api.publish("prod")

View file

@ -32,6 +32,11 @@ type Deployment struct {
// stageName is a name for the stage that API Gateway creates with this deployment. Use only alphanumeric
// characters.
StageName *string `lumi:"stageName,optional"`
// The identifier for the deployment resource.
ID string `lumi:"id,out"`
// The date and time that the deployment resource was created.
CreatedDate string `lumi:"createdDate,out"`
}
type StageDescription struct {

View file

@ -41,6 +41,22 @@ type RestAPI struct {
APIName *string `lumi:"apiName,optional"`
// Custom header parameters for the request.
Parameters *[]string `lumi:"parameters,optional"`
// The API's identifier. This identifier is unique across all of your APIs in Amazon API Gateway.
ID string `lumi:"id,out"`
// The timestamp when the API was created.
CreatedDate string `lumi:"createdDate,out"`
// A version identifier for the API.
Version string `lumi:"version,out"`
// TODO[pulumi/lumi#198] Exposing array-valued output properties
// currently triggers failures serializing resource state, so
// supressing these properties.
// The warning messages reported when failonwarnings is turned on during API import.
//Warnings []string `lumi:"warnings,out"`
// The list of binary media types supported by the RestApi. By default, the RestApi supports only UTF-8-encoded
// text payloads.
//BinaryMediaTypes []string `lumi:"binaryMediaTypes,out"`
}
// S3Location is a property of the RestAPI resource that specifies the Amazon Simple Storage Service (Amazon S3)

View file

@ -43,4 +43,9 @@ type Stage struct {
// variable value is the value. Variable names are limited to alphanumeric characters. Values must match the
// following regular expression: `[A-Za-z0-9-._~:/?#&=,]+`.
Variables *map[string]string `lumi:"variables,optional"`
// The timestamp when the stage was created.
CreatedDate string `lumi:"createdDate,out"`
// The timestamp when the stage last updated.
LastUpdatedDate string `lumi:"lastUpdatedDate,out"`
}

View file

@ -13,6 +13,8 @@ export class Deployment extends lumi.Resource implements DeploymentArgs {
public description?: string;
public stageDescription?: StageDescription;
public stageName?: string;
@lumi.out public id: string;
@lumi.out public createdDate: string;
constructor(name: string, args: DeploymentArgs) {
super();

View file

@ -14,6 +14,9 @@ export class RestAPI extends lumi.Resource implements RestAPIArgs {
public failOnWarnings?: boolean;
public apiName?: string;
public parameters?: string[];
@lumi.out public id: string;
@lumi.out public createdDate: string;
@lumi.out public version: string;
constructor(name: string, args?: RestAPIArgs) {
super();

View file

@ -19,6 +19,8 @@ export class Stage extends lumi.Resource implements StageArgs {
public description?: string;
public methodSettings?: MethodSetting[];
public variables?: {[key: string]: string};
@lumi.out public createdDate: string;
@lumi.out public lastUpdatedDate: string;
constructor(name: string, args: StageArgs) {
super();

View file

@ -25,7 +25,8 @@ import * as iam from "./iam";
import * as kms from "./kms";
import * as lambda from "./lambda";
import * as s3 from "./s3";
import * as serverless from "./serverless";
import * as sns from "./sns";
import * as sqs from "./sqs";
export {apigateway, cloudwatch, config, dynamodb, ec2, elasticbeanstalk, iam, kms, lambda, s3, sns, sqs};
export {apigateway, cloudwatch, config, dynamodb, ec2, elasticbeanstalk, iam, kms, lambda, s3, serverless, sns, sqs};

View file

@ -16,5 +16,3 @@
export * from "./function";
export * from "./permission";
export * from "./functionx";

View file

@ -0,0 +1,121 @@
import { jsonStringify, sha1hash, printf } from "@lumi/lumi/runtime"
import { Deployment, RestAPI, Stage } from "../apigateway"
import { Function } from "./function"
import { region } from "../config"
export interface Route {
method: string;
path: string;
lambda: Function;
}
interface SwaggerSpec {
swagger: string;
info: SwaggerInfo;
paths: { [path: string]: { [method: string]: SwaggerOperation; }; };
}
interface SwaggerInfo {
title: string;
version: string;
}
interface SwaggerOperation {
"x-amazon-apigateway-integration": {
uri: string;
passthroughBehavior?: string;
httpMethod: string;
type: string;
}
}
interface SwaggerParameter {
name: string;
in: string;
required: boolean;
type: string;
}
interface SwaggerResponse {
statusCode: string;
}
function createBaseSpec(apiName: string): SwaggerSpec {
return {
swagger: "2.0",
info: { title: apiName, version: "1.0" },
paths: {}
}
}
function createPathSpec(lambdaARN: string): SwaggerOperation {
return {
"x-amazon-apigateway-integration": {
uri: "arn:aws:apigateway:" + region + ":lambda:path/2015-03-31/functions/" + lambdaARN + "/invocations",
passthroughBehavior: "when_no_match",
httpMethod: "POST",
type: "aws_proxy"
}
}
}
// API is a higher level abstraction for working with AWS APIGateway reources.
export class API {
public api: RestAPI
public deployment: Deployment
private swaggerSpec: SwaggerSpec
private apiName: string
constructor(apiName: string) {
this.apiName = apiName
this.swaggerSpec = createBaseSpec(apiName);
this.api = new RestAPI(apiName, {
body: this.swaggerSpec
});
}
public route(method: string, path: string, lambda: Function) {
if (this.swaggerSpec.paths[path] === undefined) {
this.swaggerSpec.paths[path] = {}
}
let swaggerMethod: string;
switch ((<any>method).toLowerCase()) {
case "get":
case "put":
case "post":
case "delete":
case "options":
case "head":
case "patch":
swaggerMethod = (<any>method).toLowerCase()
break;
case "any":
swaggerMethod = "x-amazon-apigateway-any-method"
break;
default:
throw new Error("Method not supported: " + method);
}
// 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("arn:aws:lambda:us-east-1:490047557317:function:webapi-test-func");
}
public publish(stageName?: string): Stage {
if (stageName === undefined) {
stageName = "prod"
}
let deploymentId = sha1hash(jsonStringify(this.swaggerSpec));
this.deployment = new Deployment(this.apiName + "_" + deploymentId, {
restAPI: this.api,
description: "Deployment of version " + deploymentId,
});
let stage = new Stage(this.apiName + "_" + stageName, {
stageName: stageName,
description: "The production deployment of the API.",
restAPI: this.api,
deployment: this.deployment,
});
return stage;
}
}

View file

@ -1,10 +1,10 @@
import { AssetArchive, String } from "@lumi/lumi/asset"
import { serializeClosure, jsonStringify } from "@lumi/lumi/runtime"
import { Function as LambdaFunction } from "./function"
import { Function as LambdaFunction } from "../lambda/function"
import { ARN } from "../types"
import { Role } from "../iam/role";
// Context is the shape of the context object passed to a FunctionX callback.
// Context is the shape of the context object passed to a Function callback.
export interface Context {
callbackWaitsForEmptyEventLoop: boolean;
readonly functionName: string;
@ -33,11 +33,11 @@ let policy = {
]
}
// FunctionX is a higher-level API for creating and managing AWS Lambda Function resources implemented
// Function is a higher-level API for creating and managing AWS Lambda Function resources implemented
// by a Lumi lambda expression and with a set of attached policies.
export class FunctionX {
private lambda: LambdaFunction;
private role: Role;
export class Function {
public lambda: LambdaFunction;
public role: Role;
constructor(name: string, policies: ARN[],
func: (event: any, context: Context, callback: (error: any, result: any) => void) => any) {

View file

@ -13,14 +13,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// jsonStringify converts a Lumi value into a JSON string.
export function jsonStringify(val: any): string {
// functionality provided by the runtime
return "";
}
// The aws.serverless module provides abstractions similar to those available in the Serverless Application Model.
// See: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessapi
//
// In particular, these are similar to the following AWS CloudFormation resource types:
// * AWS::Serverless::Function
// * AWS::Serverless::Api
// * AWS::Serverless::SimpleTable
// jsonParse converts a JSON string into a Lumi value.
export function jsonParse(json: string): any {
// functionality provided by the runtime
return undefined;
}
export * from "./api";
export * from "./function";

View file

@ -0,0 +1,181 @@
// Copyright 2017 Pulumi, Inc. All rights reserved.
package apigateway
import (
"crypto/sha1"
"fmt"
"strings"
"github.com/aws/aws-sdk-go/aws"
awsapigateway "github.com/aws/aws-sdk-go/service/apigateway"
"github.com/pulumi/lumi/pkg/resource"
"github.com/pulumi/lumi/pkg/util/mapper"
"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"
"github.com/pulumi/lumi/lib/aws/rpc/apigateway"
)
const DeploymentToken = apigateway.DeploymentToken
// constants for the various deployment limits.
const (
maxDeploymentName = 255
)
// NewDeploymentID returns an AWS APIGateway Deployment ARN ID for the given restAPIID and deploymentID
func NewDeploymentID(region, restAPIID, deploymentID string) resource.ID {
return arn.NewID("apigateway", region, "", "/restapis/"+restAPIID+"/deployments/"+deploymentID)
}
// ParseDeploymentID parses an AWS APIGateway Deployment ARN ID to extract the restAPIID and deploymentID
func ParseDeploymentID(id resource.ID) (string, string, error) {
res, err := arn.ParseResourceName(id)
if err != nil {
return "", "", err
}
parts := strings.Split(res, "/")
if len(parts) != 4 || parts[0] != "restapis" || parts[2] != "deployments" {
return "", "", fmt.Errorf("expected Deployment ARN of the form arn:aws:apigateway:region::/restapis/api-id/deployments/deployment-id: %v", id)
}
return parts[1], parts[3], nil
}
// NewDeploymentProvider creates a provider that handles APIGateway Deployment operations.
func NewDeploymentProvider(ctx *awsctx.Context) lumirpc.ResourceProviderServer {
ops := &deploymentProvider{ctx}
return apigateway.NewDeploymentProvider(ops)
}
type deploymentProvider struct {
ctx *awsctx.Context
}
// Check validates that the given property bag is valid for a resource of the given type.
func (p *deploymentProvider) Check(ctx context.Context, obj *apigateway.Deployment) ([]mapper.FieldError, error) {
var failures []mapper.FieldError
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 *deploymentProvider) Create(ctx context.Context, obj *apigateway.Deployment) (resource.ID, error) {
// If an explicit name is given, use it. Otherwise, auto-generate a name in part based on the resource name.
var stageName string
if obj.StageName != nil {
stageName = *obj.StageName
} else {
stageName = resource.NewUniqueHex(*obj.Name+"_", maxDeploymentName, sha1.Size)
}
restAPIID, err := ParseRestAPIID(obj.RestAPI)
if err != nil {
return "", err
}
fmt.Printf("Creating APIGateway Deployment '%v'\n", obj.Name)
create := &awsapigateway.CreateDeploymentInput{
RestApiId: aws.String(restAPIID),
Description: obj.Description,
StageName: aws.String(stageName),
}
deployment, err := p.ctx.APIGateway().CreateDeployment(create)
if err != nil {
return "", err
}
id := NewDeploymentID(p.ctx.Region(), restAPIID, *deployment.Id)
return id, nil
}
// Get reads the instance state identified by ID, returning a populated resource object, or an error if not found.
func (p *deploymentProvider) Get(ctx context.Context, id resource.ID) (*apigateway.Deployment, error) {
restAPIID, deploymentID, err := ParseDeploymentID(id)
if err != nil {
return nil, err
}
resp, err := p.ctx.APIGateway().GetDeployment(&awsapigateway.GetDeploymentInput{
RestApiId: aws.String(restAPIID),
DeploymentId: aws.String(deploymentID),
})
if err != nil {
return nil, err
}
if resp == nil || resp.Id == nil {
return nil, nil
}
return &apigateway.Deployment{
RestAPI: NewRestAPIID(p.ctx.Region(), restAPIID),
ID: aws.StringValue(resp.Id),
Description: resp.Description,
CreatedDate: resp.CreatedDate.String(),
}, nil
}
// InspectChange checks what impacts a hypothetical update will have on the resource's properties.
func (p *deploymentProvider) InspectChange(ctx context.Context, id resource.ID,
new *apigateway.Deployment, old *apigateway.Deployment, 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 *deploymentProvider) Update(ctx context.Context, id resource.ID,
old *apigateway.Deployment, new *apigateway.Deployment, diff *resource.ObjectDiff) error {
ops, err := patchOperations(diff, apigateway.Deployment_StageName)
if err != nil {
return err
}
if len(ops) > 0 {
restAPIID, deploymentID, err := ParseDeploymentID(id)
if err != nil {
return err
}
update := &awsapigateway.UpdateDeploymentInput{
RestApiId: aws.String(restAPIID),
DeploymentId: aws.String(deploymentID),
PatchOperations: ops,
}
_, err = p.ctx.APIGateway().UpdateDeployment(update)
if err != nil {
return err
}
}
return nil
}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
func (p *deploymentProvider) Delete(ctx context.Context, id resource.ID) error {
fmt.Printf("Deleting APIGateway Deployment '%v'\n", id)
restAPIID, deploymentID, err := ParseDeploymentID(id)
if err != nil {
return err
}
resp, err := p.ctx.APIGateway().GetStages(&awsapigateway.GetStagesInput{
RestApiId: aws.String(restAPIID),
DeploymentId: aws.String(deploymentID),
})
if err != nil || resp == nil {
return err
}
if len(resp.Item) == 1 {
// Assume that the single stage associated with this deployment
// is the stage that was automatically created along with the deployment.
_, err := p.ctx.APIGateway().DeleteStage(&awsapigateway.DeleteStageInput{
RestApiId: aws.String(restAPIID),
StageName: resp.Item[0].StageName,
})
if err != nil {
return err
}
}
_, err = p.ctx.APIGateway().DeleteDeployment(&awsapigateway.DeleteDeploymentInput{
RestApiId: aws.String(restAPIID),
DeploymentId: aws.String(deploymentID),
})
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,190 @@
package apigateway
import (
"fmt"
"strconv"
"github.com/aws/aws-sdk-go/aws"
"github.com/pulumi/lumi/pkg/resource"
awsapigateway "github.com/aws/aws-sdk-go/service/apigateway"
)
// patchOperations is a utility function to compute a set of PatchOperations to perform based on the diffs
// for a provided map of properties and new values. Any provided ignoreProps are skipped from being considered
// as sources of patch operations.
func patchOperations(diff *resource.ObjectDiff, ignoreProps ...resource.PropertyKey) (
[]*awsapigateway.PatchOperation, error) {
ignores := map[resource.PropertyKey]bool{}
for _, p := range ignoreProps {
ignores[p] = true
}
return patchOperationsForObject("", diff, ignores)
}
func patchOperationsForObject(path string, diff *resource.ObjectDiff, ignores map[resource.PropertyKey]bool) (
[]*awsapigateway.PatchOperation, error) {
var ops []*awsapigateway.PatchOperation
if diff == nil {
return ops, nil
}
for _, name := range diff.Keys() {
if ignores != nil && ignores[name] {
continue
}
if diff.Added(name) {
v, err := jsonStringify(diff.Adds[name])
if err != nil {
return nil, err
}
ops = append(ops, &awsapigateway.PatchOperation{
Op: aws.String("add"),
Path: aws.String(path + "/" + string(name)),
Value: v,
})
}
if diff.Updated(name) {
update := diff.Updates[name]
arrayOps, err := patchOperationsForValue(path+"/"+string(name), &update)
if err != nil {
return nil, err
}
for _, op := range arrayOps {
ops = append(ops, op)
}
}
if diff.Deleted(name) {
ops = append(ops, &awsapigateway.PatchOperation{
Op: aws.String("remove"),
Path: aws.String(path + "/" + string(name)),
})
}
}
return ops, nil
}
func patchOperationsForValue(path string, diff *resource.ValueDiff) ([]*awsapigateway.PatchOperation, error) {
var ops []*awsapigateway.PatchOperation
if diff.Array != nil {
return patchOperationsForArray(path, diff.Array)
} else if diff.Object != nil {
return patchOperationsForObject(path, diff.Object, nil)
} else {
v, err := jsonStringify(diff.New)
if err != nil {
return nil, err
}
ops = append(ops, &awsapigateway.PatchOperation{
Op: aws.String("replace"),
Path: aws.String(path),
Value: v,
})
}
return ops, nil
}
func patchOperationsForArray(path string, diff *resource.ArrayDiff) ([]*awsapigateway.PatchOperation, error) {
var ops []*awsapigateway.PatchOperation
if diff == nil {
return ops, nil
}
for i, add := range diff.Adds {
addOp, err := newOp("add", path+"/"+strconv.Itoa(i), &add)
if err != nil {
return nil, err
}
ops = append(ops, addOp)
}
for i, update := range diff.Updates {
arrayOps, err := patchOperationsForValue(path+"/"+strconv.Itoa(i), &update)
if err != nil {
return nil, err
}
for _, op := range arrayOps {
ops = append(ops, op)
}
}
for i := range diff.Deletes {
deleteOp, err := newOp("delete", path+"/"+strconv.Itoa(i), nil)
if err != nil {
return nil, err
}
ops = append(ops, deleteOp)
}
return ops, nil
}
func newOp(op string, path string, i *resource.PropertyValue) (*awsapigateway.PatchOperation, error) {
patchOp := awsapigateway.PatchOperation{
Op: aws.String(op),
Path: aws.String(path),
}
if i != nil {
v, err := jsonStringify(*i)
if err != nil {
return nil, err
}
patchOp.Value = v
}
return &patchOp, nil
}
func jsonStringify(i resource.PropertyValue) (*string, error) {
var s string
switch v := i.V.(type) {
case nil:
return nil, nil
case bool:
if v {
s = "true"
} else {
s = "false"
}
case float64:
s = strconv.Itoa(int(v))
case string:
s = v
case []resource.PropertyValue:
s = "["
isFirst := true
for _, pv := range v {
pvj, err := jsonStringify(pv)
if pvj == nil {
tmp := "null"
pvj = &tmp
}
if err != nil {
return nil, err
}
if !isFirst {
s += ", "
}
s += *pvj
}
s += "]"
case resource.PropertyMap:
s = "{"
isFirst := true
for pk, pv := range v {
pvj, err := jsonStringify(pv)
if pvj != nil {
if err != nil {
return nil, err
}
if !isFirst {
s += ", "
}
s += "\"" + string(pk) + "\": " + *pvj
}
}
s += "}"
case resource.URN:
s = string(v)
default:
return nil, fmt.Errorf("unexpected diff type %v", v)
}
return &s, nil
}

View file

@ -0,0 +1,84 @@
package apigateway
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/apigateway"
"github.com/pulumi/lumi/pkg/resource"
"github.com/stretchr/testify/assert"
)
type TestStruct struct {
Number float64 `json:"number"`
OptionalString *string `json:"optionalString,omitempty"`
OptionalNumber *float64 `json:"optionalNumber,omitempty"`
OptionalBool *bool `json:"optionalBool,omitempty"`
OptionalArray *[]TestStruct `json:"optionalArray,omitempty"`
OptionalObject *TestStruct `json:"optionalObject,omitempty"`
}
func Test(t *testing.T) {
before := TestStruct{
Number: 1,
OptionalString: aws.String("hello"),
OptionalNumber: nil,
OptionalBool: aws.Bool(true),
OptionalArray: &[]TestStruct{
{Number: 1},
},
}
after := TestStruct{
Number: 1,
OptionalString: aws.String("goodbye"),
OptionalNumber: aws.Float64(3),
OptionalArray: &[]TestStruct{
{
Number: 3,
OptionalBool: aws.Bool(true),
},
{
Number: 1,
},
},
}
expectedPatchOps := []*apigateway.PatchOperation{
&apigateway.PatchOperation{
Op: aws.String("add"),
Path: aws.String("/optionalArray/1"),
Value: aws.String("{\"number\": 1}"),
},
&apigateway.PatchOperation{
Op: aws.String("replace"),
Path: aws.String("/optionalArray/0/number"),
Value: aws.String("3"),
},
&apigateway.PatchOperation{
Op: aws.String("add"),
Path: aws.String("/optionalArray/0/optionalBool"),
Value: aws.String("true"),
},
&apigateway.PatchOperation{
Op: aws.String("remove"),
Path: aws.String("/optionalBool"),
},
&apigateway.PatchOperation{
Op: aws.String("add"),
Path: aws.String("/optionalNumber"),
Value: aws.String("3"),
},
&apigateway.PatchOperation{
Op: aws.String("replace"),
Path: aws.String("/optionalString"),
Value: aws.String("goodbye"),
},
}
beforeProps := resource.NewPropertyMap(before)
afterProps := resource.NewPropertyMap(after)
diff := beforeProps.Diff(afterProps)
assert.NotEqual(t, nil, diff, "expected diff should not be nil")
patchOps, err := patchOperations(diff)
assert.Nil(t, err, "expected no error generating patch operations")
assert.EqualValues(t, expectedPatchOps, patchOps)
}

View file

@ -0,0 +1,206 @@
// Copyright 2017 Pulumi, Inc. All rights reserved.
package apigateway
import (
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/aws/aws-sdk-go/aws"
awsapigateway "github.com/aws/aws-sdk-go/service/apigateway"
"github.com/pulumi/lumi/pkg/resource"
"github.com/pulumi/lumi/pkg/util/mapper"
"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"
"github.com/pulumi/lumi/lib/aws/rpc/apigateway"
)
const RestAPIToken = apigateway.RestAPIToken
// constants for the various restAPI limits.
const (
maxRestAPIName = 255
)
// NewRestAPIID returns an AWS APIGateway RestAPI ARN ID for the given restAPIID
func NewRestAPIID(region, restAPIID string) resource.ID {
return arn.NewID("apigateway", region, "", "/restapis/"+restAPIID)
}
// ParseRestAPIID parses an AWS APIGateway RestAPI ARN ID to extract the restAPIID
func ParseRestAPIID(id resource.ID) (string, error) {
res, err := arn.ParseResourceName(id)
if err != nil {
return "", err
}
parts := strings.Split(res, "/")
if len(parts) != 2 || parts[0] != "restapis" {
return "", fmt.Errorf("expected RestAPI ARN of the form arn:aws:apigateway:region::/restapis/api-id: %v", id)
}
return parts[1], nil
}
// NewRestAPIProvider creates a provider that handles APIGateway RestAPI operations.
func NewRestAPIProvider(ctx *awsctx.Context) lumirpc.ResourceProviderServer {
ops := &restAPIProvider{ctx}
return apigateway.NewRestAPIProvider(ops)
}
type restAPIProvider struct {
ctx *awsctx.Context
}
// Check validates that the given property bag is valid for a resource of the given type.
func (p *restAPIProvider) Check(ctx context.Context, obj *apigateway.RestAPI) ([]mapper.FieldError, error) {
var failures []mapper.FieldError
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 *restAPIProvider) Create(ctx context.Context, obj *apigateway.RestAPI) (resource.ID, error) {
// If an explicit name is given, use it. Otherwise, auto-generate a name in part based on the resource name.
var apiName string
if obj.APIName != nil {
apiName = *obj.APIName
} else {
apiName = resource.NewUniqueHex(*obj.Name+"-", maxRestAPIName, sha1.Size)
}
// First create the API Gateway
fmt.Printf("Creating APIGateway RestAPI '%v' with name '%v'\n", obj.Name, apiName)
create := &awsapigateway.CreateRestApiInput{
Name: aws.String(apiName),
Description: obj.Description,
CloneFrom: obj.CloneFrom.StringPtr(),
}
restAPI, err := p.ctx.APIGateway().CreateRestApi(create)
if err != nil {
return "", err
}
// Next, if a body is specified, put the rest api contents
if obj.Body != nil {
body := *obj.Body
bodyJSON, _ := json.Marshal(body)
fmt.Printf("APIGateway RestAPI created: %v; putting API contents from OpenAPI specification\n", restAPI.Id)
put := &awsapigateway.PutRestApiInput{
RestApiId: restAPI.Id,
Body: bodyJSON,
Mode: aws.String("overwrite"),
}
_, err := p.ctx.APIGateway().PutRestApi(put)
if err != nil {
return "", err
}
}
return NewRestAPIID(p.ctx.Region(), *restAPI.Id), nil
}
// Get reads the instance state identified by ID, returning a populated resource object, or an error if not found.
func (p *restAPIProvider) Get(ctx context.Context, id resource.ID) (*apigateway.RestAPI, error) {
restAPIID, err := ParseRestAPIID(id)
if err != nil {
return nil, err
}
resp, err := p.ctx.APIGateway().GetRestApi(&awsapigateway.GetRestApiInput{
RestApiId: aws.String(restAPIID),
})
if err != nil {
return nil, err
}
if resp == nil || resp.Id == nil {
return nil, nil
}
return &apigateway.RestAPI{
ID: aws.StringValue(resp.Id),
APIName: resp.Name,
Description: resp.Description,
CreatedDate: resp.CreatedDate.String(),
Version: aws.StringValue(resp.Version),
// TODO[pulumi/lumi#198] Exposing array-valued output properties
// currently triggers failures serializing resource state, so
// supressing these properties.
// Warnings: aws.StringValueSlice(resp.Warnings),
// BinaryMediaTypes: aws.StringValueSlice(resp.BinaryMediaTypes),
}, nil
}
// InspectChange checks what impacts a hypothetical update will have on the resource's properties.
func (p *restAPIProvider) InspectChange(ctx context.Context, id resource.ID,
new *apigateway.RestAPI, old *apigateway.RestAPI, 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 *restAPIProvider) Update(ctx context.Context, id resource.ID,
old *apigateway.RestAPI, new *apigateway.RestAPI, diff *resource.ObjectDiff) error {
restAPIID, err := ParseRestAPIID(id)
if err != nil {
return err
}
if diff.Updated(apigateway.RestAPI_Body) {
if new.Body != nil {
body := *new.Body
bodyJSON, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("Could not convert Swagger defintion object to JSON: %v", err)
}
fmt.Printf("Updating API definition for %v from OpenAPI specification\n", id)
put := &awsapigateway.PutRestApiInput{
RestApiId: aws.String(restAPIID),
Body: bodyJSON,
Mode: aws.String("overwrite"),
}
newAPI, err := p.ctx.APIGateway().PutRestApi(put)
if err != nil {
return err
}
fmt.Printf("Updated to: %v\n", newAPI)
} else {
return errors.New("Cannot remove Body from Rest API which previously had one")
}
}
ops, err := patchOperations(diff, apigateway.RestAPI_Body)
if err != nil {
return err
}
if len(ops) > 0 {
update := &awsapigateway.UpdateRestApiInput{
RestApiId: aws.String(restAPIID),
PatchOperations: ops,
}
_, err := p.ctx.APIGateway().UpdateRestApi(update)
if err != nil {
return err
}
}
return nil
}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
func (p *restAPIProvider) Delete(ctx context.Context, id resource.ID) error {
restAPIID, err := ParseRestAPIID(id)
if err != nil {
return err
}
fmt.Printf("Deleting APIGateway RestAPI '%v'\n", id)
_, err = p.ctx.APIGateway().DeleteRestApi(&awsapigateway.DeleteRestApiInput{
RestApiId: aws.String(restAPIID),
})
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,185 @@
// Copyright 2017 Pulumi, Inc. All rights reserved.
package apigateway
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
awsapigateway "github.com/aws/aws-sdk-go/service/apigateway"
"github.com/pulumi/lumi/pkg/resource"
"github.com/pulumi/lumi/pkg/util/mapper"
"github.com/pulumi/lumi/sdk/go/pkg/lumirpc"
"golang.org/x/net/context"
"strings"
"github.com/pulumi/lumi/lib/aws/provider/arn"
"github.com/pulumi/lumi/lib/aws/provider/awsctx"
"github.com/pulumi/lumi/lib/aws/rpc/apigateway"
)
const StageToken = apigateway.StageToken
// constants for the various stage limits.
const (
maxStageName = 255
)
// NewStageID returns an AWS APIGateway Stage ARN ID for the given restAPIID and stageID
func NewStageID(region, restAPIID, stageID string) resource.ID {
return arn.NewID("apigateway", region, "", "/restapis/"+restAPIID+"/stages/"+stageID)
}
// ParseStageID parses an AWS APIGateway Stage ARN ID to extract the restAPIID and stageID
func ParseStageID(id resource.ID) (string, string, error) {
res, err := arn.ParseResourceName(id)
if err != nil {
return "", "", err
}
parts := strings.Split(res, "/")
if len(parts) != 4 || parts[0] != "restapis" || parts[2] != "stages" {
return "", "", fmt.Errorf("expected Stage ARN of the form arn:aws:apigateway:region::/restapis/api-id/stages/stage-id: %v", id)
}
return parts[1], parts[3], nil
}
// NewStageProvider creates a provider that handles APIGateway Stage operations.
func NewStageProvider(ctx *awsctx.Context) lumirpc.ResourceProviderServer {
ops := &stageProvider{ctx}
return apigateway.NewStageProvider(ops)
}
type stageProvider struct {
ctx *awsctx.Context
}
// Check validates that the given property bag is valid for a resource of the given type.
func (p *stageProvider) Check(ctx context.Context, obj *apigateway.Stage) ([]mapper.FieldError, error) {
var failures []mapper.FieldError
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 *stageProvider) Create(ctx context.Context, obj *apigateway.Stage) (resource.ID, error) {
if obj.MethodSettings != nil || obj.ClientCertificate != nil {
return "", fmt.Errorf("Not yet supported - MethodSettings or ClientCertificate")
}
fmt.Printf("Creating APIGateway Stage '%v' with stage name '%v'\n", obj.Name, obj.StageName)
restAPIID, deploymentID, err := ParseDeploymentID(obj.Deployment)
if err != nil {
return "", err
}
create := &awsapigateway.CreateStageInput{
StageName: aws.String(obj.StageName),
RestApiId: aws.String(restAPIID),
DeploymentId: aws.String(deploymentID),
Description: obj.Description,
CacheClusterEnabled: obj.CacheClusterEnabled,
CacheClusterSize: obj.CacheClusterSize,
}
if obj.Variables != nil {
create.Variables = aws.StringMap(*obj.Variables)
}
stage, err := p.ctx.APIGateway().CreateStage(create)
if err != nil {
return "", err
}
return NewStageID(p.ctx.Region(), restAPIID, *stage.StageName), nil
}
// Get reads the instance state identified by ID, returning a populated resource object, or an error if not found.
func (p *stageProvider) Get(ctx context.Context, id resource.ID) (*apigateway.Stage, error) {
restAPIID, stageName, err := ParseStageID(id)
if err != nil {
return nil, err
}
resp, err := p.ctx.APIGateway().GetStage(&awsapigateway.GetStageInput{
RestApiId: aws.String(restAPIID),
StageName: aws.String(stageName),
})
if err != nil {
return nil, err
}
if resp == nil || resp.DeploymentId == nil {
return nil, nil
}
variables := aws.StringValueMap(resp.Variables)
return &apigateway.Stage{
RestAPI: NewRestAPIID(p.ctx.Region(), restAPIID),
Deployment: NewDeploymentID(p.ctx.Region(), restAPIID, aws.StringValue(resp.DeploymentId)),
CacheClusterEnabled: resp.CacheClusterEnabled,
CacheClusterSize: resp.CacheClusterSize,
StageName: aws.StringValue(resp.StageName),
Variables: &variables,
Description: resp.Description,
CreatedDate: resp.CreatedDate.String(),
LastUpdatedDate: resp.LastUpdatedDate.String(),
}, nil
}
// InspectChange checks what impacts a hypothetical update will have on the resource's properties.
func (p *stageProvider) InspectChange(ctx context.Context, id resource.ID,
new *apigateway.Stage, old *apigateway.Stage, 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 *stageProvider) Update(ctx context.Context, id resource.ID,
old *apigateway.Stage, new *apigateway.Stage, diff *resource.ObjectDiff) error {
ops, err := patchOperations(diff, apigateway.Stage_Deployment)
if err != nil {
return err
}
if diff.Updated(apigateway.Stage_Deployment) {
_, deploymentID, err := ParseDeploymentID(new.Deployment)
if err != nil {
return err
}
ops = append(ops, &awsapigateway.PatchOperation{
Op: aws.String("replace"),
Path: aws.String("/deploymentId"),
Value: aws.String(deploymentID),
})
}
restAPIId, stageName, err := ParseStageID(id)
if err != nil {
return err
}
if len(ops) > 0 {
update := &awsapigateway.UpdateStageInput{
StageName: aws.String(stageName),
RestApiId: aws.String(restAPIId),
PatchOperations: ops,
}
_, err := p.ctx.APIGateway().UpdateStage(update)
if err != nil {
return err
}
}
return nil
}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
func (p *stageProvider) Delete(ctx context.Context, id resource.ID) error {
fmt.Printf("Deleting APIGateway Stage '%v'\n", id)
restAPIID, stageName, err := ParseStageID(id)
if err != nil {
return err
}
_, err = p.ctx.APIGateway().DeleteStage(&awsapigateway.DeleteStageInput{
RestApiId: aws.String(restAPIID),
StageName: aws.String(stageName),
})
if err != nil {
return err
}
return nil
}

View file

@ -19,6 +19,7 @@ import (
"context"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/apigateway"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/elasticbeanstalk"
@ -34,11 +35,13 @@ import (
// Context represents state shared amongst all parties in this process. In particular, it wraps an AWS session
// object and offers convenient wrappers for creating connections to the various sub-services (EC2, S3, etc).
type Context struct {
sess *session.Session // a global session object, shared amongst all service connections.
accountID string // the currently authenticated account's ID.
accountRole string // the currently authenticated account's IAM role.
sess *session.Session // a global session object, shared amongst all service connections.
accountID string // the currently authenticated account's ID.
accountRole string // the currently authenticated account's IAM role.
// per-service connections (lazily allocated and reused);
apigateway *apigateway.APIGateway
dynamodb *dynamodb.DynamoDB
ec2 *ec2.EC2
elasticbeanstalk *elasticbeanstalk.ElasticBeanstalk
@ -89,6 +92,14 @@ func New() (*Context, error) {
func (ctx *Context) AccountID() string { return ctx.accountID }
func (ctx *Context) Region() string { return *ctx.sess.Config.Region }
func (ctx *Context) APIGateway() *apigateway.APIGateway {
contract.Assert(ctx.sess != nil)
if ctx.apigateway == nil {
ctx.apigateway = apigateway.New(ctx.sess)
}
return ctx.apigateway
}
func (ctx *Context) DynamoDB() *dynamodb.DynamoDB {
contract.Assert(ctx.sess != nil)
if ctx.dynamodb == nil {

View file

@ -23,6 +23,7 @@ import (
"github.com/pulumi/lumi/sdk/go/pkg/lumirpc"
"golang.org/x/net/context"
"github.com/pulumi/lumi/lib/aws/provider/apigateway"
"github.com/pulumi/lumi/lib/aws/provider/awsctx"
"github.com/pulumi/lumi/lib/aws/provider/dynamodb"
"github.com/pulumi/lumi/lib/aws/provider/ec2"
@ -45,6 +46,9 @@ func NewProvider() (*Provider, error) {
}
return &Provider{
impls: map[tokens.Type]lumirpc.ResourceProviderServer{
apigateway.DeploymentToken: apigateway.NewDeploymentProvider(ctx),
apigateway.RestAPIToken: apigateway.NewRestAPIProvider(ctx),
apigateway.StageToken: apigateway.NewStageProvider(ctx),
dynamodb.TableToken: dynamodb.NewTableProvider(ctx),
ec2.InstanceToken: ec2.NewInstanceProvider(ctx),
ec2.SecurityGroupToken: ec2.NewSecurityGroupProvider(ctx),

View file

@ -179,6 +179,8 @@ type Deployment struct {
Description *string `json:"description,omitempty"`
StageDescription *StageDescription `json:"stageDescription,omitempty"`
StageName *string `json:"stageName,omitempty"`
ID string `json:"id,omitempty"`
CreatedDate string `json:"createdDate,omitempty"`
}
// Deployment's properties have constants to make dealing with diffs and property bags easier.
@ -188,6 +190,8 @@ const (
Deployment_Description = "description"
Deployment_StageDescription = "stageDescription"
Deployment_StageName = "stageName"
Deployment_ID = "id"
Deployment_CreatedDate = "createdDate"
)
/* Marshalable StageDescription structure(s) */

View file

@ -182,6 +182,9 @@ type RestAPI struct {
FailOnWarnings *bool `json:"failOnWarnings,omitempty"`
APIName *string `json:"apiName,omitempty"`
Parameters *[]string `json:"parameters,omitempty"`
ID string `json:"id,omitempty"`
CreatedDate string `json:"createdDate,omitempty"`
Version string `json:"version,omitempty"`
}
// RestAPI's properties have constants to make dealing with diffs and property bags easier.
@ -194,6 +197,9 @@ const (
RestAPI_FailOnWarnings = "failOnWarnings"
RestAPI_APIName = "apiName"
RestAPI_Parameters = "parameters"
RestAPI_ID = "id"
RestAPI_CreatedDate = "createdDate"
RestAPI_Version = "version"
)
/* Marshalable S3Location structure(s) */

View file

@ -190,6 +190,8 @@ type Stage struct {
Description *string `json:"description,omitempty"`
MethodSettings *[]MethodSetting `json:"methodSettings,omitempty"`
Variables *map[string]string `json:"variables,omitempty"`
CreatedDate string `json:"createdDate,omitempty"`
LastUpdatedDate string `json:"lastUpdatedDate,omitempty"`
}
// Stage's properties have constants to make dealing with diffs and property bags easier.
@ -204,6 +206,8 @@ const (
Stage_Description = "description"
Stage_MethodSettings = "methodSettings"
Stage_Variables = "variables"
Stage_CreatedDate = "createdDate"
Stage_LastUpdatedDate = "lastUpdatedDate"
)

View file

@ -1,30 +0,0 @@
// 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.
// serializeClosure serializes a function and its closure environment into a form that is amenable to persistence
// as simple JSON. Like toString, it includes the full text of the function's source code, suitable for execution.
export function serializeClosure(func: any): Closure | undefined {
// functionality provided by the runtime
return undefined;
}
// Closure represents the serialized form of a Lumi function.
export interface Closure {
code: string; // a serialization of the function's source code as text.
language: string; // the language runtime required to execute the serialized code.
signature: string; // the function signature type token.
environment?: {[key: string]: any}; // the captured lexical environment of variables to values, if any.
}

View file

@ -1,30 +0,0 @@
// 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.
// isFunction checks whether the given object is a function (and hence invocable).
export function isFunction(obj: Object): boolean {
return false; // functionality provided by the runtime.
}
// dynamicInvoke dynamically calls the target function. If the target is not a function, an error is thrown.
export function dynamicInvoke(obj: Object, thisArg: Object, args: Object[]): Object {
return <any>undefined; // functionality provided by the runtime.
}
// log prints the provided message to standard out.
export function printf(message: any): void {
// functionality provided by the runtime.
}

View file

@ -13,7 +13,51 @@
// See the License for the specific language governing permissions and
// limitations under the License.
export * from "./closure";
export * from "./dynamic";
export * from "./json";
// printf prints the provided message to standard out.
export function printf(message: any): void {
// functionality provided by the runtime.
}
// sha1hash generates the SHA-1 hash of the provided string.
export function sha1hash(str: string): string {
// functionality provided by the runtime.
return "";
}
// isFunction checks whether the given object is a function (and hence invocable).
export function isFunction(obj: Object): boolean {
return false; // functionality provided by the runtime.
}
// dynamicInvoke dynamically calls the target function. If the target is not a function, an error is thrown.
export function dynamicInvoke(obj: Object, thisArg: Object, args: Object[]): Object {
return <any>undefined; // functionality provided by the runtime.
}
// jsonStringify converts a Lumi value into a JSON string.
export function jsonStringify(val: any): string {
// functionality provided by the runtime
return "";
}
// jsonParse converts a JSON string into a Lumi value.
export function jsonParse(json: string): any {
// functionality provided by the runtime
return undefined;
}
// serializeClosure serializes a function and its closure environment into a form that is amenable to persistence
// as simple JSON. Like toString, it includes the full text of the function's source code, suitable for execution.
export function serializeClosure(func: any): Closure | undefined {
// functionality provided by the runtime
return undefined;
}
// Closure represents the serialized form of a Lumi function.
export interface Closure {
code: string; // a serialization of the function's source code as text.
language: string; // the language runtime required to execute the serialized code.
signature: string; // the function signature type token.
environment?: {[key: string]: any}; // the captured lexical environment of variables to values, if any.
}

View file

@ -26,8 +26,7 @@
"asset/archive.ts",
"asset/decors.ts",
"runtime/index.ts",
"runtime/dynamic.ts"
"runtime/index.ts"
]
}

View file

@ -1829,7 +1829,6 @@ func (e *evaluator) evalInvokeFunctionExpression(node *ast.InvokeFunctionExpress
switch t := fncobj.Type().(type) {
case *symbols.FunctionType:
fnc = fncobj.FunctionValue()
contract.Assert(fnc.Func != nil)
case *symbols.PrototypeType:
contract.Assertf(dynamic, "Prototype invocation is only valid for dynamic invokes")
// For dynamic invokes, we permit invocation of class prototypes (a "new").

View file

@ -33,17 +33,20 @@ var Intrinsics map[tokens.Token]Invoker
func init() {
Intrinsics = map[tokens.Token]Invoker{
// These intrinsics are exposed directly to users in the `lumi.runtime` package.
"lumi:runtime/dynamic:isFunction": isFunction,
"lumi:runtime/dynamic:dynamicInvoke": dynamicInvoke,
"lumi:runtime/dynamic:printf": printf,
"lumi:runtime/json:jsonStringify": jsonStringify,
"lumi:runtime/json:jsonParse": jsonParse,
"lumi:runtime/closure:serializeClosure": serializeClosure,
"lumi:runtime/index:isFunction": isFunction,
"lumi:runtime/index:dynamicInvoke": dynamicInvoke,
"lumi:runtime/index:printf": printf,
"lumi:runtime/index:sha1hash": sha1hash,
"lumi:runtime/index:jsonStringify": jsonStringify,
"lumi:runtime/index:jsonParse": jsonParse,
"lumi:runtime/index:serializeClosure": serializeClosure,
// These intrinsics are built-ins with no Lumi function exposed to users.
// They are used as the implementation of core object APIs in the runtime.
"lumi:builtin/array:getLength": arrayGetLength,
"lumi:builtin/array:setLength": arraySetLength,
"lumi:builtin/array:getLength": arrayGetLength,
"lumi:builtin/array:setLength": arraySetLength,
"lumi:builtin/string:getLength": stringGetLength,
"lumi:builtin/string:toLowerCase": stringToLowerCase,
}
}

View file

@ -16,8 +16,12 @@
package eval
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"sort"
"strconv"
"strings"
"github.com/pulumi/lumi/pkg/compiler/ast"
"github.com/pulumi/lumi/pkg/compiler/symbols"
@ -70,6 +74,68 @@ func printf(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Obje
return rt.NewReturnUnwind(nil)
}
func sha1hash(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
var str *rt.Object
if len(args) >= 1 {
str = args[0]
} else {
return e.NewException(intrin.Tree(), "Expected a single argument string.")
}
if !str.IsString() {
return e.NewException(intrin.Tree(), "Expected a single argument string.")
}
hasher := sha1.New()
byts := []byte(str.StringValue())
hasher.Write(byts)
sum := hasher.Sum(nil)
hash := hex.EncodeToString(sum)
hashObj := e.alloc.NewString(intrin.Tree(), hash)
return rt.NewReturnUnwind(hashObj)
}
func serializeClosure(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
contract.Assert(this == nil) // module function
contract.Assert(len(args) == 1) // one arg: func
stub, ok := args[0].TryFunctionValue()
if !ok {
return e.NewException(intrin.Tree(), "Expected argument 'func' to be a function value.")
}
lambda, ok := stub.Func.(*ast.LambdaExpression)
if !ok {
return e.NewException(intrin.Tree(), "Expected argument 'func' to be a lambda expression.")
}
// TODO[pulumi/lumi#177]: We are using the full environment available at execution time here, we should
// instead capture only the free variables referenced in the function itself.
// Insert environment variables into a PropertyMap with stable ordering
envPropMap := rt.NewPropertyMap()
slots := stub.Env.Slots()
var keys []*symbols.LocalVariable
for key := range slots {
keys = append(keys, key)
}
sort.SliceStable(keys, func(i, j int) bool {
return keys[i].Name() < keys[j].Name()
})
for _, key := range keys {
envPropMap.Set(rt.PropertyKey(key.Name()), slots[key].Obj())
}
envObj := e.alloc.New(intrin.Tree(), types.Dynamic, envPropMap, nil)
// Build up the properties for the returned Closure object
props := rt.NewPropertyMap()
props.Set("code", rt.NewStringObject(lambda.SourceText))
props.Set("signature", rt.NewStringObject(string(stub.Sig.Token())))
props.Set("language", rt.NewStringObject(lambda.SourceLanguage))
props.Set("environment", envObj)
closure := e.alloc.New(intrin.Tree(), intrin.Signature().Return, props, nil)
return rt.NewReturnUnwind(closure)
}
func arrayGetLength(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
if this == nil {
return e.NewException(intrin.Tree(), "Expected receiver to be non-null")
@ -114,44 +180,142 @@ func arraySetLength(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []
return rt.NewReturnUnwind(nil)
}
func serializeClosure(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
contract.Assert(this == nil) // module function
contract.Assert(len(args) == 1) // one arg: func
stub, ok := args[0].TryFunctionValue()
if !ok {
return e.NewException(intrin.Tree(), "Expected argument 'func' to be a function value.")
func stringGetLength(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
if this == nil {
return e.NewException(intrin.Tree(), "Expected receiver to be non-null")
}
lambda, ok := stub.Func.(*ast.LambdaExpression)
if !ok {
return e.NewException(intrin.Tree(), "Expected argument 'func' to be a lambda expression.")
if !this.IsString() {
return e.NewException(intrin.Tree(), "Expected receiver to be an string value")
}
str := this.StringValue()
// TODO[pulumi/lumi#177]: We are using the full environment available at execution time here, we should
// instead capture only the free variables referenced in the function itself.
// Insert environment variables into a PropertyMap with stable ordering
envPropMap := rt.NewPropertyMap()
slots := stub.Env.Slots()
var keys []*symbols.LocalVariable
for key := range slots {
keys = append(keys, key)
}
sort.SliceStable(keys, func(i, j int) bool {
return keys[i].Name() < keys[j].Name()
})
for _, key := range keys {
envPropMap.Set(rt.PropertyKey(key.Name()), slots[key].Obj())
}
envObj := e.alloc.New(intrin.Tree(), types.Dynamic, envPropMap, nil)
// Build up the properties for the returned Closure object
props := rt.NewPropertyMap()
props.Set("code", rt.NewStringObject(lambda.SourceText))
props.Set("signature", rt.NewStringObject(string(stub.Sig.Token())))
props.Set("language", rt.NewStringObject(lambda.SourceLanguage))
props.Set("environment", envObj)
closure := e.alloc.New(intrin.Tree(), intrin.Signature().Return, props, nil)
return rt.NewReturnUnwind(closure)
return rt.NewReturnUnwind(e.alloc.NewNumber(intrin.Tree(), float64(len(str))))
}
func stringToLowerCase(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
if this == nil {
return e.NewException(intrin.Tree(), "Expected receiver to be non-null")
}
if !this.IsString() {
return e.NewException(intrin.Tree(), "Expected receiver to be a string value")
}
str := this.StringValue()
out := strings.ToLower(str)
return rt.NewReturnUnwind(e.alloc.NewString(intrin.Tree(), out))
}
type jsonSerializer struct {
stack map[*rt.Object]bool
intrin *rt.Intrinsic
e *evaluator
}
func (s jsonSerializer) serializeJSONProperty(o *rt.Object) (string, *rt.Unwind) {
if o == nil {
return "null", nil
}
if o.IsNull() {
return "null", nil
} else if o.IsBool() {
if o.BoolValue() {
return "true", nil
}
return "false", nil
} else if o.IsString() {
return o.String(), nil
} else if o.IsNumber() {
return o.String(), nil
} else if o.IsArray() {
return s.serializeJSONArray(o)
}
return s.serializeJSONObject(o)
}
func (s jsonSerializer) serializeJSONObject(o *rt.Object) (string, *rt.Unwind) {
if _, found := s.stack[o]; found {
return "", s.e.NewException(s.intrin.Tree(), "Cannot JSON serialize an object with cyclic references")
}
s.stack[o] = true
ownProperties := o.Properties().Stable()
isFirst := true
final := "{"
for _, prop := range ownProperties {
propValuePointer := o.GetPropertyAddr(prop, false, false)
propValue := propValuePointer.Obj() // TODO: What about getters?
if propValue == nil {
continue
}
if isFirst {
final += " "
} else {
final += ", "
}
isFirst = false
strP, uw := s.serializeJSONProperty(propValue)
if uw != nil {
return "", uw
}
final += strconv.Quote(string(prop)) + ": " + strP
}
final += "}"
delete(s.stack, o)
return final, nil
}
func (s jsonSerializer) serializeJSONArray(o *rt.Object) (string, *rt.Unwind) {
contract.Assert(o.IsArray()) // expect to be called on an Array
if _, found := s.stack[o]; found {
return "", s.e.NewException(s.intrin.Tree(), "Cannot JSON serialize an object with cyclic references")
}
s.stack[o] = true
arr := o.ArrayValue()
contract.Assert(arr != nil)
isFirst := true
final := "["
for index := 0; index < len(*arr); index++ {
propValuePointer := (*arr)[index]
propValue := propValuePointer.Obj() // TODO: What about getters?
if isFirst {
final += " "
} else {
final += ", "
}
isFirst = false
strP, uw := s.serializeJSONProperty(propValue)
if uw != nil {
return "", uw
}
final += strP
}
final += "]"
delete(s.stack, o)
return final, nil
}
// jsonStringify provides JSON serialization of a Lumi object. This implementation follows a subset of
// https://tc39.github.io/ecma262/2017/#sec-json.stringify without `replacer` and `space` arguments.
func jsonStringify(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
contract.Assert(len(args) == 1) // just one arg: the object to stringify
obj := args[0]
if obj == nil {
return rt.NewReturnUnwind(e.alloc.NewString(intrin.Tree(), "{}"))
}
s := jsonSerializer{
map[*rt.Object]bool{},
intrin,
e,
}
str, uw := s.serializeJSONProperty(obj)
if uw != nil {
return uw
}
return rt.NewReturnUnwind(e.alloc.NewString(intrin.Tree(), str))
}
func jsonParse(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
return e.NewException(intrin.Tree(), "Not yet implemented - jsonParse")
}

View file

@ -44,7 +44,7 @@ func newTestEval() (binder.Binder, Interpreter) {
return b, New(b.Ctx(), nil)
}
var isFunctionIntrin = tokens.ModuleMember("lumi:runtime/dynamic:isFunction")
var isFunctionIntrin = tokens.ModuleMember("lumi:runtime/index:isFunction")
func makeIsFunctionExprAST(dynamic bool) ast.Expression {
if dynamic {
@ -140,10 +140,10 @@ func makeTestIsFunctionAST(dynamic bool, realFunc bool) *pack.Package {
},
},
},
tokens.ModuleName("runtime/dynamic"): &ast.Module{
tokens.ModuleName("runtime/index"): &ast.Module{
DefinitionNode: ast.DefinitionNode{
Name: &ast.Identifier{
Ident: tokens.Name("runtime/dynamic"),
Ident: tokens.Name("runtime/index"),
},
},
Exports: &ast.ModuleExports{
@ -154,7 +154,7 @@ func makeTestIsFunctionAST(dynamic bool, realFunc bool) *pack.Package {
},
},
Referent: &ast.Token{
Tok: tokens.Token("lumi:runtime/dynamic:isFunction"),
Tok: tokens.Token("lumi:runtime/index:isFunction"),
},
},
},
@ -204,7 +204,7 @@ func makeTestIsFunctionAST(dynamic bool, realFunc bool) *pack.Package {
}
}
// TestIsFunction verifies the `lumi:runtime/dynamic:isFunction` intrinsic.
// TestIsFunction verifies the `lumi:runtime/index:isFunction` intrinsic.
func TestIsFunction(t *testing.T) {
t.Parallel()

View file

@ -1,138 +0,0 @@
// 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 eval
import (
"strconv"
"github.com/pulumi/lumi/pkg/eval/rt"
"github.com/pulumi/lumi/pkg/util/contract"
)
type jsonSerializer struct {
stack map[*rt.Object]bool
intrin *rt.Intrinsic
e *evaluator
}
func (s jsonSerializer) serializeJSONProperty(o *rt.Object) (string, *rt.Unwind) {
if o == nil {
return "null", nil
}
if o.IsNull() {
return "null", nil
} else if o.IsBool() {
if o.BoolValue() {
return "true", nil
}
return "false", nil
} else if o.IsString() {
return o.String(), nil
} else if o.IsNumber() {
return o.String(), nil
} else if o.IsArray() {
return s.serializeJSONArray(o)
}
return s.serializeJSONObject(o)
}
func (s jsonSerializer) serializeJSONObject(o *rt.Object) (string, *rt.Unwind) {
if _, found := s.stack[o]; found {
return "", s.e.NewException(s.intrin.Tree(), "Cannot JSON serialize an object with cyclic references")
}
s.stack[o] = true
ownProperties := o.Properties().Stable()
isFirst := true
final := "{"
for _, prop := range ownProperties {
propValuePointer := o.GetPropertyAddr(prop, false, false)
propValue := propValuePointer.Obj() // TODO: What about getters?
if propValue == nil {
continue
}
if isFirst {
final += " "
} else {
final += ", "
}
isFirst = false
strP, uw := s.serializeJSONProperty(propValue)
if uw != nil {
return "", uw
}
final += strconv.Quote(string(prop)) + ": " + strP
}
final += "}"
delete(s.stack, o)
return final, nil
}
func (s jsonSerializer) serializeJSONArray(o *rt.Object) (string, *rt.Unwind) {
contract.Assert(o.IsArray()) // expect to be called on an Array
if _, found := s.stack[o]; found {
return "", s.e.NewException(s.intrin.Tree(), "Cannot JSON serialize an object with cyclic references")
}
s.stack[o] = true
arr := o.ArrayValue()
contract.Assert(arr != nil)
isFirst := true
final := "["
for index := 0; index < len(*arr); index++ {
propValuePointer := (*arr)[index]
propValue := propValuePointer.Obj() // TODO: What about getters?
if isFirst {
final += " "
} else {
final += ", "
}
isFirst = false
strP, uw := s.serializeJSONProperty(propValue)
if uw != nil {
return "", uw
}
final += strP
}
final += "]"
delete(s.stack, o)
return final, nil
}
// jsonStringify provides JSON serialization of a Lumi object. This implementation follows a subset of
// https://tc39.github.io/ecma262/2017/#sec-json.stringify without `replacer` and `space` arguments.
func jsonStringify(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
contract.Assert(len(args) == 1) // just one arg: the object to stringify
obj := args[0]
if obj == nil {
return rt.NewReturnUnwind(e.alloc.NewString(intrin.Tree(), "{}"))
}
s := jsonSerializer{
map[*rt.Object]bool{},
intrin,
e,
}
str, uw := s.serializeJSONProperty(obj)
if uw != nil {
return uw
}
return rt.NewReturnUnwind(e.alloc.NewString(intrin.Tree(), str))
}
func jsonParse(intrin *rt.Intrinsic, e *evaluator, this *rt.Object, args []*rt.Object) *rt.Unwind {
return e.NewException(intrin.Tree(), "Not yet implemented - jsonParse")
}

View file

@ -381,12 +381,42 @@ func NewNullObject() *Object {
// NewStringObject creates a new primitive number object.
func NewStringObject(v string) *Object {
return NewPrimitiveObject(types.String, v)
// Add a `length` property to the object
arrayProps := NewPropertyMap()
lengthGetter := NewBuiltinIntrinsic(
tokens.Token("lumi:builtin/string:getLength"),
symbols.NewFunctionType([]symbols.Type{}, types.Number),
)
arrayProps.InitAddr(PropertyKey("length"), nil, true, lengthGetter, nil)
stringProto := StringPrototypeObject()
return NewObject(types.String, v, arrayProps, stringProto)
}
// stringProto is a cached reference to the String prototype object
var stringProto *Object
// StringPrototypeObject returns the String prototype object
func StringPrototypeObject() *Object {
if stringProto != nil {
return stringProto
}
stringProtoProps := NewPropertyMap()
stringProto = NewObject(types.String, "", stringProtoProps, nil)
toLowerCase := NewFunctionObjectFromSymbol(NewBuiltinIntrinsic(
tokens.Token("lumi:builtin/string:toLowerCase"),
symbols.NewFunctionType([]symbols.Type{}, types.String),
), stringProto)
stringProtoProps.InitAddr(PropertyKey("toLowerCase"), toLowerCase, true, nil, nil)
return stringProto
}
// NewFunctionObject creates a new function object out of consistuent parts.
func NewFunctionObject(stub FuncStub) *Object {
contract.Assert(stub.Func != nil)
contract.Assert(stub.Sig != nil)
return NewObject(stub.Sig, stub, nil, nil)
}