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 }