├── .dockerignore ├── .editorconfig ├── .githooks ├── commit-msg ├── pre-commit └── pre-push ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .go-version ├── .golangci.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── Melody.toml ├── README.md ├── actions ├── action.go ├── assert │ ├── action.go │ ├── action_test.go │ ├── decoder.go │ ├── decoder_test.go │ └── testdata │ │ ├── assert-decoder.hcl │ │ └── assert-evaluation.hcl ├── attributes.go ├── attributes_test.go ├── block │ ├── block.go │ ├── block_test.go │ └── testdata │ │ └── test.hcl ├── call │ ├── action.go │ └── decoder.go ├── decoder.go ├── executor.go ├── http │ ├── action.go │ ├── decoder.go │ └── internal │ │ ├── decoder │ │ ├── connection.go │ │ ├── cookie.go │ │ ├── decoder.go │ │ ├── headers.go │ │ ├── payload.go │ │ ├── queryParams.go │ │ ├── request.go │ │ └── response.go │ │ └── executor │ │ ├── http.go │ │ └── variables.go ├── mongo │ ├── action.go │ ├── decoder.go │ ├── examples │ │ └── mongo.hcl │ ├── internal │ │ ├── decoder │ │ │ ├── auth.go │ │ │ ├── common.go │ │ │ ├── connection.go │ │ │ ├── decoder.go │ │ │ ├── query.go │ │ │ └── response.go │ │ ├── executor │ │ │ ├── executor.go │ │ │ └── operations.go │ │ └── helper │ │ │ ├── maps.go │ │ │ └── value.go │ └── testdata │ │ └── sample.hcl ├── print │ ├── action.go │ ├── action_test.go │ ├── decoder.go │ ├── decoder_test.go │ ├── internal │ │ ├── helper.go │ │ ├── print.go │ │ └── print_test.go │ └── testdata │ │ ├── scenario1.hcl │ │ ├── scenario2.hcl │ │ ├── scenario3.hcl │ │ └── spec.hcl ├── register │ ├── decoder.go │ └── handler.go ├── set │ ├── action.go │ ├── decoder.go │ └── testdata │ │ └── set-decoder.hcl ├── shared │ └── parse.go └── sleep │ ├── action.go │ ├── action_test.go │ ├── decoder.go │ ├── decoder_test.go │ └── testdata │ └── scenario1.hcl ├── build └── docker │ └── Dockerfile ├── cmd └── orion │ ├── common │ ├── config.go │ ├── flags.go │ └── print.go │ ├── help │ └── help.go │ ├── init │ └── init.go │ ├── lint │ └── lint.go │ ├── orion.go │ └── run │ ├── run.go │ ├── run_test.go │ └── testdata │ ├── feature001.hcl │ ├── feature002.hcl │ ├── feature003.hcl │ ├── feature004.hcl │ └── variables004.hcl ├── context ├── context.go ├── context_test.go ├── doc.go ├── metrics.go ├── metrics_test.go ├── variables.go └── variables_test.go ├── docs ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _about │ ├── contact.md │ └── contributing.md ├── _config.local.yml ├── _config.yml ├── _gettingstarted │ ├── examples.md │ ├── installation.md │ ├── links.md │ └── usage.md ├── _spec │ ├── actions │ │ ├── assert.md │ │ ├── block.md │ │ ├── call.md │ │ ├── http.md │ │ ├── index.md │ │ ├── mongo.md │ │ ├── print.md │ │ ├── set.md │ │ └── sleep.md │ ├── features │ │ ├── args.md │ │ ├── functions.md │ │ ├── hooks.md │ │ ├── includes.md │ │ ├── index.md │ │ ├── scenarios.md │ │ └── vars.md │ ├── functions │ │ ├── boolean.md │ │ ├── collection.md │ │ ├── format.md │ │ ├── index.md │ │ ├── number.md │ │ └── text.md │ └── syntax.md ├── assets │ ├── css │ │ └── custom.css │ ├── logo.jpg │ └── work-in-progress.png ├── docker-compose.yml ├── index.markdown ├── medium-resources │ ├── step-1 │ │ └── feature-math-operations.hcl │ ├── step-2 │ │ └── feature-math-operations.hcl │ ├── step-3 │ │ ├── feature-math-operations.hcl │ │ └── vars-math-operations.hcl │ ├── step-4 │ │ ├── feature-math-operations.hcl │ │ └── vars-math-operations.hcl │ ├── step-5 │ │ ├── feature-math-operations.hcl │ │ ├── scenario-sub.hcl │ │ ├── scenario-sum.hcl │ │ └── vars-math-operations.hcl │ ├── step-6 │ │ ├── feature-math-operations.hcl │ │ ├── scenario-sub.hcl │ │ ├── scenario-sum.hcl │ │ └── vars-math-operations.hcl │ └── step-7 │ │ ├── feature-math-operations.hcl │ │ ├── scenario-sub.hcl │ │ ├── scenario-sum.hcl │ │ └── vars-math-operations.hcl └── samples │ ├── functions │ └── feature001.hcl │ ├── hooks │ ├── feature.hcl │ └── feature2.hcl │ ├── includes │ ├── feature-with-includes.hcl │ └── hooks.hcl │ ├── operations │ ├── boolean-comparison.hcl │ ├── collection.hcl │ ├── formatter.hcl │ ├── number-comparisons.hcl │ ├── number-math-operations.hcl │ ├── text-comparison.hcl │ ├── text-converter.hcl │ ├── text-others.hcl │ ├── text-search.hcl │ ├── text │ │ ├── arguments.hcl │ │ ├── feature-eq.hcl │ │ ├── feature-eqIgnoreCase.hcl │ │ ├── feature-toLowercase.hcl │ │ ├── feature-toUppercase copy.hcl │ │ ├── feature-toUppercase.hcl │ │ ├── feature-trim.hcl │ │ └── feature-trimPrefix.hcl │ └── trimPrefix.md │ ├── plugins │ └── mongo │ │ └── sample.hcl │ ├── scenarios │ ├── scenario2.hcl │ └── scenarios.hcl │ ├── traits │ └── feature.hcl │ ├── variables │ ├── feature.hcl │ └── variables.hcl │ └── vars │ └── feature001.hcl ├── dsl ├── arg.go ├── arg_test.go ├── body.go ├── body_test.go ├── constants.go ├── doc.go ├── feature.go ├── functions.go ├── given.go ├── helper.go ├── hook.go ├── include.go ├── input.go ├── return.go ├── scenario.go ├── section.go ├── testdata │ ├── args.hcl │ └── body.hcl ├── then.go ├── vars.go └── when.go ├── executor ├── decoder.go ├── executor.go ├── parser.go ├── parser_test.go └── testdata │ ├── feature.hcl │ ├── feature2.hcl │ ├── feature3.hcl │ ├── variables.error.hcl │ ├── variables.error2.hcl │ ├── variables.error3.hcl │ └── variables.hcl ├── functions ├── collection_operations.go ├── doc.go ├── eval.go ├── functions.go ├── heper.go ├── json.go ├── len.go ├── number_math_ops.go ├── string.go ├── string_comparison.go ├── string_search.go └── string_transform.go ├── go.mod ├── go.sum ├── helper ├── attributes.go ├── attributes_test.go ├── bool.go ├── bool_test.go ├── duration.go ├── duration_test.go ├── int.go ├── int_test.go ├── interface.go ├── interface_test.go ├── string.go ├── string_test.go ├── testdata │ └── attributes.hcl ├── types.go └── types_test.go ├── internal ├── config │ └── config.go ├── errors │ ├── code.go │ ├── custom.go │ ├── diagnostics.go │ ├── doc.go │ ├── errors.go │ └── errors_test.go ├── info.go ├── logger │ ├── config.go │ ├── formatter.go │ └── setup.go └── oriontest │ └── test_utils.go ├── scripts ├── build.sh ├── deps.sh ├── format.sh ├── lint.sh ├── publish-docker.sh ├── run.sh ├── setup.sh └── test.sh ├── testutil ├── hcl.go └── parser.go └── tools └── tools.go /.dockerignore: -------------------------------------------------------------------------------- 1 | docs 2 | .ics/examples 3 | githooks 4 | .idea 5 | .gitignore 6 | .git 7 | Makefile 8 | README.md 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [{*.go,Makefile,.gitmodules,go.mod,go.sum}] 10 | indent_style = tab 11 | 12 | [*.md] 13 | indent_style = tab 14 | trim_trailing_whitespace = false 15 | 16 | [*.{yml,yaml,json}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.{js,jsx,ts,tsx,css,less,sass,scss,vue,py}] 21 | indent_style = space 22 | indent_size = 4 -------------------------------------------------------------------------------- /.githooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | INPUT_FILE=$1 4 | START_LINE=`head -n1 $INPUT_FILE` 5 | PATTERN="^(feat|refactor|docs|fix|test)(:)(.*)" 6 | if ! [[ "$START_LINE" =~ $PATTERN ]]; then 7 | echo "" 8 | echo "[ERROR] Bad commit message, see example: {prefix}: commit message" 9 | echo "Supported values for {prefix} are: feat,refactor,test,fix and docs" 10 | echo "" 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | make fmt 4 | -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | make lint 4 | if ! [[ "$?" =~ 0 ]]; then 5 | exit 1 6 | fi 7 | make test 8 | if ! [[ "$?" =~ 0 ]]; then 9 | exit 1 10 | fi 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md : -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | test_report 3 | bin 4 | dist 5 | vendor 6 | coverage.txt 7 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesovilabs/orion/f8becd33b5ea5e3d09a74f3026d4cf3ae58f9fab/.go-version -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.15.x 5 | 6 | before_script: 7 | - go mod vendor 8 | 9 | script: 10 | - make lint 11 | - make test-coverage 12 | - make build 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | 17 | before_deploy: 18 | - go mod vendor 19 | deploy: 20 | - provider: script 21 | skip_cleanup: true 22 | script: make build-release 23 | on: 24 | tags: true 25 | - provider: releases 26 | skip_cleanup: true 27 | draft: true 28 | api_key: $GITHUB_OAUTH_TOKEN 29 | file: 30 | - bin/orion.exe 31 | - bin/orion.darwin 32 | - bin/orion.linux 33 | on: 34 | tags: true 35 | - provider: script 36 | skip_cleanup: true 37 | script: make build-docker 38 | on: 39 | tags: true 40 | - provider: script 41 | skip_cleanup: true 42 | script: bash scripts/publish-docker.sh 43 | on: 44 | tags: true 45 | 46 | notifications: 47 | email: 48 | on_success: change 49 | on_failure: always 50 | 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Visit [the open issues](https://github.com/wesovilabs/orion/issues) and feel free to take one of this. 4 | 5 | This is only the beginning! By the way, If you missed any action, or you found a bug, please [create a new issue on Github or vote the existing ones](https://github.com/wesovilabs/orion/issues)! 6 | 7 | 8 | ## Pull Request Checklist 9 | 10 | Before sending your pull requests, make sure you followed this list. 11 | 12 | - Read [contributing guidelines](CONTRIBUTING.md). 13 | - Read [Code of Conduct](CODE_OF_CONDUCT.md). 14 | - A pull request must b 15 | - Run unit tests (`make test`) 16 | - Run linter checks ( `make lint`) 17 | - Code coverage must be equal or higher than the existing one. 18 | 19 | Before starting to code, we recommend you to execute command `make setup`. This command downloads the dependencies, but it also 20 | creates a set of useful git hooks. 21 | 22 | Keep in mind the below considerations: 23 | 24 | - Commits must be descriptive and starts by one of the following prefixes `feat`, `fix`, `refactor`, `test` and`docs` 25 | - Directory `vendor` mustn't be pushed to repository. 26 | - Write code thinking other people have to read it. 27 | - Update documentation when required. Documentation can be found in [docs/](/docs). 28 | 29 | 30 | From the code, you can run any example with command 31 | 32 | ```bash 33 | go run ./cmd/orion/orion.go run --input 34 | go run ./cmd/orion/orion.go run --input --vars 35 | ``` 36 | 37 | ## Others 38 | 39 | Additionally, you can contribute to Orion by reviewing documentation and making it clearer for others 40 | or writing `Guides & Tutorials`. 41 | 42 | In case of you like Orion, please click on like button or sharing the project with your networks. 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Iván Corrales Solera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: setup fmt lint test test-e2e build build-docker 2 | 3 | .PHONY: setup 4 | setup: # 5 | sh scripts/setup.sh 6 | 7 | .PHONY: fmt 8 | fmt: ; @ ## Code formatter 9 | sh scripts/format.sh 10 | 11 | .PHONY: lint 12 | lint: ; @ ## Code analysis 13 | sh scripts/lint.sh 14 | 15 | .PHONY: test 16 | test: test-unit 17 | 18 | .PHONY: test-% 19 | test-%: ; @ ## Run tests 20 | TEST_MODE=$* sh scripts/test.sh 21 | 22 | .PHONY: build 23 | build: build-dev 24 | 25 | .PHONY: build-% 26 | build-%: 27 | BUILD_MODE=$* sh scripts/build.sh 28 | 29 | .PHONY: run 30 | run: run-code; 31 | 32 | .PHONY: run-% 33 | run-%: ; 34 | RUN_MODE=$* sh scripts/run.sh 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/wesovilabs/orion.svg?branch=master)](https://travis-ci.org/wesovilabs/orion) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/wesovilabs/orion)](https://goreportcard.com/report/github.com/wesovilabs/orion) 3 | [![GoDoc](https://godoc.org/github.com/wesovilabs/orion?status.svg)](https://godoc.org/github.com/wesovilabs/orion) 4 | [![codecov](https://codecov.io/gh/wesovilabs/orion/branch/master/graph/badge.svg)](https://codecov.io/gh/wesovilabs/orion) 5 | 6 | 7 | # Orion 8 | 9 | Orion is born to change the way we implement our acceptance tests. It takes advantage of HCL from Hashicorp t 10 | o provide a **simple DSL to write the acceptance tests**. The syntax is inspired in Gherkin. 11 | 12 | > Please visit full documentation [http://www.wesovilabs.com/orion/](http://www.wesovilabs.com/orion/) 13 | 14 | ## Tutorials & Guides 15 | 16 | - [Orion: A next-generation automation testing tool](https://ivan-corrales-solera.medium.com/orion-a-next-generation-automation-testing-tool-4ea53eeb2517) 17 | - [Orion in action: Testing a real API](https://ivan-corrales-solera.medium.com/orion-in-action-testing-a-real-api-a780244141a2) 18 | 19 | Additionally, a repository with a list of examples can be found [here](https://github.com/wesovilabs/orion-examples) 20 | 21 | ## Contact 22 | 23 | Don't hesitate to reach me by email at `ivan.corrales.solera@gmail.com` or send me a message through of 24 | [Linkedin](https://www.linkedin.com/in/ivan-corrales-solera/) or [Twitter](https://twitter.com/wesovilabs) 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /actions/assert/action.go: -------------------------------------------------------------------------------- 1 | // Package assert contains types and methods used by the plugin 2 | package assert 3 | 4 | import ( 5 | "github.com/hashicorp/hcl/v2" 6 | "github.com/wesovilabs/orion/actions" 7 | "github.com/wesovilabs/orion/context" 8 | "github.com/wesovilabs/orion/helper" 9 | "github.com/wesovilabs/orion/internal/errors" 10 | ) 11 | 12 | const ( 13 | defaultAssertion = false 14 | ) 15 | 16 | // Assert plugin definition. 17 | type Assert struct { 18 | *actions.Base 19 | assertion hcl.Expression 20 | } 21 | 22 | // SetAssertion establish the assertion. 23 | func (a *Assert) SetAssertion(assertion hcl.Expression) { 24 | a.assertion = assertion 25 | } 26 | 27 | // Assertion obtain the assertion. 28 | func (a *Assert) Assertion() hcl.Expression { 29 | return a.assertion 30 | } 31 | 32 | // Execute execute the assert plugin. 33 | func (a *Assert) Execute(ctx context.OrionContext) errors.Error { 34 | return actions.Execute(ctx, a.Base, func(ctx context.OrionContext) errors.Error { 35 | assertion, err := helper.GetExpressionValueAsBool(ctx.EvalContext(), a.assertion, defaultAssertion) 36 | if err != nil { 37 | return err 38 | } 39 | if !assertion { 40 | err := errors.Unexpected("assertion is not satisfied!") 41 | if a.assertion != nil { 42 | return err.AddMeta(errors.MetaLocation, a.assertion.Range().String()) 43 | } 44 | return err 45 | } 46 | return nil 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /actions/assert/action_test.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/wesovilabs/orion/context" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/wesovilabs/orion/actions/shared" 11 | "github.com/zclconf/go-cty/cty" 12 | ) 13 | 14 | var decoder = new(Decoder) 15 | 16 | var assertPluginResult = []bool{true, false, false, true, true, true, false, true, false, false} 17 | 18 | func TestAssert_Assertion(t *testing.T) { 19 | content, err := shared.GetBodyContent(path.Join("testdata", "assert-evaluation.hcl"), BlockAssert, []string{}) 20 | assert.Nil(t, err) 21 | assert.NotNil(t, content) 22 | ctx := context.New(map[string]cty.Value{ 23 | "person": cty.ObjectVal(map[string]cty.Value{ 24 | "lastname": cty.StringVal("Robot"), 25 | "age": cty.NumberIntVal(int64(20)), 26 | }), 27 | }, nil) 28 | 29 | for index := range content.Blocks { 30 | expected := assertPluginResult[index] 31 | plugin, err := decoder.DecodeBlock(content.Blocks[index]) 32 | assert.Nil(t, err) 33 | assert.NotNil(t, plugin) 34 | a, ok := plugin.(*Assert) 35 | assert.True(t, ok) 36 | assert.NotNil(t, a) 37 | assert.NotNil(t, a.Assertion()) 38 | err = a.Execute(ctx) 39 | if expected { 40 | assert.Nil(t, err) 41 | continue 42 | } 43 | assert.NotNil(t, err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /actions/assert/decoder.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/actions" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | const ( 10 | // BlockAssert identifier of the plugin. 11 | BlockAssert = "assert" 12 | argAssertion = "assertion" 13 | ) 14 | 15 | var schemaAssert = &hcl.BodySchema{ 16 | Attributes: append(actions.BaseArguments, []hcl.AttributeSchema{ 17 | {Name: argAssertion, Required: true}, 18 | }...), 19 | } 20 | 21 | // Decoder implements interface plugin.Decoder. 22 | type Decoder struct{} 23 | 24 | // BlockHeaderSchema return the header schema for the plugin. 25 | func (dec *Decoder) BlockHeaderSchema() hcl.BlockHeaderSchema { 26 | return hcl.BlockHeaderSchema{ 27 | Type: BlockAssert, 28 | LabelNames: nil, 29 | } 30 | } 31 | 32 | // DecodeBlock required method implementation by interface Decoder. 33 | func (dec *Decoder) DecodeBlock(block *hcl.Block) (actions.Action, errors.Error) { 34 | bodyContent, d := block.Body.Content(schemaAssert) 35 | if err := errors.EvalDiagnostics(d); err != nil { 36 | return nil, err 37 | } 38 | assert := &Assert{Base: &actions.Base{}} 39 | if err := populateAttributes(assert, bodyContent.Attributes); err != nil { 40 | return nil, err 41 | } 42 | return assert, nil 43 | } 44 | 45 | func populateAttributes(assert *Assert, attrs hcl.Attributes) errors.Error { 46 | for name := range attrs { 47 | attribute := attrs[name] 48 | switch { 49 | case actions.IsCommonAttribute(name): 50 | if err := actions.SetBaseArgs(assert, attribute); err != nil { 51 | return err 52 | } 53 | case name == argAssertion: 54 | assert.SetAssertion(attribute.Expr) 55 | default: 56 | return errors.ThrowUnsupportedArgument(BlockAssert, name) 57 | } 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /actions/assert/decoder_test.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/hashicorp/hcl/v2/hclsyntax" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/wesovilabs/orion/actions/shared" 11 | ) 12 | 13 | var assertDecoderPath = path.Join( 14 | "testdata", "assert-decoder.hcl", 15 | ) 16 | 17 | var assertDecoderBlocks = []*Assert{ 18 | { 19 | assertion: &hclsyntax.TemplateExpr{ 20 | SrcRange: hcl.Range{ 21 | Filename: assertDecoderPath, 22 | Start: shared.CreatePos(2, 3, 11), 23 | End: shared.CreatePos(2, 19, 27), 24 | }, 25 | }, 26 | }, 27 | { 28 | assertion: &hclsyntax.TemplateExpr{ 29 | SrcRange: hcl.Range{ 30 | Filename: assertDecoderPath, 31 | Start: shared.CreatePos(6, 3, 42), 32 | End: shared.CreatePos(6, 28, 67), 33 | }, 34 | }, 35 | }, 36 | } 37 | 38 | func TestDecoder_DecodeBlock(t *testing.T) { 39 | content, err := shared.GetBodyContent(assertDecoderPath, BlockAssert, []string{}) 40 | assert.Nil(t, err) 41 | assert.NotNil(t, content) 42 | 43 | assert.Len(t, content.Blocks, len(assertDecoderBlocks)) 44 | for index := range content.Blocks { 45 | block := content.Blocks[index] 46 | expectedBlock := assertDecoderBlocks[index] 47 | assertBlockAssert(t, block, expectedBlock) 48 | } 49 | } 50 | 51 | func assertBlockAssert(t *testing.T, block *hcl.Block, a *Assert) { 52 | assert.Len(t, block.Labels, 0) 53 | assert.Equal(t, BlockAssert, block.Type) 54 | c, d := block.Body.Content(schemaAssert) 55 | assert.Nil(t, d) 56 | assert.NotNil(t, c) 57 | for name := range c.Attributes { 58 | attribute := c.Attributes[name] 59 | switch name { 60 | case argAssertion: 61 | assert.NotNil(t, a.assertion) 62 | assert.Equal(t, a.assertion.Range().Start, attribute.Range.Start) 63 | assert.Equal(t, a.assertion.Range().End, attribute.Range.End) 64 | default: 65 | assert.Failf(t, "error", "unexpected attribute %s", name) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /actions/assert/testdata/assert-decoder.hcl: -------------------------------------------------------------------------------- 1 | assert { 2 | assertion = true 3 | } 4 | 5 | assert { 6 | assertion = true || false 7 | } -------------------------------------------------------------------------------- /actions/assert/testdata/assert-evaluation.hcl: -------------------------------------------------------------------------------- 1 | assert { 2 | assertion = true 3 | when = true 4 | } 5 | 6 | assert { 7 | assertion = false 8 | } 9 | 10 | assert { 11 | assertion = true && false 12 | } 13 | 14 | assert { 15 | assertion = person.lastname == "Robot" 16 | } 17 | 18 | assert { 19 | assertion = person.lastname == "Robot" || person.age <15 20 | } 21 | 22 | assert { 23 | assertion = person.lastname == "Robot" && person.age >15 24 | } 25 | 26 | assert { 27 | assertion = person.lastname == "Robotic" || person.age <15 28 | } 29 | 30 | assert { 31 | assertion = true 32 | } 33 | 34 | assert { 35 | assertion = unknown 36 | } 37 | 38 | -------------------------------------------------------------------------------- /actions/attributes.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | ) 6 | 7 | const ( 8 | // ArgDesc contains the name of the argument used in the plugins block. 9 | ArgDesc = "description" 10 | // ArgWhen contains the name of the argument used in the plugins block. 11 | ArgWhen = "when" 12 | // ArgWhile contains the name of the argument used in the plugins block. 13 | ArgWhile = "while" 14 | // ArgCount contains the name of the argument used in the plugins block. 15 | ArgCount = "count" 16 | // ArgItems contains the name of the slice to be iterated over. 17 | ArgItems = "items" 18 | ) 19 | 20 | // BaseArguments contain the list of common attributes. 21 | var BaseArguments = []hcl.AttributeSchema{ 22 | {Name: ArgDesc, Required: false}, 23 | {Name: ArgWhen, Required: false}, 24 | {Name: ArgWhile, Required: false}, 25 | {Name: ArgCount, Required: false}, 26 | {Name: ArgItems, Required: false}, 27 | } 28 | 29 | var baseArgumentsNames = map[string]struct{}{ 30 | ArgDesc: {}, 31 | ArgWhen: {}, 32 | ArgWhile: {}, 33 | ArgCount: {}, 34 | ArgItems: {}, 35 | } 36 | 37 | // IsCommonAttribute returns true if the name is common for all the plugins. 38 | func IsCommonAttribute(name string) bool { 39 | _, ok := baseArgumentsNames[name] 40 | 41 | return ok 42 | } 43 | -------------------------------------------------------------------------------- /actions/attributes_test.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsPluginBaseArgument(t *testing.T) { 10 | assert.False(t, IsCommonAttribute("_")) 11 | assert.True(t, IsCommonAttribute(ArgCount)) 12 | } 13 | -------------------------------------------------------------------------------- /actions/block/block_test.go: -------------------------------------------------------------------------------- 1 | package block 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/wesovilabs/orion/testutil" 8 | ) 9 | 10 | var testDecoder = &Decoder{} 11 | 12 | func assertDecodeBlockError(t *testing.T, input string) { 13 | content := testutil.GetStringAsBodyContent(input, BlockBlock, []string{}) 14 | assert.NotNil(t, content) 15 | block := content.Blocks[0] 16 | action, err := testDecoder.DecodeBlock(block) 17 | assert.NotNil(t, err) 18 | assert.Nil(t, action) 19 | } 20 | 21 | func TestDecoder_DecodeBlock(t *testing.T) { 22 | input := ` 23 | block { 24 | print { 25 | msg = "Hi" 26 | } 27 | } 28 | ` 29 | content := testutil.GetStringAsBodyContent(input, BlockBlock, []string{}) 30 | assert.NotNil(t, content) 31 | 32 | block := content.Blocks[0] 33 | action, err := testDecoder.DecodeBlock(block) 34 | assert.Nil(t, err) 35 | assert.NotNil(t, action) 36 | assert.Equal(t, BlockBlock, action.String()) 37 | actionBlock, ok := action.(*Block) 38 | assert.True(t, ok) 39 | assert.NotNil(t, actionBlock) 40 | assert.Len(t, actionBlock.actions, 1) 41 | 42 | input = ` 43 | block { 44 | }` 45 | assertDecodeBlockError(t, input) 46 | input = ` 47 | block { 48 | unsupported = 123 49 | }` 50 | assertDecodeBlockError(t, input) 51 | input = ` 52 | block { 53 | name = unknown 54 | }` 55 | assertDecodeBlockError(t, input) 56 | input = ` 57 | block { 58 | unknown {} 59 | }` 60 | assertDecodeBlockError(t, input) 61 | } 62 | -------------------------------------------------------------------------------- /actions/block/testdata/test.hcl: -------------------------------------------------------------------------------- 1 | block { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /actions/call/action.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/actions" 6 | "github.com/wesovilabs/orion/context" 7 | "github.com/wesovilabs/orion/helper" 8 | "github.com/wesovilabs/orion/internal/errors" 9 | "github.com/zclconf/go-cty/cty" 10 | ) 11 | 12 | // Call used to invoke functions. 13 | type Call struct { 14 | *actions.Base 15 | name string 16 | as string 17 | with hcl.Attributes 18 | } 19 | 20 | func (c *Call) populateAttributes(attrs hcl.Attributes) errors.Error { 21 | for name := range attrs { 22 | attribute := attrs[name] 23 | switch { 24 | case actions.IsCommonAttribute(name): 25 | if err := actions.SetBaseArgs(c, attribute); err != nil { 26 | return err 27 | } 28 | case name == argAs: 29 | as, err := helper.AttributeToStringWithoutContext(attribute) 30 | if err != nil { 31 | return err 32 | } 33 | c.as = as 34 | default: 35 | return errors.ThrowUnsupportedArgument(BlockCall, name) 36 | } 37 | } 38 | return nil 39 | } 40 | 41 | func (c *Call) populateBlocks(blocks hcl.Blocks) errors.Error { 42 | if len(blocks) > 1 { 43 | return errors.ThrowsExceededNumberOfBlocks(blockWith, 1) 44 | } 45 | if len(blocks) == 0 { 46 | return nil 47 | } 48 | attributes, d := blocks[0].Body.JustAttributes() 49 | if err := errors.EvalDiagnostics(d); err != nil { 50 | return err 51 | } 52 | c.with = attributes 53 | return nil 54 | } 55 | 56 | func cloneEvalContext(ctx *hcl.EvalContext) *hcl.EvalContext { 57 | vars := make(map[string]cty.Value) 58 | for name, val := range ctx.Variables { 59 | vars[name] = val 60 | } 61 | return &hcl.EvalContext{ 62 | Variables: vars, 63 | Functions: ctx.Functions, 64 | } 65 | } 66 | 67 | func (c *Call) Execute(ctx context.OrionContext) errors.Error { 68 | fn, ok := ctx.Functions()[c.name] 69 | oldVariables := cloneEvalContext(ctx.EvalContext()).Variables 70 | for index := range c.with { 71 | arg := c.with[index] 72 | val, d := arg.Expr.Value(ctx.EvalContext()) 73 | if err := errors.EvalDiagnostics(d); err != nil { 74 | return err 75 | } 76 | ctx.EvalContext().Variables[arg.Name] = val 77 | } 78 | if !ok { 79 | return errors.IncorrectUsage("missing required function '%s'", c.name) 80 | } 81 | defer func() { 82 | oldVariables[c.as] = ctx.EvalContext().Variables[c.as] 83 | ctx.EvalContext().Variables = oldVariables 84 | }() 85 | return fn(ctx, c.as) 86 | } 87 | -------------------------------------------------------------------------------- /actions/call/decoder.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/wesovilabs/orion/actions" 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | const ( 11 | // BlockCall identifier of action. 12 | BlockCall = "call" 13 | blockWith = "with" 14 | labelName = "name" 15 | argAs = "as" 16 | ) 17 | 18 | var schemaCall = &hcl.BodySchema{ 19 | Attributes: append(actions.BaseArguments, []hcl.AttributeSchema{ 20 | {Name: argAs, Required: false}, 21 | }...), 22 | Blocks: []hcl.BlockHeaderSchema{ 23 | {Type: blockWith, LabelNames: nil}, 24 | }, 25 | } 26 | 27 | // Decoder implement interface Decoder. 28 | type Decoder struct{} 29 | 30 | // BlockHeaderSchema return the header schema for the plugin. 31 | func (dec *Decoder) BlockHeaderSchema() hcl.BlockHeaderSchema { 32 | return hcl.BlockHeaderSchema{ 33 | Type: BlockCall, 34 | LabelNames: []string{labelName}, 35 | } 36 | } 37 | 38 | // DecodeBlock inherited method from interface Decoder. 39 | func (dec *Decoder) DecodeBlock(block *hcl.Block) (actions.Action, errors.Error) { 40 | log.Tracef("it starts decoding of block %s", BlockCall) 41 | bodyContent, d := block.Body.Content(schemaCall) 42 | if err := errors.EvalDiagnostics(d); err != nil { 43 | return nil, err 44 | } 45 | if len(block.Labels) != 1 { 46 | return nil, errors.ThrowMissingRequiredLabel(BlockCall) 47 | } 48 | call := &Call{ 49 | name: block.Labels[0], 50 | Base: &actions.Base{}, 51 | } 52 | if err := call.populateAttributes(bodyContent.Attributes); err != nil { 53 | return nil, err 54 | } 55 | if err := call.populateBlocks(bodyContent.Blocks); err != nil { 56 | return nil, err 57 | } 58 | return call, nil 59 | } 60 | -------------------------------------------------------------------------------- /actions/decoder.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/internal/errors" 6 | ) 7 | 8 | // Decoder interface to be implemented bu the plugin Decoders. 9 | type Decoder interface { 10 | BlockHeaderSchema() hcl.BlockHeaderSchema 11 | DecodeBlock(block *hcl.Block) (Action, errors.Error) 12 | } 13 | -------------------------------------------------------------------------------- /actions/executor.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "github.com/wesovilabs/orion/context" 5 | "github.com/wesovilabs/orion/helper" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | // ExecuteFn defined type to be implemented by the plugins. 10 | type ExecuteFn func(ctx context.OrionContext) errors.Error 11 | 12 | // Execute Given a plugin and the context is executed. 13 | func Execute(ctx context.OrionContext, action *Base, fn ExecuteFn) errors.Error { 14 | // ctx.StartAction() 15 | evalCtx := ctx.EvalContext() 16 | when, err := helper.GetExpressionValueAsBool(evalCtx, action.when, true) 17 | if err != nil { 18 | return err 19 | } 20 | if !when { 21 | return nil 22 | } 23 | if action.count != nil && !action.count.Range().Empty() { 24 | return doCount(ctx, action, fn) 25 | } 26 | if action.while != nil && !action.while.Range().Empty() { 27 | return doWhile(ctx, action, fn) 28 | } 29 | return fn(ctx) 30 | } 31 | 32 | func doWhile(ctx context.OrionContext, plugin *Base, fn ExecuteFn) errors.Error { 33 | evalCtx := ctx.EvalContext() 34 | index := 0 35 | for { 36 | ctx.Variables().SetIndex(index) 37 | ctx.Variables().SetToContext(ctx.EvalContext()) 38 | if while, err := helper.GetExpressionValueAsBool(evalCtx, plugin.while, true); err != nil || !while { 39 | return err 40 | } 41 | if err := fn(ctx); err != nil { 42 | return err 43 | } 44 | index++ 45 | } 46 | } 47 | 48 | func doCount(ctx context.OrionContext, plugin *Base, fn ExecuteFn) errors.Error { 49 | evalCtx := ctx.EvalContext() 50 | count, err := helper.GetExpressionValueAsInt(evalCtx, plugin.count, 1) 51 | if err != nil { 52 | return err 53 | } 54 | for index := 0; index < count; index++ { 55 | ctx.Variables().SetIndex(index) 56 | ctx.Variables().SetToContext(ctx.EvalContext()) 57 | if while, err := helper.GetExpressionValueAsBool(evalCtx, plugin.while, true); err != nil || !while { 58 | return err 59 | } 60 | if err := fn(ctx); err != nil { 61 | return err 62 | } 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /actions/http/internal/decoder/connection.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/wesovilabs/orion/helper" 8 | "github.com/wesovilabs/orion/internal/errors" 9 | ) 10 | 11 | var ( 12 | defConnectionTimeout = 10 * time.Second 13 | defConnectionProxy = "" 14 | ) 15 | 16 | type Connection struct { 17 | timeout hcl.Expression 18 | proxy hcl.Expression 19 | } 20 | 21 | func (c *Connection) SetTimeout(expr hcl.Expression) { 22 | c.timeout = expr 23 | } 24 | 25 | func (c *Connection) Timeout(ctx *hcl.EvalContext) (time.Duration, errors.Error) { 26 | timeDuration, err := helper.GetExpressionValueAsDuration(ctx, c.timeout, &defConnectionTimeout) 27 | return *timeDuration, err 28 | } 29 | 30 | func (c *Connection) SetProxy(expr hcl.Expression) { 31 | c.proxy = expr 32 | } 33 | 34 | func (c *Connection) Proxy(ctx *hcl.EvalContext) (string, errors.Error) { 35 | return helper.GetExpressionValueAsString(ctx, c.proxy, defConnectionProxy) 36 | } 37 | -------------------------------------------------------------------------------- /actions/http/internal/decoder/cookie.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/helper" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | const ( 10 | defCookieName = "" 11 | defCookieValue = "" 12 | defCookiePath = "" 13 | defCookieDomain = "" 14 | ) 15 | 16 | type Cookie struct { 17 | name hcl.Expression 18 | value hcl.Expression 19 | path hcl.Expression 20 | domain hcl.Expression 21 | } 22 | 23 | func (c *Cookie) SetName(expr hcl.Expression) { 24 | c.name = expr 25 | } 26 | 27 | func (c *Cookie) Name(ctx *hcl.EvalContext) (string, errors.Error) { 28 | return helper.GetExpressionValueAsString(ctx, c.name, defCookieName) 29 | } 30 | 31 | func (c *Cookie) SetValue(expr hcl.Expression) { 32 | c.value = expr 33 | } 34 | 35 | func (c *Cookie) Value(ctx *hcl.EvalContext) (string, errors.Error) { 36 | return helper.GetExpressionValueAsString(ctx, c.value, defCookieValue) 37 | } 38 | 39 | func (c *Cookie) SetPath(expr hcl.Expression) { 40 | c.path = expr 41 | } 42 | 43 | func (c *Cookie) Path(ctx *hcl.EvalContext) (string, errors.Error) { 44 | return helper.GetExpressionValueAsString(ctx, c.path, defCookiePath) 45 | } 46 | 47 | func (c *Cookie) SetDomain(expr hcl.Expression) { 48 | c.domain = expr 49 | } 50 | 51 | func (c *Cookie) Domain(ctx *hcl.EvalContext) (string, errors.Error) { 52 | return helper.GetExpressionValueAsString(ctx, c.domain, defCookieDomain) 53 | } 54 | -------------------------------------------------------------------------------- /actions/http/internal/decoder/headers.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | ) 6 | 7 | type Headers map[string]hcl.Expression 8 | -------------------------------------------------------------------------------- /actions/http/internal/decoder/payload.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "fmt" 7 | "net/url" 8 | 9 | "github.com/hashicorp/hcl/v2" 10 | "github.com/wesovilabs/orion/helper" 11 | "github.com/wesovilabs/orion/internal/errors" 12 | ) 13 | 14 | const ( 15 | payloadJSON = "json" 16 | payloadXML = "xml" 17 | payloadForm = "form" 18 | payloadRaw = "raw" 19 | ) 20 | 21 | type Payload struct { 22 | payloadType string 23 | data hcl.Expression 24 | } 25 | 26 | func (p *Payload) SetType(t string) { 27 | p.payloadType = t 28 | } 29 | 30 | func (p *Payload) Type(ctx *hcl.EvalContext) string { 31 | return p.payloadType 32 | } 33 | 34 | func (p *Payload) SetData(expr hcl.Expression) { 35 | p.data = expr 36 | } 37 | 38 | func (p *Payload) Data(ctx *hcl.EvalContext) (string, errors.Error) { 39 | if p.payloadType == payloadRaw { 40 | return helper.GetExpressionValueAsString(ctx, p.data, "") 41 | } 42 | data, err := helper.GetExpressionValueAsInterface(ctx, p.data, nil) 43 | if err != nil { 44 | return "", err 45 | } 46 | pType := p.Type(ctx) 47 | switch pType { 48 | case payloadJSON: 49 | return p.toJSON(data) 50 | case payloadXML: 51 | return p.toXML(data) 52 | case payloadForm: 53 | return p.toForm(data) 54 | } 55 | return "", nil 56 | } 57 | 58 | func (p *Payload) toJSON(value interface{}) (string, errors.Error) { 59 | b, err := json.Marshal(value) 60 | if err != nil { 61 | return "", errors.Unexpected(err.Error()) 62 | } 63 | return string(b), nil 64 | } 65 | 66 | func (p *Payload) toXML(value interface{}) (string, errors.Error) { 67 | b, err := xml.Marshal(value) 68 | if err != nil { 69 | return "", errors.Unexpected(err.Error()) 70 | } 71 | return string(b), nil 72 | } 73 | 74 | func (p *Payload) toForm(value interface{}) (string, errors.Error) { 75 | form := url.Values{} 76 | switch dataMap := value.(type) { 77 | case map[string]interface{}: 78 | for k, v := range dataMap { 79 | form.Add(k, fmt.Sprintf("%v", v)) 80 | } 81 | default: 82 | return "", errors.InvalidArguments("unsupported type for payload (form)") 83 | } 84 | return form.Encode(), nil 85 | } 86 | -------------------------------------------------------------------------------- /actions/http/internal/decoder/queryParams.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | ) 6 | 7 | type QueryParams map[string]hcl.Expression 8 | -------------------------------------------------------------------------------- /actions/http/internal/decoder/response.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import "github.com/hashicorp/hcl/v2" 4 | 5 | type Response struct { 6 | values map[string]hcl.Expression 7 | } 8 | 9 | func (res *Response) SetValues(attributes hcl.Attributes) { 10 | for index := range attributes { 11 | attribute := attributes[index] 12 | if res.values == nil { 13 | res.values = make(map[string]hcl.Expression) 14 | } 15 | res.values[attribute.Name] = attribute.Expr 16 | } 17 | } 18 | 19 | func (res *Response) Values() map[string]hcl.Expression { 20 | return res.values 21 | } 22 | -------------------------------------------------------------------------------- /actions/http/internal/executor/variables.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | type Variables interface { 11 | SetStatusCode(code int) 12 | SetBody(body []byte) 13 | SetToContext(ctx *hcl.EvalContext) 14 | SetHeaders(headers map[string][]string) 15 | } 16 | 17 | type variables struct { 18 | statusCode int 19 | body []byte 20 | headers map[string][]string 21 | } 22 | 23 | func createVariables() Variables { 24 | return &variables{} 25 | } 26 | 27 | func (v *variables) SetStatusCode(code int) { 28 | v.statusCode = code 29 | } 30 | 31 | func (v *variables) SetBody(body []byte) { 32 | v.body = body 33 | } 34 | 35 | func (v *variables) SetHeaders(headers map[string][]string) { 36 | v.headers = headers 37 | } 38 | 39 | func (v *variables) SetToContext(ctx *hcl.EvalContext) { 40 | statusCode := big.NewFloat(float64(v.statusCode)) 41 | body := "" 42 | if v.body != nil { 43 | body = string(v.body) 44 | } 45 | 46 | headers := map[string]cty.Value{} 47 | for headerName, headerValue := range v.headers { 48 | if len(headerValue) == 1 { 49 | headers[headerName] = cty.StringVal(headerValue[0]) 50 | continue 51 | } 52 | items := make([]cty.Value, len(headerValue)) 53 | for index := range headerValue { 54 | items[index] = cty.StringVal(headerValue[index]) 55 | } 56 | headers[headerName] = cty.ListVal(items) 57 | } 58 | httpVars := cty.ObjectVal(map[string]cty.Value{ 59 | "body": cty.StringVal(body), 60 | "headers": cty.ObjectVal(headers), 61 | "statusCode": cty.NumberVal(statusCode), 62 | }) 63 | if rootVars, ok := ctx.Variables["_"]; ok { 64 | rootValueMap := rootVars.AsValueMap() 65 | rootValueMap["http"] = httpVars 66 | ctx.Variables["_"] = cty.ObjectVal(rootValueMap) 67 | return 68 | } 69 | ctx.Variables["_"] = cty.ObjectVal(map[string]cty.Value{ 70 | "http": httpVars, 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /actions/mongo/examples/mongo.hcl: -------------------------------------------------------------------------------- 1 | scenario "mongo showcase"{ 2 | given ""{ 3 | mongo insert { 4 | request{ 5 | auth { 6 | 7 | } 8 | database = "" 9 | collection = "" 10 | document = {} 11 | } 12 | response { 13 | 14 | } 15 | } 16 | } 17 | when "" { 18 | 19 | } 20 | then "" { 21 | 22 | } 23 | } -------------------------------------------------------------------------------- /actions/mongo/internal/decoder/auth.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | mngHelper "github.com/wesovilabs/orion/actions/mongo/internal/helper" 8 | "github.com/wesovilabs/orion/helper" 9 | "github.com/wesovilabs/orion/internal/errors" 10 | "go.mongodb.org/mongo-driver/mongo/options" 11 | ) 12 | 13 | const ( 14 | defUsername = "test" 15 | defPassword = "" 16 | mechanismSha1 = "scram-sha-1" 17 | mechanismSha256 = "scram-sha-256" 18 | mechanismMongoDBCr = "mongodb-cr" 19 | mechanismPlain = "plain" 20 | mechanismGssapi = "gssapi" 21 | mechanismX509 = "mongodb-x509" 22 | mechanismAWS = "mongodb-aws" 23 | ) 24 | 25 | var ( 26 | supportedAuthMechanisms = map[string]struct{}{ 27 | mechanismSha1: {}, 28 | mechanismSha256: {}, 29 | mechanismMongoDBCr: {}, 30 | mechanismPlain: {}, 31 | mechanismGssapi: {}, 32 | mechanismX509: {}, 33 | mechanismAWS: {}, 34 | } 35 | supportedAuthMechanismsName = mngHelper.MapStructToArray(supportedAuthMechanisms) 36 | ) 37 | 38 | type Auth struct { 39 | mechanism string 40 | username hcl.Expression 41 | password hcl.Expression 42 | } 43 | 44 | func (auth *Auth) SetUsername(expr hcl.Expression) { 45 | auth.username = expr 46 | } 47 | 48 | func (auth *Auth) SetPassword(expr hcl.Expression) { 49 | auth.password = expr 50 | } 51 | 52 | func (auth *Auth) Mechanism() string { 53 | return strings.ToUpper(auth.mechanism) 54 | } 55 | 56 | func (auth *Auth) Username(ctx *hcl.EvalContext) (string, errors.Error) { 57 | return helper.GetExpressionValueAsString(ctx, auth.username, defUsername) 58 | } 59 | 60 | func (auth *Auth) Password(ctx *hcl.EvalContext) (string, errors.Error) { 61 | return helper.GetExpressionValueAsString(ctx, auth.password, defPassword) 62 | } 63 | 64 | func (auth *Auth) mongoCredentials(ctx *hcl.EvalContext) (options.Credential, errors.Error) { 65 | mechanism := auth.Mechanism() 66 | username, err := auth.Username(ctx) 67 | if err != nil { 68 | return options.Credential{}, err 69 | } 70 | password, err := auth.Password(ctx) 71 | if err != nil { 72 | return options.Credential{}, err 73 | } 74 | return options.Credential{ 75 | AuthMechanism: mechanism, 76 | Username: username, 77 | Password: password, 78 | }, nil 79 | } 80 | -------------------------------------------------------------------------------- /actions/mongo/internal/decoder/common.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/helper" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | type BlockProperties struct { 10 | values map[string]hcl.Expression 11 | } 12 | 13 | func (b *BlockProperties) SetValues(attributes hcl.Attributes) { 14 | for index := range attributes { 15 | attribute := attributes[index] 16 | if b.values == nil { 17 | b.values = make(map[string]hcl.Expression) 18 | } 19 | b.values[attribute.Name] = attribute.Expr 20 | } 21 | } 22 | 23 | func (b *BlockProperties) Values(ctx *hcl.EvalContext) (map[string]interface{}, errors.Error) { 24 | out := make(map[string]interface{}) 25 | for name, val := range b.values { 26 | value, err := helper.GetExpressionValueAsInterface(ctx, val, nil) 27 | if err != nil { 28 | return nil, err 29 | } 30 | out[name] = value 31 | } 32 | return out, nil 33 | } 34 | 35 | func (b BlockProperties) Evaluate(ctx *hcl.EvalContext) errors.Error { 36 | return helper.EvalUnorderedExpression(ctx, b.values) 37 | } 38 | 39 | func NewBlockProperties(block *hcl.Block) (*BlockProperties, errors.Error) { 40 | b := new(BlockProperties) 41 | attributes, d := block.Body.JustAttributes() 42 | if err := errors.EvalDiagnostics(d); err != nil { 43 | return nil, err 44 | } 45 | b.SetValues(attributes) 46 | return b, nil 47 | } 48 | -------------------------------------------------------------------------------- /actions/mongo/internal/decoder/connection.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/wesovilabs/orion/helper" 7 | "go.mongodb.org/mongo-driver/mongo/options" 8 | 9 | "github.com/hashicorp/hcl/v2" 10 | 11 | "github.com/wesovilabs/orion/internal/errors" 12 | ) 13 | 14 | var ( 15 | defConnectionTimeout = 10 * time.Second 16 | defConnectionURI = "mongodb://localhost:27017" 17 | ) 18 | 19 | type Connection struct { 20 | uri hcl.Expression 21 | timeout hcl.Expression 22 | auth *Auth 23 | } 24 | 25 | func (c *Connection) SetURI(expr hcl.Expression) { 26 | c.uri = expr 27 | } 28 | 29 | func (c *Connection) SetTimeout(expr hcl.Expression) { 30 | c.timeout = expr 31 | } 32 | 33 | func (c *Connection) SetAuth(auth *Auth) { 34 | c.auth = auth 35 | } 36 | 37 | func (c *Connection) Timeout(ctx *hcl.EvalContext) (*time.Duration, errors.Error) { 38 | return helper.GetExpressionValueAsDuration(ctx, c.timeout, &defConnectionTimeout) 39 | } 40 | 41 | func (c *Connection) ClientOpts(ctx *hcl.EvalContext) (*options.ClientOptions, errors.Error) { 42 | credential, err := c.auth.mongoCredentials(ctx) 43 | if err != nil { 44 | return nil, err 45 | } 46 | uri, err := helper.GetExpressionValueAsString(ctx, c.uri, defConnectionURI) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return options.Client().ApplyURI(uri).SetAuth(credential), nil 51 | } 52 | -------------------------------------------------------------------------------- /actions/mongo/internal/decoder/query.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/helper" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | const ( 10 | defDatabase = "test" 11 | defCollection = "test" 12 | defLimit = 0 13 | ) 14 | 15 | type Filter struct { 16 | *BlockProperties 17 | } 18 | 19 | type Set struct { 20 | *BlockProperties 21 | } 22 | 23 | type Document struct { 24 | *BlockProperties 25 | } 26 | 27 | type Query struct { 28 | database hcl.Expression 29 | collection hcl.Expression 30 | limit hcl.Expression 31 | filter *Filter 32 | set *Set 33 | documents []*Document 34 | } 35 | 36 | func (q *Query) HasSet() bool { 37 | return q.set != nil && len(q.set.values) > 0 38 | } 39 | 40 | func (q *Query) SetDatabase(expr hcl.Expression) { 41 | q.database = expr 42 | } 43 | 44 | func (q *Query) SetCollection(expr hcl.Expression) { 45 | q.collection = expr 46 | } 47 | 48 | func (q *Query) SetLimit(expr hcl.Expression) { 49 | q.limit = expr 50 | } 51 | 52 | func (q *Query) Database(ctx *hcl.EvalContext) (string, errors.Error) { 53 | return helper.GetExpressionValueAsString(ctx, q.database, defDatabase) 54 | } 55 | 56 | func (q *Query) Collection(ctx *hcl.EvalContext) (string, errors.Error) { 57 | return helper.GetExpressionValueAsString(ctx, q.collection, defCollection) 58 | } 59 | 60 | func (q *Query) Limit(ctx *hcl.EvalContext) (int, errors.Error) { 61 | return helper.GetExpressionValueAsInt(ctx, q.limit, defLimit) 62 | } 63 | 64 | func (q *Query) Filter(ctx *hcl.EvalContext) (map[string]interface{}, errors.Error) { 65 | if q.filter == nil { 66 | return make(map[string]interface{}), nil 67 | } 68 | return q.filter.Values(ctx) 69 | } 70 | 71 | func (q *Query) Set(ctx *hcl.EvalContext) (map[string]interface{}, errors.Error) { 72 | if q.set == nil { 73 | return make(map[string]interface{}), nil 74 | } 75 | return q.set.Values(ctx) 76 | } 77 | 78 | func (q *Query) Documents(ctx *hcl.EvalContext) ([]map[string]interface{}, errors.Error) { 79 | documents := make([]map[string]interface{}, len(q.documents)) 80 | for index := range q.documents { 81 | document, err := q.documents[index].Values(ctx) 82 | if err != nil { 83 | return nil, err 84 | } 85 | documents[index] = document 86 | } 87 | return documents, nil 88 | } 89 | -------------------------------------------------------------------------------- /actions/mongo/internal/decoder/response.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | type Response struct { 4 | *BlockProperties 5 | } 6 | -------------------------------------------------------------------------------- /actions/mongo/internal/helper/maps.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | func MapStructToArray(dict map[string]struct{}) []string { 4 | out := make([]string, len(dict)) 5 | index := 0 6 | for name := range dict { 7 | out[index] = name 8 | index++ 9 | } 10 | return out 11 | } 12 | -------------------------------------------------------------------------------- /actions/mongo/internal/helper/value.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | ) 9 | 10 | // ToValue convert the interface into a value. 11 | func ToValue(v interface{}) cty.Value { 12 | switch v := v.(type) { 13 | case string: 14 | return cty.StringVal(v) 15 | case int, int8, int16, int32, int64: 16 | return cty.NumberIntVal(v.(int64)) 17 | case float32, float64: 18 | return cty.NumberFloatVal(v.(float64)) 19 | case bool: 20 | return cty.BoolVal(v) 21 | case map[string]interface{}: 22 | return toValueMap(v) 23 | case []interface{}: 24 | return toValueList(v) 25 | case []map[string]interface{}: 26 | items := make([]cty.Value, len(v)) 27 | for i := range v { 28 | items[i] = toValueMap(v[i]) 29 | } 30 | return cty.ListVal(items) 31 | case nil: 32 | return cty.NilVal 33 | case primitive.ObjectID: 34 | return cty.StringVal(v.Hex()) 35 | default: 36 | return cty.StringVal(fmt.Sprintf("%v", v)) 37 | } 38 | } 39 | 40 | func toValueMap(input map[string]interface{}) cty.Value { 41 | output := make(map[string]cty.Value) 42 | for name, value := range input { 43 | output[name] = ToValue(value) 44 | } 45 | 46 | return cty.ObjectVal(output) 47 | } 48 | 49 | func toValueList(input []interface{}) cty.Value { 50 | output := make([]cty.Value, len(input)) 51 | for index := range input { 52 | value := input[index] 53 | output[index] = ToValue(value) 54 | } 55 | return cty.TupleVal(output) 56 | } 57 | -------------------------------------------------------------------------------- /actions/mongo/testdata/sample.hcl: -------------------------------------------------------------------------------- 1 | vars { 2 | connection = { 3 | uri = "" 4 | timeout = "10s" 5 | } 6 | } 7 | 8 | mongo insert { 9 | _ = { 10 | connection = connection 11 | } 12 | 13 | 14 | } 15 | 16 | mongo query { 17 | 18 | } 19 | 20 | mongo update { 21 | 22 | } 23 | 24 | mongo delete { 25 | 26 | } -------------------------------------------------------------------------------- /actions/print/internal/helper.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // TimeExpression convert a string expression into a valid go timestamp format. 8 | func TimeExpression(value string) string { 9 | value = strings.ReplaceAll(value, "yyyy", "2006") 10 | value = strings.ReplaceAll(value, "MM", "01") 11 | value = strings.ReplaceAll(value, "dd", "02") 12 | value = strings.ReplaceAll(value, "HH", "15") 13 | value = strings.ReplaceAll(value, "hh", "3") 14 | value = strings.ReplaceAll(value, "mm", "04") 15 | value = strings.ReplaceAll(value, "ss", "05") 16 | 17 | return value 18 | } 19 | -------------------------------------------------------------------------------- /actions/print/internal/print.go: -------------------------------------------------------------------------------- 1 | // Package internal contains not exported types and functions 2 | package internal 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | ct "github.com/daviddengcn/go-colortext" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/wesovilabs/orion/internal/errors" 12 | ) 13 | 14 | // DefTimestampFormat default timestamp format used by the plugin. 15 | const DefTimestampFormat = "2006-01-02 15:04:05" 16 | 17 | // Print contains the required information to print a message into the stdout. 18 | type Print struct { 19 | Prefix string 20 | Message string 21 | ShowTimestamp bool 22 | TimestampFormat string 23 | Format string 24 | } 25 | 26 | func (action *Print) normalize() { 27 | if action.ShowTimestamp && action.TimestampFormat == "" { 28 | action.TimestampFormat = DefTimestampFormat 29 | } 30 | } 31 | 32 | // Execute function in charge of the plugin execution. 33 | func (action *Print) Execute() errors.Error { 34 | action.normalize() 35 | ct.ResetColor() 36 | switch action.Format { 37 | case "json": 38 | return action.json() 39 | default: 40 | action.plain() 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (action *Print) plain() { 47 | prefix := "" 48 | if action.Prefix != "" { 49 | prefix += fmt.Sprintf("%s ", action.Prefix) 50 | } 51 | 52 | if action.ShowTimestamp { 53 | now := time.Now() 54 | fmt.Printf("%s%s %s\n", prefix, now.Format(action.TimestampFormat), action.Message) 55 | 56 | return 57 | } 58 | 59 | fmt.Printf("%s%s\n", prefix, action.Message) 60 | } 61 | 62 | func (action *Print) json() errors.Error { 63 | value := &jsonFormat{ 64 | Prefix: action.Prefix, 65 | Msg: action.Message, 66 | Timestamp: "", 67 | } 68 | if action.ShowTimestamp { 69 | now := time.Now() 70 | value.Timestamp = now.Format(action.TimestampFormat) 71 | } 72 | bytes, err := json.Marshal(value) 73 | if err != nil { 74 | log.Warn(err.Error()) 75 | 76 | return errors.Unexpected(err.Error()) 77 | } 78 | fmt.Println(string(bytes)) 79 | return nil 80 | } 81 | 82 | type jsonFormat struct { 83 | Prefix string `json:"prefix,omitempty"` 84 | Msg string `json:"msg"` 85 | Timestamp string `json:"timestamp,omitempty"` 86 | } 87 | -------------------------------------------------------------------------------- /actions/print/internal/print_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestPrint_Execute(t *testing.T) { 16 | AssertStdout(t, []Print{ 17 | { 18 | Message: "Hey Ms Robinson", 19 | }, 20 | { 21 | Prefix: "[DEBUG]", 22 | Message: "Hey Ms Robinson", 23 | }, 24 | { 25 | Prefix: "[INFO]", 26 | Message: "Hey Ms Robinson", 27 | }, 28 | { 29 | Message: "Hey Ms Robinson", 30 | ShowTimestamp: true, 31 | TimestampFormat: DefTimestampFormat, 32 | }, 33 | { 34 | Message: "Hey Ms Robinson", 35 | Format: "json", 36 | }, 37 | { 38 | Prefix: "A", 39 | Message: "Hey Ms Robinson", 40 | ShowTimestamp: true, 41 | Format: "json", 42 | }, 43 | }, 44 | []string{ 45 | "Hey Ms Robinson", 46 | "[DEBUG] Hey Ms Robinson", 47 | "[INFO] Hey Ms Robinson", 48 | fmt.Sprintf("%s Hey Ms Robinson", time.Now().Format(DefTimestampFormat)), 49 | "{\"msg\":\"Hey Ms Robinson\"}", 50 | fmt.Sprintf("{\"prefix\":\"A\",\"msg\":\"Hey Ms Robinson\",\"timestamp\":\"%s\"}", time.Now().Format(DefTimestampFormat)), 51 | }, 52 | ) 53 | } 54 | 55 | func AssertStdout(t *testing.T, prints []Print, messages []string) { 56 | old := os.Stdout 57 | r, w, _ := os.Pipe() 58 | os.Stdout = w 59 | 60 | print() 61 | outC := make(chan string) 62 | go func() { 63 | var buf bytes.Buffer 64 | io.Copy(&buf, r) 65 | outC <- buf.String() 66 | }() 67 | for index := range prints { 68 | err := prints[index].Execute() 69 | assert.Nil(t, err) 70 | } 71 | w.Close() 72 | os.Stdout = old 73 | 74 | lines := strings.Split(<-outC, "\n") 75 | assert.Len(t, lines, len(messages)+1) 76 | for index := range messages { 77 | line := lines[index] 78 | assert.Equal(t, line, messages[index]) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /actions/print/testdata/scenario1.hcl: -------------------------------------------------------------------------------- 1 | 2 | print { 3 | msg = "Hello Mr Robot" 4 | prefix = "DEBUG" 5 | } 6 | 7 | print { 8 | msg = "Hello Mr ${lastname}" 9 | prefix = "DEBUG" 10 | } 11 | 12 | print { 13 | msg = "Hello Mr ${lastname}" 14 | prefix = "DEBUG" 15 | showTimestamp = true 16 | } 17 | 18 | print { 19 | msg = "Hello Mr ${lastname}" 20 | prefix = "DEBUG" 21 | showTimestamp = showTimestamp 22 | } 23 | 24 | print { 25 | msg = "Hello Mr ${lastname}" 26 | prefix = "DEBUG" 27 | showTimestamp = showTimestamp 28 | when = true 29 | } -------------------------------------------------------------------------------- /actions/print/testdata/scenario2.hcl: -------------------------------------------------------------------------------- 1 | print { 2 | msg = "Hello Mr Robot" 3 | } 4 | 5 | print { 6 | msg = "Hello Mr Robot" 7 | prefix = "DEBUG" 8 | } 9 | 10 | print { 11 | msg = "Hello Mr ${lastname}" 12 | prefix = "DEBUG" 13 | } 14 | 15 | print { 16 | msg = lastname 17 | } 18 | 19 | print { 20 | msg = true 21 | } 22 | 23 | print { 24 | msg = "${lastname}_${lastname}" 25 | format="json" 26 | } 27 | 28 | print { 29 | msg = "${lastname}_${lastname}" 30 | format="plain" 31 | } 32 | 33 | print { 34 | msg = "Hello Mr ${lastname}" 35 | showTimestamp = true 36 | timestampFormat = "yyyy-MM-dd hh:mm" 37 | format = "json" 38 | } -------------------------------------------------------------------------------- /actions/print/testdata/scenario3.hcl: -------------------------------------------------------------------------------- 1 | print { 2 | msg = "Hello Mr ${lastname}" 3 | prefix = prefixes[0] 4 | } 5 | 6 | print { 7 | msg = "Hello Mr ${lastname}" 8 | } 9 | 10 | print { 11 | prefix = prefixes[0] 12 | msg = "Hello Mr ${lastname}" 13 | showTimestamp = "" 14 | timestampFormat = timestampFmt 15 | format = "json" 16 | } -------------------------------------------------------------------------------- /actions/print/testdata/spec.hcl: -------------------------------------------------------------------------------- 1 | vars { 2 | gender = "masculine" 3 | people = [ 4 | { 5 | firstname = "John" 6 | lastname = "Doe" 7 | }, 8 | { 9 | firstname = "Jane" 10 | lastname = "Doe" 11 | }, 12 | { 13 | firstname = "Jimmie" 14 | lastname = "Loe" 15 | } 16 | ] 17 | letters = ["a","b","c"] 18 | } 19 | 20 | print { 21 | msg = "Hello Mr ${firstname} ${lastname}" 22 | when = eq(gender,"masculine") 23 | } 24 | 25 | print { 26 | msg = "Hello Ms ${firstname} ${lastname}" 27 | when = eq(gender,"female") 28 | } 29 | 30 | print { 31 | msg ="iteration number ${_.index}" 32 | while = _.index<10 33 | } 34 | 35 | print { 36 | msg ="iteration number ${_.index}" 37 | count = 10 38 | } 39 | 40 | print { 41 | msg ="iteration number ${_.index}" 42 | count = 10 43 | while = _.index < 5 44 | } 45 | 46 | print { 47 | msg = "There are ${len(people)} people." 48 | } 49 | 50 | print { 51 | msg = "${people[_.index].firstname} ${people[_.index].lastname}" 52 | while = _.index < len(people) 53 | } 54 | 55 | loop { 56 | items = people 57 | as = person 58 | filter = person.lastname == "Doe" 59 | 60 | print { 61 | msg = "${person.firstname} ${person.lastname}" 62 | } 63 | 64 | set { 65 | variable = 2 * _.index 66 | } 67 | } 68 | 69 | loop { 70 | items = range(1,20) 71 | print { 72 | msg = "value ${_.item}" 73 | } 74 | } 75 | 76 | loop { 77 | items = ["a","b","c"] 78 | as = letter 79 | print { 80 | msg = letter 81 | } 82 | } 83 | 84 | print { 85 | with logger{} 86 | msg = "There are 20 cars" 87 | } 88 | 89 | vars { 90 | apiBaseUrl = "http://mycompany.com" 91 | } 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | print { 104 | trait logger{} 105 | msg = "Hello Mr Robinson" 106 | } 107 | 108 | print { 109 | trait logger{} 110 | msg = "Hello Mr Robinson" 111 | } 112 | 113 | 114 | -------------------------------------------------------------------------------- /actions/register/decoder.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/actions" 6 | "github.com/wesovilabs/orion/actions/assert" 7 | "github.com/wesovilabs/orion/actions/block" 8 | "github.com/wesovilabs/orion/actions/call" 9 | "github.com/wesovilabs/orion/actions/http" 10 | "github.com/wesovilabs/orion/actions/mongo" 11 | pprint "github.com/wesovilabs/orion/actions/print" 12 | "github.com/wesovilabs/orion/actions/set" 13 | "github.com/wesovilabs/orion/actions/sleep" 14 | ) 15 | 16 | var decoders = map[string]actions.Decoder{ 17 | assert.BlockAssert: new(assert.Decoder), 18 | block.BlockBlock: new(block.Decoder), 19 | call.BlockCall: new(call.Decoder), 20 | http.BlockHTTP: new(http.Decoder), 21 | mongo.BlockMongo: new(mongo.Decoder), 22 | pprint.BlockPrint: new(pprint.Decoder), 23 | set.BlockSet: new(set.Decoder), 24 | sleep.BlockSleep: new(sleep.Decoder), 25 | } 26 | 27 | var schemaPlugins = &hcl.BodySchema{ 28 | Attributes: nil, 29 | Blocks: []hcl.BlockHeaderSchema{ 30 | decoders[assert.BlockAssert].BlockHeaderSchema(), 31 | decoders[block.BlockBlock].BlockHeaderSchema(), 32 | decoders[call.BlockCall].BlockHeaderSchema(), 33 | decoders[http.BlockHTTP].BlockHeaderSchema(), 34 | decoders[mongo.BlockMongo].BlockHeaderSchema(), 35 | decoders[pprint.BlockPrint].BlockHeaderSchema(), 36 | decoders[set.BlockSet].BlockHeaderSchema(), 37 | decoders[sleep.BlockSleep].BlockHeaderSchema(), 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /actions/register/handler.go: -------------------------------------------------------------------------------- 1 | // Package plugins contain the types and method to deal with plugins 2 | package register 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/wesovilabs/orion/actions" 9 | "github.com/wesovilabs/orion/internal/errors" 10 | ) 11 | 12 | var handler Handler 13 | 14 | var once sync.Once 15 | 16 | // Get return an instance of the Handler interface. 17 | func GetHandler() Handler { 18 | once.Do(func() { 19 | handler = new(manager) 20 | }) 21 | return handler 22 | } 23 | 24 | // Handler interface to obtain the plugin to be performed. 25 | type Handler interface { 26 | DecodePlugin(block *hcl.Block) (actions.Action, errors.Error) 27 | DecodePlugins(blocks hcl.Blocks) (actions.Actions, errors.Error) 28 | GetBlocksSpec() []hcl.BlockHeaderSchema 29 | } 30 | 31 | type manager struct{} 32 | 33 | func (m *manager) DecodePlugin(block *hcl.Block) (actions.Action, errors.Error) { 34 | dec, ok := decoders[block.Type] 35 | if !ok { 36 | return nil, errors.ThrowUnsupportedBlock("", block.Type) 37 | } 38 | return dec.DecodeBlock(block) 39 | } 40 | 41 | func (m *manager) DecodePlugins(blocks hcl.Blocks) (actions.Actions, errors.Error) { 42 | actions := make(actions.Actions, len(blocks)) 43 | for i, block := range blocks { 44 | action, err := m.DecodePlugin(block) 45 | if err != nil { 46 | return nil, err 47 | } 48 | action.SetKind(block.Type) 49 | actions[i] = action 50 | } 51 | return actions, nil 52 | } 53 | 54 | func (m *manager) GetBlocksSpec() []hcl.BlockHeaderSchema { 55 | return schemaPlugins.Blocks 56 | } 57 | -------------------------------------------------------------------------------- /actions/set/action.go: -------------------------------------------------------------------------------- 1 | // Package set contain types and method to deal with this plugin 2 | package set 3 | 4 | import ( 5 | "github.com/hashicorp/hcl/v2" 6 | log "github.com/sirupsen/logrus" 7 | "github.com/wesovilabs/orion/actions" 8 | "github.com/wesovilabs/orion/context" 9 | "github.com/wesovilabs/orion/helper" 10 | "github.com/wesovilabs/orion/internal/errors" 11 | ) 12 | 13 | // Set common block used to define variables. 14 | type Set struct { 15 | *actions.Base 16 | name string 17 | index hcl.Expression 18 | value hcl.Expression 19 | } 20 | 21 | func (set *Set) populateAttributes(attrs hcl.Attributes) errors.Error { 22 | for name := range attrs { 23 | attribute := attrs[name] 24 | switch { 25 | case actions.IsCommonAttribute(name): 26 | if err := actions.SetBaseArgs(set, attribute); err != nil { 27 | return err 28 | } 29 | case name == argValue: 30 | set.value = attribute.Expr 31 | case name == argIndex: 32 | set.index = attribute.Expr 33 | default: 34 | return errors.ThrowUnsupportedArgument(BlockSet, name) 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | // Execute method to run the plugin. 41 | func (set *Set) Execute(ctx context.OrionContext) errors.Error { 42 | log.Debugf("[%s] It sets value for variable %s", BlockSet, set.name) 43 | key := set.name 44 | if set.index != nil { 45 | index, d := set.index.Value(ctx.EvalContext()) 46 | if err := errors.EvalDiagnostics(d); err != nil { 47 | return err 48 | } 49 | indexV, _ := index.AsBigFloat().Int64() 50 | return helper.EvaluateArrayItemExpression(ctx.EvalContext(), set.name, int(indexV), set.value) 51 | } 52 | return helper.EvalUnorderedExpression(ctx.EvalContext(), map[string]hcl.Expression{ 53 | key: set.value, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /actions/set/decoder.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/wesovilabs/orion/actions" 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | const ( 11 | // BlockSet identifier of plugin. 12 | BlockSet = "set" 13 | // LabelName label required by block set. 14 | LabelName = "name" 15 | argValue = "value" 16 | argIndex = "arrayIndex" 17 | ) 18 | 19 | var schemaSet = &hcl.BodySchema{ 20 | Attributes: append(actions.BaseArguments, []hcl.AttributeSchema{ 21 | {Name: argValue, Required: true}, 22 | {Name: argIndex, Required: false}, 23 | }...), 24 | } 25 | 26 | // Decoder implement interface Decoder. 27 | type Decoder struct{} 28 | 29 | // BlockHeaderSchema return the header schema for the plugin. 30 | func (dec *Decoder) BlockHeaderSchema() hcl.BlockHeaderSchema { 31 | return hcl.BlockHeaderSchema{ 32 | Type: BlockSet, 33 | LabelNames: []string{LabelName}, 34 | } 35 | } 36 | 37 | // DecodeBlock inherited method from interface Decoder. 38 | func (dec *Decoder) DecodeBlock(block *hcl.Block) (actions.Action, errors.Error) { 39 | log.Tracef("it starts decoding of block %s", BlockSet) 40 | bodyContent, d := block.Body.Content(schemaSet) 41 | if err := errors.EvalDiagnostics(d); err != nil { 42 | return nil, err 43 | } 44 | if len(block.Labels) != 1 { 45 | return nil, errors.ThrowMissingRequiredLabel(BlockSet) 46 | } 47 | set := &Set{ 48 | name: block.Labels[0], 49 | Base: &actions.Base{}, 50 | } 51 | if err := set.populateAttributes(bodyContent.Attributes); err != nil { 52 | return nil, err 53 | } 54 | if len(bodyContent.Blocks) > 0 { 55 | return nil, errors.ThrowBlocksAreNotPermitted(BlockSet) 56 | } 57 | return set, nil 58 | } 59 | -------------------------------------------------------------------------------- /actions/set/testdata/set-decoder.hcl: -------------------------------------------------------------------------------- 1 | set firstname { 2 | value = "John" 3 | } -------------------------------------------------------------------------------- /actions/shared/parse.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/hashicorp/hcl/v2/hclparse" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | func GetBodyContent(path string, blockName string, labels []string) (*hcl.BodyContent, errors.Error) { 10 | parentSchema := &hcl.BodySchema{ 11 | Blocks: []hcl.BlockHeaderSchema{{ 12 | Type: blockName, 13 | LabelNames: labels, 14 | }}, 15 | } 16 | parser := hclparse.NewParser() 17 | f, d := parser.ParseHCLFile(path) 18 | if err := errors.EvalDiagnostics(d); err != nil { 19 | return nil, err 20 | } 21 | content, d := f.Body.Content(parentSchema) 22 | if err := errors.EvalDiagnostics(d); err != nil { 23 | return nil, err 24 | } 25 | return content, nil 26 | } 27 | 28 | func CreatePos(line, col, b int) hcl.Pos { 29 | return hcl.Pos{ 30 | Line: line, 31 | Column: col, 32 | Byte: b, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /actions/sleep/action.go: -------------------------------------------------------------------------------- 1 | package sleep 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/wesovilabs/orion/actions" 9 | "github.com/wesovilabs/orion/context" 10 | "github.com/wesovilabs/orion/helper" 11 | "github.com/wesovilabs/orion/internal/errors" 12 | ) 13 | 14 | var defSleepDuration = 1 * time.Second 15 | 16 | type Sleep struct { 17 | *actions.Base 18 | duration hcl.Expression 19 | } 20 | 21 | func (s *Sleep) SetDuration(expr hcl.Expression) { 22 | s.duration = expr 23 | } 24 | 25 | func (s *Sleep) Duration(ctx *hcl.EvalContext) (time.Duration, errors.Error) { 26 | sleepDuration, err := helper.GetExpressionValueAsDuration(ctx, s.duration, &defSleepDuration) 27 | return *sleepDuration, err 28 | } 29 | 30 | // Execute function in charge of executing the plugin. 31 | func (s *Sleep) Execute(ctx context.OrionContext) errors.Error { 32 | return actions.Execute(ctx, s.Base, func(ctx context.OrionContext) errors.Error { 33 | duration, err := s.Duration(ctx.EvalContext()) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | log.Tracef("sleeping for duration %v", duration) 39 | time.Sleep(duration) 40 | 41 | return nil 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /actions/sleep/action_test.go: -------------------------------------------------------------------------------- 1 | package sleep 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/wesovilabs/orion/actions/shared" 10 | "github.com/wesovilabs/orion/context" 11 | "github.com/zclconf/go-cty/cty" 12 | ) 13 | 14 | const ( 15 | featuresDir = "testdata" 16 | featureScenario = "scenario1.hcl" 17 | ) 18 | 19 | var decoder = new(Decoder) 20 | 21 | func TestSleep(t *testing.T) { 22 | content, err := shared.GetBodyContent(path.Join(featuresDir, featureScenario), BlockSleep, []string{}) 23 | assert.Nil(t, err) 24 | assert.NotNil(t, content) 25 | 26 | ctx := context.New(map[string]cty.Value{}, nil) 27 | 28 | for i := range content.Blocks { 29 | action, err := decoder.DecodeBlock(content.Blocks[i]) 30 | assert.Nil(t, err) 31 | assert.NotNil(t, action) 32 | 33 | s, ok := action.(*Sleep) 34 | assert.True(t, ok) 35 | assert.NotNil(t, s) 36 | 37 | duration, err := s.Duration(ctx.EvalContext()) 38 | assert.Nil(t, err) 39 | assert.Equal(t, 10*time.Millisecond, duration) 40 | 41 | start := time.Now() 42 | s.Execute(ctx) 43 | actualDuration := time.Now().Sub(start) 44 | assert.GreaterOrEqual(t, actualDuration.Milliseconds(), duration.Milliseconds()) 45 | assert.LessOrEqual(t, actualDuration.Milliseconds(), duration.Milliseconds()+10) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /actions/sleep/decoder.go: -------------------------------------------------------------------------------- 1 | package sleep 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/actions" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | const ( 10 | // BlockSleep identifier of the sleep block. 11 | BlockSleep = "sleep" 12 | // AttributeDuration name of the argument used to specify the sleep duration. 13 | AttributeDuration = "duration" 14 | ) 15 | 16 | var schemaSleep = &hcl.BodySchema{ 17 | Attributes: append(actions.BaseArguments, []hcl.AttributeSchema{ 18 | {Name: AttributeDuration, Required: true}, 19 | }...), 20 | } 21 | 22 | // Decoder implements interface plugin.Decoder. 23 | type Decoder struct{} 24 | 25 | // BlockHeaderSchema return the header schema for the plugin. 26 | func (dec *Decoder) BlockHeaderSchema() hcl.BlockHeaderSchema { 27 | return hcl.BlockHeaderSchema{ 28 | Type: BlockSleep, 29 | LabelNames: nil, 30 | } 31 | } 32 | 33 | // DecodeBlock required to implement the plugin interface. 34 | func (dec *Decoder) DecodeBlock(block *hcl.Block) (actions.Action, errors.Error) { 35 | bodyContent, d := block.Body.Content(schemaSleep) 36 | if err := errors.EvalDiagnostics(d); err != nil { 37 | return nil, err 38 | } 39 | assert := &Sleep{Base: &actions.Base{}} 40 | if err := populateAttributes(assert, bodyContent.Attributes); err != nil { 41 | return nil, err 42 | } 43 | return assert, nil 44 | } 45 | 46 | func populateAttributes(s *Sleep, attrs hcl.Attributes) errors.Error { 47 | for name := range attrs { 48 | attribute := attrs[name] 49 | 50 | switch { 51 | case actions.IsCommonAttribute(name): 52 | if err := actions.SetBaseArgs(s, attribute); err != nil { 53 | return err 54 | } 55 | case name == AttributeDuration: 56 | s.SetDuration(attribute.Expr) 57 | default: 58 | return errors.ThrowUnsupportedArgument(BlockSleep, name) 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /actions/sleep/decoder_test.go: -------------------------------------------------------------------------------- 1 | package sleep 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/hashicorp/hcl/v2/hclsyntax" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/wesovilabs/orion/actions/shared" 11 | ) 12 | 13 | var assertDecoderPath = path.Join( 14 | featuresDir, featureScenario, 15 | ) 16 | 17 | var assertDecoderBlocks = []*Sleep{ 18 | { 19 | duration: &hclsyntax.TemplateExpr{ 20 | SrcRange: hcl.Range{ 21 | Filename: assertDecoderPath, 22 | Start: shared.CreatePos(2, 3, 10), 23 | End: shared.CreatePos(2, 20, 27), 24 | }, 25 | }, 26 | }, 27 | } 28 | 29 | func TestDecoder_DecodeBlock(t *testing.T) { 30 | content, err := shared.GetBodyContent(assertDecoderPath, BlockSleep, []string{}) 31 | assert.Nil(t, err) 32 | assert.NotNil(t, content) 33 | 34 | assert.Len(t, content.Blocks, len(assertDecoderBlocks)) 35 | for index := range content.Blocks { 36 | block := content.Blocks[index] 37 | expectedBlock := assertDecoderBlocks[index] 38 | assertBlockAssert(t, block, expectedBlock) 39 | } 40 | } 41 | 42 | func assertBlockAssert(t *testing.T, block *hcl.Block, s *Sleep) { 43 | assert.Len(t, block.Labels, 0) 44 | assert.Equal(t, BlockSleep, block.Type) 45 | c, d := block.Body.Content(schemaSleep) 46 | assert.Nil(t, d) 47 | assert.NotNil(t, c) 48 | for name := range c.Attributes { 49 | attribute := c.Attributes[name] 50 | switch name { 51 | case AttributeDuration: 52 | assert.NotNil(t, s.duration) 53 | assert.Equal(t, s.duration.Range().Start, attribute.Range.Start) 54 | assert.Equal(t, s.duration.Range().End, attribute.Range.End) 55 | default: 56 | assert.Failf(t, "error", "unexpected attribute %s", name) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /actions/sleep/testdata/scenario1.hcl: -------------------------------------------------------------------------------- 1 | sleep { 2 | duration = "10ms" 3 | } 4 | -------------------------------------------------------------------------------- /build/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | ADD bin/orion.linux /usr/local/bin/orion 3 | ENTRYPOINT ["orion"] 4 | -------------------------------------------------------------------------------- /cmd/orion/common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/spf13/viper" 7 | "github.com/wesovilabs/orion/internal/config" 8 | "github.com/wesovilabs/orion/internal/logger" 9 | ) 10 | 11 | var cfg = &config.Config{ 12 | Logger: logger.Default(), 13 | } 14 | 15 | func SetUpConfig(stdOut io.Writer) { 16 | viper.AutomaticEnv() 17 | logger.SetUp(cfg.Logger, stdOut) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/orion/common/flags.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | "github.com/wesovilabs/orion/internal/config" 9 | "github.com/wesovilabs/orion/internal/logger" 10 | ) 11 | 12 | const ( 13 | flagVerbose = "verbose" 14 | flagConfig = "config" 15 | ) 16 | 17 | var ( 18 | verbose bool 19 | cfgPath string 20 | ) 21 | 22 | func SetCommonFlags(cmd *cobra.Command) { 23 | cmd.PersistentFlags().BoolVar( 24 | &verbose, 25 | flagVerbose, 26 | false, 27 | "it displays info about time & durations execution (default is false)", 28 | ) 29 | cmd.PersistentFlags().StringVar( 30 | &cfgPath, 31 | flagConfig, 32 | config.DefaultPath, 33 | fmt.Sprintf("config file (default is %s)", config.DefaultPath)) 34 | } 35 | 36 | func PreRun(cmd *cobra.Command, args []string) { 37 | viper.AutomaticEnv() 38 | // cfg := config.Load(cfgPath) 39 | cfg := &config.Config{ 40 | Logger: logger.Default(), 41 | } 42 | logger.SetUp(cfg.Logger, cmd.OutOrStdout()) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/orion/common/print.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | ct "github.com/daviddengcn/go-colortext" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/spf13/cobra" 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | func PrintError(cmd *cobra.Command, err errors.Error) { 11 | cmd.Print() 12 | ct.ChangeColor(ct.Red, false, ct.None, false) 13 | log.Error(err) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/orion/help/help.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The orion Authors. 3 | */ 4 | package help 5 | 6 | import ( 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | use = "help [command]" 14 | helpShort = `Help about any command.` 15 | helpLong = ` 16 | Help provides help for any command in the application. 17 | Simply type orion help [path to command] for full details. 18 | ` 19 | ) 20 | 21 | func New() *cobra.Command { 22 | return &cobra.Command{ 23 | Use: use, 24 | Short: helpShort, 25 | Long: helpLong, 26 | Example: "help run", 27 | Run: run, 28 | } 29 | } 30 | 31 | // RunHelp checks given arguments and executes command. 32 | func run(cmd *cobra.Command, args []string) { 33 | foundCmd, _, err := cmd.Root().Find(args) 34 | switch { 35 | case foundCmd == nil: 36 | cmd.Printf("Unknown help topic %#q.\n", args) 37 | if usageErr := cmd.Root().Usage(); usageErr != nil { 38 | panic(usageErr) 39 | } 40 | return 41 | case err != nil: 42 | cmd.Println(err) 43 | argsString := strings.Join(args, " ") 44 | matchedMsgIsPrinted := false 45 | for _, foundCmd := range foundCmd.Commands() { 46 | if strings.Contains(foundCmd.Short, argsString) { 47 | if !matchedMsgIsPrinted { 48 | cmd.Printf("Matchers of string '%s' in short descriptions of commands: \n", argsString) 49 | matchedMsgIsPrinted = true 50 | } 51 | cmd.Printf(" %-14s %s\n", foundCmd.Name(), foundCmd.Short) 52 | } 53 | } 54 | if !matchedMsgIsPrinted { 55 | if err := cmd.Root().Usage(); err != nil { 56 | panic(err) 57 | } 58 | } 59 | return 60 | default: 61 | if len(args) == 0 { 62 | foundCmd = cmd 63 | } 64 | helpFunc := foundCmd.HelpFunc() 65 | helpFunc(foundCmd, args) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cmd/orion/init/init.go: -------------------------------------------------------------------------------- 1 | package init 2 | -------------------------------------------------------------------------------- /cmd/orion/lint/lint.go: -------------------------------------------------------------------------------- 1 | package lint 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | ct "github.com/daviddengcn/go-colortext" 8 | "github.com/spf13/cobra" 9 | "github.com/wesovilabs/orion/cmd/orion/common" 10 | "github.com/wesovilabs/orion/executor" 11 | ) 12 | 13 | const ( 14 | flagInputPath = "input" 15 | defInputPath = "feature.hcl" 16 | ) 17 | 18 | var ( 19 | use = "lint" 20 | helpShort = `static analysis of feature definition.` 21 | helpLong = ` 22 | Verify the content of the input file 23 | ` 24 | example = ` 25 | # Execute the scenario in file feature.hcl 26 | orion lint --input feature.hcl 27 | ` 28 | inputPath string 29 | ) 30 | 31 | func New() *cobra.Command { 32 | cmd := &cobra.Command{ 33 | Use: use, 34 | Short: helpShort, 35 | Long: helpLong, 36 | Example: example, 37 | Run: run, 38 | PreRun: common.PreRun, 39 | } 40 | cmd.PersistentFlags().StringVar( 41 | &inputPath, 42 | flagInputPath, 43 | defInputPath, 44 | fmt.Sprintf("path of the input file (default is %s)", defInputPath)) 45 | return cmd 46 | } 47 | 48 | func run(cmd *cobra.Command, args []string) { 49 | ct.ChangeColor(ct.Green, false, ct.None, false) 50 | inputPath := cmd.Flag(flagInputPath) 51 | exec := executor.New() 52 | if err := exec.SetUp(inputPath.Value.String()); err != nil { 53 | common.PrintError(cmd, err) 54 | os.Exit(err.ExitStatus()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/orion/orion.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/wesovilabs/orion/cmd/orion/help" 6 | "github.com/wesovilabs/orion/cmd/orion/lint" 7 | "github.com/wesovilabs/orion/cmd/orion/run" 8 | ) 9 | 10 | // const flagConfig = "config" 11 | 12 | var ( 13 | // cfgPath string. 14 | helpCmd = help.New() 15 | runCmd = run.New() 16 | lintCmd = lint.New() 17 | ) 18 | 19 | func main() { 20 | cmd := command() 21 | // common.SetCommonFlags(cmd) 22 | // setUpConfig(cmd.OutOrStdout()) 23 | cmd.SetHelpCommand(helpCmd) 24 | cmd.AddCommand(helpCmd, runCmd, lintCmd) 25 | if err := cmd.Execute(); err != nil { 26 | panic(err) 27 | } 28 | } 29 | 30 | func command() *cobra.Command { 31 | return &cobra.Command{ 32 | Use: "orion [cmd]", 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cmd/orion/run/testdata/feature001.hcl: -------------------------------------------------------------------------------- 1 | scenario "basic usage" { 2 | tags = ["basic", "usage"] 3 | given "a couple of numbers" { 4 | set a { 5 | value = 1 6 | } 7 | set b { 8 | value = 2 9 | } 10 | } 11 | when "input values are sum" { 12 | set c { 13 | value = a + b 14 | } 15 | } 16 | then "the result of variable c is correct" { 17 | assert { 18 | assertion = c == 3 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cmd/orion/run/testdata/feature002.hcl: -------------------------------------------------------------------------------- 1 | scenario "basic usage" { 2 | given "a couple of numbers" { 3 | set a { 4 | value = 1 5 | } 6 | set b { 7 | value = 2 8 | } 9 | } 10 | when "do addition with the numbers" { 11 | set c { 12 | value = a + b 13 | } 14 | } 15 | then "the result of variable c is correct" { 16 | assert { 17 | assertion = c == 3 18 | } 19 | } 20 | when "do subtraction with the numbers" { 21 | set c { 22 | value = a - b 23 | } 24 | } 25 | then "the result of variable c is correct" { 26 | assert { 27 | assertion = c == -1 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/orion/run/testdata/feature003.hcl: -------------------------------------------------------------------------------- 1 | scenario "basic usage" { 2 | when "input values are sum" { 3 | set c { 4 | value = 2 + 3 5 | } 6 | } 7 | then "the result of variable c is correct" { 8 | assert { 9 | assertion = c == 5 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cmd/orion/run/testdata/feature004.hcl: -------------------------------------------------------------------------------- 1 | input { 2 | arg x { 3 | default = 10 4 | } 5 | arg y { 6 | default = 5 7 | } 8 | arg out { 9 | default = 50 10 | } 11 | } 12 | 13 | scenario "scenario demo" { 14 | when "multiply x * y" { 15 | set result { 16 | value = x * y 17 | } 18 | } 19 | then "check the output" { 20 | assert { 21 | assertion = result==out 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/orion/run/testdata/variables004.hcl: -------------------------------------------------------------------------------- 1 | x=20 2 | y=9 3 | out=180 -------------------------------------------------------------------------------- /context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/functions" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | type functionsType map[string]func(ctx OrionContext, as string) errors.Error 11 | 12 | // OrionContext context used during the feature execution. 13 | type OrionContext interface { 14 | StartScenario() 15 | FailScenario() 16 | StopScenario() 17 | EvalContext() *hcl.EvalContext 18 | Variables() Variables 19 | Functions() functionsType 20 | } 21 | 22 | // New returns a initialization of interface OrionContext. 23 | func New(variables map[string]cty.Value, funcs functionsType) OrionContext { 24 | vars := make(map[string]cty.Value) 25 | for name, value := range variables { 26 | vars[name] = value 27 | } 28 | 29 | return &orionContext{ 30 | ctx: &hcl.EvalContext{ 31 | Functions: functions.Functions, 32 | Variables: vars, 33 | }, 34 | variables: createVariables(), 35 | functions: funcs, 36 | } 37 | } 38 | 39 | type orionContext struct { 40 | ctx *hcl.EvalContext 41 | variables Variables 42 | metrics *scenarioMetrics 43 | functions map[string]func(ctx OrionContext, as string) errors.Error 44 | } 45 | 46 | // StartScenario starts the scenario. 47 | func (c *orionContext) StartScenario() { 48 | c.metrics = newScenarioMetrics() 49 | } 50 | 51 | // CompleteScenario completes the scenario. 52 | func (c *orionContext) CompleteScenario() { 53 | c.metrics.stopScenario() 54 | } 55 | 56 | // Variables return set of variables. 57 | func (c *orionContext) Variables() Variables { 58 | return c.variables 59 | } 60 | 61 | // FailScenario scenario failed. 62 | func (c *orionContext) FailScenario() { 63 | c.metrics.stopScenario() 64 | } 65 | 66 | // StopScenario scenario stopped. 67 | func (c *orionContext) StopScenario() { 68 | c.metrics.stopScenario() 69 | } 70 | 71 | // EvalContext return the hcl eval context. 72 | func (c *orionContext) EvalContext() *hcl.EvalContext { 73 | return c.ctx 74 | } 75 | 76 | // Functions returns the functions. 77 | func (c *orionContext) Functions() functionsType { 78 | return c.functions 79 | } 80 | -------------------------------------------------------------------------------- /context/context_test.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestNewFeatureContextAndStop(t *testing.T) { 11 | ctx := New(map[string]cty.Value{ 12 | "firstname": cty.StringVal("Sally"), 13 | }, nil).(*orionContext) 14 | assert.NotNil(t, ctx.Variables()) 15 | assert.NotNil(t, ctx.EvalContext()) 16 | assert.Equal(t, ctx.EvalContext().Variables["firstname"], cty.StringVal("Sally")) 17 | ctx.StartScenario() 18 | ctx.StopScenario() 19 | assert.NotNil(t, ctx.metrics.endTime) 20 | } 21 | 22 | func TestNewFeatureContextAndFail(t *testing.T) { 23 | ctx := New(map[string]cty.Value{ 24 | "firstname": cty.StringVal("Sally"), 25 | }, nil).(*orionContext) 26 | assert.NotNil(t, ctx.Variables()) 27 | assert.NotNil(t, ctx.EvalContext()) 28 | assert.Equal(t, ctx.EvalContext().Variables["firstname"], cty.StringVal("Sally")) 29 | ctx.StartScenario() 30 | ctx.FailScenario() 31 | assert.NotNil(t, ctx.metrics.endTime) 32 | } 33 | -------------------------------------------------------------------------------- /context/doc.go: -------------------------------------------------------------------------------- 1 | // Package context contains the types used to organize the context of a execution 2 | package context 3 | -------------------------------------------------------------------------------- /context/metrics.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "time" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type scenarioMetrics struct { 10 | startTime time.Time 11 | endTime *time.Time 12 | } 13 | 14 | func (m *scenarioMetrics) duration() time.Duration { 15 | return m.endTime.Sub(m.startTime) 16 | } 17 | 18 | func newScenarioMetrics() *scenarioMetrics { 19 | return &scenarioMetrics{ 20 | startTime: time.Now(), 21 | } 22 | } 23 | 24 | func (m *scenarioMetrics) stopScenario() { 25 | endtime := time.Now() 26 | m.endTime = &endtime 27 | log.Infof("The scenario took %s.", m.duration().String()) 28 | } 29 | -------------------------------------------------------------------------------- /context/metrics_test.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestScenarioMetric(t *testing.T) { 10 | m := newScenarioMetrics() 11 | assert.NotNil(t, m.startTime) 12 | assert.Nil(t, m.endTime) 13 | m.stopScenario() 14 | assert.NotNil(t, m.startTime) 15 | assert.NotNil(t, m.endTime) 16 | assert.Equal(t, m.endTime.Sub(m.startTime), m.duration()) 17 | } 18 | -------------------------------------------------------------------------------- /context/variables.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | // Variables interface definition. 9 | type Variables interface { 10 | SetIndex(int) 11 | SetToContext(ctx *hcl.EvalContext) 12 | } 13 | 14 | type variables struct { 15 | index int 16 | } 17 | 18 | func createVariables() Variables { 19 | v := new(variables) 20 | return v 21 | } 22 | 23 | // SetIndex establish index variable. 24 | func (v *variables) SetIndex(index int) { 25 | v.index = index 26 | } 27 | 28 | // SetToContext set the variables into the context. 29 | func (v *variables) SetToContext(ctx *hcl.EvalContext) { 30 | if ctx.Variables == nil { 31 | ctx.Variables = make(map[string]cty.Value) 32 | } 33 | ctx.Variables["_"] = cty.MapVal(map[string]cty.Value{ 34 | "index": cty.NumberIntVal(int64(v.index)), 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /context/variables_test.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/zclconf/go-cty/cty" 10 | ) 11 | 12 | func TestVariables_SetIndex(t *testing.T) { 13 | vars := createVariables().(*variables) 14 | vars.SetIndex(-1) 15 | assert.Equal(t, vars.index, -1) 16 | vars.SetIndex(2) 17 | assert.Equal(t, vars.index, 2) 18 | } 19 | 20 | func TestVariables_SetToContext(t *testing.T) { 21 | vars := createVariables().(*variables) 22 | vars.SetIndex(2) 23 | evalCtx := &hcl.EvalContext{} 24 | rootVar := evalCtx.Variables["_"] 25 | assert.Equal(t, rootVar, cty.NilVal) 26 | vars.SetToContext(evalCtx) 27 | assert.Len(t, evalCtx.Variables, 1) 28 | rootVar = evalCtx.Variables["_"] 29 | index, acc := rootVar.AsValueMap()["index"].AsBigFloat().Int64() 30 | assert.Equal(t, index, int64(2)) 31 | assert.Equal(t, acc, big.Accuracy(0)) 32 | } 33 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | .ignore -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | gem "jekyll", "~> 4.2.0" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | # gem "minima", "~> 2.5" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | # gem "github-pages", group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", "~> 1.2" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | 31 | 32 | gem "webrick", "~> 1.7" 33 | 34 | gem "just-the-docs" 35 | gem 'jekyll-watch' -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.7.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.8) 8 | em-websocket (0.5.2) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0.6.0) 11 | eventmachine (1.2.7) 12 | ffi (1.14.2) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.6.0) 15 | i18n (1.8.7) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (4.2.0) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (~> 1.0) 22 | jekyll-sass-converter (~> 2.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (~> 2.3) 25 | kramdown-parser-gfm (~> 1.0) 26 | liquid (~> 4.0) 27 | mercenary (~> 0.4.0) 28 | pathutil (~> 0.9) 29 | rouge (~> 3.0) 30 | safe_yaml (~> 1.0) 31 | terminal-table (~> 2.0) 32 | jekyll-feed (0.15.1) 33 | jekyll (>= 3.7, < 5.0) 34 | jekyll-sass-converter (2.1.0) 35 | sassc (> 2.0.1, < 3.0) 36 | jekyll-seo-tag (2.7.1) 37 | jekyll (>= 3.8, < 5.0) 38 | jekyll-watch (2.2.1) 39 | listen (~> 3.0) 40 | just-the-docs (0.3.3) 41 | jekyll (>= 3.8.5) 42 | jekyll-seo-tag (~> 2.0) 43 | rake (>= 12.3.1, < 13.1.0) 44 | kramdown (2.3.0) 45 | rexml 46 | kramdown-parser-gfm (1.1.0) 47 | kramdown (~> 2.0) 48 | liquid (4.0.3) 49 | listen (3.4.1) 50 | rb-fsevent (~> 0.10, >= 0.10.3) 51 | rb-inotify (~> 0.9, >= 0.9.10) 52 | mercenary (0.4.0) 53 | pathutil (0.16.2) 54 | forwardable-extended (~> 2.6) 55 | public_suffix (4.0.6) 56 | rake (13.0.3) 57 | rb-fsevent (0.10.4) 58 | rb-inotify (0.10.1) 59 | ffi (~> 1.0) 60 | rexml (3.2.4) 61 | rouge (3.26.0) 62 | safe_yaml (1.0.5) 63 | sassc (2.4.0) 64 | ffi (~> 1.9) 65 | terminal-table (2.0.0) 66 | unicode-display_width (~> 1.1, >= 1.1.1) 67 | unicode-display_width (1.7.0) 68 | webrick (1.7.0) 69 | 70 | PLATFORMS 71 | x86_64-darwin-20 72 | 73 | DEPENDENCIES 74 | jekyll (~> 4.2.0) 75 | jekyll-feed (~> 0.12) 76 | jekyll-watch 77 | just-the-docs 78 | tzinfo (~> 1.2) 79 | tzinfo-data 80 | wdm (~> 0.1.1) 81 | webrick (~> 1.7) 82 | 83 | BUNDLED WITH 84 | 2.2.3 85 | -------------------------------------------------------------------------------- /docs/_about/contact.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Contact 4 | nav_order: 0 5 | --- 6 | 7 | 8 | 9 | # Contact 10 | 11 | Any feedback or suggestion is welcome to make it a useful tool for everyone interested in writing acceptance tests. 12 | 13 | You can drop me an email at [`ivan.corrales.solera@gmail.com`]() or you can find me in any of these social networks: [Linkedin](https://www.linkedin.com/in/ivan-corrales-solera/), [Twitter](https://twitter.com/wesovilabs) or [Github](https://github.com/ivancorrales) 14 | -------------------------------------------------------------------------------- /docs/_about/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Contributing 4 | nav_order: 3 5 | --- 6 | 7 | 8 | 9 | # Contributing 10 | 11 | The purpose of release v0.0.1 is to make Orion be `ready for production`. To achieve it, the [v0.0.1 issues](https://github.com/wesovilabs/orion/milestone/1) must be closed. 12 | 13 | Have a look at our [CONTRIBUTING.md](https://github.com/wesovilabs/orion/blob/master/CONTRIBUTING.md). 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/_gettingstarted/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Examples 4 | nav_order: 3 5 | --- 6 | 7 | # Examples 8 | 9 | A full and growing list of examples can be download from [here](https://github.com/wesovilabs/orion-examples). Additionally, you could clone the repository with all the examples. 10 | 11 | ```bash 12 | git clone git@github.com:wesovilabs/orion-examples.git 13 | ``` 14 | 15 | 16 | *Please feel free to create a Pull Request with new examples* 17 | 18 | ## Preconditions 19 | 20 | Orion must be installed in your computes. [Visit the documentation](../installation) 21 | 22 | ## Examples 23 | 24 | Visit [orion-examples](https://github.com/wesovilabs/orion-examples) 25 | -------------------------------------------------------------------------------- /docs/_gettingstarted/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Installation 4 | nav_order: 1 5 | --- 6 | 7 | 8 | # Installation 9 | 10 | ## orion 11 | 12 | ### Install the pre-compiled binary 13 | 14 | **manually**: Download the pre-compiled binaries from the [releases page](https://github.com/wesovilabs/orion/releases) and copy to the desired location 15 | 16 | Latest version: beta-v2 17 | 18 | - [osx](https://github.com/wesovilabs/orion/releases/download/beta-v2/orion.darwin) 19 | - [linux](https://github.com/wesovilabs/orion/releases/download/beta-v2/orion.linux) 20 | - [windows](https://github.com/wesovilabs/orion/releases/download/beta-v2/orion.exe) 21 | 22 | **Tip** 23 | 24 | Once you download the binary I recommend you to do the below: 25 | 26 | ```bash 27 | mkdir ~/.orion 28 | mv orion.darwin ~/.orion/orion 29 | ``` 30 | Add an entry to your `~/.bash_profile` 31 | 32 | ```bash 33 | export PATH="~/.orion:$PATH" 34 | ``` 35 | 36 | To verify you installed orion correctly, you just need to execute 37 | 38 | ```bash 39 | orion help 40 | ``` 41 | 42 | ## Running with Docker 43 | 44 | You can also use it within a Docker container. To do that, you'll need to execute something more-or-less like the following: 45 | 46 | ```bash 47 | docker run --rm \ 48 | -v $(PWD):/vars/feaatures \ 49 | wesovilabs/orion:beta-v2 run --input /vars/feaatures/feature.hcl 50 | ``` 51 | 52 | ## Compiling from source 53 | 54 | ```bash 55 | git clone git@github.com:wesovilabs/orion.git 56 | cd orion 57 | make setup 58 | make build 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/_gettingstarted/links.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Guides & Tutorials 4 | nav_order: 4 5 | --- 6 | 7 | # Guides & Tutorials 8 | 9 | - [Orion: A next-generation automation testing tool](https://ivan-corrales-solera.medium.com/orion-a-next-generation-automation-testing-tool-4ea53eeb2517) 10 | - [Orion in action: Testing a real API](https://ivan-corrales-solera.medium.com/orion-in-action-testing-a-real-api-a780244141a2) 11 | -------------------------------------------------------------------------------- /docs/_gettingstarted/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Usage 4 | nav_order: 2 5 | --- 6 | 7 | 8 | # orion 9 | 10 | orion is the command line interface for orion. Since the tool is still in progress 11 | new command will be available in short time. So far, we can use `orion lint` and `orion lint`. 12 | 13 | ## orion help 14 | 15 | Help about any command. Additionally, we can show the help of a specific command: 16 | 17 | - `orion help lint` 18 | - `orion help run` 19 | 20 | ## orion lint 21 | 22 | Verify the content of the input file is correctly written. 23 | 24 | Available flags: 25 | - `--input`: Path of input file. 26 | 27 | ## orion run 28 | 29 | Execute the scenarios in the provided input file. 30 | 31 | Available flags: 32 | - `--input`: path of the input file. 33 | - `--vars`: path of the variables file. 34 | - `--tags`: comma separated list of tag names. Scenarios containing any of the listed tags will be executed. 35 | - `--verbose`: log level. Supported values are: 'DEBUG','INFO','WARN','ERROR' (default "INFO"). 36 | -------------------------------------------------------------------------------- /docs/_spec/actions/block.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: block 4 | parent: Actions 5 | nav_order: 8 6 | --- 7 | 8 | # block 9 | It is used to group a set of actions. 10 | 11 | ```hcl 12 | block { 13 | set carOwner { 14 | value = "John Doe" 15 | } 16 | print { 17 | msg = "the owner of the car is ${carOwner}" 18 | } 19 | when = car.color=="red" 20 | } 21 | ``` 22 | 23 | **Supported actions:** 24 | - [set](../set) 25 | - [print](../print) 26 | - [assert](../assert) 27 | - [http](../http) 28 | - [mongo](../mongo) 29 | - [call](../call) 30 | - [sleep](../sleep) 31 | 32 | ## Specification 33 | 34 | ### Arguments 35 | 36 | | | Type | Required?| Vars supported? | 37 | |:----------------|:----------|---------:|----------------:| 38 | | **description** | string | no | no | 39 | | **while** | boolean | no | yes | 40 | | **when** | boolean | no | yes | 41 | | **count** | numeric | no | yes | 42 | 43 | 44 | **description** ( string \| optional ) It is used to apply descriptive text to the action. 45 | 46 | *Example 1: Basic use of argument* 47 | 48 | ```hcl 49 | block{ 50 | description = "a new person is created and registered." 51 | } 52 | ``` 53 | --- 54 | **when** ( bool | optional ) It is used to control if the action must be executed. 55 | 56 | ```hcl 57 | var { 58 | evalJobStatus = false 59 | } 60 | 61 | block{ 62 | when = evalJobStatus 63 | } 64 | ``` 65 | --- 66 | **count** ( number || optional ) It determines the number of times the action is executed. Additionally, the variable **_.index** is increased in each iteration. 67 | The value of _.index starts with 0 and it ends with count-1. 68 | 69 | *Example 1: Basic use of the argument* 70 | ```hcl 71 | block { 72 | 73 | count = 3 74 | } 75 | ``` 76 | 77 | --- 78 | **while** ( boolean \| optional ) The action is executed repeatedly as long as the value of this argument is met. Additionally, the variable **_.index** is increased in each iteration. The value of _.index starts with 0 and increase in 1 in each iteration. 79 | 80 | *Example 1: Basic use of argument while* 81 | ```hcl 82 | block { 83 | while = _.index<=2 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/_spec/actions/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Actions 4 | nav_order: 4 5 | has_children: true 6 | has_toc: false 7 | --- 8 | 9 | 10 | # Actions 11 | 12 | Documentation and examples of the existing actions can be found on the below links: 13 | 14 | - [set](../set) 15 | - [print](../print) 16 | - [assert](../assert) 17 | - [http](../http) 18 | - [mongo](../mongo). 19 | - [call](../call). 20 | - [sleep](../sleep). 21 | - [block](../block). 22 | 23 | New actions are in progress: `graphql`, `grpc`, `mysql` and `postgres` among others. 24 | 25 | Currently, I'm working on a guide for developers to explain how to create new actions. 26 | -------------------------------------------------------------------------------- /docs/_spec/actions/sleep.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: sleep 4 | parent: Actions 5 | nav_order: 7 6 | --- 7 | 8 | # sleep 9 | 10 | The action **sleep** is used to pause the execution for a specified duration. 11 | 12 | ## Specification 13 | 14 | ### Arguments 15 | 16 | | | Type | Required?| Vars supported? | 17 | |:----------------|:----------|---------:|----------------:| 18 | | **duration** | duration | yes | yes | 19 | | **description** | string | no | no | 20 | | **while** | boolean | no | yes | 21 | | **when** | boolean | no | yes | 22 | | **count** | numeric | no | yes | 23 | 24 | 25 | **duration** ( duration \| required ) : It specified the `time.duration` to pause the execution. 26 | 27 | *Example 1: Basic use of argument duration* 28 | 29 | ```hcl 30 | sleep { 31 | duration = "2s" 32 | } 33 | ``` 34 | --- 35 | **description** ( string \| optional ) It is used to apply descriptive text to the action. 36 | 37 | *Example 1: Basic use of argument* 38 | 39 | ```hcl 40 | sleep { 41 | description = "this statement is used to pause the execution for 2 seconds" 42 | duration = "2s" 43 | } 44 | ``` 45 | --- 46 | **when** ( bool | optional ) It is used to control if the action must be executed. 47 | 48 | ```hcl 49 | var { 50 | evalJobStatus = false 51 | } 52 | 53 | sleep { 54 | duration = "2s" 55 | when = evalJobStatus && exists(job) 56 | } 57 | ``` 58 | --- 59 | **count** ( number || optional ) It determines the number of times the action is executed. 60 | 61 | *Example 1: Basic use of the argument count* 62 | ```hcl 63 | sleep { 64 | duration = "2s" 65 | count = 3 66 | } 67 | ``` 68 | --- 69 | **while** ( boolean \| optional ) The action is executed repeatedly as long as the value of this argument is met. Additionally, the variable **_.index** is increased in each iteration. The value of _.index starts with 0 and increase in 1 in each iteration. 70 | 71 | *Example 1: Basic use of argument while* 72 | ```hcl 73 | assert { 74 | duration = "2s" 75 | while = _.index<=2 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/_spec/features/includes.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Includes 4 | nav_order: 4 5 | parent: Feature 6 | --- 7 | # Includes 8 | 9 | 10 | 11 | The purpose of use includes is to reuse any feature part between multiple executions. 12 | The includes are defined in attribute `includes`, of type array, in the root of the feature. 13 | 14 | Let's go through the below example. We could define a file with common hooks. 15 | 16 | **Example: common-hooks.hcl** [download](https://raw.githubusercontent.com/wesovilabs/orion-examples/master/site/common-hooks.hcl) 17 | ```hcl 18 | before sample { 19 | set a { 20 | value = 1 21 | } 22 | } 23 | before common { 24 | set b { 25 | value = 2 26 | } 27 | } 28 | after common { 29 | print { 30 | msg = "operation completed with result ${result}" 31 | } 32 | } 33 | ``` 34 | 35 | The common-hooks.hcl file could be included in any feature: 36 | 37 | **Example** [download](https://raw.githubusercontent.com/wesovilabs/orion-examples/master/site/feature008.hcl) 38 | ```hcl 39 | includes = [ 40 | "common-hooks.hcl" 41 | ] 42 | 43 | scenario "scenario1" { 44 | tags = ["common", "sample"] 45 | when "a and b are added" { 46 | set result { 47 | value = a + b 48 | } 49 | } 50 | then "the result is the expected"{ 51 | assert { 52 | assertion = result==3 53 | } 54 | } 55 | } 56 | scenario "scenario2" { 57 | tags = ["common"] 58 | when "b is duplicated" { 59 | set result { 60 | value = 2 * b 61 | } 62 | } 63 | then "verify the result"{ 64 | assert { 65 | assertion = result==4 66 | } 67 | } 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/_spec/features/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Feature 4 | nav_order: 0 5 | has_children: true 6 | has_toc: false 7 | --- 8 | 9 | 10 | # Feature 11 | 12 | A feature is described in a file and it contains one or more scenarios. The extension of the file doesn't matter at all. On the other hand, we recommend you to use extension `hcl` so far. 13 | 14 | **Example** [download](https://raw.githubusercontent.com/wesovilabs/orion-examples/master/site/feature001.hcl) 15 | ```hcl 16 | description = < 9 | 10 | # Vars 11 | 12 | By making use of block `vars`, we define variables that could be reused by one or more scenarios. This block contains a 13 | set of attributes in hcl format. Only one block `vars` is permitted per file. 14 | 15 | **Example** [download](https://raw.githubusercontent.com/wesovilabs/orion/master/examples/vars/feature001.hcl) 16 | 17 | ```hcl 18 | vars { 19 | x = 1 20 | elements = [ 21 | { product = "tofu", vegan = true}, 22 | { product = "meat", vegan = false}, 23 | { product = "fish", vegan = false}, 24 | { product = "avocado", vegan = true}, 25 | ] 26 | } 27 | scenario "feature with plain var" { 28 | when "multiply by 2" { 29 | set result{ 30 | value = x * 2 31 | } 32 | } 33 | then "check output" { 34 | assert { 35 | assertion = result==2 36 | } 37 | } 38 | } 39 | scenario "feature with array var" { 40 | given "initial karma"{ 41 | set karma { 42 | value = 0 43 | } 44 | } 45 | when "calculate karma" { 46 | block { 47 | set karma { 48 | value = karma + 1 49 | when = elements[_.index].vegan 50 | } 51 | set karma { 52 | value = karma - 1 53 | when = !elements[_.index].vegan 54 | } 55 | count = len(elements) 56 | } 57 | } 58 | then "check output" { 59 | assert { 60 | assertion = karma==0 61 | } 62 | } 63 | } 64 | 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/_spec/functions/boolean.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Boolean functions 4 | parent: Functions 5 | nav_order: 2 6 | --- 7 | 8 | 9 | # Boolean functions 10 | 11 | ## Comparisons 12 | 13 | | Operation | Description | Signature | 14 | |:--------------|:--------------------------------------------|-----------:| 15 | | == | It returns true if `val1`is equal to `val2` | val1 == val2 | 16 | | != | It returns true if `val1`is equal to `val2` and false otherwise| val1 != val2 | 17 | | \|\| | or comparator|val1 \|\| val2 | 18 | | && | and comparator|val1 && val2 | 19 | | ! | not | !val1 | 20 | | () | group conditions | (val1 && val2) | 21 | 22 | 23 | **Example** [download](https://raw.githubusercontent.com/wesovilabs/orion-examples/master/site/feature013.hcl) 24 | ```hcl 25 | scenario "check boolean operations" { 26 | given "set value" { 27 | set val1{ 28 | value = true 29 | } 30 | } 31 | when "do the operations" { 32 | set val2{ 33 | value = !val1 34 | } 35 | } 36 | then "the result is the expected"{ 37 | assert { 38 | assertion = val1 && !val2 && (val1 || val2) && !val2 && val2==false 39 | } 40 | } 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/_spec/functions/collection.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Collection functions 4 | parent: Functions 5 | nav_order: 4 6 | --- 7 | 8 | 9 | # Collection functions 10 | 11 | ## Items 12 | 13 | | Operation | Description | Signature | 14 | |:--------------|:--------------------------------------------|-----------:| 15 | | at | It returns element in position `pos`in the collection `coll`| at(coll,post)| 16 | | first | It returns the first element in the collection `coll` | first(coll) | 17 | | last | It returns the last element in the collection `coll` | last(coll) | 18 | | len | It returns the number of elements in the collection `coll` | len(coll) | 19 | 20 | 21 | **Example** [download](https://raw.githubusercontent.com/wesovilabs/orion-examples/master/site/feature016.hcl) 22 | ```hcl 23 | scenario "check collection functions" { 24 | given "set value" { 25 | set elements{ 26 | value = [ 27 | { 28 | name = "Sally" 29 | specie = "dog" 30 | }, 31 | { 32 | name = "Molly" 33 | specie = "dog" 34 | }, 35 | { 36 | name = "Coco" 37 | specie = "dog" 38 | } 39 | ] 40 | } 41 | } 42 | when "obtain the element at position 1" { 43 | set element { 44 | value = elements[1] 45 | } 46 | 47 | } 48 | then "check the element"{ 49 | assert { 50 | assertion = element.name == "Molly" 51 | } 52 | } 53 | when "obtain the first element" { 54 | set element { 55 | value = first(elements) 56 | } 57 | 58 | } 59 | then "check the element"{ 60 | assert { 61 | assertion = element.name == "Sally" 62 | } 63 | } 64 | when "obtain the last element" { 65 | set element { 66 | value = last(elements) 67 | } 68 | 69 | } 70 | then "check the element"{ 71 | assert { 72 | assertion = element.name == "Coco" 73 | } 74 | } 75 | when "obtain the number of elements in the list" { 76 | set totalElement { 77 | value = len(elements) 78 | } 79 | 80 | } 81 | then "check the element"{ 82 | assert { 83 | assertion = totalElement==3 84 | } 85 | } 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/_spec/functions/format.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Formatter functions 4 | parent: Functions 5 | nav_order: 45 6 | --- 7 | 8 | 9 | 10 | # Formatter functions 11 | 12 | ## Converter 13 | 14 | | Operation | Description | Signature | 15 | |:--------------|:--------------------------------------------|-----------:| 16 | | json | It unmarshals the input `text` as a json| json(text)| 17 | 18 | 19 | **formatter.hcl** 20 | ```hcl 21 | scenario "test formatter functions" { 22 | given "string values" { 23 | set valueJSON { 24 | value = "{\"firstname\":\"John\"}" 25 | } 26 | } 27 | when "convert json into map" { 28 | set person { 29 | value = json(valueJSON) 30 | } 31 | } 32 | then "the json has been formatted successfully" { 33 | assert { 34 | assertion = person.firstname == "John" 35 | } 36 | } 37 | } 38 | ``` -------------------------------------------------------------------------------- /docs/_spec/functions/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Functions 4 | nav_order: 3 5 | has_children: true 6 | has_toc: false 7 | --- 8 | 9 | 10 | 11 | # Functions 12 | 13 | So far there are a great set of provided functions by Orion but **much more are coming in upcoming releases**. 14 | 15 | - [Text functions](../text) 16 | - [Boolean functions](../boolean) 17 | - [Number functions](../number) 18 | - [Collection functions](../collection) 19 | - [Formatter functions](../format). -------------------------------------------------------------------------------- /docs/_spec/syntax.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Syntax 4 | nav_order: 0 5 | --- 6 | 7 | 8 | # Syntax 9 | 10 | The Orion syntax is implemented with HCL from Hashicorp. If you worked with Nomad or Terraform you 11 | are used to working with this type of syntax. 12 | 13 | The below context has been extracted from [HCL README](https://raw.githubusercontent.com/hashicorp/hcl/master/README.md) 14 | 15 | 16 | For a complete grammar, please see the parser itself. A high-level overview 17 | of the syntax and grammar is listed here. 18 | 19 | * Single line comments start with `#` or `//` 20 | 21 | * Multi-line comments are wrapped in `/*` and `*/`. Nested block comments 22 | are not allowed. A multi-line comment (also known as a block comment) 23 | terminates at the first `*/` found. 24 | 25 | * Values are assigned with the syntax `key = value` (whitespace doesn't 26 | matter). The value can be any primitive: a string, number, boolean, 27 | object, or list. 28 | 29 | * Strings are double-quoted and can contain any UTF-8 characters. 30 | Example: `"Hello, World"` 31 | 32 | * Multi-line strings start with `< 8 | 9 | # Hello, I'm Orion 10 | 11 | Orion is born to simplify the way we write our acceptance tests. 12 | 13 | ```hcl 14 | input { 15 | arg ghBase { 16 | default = "https://api.github.com" 17 | } 18 | arg expect {} 19 | arg orgId {} 20 | } 21 | 22 | before each { 23 | set mediaType { 24 | value = "application/vnd.github.v3+json" 25 | } 26 | } 27 | 28 | scenario "username details" { 29 | when "call Github api" { 30 | http get { 31 | request { 32 | baseUrl = ghBase 33 | path = "/users/${username}" 34 | headers { 35 | Accept = mediaType 36 | } 37 | } 38 | response { 39 | user = json(_.http.body) 40 | } 41 | } 42 | print { 43 | msg = "user ${username} has ${user.followers} followers." 44 | } 45 | } 46 | then "the user details are the expected" { 47 | assert { 48 | assertion = user!=null && user.followers>expect.user.followers 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ## About the project 55 | 56 | Orion is born to change the way we implement our acceptance tests. It takes advantage of HCL from Hashicorp to provide a **simple DSL to write the acceptance tests**. The syntax is inspired in Gherkin. 57 | 58 | ## Goals 59 | 60 | - **Non-technical** people can design but also **implement the acceptance tests**. 61 | - It's implemented under a **pluggable architecture**. New actions can be implemented easily. 62 | - **Reusable** functionality can be shared between **features**. 63 | 64 | ## License 65 | 66 | Software is completely free, and It will be distributed as opensource soon. 67 | 68 | ## Contributing 69 | 70 | Orion will be opensource soon. In the meanwhile any feedback, suggestion is welcome! [Contact me](/contact/index/) 71 | -------------------------------------------------------------------------------- /docs/medium-resources/step-1/feature-math-operations.hcl: -------------------------------------------------------------------------------- 1 | description = < 10 36 | when "values are subtracted" { 37 | set result { 38 | value = opSub.a - opSub.b 39 | } 40 | } 41 | then "the result of the operation is the expected" { 42 | assert { 43 | assertion = result==opSub.expected 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/medium-resources/step-4/vars-math-operations.hcl: -------------------------------------------------------------------------------- 1 | opSum = { 2 | a = 12 3 | b = 23 4 | expected = 35 5 | } 6 | opSub = { 7 | a = 45 8 | b = 27 9 | expected = 18 10 | } -------------------------------------------------------------------------------- /docs/medium-resources/step-5/feature-math-operations.hcl: -------------------------------------------------------------------------------- 1 | description = < 10 3 | when "values are subtracted" { 4 | set result { 5 | value = opSub.a - opSub.b 6 | } 7 | } 8 | then "the result of the operation is the expected" { 9 | assert { 10 | assertion = result==opSub.expected 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/medium-resources/step-5/scenario-sum.hcl: -------------------------------------------------------------------------------- 1 | scenario "operation add" { 2 | when "values are added" { 3 | set result { 4 | value = opSum.a + opSum.b 5 | } 6 | } 7 | then "the result of the operation is the expected" { 8 | assert { 9 | assertion = result==opSum.expected 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /docs/medium-resources/step-5/vars-math-operations.hcl: -------------------------------------------------------------------------------- 1 | opSum = { 2 | a = 12 3 | b = 23 4 | expected = 35 5 | } 6 | opSub = { 7 | a = 45 8 | b = 27 9 | expected = 18 10 | } -------------------------------------------------------------------------------- /docs/medium-resources/step-6/feature-math-operations.hcl: -------------------------------------------------------------------------------- 1 | description = < opSub.b 6 | } 7 | set result { 8 | value = opSub.b - opSub.a 9 | when = opSub.a <= opSub.b 10 | } 11 | } 12 | then "the result of the operation is the expected" { 13 | assert { 14 | assertion = result==opSub.expected 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/medium-resources/step-6/scenario-sum.hcl: -------------------------------------------------------------------------------- 1 | scenario "operation add" { 2 | when "values are added" { 3 | set result { 4 | value = opSum.a + opSum.b 5 | } 6 | } 7 | then "the result of the operation is the expected" { 8 | assert { 9 | assertion = result==opSum.expected 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /docs/medium-resources/step-6/vars-math-operations.hcl: -------------------------------------------------------------------------------- 1 | opSum = { 2 | a = 12 3 | b = 23 4 | expected = 35 5 | } 6 | opSub = { 7 | a = 45 8 | b = 27 9 | expected = 18 10 | } -------------------------------------------------------------------------------- /docs/medium-resources/step-7/feature-math-operations.hcl: -------------------------------------------------------------------------------- 1 | description = < opSub.b 6 | } 7 | set result { 8 | value = opSub.b - opSub.a 9 | when = opSub.a <= opSub.b 10 | } 11 | } 12 | then "the result of the operation is the expected" { 13 | assert { 14 | assertion = result==opSub.expected 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/medium-resources/step-7/scenario-sum.hcl: -------------------------------------------------------------------------------- 1 | scenario "operation add" { 2 | examples = [ 3 | { a = 3, b = 2, c = 5}, 4 | { a = 4, b = 2, c = 6}, 5 | { a = 8, b = 12, c = 20}, 6 | { a = 13, b = 12, c = 25}, 7 | ] 8 | when "values are added" { 9 | set result { 10 | value = a + b 11 | } 12 | } 13 | then "the result of the operation is the expected" { 14 | assert { 15 | assertion = result==expected 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /docs/medium-resources/step-7/vars-math-operations.hcl: -------------------------------------------------------------------------------- 1 | opSunDataset = [ 2 | { a = 3, b = 2, c = 5}, 3 | { a = 4, b = 2, c = 6}, 4 | { a = 8, b = 12, c = 20}, 5 | { a = 13, b = 12, c = 25}, 6 | ] 7 | opSub = { 8 | a = 45 9 | b = 27 10 | expected = 18 11 | } 12 | 13 | -------------------------------------------------------------------------------- /docs/samples/functions/feature001.hcl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wesovilabs/orion/f8becd33b5ea5e3d09a74f3026d4cf3ae58f9fab/docs/samples/functions/feature001.hcl -------------------------------------------------------------------------------- /docs/samples/hooks/feature.hcl: -------------------------------------------------------------------------------- 1 | input { 2 | arg b { 3 | default = 2 4 | } 5 | } 6 | 7 | before sample { 8 | set a { 9 | value = 1 10 | } 11 | } 12 | 13 | after common { 14 | print { 15 | msg = "operation completed with result ${result}" 16 | } 17 | } 18 | 19 | scenario "scenario1" { 20 | tags = ["common", "sample"] 21 | when "a and b are added" { 22 | set result { 23 | value = a + b 24 | } 25 | } 26 | then "the result is the expected"{ 27 | assert { 28 | assertion = result==3 29 | } 30 | } 31 | } 32 | scenario "scenario2" { 33 | tags = ["common"] 34 | when "b is duplicated" { 35 | set result { 36 | value = 2 * b 37 | } 38 | } 39 | then "verify the result"{ 40 | assert { 41 | assertion = result==4 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /docs/samples/hooks/feature2.hcl: -------------------------------------------------------------------------------- 1 | before each { 2 | description = "common hook to be executed before each scenario" 3 | set a { 4 | value = 1 5 | } 6 | } 7 | 8 | after each { 9 | description = "common hook to be executed after each scenario" 10 | print { 11 | msg = "value of b is ${b}" 12 | } 13 | } 14 | 15 | scenario "scenario3" { 16 | tags = ["common"] 17 | when "do a sum" { 18 | set b { 19 | value = 2 * a 20 | } 21 | } 22 | then "verify the result"{ 23 | assert { 24 | assertion= b==2 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /docs/samples/includes/feature-with-includes.hcl: -------------------------------------------------------------------------------- 1 | includes = [ 2 | "hooks.hcl" 3 | ] 4 | 5 | scenario "scenario with tag smoke-test" { 6 | tags = ["smoke-test","other"] 7 | when "assign true to a local variable" { 8 | set a { 9 | value = true 10 | } 11 | } 12 | then "the value of the variable is true" { 13 | assert { 14 | assertion = a 15 | } 16 | } 17 | } 18 | scenario "scenario with tag smoke-test" { 19 | tags = ["performance-test","other"] 20 | when "assign true to a local variable" { 21 | set a { 22 | value = true 23 | } 24 | } 25 | then "the value of the variable is true" { 26 | assert { 27 | assertion = a 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /docs/samples/includes/hooks.hcl: -------------------------------------------------------------------------------- 1 | before smoke-test { 2 | print { 3 | msg = "hook before: smoke-test" 4 | } 5 | } 6 | after each { 7 | print { 8 | msg = "hook after: each" 9 | } 10 | } -------------------------------------------------------------------------------- /docs/samples/operations/boolean-comparison.hcl: -------------------------------------------------------------------------------- 1 | scenario "check boolean operations" { 2 | given "set value" { 3 | set val1{ 4 | value = true 5 | } 6 | } 7 | when "do the operations" { 8 | set val2{ 9 | value = !val1 10 | } 11 | } 12 | then "the result is the expected"{ 13 | assert { 14 | assertion = val1 && val2 && (val1 || val2) && !val2 && val2==false 15 | } 16 | } 17 | } 18 | 19 | // go run ./cmd/orion.go run --input ${ORION_SAMPLES}/functions/boolean-comparison.hcl -------------------------------------------------------------------------------- /docs/samples/operations/collection.hcl: -------------------------------------------------------------------------------- 1 | scenario "check collection functions" { 2 | given "set value" { 3 | set elements{ 4 | value = [ 5 | { 6 | name = "Sally" 7 | specie = "dog" 8 | }, 9 | { 10 | name = "Molly" 11 | specie = "dog" 12 | }, 13 | { 14 | name = "Coco" 15 | specie = "dog" 16 | } 17 | ] 18 | } 19 | } 20 | when "obtain the element at position 1" { 21 | set element { 22 | value = elements[1] 23 | } 24 | 25 | } 26 | then "check the element"{ 27 | assert { 28 | assertion = element.name == "Molly" 29 | } 30 | } 31 | when "obtain the first element" { 32 | set element { 33 | value = first(elements) 34 | } 35 | 36 | } 37 | then "check the element"{ 38 | assert { 39 | assertion = element.name == "Sally" 40 | } 41 | } 42 | when "obtain the last element" { 43 | set element { 44 | value = last(elements) 45 | } 46 | 47 | } 48 | then "check the element"{ 49 | assert { 50 | assertion = element.name == "Coco" 51 | } 52 | } 53 | when "obtain the number of elements in the list" { 54 | set totalElement { 55 | value = len(elements) 56 | } 57 | 58 | } 59 | then "check the element"{ 60 | assert { 61 | assertion = totalElement==3 62 | } 63 | } 64 | } 65 | 66 | // go run ./cmd/orion.go run --input ${ORION_SAMPLES}/functions/collection.hcl -------------------------------------------------------------------------------- /docs/samples/operations/formatter.hcl: -------------------------------------------------------------------------------- 1 | scenario "test formatter functions" { 2 | given "string values" { 3 | set valueJSON { 4 | value = "{\"firstname\":\"John\"}" 5 | } 6 | } 7 | when "convert json into map" { 8 | set person { 9 | value = json(valueJSON) 10 | } 11 | } 12 | then "the json has been formatted successfully" { 13 | assert { 14 | assertion = person.firstname == "John" 15 | } 16 | } 17 | } 18 | 19 | // go run ./cmd/orion.go run --input ${ORION_SAMPLES}/functions/formatter.hcl -------------------------------------------------------------------------------- /docs/samples/operations/number-comparisons.hcl: -------------------------------------------------------------------------------- 1 | 2 | scenario "number comparisons" { 3 | given "input values" { 4 | set val1 { 5 | value = 100 6 | } 7 | set val2 { 8 | value = 30.213 9 | } 10 | } 11 | when "do operations with numbers" { 12 | print { 13 | msg="Input values are val1:${val1} and val2:${val2}" 14 | } 15 | } 16 | then "result is the expected"{ 17 | assert { 18 | assertion= val1>val2 && val1>=val2 && val2 0 { 29 | return nil, errors.ThrowUnsupportedArguments(blockBody) 30 | } 31 | if len(bodyContent.Blocks) == 0 { 32 | return nil, errors.IncorrectUsage("block '%s' must contain one action at least", blockBody) 33 | } 34 | actions, err := handler.DecodePlugins(bodyContent.Blocks) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return &Body{ 39 | actions: actions, 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /dsl/body_test.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/wesovilabs/orion/internal/oriontest" 10 | ) 11 | 12 | var expectedBodiesDecodeErrs = map[string]struct{}{ 13 | "testdata/body.hcl:6,3-7: Unsupported argument; An argument named \"name\" is not expected here.": {}, 14 | "block 'body' must contain one action at least": {}, 15 | } 16 | 17 | var expectedBodiesActions = []int{0, 0, 1, 2} 18 | 19 | func TestBody_Execute(t *testing.T) { 20 | blocks := oriontest.ParseHCL("testdata/body.hcl", &hcl.BodySchema{ 21 | Blocks: []hcl.BlockHeaderSchema{ 22 | {Type: blockBody}, 23 | }, 24 | }) 25 | for index := range blocks { 26 | block := blocks[index] 27 | body, err := decodeBody(block) 28 | if err != nil { 29 | fmt.Println(err.Message()) 30 | assert.Contains(t, expectedBodiesDecodeErrs, err.Message()) 31 | continue 32 | } 33 | assert.NotNil(t, body) 34 | assert.Len(t, body.actions, expectedBodiesActions[index]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dsl/constants.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | const ( 4 | blockInput = "input" 5 | blockArg = "arg" 6 | blockGiven = "given" 7 | blockWhen = "when" 8 | blockThen = "then" 9 | 10 | argDescription = "description" 11 | argIncludes = "includes" 12 | ArgDefault = "default" 13 | blockScenario = "scenario" 14 | blockHookBefore = "before" 15 | blockHookAfter = "after" 16 | 17 | labelName = "name" 18 | labelDescription = "description" 19 | labelTag = "tag" 20 | 21 | argTags = "tags" 22 | argContinueOnError = "continueOnError" 23 | argIgnore = "ignore" 24 | argExamples = "examples" 25 | ) 26 | -------------------------------------------------------------------------------- /dsl/doc.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | -------------------------------------------------------------------------------- /dsl/given.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/actions/set" 6 | 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | var schemaGiven = &hcl.BodySchema{ 11 | Blocks: []hcl.BlockHeaderSchema{ 12 | {Type: set.BlockSet, LabelNames: []string{set.LabelName}}, 13 | }, 14 | Attributes: nil, 15 | } 16 | 17 | // Given represents the stage in a schemaScenario in which the preconditions 18 | // must be declared and the data are initialized. 19 | type Given struct { 20 | *section 21 | } 22 | 23 | func decodeGiven(block *hcl.Block) (*Given, errors.Error) { 24 | given := &Given{ 25 | section: newSection(blockGiven, block), 26 | } 27 | 28 | bc, d := block.Body.Content(schemaGiven) 29 | if err := errors.EvalDiagnostics(d); err != nil { 30 | return nil, err 31 | } 32 | if err := given.populateAttributes(bc.Attributes); err != nil { 33 | return nil, err 34 | } 35 | if err := given.populateActions(bc.Blocks); err != nil { 36 | return nil, err 37 | } 38 | return given, nil 39 | } 40 | 41 | func (given *Given) populateAttributes(attributes hcl.Attributes) errors.Error { 42 | if len(attributes) > 0 { 43 | return errors.ThrowUnsupportedArguments(blockThen) 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /dsl/helper.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/actions/register" 6 | 7 | "github.com/wesovilabs/orion/helper" 8 | "github.com/wesovilabs/orion/internal/errors" 9 | "github.com/zclconf/go-cty/cty" 10 | ) 11 | 12 | var handler = register.GetHandler() 13 | 14 | func attributeToStringWithoutContext(attribute *hcl.Attribute) (string, errors.Error) { 15 | if len(attribute.Expr.Variables()) > 0 { 16 | return "", errors.IncorrectUsage("variables are not permitted in attribute %s", attribute.Name) 17 | } 18 | val, err := helper.EvalAttribute(nil, attribute) 19 | if err != nil { 20 | return "", err 21 | } 22 | return helper.ToStrictString(val) 23 | } 24 | 25 | func attributeToSliceWithoutContext(attribute *hcl.Attribute) ([]cty.Value, errors.Error) { 26 | if len(attribute.Expr.Variables()) > 0 { 27 | return nil, errors.IncorrectUsage("variables are not permitted in attribute %s", attribute.Name) 28 | } 29 | value, err := helper.EvalAttribute(nil, attribute) 30 | if err != nil { 31 | return nil, err 32 | } 33 | if value.Type().IsListType() || value.Type().IsTupleType() || value.Type().IsCollectionType() { 34 | return value.AsValueSlice(), nil 35 | } 36 | return nil, errors.InvalidArguments("expected a slice argument but it's not") 37 | } 38 | -------------------------------------------------------------------------------- /dsl/hook.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "fmt" 5 | 6 | actions2 "github.com/wesovilabs/orion/actions" 7 | "github.com/wesovilabs/orion/context" 8 | 9 | "github.com/hashicorp/hcl/v2" 10 | 11 | "github.com/wesovilabs/orion/internal/errors" 12 | ) 13 | 14 | var schemaHook = &hcl.BodySchema{ 15 | Attributes: []hcl.AttributeSchema{ 16 | {Name: argDescription, Required: false}, 17 | }, 18 | Blocks: handler.GetBlocksSpec(), 19 | } 20 | 21 | const ( 22 | HookBefore = "before" 23 | HookAfter = "after" 24 | hookEach string = "each" 25 | ) 26 | 27 | type Hooks map[string]*Hook 28 | 29 | type Hook struct { 30 | kind string 31 | description string 32 | tag string 33 | actions actions2.Actions 34 | } 35 | 36 | func (h *Hook) TotalActions() int { 37 | return len(h.actions) 38 | } 39 | 40 | func (h *Hook) String() string { 41 | if h.description == "" { 42 | return fmt.Sprintf("[%s|%s]", h.tag, h.kind) 43 | } 44 | return fmt.Sprintf("[%s|%s]: %s", h.tag, h.kind, h.description) 45 | } 46 | 47 | func (h *Hook) Execute(ctx context.OrionContext) errors.Error { 48 | for index := range h.actions { 49 | action := h.actions[index] 50 | if err := action.Execute(ctx); err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | func decodeHook(block *hcl.Block) (*Hook, errors.Error) { 58 | if len(block.Labels) != 1 { 59 | return nil, errors.ThrowMissingRequiredLabel(block.Type) 60 | } 61 | hook := &Hook{ 62 | tag: block.Labels[0], 63 | } 64 | bc, d := block.Body.Content(schemaHook) 65 | if err := errors.EvalDiagnostics(d); err != nil { 66 | return nil, err 67 | } 68 | if err := hook.populateAttributes(bc.Attributes); err != nil { 69 | return nil, err 70 | } 71 | if err := hook.populateActions(bc.Blocks); err != nil { 72 | return nil, err 73 | } 74 | return hook, nil 75 | } 76 | 77 | func (h *Hook) populateAttributes(attrs hcl.Attributes) errors.Error { 78 | var err errors.Error 79 | for name, attribute := range attrs { 80 | switch name { 81 | case argDescription: 82 | if h.description, err = attributeToStringWithoutContext(attribute); err != nil { 83 | return err 84 | } 85 | default: 86 | return errors.ThrowUnsupportedArgument("hook", name) 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | func (h *Hook) populateActions(blocks hcl.Blocks) (err errors.Error) { 93 | h.actions, err = handler.DecodePlugins(blocks) 94 | return 95 | } 96 | -------------------------------------------------------------------------------- /dsl/include.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | 8 | "github.com/wesovilabs/orion/helper" 9 | "github.com/wesovilabs/orion/internal/errors" 10 | ) 11 | 12 | const ( 13 | filePath = "file://" 14 | fileType = "file" 15 | ) 16 | 17 | type Includes []*Include 18 | 19 | type Include struct { 20 | kind string 21 | path string 22 | } 23 | 24 | func (i *Include) IsFile() bool { 25 | return i.kind == fileType 26 | } 27 | 28 | func (i *Include) Path() string { 29 | return i.path 30 | } 31 | 32 | func includesFromValue(attr *hcl.Attribute) (Includes, errors.Error) { 33 | includesValue, d := attr.Expr.Value(nil) 34 | if err := errors.EvalDiagnostics(d); err != nil { 35 | return nil, err 36 | } 37 | if !helper.IsSlice(includesValue) { 38 | return nil, errors.IncorrectUsage("attribute '$s' must be a slice", argIncludes) 39 | } 40 | 41 | slice := includesValue.AsValueSlice() 42 | includes := make(Includes, len(slice)) 43 | for index := range slice { 44 | itemValue := slice[index] 45 | path, err := helper.ToStrictString(itemValue) 46 | if err != nil { 47 | return nil, err 48 | } 49 | var include *Include 50 | switch { 51 | case strings.HasPrefix(path, filePath): 52 | include = &Include{ 53 | kind: fileType, 54 | path: strings.TrimPrefix(path, filePath), 55 | } 56 | default: 57 | include = &Include{ 58 | kind: fileType, 59 | path: path, 60 | } 61 | } 62 | includes[index] = include 63 | } 64 | return includes, nil 65 | } 66 | -------------------------------------------------------------------------------- /dsl/input.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/context" 6 | 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | var schemaInput = &hcl.BodySchema{ 11 | Blocks: []hcl.BlockHeaderSchema{ 12 | {Type: blockArg, LabelNames: []string{labelName}}, 13 | }, 14 | } 15 | 16 | // Input represents the list of variables to be provided. 17 | type Input struct { 18 | args Args 19 | } 20 | 21 | // TotalArgs return the number of defined tags in the block. 22 | func (i *Input) TotalArgs() int { 23 | return len(i.args) 24 | } 25 | 26 | // AddArg add a new arg to the lis tof arguments. 27 | func (i *Input) AddArg(arg *Arg) { 28 | if i.args == nil { 29 | i.args = make(Args, 0) 30 | } 31 | i.args = append(i.args, arg) 32 | } 33 | 34 | func (i *Input) Execute(ctx context.OrionContext) errors.Error { 35 | if len(i.args) == 0 { 36 | return errors.IncorrectUsage("'%s' only can be declared if it contains one or more block '%s'", blockInput, blockArg) 37 | } 38 | for index := range i.args { 39 | if err := i.args[index].Execute(ctx); err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | // decodeInput decode block input. 47 | func decodeInput(b *hcl.Block) (*Input, errors.Error) { 48 | input := new(Input) 49 | bc, d := b.Body.Content(schemaInput) 50 | if err := errors.EvalDiagnostics(d); err != nil { 51 | return nil, err 52 | } 53 | if err := input.populateBlocks(bc.Blocks); err != nil { 54 | return nil, err 55 | } 56 | return input, nil 57 | } 58 | 59 | func (i *Input) populateBlocks(blocks hcl.Blocks) errors.Error { 60 | for index := range blocks { 61 | block := blocks[index] 62 | switch block.Type { 63 | case blockArg: 64 | arg, err := decodeArg(block) 65 | if err != nil { 66 | return err 67 | } 68 | i.AddArg(arg) 69 | default: 70 | return errors.ThrowUnsupportedBlock(blockInput, block.Type) 71 | } 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /dsl/return.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | const ( 10 | blockReturn = "return" 11 | argValue = "value" 12 | ) 13 | 14 | var schemaReturn = &hcl.BodySchema{ 15 | Attributes: []hcl.AttributeSchema{ 16 | {Name: argValue, Required: true}, 17 | }, 18 | } 19 | 20 | type Return struct { 21 | value hcl.Expression 22 | } 23 | 24 | func (r *Return) Value() hcl.Expression { 25 | return r.value 26 | } 27 | 28 | // DecodeBlock inherited method from interface Decoder. 29 | func decodeReturn(block *hcl.Block) (*Return, errors.Error) { 30 | bodyContent, d := block.Body.Content(schemaReturn) 31 | if err := errors.EvalDiagnostics(d); err != nil { 32 | return nil, err 33 | } 34 | if len(bodyContent.Blocks) > 0 { 35 | return nil, errors.IncorrectUsage("blocks are not permitted in '%s'", blockReturn) 36 | } 37 | ret := &Return{} 38 | for name, value := range bodyContent.Attributes { 39 | if name != argValue { 40 | return nil, errors.ThrowUnsupportedArgument(blockReturn, name) 41 | } 42 | ret.value = value.Expr 43 | } 44 | return ret, nil 45 | } 46 | -------------------------------------------------------------------------------- /dsl/testdata/args.hcl: -------------------------------------------------------------------------------- 1 | arg firstname { 2 | description = "firstname of the person" 3 | default = "John" 4 | } 5 | arg lastname {} 6 | arg age { 7 | default = 2 * 3 8 | } 9 | 10 | arg test{ 11 | unknown="" 12 | } 13 | 14 | arg elements { 15 | default = ["a","b","c"] 16 | } 17 | 18 | arg city{} 19 | 20 | arg country{ 21 | description = "${home}" 22 | } 23 | 24 | arg gender{} 25 | 26 | arg team{ 27 | default = "team-${unknown}" 28 | } 29 | -------------------------------------------------------------------------------- /dsl/testdata/body.hcl: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | } 4 | 5 | body { 6 | name = "unknown" 7 | } 8 | 9 | body { 10 | print { 11 | msg = "Hi" 12 | } 13 | } 14 | 15 | body { 16 | print { 17 | msg = "Hi" 18 | } 19 | print { 20 | msg = "Hi" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dsl/then.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/actions/assert" 6 | 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | var schemaThen = &hcl.BodySchema{ 11 | Blocks: []hcl.BlockHeaderSchema{ 12 | {Type: assert.BlockAssert}, 13 | }, 14 | Attributes: nil, 15 | } 16 | 17 | // Then represents the stage in a schemaScenario in which the assertions take place. 18 | type Then struct { 19 | *section 20 | } 21 | 22 | func decodeThen(block *hcl.Block) (*Then, errors.Error) { 23 | then := &Then{ 24 | section: newSection(blockThen, block), 25 | } 26 | bc, d := block.Body.Content(schemaThen) 27 | if err := errors.EvalDiagnostics(d); err != nil { 28 | return nil, err 29 | } 30 | if err := then.populateAttributes(bc.Attributes); err != nil { 31 | return nil, err 32 | } 33 | if err := then.populateActions(bc.Blocks); err != nil { 34 | return nil, err 35 | } 36 | return then, nil 37 | } 38 | 39 | func (then *Then) populateAttributes(attributes hcl.Attributes) errors.Error { 40 | if len(attributes) > 0 { 41 | return errors.ThrowUnsupportedArguments(blockThen) 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /dsl/vars.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | 6 | "github.com/wesovilabs/orion/internal/errors" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | const blockVars = "vars" 11 | 12 | type VarsList []Vars 13 | 14 | type Vars hcl.Attributes 15 | 16 | func (vars Vars) Append(newVars Vars) { 17 | for name, value := range newVars { 18 | vars[name] = value 19 | } 20 | } 21 | 22 | func decodeVars(block *hcl.Block) (Vars, errors.Error) { 23 | attributes, d := block.Body.JustAttributes() 24 | if err := errors.EvalDiagnostics(d); err != nil { 25 | return nil, err 26 | } 27 | return Vars(attributes), nil 28 | } 29 | 30 | func (vars Vars) To(current map[string]cty.Value) errors.Error { 31 | for name, value := range vars { 32 | v, d := value.Expr.Value(nil) 33 | if err := errors.EvalDiagnostics(d); err != nil { 34 | return err 35 | } 36 | current[name] = v 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /dsl/when.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | var schemaWhen = &hcl.BodySchema{ 10 | Blocks: handler.GetBlocksSpec(), 11 | Attributes: nil, 12 | } 13 | 14 | // When represents the stage in a schemaScenario in which we perform the actions 15 | // or actions to be tested. 16 | type When struct { 17 | *section 18 | } 19 | 20 | func decodeWhen(block *hcl.Block) (*When, errors.Error) { 21 | when := &When{ 22 | section: newSection(blockWhen, block), 23 | } 24 | 25 | bodyContent, d := block.Body.Content(schemaWhen) 26 | if err := errors.EvalDiagnostics(d); err != nil { 27 | return nil, err 28 | } 29 | if err := when.populateAttributes(bodyContent.Attributes); err != nil { 30 | return nil, err 31 | } 32 | var err errors.Error 33 | if when.actions, err = handler.DecodePlugins(bodyContent.Blocks); err != nil { 34 | return nil, err 35 | } 36 | return when, nil 37 | } 38 | 39 | func (when *When) populateAttributes(attributes hcl.Attributes) errors.Error { 40 | if len(attributes) > 0 { 41 | return errors.ThrowUnsupportedArguments(blockWhen) 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /executor/decoder.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/wesovilabs/orion/dsl" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | ) 8 | 9 | type Decoder interface { 10 | Decode(body hcl.Body) (*dsl.Feature, errors.Error) 11 | } 12 | 13 | type decoder struct{} 14 | 15 | func (dec *decoder) Decode(body hcl.Body) (*dsl.Feature, errors.Error) { 16 | return dsl.DecodeFeature(body) 17 | } 18 | -------------------------------------------------------------------------------- /executor/parser.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | 8 | "github.com/hashicorp/hcl/v2" 9 | "github.com/hashicorp/hcl/v2/hclsyntax" 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/wesovilabs/orion/dsl" 13 | "github.com/wesovilabs/orion/internal/errors" 14 | ) 15 | 16 | // Parse will parse file content into valid config. 17 | func (e *executor) Parse(path string) (*dsl.Feature, errors.Error) { 18 | content, err := ioutil.ReadFile(path) 19 | if err != nil { 20 | log.Error(err.Error()) 21 | return nil, errors.InvalidArguments("file cannot be read") 22 | } 23 | file, diagnostics := hclsyntax.ParseConfig(content, path, hcl.Pos{Line: 1, Column: 1, Byte: 0}) 24 | if diagnostics != nil && diagnostics.HasErrors() { 25 | for i := range diagnostics.Errs() { 26 | log.Errorf("failed parsing file: '%s", diagnostics.Errs()[i].Error()) 27 | } 28 | return nil, errors.Unexpected("file cannot be parsed") 29 | } 30 | out, decodeErr := e.dec.Decode(file.Body) 31 | if decodeErr != nil { 32 | return nil, decodeErr 33 | } 34 | out.SetPath(path) 35 | return out, nil 36 | } 37 | 38 | // ParseVariables will parse file content into map. 39 | func ParseVariables(path string) (map[string]cty.Value, errors.Error) { 40 | content, err := ioutil.ReadFile(path) 41 | if err != nil { 42 | log.Error(err.Error()) 43 | return nil, errors.InvalidArguments("file cannot be read") 44 | } 45 | file, d := hclsyntax.ParseConfig(content, path, hcl.Pos{Line: 1, Column: 1, Byte: 0}) 46 | if err := errors.EvalDiagnostics(d); err != nil { 47 | for i := range d.Errs() { 48 | log.Errorf("failed parsing file: '%s", d.Errs()[i].Error()) 49 | } 50 | return nil, err 51 | } 52 | 53 | attributes, d := file.Body.JustAttributes() 54 | if err := errors.EvalDiagnostics(d); err != nil { 55 | return nil, err 56 | } 57 | output := make(map[string]cty.Value) 58 | for index := range attributes { 59 | attribute := attributes[index] 60 | val, d := attribute.Expr.Value(nil) 61 | if err := errors.EvalDiagnostics(d); err != nil { 62 | return nil, err 63 | } 64 | output[attribute.Name] = val 65 | } 66 | return output, nil 67 | } 68 | -------------------------------------------------------------------------------- /executor/parser_test.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestParseVariables(t *testing.T) { 11 | vars, err := ParseVariables("testdata/variables.hcl") 12 | assert.Nil(t, err) 13 | assert.NotNil(t, vars) 14 | assert.True(t, vars["firstname"].RawEquals(cty.StringVal("John"))) 15 | assert.True(t, vars["age"].RawEquals(cty.NumberIntVal(25))) 16 | assert.True(t, vars["person"].AsValueMap()["firstname"].RawEquals(cty.StringVal("Louis"))) 17 | assert.True(t, vars["person"].AsValueMap()["age"].RawEquals(cty.NumberIntVal(20))) 18 | vars, err = ParseVariables("testdata/variables.error.hcl") 19 | assert.Nil(t, vars) 20 | assert.NotNil(t, err) 21 | assert.Equal(t, "testdata/variables.error.hcl:4,15-25: Variables not allowed; Variables may not be used here.", err.Message()) 22 | vars, err = ParseVariables("unknown-file") 23 | assert.Nil(t, vars) 24 | assert.NotNil(t, err) 25 | assert.Equal(t, "file cannot be read", err.Message()) 26 | vars, err = ParseVariables("testdata/variables.error2.hcl") 27 | assert.Nil(t, vars) 28 | assert.NotNil(t, err) 29 | assert.Equal(t, "testdata/variables.error2.hcl:1,1-8: Argument or block definition required; An argument or block definition is required here. To set an argument, use the equals sign \"=\" to introduce the argument value.", err.Message()) 30 | vars, err = ParseVariables("testdata/variables.error3.hcl") 31 | assert.Nil(t, vars) 32 | assert.NotNil(t, err) 33 | assert.Equal(t, "testdata/variables.error3.hcl:2,1-7: Unexpected \"person\" block; Blocks are not allowed here.", err.Message()) 34 | } 35 | 36 | func TestExecutor_Parse(t *testing.T) { 37 | exec := New().(*executor) 38 | feature, err := exec.Parse("testdata/feature.hcl") 39 | assert.Nil(t, err) 40 | assert.NotNil(t, feature) 41 | feature, err = exec.Parse("testdata/feature2.hcl") 42 | assert.Nil(t, err) 43 | assert.NotNil(t, feature) 44 | feature, err = exec.Parse("testdata/feature3.hcl") 45 | assert.Nil(t, err) 46 | assert.NotNil(t, feature) 47 | feature, err = exec.Parse("unknown") 48 | assert.Nil(t, feature) 49 | assert.NotNil(t, err) 50 | assert.Equal(t, "file cannot be read", err.Message()) 51 | } 52 | -------------------------------------------------------------------------------- /executor/testdata/feature.hcl: -------------------------------------------------------------------------------- 1 | # feature-math-operations.hcl 2 | description = <0 && y>0 25 | } 26 | set result { 27 | value = x * y 28 | when = x<0 || y<0 29 | } 30 | } 31 | then "the result of the operation is the expected" { 32 | assert { 33 | assertion = result==multResult 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /executor/testdata/variables.error.hcl: -------------------------------------------------------------------------------- 1 | firstname = "Jhon" 2 | age = 20 3 | person = { 4 | firstname = unknownVar 5 | age = 20 6 | } 7 | -------------------------------------------------------------------------------- /executor/testdata/variables.error2.hcl: -------------------------------------------------------------------------------- 1 | invalid 2 | -------------------------------------------------------------------------------- /executor/testdata/variables.error3.hcl: -------------------------------------------------------------------------------- 1 | attribute = 1 2 | person { 3 | block = 123 4 | } 5 | -------------------------------------------------------------------------------- /executor/testdata/variables.hcl: -------------------------------------------------------------------------------- 1 | firstname = "John" 2 | age = 25 3 | person = { 4 | firstname = "Louis" 5 | age = 20 6 | } 7 | -------------------------------------------------------------------------------- /functions/collection_operations.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | "github.com/zclconf/go-cty/cty/function" 6 | 7 | "github.com/wesovilabs/orion/helper" 8 | ) 9 | 10 | const ( 11 | opFirst = "first" 12 | opLast = "last" 13 | ) 14 | 15 | var ( 16 | first = function.New(collectionFixedPosition(opFirst, func(slice []cty.Value) cty.Value { 17 | return slice[0] 18 | })) 19 | last = function.New(collectionFixedPosition(opLast, func(slice []cty.Value) cty.Value { 20 | return slice[len(slice)-1] 21 | })) 22 | ) 23 | 24 | func collectionFixedPosition(operation string, fn func([]cty.Value) cty.Value) *function.Spec { 25 | return &function.Spec{ 26 | VarParam: nil, 27 | Params: []function.Parameter{ 28 | {Type: cty.List(cty.DynamicPseudoType)}, 29 | }, 30 | Type: func(args []cty.Value) (cty.Type, error) { 31 | var err error 32 | if len(args) > 1 { 33 | err = invalidArgs(operation, 1) 34 | } 35 | if !helper.IsSlice(args[0]) { 36 | err = invalidArgType(operation, 0, "slice") 37 | } 38 | return cty.DynamicPseudoType, err 39 | }, 40 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 41 | slice := args[0].AsValueSlice() 42 | return fn(slice), nil 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /functions/doc.go: -------------------------------------------------------------------------------- 1 | package functions 2 | -------------------------------------------------------------------------------- /functions/eval.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | "github.com/zclconf/go-cty/cty/function" 6 | 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | const opEval = "eval" 11 | 12 | var eval = function.New(&function.Spec{ 13 | VarParam: nil, 14 | Params: []function.Parameter{ 15 | { 16 | Type: cty.Bool, 17 | }, 18 | }, 19 | Type: func(args []cty.Value) (t cty.Type, err error) { 20 | t = cty.Bool 21 | if len(args) != 1 { 22 | err = errors.IncorrectUsage("`eval` function must retrieve 2 arguments", nil) 23 | return 24 | } 25 | return 26 | }, 27 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 28 | return args[0], nil 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /functions/functions.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import "github.com/zclconf/go-cty/cty/function" 4 | 5 | // Functions contain the list of provided functions. 6 | var Functions = map[string]function.Function{ 7 | 8 | opEqIgnoreCase: eqIgnoreCase, 9 | opLen: lenOp, 10 | opEval: eval, 11 | opJSON: toJSON, 12 | 13 | // string converter 14 | opToUppercase: toUppercase, 15 | opToLowercase: toLowercase, 16 | opReplaceAll: replaceAll, 17 | opReplaceOne: replaceOne, 18 | opTrimPrefix: trimPrefix, 19 | opTrimSuffix: trimSuffix, 20 | // string search 21 | opContains: containsStr, 22 | opHasPrefix: hasPrefix, 23 | opHasSuffix: hasSuffix, 24 | opCount: count, 25 | opIndexOf: indexOf, 26 | opLastIndexOf: lastIndexOf, 27 | // string others 28 | opSplit: split, 29 | 30 | // Numbers - converter 31 | opSqrt: sqrt, 32 | opCos: cos, 33 | opSin: sin, 34 | opRound: round, 35 | opPow: pow, 36 | opMod: mod, 37 | opMax: max, 38 | opMin: min, 39 | 40 | // collection operations 41 | opFirst: first, 42 | opLast: last, 43 | } 44 | -------------------------------------------------------------------------------- /functions/heper.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func checkArgumentType(operation string, args []cty.Value, types ...cty.Type) error { 11 | if len(args) != len(types) { 12 | return invalidArgs(operation, 1) 13 | } 14 | for index := range args { 15 | if args[index].Type() != types[index] { 16 | return invalidArgType(operation, index, cty.String.GoString()) 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // nolint:goerr113 23 | func invalidArgs(name string, args int) error { 24 | errMsg := fmt.Sprintf("function `%s` must retrieve %d arguments", name, args) 25 | return errors.New(errMsg) 26 | } 27 | 28 | // nolint:goerr113 29 | func invalidArgType(name string, index int, t string) error { 30 | errMsg := fmt.Sprintf("argument at position %d in function `%s` must be %s", index, name, t) 31 | return errors.New(errMsg) 32 | } 33 | -------------------------------------------------------------------------------- /functions/json.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "reflect" 7 | 8 | "github.com/zclconf/go-cty/cty" 9 | "github.com/zclconf/go-cty/cty/function" 10 | 11 | "github.com/wesovilabs/orion/helper" 12 | ) 13 | 14 | const opJSON = "json" 15 | 16 | var errUnmarshalJSON = errors.New("error un-marshaling response") 17 | 18 | var toJSON = function.New(&function.Spec{ 19 | VarParam: nil, 20 | Params: []function.Parameter{ 21 | { 22 | Type: cty.String, 23 | }, 24 | }, 25 | Type: func(args []cty.Value) (cty.Type, error) { 26 | return cty.DynamicPseudoType, nil 27 | }, 28 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 29 | content := []byte(args[0].AsString()) 30 | var output interface{} 31 | if err := json.Unmarshal(content, &output); err != nil { 32 | return cty.NilVal, errUnmarshalJSON 33 | } 34 | if reflect.TypeOf(output).Kind() == reflect.Map { 35 | return helper.ToValueMap(output.(map[string]interface{})), nil 36 | } 37 | return helper.ToValueList(output.([]interface{})), nil 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /functions/len.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | "github.com/zclconf/go-cty/cty/function" 6 | 7 | "github.com/wesovilabs/orion/helper" 8 | "github.com/wesovilabs/orion/internal/errors" 9 | ) 10 | 11 | const opLen = "len" 12 | 13 | var lenOp = function.New(&function.Spec{ 14 | VarParam: nil, 15 | Params: []function.Parameter{ 16 | { 17 | Type: cty.DynamicPseudoType, 18 | }, 19 | }, 20 | Type: func(args []cty.Value) (t cty.Type, err error) { 21 | t = cty.Number 22 | if len(args) != 1 { 23 | err = errors.IncorrectUsage("`len` function must retrieve 1 arguments") 24 | return 25 | } 26 | if !helper.IsSlice(args[0]) { 27 | err = errors.IncorrectUsage("argument must be a slice") 28 | } 29 | return 30 | }, 31 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 32 | listLen := len(args[0].AsValueSlice()) 33 | return cty.NumberFloatVal(float64(listLen)), nil 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /functions/string.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | "github.com/zclconf/go-cty/cty/function" 8 | ) 9 | 10 | const opSplit = "split" 11 | 12 | var split = function.New(&function.Spec{ 13 | VarParam: nil, 14 | Params: []function.Parameter{ 15 | {Type: cty.String}, 16 | {Type: cty.String}, 17 | }, 18 | Type: func(args []cty.Value) (cty.Type, error) { 19 | return cty.List(cty.String), checkArgumentType(opSplit, args, cty.String, cty.String) 20 | }, 21 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 22 | s := args[0].AsString() 23 | sep := args[1].AsString() 24 | res := strings.Split(s, sep) 25 | items := make([]cty.Value, len(res)) 26 | for index := range res { 27 | items[index] = cty.StringVal(res[index]) 28 | } 29 | return cty.ListVal(items), nil 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /functions/string_comparison.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | "github.com/zclconf/go-cty/cty/function" 8 | ) 9 | 10 | const opEqIgnoreCase = "eqIgnoreCase" 11 | 12 | var eqIgnoreCase = function.New(&function.Spec{ 13 | VarParam: nil, 14 | Params: []function.Parameter{ 15 | { 16 | Type: cty.String, 17 | }, 18 | { 19 | Type: cty.String, 20 | }, 21 | }, 22 | Type: func(args []cty.Value) (cty.Type, error) { 23 | return cty.Bool, checkArgumentType(opEqIgnoreCase, args, cty.String, cty.String) 24 | }, 25 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 26 | if strings.EqualFold(args[0].AsString(), args[1].AsString()) { 27 | return cty.True, nil 28 | } 29 | return cty.False, nil 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wesovilabs/orion 2 | 3 | require ( 4 | github.com/daviddengcn/go-colortext v1.0.0 5 | github.com/go-delve/delve v1.6.0 6 | github.com/golangci/golangci-lint v1.35.2 7 | github.com/hashicorp/hcl/v2 v2.8.2 8 | github.com/mattn/go-runewidth v0.0.10 // indirect 9 | github.com/mitchellh/go-homedir v1.1.0 10 | github.com/peterh/liner v1.2.1 // indirect 11 | github.com/rivo/uniseg v0.2.0 // indirect 12 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 13 | github.com/sirupsen/logrus v1.7.0 14 | github.com/spf13/cobra v1.1.3 15 | github.com/spf13/viper v1.7.1 16 | github.com/stretchr/testify v1.6.1 17 | github.com/zclconf/go-cty v1.7.0 18 | go.mongodb.org/mongo-driver v1.4.6 19 | go.starlark.net v0.0.0-20210208172022-0a10e4fe7402 // indirect 20 | golang.org/x/arch v0.0.0-20210127225635-455c95562d18 // indirect 21 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect 22 | golang.org/x/tools v0.0.0-20210105210202-9ed45478a130 23 | mvdan.cc/gofumpt v0.1.0 24 | 25 | ) 26 | 27 | go 1.15 28 | -------------------------------------------------------------------------------- /helper/bool.go: -------------------------------------------------------------------------------- 1 | // Package converter content relative to the package 2 | package helper 3 | 4 | import ( 5 | "github.com/hashicorp/hcl/v2" 6 | "github.com/wesovilabs/orion/internal/errors" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | // GetExpressionValueAsBool return the expression as bool. 11 | func GetExpressionValueAsBool(ctx *hcl.EvalContext, expr hcl.Expression, def bool) (bool, errors.Error) { 12 | if expr == nil || expr.Range().Empty() { 13 | return def, nil 14 | } 15 | value, d := expr.Value(ctx) 16 | if err := errors.EvalDiagnostics(d); err != nil { 17 | return def, err. 18 | AddMeta(errors.MetaLocation, expr.Range().String()) 19 | } 20 | if value.Type() == cty.Bool { 21 | return value.True(), nil 22 | } 23 | 24 | return def, nil 25 | } 26 | -------------------------------------------------------------------------------- /helper/bool_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/wesovilabs/orion/testutil" 8 | ) 9 | 10 | func TestGetExpressionValueAsBool(t *testing.T) { 11 | text := ` 12 | people = { 13 | fullName = "Jane Doe" 14 | } 15 | invalid = unknown 16 | varFalse = false 17 | varFalse2 = 1==2 18 | varTrue = true 19 | varTrue2 = 1==1 20 | ` 21 | attrs, err := testutil.GetAttributesFromText(text) 22 | assert.Nil(t, err) 23 | _, err = GetExpressionValueAsBool(testEvalCtx, attrs["invalid"].Expr, true) 24 | assert.NotNil(t, err) 25 | v, err := GetExpressionValueAsBool(testEvalCtx, attrs["people"].Expr, true) 26 | assert.Nil(t, err) 27 | assert.True(t, v) 28 | v, err = GetExpressionValueAsBool(testEvalCtx, attrs["varFalse"].Expr, true) 29 | assert.Nil(t, err) 30 | assert.False(t, v) 31 | v, err = GetExpressionValueAsBool(testEvalCtx, attrs["varFalse2"].Expr, true) 32 | assert.Nil(t, err) 33 | assert.False(t, v) 34 | v, err = GetExpressionValueAsBool(testEvalCtx, attrs["varTrue"].Expr, true) 35 | assert.Nil(t, err) 36 | assert.True(t, v) 37 | v, err = GetExpressionValueAsBool(testEvalCtx, attrs["varTrue2"].Expr, true) 38 | assert.Nil(t, err) 39 | assert.True(t, v) 40 | } 41 | -------------------------------------------------------------------------------- /helper/duration.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | 8 | "github.com/wesovilabs/orion/internal/errors" 9 | ) 10 | 11 | // GetExpressionValueAsDuration evaluate the expression and return a duration value. 12 | func GetExpressionValueAsDuration(ctx *hcl.EvalContext, expr hcl.Expression, def *time.Duration) (*time.Duration, 13 | errors.Error) { 14 | valueStr, err := GetExpressionValueAsString(ctx, expr, "") 15 | if err != nil { 16 | return nil, err 17 | } 18 | if valueStr == "" { 19 | return def, nil 20 | } 21 | value, parseErr := time.ParseDuration(valueStr) 22 | if parseErr != nil { 23 | return nil, errors.Unexpected(parseErr.Error()).AddMeta(errors.MetaLocation, expr.Range().String()) 24 | } 25 | 26 | return &value, nil 27 | } 28 | -------------------------------------------------------------------------------- /helper/int.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/zclconf/go-cty/cty" 6 | 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | // GetExpressionValueAsInt evaluate the expression and return a int value. 11 | func GetExpressionValueAsInt(ctx *hcl.EvalContext, expr hcl.Expression, def int) (int, errors.Error) { 12 | if expr == nil || expr.Range().Empty() { 13 | return def, nil 14 | } 15 | value, diagnostics := expr.Value(ctx) 16 | if diagnostics != nil && diagnostics.HasErrors() { 17 | return 0, errors.IncorrectUsage(diagnostics.Error()).AddMeta(errors.MetaLocation, expr.Range().String()) 18 | } 19 | if value.Type() == cty.Number { 20 | valFloat := value.AsBigFloat() 21 | out, _ := valFloat.Int64() 22 | 23 | return int(out), nil 24 | } 25 | 26 | return def, nil 27 | } 28 | -------------------------------------------------------------------------------- /helper/int_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/wesovilabs/orion/testutil" 8 | ) 9 | 10 | func TestGetExpressionValueAsInt(t *testing.T) { 11 | text := ` 12 | people = { 13 | fullName = "Jane Doe" 14 | } 15 | invalid = unknown 16 | var1 = 1 17 | var2 = 1+1 18 | var3 = 3.00 19 | ` 20 | attrs, err := testutil.GetAttributesFromText(text) 21 | assert.Nil(t, err) 22 | _, err = GetExpressionValueAsInt(testEvalCtx, attrs["invalid"].Expr, 100) 23 | assert.NotNil(t, err) 24 | v, err := GetExpressionValueAsInt(testEvalCtx, attrs["people"].Expr, 100) 25 | assert.Nil(t, err) 26 | assert.Equal(t, v, 100) 27 | v, err = GetExpressionValueAsInt(testEvalCtx, attrs["var1"].Expr, 100) 28 | assert.Nil(t, err) 29 | assert.Equal(t, v, 1) 30 | v, err = GetExpressionValueAsInt(testEvalCtx, attrs["var2"].Expr, 100) 31 | assert.Nil(t, err) 32 | assert.Equal(t, v, 2) 33 | v, err = GetExpressionValueAsInt(testEvalCtx, attrs["var3"].Expr, 100) 34 | assert.Nil(t, err) 35 | assert.Equal(t, v, 3) 36 | } 37 | -------------------------------------------------------------------------------- /helper/interface.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2" 5 | "github.com/zclconf/go-cty/cty" 6 | 7 | "github.com/wesovilabs/orion/internal/errors" 8 | ) 9 | 10 | // GetExpressionValueAsInterface evaluate an hcl expresion and converts into an interface. 11 | func GetExpressionValueAsInterface(ctx *hcl.EvalContext, expr hcl.Expression, def interface{}) (interface{}, 12 | errors.Error) { 13 | if expr == nil || expr.Range().Empty() { 14 | return def, nil 15 | } 16 | value, diagnostics := expr.Value(ctx) 17 | if diagnostics != nil && diagnostics.HasErrors() { 18 | return nil, errors.IncorrectUsage(diagnostics.Error()).AddMeta(errors.MetaLocation, expr.Range().String()) 19 | } 20 | 21 | return ToInterface(value) 22 | } 23 | 24 | // ToInterface converts a given cty.Value into an interface. 25 | func ToInterface(v cty.Value) (interface{}, errors.Error) { 26 | switch { 27 | case IsMap(v): 28 | result := make(map[string]interface{}) 29 | valueMap := v.AsValueMap() 30 | for k, v := range valueMap { 31 | item, err := ToInterface(v) 32 | if err != nil { 33 | return nil, err 34 | } 35 | result[k] = item 36 | } 37 | 38 | return result, nil 39 | case IsSlice(v): 40 | slice := v.AsValueSlice() 41 | result := make([]interface{}, len(slice)) 42 | for index := range slice { 43 | valueItem := slice[index] 44 | item, err := ToString(valueItem) 45 | if err != nil { 46 | return "", err 47 | } 48 | result[index] = item 49 | } 50 | 51 | return result, nil 52 | case v.Type() == cty.String: 53 | return v.AsString(), nil 54 | case v.Type() == cty.Number: 55 | valueFloat := v.AsBigFloat() 56 | if valueFloat.IsInf() { 57 | vFloat, _ := valueFloat.Int64() 58 | return vFloat, nil 59 | } 60 | result, _ := valueFloat.Float64() 61 | 62 | return result, nil 63 | case v.Type() == cty.Bool: 64 | return v.True(), nil 65 | } 66 | 67 | return "", errors.Unexpected("v type %s is not supported", v.Type().GoString()) 68 | } 69 | -------------------------------------------------------------------------------- /helper/interface_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/wesovilabs/orion/testutil" 8 | ) 9 | 10 | func TestGetExpressionValueAsInterface(t *testing.T) { 11 | text := ` 12 | varMap = { 13 | fullName = "Jane Doe" 14 | } 15 | varMap2 = { 16 | fullName = unknown 17 | } 18 | invalid = unknown 19 | varNumber = 1+1 20 | varNumber2 = 3.00 21 | varBool = 1==1 22 | varSlice = [1,2,3,4,5] 23 | ` 24 | attrs, err := testutil.GetAttributesFromText(text) 25 | assert.Nil(t, err) 26 | 27 | var value interface{} 28 | value, err = GetExpressionValueAsInterface(testEvalCtx, attrs["varMap"].Expr, nil) 29 | assert.Nil(t, err) 30 | assert.NotNil(t, value) 31 | valMap, ok := value.(map[string]interface{}) 32 | assert.True(t, ok) 33 | assert.Equal(t, "Jane Doe", valMap["fullName"]) 34 | 35 | var valSlice []interface{} 36 | value, err = GetExpressionValueAsInterface(testEvalCtx, attrs["varSlice"].Expr, nil) 37 | assert.Nil(t, err) 38 | assert.NotNil(t, value) 39 | valSlice, ok = value.([]interface{}) 40 | assert.True(t, ok) 41 | assert.Len(t, valSlice, 5) 42 | 43 | var valFloat float64 44 | value, err = GetExpressionValueAsInterface(testEvalCtx, attrs["varNumber"].Expr, nil) 45 | assert.Nil(t, err) 46 | assert.NotNil(t, value) 47 | valFloat, ok = value.(float64) 48 | assert.True(t, ok) 49 | assert.EqualValues(t, 2, valFloat) 50 | value, err = GetExpressionValueAsInterface(testEvalCtx, attrs["varNumber2"].Expr, nil) 51 | assert.Nil(t, err) 52 | assert.NotNil(t, value) 53 | valFloat, ok = value.(float64) 54 | assert.True(t, ok) 55 | assert.EqualValues(t, 3.00, valFloat) 56 | 57 | var valBool bool 58 | value, err = GetExpressionValueAsInterface(testEvalCtx, attrs["varBool"].Expr, nil) 59 | assert.Nil(t, err) 60 | assert.NotNil(t, value) 61 | valBool, ok = value.(bool) 62 | assert.True(t, ok) 63 | assert.EqualValues(t, true, valBool) 64 | 65 | value, err = GetExpressionValueAsInterface(testEvalCtx, attrs["invalid"].Expr, nil) 66 | assert.NotNil(t, err) 67 | assert.Nil(t, value) 68 | 69 | value, err = GetExpressionValueAsInterface(testEvalCtx, nil, nil) 70 | assert.Nil(t, err) 71 | assert.Nil(t, value) 72 | 73 | value, err = GetExpressionValueAsInterface(testEvalCtx, attrs["varMap2"].Expr, nil) 74 | assert.NotNil(t, err) 75 | assert.Nil(t, value) 76 | } 77 | -------------------------------------------------------------------------------- /helper/string.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/zclconf/go-cty/cty" 8 | 9 | "github.com/wesovilabs/orion/internal/errors" 10 | ) 11 | 12 | // GetExpressionValueAsString return the value fo the expression as a string. 13 | func GetExpressionValueAsString(ctx *hcl.EvalContext, expr hcl.Expression, def string) (string, errors.Error) { 14 | if expr == nil || expr.Range().Empty() { 15 | return def, nil 16 | } 17 | value, d := expr.Value(ctx) 18 | if err := errors.EvalDiagnostics(d); err != nil { 19 | return "", err 20 | } 21 | return ToString(value) 22 | } 23 | 24 | // ToString converts a given cty.Value into a string. 25 | func ToString(v cty.Value) (string, errors.Error) { 26 | if v.IsNull() { 27 | return "", nil 28 | } 29 | switch { 30 | case IsMap(v): 31 | result := "" 32 | valueMap := v.AsValueMap() 33 | for k, v := range valueMap { 34 | item, err := ToString(v) 35 | if err != nil { 36 | return "", err 37 | } 38 | result += k + ":[" + item + "]" 39 | } 40 | return result, nil 41 | case IsSlice(v): 42 | result := "" 43 | slice := v.AsValueSlice() 44 | for index := range slice { 45 | valueItem := slice[index] 46 | if index != 0 { 47 | result += ", " 48 | } 49 | item, err := ToString(valueItem) 50 | if err != nil { 51 | return "", err 52 | } 53 | result += item 54 | } 55 | return result, nil 56 | case v.Type() == cty.String: 57 | return v.AsString(), nil 58 | case v.Type() == cty.Number: 59 | return v.AsBigFloat().String(), nil 60 | case v.Type() == cty.Bool: 61 | return fmt.Sprintf("%v", v.True()), nil 62 | } 63 | return "", errors.Unexpected("value type %s is not supported", v.Type().GoString()) 64 | } 65 | 66 | // AttributeToStringWithoutContext convert a hc Attribute into a string value. 67 | func AttributeToStringWithoutContext(attribute *hcl.Attribute) (string, errors.Error) { 68 | if len(attribute.Expr.Variables()) > 0 { 69 | return "", errors.IncorrectUsage("variables are not permitted in attribute %s", attribute.Name) 70 | } 71 | val, err := EvalAttribute(nil, attribute) 72 | if err != nil { 73 | return "", err 74 | } 75 | return ToStrictString(val) 76 | } 77 | -------------------------------------------------------------------------------- /helper/testdata/attributes.hcl: -------------------------------------------------------------------------------- 1 | attribute_text="John" 2 | attribute_text2=firstname 3 | attribute_text3="${firstname} ${lastname}" 4 | 5 | attribute_number = 2 6 | attribute_number2 = 2 * 4 7 | attribute_number3 = myNumber 8 | 9 | attribute_map = { 10 | firstname = firstname 11 | age = myNumber 12 | } 13 | 14 | 15 | attribute_array = [firstname, "a", 5, myNumber] 16 | 17 | error = unknown 18 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "path/filepath" 5 | "sync" 6 | 7 | "github.com/mitchellh/go-homedir" 8 | "github.com/wesovilabs/orion/internal/logger" 9 | ) 10 | 11 | const ( 12 | baseDir = ".orion" 13 | ) 14 | 15 | var ( 16 | rootDir, _ = homedir.Dir() 17 | DefaultPath = filepath.Join(rootDir, baseDir, "orion.yml") 18 | // nolint:godot 19 | // v = viper.New() 20 | cfg *Config 21 | once sync.Once 22 | ) 23 | 24 | type Config struct { 25 | Logger *logger.Config `yaml:"logger"` 26 | } 27 | 28 | /** 29 | func Load(path string) *Config { 30 | once.Do(func() { 31 | v.SetConfigFile(path) 32 | if err := read(); err == nil { 33 | log.Debugf("configuration file in '%s' was read successfully!", path) 34 | } else { 35 | if path != DefaultPath { 36 | log.Errorf("it failed reading config file: %s", err) 37 | os.Exit(1) 38 | } 39 | cfg.Logger = logger.Default() 40 | } 41 | 42 | }) 43 | return cfg 44 | }**/ 45 | 46 | func Get() *Config { 47 | once.Do(func() { 48 | cfg = new(Config) 49 | cfg.Logger = logger.Default() 50 | }) 51 | return cfg 52 | } 53 | 54 | /** 55 | func read() errors.Error { 56 | cfg = &Config{} 57 | if err := v.ReadInConfig(); err != nil { 58 | return errors.InvalidArguments(err.Error()) 59 | } 60 | if err := v.Unmarshal(cfg); err != nil { 61 | return errors.InvalidArguments(err.Error()) 62 | } 63 | return nil 64 | } 65 | **/ 66 | -------------------------------------------------------------------------------- /internal/errors/code.go: -------------------------------------------------------------------------------- 1 | // Package errors content relative to the package 2 | package errors 3 | 4 | // code type used to define the error codes in koazee. 5 | type code string 6 | 7 | const ( 8 | unexpectedErrCode = "unexpectedErrCode" 9 | invalidArgumentsErrCode = "invalidArgumentsErrCode" 10 | commandNotFoundErrCode = "commandNotFoundErrCode" 11 | incorrectUsageErrCode = "incorrectUsageErrCode" 12 | limitsExceedErrCode = "limitsExceedErrCode" 13 | ) 14 | 15 | // Unexpected return an unexpectedErrCode error. 16 | func Unexpected(msg string, args ...interface{}) Error { 17 | return createError(unexpectedErrCode, 1). 18 | withMessage(msg, args) 19 | } 20 | 21 | // InvalidArguments return an invalid arguments error. 22 | func InvalidArguments(msg string, args ...interface{}) Error { 23 | return createError(invalidArgumentsErrCode, 128). 24 | withMessage(msg, args) 25 | } 26 | 27 | // CommandNotFound return a command not found error. 28 | func CommandNotFound(msg string, args ...interface{}) Error { 29 | return createError(commandNotFoundErrCode, 127). 30 | withMessage(msg, args) 31 | } 32 | 33 | // IncorrectUsage return an incorrect usage error. 34 | func IncorrectUsage(msg string, args ...interface{}) Error { 35 | return createError(incorrectUsageErrCode, 2). 36 | withMessage(msg, args) 37 | } 38 | 39 | // LimitsExceed return an limit exceed error. 40 | func LimitsExceed(msg string, args ...interface{}) Error { 41 | return createError(limitsExceedErrCode, 2). 42 | withMessage(msg, args) 43 | } 44 | 45 | // String print string of error code. 46 | func (c code) String() string { 47 | return string(c) 48 | } 49 | -------------------------------------------------------------------------------- /internal/errors/custom.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // ThrowMissingRequiredLabel throw a missing required label error. 4 | func ThrowMissingRequiredLabel(block string) Error { 5 | return IncorrectUsage("missing required label in block %s", block) 6 | } 7 | 8 | // ThrowUnsupportedArgument throw an unsupported argument error. 9 | func ThrowUnsupportedArgument(block, argument string) Error { 10 | return IncorrectUsage("unsupported argument %s in block %s", argument, block) 11 | } 12 | 13 | // ThrowUnsupportedBlock throw an unsupported block error. 14 | func ThrowUnsupportedBlock(parent, child string) Error { 15 | return IncorrectUsage("unsupported block %s in block %s", child, parent) 16 | } 17 | 18 | // ThrowBlocksAreNotPermitted throw an unsupported block error. 19 | func ThrowBlocksAreNotPermitted(action string) Error { 20 | return IncorrectUsage("blocks are not permitted in action %s ", action) 21 | } 22 | 23 | func ThrowUnsupportedBlocks(block string) Error { 24 | return IncorrectUsage("blocks are not supported by %s", block) 25 | } 26 | 27 | func ThrowUnsupportedArguments(block string) Error { 28 | return IncorrectUsage("arguments are not supported by %s", block) 29 | } 30 | 31 | func ThrowsExceededNumberOfBlocks(block string, max int) Error { 32 | return IncorrectUsage("only %d block '%s' is permitted here", max, block) 33 | } 34 | -------------------------------------------------------------------------------- /internal/errors/diagnostics.go: -------------------------------------------------------------------------------- 1 | // Package diagnostics content relative to the package 2 | package errors 3 | 4 | import ( 5 | "github.com/hashicorp/hcl/v2" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // EvalDiagnostics return error or nil depending on the diagnostics. 10 | func EvalDiagnostics(d hcl.Diagnostics) Error { 11 | if d != nil && d.HasErrors() { 12 | for index := range d.Errs() { 13 | log.Warn(d.Errs()[index].Error()) 14 | } 15 | return IncorrectUsage(d.Error()) 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/errors/doc.go: -------------------------------------------------------------------------------- 1 | package errors 2 | -------------------------------------------------------------------------------- /internal/info.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | var ( 4 | Commit string 5 | Version string 6 | BuildDate string 7 | ) 8 | -------------------------------------------------------------------------------- /internal/logger/config.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | const timestampFormat = "15:04:05.000000" 8 | 9 | var defaultConfig = &Config{ 10 | Level: logrus.TraceLevel.String(), 11 | } 12 | 13 | // Config contains configuration for logrus. 14 | type Config struct { 15 | Level string `yaml:"level"` 16 | } 17 | 18 | func (c *Config) level() logrus.Level { 19 | lvl, err := logrus.ParseLevel(c.Level) 20 | if err == nil { 21 | return lvl 22 | } 23 | logrus.Warningf("unexpected error parsing log level: '%s'", err) 24 | return logrus.InfoLevel 25 | } 26 | 27 | func (c *Config) SetLevel(level string) { 28 | lvl, err := logrus.ParseLevel(level) 29 | if err != nil { 30 | c.Level = logrus.InfoLevel.String() 31 | } 32 | c.Level = lvl.String() 33 | } 34 | 35 | func (c *Config) formatter() logrus.Formatter { 36 | return &Formatter{ 37 | TimestampFormat: timestampFormat, 38 | ColorDisabled: false, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/logger/formatter.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | 6 | ct "github.com/daviddengcn/go-colortext" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | var levelColors = []ct.Color{ 11 | ct.Red, 12 | ct.Red, 13 | ct.Red, 14 | ct.Magenta, 15 | ct.Cyan, 16 | ct.Green, 17 | ct.Yellow, 18 | } 19 | 20 | type Formatter struct { 21 | TimestampFormat string 22 | ColorDisabled bool 23 | } 24 | 25 | func (f *Formatter) Format(entry *log.Entry) ([]byte, error) { 26 | timestamp := entry.Time.Format(f.TimestampFormat) 27 | f.setColor(entry.Level) 28 | return []byte(fmt.Sprintf("%s %s\n", timestamp, entry.Message)), nil 29 | } 30 | 31 | func (f *Formatter) setColor(lvlIndex log.Level) { 32 | if !f.ColorDisabled { 33 | ct.ChangeColor(levelColors[lvlIndex], false, ct.None, false) 34 | return 35 | } 36 | ct.ResetColor() 37 | } 38 | -------------------------------------------------------------------------------- /internal/logger/setup.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "io" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // SetUp configure logger. 10 | func SetUp(cfg *Config, stdOut io.Writer) { 11 | log.SetFormatter(cfg.formatter()) 12 | log.SetLevel(cfg.level()) 13 | log.SetOutput(stdOut) 14 | } 15 | 16 | // Default configure logger with default values. 17 | func Default() *Config { 18 | return defaultConfig 19 | } 20 | -------------------------------------------------------------------------------- /internal/oriontest/test_utils.go: -------------------------------------------------------------------------------- 1 | package oriontest 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/hashicorp/hcl/v2/hclsyntax" 8 | ) 9 | 10 | func ParseHCL(path string, schema *hcl.BodySchema) hcl.Blocks { 11 | content, err := ioutil.ReadFile(path) 12 | if err != nil { 13 | panic(err) 14 | } 15 | file, diagnostics := hclsyntax.ParseConfig(content, path, hcl.Pos{Line: 1, Column: 1, Byte: 0}) 16 | if diagnostics != nil && diagnostics.HasErrors() { 17 | panic(diagnostics.Error()) 18 | } 19 | bodyContent, diagnostics := file.Body.Content(schema) 20 | if diagnostics != nil && diagnostics.HasErrors() { 21 | panic(diagnostics.Error()) 22 | } 23 | return bodyContent.Blocks 24 | } 25 | -------------------------------------------------------------------------------- /scripts/deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go mod tidy; 4 | go mod vendor; 5 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | for pkg in $(GOFLAGS=-mod=vendor go list -f '{{.Dir}}' ./... | grep -v /vendor/ ); do \ 5 | echo $pkg 6 | GOFLAGS=-mod=vendor go run -mod=vendor golang.org/x/tools/cmd/goimports -l -w -e $pkg/*.go; \ 7 | GOFLAGS=-mod=vendor go run mvdan.cc/gofumpt -l -w $pkg/*.go; 8 | done 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | GOFLAGS=-mod=vendor go run -mod=vendor \ 4 | github.com/golangci/golangci-lint/cmd/golangci-lint run --verbose 5 | -------------------------------------------------------------------------------- /scripts/publish-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "publishing docker image..." 3 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 4 | docker push wesovilabs/orion 5 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Set module 3 | MODULE="github.com/wesovilabs/orion" 4 | 5 | RUN_MODE=${RUN_MODE:-code} 6 | 7 | case ${RUN_MODE} in 8 | code) 9 | GOFLAGS=-mod=vendor go run ${MODULE}/cmd/orion; 10 | ;; 11 | debug) 12 | echo "running code in debug mode" 13 | GOFLAGS=-mod=vendor go run github.com/go-delve/delve/cmd/dlv debug \ 14 | --headless --api-version=2 --log --listen=127.0.0.1:2345 ${MODULE}/cmd/orion 15 | ;; 16 | esac 17 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Get the parent directory of where this script is. 4 | SOURCE="${BASH_SOURCE[0]}" 5 | while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done 6 | DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" 7 | # Change into that directory 8 | cd "$DIR" 9 | 10 | 11 | echo "Downloading dependencies..." 12 | go mod tidy; 13 | go mod vendor; 14 | 15 | echo "Create git hooks" 16 | chmod +x .githooks/* 17 | cp .githooks/* .git/hooks/ 18 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TEST_MODE=${TEST_MODE:-unit} 4 | 5 | 6 | # Download dependencies 7 | go mod download 8 | 9 | case ${TEST_MODE} in 10 | unit) 11 | GOFLAGS=-mod=vendor go test -p=1 $(GOFLAGS=-mod=vendor go list -f '{{ if or .TestGoFiles .XTestGoFiles }}{{.ImportPath}}{{ end }}' ./... | grep -v test ) -v -timeout 10s; 12 | ;; 13 | coverage) 14 | 15 | GOFLAGS=-mod=vendor go test -p=1 $(GOFLAGS=-mod=vendor go list -f '{{ if or .TestGoFiles .XTestGoFiles }}{{.ImportPath}}{{ end }}' ./... | grep -v test ) -v -timeout 10s -race -coverprofile=coverage.txt -covermode=atomic; 16 | ;; 17 | e2e) 18 | echo "Not implemented yet" 19 | ;; 20 | esac 21 | -------------------------------------------------------------------------------- /testutil/hcl.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/hashicorp/hcl/v2/hclparse" 9 | "github.com/wesovilabs/orion/internal/errors" 10 | ) 11 | 12 | func GetBodyContent(path string, blockName string, labels []string) (*hcl.BodyContent, errors.Error) { 13 | parentSchema := &hcl.BodySchema{ 14 | Blocks: []hcl.BlockHeaderSchema{{ 15 | Type: blockName, 16 | LabelNames: labels, 17 | }}, 18 | } 19 | parser := hclparse.NewParser() 20 | f, d := parser.ParseHCLFile(path) 21 | if err := errors.EvalDiagnostics(d); err != nil { 22 | return nil, err 23 | } 24 | content, d := f.Body.Content(parentSchema) 25 | if err := errors.EvalDiagnostics(d); err != nil { 26 | return nil, err 27 | } 28 | return content, nil 29 | } 30 | 31 | func GetAttributes(path string) (hcl.Attributes, errors.Error) { 32 | parser := hclparse.NewParser() 33 | f, d := parser.ParseHCLFile(path) 34 | if err := errors.EvalDiagnostics(d); err != nil { 35 | return nil, err 36 | } 37 | attributes, d := f.Body.JustAttributes() 38 | if err := errors.EvalDiagnostics(d); err != nil { 39 | return nil, err 40 | } 41 | return attributes, nil 42 | } 43 | 44 | func GetAttributesFromText(content string) (hcl.Attributes, error) { 45 | tmpFile, err := createTemporaryFileWith(content) 46 | if err != nil { 47 | return nil, err 48 | } 49 | defer os.Remove(tmpFile.Name()) 50 | return GetAttributes(tmpFile.Name()) 51 | } 52 | 53 | func MapStringAttributeToStringExpression(input map[string]*hcl.Attribute) map[string]hcl.Expression { 54 | out := make(map[string]hcl.Expression) 55 | for n, attr := range input { 56 | out[n] = attr.Expr 57 | } 58 | return out 59 | } 60 | 61 | func createTemporaryFileWith(content string) (*os.File, error) { 62 | tmpFile, err := ioutil.TempFile(os.TempDir(), "pre-") 63 | if err != nil { 64 | return nil, err 65 | } 66 | text := []byte(content) 67 | if _, err = tmpFile.Write(text); err != nil { 68 | return nil, err 69 | } 70 | 71 | // Close the file 72 | if err := tmpFile.Close(); err != nil { 73 | return nil, err 74 | } 75 | return tmpFile, nil 76 | } 77 | -------------------------------------------------------------------------------- /testutil/parser.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/hashicorp/hcl/v2/hclparse" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/wesovilabs/orion/internal/errors" 11 | ) 12 | 13 | func GetStringAsBodyContent(text string, blockName string, labels []string) *hcl.BodyContent { 14 | parentSchema := &hcl.BodySchema{ 15 | Blocks: []hcl.BlockHeaderSchema{{ 16 | Type: blockName, 17 | LabelNames: labels, 18 | }}, 19 | } 20 | file, err := ioutil.TempFile("", "orion.*.hcl") 21 | if err != nil { 22 | log.Errorf("error creating temporary file: %s", err) 23 | return nil 24 | } 25 | defer os.Remove(file.Name()) 26 | 27 | parser := hclparse.NewParser() 28 | f, d := parser.ParseHCL([]byte(text), file.Name()) 29 | if err := errors.EvalDiagnostics(d); err != nil { 30 | log.Error(err) 31 | return nil 32 | } 33 | content, d := f.Body.Content(parentSchema) 34 | if err := errors.EvalDiagnostics(d); err != nil { 35 | log.Error(err) 36 | return nil 37 | } 38 | return content 39 | } 40 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/go-delve/delve/cmd/dlv" 7 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 8 | _ "golang.org/x/tools/cmd/goimports" 9 | _ "mvdan.cc/gofumpt" 10 | ) 11 | --------------------------------------------------------------------------------