github.com/suedadam/up@v0.1.12/config.go (about)

     1  package up
     2  
     3  import (
     4  	"encoding/json"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/apex/log"
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/apex/up/config"
    13  	"github.com/apex/up/internal/header"
    14  	"github.com/apex/up/internal/inject"
    15  	"github.com/apex/up/internal/redirect"
    16  	"github.com/apex/up/internal/util"
    17  	"github.com/apex/up/internal/validate"
    18  	"github.com/apex/up/platform/lambda/regions"
    19  	"github.com/aws/aws-sdk-go/aws/session"
    20  )
    21  
    22  // TODO: refactor defaulting / validation with slices
    23  
    24  // defaulter is the interface that provides config defaulting.
    25  type defaulter interface {
    26  	Default() error
    27  }
    28  
    29  // validator is the interface that provides config validation.
    30  type validator interface {
    31  	Validate() error
    32  }
    33  
    34  // Config for the project.
    35  type Config struct {
    36  	// Name of the project.
    37  	Name string `json:"name"`
    38  
    39  	// Description of the project.
    40  	Description string `json:"description"`
    41  
    42  	// Type of project.
    43  	Type string `json:"type"`
    44  
    45  	// Headers injection rules.
    46  	Headers header.Rules `json:"headers"`
    47  
    48  	// Redirects redirection rules.
    49  	Redirects redirect.Rules `json:"redirects"`
    50  
    51  	// Hooks defined for the project.
    52  	Hooks config.Hooks `json:"hooks"`
    53  
    54  	// Environment variables.
    55  	Environment config.Environment `json:"environment"`
    56  
    57  	// Regions is a list of regions to deploy to.
    58  	Regions []string `json:"regions"`
    59  
    60  	// Profile is the AWS profile name to reference for credentials.
    61  	Profile string `json:"profile"`
    62  
    63  	// Inject rules.
    64  	Inject inject.Rules `json:"inject"`
    65  
    66  	// Lambda provider configuration.
    67  	Lambda config.Lambda `json:"lambda"`
    68  
    69  	// CORS config.
    70  	CORS *config.CORS `json:"cors"`
    71  
    72  	// ErrorPages config.
    73  	ErrorPages config.ErrorPages `json:"error_pages"`
    74  
    75  	// Proxy config.
    76  	Proxy config.Relay `json:"proxy"`
    77  
    78  	// Static config.
    79  	Static config.Static `json:"static"`
    80  
    81  	// Logs config.
    82  	Logs config.Logs `json:"logs"`
    83  
    84  	// Certs config.
    85  	Certs config.Certs `json:"certs"`
    86  
    87  	// DNS config.
    88  	DNS config.DNS `json:"dns"`
    89  }
    90  
    91  // Validate implementation.
    92  func (c *Config) Validate() error {
    93  	if err := validate.Name(c.Name); err != nil {
    94  		return errors.Wrapf(err, ".name %q", c.Name)
    95  	}
    96  
    97  	if err := validate.List(c.Type, []string{"static", "server"}); err != nil {
    98  		return errors.Wrap(err, ".type")
    99  	}
   100  
   101  	if err := validate.Lists(c.Regions, regions.All); err != nil {
   102  		return errors.Wrap(err, ".regions")
   103  	}
   104  
   105  	if err := c.Certs.Validate(); err != nil {
   106  		return errors.Wrap(err, ".certs")
   107  	}
   108  
   109  	if err := c.DNS.Validate(); err != nil {
   110  		return errors.Wrap(err, ".dns")
   111  	}
   112  
   113  	if err := c.Static.Validate(); err != nil {
   114  		return errors.Wrap(err, ".static")
   115  	}
   116  
   117  	if err := c.Inject.Validate(); err != nil {
   118  		return errors.Wrap(err, ".inject")
   119  	}
   120  
   121  	return nil
   122  }
   123  
   124  // Default implementation.
   125  func (c *Config) Default() error {
   126  	// TODO: hack, move to the instantiation of aws clients
   127  	if c.Profile != "" {
   128  		os.Setenv("AWS_PROFILE", c.Profile)
   129  	}
   130  
   131  	// default type to server
   132  	if c.Type == "" {
   133  		c.Type = "server"
   134  	}
   135  
   136  	// runtime defaults
   137  	switch {
   138  	case util.Exists("main.go"):
   139  		golang(c)
   140  	case util.Exists("main.cr"):
   141  		crystal(c)
   142  	case util.Exists("package.json"):
   143  		if err := nodejs(c); err != nil {
   144  			return err
   145  		}
   146  	case util.Exists("app.js"):
   147  		c.Proxy.Command = "node app.js"
   148  	case util.Exists("app.py"):
   149  		python(c)
   150  	case util.Exists("index.html"):
   151  		c.Type = "static"
   152  	}
   153  
   154  	// default .name
   155  	if err := c.defaultName(); err != nil {
   156  		return errors.Wrap(err, ".name")
   157  	}
   158  
   159  	// default .regions
   160  	if err := c.defaultRegions(); err != nil {
   161  		return errors.Wrap(err, ".region")
   162  	}
   163  
   164  	// region globbing
   165  	c.Regions = regions.Match(c.Regions)
   166  
   167  	if err := c.Proxy.Default(); err != nil {
   168  		return errors.Wrap(err, ".proxy")
   169  	}
   170  
   171  	// default .lambda
   172  	if err := c.Lambda.Default(); err != nil {
   173  		return errors.Wrap(err, ".lambda")
   174  	}
   175  
   176  	// default .dns
   177  	if err := c.DNS.Default(); err != nil {
   178  		return errors.Wrap(err, ".dns")
   179  	}
   180  
   181  	// default .inject
   182  	if err := c.Inject.Default(); err != nil {
   183  		return errors.Wrap(err, ".inject")
   184  	}
   185  
   186  	// default .static
   187  	if err := c.Static.Default(); err != nil {
   188  		return errors.Wrap(err, ".static")
   189  	}
   190  
   191  	// default .error_pages
   192  	if err := c.ErrorPages.Default(); err != nil {
   193  		return errors.Wrap(err, ".error_pages")
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  // defaultName infers the name from the CWD if it's not set.
   200  func (c *Config) defaultName() error {
   201  	if c.Name != "" {
   202  		return nil
   203  	}
   204  
   205  	dir, err := os.Getwd()
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	c.Name = filepath.Base(dir)
   211  	log.Debugf("infer name from current working directory %q", c.Name)
   212  	return nil
   213  }
   214  
   215  // defaultRegions checks AWS_REGION and falls back on us-west-2.
   216  func (c *Config) defaultRegions() error {
   217  	if len(c.Regions) != 0 {
   218  		log.Debugf("%d regions from config", len(c.Regions))
   219  		return nil
   220  	}
   221  
   222  	s, err := session.NewSessionWithOptions(session.Options{
   223  		SharedConfigState: session.SharedConfigEnable,
   224  	})
   225  
   226  	if err != nil {
   227  		return errors.Wrap(err, "creating session")
   228  	}
   229  
   230  	if r := *s.Config.Region; r != "" {
   231  		log.Debugf("region from aws shared config %q", r)
   232  		c.Regions = append(c.Regions, r)
   233  		return nil
   234  	}
   235  
   236  	r := "us-west-2"
   237  	log.Debugf("region defaulted to %q", r)
   238  	c.Regions = append(c.Regions, r)
   239  	return nil
   240  }
   241  
   242  // ParseConfig returns config from JSON bytes.
   243  func ParseConfig(b []byte) (*Config, error) {
   244  	c := &Config{}
   245  
   246  	if err := json.Unmarshal(b, c); err != nil {
   247  		return nil, errors.Wrap(err, "parsing json")
   248  	}
   249  
   250  	if err := c.Default(); err != nil {
   251  		return nil, errors.Wrap(err, "defaulting")
   252  	}
   253  
   254  	if err := c.Validate(); err != nil {
   255  		return nil, errors.Wrap(err, "validating")
   256  	}
   257  
   258  	return c, nil
   259  }
   260  
   261  // ParseConfigString returns config from JSON string.
   262  func ParseConfigString(s string) (*Config, error) {
   263  	return ParseConfig([]byte(s))
   264  }
   265  
   266  // MustParseConfigString returns config from JSON string.
   267  func MustParseConfigString(s string) *Config {
   268  	c, err := ParseConfigString(s)
   269  	if err != nil {
   270  		panic(err)
   271  	}
   272  
   273  	return c
   274  }
   275  
   276  // ReadConfig reads the configuration from `path`.
   277  func ReadConfig(path string) (*Config, error) {
   278  	b, err := ioutil.ReadFile(path)
   279  
   280  	if os.IsNotExist(err) {
   281  		c := &Config{}
   282  
   283  		if err := c.Default(); err != nil {
   284  			return nil, errors.Wrap(err, "defaulting")
   285  		}
   286  
   287  		if err := c.Validate(); err != nil {
   288  			return nil, errors.Wrap(err, "validating")
   289  		}
   290  
   291  		return c, nil
   292  	}
   293  
   294  	if err != nil {
   295  		return nil, errors.Wrap(err, "reading file")
   296  	}
   297  
   298  	return ParseConfig(b)
   299  }
   300  
   301  // golang config.
   302  func golang(c *Config) {
   303  	if c.Hooks.Build == "" {
   304  		c.Hooks.Build = `GOOS=linux GOARCH=amd64 go build -o server *.go`
   305  	}
   306  
   307  	if c.Hooks.Clean == "" {
   308  		c.Hooks.Clean = `rm server`
   309  	}
   310  }
   311  
   312  // crystal config.
   313  func crystal(c *Config) {
   314  	if c.Hooks.Build == "" {
   315  		c.Hooks.Build = `docker run --rm -v $(PWD):/src -w /src tjholowaychuk/up-crystal crystal build --link-flags -static -o server main.cr`
   316  	}
   317  
   318  	if c.Hooks.Clean == "" {
   319  		c.Hooks.Clean = `rm server`
   320  	}
   321  }
   322  
   323  // nodejs config.
   324  func nodejs(c *Config) error {
   325  	var pkg struct {
   326  		Scripts struct {
   327  			Start string `json:"start"`
   328  			Build string `json:"build"`
   329  		} `json:"scripts"`
   330  	}
   331  
   332  	// read package.json
   333  	if err := util.ReadFileJSON("package.json", &pkg); err != nil {
   334  		return err
   335  	}
   336  
   337  	// use "start" script unless explicitly defined in up.json
   338  	if c.Proxy.Command == "" {
   339  		if s := pkg.Scripts.Start; s == "" {
   340  			c.Proxy.Command = "node app.js"
   341  		} else {
   342  			c.Proxy.Command = s
   343  		}
   344  	}
   345  
   346  	// use "build" script unless explicitly defined in up.json
   347  	if c.Hooks.Build == "" {
   348  		c.Hooks.Build = pkg.Scripts.Build
   349  	}
   350  
   351  	return nil
   352  }
   353  
   354  // python config.
   355  func python(c *Config) {
   356  	if c.Proxy.Command == "" {
   357  		c.Proxy.Command = "python app.py"
   358  	}
   359  
   360  	// Only add build & clean hooks if a requirements.txt exists
   361  	if !util.Exists("requirements.txt") {
   362  		return
   363  	}
   364  
   365  	// Set PYTHONPATH env
   366  	if c.Environment == nil {
   367  		c.Environment = config.Environment{}
   368  	}
   369  	c.Environment["PYTHONPATH"] = ".pypath/"
   370  
   371  	// Copy libraries into .pypath/
   372  	if c.Hooks.Build == "" {
   373  		c.Hooks.Build = `mkdir -p .pypath/ && pip install -r requirements.txt -t .pypath/`
   374  	}
   375  
   376  	// Clean .pypath/
   377  	if c.Hooks.Clean == "" {
   378  		c.Hooks.Clean = `rm -r .pypath/`
   379  	}
   380  }