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

     1  // +build !lambdabinary
     2  
     3  package sparta
     4  
     5  import (
     6  	"github.com/aws/aws-sdk-go/aws"
     7  	"github.com/aws/aws-sdk-go/service/s3"
     8  	cfCustomResources "github.com/mweagle/Sparta/aws/cloudformation/resources"
     9  	spartaIAM "github.com/mweagle/Sparta/aws/iam"
    10  	gocf "github.com/mweagle/go-cloudformation"
    11  	"github.com/pkg/errors"
    12  	"github.com/sirupsen/logrus"
    13  )
    14  
    15  const (
    16  	// OutputS3SiteURL is the keyname used in the CloudFormation Output
    17  	// that stores the S3 backed static site provisioned with this Sparta application
    18  	// @enum OutputKey
    19  	OutputS3SiteURL = "S3SiteURL"
    20  )
    21  
    22  // Create the resource, which will be part of the stack definition and use a CustomResource
    23  // to copy the content.  Which means we need PutItem access to the target Bucket.  Use
    24  // Cloudformation to create a random bucketname:
    25  // http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html
    26  
    27  // Need to create the S3 target bucket
    28  // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putBucketWebsite-property
    29  
    30  // export marshals the API data to a CloudFormation compatible representation
    31  func (s3Site *S3Site) export(serviceName string,
    32  	binaryName string,
    33  	S3Bucket string,
    34  	S3Key string,
    35  	S3ResourcesKey string,
    36  	apiGatewayOutputs map[string]*gocf.Output,
    37  	roleNameMap map[string]*gocf.StringExpr,
    38  	template *gocf.Template,
    39  	logger *logrus.Logger) error {
    40  
    41  	if s3Site.WebsiteConfiguration == nil {
    42  		s3Site.WebsiteConfiguration = &s3.WebsiteConfiguration{
    43  			ErrorDocument: &s3.ErrorDocument{
    44  				Key: aws.String("error.html"),
    45  			},
    46  			IndexDocument: &s3.IndexDocument{
    47  				Suffix: aws.String("index.html"),
    48  			},
    49  		}
    50  	}
    51  	// Ensure everything is set
    52  	if s3Site.WebsiteConfiguration.ErrorDocument == nil {
    53  		s3Site.WebsiteConfiguration.ErrorDocument = &s3.ErrorDocument{
    54  			Key: aws.String("error.html"),
    55  		}
    56  	}
    57  	if s3Site.WebsiteConfiguration.IndexDocument == nil {
    58  		s3Site.WebsiteConfiguration.IndexDocument = &s3.IndexDocument{
    59  			Suffix: aws.String("index.html"),
    60  		}
    61  	}
    62  
    63  	//////////////////////////////////////////////////////////////////////////////
    64  	// 1 - Create the S3 bucket.  The "BucketName" property is empty s.t.
    65  	// AWS will assign a unique one.
    66  
    67  	s3WebsiteConfig := &gocf.S3BucketWebsiteConfiguration{
    68  		ErrorDocument: gocf.String(aws.StringValue(s3Site.WebsiteConfiguration.ErrorDocument.Key)),
    69  		IndexDocument: gocf.String(aws.StringValue(s3Site.WebsiteConfiguration.IndexDocument.Suffix)),
    70  	}
    71  	s3Bucket := &gocf.S3Bucket{
    72  		AccessControl:        gocf.String("PublicRead"),
    73  		WebsiteConfiguration: s3WebsiteConfig,
    74  	}
    75  	if s3Site.BucketName != nil {
    76  		s3Bucket.BucketName = s3Site.BucketName
    77  	}
    78  	s3BucketResourceName := s3Site.CloudFormationS3ResourceName()
    79  	cfResource := template.AddResource(s3BucketResourceName, s3Bucket)
    80  	cfResource.DeletionPolicy = "Delete"
    81  
    82  	template.Outputs[OutputS3SiteURL] = &gocf.Output{
    83  		Description: "S3 Website URL",
    84  		Value:       gocf.GetAtt(s3BucketResourceName, "WebsiteURL"),
    85  	}
    86  
    87  	// Represents the S3 ARN that is provisioned
    88  	s3SiteBucketResourceValue := gocf.Join("",
    89  		gocf.String("arn:aws:s3:::"),
    90  		gocf.Ref(s3BucketResourceName))
    91  	s3SiteBucketAllKeysResourceValue := gocf.Join("",
    92  		gocf.String("arn:aws:s3:::"),
    93  		gocf.Ref(s3BucketResourceName),
    94  		gocf.String("/*"))
    95  
    96  	//////////////////////////////////////////////////////////////////////////////
    97  	// 2 - Add a bucket policy to enable anonymous access, as the PublicRead
    98  	// canned ACL doesn't seem to do what is implied.
    99  	// TODO - determine if this is needed or if PublicRead is being misued
   100  	s3SiteBucketPolicy := &gocf.S3BucketPolicy{
   101  		Bucket: gocf.Ref(s3BucketResourceName).String(),
   102  		PolicyDocument: ArbitraryJSONObject{
   103  			"Version": "2012-10-17",
   104  			"Statement": []ArbitraryJSONObject{
   105  				{
   106  					"Sid":    "PublicReadGetObject",
   107  					"Effect": "Allow",
   108  					"Principal": ArbitraryJSONObject{
   109  						"AWS": "*",
   110  					},
   111  					"Action":   "s3:GetObject",
   112  					"Resource": s3SiteBucketAllKeysResourceValue,
   113  				},
   114  			},
   115  		},
   116  	}
   117  	s3BucketPolicyResourceName := stableCloudformationResourceName("S3SiteBucketPolicy")
   118  	template.AddResource(s3BucketPolicyResourceName, s3SiteBucketPolicy)
   119  
   120  	//////////////////////////////////////////////////////////////////////////////
   121  	// 3 - Create the IAM role for the lambda function
   122  	// The lambda function needs to download the posted resource content, as well
   123  	// as manage the S3 bucket that hosts the site.
   124  	statements := CommonIAMStatements.Core
   125  	statements = append(statements, spartaIAM.PolicyStatement{
   126  		Action: []string{"s3:ListBucket",
   127  			"s3:ListObjectsPages"},
   128  		Effect:   "Allow",
   129  		Resource: s3SiteBucketResourceValue,
   130  	})
   131  	statements = append(statements, spartaIAM.PolicyStatement{
   132  		Action: []string{"s3:DeleteObject",
   133  			"s3:PutObject",
   134  			"s3:DeleteObjects"},
   135  		Effect:   "Allow",
   136  		Resource: s3SiteBucketAllKeysResourceValue,
   137  	})
   138  	statements = append(statements, spartaIAM.PolicyStatement{
   139  		Action: []string{"s3:GetObject"},
   140  		Effect: "Allow",
   141  		Resource: gocf.Join("",
   142  			gocf.String("arn:aws:s3:::"),
   143  			gocf.String(S3Bucket),
   144  			gocf.String("/"),
   145  			gocf.String(S3ResourcesKey)),
   146  	})
   147  
   148  	iamPolicyList := gocf.IAMRolePolicyList{}
   149  	iamPolicyList = append(iamPolicyList,
   150  		gocf.IAMRolePolicy{
   151  			PolicyDocument: ArbitraryJSONObject{
   152  				"Version":   "2012-10-17",
   153  				"Statement": statements,
   154  			},
   155  			PolicyName: gocf.String("S3SiteMgmnt"),
   156  		},
   157  	)
   158  
   159  	iamS3Role := &gocf.IAMRole{
   160  		AssumeRolePolicyDocument: AssumePolicyDocument,
   161  		Policies:                 &iamPolicyList,
   162  	}
   163  
   164  	iamRoleName := stableCloudformationResourceName("S3SiteIAMRole")
   165  	cfResource = template.AddResource(iamRoleName, iamS3Role)
   166  	cfResource.DependsOn = append(cfResource.DependsOn, s3BucketResourceName)
   167  	iamRoleRef := gocf.GetAtt(iamRoleName, "Arn")
   168  
   169  	// Create the IAM role and CustomAction handler to do the work
   170  
   171  	//////////////////////////////////////////////////////////////////////////////
   172  	// 4 - Create the lambda function definition that executes with the
   173  	// dynamically provisioned IAM policy.  This is similar to what happens in
   174  	// EnsureCustomResourceHandler, but due to the more complex IAM rules
   175  	// there's a bit of duplication
   176  	//	handlerName := lambdaExportNameForCustomResourceType(cloudformationresources.ZipToS3Bucket)
   177  	logger.WithFields(logrus.Fields{
   178  		"CustomResourceType": cfCustomResources.ZipToS3Bucket,
   179  	}).Debug("Sparta CloudFormation custom resource handler info")
   180  
   181  	// Since this is a custom resource command, stuff the type in the environment
   182  	userDispatchMap := map[string]*gocf.StringExpr{
   183  		EnvVarCustomResourceTypeName: gocf.String(cfCustomResources.ZipToS3Bucket),
   184  	}
   185  	lambdaEnv, lambdaEnvErr := lambdaFunctionEnvironment(userDispatchMap,
   186  		cfCustomResources.ZipToS3Bucket,
   187  		nil,
   188  		logger)
   189  	if lambdaEnvErr != nil {
   190  		return errors.Wrapf(lambdaEnvErr, "Failed to create S3 site resource")
   191  	}
   192  	customResourceHandlerDef := gocf.LambdaFunction{
   193  		Code: &gocf.LambdaFunctionCode{
   194  			S3Bucket: gocf.String(S3Bucket),
   195  			S3Key:    gocf.String(S3Key),
   196  		},
   197  		Description: gocf.String(customResourceDescription(serviceName,
   198  			"S3 static site")),
   199  		Handler:    gocf.String(binaryName),
   200  		Role:       iamRoleRef,
   201  		Runtime:    gocf.String(GoLambdaVersion),
   202  		MemorySize: gocf.Integer(256),
   203  		Timeout:    gocf.Integer(180),
   204  		// Let AWS assign the function name
   205  		/*
   206  			FunctionName: lambdaFunctionName.String(),
   207  		*/
   208  		Environment: lambdaEnv,
   209  	}
   210  	lambdaResourceName := stableCloudformationResourceName("S3SiteCreator")
   211  	cfResource = template.AddResource(lambdaResourceName, customResourceHandlerDef)
   212  	cfResource.DependsOn = append(cfResource.DependsOn,
   213  		s3BucketResourceName,
   214  		iamRoleName)
   215  
   216  	//////////////////////////////////////////////////////////////////////////////
   217  	// 5 - Create the custom resource that invokes the site bootstrapper lambda to
   218  	// actually populate the S3 with content
   219  	customResourceName := CloudFormationResourceName("S3SiteBuilder")
   220  	newResource, err := newCloudFormationResource(cfCustomResources.ZipToS3Bucket, logger)
   221  	if nil != err {
   222  		return errors.Wrapf(err, "Failed to create ZipToS3Bucket CustomResource")
   223  	}
   224  	zipResource, zipResourceOK := newResource.(*cfCustomResources.ZipToS3BucketResource)
   225  	if !zipResourceOK {
   226  		return errors.Errorf("Failed to type assert *cfCustomResources.ZipToS3BucketResource custom resource")
   227  	}
   228  	zipResource.ServiceToken = gocf.GetAtt(lambdaResourceName, "Arn")
   229  	zipResource.SrcKeyName = gocf.String(S3ResourcesKey)
   230  	zipResource.SrcBucket = gocf.String(S3Bucket)
   231  	zipResource.DestBucket = gocf.Ref(s3BucketResourceName).String()
   232  
   233  	// Build the manifest data with any output info...
   234  	manifestData := make(map[string]interface{})
   235  	for eachKey, eachOutput := range apiGatewayOutputs {
   236  		manifestData[eachKey] = map[string]interface{}{
   237  			"Description": eachOutput.Description,
   238  			"Value":       eachOutput.Value,
   239  		}
   240  	}
   241  	if len(s3Site.UserManifestData) != 0 {
   242  		manifestData["userdata"] = s3Site.UserManifestData
   243  	}
   244  
   245  	zipResource.Manifest = manifestData
   246  	cfResource = template.AddResource(customResourceName, zipResource)
   247  	cfResource.DependsOn = append(cfResource.DependsOn,
   248  		lambdaResourceName,
   249  		s3BucketResourceName)
   250  
   251  	return nil
   252  }
   253  
   254  // NewS3Site returns a new S3Site pointer initialized with the
   255  // static resources at the supplied path.  If resources is a directory,
   256  // the contents will be recursively archived and used to populate
   257  // the new S3 bucket.
   258  func NewS3Site(resources string) (*S3Site, error) {
   259  	// We'll ensure its valid during the build step, since
   260  	// there could be a go:generate command in the source that
   261  	// actually builds it.
   262  	site := &S3Site{
   263  		resources:        resources,
   264  		UserManifestData: map[string]interface{}{},
   265  	}
   266  	return site, nil
   267  }