github.com/tooploox/oya@v0.0.21-0.20230524103240-1cda1861aad6/README.md (about) 1 --- 2 layout: docs 3 permalink: /documentation/ 4 --- 5 # Oya 6 7 Oya is a command line tool aiming to help you bootstrap and manage deployable 8 projects. 9 10 11 ## Quick start 12 13 Install oya and its dependencies: 14 15 ``` bash 16 $ curl https://oya.sh/get | bash 17 ``` 18 19 Initialize a project: 20 21 ``` bash 22 $ oya init OyaExample 23 ``` 24 25 Add an example task to the bottom of the generated `Oyafile`: 26 27 ``` bash 28 $ cat Oyafile 29 ``` 30 ``` yaml 31 Project: project 32 33 build: | 34 echo "Hello, world" 35 ``` 36 37 A task is simply a bash-compatible script. Its name is any valid camel-case identifier starting with a lowercase letter. 38 39 > Identifiers starting with caps are reserved by Oya. 40 41 List available tasks: 42 43 ``` bash 44 $ oya tasks 45 ``` 46 47 Run the task: 48 49 ``` bash 50 $ oya run build 51 Hello, world 52 ``` 53 54 > If you're familiar with Makefiles, you may have noticed some similarity here. 55 > The main difference is because we're using standard YAML files, is the pipe 56 > character after task name. An added bonus you don't have to use tabs. :> 57 58 If all Oya offered was poor-man's Makefiles, you'd better find [something 59 better](http://www.dougmcinnes.com/html-5-asteroids) to do. Fortunately, Oya has 60 much more to offer so keep reading. 61 62 63 # Key concepts 64 65 - **Oyafile -** is an YAML file containing Oya config and task definitions. 66 - **Oya task -** a named bash-compatible script you can run using `oya run <task 67 name>`. 68 - **Oya project -** is a directory and any number of subdirectories containing 69 `Oyafiles`; the top-level `Oyafile` must contain a `Project:` directive. 70 - **Oya pack -** an installable Oya project containing reusable tasks you can 71 easily use in other projects. 72 73 # Installation 74 75 To install the latest version of Oya run the following command: 76 77 ``` bash 78 $ curl https://oya.sh/get | bash 79 ``` 80 81 > You can also specify which version should be installed 82 > 83 > ``` bash 84 > $ curl https://oya.sh/get | bash -s v0.0.7 85 > $ oya --version 86 > ``` 87 88 89 # Initializing a project 90 91 To get started using Oya in an existing project you need to initialize it by 92 running the following command in its top-level directory: 93 94 ``` bash 95 $ oya init OyaExample 96 ``` 97 98 All the command does is generate a file named `Oyafile` that looks like this. 99 100 ``` bash 101 $ cat Oyafile 102 ``` 103 ``` yaml 104 Project: OyaExample 105 ``` 106 107 # Creating your first task 108 109 Oya task is a named bash script defined in an `Oyafile`. Let's pretend our 110 project is a Golang HTTP server and we need tasks for building and running the 111 server. Edit the generated `Oyafile` so it looks like this: 112 113 ``` bash 114 $ cat Oyafile 115 ``` 116 ``` yaml 117 Project: OyaExample 118 119 build: | 120 go build . 121 122 start: | 123 go run . 124 ``` 125 126 > Notice the pipe characters after task names. This is YAML and the pipe is 127 > required for multi-line script definitions. 128 129 Here's how you can list available tasks: 130 131 ``` bash 132 $ oya tasks 133 ``` 134 ``` yaml 135 # in ./Oyafile 136 oya run build 137 oya run start 138 ``` 139 140 To make it work, let's create a simple server implementation: 141 142 ``` bash 143 $ cat app.go 144 ``` 145 ``` golang 146 package main 147 148 import ( 149 "flag" 150 "fmt" 151 "log" 152 "net/http" 153 ) 154 155 func handler(w http.ResponseWriter, r *http.Request) { 156 fmt.Fprintf(w, "Hello, world!") 157 } 158 159 func main() { 160 host := flag.String("host", "0.0.0.0", "host name to bind to") 161 port := flag.Int("port", 8080, "port number") 162 flag.Parse() 163 http.HandleFunc("/", handler) 164 bind := fmt.Sprintf("%s:%d", *host, *port) 165 fmt.Printf("Starting web server on %s\n", bind) 166 log.Fatal(http.ListenAndServe(bind, nil)) 167 } 168 ``` 169 170 > Well, to really make it work, you also need the Go language tools 171 > [installed](https://golang.org/doc/install). 172 173 174 175 Let's start the server 176 177 ``` bash 178 $ oya run start 179 ``` 180 181 Ok, but does our server work? 182 183 ``` bash 184 $ curl localhost:8000 185 Hello, world! 186 ``` 187 188 Success! 189 190 191 # Parametrizing tasks 192 193 Alongside the `Oyafile` you can create any number of YAML files with an `.oya` 194 extension containing predefined values you can use in your tasks (and in 195 generated boilerplate as you'll find out later). 196 197 Let's put the default port number and host name into an '.oya' file. Create file 198 named `values.oya` with the following contents: 199 200 ``` bash 201 $ cat values.oya 202 ``` 203 ``` yaml 204 port: 4000 205 host: localhost 206 ``` 207 208 You can use any names for your values as long as they start with a lowercase letter. By convention, the names are camel-case. 209 210 Let's now modify our task definitions so we pass port and host name explicitly 211 when starting the server: 212 213 214 ``` bash 215 $ cat Oyafile 216 ``` 217 ``` yaml 218 Project: OyaExample 219 220 build: | 221 go build . 222 223 start: | 224 go run . --port ${Oya[port]} --host ${Oya[host]} 225 ``` 226 227 > The `${Oya[...]}` syntax is how you access bash associative arrays. Oya comes 228 > with its own shell implementation aiming to be compatible with Bash 4. 229 230 231 After restarting the server (using `oya run start`) it's reachable on a 232 different port: 233 234 ``` bash 235 $ curl localhost:4000 236 Hello, world! 237 ``` 238 239 This is a YAML file you can use maps, arrays and nest values. Let's modify `values.oya` slightly: 240 241 ``` bash 242 $ cat values.oya 243 ``` 244 ``` yaml 245 port: 4000 246 host: localhost 247 app: 248 version: v0.1.0 249 ``` 250 251 This is how you can use it in the `start` task: 252 253 ``` bash 254 $ cat Oyafile 255 ``` 256 ``` yaml 257 [...] 258 start: | 259 echo "Starting server version ${Oya[app.version]}" 260 go run . --port ${Oya[port]} --host ${Oya[host]} 261 ``` 262 263 # Passing arguments 264 265 TODO: Override port/host via flags. 266 267 # Storing confidential information 268 269 You can also store confidential data right in your projects. Oya uses 270 [SOPS](<https://github.com/mozilla/sops>) to store them in an encrypted format. 271 272 Imagine you want to protect our oh so very secret HTTP endpoint using a password 273 you need to supply as a parameter, example: 274 275 276 ``` bash 277 $ curl localhost:4000?password=badpassword 278 Unauthorized 279 ``` 280 281 First, configure SOPS for encryption method, check 282 <https://github.com/mozilla/sops/blob/master/README.rst#usage>. 283 284 For our example we can use a sample PGP key: 285 286 ``` bash 287 $ export SOPS_PGP_FP="317D 6971 DD80 4501 A6B8 65B9 0F1F D46E 2E8C 7202" 288 ``` 289 290 Oya secrets commands: 291 292 ``` bash 293 $ oya secrets --help 294 ``` 295 ``` yaml 296 ... 297 edit Edit secrets file 298 encrypt Encrypt secrets file 299 view View secrets 300 ... 301 ``` 302 303 Let's first slightly modify our HTTP server so it checks if the provided 304 password matches one in an environment variable. Change `app.go` so it looks 305 like this: 306 307 ``` bash 308 $ cat app.go 309 ``` 310 ``` golang 311 package main 312 313 import ( 314 "flag" 315 "fmt" 316 "log" 317 "net/http" 318 "os" 319 ) 320 321 func handler(w http.ResponseWriter, r *http.Request) { 322 requiredPassword := os.Getenv("PASSWORD") 323 password, ok := r.URL.Query()["password"] 324 if !ok || len(password) != 1 || password[0] != requiredPassword { 325 http.Error(w, "Unauthorized", http.StatusUnauthorized) 326 return 327 } 328 329 fmt.Fprintf(w, "Hello, world!") 330 } 331 332 func main() { 333 host := flag.String("host", "0.0.0.0", "host name to bind to") 334 port := flag.Int("port", 8080, "port number") 335 flag.Parse() 336 http.HandleFunc("/", handler) 337 bind := fmt.Sprintf("%s:%d", *host, *port) 338 fmt.Printf("Starting web server on %s\n", bind) 339 log.Fatal(http.ListenAndServe(bind, nil)) 340 } 341 ``` 342 343 Long story short, the server now requires the `PASSWORD` environment variable to 344 be present. Let's modify our `Oyafile` so it sets that variable: 345 346 ``` bash 347 $ cat Oyafile 348 ``` 349 ``` yaml 350 Project: OyaExample 351 352 build: | 353 go build . 354 355 start: | 356 PASSWORD=${password} go run . --port ${Oya[port]} --host ${Oya[host]} 357 ``` 358 359 Because we don't want to store the password in the plain, we'll encrypt it. 360 361 First you need to create `secrets.oya` file and encrypt it: 362 363 ``` bash 364 $ oya secrets encrypt secrets.oya 365 ``` 366 ``` bash 367 $ cat secrets.oya 368 ``` 369 ``` yaml 370 password: hokuspokus 371 EOT 372 ``` 373 ``` bash 374 $ oya secrets encrypt secrets.oya 375 ``` 376 377 > There's nothing special about the name of the file. You can encrypt any YAML 378 > file with .oya extension. In larger projects you could keep your secrets in 379 > several encrypted .oya files, grouping secrets by function. 380 381 Now our precious secret is safe! 382 383 ``` bash 384 $ cat secrets.oya 385 ``` 386 ``` yaml 387 { 388 "data": "ENC[AES256_GCM,data:XXXX=,tag:XXXX==,type:str]", 389 "sops": { 390 ... 391 "pgp": [...], 392 ... 393 } 394 } 395 ``` 396 397 Only SOPS metadata is out in the plain, the password itself is encrypted. 398 399 Restart the HTTP server and test it: 400 401 ``` bash 402 $ curl localhost:4000?password=badpassword 403 Unauthorized 404 $ curl localhost:4000?password=hokuspokus 405 Hello, world! 406 ``` 407 408 To view or edit an encrypted file later: 409 410 ``` bash 411 $ oya secrets view secrets.oya 412 ``` 413 ``` yaml 414 password: hokuspokus 415 ``` 416 ``` bash 417 $ oya secrets edit secrets.oya 418 ``` 419 420 > You can use your favorite editor by setting the `EDITOR` environment variable. 421 > The default is vim but you should be able to make it work even with [GUI 422 > editors](https://github.com/mozilla/sops/issues/127). 423 424 425 # Using Oya packs 426 427 Pack is an installable Oya project containing reusable tasks you can easily use 428 in other projects. 429 430 > Oya installs pack in your home `~/.oya` directory by default but you can 431 > change the location by setting the `OYA_HOME` environment variable. 432 433 In this tutorial, let's use the `docker` pack to generate a Dockerfile for the 434 application: 435 436 ``` bash 437 $ oya import github.com/tooploox/oya-packs/docker 438 ``` 439 440 Import automatically resolves dependencies using the newest available version of 441 the pack, pinning it in the `Require:` section: 442 443 ``` bash 444 $ cat Oyafile 445 ``` 446 ``` yaml 447 Project: OyaExample 448 Require: 449 github.com/tooploox/oya-packs/docker: v0.0.6 450 [...] 451 ``` 452 453 It makes the pack's tasks available under an alias. In case of this pack, it's 454 `docker`: 455 456 ``` bash 457 $ oya tasks 458 # in ./Oyafile 459 oya run build 460 oya run docker.build 461 oya run docker.generate 462 oya run docker.run 463 oya run docker.stop 464 oya run docker.version 465 oya run start 466 ``` 467 468 469 You can change the alias by editing the alias in the `Import` section: 470 471 ``` bash 472 $ cat Oyafile 473 ``` 474 ``` yaml 475 Project: OyaExample 476 [...] 477 Import: 478 docker: github.com/tooploox/oya-packs/docker 479 [...] 480 ``` 481 482 Let's now generate a Dockerfile for our server: 483 484 ``` bash 485 $ oya run docker.generate 486 ``` 487 488 This is what the Dockerfile looks like: 489 490 ``` bash 491 $ cat Dockefile 492 ``` 493 ``` dockerfile 494 FROM golang 495 496 COPY . /go/src/app 497 WORKDIR /go/src/app 498 499 RUN go get 500 RUN go build -o app 501 502 CMD [ "app" ] 503 ``` 504 505 You can build the image and start the server in a container: 506 507 ``` bash 508 $ oya run docker.build 509 [...] 510 $ oya run docker.run 511 Starting web server on 0.0.0.0:8080 512 ``` 513 514 # Generating boilerplate 515 516 Oya can also render files and even entire directories from templates. Oya uses 517 [Plush templating engine](https://github.com/gobuffalo/plush). 518 519 In an earlier section you were asked to copy & paste a simple web server. For 520 the sake of illustration imagine that you want to make creating HTTP web servers 521 easier by generating the boilerplate from a template. 522 523 Let's create `app.go` in `templates/` directory: 524 525 ``` bash 526 $ cat templates/app.go 527 ``` 528 ``` golang 529 package main 530 531 import ( 532 "flag" 533 "fmt" 534 "log" 535 "net/http" 536 ) 537 538 func handler(w http.ResponseWriter, r *http.Request) { 539 fmt.Fprintf(w, "Hello, world!") 540 } 541 542 func main() { 543 host := flag.String("host", "0.0.0.0", "host name to bind to") 544 port := flag.Int("port", 8080, "port number") 545 flag.Parse() 546 http.HandleFunc("/", handler) 547 bind := fmt.Sprintf("%s:%d", *host, *port) 548 fmt.Printf("Starting web server on %s\n", bind) 549 log.Fatal(http.ListenAndServe(bind, nil)) 550 } 551 EOT 552 ``` 553 554 This is how you can render it to the current directory (the command will 555 override `app.go` so ****be careful!**): 556 557 ``` bash 558 $ oya render templates/app.go 559 ``` 560 561 562 # Parametrizing boilerplate 563 564 # Reusing your scripts 565 566 Ok, all is good and fine but how to make the ^ code available when creating new 567 projects? Easy, turn it into a pack! 568 569 Technically, all you need to do to turn the project we created into an Oya pack 570 is push it to Github and tag it with a version number by adding & pushing a git 571 tag in the right format (e.g. `v0.1.0`) along with a few small changes. 572 573 Rather than doing that, let's create it step-by-step in a fresh new git repo. 574 575 First, let's add a task you'll use to generate boilerplate to the original 576 Oyafile so it looks like this: 577 578 ``` bash 579 $ cat Oyafile 580 ``` 581 ``` yaml 582 Project: project 583 584 build: | 585 go build . 586 587 start: | 588 go run . --port ${Oya[port]} --host ${Oya[host]} 589 590 generate: | 591 oya render ${Oya[BasePath]}/templates/app.go 592 ``` 593 594 The new `generate` task will generate files into the current directory based on 595 the contents of the templates directory. 596 597 > BasePath is the base directory of the path so the `oya render` command knows 598 > where to take templates from. 599 600 Because the script needs `port` and `host`, let's also create `values.oya` 601 containing pack defaults: 602 603 ``` bash 604 $ cat values.oya 605 ``` 606 ``` yaml 607 port: 8080 608 host: localhost 609 ``` 610 611 To share your pack all you need is push the project to a Github repository and 612 tag it with a version number, roughly: 613 614 ``` bash 615 $ git push origin 616 $ git tag v0.1.0 617 $ git push --tags 618 ``` 619 620 That's it! 621 622 > Currently, only Github is supported as a way of sharing packs but if you want 623 > to help with adding support for Bitbucket and others, do get in touch! 624 625 So how do you use the pack? Easy. First, create a new empty project and 626 initialize it: 627 628 ``` bash 629 $ oya init OyaExample 630 ``` 631 632 Then import the pack. Here, I'm assuming it's under 633 github.com/tooploox/oya-gohttp: 634 635 ``` bash 636 $ oya import github.com/tooploox/oya-gohttp 637 ``` 638 639 That's it! Let's see what tasks we have available: 640 641 642 ``` bash 643 $ oya tasks 644 # in ./Oyafile 645 oya run oya-gohttp.build 646 oya run oya-gohttp.generate 647 oya run oya-gohttp.start 648 ``` 649 650 Let's generate the server source: 651 652 ``` bash 653 $ oya run oya-gohttp.generate 654 ``` 655 656 This will generate `app.go` file in the current directory: 657 658 ``` bash 659 $ cat app.go 660 ``` 661 ``` golang 662 package main 663 664 import ( 665 [...] 666 ``` 667 668 Let's start the server: 669 670 ``` bash 671 $ oya run oya-gohttp.start 672 ``` 673 674 # Overriding pack values 675 676 > TODO: Overriding port. 677 678 679 # Pack repositories 680 681 TODO: Describe multiple packs in a single repo (i.e. how to version and use them). 682 683 684 # Running tasks recursively 685 686 So far we only talked about one Oyafile in the project's top-level directory. 687 But you can put Oyafiles in subdirectories. 688 689 This is especially useful in monorepos so we'll use that as an example. Imagine 690 we have a web application consisting of a REST API back-end server and front-end 691 SPA application. 692 693 For the sake of illustration let's create a mono-repository containing both 694 back-end and front-end in separate directories. 695 696 ``` bash 697 $ tree 698 . 699 ├── Oyafile 700 ├── backend 701 │ ├── Oyafile 702 │ ├── server.go 703 └── frontend 704 ├── Oyafile 705 └── main.ts 706 707 2 directories, 3 files 708 ``` 709 710 In addition to the top-level `Oyafile` both front-end and back-end have their 711 own `Oyafiles`, let's have a look at each. 712 713 # Top-level Oyafile 714 715 The top level Oyafile contains a `Project:` directive to mark the top-level 716 project directory and a task named `build` preparing the output directory. 717 718 ``` bash 719 $ cat Oyafile 720 ``` 721 ``` yaml 722 Project: myproject 723 724 build: | 725 echo "Preparing build/ folder" 726 rm -rf build/ && mkdir -p build/public 727 ``` 728 729 ## Backend 730 731 In this example, let's assume that back-end is an HTTP API written in Go. Here 732 is how the back-end `Oyafile` looks like: 733 734 735 ``` bash 736 $ cat backend/Oyafile 737 ``` 738 ``` yaml 739 build: | 740 echo "Compiling server" 741 go build -o ../build/server . 742 ``` 743 744 Notice that it contains just one task, named `build` for compiling the back-end 745 server. 746 747 ## Frontend 748 749 Front-end is a TypeScript SPA application. The `build` task compiles TypeScript 750 source to JavaScript: 751 752 753 ``` bash 754 $ cat frontend/Oyafile 755 ``` 756 ``` yaml 757 build: | 758 echo "Compiling front-end" 759 tsc main.ts --outFile ../build/public/main.js 760 ``` 761 762 ## Recursive run 763 764 Now let’s list the available tasks. To do it for the whole project including 765 subdirectories we need to use `-r or --recurse` flag. 766 767 ``` bash 768 $ oya tasks -r 769 ``` 770 ``` yaml 771 # in ./Oyafile 772 oya run build 773 774 # in ./backend/Oyafile 775 oya run build 776 777 # in ./frontend/Oyafile 778 oya run build 779 ``` 780 781 As you can see we have three `build` tasks one per Oyafile. We can now run them 782 all. 783 784 ``` bash 785 $ oya run -r build 786 Preparing build/ folder 787 Compiling server 788 Compiling front-end 789 ``` 790 791 The result is the compiled server as well as the JavaScript it serves. 792 793 TODO: For a working example, clone ... 794 795 # Contributing 796 797 1. Install go 1.11 (goenv is recommended, example: `goenv install 1.11.4`). 798 2. Checkout oya outside GOHOME. 799 3. Install godog: `go get -u github.com/cucumber/godog/cmd/godog`. 800 4. Run acceptance tests: `godog`. 801 802 > For all test to pass you need to import the PGP key used to encrypt secrets. 803 > $ gpg --import testutil/pgp/private.rsa 804 805 5. Run tests: `go test ./...`. 806 6. Run Oya: `go run oya.go`.