github.com/mweagle/Sparta@v1.15.0/apigatewayv2.go (about)

     1  package sparta
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/aws/aws-sdk-go/aws/session"
     7  	gocf "github.com/mweagle/go-cloudformation"
     8  	"github.com/pkg/errors"
     9  	"github.com/sirupsen/logrus"
    10  )
    11  
    12  // Ref: https://github.com/aws-samples/simple-websockets-chat-app
    13  
    14  // APIV2RouteSelectionExpression represents a route selection
    15  type APIV2RouteSelectionExpression string
    16  
    17  // APIV2Protocol is the type of API V2 protocols
    18  type APIV2Protocol string
    19  
    20  const (
    21  	// Websocket represents the only supported V2 protocol
    22  	Websocket APIV2Protocol = "WEBSOCKET"
    23  )
    24  
    25  // APIV2 contains the information necessary for the routes in here.
    26  // Please tell me they can use the same routes...
    27  // They cannot
    28  type APIV2 struct {
    29  	protocol                  APIV2Protocol
    30  	name                      string
    31  	routeSelectionExpression  string
    32  	stage                     *APIV2Stage
    33  	APIKeySelectionExpression string
    34  	Description               string
    35  	DisableSchemaValidation   bool
    36  	Tags                      map[string]interface{}
    37  	Version                   string
    38  	// Routes mapping selection expression to Route handler
    39  	routes map[APIV2RouteSelectionExpression]*APIV2Route
    40  }
    41  
    42  // APIV2GatewayDecorator is the compound decorator that handles both
    43  // the DDB table creation and the lambda decorator...winning.
    44  type APIV2GatewayDecorator struct {
    45  	envTableKeyName string
    46  	propertyName    string
    47  	readCapacity    int64
    48  	writeCapacity   int64
    49  }
    50  
    51  func (apigd *APIV2GatewayDecorator) logicalResourceName() string {
    52  	return CloudFormationResourceName("WSSConnectionTable",
    53  		"WSSConnectionTable")
    54  }
    55  
    56  // DecorateService handles inserting the DDB Table
    57  func (apigd *APIV2GatewayDecorator) DecorateService(context map[string]interface{},
    58  	serviceName string,
    59  	template *gocf.Template,
    60  	S3Bucket string,
    61  	S3Key string,
    62  	buildID string,
    63  	awsSession *session.Session,
    64  	noop bool,
    65  	logger *logrus.Logger) error {
    66  
    67  	// Create the table...
    68  	dynamoDBResourceName := apigd.logicalResourceName()
    69  	dynamoDBResource := &gocf.DynamoDBTable{
    70  		AttributeDefinitions: &gocf.DynamoDBTableAttributeDefinitionList{
    71  			gocf.DynamoDBTableAttributeDefinition{
    72  				AttributeName: gocf.String(apigd.propertyName),
    73  				AttributeType: gocf.String("S"),
    74  			},
    75  		},
    76  		KeySchema: &gocf.DynamoDBTableKeySchemaList{
    77  			gocf.DynamoDBTableKeySchema{
    78  				AttributeName: gocf.String(apigd.propertyName),
    79  				KeyType:       gocf.String("HASH"),
    80  			},
    81  		},
    82  		SSESpecification: &gocf.DynamoDBTableSSESpecification{
    83  			SSEEnabled: gocf.Bool(true),
    84  		},
    85  		ProvisionedThroughput: &gocf.DynamoDBTableProvisionedThroughput{
    86  			ReadCapacityUnits:  gocf.Integer(apigd.readCapacity),
    87  			WriteCapacityUnits: gocf.Integer(apigd.writeCapacity),
    88  		},
    89  	}
    90  	template.AddResource(dynamoDBResourceName, dynamoDBResource)
    91  	return nil
    92  }
    93  
    94  // AnnotateLambdas handles hooking up the lambda perms
    95  func (apigd *APIV2GatewayDecorator) AnnotateLambdas(lambdaFns []*LambdaAWSInfo) error {
    96  
    97  	var ddbPermissions = []IAMRolePrivilege{
    98  		{
    99  			Actions: []string{"dynamodb:GetItem",
   100  				"dynamodb:DeleteItem",
   101  				"dynamodb:PutItem",
   102  				"dynamodb:Scan",
   103  				"dynamodb:Query",
   104  				"dynamodb:UpdateItem",
   105  				"dynamodb:BatchWriteItem",
   106  				"dynamodb:BatchGetItem"},
   107  			Resource: gocf.Join("",
   108  				gocf.String("arn:"),
   109  				gocf.Ref("AWS::Partition"),
   110  				gocf.String(":dynamodb:"),
   111  				gocf.Ref("AWS::Region"),
   112  				gocf.String(":"),
   113  				gocf.Ref("AWS::AccountId"),
   114  				gocf.String(":table/"),
   115  				gocf.Ref(apigd.logicalResourceName())),
   116  		},
   117  		{
   118  			Actions: []string{"dynamodb:GetItem",
   119  				"dynamodb:DeleteItem",
   120  				"dynamodb:PutItem",
   121  				"dynamodb:Scan",
   122  				"dynamodb:Query",
   123  				"dynamodb:UpdateItem",
   124  				"dynamodb:BatchWriteItem",
   125  				"dynamodb:BatchGetItem"},
   126  			Resource: gocf.Join("",
   127  				gocf.String("arn:"),
   128  				gocf.Ref("AWS::Partition"),
   129  				gocf.String(":dynamodb:"),
   130  				gocf.Ref("AWS::Region"),
   131  				gocf.String(":"),
   132  				gocf.Ref("AWS::AccountId"),
   133  				gocf.String(":table/"),
   134  				gocf.Ref(apigd.logicalResourceName()),
   135  				gocf.String("/index/*")),
   136  		},
   137  	}
   138  
   139  	for _, eachLambda := range lambdaFns {
   140  		// Add the permission
   141  		eachLambda.RoleDefinition.Privileges = append(eachLambda.RoleDefinition.Privileges,
   142  			ddbPermissions...)
   143  
   144  		// Add the env
   145  		env := eachLambda.Options.Environment
   146  		if env == nil {
   147  			env = make(map[string]*gocf.StringExpr)
   148  		}
   149  		env[apigd.envTableKeyName] = gocf.Ref(apigd.logicalResourceName()).String()
   150  		eachLambda.Options.Environment = env
   151  	}
   152  	return nil
   153  }
   154  
   155  // NewConnectionTableDecorator returns a *APIV2GatewayDecorator that handles
   156  // creating the DynamoDDB table and hooking up all the lambda permissions
   157  func (apiv2 *APIV2) NewConnectionTableDecorator(envTableNameKey string,
   158  	propertyName string,
   159  	readCapacity int64,
   160  	writeCapacity int64) (*APIV2GatewayDecorator, error) {
   161  
   162  	return &APIV2GatewayDecorator{
   163  		envTableKeyName: envTableNameKey,
   164  		propertyName:    propertyName,
   165  		readCapacity:    readCapacity,
   166  		writeCapacity:   writeCapacity,
   167  	}, nil
   168  }
   169  
   170  // NewAPIV2Route returns a new Route
   171  func (apiv2 *APIV2) NewAPIV2Route(routeKey APIV2RouteSelectionExpression,
   172  	lambdaFn *LambdaAWSInfo) (*APIV2Route, error) {
   173  
   174  	_, exists := apiv2.routes[routeKey]
   175  	if exists {
   176  		return nil, errors.Errorf("APIV2 Route for expression `%s` already exists",
   177  			routeKey)
   178  	}
   179  	route := &APIV2Route{
   180  		routeKey: routeKey,
   181  		lambdaFn: lambdaFn,
   182  		Integration: &APIV2Integration{
   183  			IntegrationType: "AWS_PROXY",
   184  		},
   185  	}
   186  	apiv2.routes[routeKey] = route
   187  	return route, nil
   188  }
   189  
   190  // LogicalResourceName returns the logical resoource name of this API V2 Gateway
   191  // instance
   192  func (apiv2 *APIV2) LogicalResourceName() string {
   193  	return CloudFormationResourceName("APIGateway",
   194  		fmt.Sprintf("v2%s", apiv2.name))
   195  }
   196  
   197  // Describe satisfies the API interface
   198  func (apiv2 *APIV2) Describe(targetNodeName string) (*DescriptionInfo, error) {
   199  	descInfo := &DescriptionInfo{
   200  		Name:  "APIGatewayV2",
   201  		Nodes: make([]*DescriptionTriplet, 0),
   202  	}
   203  
   204  	descInfo.Nodes = append(descInfo.Nodes, &DescriptionTriplet{
   205  		SourceNodeName: nodeNameAPIGateway,
   206  		DisplayInfo: &DescriptionDisplayInfo{
   207  			SourceNodeColor: nodeColorAPIGateway,
   208  			SourceIcon: &DescriptionIcon{
   209  				Category: "Mobile",
   210  				Name:     "Amazon-API-Gateway_light-bg@4x.png",
   211  			},
   212  		},
   213  		TargetNodeName: targetNodeName,
   214  	})
   215  
   216  	for eachRouteExpr, eachRoute := range apiv2.routes {
   217  		opName := ""
   218  		if eachRoute.OperationName != "" {
   219  			opName = fmt.Sprintf(" - %s", eachRoute.OperationName)
   220  		}
   221  		var nodeName = fmt.Sprintf("%s%s", eachRouteExpr, opName)
   222  
   223  		descInfo.Nodes = append(descInfo.Nodes,
   224  			&DescriptionTriplet{
   225  				SourceNodeName: nodeNameAPIGateway,
   226  				DisplayInfo: &DescriptionDisplayInfo{
   227  					SourceNodeColor: nodeColorAPIGateway,
   228  					SourceIcon: &DescriptionIcon{
   229  						Category: "_General",
   230  						Name:     "Internet-alt1_light-bg@4x.png",
   231  					},
   232  				},
   233  				TargetNodeName: nodeName,
   234  			},
   235  			&DescriptionTriplet{
   236  				SourceNodeName: nodeName,
   237  				TargetNodeName: eachRoute.lambdaFn.lambdaFunctionName(),
   238  			})
   239  	}
   240  	return descInfo, nil
   241  }
   242  
   243  // Marshal marshals the API V2 Gateway instance to the given template instance
   244  func (apiv2 *APIV2) Marshal(serviceName string,
   245  	session *session.Session,
   246  	S3Bucket string,
   247  	S3Key string,
   248  	S3Version string,
   249  	roleNameMap map[string]*gocf.StringExpr,
   250  	template *gocf.Template,
   251  	noop bool,
   252  	logger *logrus.Logger) error {
   253  
   254  	apiV2Entry := &gocf.APIGatewayV2API{
   255  		APIKeySelectionExpression: marshalString(apiv2.APIKeySelectionExpression),
   256  		Description:               marshalString(apiv2.Description),
   257  		DisableSchemaValidation:   marshalBool(apiv2.DisableSchemaValidation),
   258  		Name:                      marshalString(apiv2.name),
   259  		ProtocolType:              marshalString(string(Websocket)),
   260  		RouteSelectionExpression:  marshalString(apiv2.routeSelectionExpression),
   261  		Version:                   marshalString(apiv2.Version),
   262  	}
   263  	// Add it
   264  	apiV2Resource := template.AddResource(apiv2.LogicalResourceName(), apiV2Entry)
   265  	apiV2Resource.DependsOn = []string{}
   266  	allRouteResources := []string{}
   267  
   268  	// Alright, setup the route
   269  	for eachExpression, eachRoute := range apiv2.routes {
   270  		routeResourceName := CloudFormationResourceName("Route", string(eachExpression))
   271  		allRouteResources = append(allRouteResources, routeResourceName)
   272  
   273  		routeIntegrationResourceName := CloudFormationResourceName("RouteIntg", string(eachExpression))
   274  		routeEntry := &gocf.APIGatewayV2Route{
   275  			APIID:                            gocf.Ref(apiv2.LogicalResourceName()).String(),
   276  			APIKeyRequired:                   marshalBool(eachRoute.APIKeyRequired),
   277  			AuthorizerID:                     marshalStringExpr(eachRoute.AuthorizerID),
   278  			AuthorizationScopes:              marshalStringList(eachRoute.AuthorizationScopes),
   279  			AuthorizationType:                marshalString(eachRoute.AuthorizationType),
   280  			ModelSelectionExpression:         marshalString(eachRoute.ModelSelectionExpression),
   281  			OperationName:                    marshalString(eachRoute.OperationName),
   282  			RequestModels:                    marshalInterface(eachRoute.RequestModels),
   283  			RequestParameters:                marshalInterface(eachRoute.RequestParameters),
   284  			RouteKey:                         marshalString(string(eachRoute.routeKey)),
   285  			RouteResponseSelectionExpression: marshalString(eachRoute.RouteResponseSelectionExpression),
   286  			Target: gocf.Join("/",
   287  				gocf.String("integrations"),
   288  				gocf.Ref(routeIntegrationResourceName)),
   289  		}
   290  
   291  		// Add the route resource
   292  		template.AddResource(routeResourceName, routeEntry)
   293  		//apiV2Resource.DependsOn = append(apiV2Resource.DependsOn, routeResourceName)
   294  
   295  		// Add the integration
   296  		routeIntegration := &gocf.APIGatewayV2Integration{
   297  			APIID:                   gocf.Ref(apiv2.LogicalResourceName()).String(),
   298  			ConnectionType:          marshalString(eachRoute.Integration.ConnectionType),
   299  			ContentHandlingStrategy: marshalString(eachRoute.Integration.ContentHandlingStrategy),
   300  			CredentialsArn:          marshalStringExpr(eachRoute.Integration.CredentialsArn),
   301  			Description:             marshalString(eachRoute.Integration.Description),
   302  			IntegrationMethod:       marshalString(eachRoute.Integration.IntegrationMethod),
   303  			IntegrationType:         marshalString(eachRoute.Integration.IntegrationType),
   304  			IntegrationURI: gocf.Join("",
   305  				gocf.String("arn:aws:apigateway:"),
   306  				gocf.Ref("AWS::Region"),
   307  				gocf.String(":lambda:path/2015-03-31/functions/"),
   308  				gocf.GetAtt(eachRoute.lambdaFn.LogicalResourceName(), "Arn"),
   309  				gocf.String("/invocations")),
   310  			PassthroughBehavior: marshalString(eachRoute.Integration.PassthroughBehavior),
   311  			// TODO - auto create this...
   312  			RequestParameters:           marshalInterface(eachRoute.Integration.RequestParameters),
   313  			RequestTemplates:            marshalInterface(eachRoute.Integration.RequestTemplates),
   314  			TemplateSelectionExpression: marshalString(eachRoute.Integration.TemplateSelectionExpression),
   315  			TimeoutInMillis:             marshalInt(eachRoute.Integration.TimeoutInMillis),
   316  		}
   317  		template.AddResource(routeIntegrationResourceName, routeIntegration)
   318  
   319  		// Add the lambda permission
   320  		apiGatewayPermissionResourceName := CloudFormationResourceName("APIV2GatewayLambdaPerm",
   321  			string(eachExpression))
   322  		lambdaInvokePermission := &gocf.LambdaPermission{
   323  			Action:       gocf.String("lambda:InvokeFunction"),
   324  			FunctionName: gocf.GetAtt(eachRoute.lambdaFn.LogicalResourceName(), "Arn"),
   325  			Principal:    gocf.String(APIGatewayPrincipal),
   326  		}
   327  		template.AddResource(apiGatewayPermissionResourceName, lambdaInvokePermission)
   328  	}
   329  
   330  	// Add the Stage and Deploy...
   331  	stageResourceName := CloudFormationResourceName("APIV2GatewayStage", "APIV2GatewayStage")
   332  	// Use an unstable ID s.t. we can actually create a new deployment event.
   333  	deploymentResName := CloudFormationResourceName("APIV2GatewayDeployment")
   334  
   335  	// Unstable name to trigger a deployment
   336  	newDeployment := &gocf.APIGatewayV2Deployment{
   337  		APIID:       gocf.Ref(apiv2.LogicalResourceName()).String(),
   338  		Description: marshalString(apiv2.stage.Description),
   339  	}
   340  	// Use an unstable ID s.t. we can actually create a new deployment event.  Not sure how this
   341  	// is going to work with deletes...
   342  	deployment := template.AddResource(deploymentResName, newDeployment)
   343  	deployment.DeletionPolicy = "Retain"
   344  	deployment.DependsOn = append(deployment.DependsOn, allRouteResources...)
   345  
   346  	// Add the stage...
   347  	stageResource := &gocf.APIGatewayV2Stage{
   348  		APIID:                gocf.Ref(apiv2.LogicalResourceName()).String(),
   349  		DeploymentID:         gocf.Ref(deploymentResName).String(),
   350  		StageName:            marshalString(apiv2.stage.name),
   351  		AccessLogSettings:    apiv2.stage.AccessLogSettings,
   352  		ClientCertificateID:  marshalStringExpr(apiv2.stage.ClientCertificateID),
   353  		DefaultRouteSettings: apiv2.stage.DefaultRouteSettings,
   354  		Description:          marshalString(apiv2.stage.Description),
   355  		RouteSettings:        marshalInterface(apiv2.stage.RouteSettings),
   356  		StageVariables:       marshalInterface(apiv2.stage.StageVariables),
   357  		//Tags:                 marshalInterface(apiv2.stage.Tags),
   358  	}
   359  	template.AddResource(stageResourceName, stageResource)
   360  
   361  	// Outputs...
   362  	template.Outputs[OutputAPIGatewayURL] = &gocf.Output{
   363  		Description: "API Gateway Websocket URL",
   364  		Value: gocf.Join("",
   365  			gocf.String("wss://"),
   366  			gocf.Ref(apiv2.LogicalResourceName()),
   367  			gocf.String(".execute-api."),
   368  			gocf.Ref("AWS::Region"),
   369  			gocf.String(".amazonaws.com/"),
   370  			gocf.String(apiv2.stage.name)),
   371  	}
   372  	return nil
   373  }
   374  
   375  // NewAPIV2 returns a new API V2 Gateway instance
   376  func NewAPIV2(protocol APIV2Protocol,
   377  	name string,
   378  	routeSelectionExpression string,
   379  	stage *APIV2Stage) (*APIV2, error) {
   380  	return &APIV2{
   381  		protocol:                 protocol,
   382  		name:                     name,
   383  		routeSelectionExpression: routeSelectionExpression,
   384  		stage:                    stage,
   385  		routes:                   make(map[APIV2RouteSelectionExpression]*APIV2Route),
   386  		Tags:                     make(map[string]interface{}),
   387  	}, nil
   388  }
   389  
   390  // APIV2Route represents a V2 route
   391  type APIV2Route struct {
   392  	routeKey                         APIV2RouteSelectionExpression
   393  	APIKeyRequired                   bool
   394  	AuthorizationScopes              []string
   395  	AuthorizationType                string
   396  	AuthorizerID                     gocf.Stringable
   397  	ModelSelectionExpression         string
   398  	OperationName                    string
   399  	RequestModels                    interface{}
   400  	RequestParameters                interface{}
   401  	RouteResponseSelectionExpression string
   402  	Integration                      *APIV2Integration
   403  	lambdaFn                         *LambdaAWSInfo
   404  }
   405  
   406  // APIV2Stage represents the deployment stage
   407  type APIV2Stage struct {
   408  	AccessLogSettings    *gocf.APIGatewayV2StageAccessLogSettings
   409  	ClientCertificateID  gocf.Stringable
   410  	DefaultRouteSettings *gocf.APIGatewayV2StageRouteSettings
   411  	Description          string
   412  	RouteSettings        interface{}
   413  	name                 string
   414  	StageVariables       interface{}
   415  	Tags                 interface{}
   416  }
   417  
   418  // NewAPIV2Stage returns a new APIV2Stage entry
   419  func NewAPIV2Stage(stageName string) (*APIV2Stage, error) {
   420  	return &APIV2Stage{
   421  		name: stageName,
   422  	}, nil
   423  }
   424  
   425  // APIV2Integration is the integration type for an APIV2Route
   426  // entry
   427  type APIV2Integration struct {
   428  	//ApiID                       string
   429  	ConnectionType          string
   430  	ContentHandlingStrategy string
   431  	CredentialsArn          gocf.Stringable
   432  	Description             string
   433  	IntegrationMethod       string
   434  	IntegrationType         string
   435  	//IntegrationUri              string
   436  	PassthroughBehavior         string
   437  	RequestParameters           interface{}
   438  	RequestTemplates            interface{}
   439  	TemplateSelectionExpression string
   440  	TimeoutInMillis             int64
   441  }