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:
commit
5f1e8b7653
32 changed files with 1355 additions and 280 deletions
|
@ -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")
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -16,5 +16,3 @@
|
|||
export * from "./function";
|
||||
export * from "./permission";
|
||||
|
||||
export * from "./functionx";
|
||||
|
||||
|
|
121
lib/aws/pack/serverless/api.ts
Normal file
121
lib/aws/pack/serverless/api.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
|
@ -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";
|
181
lib/aws/provider/apigateway/deployment.go
Normal file
181
lib/aws/provider/apigateway/deployment.go
Normal 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
|
||||
}
|
190
lib/aws/provider/apigateway/patchoperations.go
Normal file
190
lib/aws/provider/apigateway/patchoperations.go
Normal 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
|
||||
}
|
84
lib/aws/provider/apigateway/patchoperations_test.go
Normal file
84
lib/aws/provider/apigateway/patchoperations_test.go
Normal 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)
|
||||
}
|
206
lib/aws/provider/apigateway/restapi.go
Normal file
206
lib/aws/provider/apigateway/restapi.go
Normal 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
|
||||
}
|
185
lib/aws/provider/apigateway/stage.go
Normal file
185
lib/aws/provider/apigateway/stage.go
Normal 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
|
||||
}
|
|
@ -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"
|
||||
|
@ -35,10 +36,12 @@ import (
|
|||
// 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.
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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) */
|
||||
|
|
|
@ -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) */
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
@ -26,8 +26,7 @@
|
|||
"asset/archive.ts",
|
||||
"asset/decors.ts",
|
||||
|
||||
"runtime/index.ts",
|
||||
"runtime/dynamic.ts"
|
||||
"runtime/index.ts"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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").
|
||||
|
|
|
@ -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/string:getLength": stringGetLength,
|
||||
"lumi:builtin/string:toLowerCase": stringToLowerCase,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue