Skip to content
Permalink
Browse files

add custom stage support. Closes #326

  • Loading branch information...
tj committed Feb 7, 2018
1 parent 7204939 commit f64a343b9e5d49f403a0dbe39a809b3358216628
@@ -102,6 +102,10 @@ func (c *Config) Validate() error {

// Default implementation.
func (c *Config) Default() error {
if c.Stages == nil {
c.Stages = make(Stages)
}

// TODO: hack, move to the instantiation of aws clients
if c.Profile != "" {
os.Setenv("AWS_PROFILE", c.Profile)
@@ -1,5 +1,19 @@
package config

import (
"sort"

"github.com/apex/up/internal/validate"
"github.com/pkg/errors"
)

// defaultStages is a list of default stage names.
var defaultStages = []string{
"development",
"staging",
"production",
}

// Stage config.
type Stage struct {
Domain string `json:"domain"`
@@ -19,6 +33,15 @@ func (s *Stage) IsRemote() bool {
return !s.IsLocal()
}

// Validate implementation.
func (s *Stage) Validate() error {
if err := validate.Stage(s.Name); err != nil {
return errors.Wrap(err, ".name")
}

return nil
}

// StageOverrides config.
type StageOverrides struct {
Hooks Hooks `json:"hooks"`
@@ -34,78 +57,95 @@ func (s *StageOverrides) Override(c *Config) {
}

// Stages config.
type Stages struct {
Development *Stage `json:"development"`
Staging *Stage `json:"staging"`
Production *Stage `json:"production"`
}
type Stages map[string]*Stage

// Default implementation.
func (s *Stages) Default() error {
if s := s.Development; s != nil {
s.Name = "development"
}

if s := s.Staging; s != nil {
s.Name = "staging"
func (s Stages) Default() error {
// defaults
for _, name := range defaultStages {
if _, ok := s[name]; !ok {
s[name] = &Stage{}
}
}

if s := s.Production; s != nil {
s.Name = "production"
// assign names
for name, s := range s {
s.Name = name
}

return nil
}

// Validate implementation.
func (s *Stages) Validate() error {
func (s Stages) Validate() error {
for _, s := range s {
if err := s.Validate(); err != nil {
return errors.Wrapf(err, "stage %q", s.Name)
}
}
return nil
}

// List returns configured stages.
func (s *Stages) List() (v []*Stage) {
if s := s.Development; s != nil {
func (s Stages) List() (v []*Stage) {
for _, s := range s {
v = append(v, s)
}

if s := s.Staging; s != nil {
v = append(v, s)
return
}

// Domains returns configured domains.
func (s Stages) Domains() (v []string) {
for _, s := range s.List() {
if s.Domain != "" {
v = append(v, s.Domain)
}
}

if s := s.Production; s != nil {
v = append(v, s)
return
}

// Names returns configured stage names.
func (s Stages) Names() (v []string) {
for _, s := range s.List() {
v = append(v, s.Name)
}

sort.Strings(v)
return
}

// Domains returns configured domains.
func (s *Stages) Domains() (v []string) {
// RemoteNames returns configured remote stage names.
func (s Stages) RemoteNames() (v []string) {
for _, s := range s.List() {
if s.Domain != "" {
v = append(v, s.Domain)
if s.IsRemote() {
v = append(v, s.Name)
}
}

sort.Strings(v)
return
}

// GetByDomain returns the stage by domain or nil.
func (s *Stages) GetByDomain(domain string) *Stage {
func (s Stages) GetByDomain(domain string) *Stage {
for _, s := range s.List() {
if s.Domain == domain {
return s
}
}

return nil
}

// GetByName returns the stage by name or nil.
func (s *Stages) GetByName(name string) *Stage {
func (s Stages) GetByName(name string) *Stage {
for _, s := range s.List() {
if s.Name == name {
return s
}
}

return nil
}
@@ -71,6 +71,31 @@ func TestStage_Override(t *testing.T) {
})
}

func TestStages_Default(t *testing.T) {
t.Run("no custom stages", func(t *testing.T) {
s := Stages{}

assert.NoError(t, s.Default(), "validate")
assert.NoError(t, s.Validate(), "validate")

assert.Len(t, s, 3)
assert.Equal(t, "staging", s["staging"].Name)
assert.Equal(t, "production", s["production"].Name)
})

t.Run("custom stages", func(t *testing.T) {
s := Stages{
"beta": &Stage{},
}

assert.NoError(t, s.Default(), "validate")
assert.NoError(t, s.Validate(), "validate")

assert.Len(t, s, 4)
assert.Equal(t, "beta", s["beta"].Name)
})
}

func TestStages_Validate(t *testing.T) {
t.Run("no stages", func(t *testing.T) {
s := Stages{}
@@ -79,18 +104,18 @@ func TestStages_Validate(t *testing.T) {

t.Run("some stages", func(t *testing.T) {
s := Stages{
Staging: &Stage{
"staging": &Stage{
Domain: "gh-polls-stage.com",
},
Production: &Stage{
"production": &Stage{
Domain: "gh-polls.com",
},
}

assert.NoError(t, s.Default(), "validate")
assert.NoError(t, s.Validate(), "validate")
assert.Equal(t, "staging", s.Staging.Name)
assert.Equal(t, "production", s.Production.Name)
assert.Equal(t, "staging", s["staging"].Name)
assert.Equal(t, "production", s["production"].Name)
})
}

@@ -104,8 +129,8 @@ func TestStages_List(t *testing.T) {
}

s := Stages{
Staging: stage,
Production: prod,
"staging": stage,
"production": prod,
}

list := []*Stage{
@@ -127,8 +152,8 @@ func TestStages_GetByDomain(t *testing.T) {
}

s := Stages{
Staging: stage,
Production: prod,
"staging": stage,
"production": prod,
}

assert.Equal(t, prod, s.GetByDomain("gh-polls.com"))
@@ -6,4 +6,4 @@ Up deploys infinitely scalable serverless apps, APIs, and static websites in sec

Up focuses on deploying "vanilla" HTTP servers so there's nothing new to learn, just develop with your favorite existing frameworks such as Express, Koa, Django, Golang net/http or others.

Up currently supports Node.js, Golang, Python, Java, Crystal, and static sites out of the box. Up is platform-agnostic, supporting AWS Lambda and API Gateway as the first targets. You can think of Up as self-hosted Heroku style user experience for a fraction of the price, with the security, flexibility, and scalability of AWS.
Up currently supports Node.js, Golang, Python, Java, Crystal, and static sites out of the box. Up is platform-agnostic, supporting AWS Lambda and API Gateway as the first targets — you can think of Up as self-hosted Heroku style user experience for a fraction of the price, with the security, flexibility, and scalability of AWS — just `$ up` and you're done!
@@ -543,9 +543,63 @@ The record `type` must be one of:
- SRV
- TXT

## Stages & Custom Domains
## Stages

Up supports the concept of "stages" for configuration, such as mapping of custom domains, or tuneing the size of Lambda function to use.

By default the following stages are defined:

- `development` — local development environment
- `staging` — remote environment for staging new features or releases
- `production` — aptly named production environment

To create a new stage, first add it to your configuration, in this case we'll call it "beta":

```json
{
"name": "app",
"lambda": {
"memory": 128
},
"stages": {
"beta": {
}
}
}
```

Now you'll need to plan your stack changes, which will set up a new API Gateway and permissions:

Up supports per-stage configuration, such as mapping of custom domains.

```
$ up stack plan
Add api deployment
id: ApiDeploymentBeta
Add lambda permission
id: ApiLambdaPermissionBeta
```

Apply those changes:

```
$ up stack apply
```

Now you can deploy to your new stage by passing the name `beta` and open the end-point in the browser:

```
$ up beta
$ up url -o beta
```

To delete a stage, simply remove it from the `up.json` configuration and run `up stack plan` again, and `up stack apply` after reviewing the changes.

You may of course assign a custom domain to these stages as well, let's take a look at that next!

## Stages & Custom Domains

By defining a stage and its `domain`, Up knows it will need to create a free SSL certificate for `gh-polls.com`, setup the DNS records, and map the domain to API Gateway.

@@ -559,14 +613,14 @@ By defining a stage and its `domain`, Up knows it will need to create a free SSL
}
```

Here's another example mapping each stage to a domain:
Here's another example mapping each stage to a domain, note that the domains do not need to be related, you could use `stage-gh-polls.com` for example.


```json
{
"stages": {
"production": {
"domain": "api.gh-polls.com"
"domain": "gh-polls.com"
},
"staging": {
"domain": "stage.gh-polls.com"
@@ -591,13 +645,9 @@ You may also provide an optional base path, for example to prefix your API with

Plan the changes via `up stack plan` and `up stack apply` to perform the changes. Note that CloudFront can take up to ~40 minutes to distribute this configuration globally, so grab a coffee while these changes are applied.

Custom stages may be supported in the future, for now there are two:

- `staging`
- `production`

You may [purchase domains](#guides.development_to_production_workflow.purchasing_a_domain) from the command-line, or map custom domains from other registrars. Up uses Route53 to purchase domains using your AWS account credit card. See `up help domains`.


## Stage Overrides

Up allows some configuration properties to be overridden at the stage level. The following example illustrates how you can tune lambda memory and hooks per-stage.
@@ -8,8 +8,8 @@ title: Help
</details>

<details>
<summary>My application times out</summary>
<p>Lambda `memory` scales CPU alongside the RAM – so if your application is booting or serving responses slowly you may want to try `1024` or above, see [Lambda Pricing](https://aws.amazon.com/lambda/pricing/) for options.</p>
<summary>My application times out or seems slow</summary>
<p>Lambda `memory` scales CPU alongside the RAM – so if your application is slow to initialize or serve responses slowly you may want to try `1024` or above, see [Lambda Pricing](https://aws.amazon.com/lambda/pricing/) for options.</p>
<p>Ensure that all of your dependencies are deployed, you may use `up -v` to view what is added or filtered from the deployment, or `up build --size` to output the contents of the zip.</p>
</details>

@@ -127,13 +127,13 @@ func (p *Proxy) Start() error {

start := time.Now()
timeout := time.Duration(p.config.Proxy.ListenTimeout) * time.Second
ctx.WithField("url", p.target.String()).Info("waiting for server to listen")
ctx.WithField("url", p.target.String()).Info("starting server")

if err := util.WaitForListen(p.target, timeout); err != nil {
return errors.Wrapf(err, "waiting for %s to be in listening state", p.target.String())
}

ctx.WithField("duration", util.MillisecondsSince(start)).Info("server is listening")
ctx.WithField("duration", util.MillisecondsSince(start)).Info("started server")
return nil
}

@@ -57,7 +57,7 @@ retry:
}

// validate stage name
if err := validate.Stage(stage); err != nil {
if err := validate.List(stage, c.Stages.RemoteNames()); err != nil {
return err
}

@@ -43,7 +43,7 @@ func init() {
"copy": *copy,
})

if err := validate.Stage(*stage); err != nil {
if err := validate.List(*stage, c.Stages.RemoteNames()); err != nil {
return err
}

Oops, something went wrong.

0 comments on commit f64a343

Please sign in to comment.
You can’t perform that action at this time.