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 }