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

     1  package sparta
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"reflect"
     7  	"text/template"
     8  
     9  	spartaCF "github.com/mweagle/Sparta/aws/cloudformation"
    10  	cfCustomResources "github.com/mweagle/Sparta/aws/cloudformation/resources"
    11  	spartaIAM "github.com/mweagle/Sparta/aws/iam"
    12  	gocf "github.com/mweagle/go-cloudformation"
    13  	"github.com/pkg/errors"
    14  	"github.com/sirupsen/logrus"
    15  )
    16  
    17  const (
    18  	// ScratchDirectory is the cwd relative path component
    19  	// where intermediate build artifacts are created
    20  	ScratchDirectory = ".sparta"
    21  	// EnvVarCustomResourceTypeName is the environment variable
    22  	// name that stores the CustomResource TypeName that should be
    23  	// instantiated
    24  	EnvVarCustomResourceTypeName = "SPARTA_CUSTOM_RESOURCE_TYPE"
    25  )
    26  
    27  // This is a literal version of the DiscoveryInfo struct.
    28  var discoveryData = `
    29  {
    30  	"ResourceID": "<< .TagLogicalResourceID >>",
    31  	"Region": "{"Ref" : "AWS::Region"}",
    32  	"StackID": "{"Ref" : "AWS::StackId"}",
    33  	"StackName": "{"Ref" : "AWS::StackName"}",
    34  	"Resources":{<<range $eachDepResource, $eachOutputString := .Resources>>
    35  		"<< $eachDepResource >>" : << $eachOutputString >><< trailingComma >><<end>>
    36  	}
    37  }`
    38  
    39  //
    40  type discoveryDataTemplateData struct {
    41  	TagLogicalResourceID string
    42  	Resources            map[string]string
    43  }
    44  
    45  func lambdaFunctionEnvironment(userEnvMap map[string]*gocf.StringExpr,
    46  	resourceID string,
    47  	deps map[string]string,
    48  	logger *logrus.Logger) (*gocf.LambdaFunctionEnvironment, error) {
    49  	// Merge everything, add the deps
    50  	envMap := make(map[string]interface{})
    51  	for eachKey, eachValue := range userEnvMap {
    52  		envMap[eachKey] = eachValue
    53  	}
    54  	discoveryInfo, discoveryInfoErr := discoveryInfoForResource(resourceID, deps)
    55  	if discoveryInfoErr != nil {
    56  		return nil, errors.Wrapf(discoveryInfoErr, "Failed to calculate dependency info")
    57  	}
    58  	envMap[envVarLogLevel] = logger.Level.String()
    59  	envMap[envVarDiscoveryInformation] = discoveryInfo
    60  	return &gocf.LambdaFunctionEnvironment{
    61  		Variables: envMap,
    62  	}, nil
    63  }
    64  
    65  func discoveryInfoForResource(resID string, deps map[string]string) (*gocf.StringExpr, error) {
    66  	discoveryDataTemplateData := &discoveryDataTemplateData{
    67  		TagLogicalResourceID: resID,
    68  		Resources:            deps,
    69  	}
    70  	totalDeps := len(deps)
    71  	var templateFuncMap = template.FuncMap{
    72  		// The name "inc" is what the function will be called in the template text.
    73  		"trailingComma": func() string {
    74  			totalDeps--
    75  			if totalDeps > 0 {
    76  				return ","
    77  			}
    78  			return ""
    79  		},
    80  	}
    81  
    82  	discoveryTemplate, discoveryTemplateErr := template.New("discoveryData").
    83  		Delims("<<", ">>").
    84  		Funcs(templateFuncMap).
    85  		Parse(discoveryData)
    86  	if nil != discoveryTemplateErr {
    87  		return nil, discoveryTemplateErr
    88  	}
    89  
    90  	var templateResults bytes.Buffer
    91  	evalResultErr := discoveryTemplate.Execute(&templateResults, discoveryDataTemplateData)
    92  	if nil != evalResultErr {
    93  		return nil, evalResultErr
    94  	}
    95  	templateReader := bytes.NewReader(templateResults.Bytes())
    96  	templateExpr, templateExprErr := spartaCF.ConvertToTemplateExpression(templateReader, nil)
    97  	if templateExprErr != nil {
    98  		return nil, templateExprErr
    99  	}
   100  	return gocf.Base64(templateExpr), nil
   101  }
   102  
   103  // EnsureCustomResourceHandler handles ensuring that the custom resource responsible
   104  // for supporting the operation is actually part of this stack. The returned
   105  // string value is the CloudFormation resource name that implements this
   106  // resource. The customResourceCloudFormationTypeName must have already
   107  // been registered with gocf and implement the resources.CustomResourceCommand
   108  // interface
   109  func EnsureCustomResourceHandler(serviceName string,
   110  	customResourceCloudFormationTypeName string,
   111  	sourceArn *gocf.StringExpr,
   112  	dependsOn []string,
   113  	template *gocf.Template,
   114  	S3Bucket string,
   115  	S3Key string,
   116  	logger *logrus.Logger) (string, error) {
   117  
   118  	// Ok, we need a way to round trip this type as the AWS lambda function name.
   119  	// The problem with this is that the full CustomResource::Type value isn't an
   120  	// AWS Lambda friendly name. We want to do this so that in the AWS lambda handler
   121  	// we can attempt to instantiate a new CustomAction resource, typecast it to a
   122  	// CustomResourceCommand type and then apply the workflow. Doing this means
   123  	// we can decouple the lookup logic for custom resource...
   124  
   125  	resource := gocf.NewResourceByType(customResourceCloudFormationTypeName)
   126  	if resource == nil {
   127  		return "", errors.Errorf("Unable to create custom resource handler of type: %v", customResourceCloudFormationTypeName)
   128  	}
   129  	command, commandOk := resource.(cfCustomResources.CustomResourceCommand)
   130  	if !commandOk {
   131  		return "", errors.Errorf("Cannot type assert resource type %s to CustomResourceCommand", customResourceCloudFormationTypeName)
   132  	}
   133  
   134  	// Prefix
   135  	commandType := reflect.TypeOf(command)
   136  	customResourceTypeName := fmt.Sprintf("%T", command)
   137  	prefixName := fmt.Sprintf("%s-CFRes", serviceName)
   138  	subscriberHandlerName := CloudFormationResourceName(prefixName, customResourceTypeName)
   139  
   140  	//////////////////////////////////////////////////////////////////////////////
   141  	// IAM Role definition
   142  	iamResourceName, err := ensureIAMRoleForCustomResource(command,
   143  		sourceArn,
   144  		template,
   145  		logger)
   146  	if nil != err {
   147  		return "", errors.Wrapf(err,
   148  			"Failed to ensure IAM Role for custom resource: %T",
   149  			command)
   150  	}
   151  	iamRoleRef := gocf.GetAtt(iamResourceName, "Arn")
   152  	_, exists := template.Resources[subscriberHandlerName]
   153  	if exists {
   154  		return subscriberHandlerName, nil
   155  	}
   156  
   157  	// Encode the resourceType...
   158  	configuratorDescription := customResourceDescription(serviceName, customResourceTypeName)
   159  
   160  	//////////////////////////////////////////////////////////////////////////////
   161  	// Custom Resource Lambda Handler
   162  	// Insert it into the template resources...
   163  	logger.WithFields(logrus.Fields{
   164  		"CloudFormationResourceType": customResourceCloudFormationTypeName,
   165  		"Resource":                   customResourceTypeName,
   166  		"TypeOf":                     commandType.String(),
   167  	}).Info("Including Lambda CustomResource")
   168  
   169  	// Don't forget the discovery info...
   170  	userDispatchMap := map[string]*gocf.StringExpr{
   171  		EnvVarCustomResourceTypeName: gocf.String(customResourceCloudFormationTypeName),
   172  	}
   173  	lambdaEnv, lambdaEnvErr := lambdaFunctionEnvironment(userDispatchMap,
   174  		customResourceTypeName,
   175  		nil,
   176  		logger)
   177  	if lambdaEnvErr != nil {
   178  		return "", errors.Wrapf(lambdaEnvErr, "Failed to create environment for required custom resource")
   179  	}
   180  	// Add the special key that's the custom resource type name
   181  	customResourceHandlerDef := gocf.LambdaFunction{
   182  		Code: &gocf.LambdaFunctionCode{
   183  			S3Bucket: gocf.String(S3Bucket),
   184  			S3Key:    gocf.String(S3Key),
   185  		},
   186  		Runtime:     gocf.String(GoLambdaVersion),
   187  		Description: gocf.String(configuratorDescription),
   188  		Handler:     gocf.String(SpartaBinaryName),
   189  		Role:        iamRoleRef,
   190  		Timeout:     gocf.Integer(30),
   191  		// Let AWS assign a name here...
   192  		//		FunctionName: lambdaFunctionName.String(),
   193  		// DISPATCH INFORMATION
   194  		Environment: lambdaEnv,
   195  	}
   196  
   197  	cfResource := template.AddResource(subscriberHandlerName, customResourceHandlerDef)
   198  	if nil != dependsOn && (len(dependsOn) > 0) {
   199  		cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...)
   200  	}
   201  	return subscriberHandlerName, nil
   202  }
   203  
   204  // ensureIAMRoleForCustomResource ensures that the single IAM::Role for a single
   205  // AWS principal (eg, s3.*.*) exists, and includes statements for the given
   206  // sourceArn.  Sparta uses a single IAM::Role for the CustomResource configuration
   207  // lambda, which is the union of all Arns in the application.
   208  func ensureIAMRoleForCustomResource(command cfCustomResources.CustomResourceCommand,
   209  	sourceArn *gocf.StringExpr,
   210  	template *gocf.Template,
   211  	logger *logrus.Logger) (string, error) {
   212  
   213  	// What's the stable IAMRoleName?
   214  	commandName := fmt.Sprintf("%T", command)
   215  	resourceBaseName := fmt.Sprintf("CFResIAMRole%s", commandName)
   216  	stableRoleName := CloudFormationResourceName(resourceBaseName, resourceBaseName)
   217  
   218  	// Is it a privileged command?
   219  	var privileges []string
   220  	privilegedCommand, privilegedCommandOk := command.(cfCustomResources.CustomResourcePrivilegedCommand)
   221  	if privilegedCommandOk {
   222  		privileges = privilegedCommand.IAMPrivileges()
   223  	}
   224  
   225  	// Ensure it exists, then check to see if this Source ARN is already specified...
   226  	// Checking equality with Stringable?
   227  
   228  	// Create a new Role
   229  	var existingIAMRole *gocf.IAMRole
   230  	existingResource, exists := template.Resources[stableRoleName]
   231  	logger.WithFields(logrus.Fields{
   232  		"PrincipalActions": privileges,
   233  		"SourceArn":        sourceArn,
   234  	}).Debug("Ensuring IAM Role results")
   235  
   236  	if !exists {
   237  		// Insert the IAM role here.  We'll walk the policies data in the next section
   238  		// to make sure that the sourceARN we have is in the list
   239  		statements := CommonIAMStatements.Core
   240  
   241  		iamPolicyList := gocf.IAMRolePolicyList{}
   242  		iamPolicyList = append(iamPolicyList,
   243  			gocf.IAMRolePolicy{
   244  				PolicyDocument: ArbitraryJSONObject{
   245  					"Version":   "2012-10-17",
   246  					"Statement": statements,
   247  				},
   248  				PolicyName: gocf.String(fmt.Sprintf("%sPolicy", stableRoleName)),
   249  			},
   250  		)
   251  
   252  		existingIAMRole = &gocf.IAMRole{
   253  			AssumeRolePolicyDocument: AssumePolicyDocument,
   254  			Policies:                 &iamPolicyList,
   255  		}
   256  		template.AddResource(stableRoleName, existingIAMRole)
   257  
   258  		// Create a new IAM Role resource
   259  		logger.WithFields(logrus.Fields{
   260  			"RoleName": stableRoleName,
   261  		}).Debug("Inserting IAM Role")
   262  	} else {
   263  		existingIAMRole = existingResource.Properties.(*gocf.IAMRole)
   264  	}
   265  
   266  	// ARNs are only required if there are non-empty privileges associated
   267  	// with the command
   268  	if sourceArn == nil {
   269  		if len(privileges) != 0 {
   270  			return "", errors.Errorf("CustomResource %s requires a SourceARN to apply it's %d principle actions",
   271  				commandName,
   272  				len(privileges))
   273  		}
   274  		return stableRoleName, nil
   275  	}
   276  	// Walk the existing statements
   277  	if nil != existingIAMRole.Policies {
   278  		for _, eachPolicy := range *existingIAMRole.Policies {
   279  			policyDoc := eachPolicy.PolicyDocument.(ArbitraryJSONObject)
   280  			statements := policyDoc["Statement"]
   281  			for _, eachStatement := range statements.([]spartaIAM.PolicyStatement) {
   282  				if sourceArn.String() == eachStatement.Resource.String() {
   283  
   284  					logger.WithFields(logrus.Fields{
   285  						"RoleName":  stableRoleName,
   286  						"SourceArn": sourceArn.String(),
   287  					}).Debug("SourceArn already exists for IAM Policy")
   288  					return stableRoleName, nil
   289  				}
   290  			}
   291  		}
   292  
   293  		logger.WithFields(logrus.Fields{
   294  			"RoleName": stableRoleName,
   295  			"Action":   privileges,
   296  			"Resource": sourceArn,
   297  		}).Debug("Inserting Actions for configuration ARN")
   298  
   299  		// Add this statement to the first policy, iff the actions are non-empty
   300  		if len(privileges) > 0 {
   301  			rootPolicy := (*existingIAMRole.Policies)[0]
   302  			rootPolicyDoc := rootPolicy.PolicyDocument.(ArbitraryJSONObject)
   303  			rootPolicyStatements := rootPolicyDoc["Statement"].([]spartaIAM.PolicyStatement)
   304  			rootPolicyDoc["Statement"] = append(rootPolicyStatements,
   305  				spartaIAM.PolicyStatement{
   306  					Effect:   "Allow",
   307  					Action:   privileges,
   308  					Resource: sourceArn,
   309  				})
   310  		}
   311  		return stableRoleName, nil
   312  	}
   313  	return "", errors.Errorf("Unable to find Policies entry for IAM role: %s", stableRoleName)
   314  }