├── .gitignore
├── .travis.yml
├── README.md
├── index.html
└── src
├── acm.go
├── cloudfront.go
├── deploy.go
├── init.go
├── route53.go
├── s3.go
├── scarr.go
└── version.go
/.gitignore:
--------------------------------------------------------------------------------
1 | foobar
2 | scarr
3 | .DS_STore
4 | scarr.yml
5 | .vscode
6 | test
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - "1.10"
4 | script:
5 | - go test -v ./...
6 | - export BUILD_DATE=$(date -Iseconds)
7 | - cp src/*.go .
8 | - GOOS=linux GOARCH=amd64 go build -ldflags "-X main.BuildDate=$BUILD_DATE" -o "dist/scarr-linux"
9 | - GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.BuildDate=$BUILD_DATE" -o "dist/scarr-macos"
10 | - GOOS=windows GOARCH=amd64 go build -ldflags "-X main.BuildDate=$BUILD_DATE" -o "dist/scarr-windows.exe"
11 | - echo $TRAVIS_BUILD_DIR
12 | - ./dist/scarr-linux --version
13 | deploy:
14 | provider: releases
15 | api_key: ${GITHUB_OATH_TOKEN}
16 | skip_cleanup: true
17 | file:
18 | - dist/scarr-linux
19 | - dist/scarr-macos
20 | - dist/scarr-windows.exe
21 | on:
22 | tags: true
23 | branch: kk_release_test
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/kkuchta/scarr)
2 |
3 | # Scarr
4 |
5 | - **S** 3
6 | - **C** loudfront
7 | - **A** CM
8 | - **R** oute53
9 | - **R** edundant letter to prevent name collisions
10 |
11 | If you want to set up a production-grade flat file site, a reasonable way to accomplish this would be to load your files to S3, put cloudfront in front of that for caching, use route53 for domain registration + DNS, and ACM for your TLS certificate. This tool automates all of that from registering the domain to uploading files.
12 |
13 | It works like this:
14 |
15 | ```
16 | $ scarr init -domain nogood.reisen -name nogoodreisen
17 | Initializing...done
18 | $ cd nogoodreisen/
19 | $ mvim scarr.yml # Edit a few fields here
20 | $ echo "hello world" > index.html
21 | $ AWS_PROFILE=scarr scarr deploy
22 | ... a bunch of aws stuff happens automatically ...
23 | $ curl https://nogood.reisen
24 | hullo wold
25 | ```
26 | The `deploy` command does the following:
27 |
28 | 1. Registers the given domain through route53 (you'll be prompted to confirm this)
29 | 2. Creates a TLS certificate through ACM
30 | 3. Uses route53 DNS to validate that certificate
31 | 4. Creates an S3 bucket
32 | 5. Creates a cloudfront distribution pointed to that S3 bucket using the ACM certificate
33 | 6. Creates an apex dns record pointing to that cloudfront
34 | 7. Syncs the current directory to that S3 bucket and invalidates the cloudfront cache.
35 |
36 | TLDR: Cheap, painless, fast, bulletproof flatfile sites with https and an apex domain.
37 |
38 | # Quickstart
39 | 1. Download the binary from [github.com/kkuchta/scarr/releases](https://github.com/kkuchta/scarr/releases)
40 | 2. Set up an aws user with the permissions listed under "Configure" below
41 | 3. Run `scarr init -domain domainyouwant.com -name mycoolproject` and cd into the generated directory
42 | 4. Create an index.html page in that directory
43 | 5. Run `AWS_ACCESS_KEY_ID=your_access_key_here AWS_SECRET_KEY_ID=your_secret_key_here scarr deploy`
44 |
45 | And once scarr finishes deploying, your site should be live at https://domainyouwant.com
46 |
47 | # Installation
48 |
49 | Scarr is distributed as a simple binary that you can download [here](github.com/kkuchta/scarr/releases).
50 |
51 | ```
52 | curl -L https://github.com/kkuchta/scarr/releases/download/v0.4.3/scarr-macos > scarr
53 | chmod +x scarr
54 | ./scarr init ...
55 | ```
56 |
57 | # Configure
58 |
59 | ### AWS Credentials
60 | You'll need an AWS IAM user with sufficient permissions. One way to do this is to go to to https://console.aws.amazon.com/iam/home and create a new policy with the following policy json:
61 | ```
62 | {
63 | "Version": "2012-10-17",
64 | "Statement": [
65 | {
66 | "Sid": "VisualEditor0",
67 | "Effect": "Allow",
68 | "Action": [
69 | "route53:CreateHostedZone",
70 | "s3:GetBucketWebsite",
71 | "route53:ListHostedZones",
72 | "cloudfront:GetInvalidation",
73 | "route53:ChangeResourceRecordSets",
74 | "s3:CreateBucket",
75 | "s3:ListBucket",
76 | "cloudfront:CreateDistribution",
77 | "route53domains:GetDomainDetail",
78 | "s3:GetBucketAcl",
79 | "cloudfront:CreateInvalidation",
80 | "route53domains:GetOperationDetail",
81 | "s3:PutObject",
82 | "s3:GetObject",
83 | "route53domains:CheckDomainAvailability",
84 | "s3:PutBucketWebsite",
85 | "acm:DescribeCertificate",
86 | "acm:RequestCertificate",
87 | "route53domains:RegisterDomain",
88 | "cloudfront:ListDistributions",
89 | "route53:ListResourceRecordSets",
90 | "s3:PutBucketAcl",
91 | "acm:ListCertificates",
92 | "s3:PutObjectAcl"
93 | ],
94 | "Resource": "*"
95 | }
96 | ]
97 | }
98 | ```
99 | Then create a new IAM user and attach that policy to it. Use that user's access key id and secret access key values to run scarr:
100 |
101 | `AWS_ACCESS_KEY_ID=your_access_key_here AWS_SECRET_KEY_ID=your_secret_key_here scarr deploy`
102 |
103 | Alternately, if you know how aws credentials files work, scarr supports supports the `AWS_PROFILE` environment variable as well.
104 |
105 | ### scarr.yml
106 |
107 | The scarr init command will generate a scarr.yml file with pretty much everything you need in it. You _will_ have to fill out the domainContact details if you want to use scarr for domain registration, though. The config options are as follows:
108 |
109 | - `domain: scarr.io` the domain of your site.
110 | - `name: scarr` used for a number of internal identifiers in the infrastructure, eg the bucket will be called `yourname-bucket`.
111 | - `region: us-west-1` the region to use for everything that's not either region independent (like route53) or that requires a specific region (ACM certs must be in us-east-1 to be used by cloudfront).
112 | - `exclude: ...` a list of regexes (_not_ glob patterns) to exclude from s3 upload. The regexes will get run against the full relative path of each file (eg if you've got file `/foo/bar/biz/baz` and you run scarr in `bar`, the regex gets run against `biz/baz`. Note that backslashes need to be escaped in yaml.
113 | ```
114 | exclude:
115 | - "src"
116 | - "\\.gitignore"
117 | - "\\.dat$"
118 | ```
119 | - `domainContact`: the contact info for domain registration. See the [aws docs](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register-values-specify.html) for more info. Most fields are accepted by aws so long as you input _something_, but contactType, countryCode, email, phone, state, and zip all have format validations.
120 | ```
121 | domainContact:
122 | address1: 'fillmein'
123 | address2: ''
124 | city: 'fillmein'
125 | contactType: 'PERSON'
126 | countryCode: 'US'
127 | email: 'kevin@kevinkuchta.com'
128 | firstName: 'fillmein'
129 | lastName: 'fillmein'
130 | phoneNumber: '+1.4157582482'
131 | state: 'CA'
132 | zipCode: '94117'
133 | ```
134 |
135 |
136 | # Commands
137 |
138 | ### Init
139 |
140 | `scarr init -domain example.com -name mycoolproject` generates a directory with a scarr.yml config file in it. It doesn't touch anything on AWS.
141 |
142 | ### Deploy
143 |
144 | `scarr deploy` should be run in a directory with a scarr.yml file in it. It checks whether your infrastructure (s3 bucket, cloudfront, etc) is already set up and if not, sets it up. It then syncs the current directory to S3 and invalidates the cloudfront cache. TODO: Actually _sync_. Right now it just copies all files to S3.
145 |
146 | - `-skip-setup` skips all the infrastructure setup and just does the S3 sync + cache invalidation. Scarr won't re-create your infrastructure if it already exists _anyway_, but this option prevents it from even checking the infrastructure, leading to slightly faster file syncs.
147 | - `-auto-register` causes scarr to automatically register the domain (rather than prompting for confirmation from the user) if it's not already in our route53 account and is available to register.
148 | - `-silent` runs scarr without any output except errors and the registration prompt (if -auto-register is off).
149 |
150 | # On the code
151 |
152 | Let's face it: this codebase is pretty ugly. The organization is a procedural mess, everything's in the same package, global functions and variables everywhere. Part of that is because this is literally the first golang code I've ever written, and part of it's because I thought this was going to be a 50-line shell script - I just got carried away and now here we are! I'll reorganize and clean everything up at some point.
153 |
154 | ### TODO:
155 | - Handle bad input better (eg init with no input gives useless error)
156 | - Handle the case where a domain is registered, but there's no hosted zone yet (eg just transferred in the domain from another registrar).
157 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/acm.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/aws/aws-sdk-go/aws"
5 | "github.com/aws/aws-sdk-go/aws/session"
6 | "github.com/aws/aws-sdk-go/service/acm"
7 | "os"
8 | "time"
9 | )
10 |
11 | func amcService() *acm.ACM {
12 | // ACM needs to do stuff in us-east-1 for cloudfront to work
13 | sess := session.Must(session.NewSession(&aws.Config{
14 | Region: aws.String("us-east-1")}))
15 | return acm.New(sess)
16 | }
17 |
18 | func getAcmCertificateARN(domain string) *string {
19 | service := amcService()
20 | listResult, err := service.ListCertificates(&acm.ListCertificatesInput{})
21 | dieOnError(err, "Failed to load acm certificates")
22 |
23 | for _, certSummary := range listResult.CertificateSummaryList {
24 | if *certSummary.DomainName == domain {
25 | return certSummary.CertificateArn
26 | }
27 | }
28 |
29 | return nil
30 | }
31 |
32 | func createACMCertificate(domain string) *string {
33 | service := amcService()
34 | requestResult, err := service.RequestCertificate(&acm.RequestCertificateInput{
35 | DomainName: &domain,
36 | SubjectAlternativeNames: aws.StringSlice([]string{"*." + domain}),
37 | ValidationMethod: aws.String("DNS"),
38 | })
39 | dieOnError(err, "Failed to request ACM certificate")
40 | setACMDNS(*requestResult.CertificateArn, domain)
41 |
42 | // TODO: wait until the acm cert lists as validated (might need to re-trigger something?)
43 | return requestResult.CertificateArn
44 | }
45 |
46 | func getCertificateValidation(certificateARN string) *acm.DomainValidation {
47 | service := amcService()
48 | describeResult, err := service.DescribeCertificate(&acm.DescribeCertificateInput{
49 | CertificateArn: &certificateARN,
50 | })
51 | dieOnError(err, "Failed to describe ACM certificate")
52 | return describeResult.Certificate.DomainValidationOptions[0]
53 | }
54 |
55 | func setACMDNS(certificateARN string, domain string) {
56 |
57 | var domainValidation *acm.DomainValidation
58 | // Right after certificate creation, validation status seems to be nil. Wait a bit.
59 | for i := 0; i < 5; i++ {
60 | domainValidation := getCertificateValidation(certificateARN)
61 | if domainValidation.ValidationStatus != nil {
62 | break
63 | } else {
64 | time.Sleep(5 * time.Second)
65 | }
66 | }
67 | // TODO: read up on go memory management so I don't have to do this again here to avoid segfaults
68 | domainValidation = getCertificateValidation(certificateARN)
69 |
70 | if *(domainValidation.ValidationStatus) == "PENDING_VALIDATION" {
71 | log("not yet valid; creating validation dns records...")
72 | dns := domainValidation.ResourceRecord
73 | // If the dns record already exists, we're just waiting for validation so don't try to recreate it.
74 | if !dnsRecordExists(getHostedZone(domain), *dns.Name, *dns.Type) {
75 | createDNSRecord(domain, *dns.Name, *dns.Type, dns.Value, nil)
76 | }
77 |
78 | log("waiting for validation (takes up to a few hours - feel free to ctrl-c and restart scarr later)...")
79 | time.Sleep(5 * time.Second)
80 |
81 | maxTries := 60 * 3
82 | for i := 0; i < maxTries; i++ {
83 | if *getCertificateValidation(certificateARN).ValidationStatus != "PENDING_VALIDATION" {
84 | setACMDNS(certificateARN, domain)
85 | break
86 | }
87 | if i == (maxTries - 1) {
88 | logln("\nTimed out waiting for ACM certificate to validate.")
89 | os.Exit(1)
90 | }
91 | time.Sleep(60 * time.Second)
92 | }
93 | } else if *domainValidation.ValidationStatus == "FAILED" {
94 | logln("Err! Cert validation failed!")
95 | os.Exit(1)
96 | } else {
97 | log("Certificate validated")
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/src/cloudfront.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/aws/aws-sdk-go/aws"
6 | "github.com/aws/aws-sdk-go/aws/awserr"
7 | "github.com/aws/aws-sdk-go/aws/session"
8 | "github.com/aws/aws-sdk-go/service/cloudfront"
9 | "os"
10 | "time"
11 | )
12 |
13 | func cloudFrontService() *cloudfront.CloudFront {
14 | sess := session.Must(session.NewSession(&aws.Config{}))
15 | return cloudfront.New(sess)
16 | }
17 |
18 | // Returns cloudfrontDomain, distId
19 | func getCloudfront(s3Domain string) (*string, *string) {
20 | service := cloudFrontService()
21 | result, err := service.ListDistributions(&cloudfront.ListDistributionsInput{})
22 | dieOnError(err, "Failed getting distribution list")
23 | if *result.DistributionList.IsTruncated {
24 | // If you have over 1k distributions
25 | fmt.Println("TODO: handle paginated result lists for cloudfront")
26 | os.Exit(1)
27 | }
28 |
29 | for _, dist := range result.DistributionList.Items {
30 | for _, origin := range dist.Origins.Items {
31 | if *origin.DomainName == s3Domain {
32 | // s3Url looks like:
33 | // voyage-found.s3-website-us-west-1.amazonaws.com
34 | return dist.DomainName, dist.Id
35 | }
36 | }
37 | }
38 | return nil, nil
39 | }
40 |
41 | func createCloudFront(s3Domain string, bucketName string, certificateArn string, domain string) *string {
42 |
43 | // Taking a break from this function to go set up ACM, since we'll need that ID
44 | service := cloudFrontService()
45 | originID := "S3-" + bucketName
46 | // s3Domain := bucketName + ".s3.amazonaws.com"
47 |
48 | aliases := cloudfront.Aliases{
49 | Items: aws.StringSlice([]string{domain}),
50 | Quantity: aws.Int64(1),
51 | }
52 |
53 | defaultCacheBehavior := cloudfront.DefaultCacheBehavior{
54 | AllowedMethods: &cloudfront.AllowedMethods{
55 | Items: aws.StringSlice([]string{"GET", "HEAD"}),
56 | Quantity: aws.Int64(2),
57 | CachedMethods: &cloudfront.CachedMethods{
58 | Items: aws.StringSlice([]string{"GET", "HEAD"}),
59 | Quantity: aws.Int64(2),
60 | },
61 | },
62 | Compress: aws.Bool(true),
63 | ForwardedValues: &cloudfront.ForwardedValues{
64 | Cookies: &cloudfront.CookiePreference{
65 | Forward: aws.String("none"),
66 | },
67 | QueryString: aws.Bool(false),
68 | },
69 | MinTTL: aws.Int64(0),
70 | TargetOriginId: &originID,
71 | TrustedSigners: &cloudfront.TrustedSigners{
72 | Enabled: aws.Bool(false),
73 | Quantity: aws.Int64(0),
74 | },
75 | ViewerProtocolPolicy: aws.String("redirect-to-https"),
76 | }
77 |
78 | // Custom-style origin.
79 | origin := cloudfront.Origin{
80 | CustomOriginConfig: &cloudfront.CustomOriginConfig{
81 | HTTPPort: aws.Int64(80),
82 | HTTPSPort: aws.Int64(443),
83 | OriginProtocolPolicy: aws.String("http-only"),
84 | },
85 | DomainName: &s3Domain,
86 | Id: &originID,
87 | }
88 | // S3-style origin
89 | // origin := cloudfront.Origin{
90 | // S3OriginConfig: &cloudfront.S3OriginConfig{
91 | // // Empty for now. Allows people to access s3 resources directly (which we don't care about)
92 | // OriginAccessIdentity: aws.String(""),
93 | // },
94 | // DomainName: &s3Domain,
95 | // Id: &originID,
96 | // }
97 |
98 | origins := cloudfront.Origins{
99 | Items: []*cloudfront.Origin{&origin},
100 | Quantity: aws.Int64(1),
101 | }
102 |
103 | certificate := cloudfront.ViewerCertificate{
104 | ACMCertificateArn: &certificateArn,
105 | SSLSupportMethod: aws.String("sni-only"),
106 | MinimumProtocolVersion: aws.String("TLSv1"),
107 | }
108 |
109 | callerReference := time.Now().Format(time.RFC850)
110 |
111 | config := cloudfront.DistributionConfig{
112 | Aliases: &aliases,
113 | CallerReference: &callerReference,
114 | Comment: aws.String("Created by Scarr.io"),
115 | DefaultCacheBehavior: &defaultCacheBehavior,
116 | CacheBehaviors: &cloudfront.CacheBehaviors{Quantity: aws.Int64(0)},
117 | Enabled: aws.Bool(true),
118 | CustomErrorResponses: &cloudfront.CustomErrorResponses{Quantity: aws.Int64(0)},
119 | PriceClass: aws.String("PriceClass_All"),
120 | Restrictions: &cloudfront.Restrictions{
121 | GeoRestriction: &cloudfront.GeoRestriction{
122 | RestrictionType: aws.String("none"),
123 | Quantity: aws.Int64(0),
124 | },
125 | },
126 | Origins: &origins,
127 | ViewerCertificate: &certificate,
128 | }
129 | createResult, err := service.CreateDistribution(&cloudfront.CreateDistributionInput{DistributionConfig: &config})
130 |
131 | if err != nil {
132 | awserror := err.(awserr.Error)
133 | fmt.Println("code=", awserror.Code())
134 | fmt.Println(awserror.Message())
135 | }
136 |
137 | dieOnError(err, "Failed to create cloudfront distribution")
138 |
139 | log("Waiting for distribution to finish (20-40 minutes)...")
140 | service.WaitUntilDistributionDeployed(&cloudfront.GetDistributionInput{
141 | Id: createResult.Distribution.Id,
142 | })
143 | logln(" done")
144 | return createResult.Distribution.DomainName
145 | }
146 |
147 | func createCloudfrontInvalidation(s3Url string, paths []string) {
148 | _, distributionID := getCloudfront(s3Url)
149 | service := cloudFrontService()
150 | callerReference := time.Now().Format(time.RFC850)
151 | log("Invalidating cache...")
152 | invalidationResult, err := service.CreateInvalidation(&cloudfront.CreateInvalidationInput{
153 | DistributionId: distributionID,
154 | InvalidationBatch: &cloudfront.InvalidationBatch{
155 | CallerReference: &callerReference,
156 | Paths: &cloudfront.Paths{
157 | Items: aws.StringSlice(paths),
158 | Quantity: aws.Int64(int64(len(paths))),
159 | },
160 | },
161 | })
162 | dieOnError(err, "Failed to create Invalidation")
163 |
164 | log("waiting (5-10 minutes)...")
165 | service.WaitUntilInvalidationCompleted(&cloudfront.GetInvalidationInput{
166 | DistributionId: distributionID,
167 | Id: invalidationResult.Invalidation.Id,
168 | })
169 | logln(" done")
170 | }
171 |
--------------------------------------------------------------------------------
/src/deploy.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "strings"
9 |
10 | "gopkg.in/yaml.v2"
11 | )
12 |
13 | type contactDetailsType struct {
14 | Address1 string `yaml:"address1"`
15 | Address2 string `yaml:"address2"`
16 | City string `yaml:"city"`
17 | ContactType string `yaml:"contactType"`
18 | CountryCode string `yaml:"countryCode"`
19 | Email string `yaml:"email"`
20 | FirstName string `yaml:"firstName"`
21 | LastName string `yaml:"lastName"`
22 | PhoneNumber string `yaml:"phoneNumber"`
23 | State string `yaml:"state"`
24 | ZipCode string `yaml:"zipCode"`
25 | }
26 |
27 | type configType struct {
28 | Domain string `yaml:"domain"`
29 | Name string `yaml:"name"`
30 | Region string `yaml:"region"`
31 | DomainContact contactDetailsType `yaml:"domainContact"`
32 | Exclude []string `yaml:"exclude"`
33 | }
34 |
35 | func dieOnError(err error, message string) {
36 | if err != nil {
37 | fmt.Println(message, err)
38 | os.Exit(1)
39 | }
40 | }
41 |
42 | func confirm(message string) bool {
43 | fmt.Print(message + " [y/N]")
44 |
45 | reader := bufio.NewReader(os.Stdin)
46 | text, err := reader.ReadString('\n')
47 | fmt.Println("")
48 | dieOnError(err, "Failed reading y/n input")
49 | return "y" == strings.TrimSpace(strings.ToLower(text))
50 | }
51 |
52 | func getConfig() configType {
53 | yamlFile, err := ioutil.ReadFile("scarr.yml")
54 | dieOnError(err, "Error reading scarr.yml")
55 |
56 | var config configType
57 | err = yaml.Unmarshal(yamlFile, &config)
58 | dieOnError(err, "Error parsing scarr.yml")
59 |
60 | return config
61 | }
62 |
63 | // Gets the root domain (eg foo.com from bar.foo.com).
64 | func getRootDomain(domain string) string {
65 | domainParts := strings.Split(domain, ".")
66 | return domainParts[len(domainParts)-2] + "." + domainParts[len(domainParts)-1]
67 | }
68 |
69 | func ensureDomainRegistered(config configType, autoRegister bool) {
70 | // We can't register a subdomain, so let's check registration on the main domain instead
71 | domain := getRootDomain(config.Domain)
72 | log("Checking domain " + domain + " registration...")
73 |
74 | domainDetail := getDomainDetails(domain)
75 | if domainDetail == nil {
76 | logln("\nNot registered in our Route53")
77 |
78 | // Not clear if there's a good way to detect this
79 | // if isRegistering(domain) {
80 | // fmt.Println("Your domain is still registering. Try again later.")
81 | // return
82 | // }
83 |
84 | domainAvailability := getDomainAvailability(domain)
85 | if domainAvailability {
86 | logln(`
87 | But it *is* available to register. For current prices, see the document linked at:
88 | https://aws.amazon.com/route53/pricing/
89 | `)
90 | if strings.HasSuffix(domain, ".com") {
91 | logln("(As of April 2018, .com TLDs were $12/yr)")
92 | }
93 | if autoRegister || confirm("Register that domain?") {
94 | registerDomain(domain, config.DomainContact)
95 | }
96 | } else {
97 | fmt.Println(`
98 | Unfortunately that domain is not available to register. Maybe it's still
99 | registering from the last time you ran scarr? If so, try again in a few.
100 | If you own that domain through a different registrar, transfer it to
101 | route53. Alternately, use both --skip-dns and --skip-domain to bypass
102 | this (you'll have to manage your own domain + dns setup then)
103 | (//TODO: implement those flags)`)
104 | os.Exit(1)
105 | }
106 | } else {
107 | logln("Looks good!")
108 | }
109 | }
110 | func ensureS3BucketExists(s3BucketName string, region string) {
111 | logf("Checking bucket %v...", s3BucketName)
112 | if !bucketExists(s3BucketName, region) {
113 | log(" bucket doesn't exist; creating it now...")
114 | createBucket(s3BucketName, region)
115 | } else {
116 | log(" bucket already exists.")
117 | }
118 |
119 | // if !bucketIsWorldReadable(s3BucketName, region) {
120 | // // We could _make_ this bucket world-readable, but that'd be bad if it turns out to have sensitive info in it.
121 | // logln("\nBucket is not world-readable. You should fix this (or delete the bucket and let us re-create it).")
122 | // os.Exit(1)
123 | // }
124 | logln(" done")
125 | ensureBucketIsWebsite(s3BucketName, region)
126 | }
127 |
128 | func ensureACMCertificate(domain string) string {
129 | logf("Checking ACM cert for %v...", domain)
130 | certificateArn := getAcmCertificateARN(domain)
131 | if certificateArn == nil {
132 | log("doesn't exist; creating...")
133 | certificateArn = createACMCertificate(domain)
134 | } else {
135 | // Ensure it's DNS is set up
136 | log("already exists; ensuring it's validated...")
137 | setACMDNS(*certificateArn, domain)
138 | }
139 | logln(" done")
140 | return *certificateArn
141 | }
142 | func ensureCloudFrontExists(certificateArn string, s3Url string, s3Bucket string, domain string) string {
143 | cloudfrontDomain, _ := getCloudfront(s3Url)
144 | if cloudfrontDomain == nil {
145 | logln("CloudFront distribution does not exist; creating")
146 | cloudfrontDomain = createCloudFront(s3Url, s3Bucket, certificateArn, domain)
147 | }
148 | return *cloudfrontDomain
149 | }
150 | func ensureDomainPointingToCloudfront(cloudfrontDomain string, mainDomain string) {
151 | hostedZoneID := getHostedZone(mainDomain)
152 | if dnsRecordExists(hostedZoneID, mainDomain, "A") {
153 | logln("Domain has a (hopefully-correct) alias already configured")
154 | } else {
155 | logln("Creating A-record alias to domain")
156 | createAliasRecord(mainDomain, mainDomain, cloudfrontDomain)
157 | }
158 |
159 | // TODO: set up an alias or redirect from www to apex
160 | }
161 |
162 | func invalidateCloudfront(s3Domain string, pathsToInvalidate []string) {
163 | // TODO: actually invalidate what's passed in
164 | createCloudfrontInvalidation(s3Domain, []string{"/*"})
165 | }
166 |
167 | func runDeploy(skipSetup bool, autoRegister bool) {
168 | logln("Deploying")
169 | config := getConfig()
170 | s3Bucket := config.Name + "-bucket"
171 | s3Url := s3Bucket + ".s3-website-" + config.Region + ".amazonaws.com"
172 |
173 | if !skipSetup {
174 | ensureDomainRegistered(config, autoRegister)
175 | certArn := ensureACMCertificate(config.Domain)
176 | ensureS3BucketExists(s3Bucket, config.Region)
177 | cloudfrontDomain := ensureCloudFrontExists(certArn, s3Url, s3Bucket, config.Domain)
178 | ensureDomainPointingToCloudfront(cloudfrontDomain, config.Domain)
179 | }
180 |
181 | changedFiles := s3Sync(config.Region, s3Bucket, &config.Exclude)
182 | invalidateCloudfront(s3Url, changedFiles)
183 |
184 | logf("Deployed to https://%v", config.Domain)
185 | }
186 |
--------------------------------------------------------------------------------
/src/init.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "text/template"
9 | )
10 |
11 | const configTemplateString = `domain: "{{.domain}}"
12 | name: "{{.name}}"
13 | region: "{{.region}}"
14 |
15 | # This section's only used if you use scarr to register a domain. Which fields
16 | # are required depends on what TLD you register. See
17 | # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register-values-specify.html
18 | # for details.
19 | domainContact:
20 | address1: 'fillmein'
21 | address2: ''
22 | city: 'fillmein'
23 | contactType: 'PERSON'
24 | countryCode: 'fillmein'
25 | email: 'fillmein'
26 | firstName: 'fillmein'
27 | lastName: 'fillmein'
28 | phoneNumber: 'fillmein'
29 | state: 'fillmein'
30 | zipCode: 'fillmein'
31 |
32 | # A list of regexes to be run against paths in the current directory. Any file path matching any of these regexes will not be synced to s3
33 | exclude:
34 | - "scarr\\.yml"
35 | - "^\\.git"
36 | - "\\.DS_Store"
37 | `
38 |
39 | func generateConfig(domain string, name string, region string) string {
40 | configTemplate := template.Must(template.New("config").Parse(configTemplateString))
41 | buffer := &bytes.Buffer{}
42 | data := map[string]interface{}{
43 | "name": name,
44 | "domain": domain,
45 | "region": region,
46 | }
47 | check(configTemplate.Execute(buffer, data))
48 |
49 | return buffer.String()
50 | }
51 |
52 | func writeFile(path string, content string) {
53 | check(ioutil.WriteFile(path, []byte(content), 0644))
54 | }
55 |
56 | func runInit(domain string, name string, region string) {
57 | log("Initializing...")
58 | err := os.Mkdir(name, 0755)
59 | if err != nil {
60 | fmt.Println(err)
61 | os.Exit(1)
62 | }
63 | config := generateConfig(domain, name, region)
64 | writeFile(name+"/scarr.yml", config)
65 | logln("done")
66 | logln("You'll need to edit scarr.yml to fill in contact details if you want to use scarr register domain names.")
67 | }
68 |
--------------------------------------------------------------------------------
/src/route53.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/aws/aws-sdk-go/aws"
6 | "github.com/aws/aws-sdk-go/aws/awserr"
7 | "github.com/aws/aws-sdk-go/aws/session"
8 | "github.com/aws/aws-sdk-go/service/route53"
9 | "github.com/aws/aws-sdk-go/service/route53domains"
10 | "os"
11 | "strings"
12 | "time"
13 | )
14 |
15 | func route53DomainsService() *route53domains.Route53Domains {
16 | // Route53 only has the one domain, so hardcode to us east
17 | sess := session.Must(session.NewSession(&aws.Config{
18 | Region: aws.String("us-east-1")}))
19 | return route53domains.New(sess)
20 | }
21 | func route53Service() *route53.Route53 {
22 | // Route53 only has the one domain, so hardcode to us east
23 | sess := session.Must(session.NewSession(&aws.Config{
24 | Region: aws.String("us-east-1")}))
25 | return route53.New(sess)
26 | }
27 |
28 | func registerDomain(domain string, contactDetails contactDetailsType) {
29 | logf("Registering %v...", domain)
30 | contact := route53domains.ContactDetail{
31 | AddressLine1: &contactDetails.Address1,
32 | AddressLine2: &contactDetails.Address2,
33 | City: &contactDetails.City,
34 | ContactType: &contactDetails.ContactType,
35 | CountryCode: &contactDetails.CountryCode,
36 | Email: &contactDetails.Email,
37 | FirstName: &contactDetails.FirstName,
38 | LastName: &contactDetails.LastName,
39 | PhoneNumber: &contactDetails.PhoneNumber,
40 | State: &contactDetails.State,
41 | ZipCode: &contactDetails.ZipCode,
42 | }
43 | autoRenew := false
44 | duration := int64(1)
45 | input := route53domains.RegisterDomainInput{
46 | AdminContact: &contact,
47 | RegistrantContact: &contact,
48 | TechContact: &contact,
49 | AutoRenew: &autoRenew,
50 | DomainName: &domain,
51 | DurationInYears: &duration,
52 | }
53 | route53DomainsService := route53DomainsService()
54 | result, err := route53DomainsService.RegisterDomain(&input)
55 | dieOnError(err, "Failed to register domain")
56 |
57 | operationInput := route53domains.GetOperationDetailInput{
58 | OperationId: result.OperationId,
59 | }
60 |
61 | // Wait up to 60 minutes for registration
62 | for i := 0; i < 60; i++ {
63 | operationResult, err := route53DomainsService.GetOperationDetail(&operationInput)
64 | dieOnError(err, "Failed to get registration operation (but probably still registered the domain)")
65 | if *operationResult.Status == "SUCCESSFUL" {
66 | logln(" done")
67 | return
68 | } else if *operationResult.Status == "FAILED" {
69 | logln("Domain registration failed")
70 | os.Exit(1)
71 | }
72 | time.Sleep(60 * time.Second)
73 | }
74 | logln("\nTimed out waiting for registration to finish. It may yet succeed - check your aws console.")
75 | }
76 |
77 | func getDomainDetails(domain string) *route53domains.GetDomainDetailOutput {
78 | route53DomainsService := route53DomainsService()
79 |
80 | // So, apparently the only way to determine if a domain exists in our route53
81 | // is to try to fetch it and see if we get a specific error. Well, ok, we could
82 | // call ListDomains, but then we'd have to paginate through an arbitrarily
83 | // large list which is just silly.
84 | input := route53domains.GetDomainDetailInput{DomainName: &domain}
85 | result, err := route53DomainsService.GetDomainDetail(&input)
86 | if err != nil {
87 | if strings.Contains(err.Error(), "Domain "+domain+" not found in") {
88 | return nil
89 | }
90 | dieOnError(err, "error loading domain: ")
91 | }
92 |
93 | // TODO: handle domain-found result
94 | //fmt.Println("get domain details result = ", result)
95 | return result
96 | }
97 |
98 | func getDomainAvailability(domain string) bool {
99 | return getDomainAvailabilityWithRetries(domain, 3)
100 | }
101 |
102 | func getDomainAvailabilityWithRetries(domain string, retries int) bool {
103 |
104 | route53DomainsService := route53DomainsService()
105 | input := route53domains.CheckDomainAvailabilityInput{DomainName: &domain}
106 | availabilityResult, err := route53DomainsService.CheckDomainAvailability(&input)
107 | dieOnError(err, "error getting domain availability")
108 | if *availabilityResult.Availability == "AVAILABLE" {
109 | return true
110 | }
111 | if *availabilityResult.Availability == "PENDING" {
112 | if retries > 0 {
113 | time.Sleep(time.Second)
114 | return getDomainAvailabilityWithRetries(domain, retries-1)
115 | }
116 | }
117 | return false
118 | }
119 |
120 | func dnsRecordExists(hostedZoneID string, domain string, recordType string) bool {
121 | service := route53Service()
122 | result, err := service.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{
123 | HostedZoneId: &hostedZoneID,
124 | })
125 | dieOnError(err, "Failed listing resource record sets")
126 |
127 | for _, recordSet := range result.ResourceRecordSets {
128 | if *recordSet.Name == domain+"." && *recordSet.Type == recordType {
129 | return true
130 | }
131 | }
132 | return false
133 | }
134 |
135 | func getHostedZone(domain string) string {
136 | rootDomain := getRootDomain(domain)
137 | service := route53Service()
138 |
139 | hostedZoneID := ""
140 |
141 | hostedZonesList, err := service.ListHostedZones(&route53.ListHostedZonesInput{})
142 | dieOnError(err, "Failed to list hosted zones")
143 |
144 | for _, hostedZone := range hostedZonesList.HostedZones {
145 | if *hostedZone.Name == rootDomain+"." {
146 | hostedZoneID = *hostedZone.Id
147 | }
148 | }
149 |
150 | if hostedZoneID == "" {
151 | // TODO: we can probably just create the hosted zone in this case
152 | logln("Couldn't find hosted zone for domain")
153 | os.Exit(1)
154 | }
155 | return hostedZoneID
156 | }
157 |
158 | func createAliasRecord(hostedZoneDomain string, recordName string, cloudfrontDomain string) {
159 | createDNSRecord(hostedZoneDomain, recordName, "A", nil, &route53.AliasTarget{
160 | DNSName: &cloudfrontDomain,
161 | EvaluateTargetHealth: aws.Bool(false),
162 | HostedZoneId: aws.String("Z2FDTNDATAQYW2"),
163 | })
164 | // ^ Hardcoded zone ID as specified in aws docs
165 | }
166 |
167 | func createDNSRecord(domain string, recordName string, recordType string, recordValue *string, aliasTarget *route53.AliasTarget) {
168 | // fmt.Println("Creating record of type", recordType, recordName, recordValue)
169 | service := route53Service()
170 |
171 | hostedZoneID := getHostedZone(domain)
172 |
173 | input := route53.ChangeResourceRecordSetsInput{
174 | HostedZoneId: &hostedZoneID,
175 | ChangeBatch: &route53.ChangeBatch{
176 | Comment: aws.String("Created by scarr.io"),
177 | Changes: []*route53.Change{
178 | {
179 | Action: aws.String("CREATE"),
180 | ResourceRecordSet: &route53.ResourceRecordSet{
181 | Name: &recordName,
182 | Type: &recordType,
183 | },
184 | },
185 | },
186 | },
187 | }
188 |
189 | if recordValue != nil {
190 | input.ChangeBatch.Changes[0].ResourceRecordSet.ResourceRecords = []*route53.ResourceRecord{
191 | {
192 | Value: recordValue,
193 | },
194 | }
195 | }
196 |
197 | // If we're setting an alias record, we need this extra input
198 | if aliasTarget != nil {
199 | input.ChangeBatch.Changes[0].ResourceRecordSet.AliasTarget = aliasTarget
200 | } else {
201 | // Maybe necessary for cert validation dns?
202 | input.ChangeBatch.Changes[0].ResourceRecordSet.TTL = aws.Int64(300)
203 | }
204 |
205 | _, err := service.ChangeResourceRecordSets(&input)
206 | if err != nil {
207 | awserror := err.(awserr.Error)
208 |
209 | fmt.Println("code=", awserror.Code())
210 | }
211 |
212 | dieOnError(err, "Failed to create dns record")
213 | }
214 |
--------------------------------------------------------------------------------
/src/s3.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/aws/aws-sdk-go/aws"
5 | "github.com/aws/aws-sdk-go/aws/awserr"
6 | "github.com/aws/aws-sdk-go/aws/session"
7 | "github.com/aws/aws-sdk-go/service/s3"
8 | "github.com/aws/aws-sdk-go/service/s3/s3manager"
9 | "mime"
10 | "net/http"
11 | "os"
12 | "path/filepath"
13 | "regexp"
14 | )
15 |
16 | func s3Service(region string) *s3.S3 {
17 | sess := session.Must(session.NewSession(&aws.Config{
18 | Region: aws.String(region)}))
19 | return s3.New(sess)
20 | }
21 | func s3ManagerService(region string) *s3manager.Uploader {
22 | sess := session.Must(session.NewSession(&aws.Config{
23 | Region: aws.String(region)}))
24 | return s3manager.NewUploader(sess)
25 | }
26 |
27 | func bucketExists(bucketName string, region string) bool {
28 | service := s3Service(region)
29 | _, err := service.HeadBucket(&s3.HeadBucketInput{Bucket: &bucketName})
30 |
31 | if err != nil {
32 | awsError := err.(awserr.Error)
33 | if awsError.Code() == "NotFound" {
34 | return false
35 | }
36 | dieOnError(err, "Error HEADing bucket")
37 | }
38 | return true
39 | }
40 |
41 | func bucketIsWorldReadable(bucketName string, region string) bool {
42 | service := s3Service(region)
43 | aclResult, err := service.GetBucketAcl(&s3.GetBucketAclInput{
44 | Bucket: &bucketName,
45 | })
46 | dieOnError(err, "Error getting bucket ACL")
47 |
48 | // Make sure this bucket is publicly readable
49 | for _, grant := range aclResult.Grants {
50 | if *grant.Grantee.Type == "Group" &&
51 | *grant.Grantee.URI == "http://acs.amazonaws.com/groups/global/AllUsers" &&
52 | *grant.Permission == "READ" {
53 | return true
54 | }
55 | }
56 | return false
57 | }
58 |
59 | func ensureBucketIsWebsite(bucketName string, region string) {
60 | service := s3Service(region)
61 | _, err := service.GetBucketWebsite(&s3.GetBucketWebsiteInput{Bucket: &bucketName})
62 | if err != nil {
63 | awsError := err.(awserr.Error)
64 | if awsError.Code() == "NoSuchWebsiteConfiguration" {
65 | log("Making S3 bucket website...")
66 | indexFile := "index.html"
67 | _, err = service.PutBucketWebsite(&s3.PutBucketWebsiteInput{
68 | Bucket: &bucketName,
69 | WebsiteConfiguration: &s3.WebsiteConfiguration{
70 | IndexDocument: &s3.IndexDocument{Suffix: &indexFile},
71 | },
72 | })
73 | dieOnError(err, "Failed to update s3 bucket website config")
74 | logln(" done")
75 | } else {
76 | dieOnError(err, "Failed to get bucket website config")
77 | }
78 | } else {
79 | logln("Bucket correctly configured for website")
80 | }
81 | }
82 |
83 | func createBucket(bucketName string, region string) {
84 | service := s3Service(region)
85 |
86 | input := s3.CreateBucketInput{
87 | Bucket: &bucketName,
88 | }
89 |
90 | // Apparently the aws api treats us-east-1 as the default for s3 buckets *and
91 | // throws an error* if you try to specify us-east-1 as the region. Wtf.
92 | if region != "us-east-1" {
93 | input.CreateBucketConfiguration = &s3.CreateBucketConfiguration{
94 | LocationConstraint: ®ion,
95 | }
96 | }
97 |
98 | _, err := service.CreateBucket(&input)
99 | dieOnError(err, "Failed to create bucket")
100 | }
101 |
102 | func s3Sync(region string, bucket string, configuredExclude *[]string) []string {
103 | service := s3ManagerService(region)
104 |
105 | fileList := []string{}
106 | err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
107 | if info.IsDir() {
108 | return nil
109 | }
110 |
111 | for _, exclude := range *configuredExclude {
112 | matched, err := regexp.MatchString(exclude, path)
113 | dieOnError(err, "Invalid exclude regex")
114 | if matched {
115 | return nil
116 | }
117 | }
118 |
119 | fileList = append(fileList, path)
120 | return nil
121 | })
122 | if err != nil {
123 | logf("walk error [%v]\n", err)
124 | }
125 |
126 | // TODO: detect differences and actually sync, rather than just overwriting everything
127 | for _, filename := range fileList {
128 | file, fileErr := os.Open(filename)
129 | dieOnError(fileErr, "Failed to open file")
130 |
131 | ext := filepath.Ext(filename)
132 |
133 | contentType := ""
134 |
135 | // Detect content type from the extension
136 | switch ext {
137 | case ".htm", ".html":
138 | contentType = "text/html"
139 | case ".css":
140 | contentType = "text/css"
141 | case ".js":
142 | contentType = "application/javascript"
143 | default:
144 | contentType = mime.TypeByExtension(ext)
145 | }
146 |
147 | // If we can't figure out content type from the extension, try DetectContentType
148 | if contentType == "" {
149 | // Grab the first 512 bytes to detect the content type
150 | buffer := make([]byte, 512)
151 | _, err = file.Read(buffer)
152 | dieOnError(err, "Failed reading start of file to detect content type for "+filename)
153 | // Reset the read pointer if necessary.
154 | file.Seek(0, 0)
155 | contentType = http.DetectContentType(buffer)
156 | }
157 |
158 | logln("Uploading ", filename, " to ", bucket)
159 | _, uploadErr := service.Upload(&s3manager.UploadInput{
160 | Bucket: aws.String(bucket),
161 | Key: aws.String(filename),
162 | Body: file,
163 | GrantRead: aws.String("uri=http://acs.amazonaws.com/groups/global/AllUsers"),
164 | ContentType: &contentType,
165 | })
166 | dieOnError(uploadErr, "Failed to upload file")
167 | }
168 | return fileList
169 | }
170 |
--------------------------------------------------------------------------------
/src/scarr.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | // "github.com/aws/aws-sdk-go/aws"
8 | // "github.com/aws/aws-sdk-go/aws/session"
9 | // "github.com/aws/aws-sdk-go/service/s3"
10 | )
11 |
12 | func check(e error) {
13 | if e != nil {
14 | panic(e)
15 | }
16 | }
17 |
18 | func exitErrorf(msg string, args ...interface{}) {
19 | fmt.Fprintf(os.Stderr, msg+"\n", args...)
20 | os.Exit(1)
21 | }
22 |
23 | func getUsage() string {
24 | return `
25 | A tool for generating and updating flat-file sites on AWS. Registers domain names,
26 | creates TLS certificates, makes S3 buckets, configures cloudfront distributions and
27 | syncs files.
28 |
29 | Available commands:
30 | init # Generates a new scarr.yml file
31 | deploy # Sets up infrastructure + syncs files to it
32 | version # Print version
33 |
34 | Use "scarr -h" for more information.
35 |
36 | Scarr requires an AWS IAM user with appropriate permissions. You can set this user
37 | using one of these two patterns:
38 |
39 | $ AWS_PROFILE=some_profile scarr deploy -args
40 | $ AWS_ACCESS_KEY_ID=some_id AWS_SECRET_ACCESS_KEY=some_key scarr deploy -args
41 |
42 | AWS_PROFILE names a profile set up in an ~/.aws/credentials file as described here:
43 |
44 | https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
45 |
46 | Either way, the IAM user you connect should have the following permissions:
47 |
48 | {
49 | "Effect": "Allow",
50 | "Action": [
51 | "route53:CreateHostedZone",
52 | "s3:GetBucketWebsite",
53 | "route53:ListHostedZones",
54 | "cloudfront:GetInvalidation",
55 | "route53:ChangeResourceRecordSets",
56 | "s3:CreateBucket",
57 | "s3:ListBucket",
58 | "cloudfront:CreateDistribution",
59 | "route53domains:GetDomainDetail",
60 | "s3:GetBucketAcl",
61 | "cloudfront:CreateInvalidation",
62 | "route53domains:GetOperationDetail",
63 | "s3:PutObject",
64 | "s3:GetObject",
65 | "route53domains:CheckDomainAvailability",
66 | "s3:PutBucketWebsite",
67 | "acm:DescribeCertificate",
68 | "acm:RequestCertificate",
69 | "route53domains:RegisterDomain",
70 | "cloudfront:ListDistributions",
71 | "route53:ListResourceRecordSets",
72 | "s3:PutBucketAcl",
73 | "acm:ListCertificates",
74 | "s3:PutObjectAcl"
75 | ],
76 | "Resource": "*"
77 | }
78 | `
79 | }
80 |
81 | // 0 = silent
82 | // 1 = normal
83 | // todo: verbose/debug
84 | var logLevel int
85 |
86 | func logln(msgs ...interface{}) {
87 | if logLevel == 1 {
88 | fmt.Println(msgs...)
89 | }
90 | }
91 | func log(msgs ...interface{}) {
92 | if logLevel == 1 {
93 | fmt.Print(msgs...)
94 | }
95 | }
96 | func logf(msg string, rest ...interface{}) {
97 | if logLevel == 1 {
98 | fmt.Printf(msg, rest...)
99 | }
100 | }
101 |
102 | func printVersion() {
103 | fmt.Println("Scarr " + getVersion())
104 | }
105 |
106 | func main() {
107 | initCommand := flag.NewFlagSet("init", flag.ExitOnError)
108 | deployCommand := flag.NewFlagSet("deploy", flag.ExitOnError)
109 |
110 | domainPtr := initCommand.String("domain", "", "The domain this site will live at")
111 | namePtr := initCommand.String("name", "", "The name of this project")
112 | regionPtr := initCommand.String("region", "us-west-1", "The aws region this project's resources will live in (eg us-west-1)")
113 |
114 | skipSetupPtr := deployCommand.Bool("skip-setup", false, "Assume the infrastructure is all set up and just do the file upload + cache invalidations.")
115 | autoRegisterPtr := deployCommand.Bool("auto-register", false, "Register the domain name without prompting if necessary and available")
116 | silentDeployPtr := deployCommand.Bool("silent", false, "Limits stdout to errors and user-input prompts. Run with -auto-register or use an existing domain name to avoid a registration prompt")
117 |
118 | if len(os.Args) < 2 {
119 | fmt.Println("Missing command")
120 | os.Exit(1)
121 | }
122 |
123 | command := os.Args[1]
124 |
125 | switch command {
126 | case "-h":
127 | fmt.Println(getUsage())
128 | case "-help":
129 | fmt.Println(getUsage())
130 | case "--help":
131 | fmt.Println(getUsage())
132 | case "init":
133 | initCommand.Parse(os.Args[2:])
134 | case "deploy":
135 | deployCommand.Parse(os.Args[2:])
136 | case "version":
137 | printVersion()
138 | case "-version":
139 | printVersion()
140 | case "--version":
141 | printVersion()
142 | case "-v":
143 | printVersion()
144 | default:
145 | fmt.Println("Unknown command ", command)
146 | flag.PrintDefaults()
147 | os.Exit(1)
148 | }
149 | logLevel = 1
150 |
151 | if initCommand.Parsed() {
152 | runInit(*domainPtr, *namePtr, *regionPtr)
153 | // fmt.Println("init parsed", *domainPtr, *namePtr, *regionPtr)
154 | } else if deployCommand.Parsed() {
155 | if *silentDeployPtr {
156 | logLevel = 0
157 | }
158 | runDeploy(*skipSetupPtr, *autoRegisterPtr)
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/version.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | func getVersion() string {
4 | return "0.4.5"
5 | }
6 |
--------------------------------------------------------------------------------