cloudflare/Stout
Publicmirrored fromhttps://github.com/cloudflare/Stout
src/admin.go
316lines · modecode
11 years ago
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "os" |
| 6 | "os/exec" |
| 7 | "strings" |
| 8 | |
| 9 | "code.google.com/p/go.net/publicsuffix" |
| 10 | "github.com/zackbloom/goamz/cloudfront" |
| 11 | "github.com/zackbloom/goamz/iam" |
| 12 | "github.com/zackbloom/goamz/route53" |
| 13 | "github.com/zackbloom/goamz/s3" |
| 14 | "golang.org/x/crypto/ssh/terminal" |
| 15 | ) |
| 16 | |
| 17 | func CreateBucket(options Options) error { |
| 18 | bucket := s3Session.Bucket(options.Bucket) |
| 19 | |
| 20 | err := bucket.PutBucket("public-read") |
| 21 | // TODO Ignore found error |
| 22 | if err != nil { |
| 23 | return err |
| 24 | } |
| 25 | |
| 26 | err = bucket.PutBucketWebsite(s3.WebsiteConfiguration{ |
| 27 | IndexDocument: &s3.IndexDocument{"index.html"}, |
| 28 | ErrorDocument: &s3.ErrorDocument{"error.html"}, |
| 29 | }) |
| 30 | if err != nil { |
| 31 | return err |
| 32 | } |
| 33 | |
| 34 | err = bucket.PutPolicy([]byte(`{ |
| 35 | "Version": "2008-10-17", |
| 36 | "Statement": [ |
| 37 | { |
| 38 | "Sid": "PublicReadForGetBucketObjects", |
| 39 | "Effect": "Allow", |
| 40 | "Principal": { |
| 41 | "AWS": "*" |
| 42 | }, |
| 43 | "Action": "s3:GetObject", |
| 44 | "Resource": "arn:aws:s3:::` + options.Bucket + `/*" |
| 45 | } |
| 46 | ] |
| 47 | }`, |
| 48 | )) |
| 49 | if err != nil { |
| 50 | return err |
| 51 | } |
| 52 | |
| 53 | return nil |
| 54 | } |
| 55 | |
| 56 | func GetDistribution(options Options) (dist cloudfront.DistributionSummary, err error) { |
| 57 | distP, err := cfSession.FindDistributionByAlias(options.Bucket) |
| 58 | if err != nil { |
| 59 | return |
| 60 | } |
| 61 | |
| 62 | if distP != nil { |
| 63 | fmt.Println("CloudFront distribution found with the provided bucket name, assuming config matches.") |
| 64 | fmt.Println("If you run into issues, delete the distribution and rerun this command.") |
| 65 | |
| 66 | dist = *distP |
| 67 | return |
| 68 | } |
| 69 | |
| 70 | conf := cloudfront.DistributionConfig{ |
| 71 | Origins: cloudfront.Origins{ |
| 72 | cloudfront.Origin{ |
| 73 | Id: "S3-" + options.Bucket, |
| 74 | DomainName: options.Bucket + ".s3-website-" + options.AWSRegion + ".amazonaws.com", |
| 75 | CustomOriginConfig: &cloudfront.CustomOriginConfig{ |
| 76 | HTTPPort: 80, |
| 77 | HTTPSPort: 443, |
| 78 | OriginProtocolPolicy: "http-only", |
| 79 | }, |
| 80 | }, |
| 81 | }, |
| 82 | DefaultRootObject: "index.html", |
| 83 | PriceClass: "PriceClass_All", |
| 84 | Enabled: true, |
| 85 | DefaultCacheBehavior: cloudfront.CacheBehavior{ |
| 86 | TargetOriginId: "S3-" + options.Bucket, |
| 87 | ViewerProtocolPolicy: "allow-all", |
| 88 | AllowedMethods: cloudfront.AllowedMethods{ |
| 89 | Allowed: []string{"GET", "HEAD"}, |
| 90 | Cached: []string{"GET", "HEAD"}, |
| 91 | }, |
| 92 | }, |
| 93 | ViewerCertificate: &cloudfront.ViewerCertificate{ |
| 94 | CloudFrontDefaultCertificate: true, |
| 95 | MinimumProtocolVersion: "TLSv1", |
| 96 | SSLSupportMethod: "sni-only", |
| 97 | }, |
| 98 | Aliases: cloudfront.Aliases{ |
| 99 | options.Bucket, |
| 100 | }, |
| 101 | } |
| 102 | |
| 103 | return cfSession.Create(conf) |
| 104 | } |
| 105 | |
| 106 | func CreateUser(options Options) (key iam.AccessKey, err error) { |
| 107 | name := options.Bucket + "_deploy" |
| 108 | |
| 109 | _, err = iamSession.CreateUser(name, "/") |
| 110 | if err != nil { |
| 111 | iamErr, ok := err.(*iam.Error) |
| 112 | if ok && iamErr.Code == "EntityAlreadyExists" { |
| 113 | err = nil |
| 114 | } else { |
| 115 | return |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | _, err = iamSession.PutUserPolicy(name, name, `{ |
| 120 | "Version": "2012-10-17", |
| 121 | "Statement": [ |
| 122 | { |
| 123 | "Effect": "Allow", |
| 124 | "Action": [ |
| 125 | "s3:DeleteObject", |
| 126 | "s3:ListBucket", |
| 127 | "s3:PutObject", |
| 128 | "s3:PutObjectAcl", |
| 129 | "s3:GetObject" |
| 130 | ], |
| 131 | "Resource": [ |
| 132 | "arn:aws:s3:::`+options.Bucket+`", "arn:aws:s3:::`+options.Bucket+`/*" |
| 133 | ] |
| 134 | } |
| 135 | ] |
| 136 | }`, |
| 137 | ) |
| 138 | if err != nil { |
| 139 | return |
| 140 | } |
| 141 | |
| 142 | keyResp, err := iamSession.CreateAccessKey(name) |
| 143 | if err != nil { |
| 144 | return |
| 145 | } |
| 146 | |
| 147 | return keyResp.AccessKey, nil |
| 148 | } |
| 149 | |
| 150 | func UpdateRoute(options Options, dist cloudfront.DistributionSummary) error { |
| 151 | zoneName, err := publicsuffix.EffectiveTLDPlusOne(options.Bucket) |
| 152 | if err != nil { |
| 153 | return err |
| 154 | } |
| 155 | |
| 156 | resp, err := r53Session.ListHostedZonesByName(zoneName, "", 100) |
| 157 | |
| 158 | if resp.IsTruncated { |
| 159 | panic("More than 100 zones in the account") |
| 160 | } |
| 161 | |
| 162 | // TODO: Figure out what happens when the zone isnt found |
| 163 | noZone := false |
| 164 | // END |
| 165 | |
| 166 | if noZone { |
| 167 | fmt.Printf("A Route 53 hosted zone was not found for %s", zoneName) |
| 168 | if zoneName != options.Bucket { |
| 169 | fmt.Println("If you would like to use Route 53 to manage your DNS, create a zone for this domain, and update your registrar's configuration to point to the DNS servers Amazon provides and rerun this command. Note that you must copy any existing DNS configuration you have to Route 53 if you do not wish existing services hosted on this domain to stop working.") |
| 170 | fmt.Printf("If you would like to continue to use your existing DNS, create a CNAME record pointing %s to %s and the site setup will be finished.", options.Bucket, dist.DomainName) |
| 171 | } else { |
| 172 | fmt.Println("Since you are hosting the root of your domain, using an alternative DNS host is unfortunately not possible.") |
| 173 | fmt.Println("If you wish to host your site at the root of your domain, you must switch your sites DNS to Amazon's Route 53 and retry this command.") |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | if err != nil { |
| 178 | return err |
| 179 | } |
| 180 | |
| 181 | if len(resp.HostedZones) > 1 { |
| 182 | panic("Multiple matching hosted zones found") |
| 183 | } |
| 184 | if len(resp.HostedZones) == 0 { |
| 185 | panic("Hosted zone not listed") |
| 186 | } |
| 187 | |
| 188 | zone := resp.HostedZones[0] |
| 189 | |
| 190 | fmt.Printf("Adding %s to %s Route 53 zone\n", options.Bucket, zone.Name) |
| 191 | parts := strings.Split(zone.Id, "/") |
| 192 | idValue := parts[2] |
| 193 | |
| 194 | _, err = r53Session.ChangeResourceRecordSet(&route53.ChangeResourceRecordSetsRequest{ |
| 195 | Changes: []route53.Change{ |
| 196 | route53.Change{ |
| 197 | Action: "CREATE", |
| 198 | Name: options.Bucket, |
| 199 | Type: "A", |
| 200 | AliasTarget: route53.AliasTarget{ |
| 201 | HostedZoneId: "Z2FDTNDATAQYW2", |
| 202 | DNSName: dist.DomainName, |
| 203 | EvaluateTargetHealth: false, |
| 204 | }, |
| 205 | }, |
| 206 | }, |
| 207 | }, idValue) |
| 208 | |
| 209 | if err != nil { |
| 210 | if strings.Contains(err.Error(), "it already exists") { |
| 211 | fmt.Println("Existing route found, assuming it is correct") |
| 212 | fmt.Printf("If you run into trouble, you may need to delete the %s route in Route53 and try again\n", options.Bucket) |
| 213 | return nil |
| 214 | } |
| 215 | return err |
| 216 | } |
| 217 | |
| 218 | return nil |
| 219 | } |
| 220 | |
| 221 | func Create(options Options) { |
| 222 | if s3Session == nil { |
| 223 | s3Session = openS3(options.AWSKey, options.AWSSecret, options.AWSRegion) |
| 224 | } |
| 225 | if iamSession == nil { |
| 226 | iamSession = openIAM(options.AWSKey, options.AWSSecret, options.AWSRegion) |
| 227 | } |
| 228 | if r53Session == nil { |
| 229 | r53Session = openRoute53(options.AWSKey, options.AWSSecret) |
| 230 | } |
| 231 | if cfSession == nil { |
| 232 | cfSession = openCloudFront(options.AWSKey, options.AWSSecret) |
| 233 | } |
| 234 | |
| 235 | _, err := exec.LookPath("aws") |
| 236 | if err != nil { |
| 237 | fmt.Println("The aws CLI executable was not found in the PATH") |
| 238 | fmt.Println("Install it from http://aws.amazon.com/cli/ and try again") |
| 239 | } |
| 240 | |
| 241 | fmt.Println("Creating Bucket") |
| 242 | err = CreateBucket(options) |
| 243 | |
| 244 | if err != nil { |
| 245 | fmt.Println("Error creating S3 bucket") |
| 246 | fmt.Println(err) |
| 247 | return |
| 248 | } |
| 249 | |
| 250 | fmt.Println("Loading/Creating CloudFront Distribution") |
| 251 | dist, err := GetDistribution(options) |
| 252 | |
| 253 | if err != nil { |
| 254 | fmt.Println("Error loading/creating CloudFront distribution") |
| 255 | fmt.Println(err) |
| 256 | return |
| 257 | } |
| 258 | |
| 259 | fmt.Println("Adding Route") |
| 260 | err = UpdateRoute(options, dist) |
| 261 | |
| 262 | if err != nil { |
| 263 | fmt.Println("Error adding route to Route53 DNS config") |
| 264 | fmt.Println(err) |
| 265 | return |
| 266 | } |
| 267 | |
| 268 | key, err := CreateUser(options) |
| 269 | |
| 270 | if err != nil { |
| 271 | fmt.Println("Error creating user") |
| 272 | fmt.Println(err) |
| 273 | return |
| 274 | } |
| 275 | |
| 276 | fmt.Println("An access key has been created with just the permissions required to deploy / rollback this site") |
| 277 | fmt.Println("It is strongly recommended you use this limited account to deploy this project in the future\n") |
| 278 | fmt.Printf("ACCESS_KEY_ID=%s\n", key.Id) |
| 279 | fmt.Printf("ACCESS_KEY_SECRET=%s\n\n", key.Secret) |
| 280 | |
| 281 | if terminal.IsTerminal(int(os.Stdin.Fd())) { |
| 282 | fmt.Println(`You can either add these credentials to the deploy.yaml file, |
| 283 | or specify them as arguments to the stout deploy / stout rollback commands. |
| 284 | You MUST NOT add them to the deploy.yaml file if this project is public |
| 285 | (i.e. a public GitHub repo). |
| 286 | |
| 287 | If you can't add them to the deploy.yaml file, you can specify them as |
| 288 | arguments on the command line. If you use a build system like CircleCI, you |
| 289 | can add them as environment variables and pass those variables to the deploy |
| 290 | commands (see the README). |
| 291 | |
| 292 | Your first deploy command might be: |
| 293 | |
| 294 | stout deploy --bucket ` + options.Bucket + ` --key ` + key.Id + ` --secret '` + key.Secret + `' |
| 295 | `) |
| 296 | } |
| 297 | |
| 298 | fmt.Println("You can begin deploying now, but it can take up to ten minutes for your site to begin to work") |
| 299 | fmt.Println("Depending on the configuration of your site, you might need to set the 'root', 'dest' or 'files' options to get your deploys working as you wish. See the README for details.") |
| 300 | fmt.Println("It's also a good idea to look into the 'env' option, as in real-world situations it usually makes sense to have a development and/or staging site for each of your production sites.") |
| 301 | } |
| 302 | |
| 303 | func createCmd() { |
| 304 | options, _ := parseOptions() |
| 305 | loadConfigFile(&options) |
| 306 | |
| 307 | if options.Bucket == "" { |
| 308 | panic("You must specify a bucket") |
| 309 | } |
| 310 | |
| 311 | if options.AWSKey == "" || options.AWSSecret == "" { |
| 312 | panic("You must specify your AWS credentials") |
| 313 | } |
| 314 | |
| 315 | Create(options) |
| 316 | } |