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