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`.