github.com/motyar/up@v0.2.10/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  	// Stages config.
    85  	Stages config.Stages `json:"stages"`
    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.DNS.Validate(); err != nil {
   106  		return errors.Wrap(err, ".dns")
   107  	}
   108  
   109  	if err := c.Static.Validate(); err != nil {
   110  		return errors.Wrap(err, ".static")
   111  	}
   112  
   113  	if err := c.Inject.Validate(); err != nil {
   114  		return errors.Wrap(err, ".inject")
   115  	}
   116  
   117  	if err := c.Lambda.Validate(); err != nil {
   118  		return errors.Wrap(err, ".lambda")
   119  	}
   120  
   121  	if err := c.Proxy.Validate(); err != nil {
   122  		return errors.Wrap(err, ".proxy")
   123  	}
   124  
   125  	if err := c.Stages.Validate(); err != nil {
   126  		return errors.Wrap(err, ".stages")
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  // Default implementation.
   133  func (c *Config) Default() error {
   134  	// TODO: hack, move to the instantiation of aws clients
   135  	if c.Profile != "" {
   136  		os.Setenv("AWS_PROFILE", c.Profile)
   137  	}
   138  
   139  	// default type to server
   140  	if c.Type == "" {
   141  		c.Type = "server"
   142  	}
   143  
   144  	// runtime defaults
   145  	switch {
   146  	case util.Exists("main.go"):
   147  		golang(c)
   148  	case util.Exists("main.cr"):
   149  		crystal(c)
   150  	case util.Exists("package.json"):
   151  		if err := nodejs(c); err != nil {
   152  			return err
   153  		}
   154  	case util.Exists("app.js"):
   155  		c.Proxy.Command = "node app.js"
   156  	case util.Exists("app.py"):
   157  		python(c)
   158  	case util.Exists("index.html"):
   159  		c.Type = "static"
   160  	}
   161  
   162  	// default .name
   163  	if err := c.defaultName(); err != nil {
   164  		return errors.Wrap(err, ".name")
   165  	}
   166  
   167  	// default .regions
   168  	if err := c.defaultRegions(); err != nil {
   169  		return errors.Wrap(err, ".region")
   170  	}
   171  
   172  	// region globbing
   173  	c.Regions = regions.Match(c.Regions)
   174  
   175  	// default .proxy
   176  	if err := c.Proxy.Default(); err != nil {
   177  		return errors.Wrap(err, ".proxy")
   178  	}
   179  
   180  	// default .lambda
   181  	if err := c.Lambda.Default(); err != nil {
   182  		return errors.Wrap(err, ".lambda")
   183  	}
   184  
   185  	// default .dns
   186  	if err := c.DNS.Default(); err != nil {
   187  		return errors.Wrap(err, ".dns")
   188  	}
   189  
   190  	// default .inject
   191  	if err := c.Inject.Default(); err != nil {
   192  		return errors.Wrap(err, ".inject")
   193  	}
   194  
   195  	// default .static
   196  	if err := c.Static.Default(); err != nil {
   197  		return errors.Wrap(err, ".static")
   198  	}
   199  
   200  	// default .error_pages
   201  	if err := c.ErrorPages.Default(); err != nil {
   202  		return errors.Wrap(err, ".error_pages")
   203  	}
   204  
   205  	// default .stages
   206  	if err := c.Stages.Default(); err != nil {
   207  		return errors.Wrap(err, ".stages")
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  // defaultName infers the name from the CWD if it's not set.
   214  func (c *Config) defaultName() error {
   215  	if c.Name != "" {
   216  		return nil
   217  	}
   218  
   219  	dir, err := os.Getwd()
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	c.Name = filepath.Base(dir)
   225  	log.Debugf("infer name from current working directory %q", c.Name)
   226  	return nil
   227  }
   228  
   229  // defaultRegions checks AWS_REGION and falls back on us-west-2.
   230  func (c *Config) defaultRegions() error {
   231  	if len(c.Regions) != 0 {
   232  		log.Debugf("%d regions from config", len(c.Regions))
   233  		return nil
   234  	}
   235  
   236  	s, err := session.NewSessionWithOptions(session.Options{
   237  		SharedConfigState: session.SharedConfigEnable,
   238  	})
   239  
   240  	if err != nil {
   241  		return errors.Wrap(err, "creating session")
   242  	}
   243  
   244  	if r := *s.Config.Region; r != "" {
   245  		log.Debugf("region from aws shared config %q", r)
   246  		c.Regions = append(c.Regions, r)
   247  		return nil
   248  	}
   249  
   250  	r := "us-west-2"
   251  	log.Debugf("region defaulted to %q", r)
   252  	c.Regions = append(c.Regions, r)
   253  	return nil
   254  }
   255  
   256  // ParseConfig returns config from JSON bytes.
   257  func ParseConfig(b []byte) (*Config, error) {
   258  	c := &Config{}
   259  
   260  	if err := json.Unmarshal(b, c); err != nil {
   261  		return nil, errors.Wrap(err, "parsing json")
   262  	}
   263  
   264  	if err := c.Default(); err != nil {
   265  		return nil, errors.Wrap(err, "defaulting")
   266  	}
   267  
   268  	if err := c.Validate(); err != nil {
   269  		return nil, errors.Wrap(err, "validating")
   270  	}
   271  
   272  	return c, nil
   273  }
   274  
   275  // ParseConfigString returns config from JSON string.
   276  func ParseConfigString(s string) (*Config, error) {
   277  	return ParseConfig([]byte(s))
   278  }
   279  
   280  // MustParseConfigString returns config from JSON string.
   281  func MustParseConfigString(s string) *Config {
   282  	c, err := ParseConfigString(s)
   283  	if err != nil {
   284  		panic(err)
   285  	}
   286  
   287  	return c
   288  }
   289  
   290  // ReadConfig reads the configuration from `path`.
   291  func ReadConfig(path string) (*Config, error) {
   292  	b, err := ioutil.ReadFile(path)
   293  
   294  	if os.IsNotExist(err) {
   295  		c := &Config{}
   296  
   297  		if err := c.Default(); err != nil {
   298  			return nil, errors.Wrap(err, "defaulting")
   299  		}
   300  
   301  		if err := c.Validate(); err != nil {
   302  			return nil, errors.Wrap(err, "validating")
   303  		}
   304  
   305  		return c, nil
   306  	}
   307  
   308  	if err != nil {
   309  		return nil, errors.Wrap(err, "reading file")
   310  	}
   311  
   312  	return ParseConfig(b)
   313  }
   314  
   315  // golang config.
   316  func golang(c *Config) {
   317  	if c.Hooks.Build.IsEmpty() {
   318  		c.Hooks.Build = config.Hook{`GOOS=linux GOARCH=amd64 go build -o server *.go`}
   319  	}
   320  
   321  	if c.Hooks.Clean.IsEmpty() {
   322  		c.Hooks.Clean = config.Hook{`rm server`}
   323  	}
   324  }
   325  
   326  // crystal config.
   327  func crystal(c *Config) {
   328  	if c.Hooks.Build.IsEmpty() {
   329  		c.Hooks.Build = config.Hook{`docker run --rm -v $(PWD):/src -w /src tjholowaychuk/up-crystal crystal build --link-flags -static -o server main.cr`}
   330  	}
   331  
   332  	if c.Hooks.Clean.IsEmpty() {
   333  		c.Hooks.Clean = config.Hook{`rm server`}
   334  	}
   335  }
   336  
   337  // nodejs config.
   338  func nodejs(c *Config) error {
   339  	var pkg struct {
   340  		Scripts struct {
   341  			Start string `json:"start"`
   342  			Build string `json:"build"`
   343  		} `json:"scripts"`
   344  	}
   345  
   346  	// read package.json
   347  	if err := util.ReadFileJSON("package.json", &pkg); err != nil {
   348  		return err
   349  	}
   350  
   351  	// use "start" script unless explicitly defined in up.json
   352  	if c.Proxy.Command == "" {
   353  		if s := pkg.Scripts.Start; s == "" {
   354  			c.Proxy.Command = `node app.js`
   355  		} else {
   356  			c.Proxy.Command = s
   357  		}
   358  	}
   359  
   360  	// use "build" script unless explicitly defined in up.json
   361  	if c.Hooks.Build.IsEmpty() {
   362  		c.Hooks.Build = config.Hook{pkg.Scripts.Build}
   363  	}
   364  
   365  	return nil
   366  }
   367  
   368  // python config.
   369  func python(c *Config) {
   370  	if c.Proxy.Command == "" {
   371  		c.Proxy.Command = "python app.py"
   372  	}
   373  
   374  	// Only add build & clean hooks if a requirements.txt exists
   375  	if !util.Exists("requirements.txt") {
   376  		return
   377  	}
   378  
   379  	// Set PYTHONPATH env
   380  	if c.Environment == nil {
   381  		c.Environment = config.Environment{}
   382  	}
   383  	c.Environment["PYTHONPATH"] = ".pypath/"
   384  
   385  	// Copy libraries into .pypath/
   386  	if c.Hooks.Build.IsEmpty() {
   387  		c.Hooks.Build = config.Hook{`mkdir -p .pypath/ && pip install -r requirements.txt -t .pypath/`}
   388  	}
   389  
   390  	// Clean .pypath/
   391  	if c.Hooks.Clean.IsEmpty() {
   392  		c.Hooks.Clean = config.Hook{`rm -r .pypath/`}
   393  	}
   394  }