├── .gitignore ├── starlark ├── runtime │ ├── testdata │ │ ├── includes │ │ │ └── foo.star │ │ └── load.star │ ├── runtime_test.go │ └── runtime.go ├── types │ ├── testdata │ │ ├── evaluate │ │ │ └── test.star │ │ ├── examples │ │ │ ├── evaluable.star │ │ │ ├── evaluate.star │ │ │ ├── provider_resource.star │ │ │ ├── ref.star │ │ │ ├── attribute.star │ │ │ ├── provider_collection.star │ │ │ ├── tf_provider.star │ │ │ ├── resource_collection_search.star │ │ │ ├── tf_overview.star │ │ │ ├── provider.star │ │ │ ├── backend_local.star │ │ │ ├── resource_collection_call.star │ │ │ └── resource.star │ │ ├── nested.star │ │ ├── evaluate.star │ │ ├── backend.star │ │ ├── provisioner.star │ │ ├── validate.star │ │ ├── state.star │ │ ├── terraform.star │ │ ├── hcl.star │ │ ├── hcl_integration.star │ │ ├── provider.star │ │ ├── attribute.star │ │ └── resource.star │ ├── fixtures │ │ ├── modules │ │ │ ├── .terraform │ │ │ │ ├── plugins │ │ │ │ │ └── linux_amd64 │ │ │ │ │ │ └── lock.json │ │ │ │ ├── modules │ │ │ │ │ └── modules.json │ │ │ │ └── terraform.tfstate │ │ │ ├── moduleB │ │ │ │ └── qux.tf │ │ │ ├── main.tf │ │ │ ├── moduleA │ │ │ │ └── bar.tf │ │ │ └── terraform.tfstate │ │ └── aws.tf │ ├── evaluate_test.go │ ├── validate_test.go │ ├── attribute_test.go │ ├── terraform_test.go │ ├── backend_test.go │ ├── doc.go │ ├── examples_test.go │ ├── evaluate.go │ ├── terraform.go │ ├── type_test.go │ ├── provider_test.go │ ├── value_test.go │ ├── provisioner.go │ ├── type.go │ ├── validate.go │ ├── value.go │ └── attribute.go ├── module │ ├── filepath │ │ ├── testdata │ │ │ └── test.star │ │ └── filepath_test.go │ ├── docker │ │ ├── image_test.go │ │ ├── testdata │ │ │ └── test.star │ │ └── image.go │ ├── os │ │ ├── os_test.go │ │ └── testdata │ │ │ └── test.star │ └── url │ │ ├── url_test.go │ │ ├── testdata │ │ └── test.star │ │ └── url.go └── test │ ├── assert.star.go │ └── assert.go ├── _documentation ├── example │ ├── .gitignore │ └── _index.md ├── reference │ ├── .gitignore │ ├── _index.md │ └── reference.md.tmpl ├── _home.md ├── _index.md ├── config.toml ├── starlark │ ├── dialect-differences.md │ ├── module-execution.md │ ├── _index.md │ ├── name-binding-and-variables.md │ ├── lexical-elements.md │ └── value-concepts.md ├── install.md ├── github-action.md └── getting-started.md ├── Dockerfile ├── cmd ├── common.go ├── repl.go ├── version.go └── run.go ├── .github └── workflows │ ├── release.yml │ ├── docker.yml │ ├── test.yml │ └── gh-pages.yml ├── _scripts ├── query-go-mod.go ├── example-to-md.go └── goldflags.go ├── main.go ├── README.md ├── go.mod ├── _examples ├── runtime.star └── functions.star ├── terraform ├── plugins_test.go └── plugins.go └── Makefile /.gitignore: -------------------------------------------------------------------------------- 1 | .providers 2 | _site -------------------------------------------------------------------------------- /starlark/runtime/testdata/includes/foo.star: -------------------------------------------------------------------------------- 1 | foo = "foo" -------------------------------------------------------------------------------- /starlark/types/testdata/evaluate/test.star: -------------------------------------------------------------------------------- 1 | foo = bar 2 | tf -------------------------------------------------------------------------------- /_documentation/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore 2 | *.md 3 | 4 | # Allow 5 | !_index.md -------------------------------------------------------------------------------- /_documentation/reference/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore 2 | *.md 3 | 4 | # Allow 5 | !_index.md -------------------------------------------------------------------------------- /_documentation/example/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Examples' 3 | weight: 30 4 | summary: true 5 | --- 6 | -------------------------------------------------------------------------------- /_documentation/reference/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'API Reference' 3 | weight: 20 4 | summary: true 5 | --- 6 | -------------------------------------------------------------------------------- /starlark/types/testdata/examples/evaluable.star: -------------------------------------------------------------------------------- 1 | # evaluable.star file 2 | print("Print from evaluable.star: '%s'" % foo) 3 | bar="bar" -------------------------------------------------------------------------------- /starlark/types/fixtures/modules/.terraform/plugins/linux_amd64/lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "null": "c56285e7bd25a806bf86fcd4893edbe46e621a46e20fe24ef209b6fd0b7cf5fc" 3 | } -------------------------------------------------------------------------------- /_documentation/_home.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | AsCode allows you to describe your infrastructure using an expressive language in Terraform without writing a single line of HCL. -------------------------------------------------------------------------------- /starlark/types/evaluate_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEvaluate(t *testing.T) { 8 | doTest(t, "testdata/evaluate.star") 9 | } 10 | -------------------------------------------------------------------------------- /starlark/types/validate_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestValidate(t *testing.T) { 8 | doTest(t, "testdata/validate.star") 9 | } 10 | -------------------------------------------------------------------------------- /starlark/types/attribute_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAttribute(t *testing.T) { 8 | doTest(t, "testdata/attribute.star") 9 | } 10 | -------------------------------------------------------------------------------- /starlark/types/terraform_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTerraform(t *testing.T) { 8 | doTest(t, "testdata/terraform.star") 9 | } 10 | -------------------------------------------------------------------------------- /starlark/types/fixtures/modules/moduleB/qux.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "qux" { 2 | triggers = { 3 | qux = "qux-value" 4 | } 5 | } 6 | 7 | output "qux" { 8 | value = null_resource.qux.triggers.qux 9 | } -------------------------------------------------------------------------------- /starlark/types/fixtures/modules/.terraform/modules/modules.json: -------------------------------------------------------------------------------- 1 | {"Modules":[{"Key":"moduleA.moduleB","Source":"../moduleB","Dir":"moduleB"},{"Key":"","Source":"","Dir":"."},{"Key":"moduleA","Source":"./moduleA","Dir":"moduleA"}]} -------------------------------------------------------------------------------- /starlark/types/fixtures/modules/main.tf: -------------------------------------------------------------------------------- 1 | module "moduleA" { 2 | source = "./moduleA" 3 | } 4 | 5 | resource "null_resource" "foo" { 6 | triggers = { 7 | foo = "foo-value" 8 | bar = module.moduleA.bar 9 | qux = module.moduleA.qux 10 | } 11 | } -------------------------------------------------------------------------------- /starlark/types/backend_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBackend(t *testing.T) { 8 | doTest(t, "testdata/backend.star") 9 | } 10 | 11 | func TestState(t *testing.T) { 12 | doTest(t, "testdata/state.star") 13 | } 14 | -------------------------------------------------------------------------------- /starlark/runtime/runtime_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLoad(t *testing.T) { 10 | rc := NewRuntime(nil) 11 | _, err := rc.ExecFile("testdata/load.star") 12 | assert.NoError(t, err) 13 | } 14 | -------------------------------------------------------------------------------- /starlark/types/fixtures/modules/moduleA/bar.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "bar" { 2 | triggers = { 3 | bar = "bar-value" 4 | } 5 | } 6 | 7 | module "moduleB" { 8 | source = "../moduleB" 9 | } 10 | 11 | 12 | output "qux" { 13 | value = module.moduleB.qux 14 | } 15 | 16 | output "bar" { 17 | value = null_resource.bar.triggers.bar 18 | } -------------------------------------------------------------------------------- /starlark/types/doc.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // outline: types 4 | // builtin package represents the predefined, Terraform domain-specific, 5 | // functions and types in the scope of any thread of AsCode. This means 6 | // that is not required to use the `load` statement, and any of the function 7 | // can be called without any prefix. 8 | // path: built-in 9 | -------------------------------------------------------------------------------- /starlark/types/testdata/examples/evaluate.star: -------------------------------------------------------------------------------- 1 | # Evaluate execute the given file, with the given context, in this case `foo` 2 | values = evaluate("evaluable.star", foo="foo") 3 | 4 | # The context it's a module, in this case contains the key `bar` 5 | print("Print from main: '%s'" % values.bar) 6 | 7 | # Output: 8 | # Print from evaluable.star: 'foo' 9 | # Print from main: 'bar' 10 | -------------------------------------------------------------------------------- /starlark/types/testdata/examples/provider_resource.star: -------------------------------------------------------------------------------- 1 | helm = tf.provider("helm", "1.0.0") 2 | 3 | podinfo = helm.resource.release("podinfo") 4 | podinfo.chart = "podinfo" 5 | podinfo.version = "3.1.8" 6 | 7 | print(hcl(podinfo)) 8 | # Output: 9 | # resource "helm_release" "podinfo" { 10 | # provider = helm.id_1 11 | # chart = "podinfo" 12 | # version = "3.1.8" 13 | # } 14 | -------------------------------------------------------------------------------- /starlark/types/fixtures/modules/.terraform/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "serial": 2, 4 | "lineage": "7ea572e8-f7f4-9c5a-d66f-34e2f3f14a44", 5 | "modules": [ 6 | { 7 | "path": [ 8 | "root" 9 | ], 10 | "outputs": {}, 11 | "resources": {}, 12 | "depends_on": [] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /starlark/types/testdata/examples/ref.star: -------------------------------------------------------------------------------- 1 | # Non-computed arguments are passed by value to one argument to another 2 | # in some cases may be required to make a referece to and relay in the HCL 3 | # interpolation. 4 | 5 | aws = tf.provider("aws") 6 | 7 | instance = aws.resource.instance("foo") 8 | instance.ami = "foo" 9 | 10 | print(ref(instance, "ami")) 11 | # Output: 12 | # ${aws_instance.foo.ami} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.0-alpine AS builder 2 | RUN apk --no-cache add make 3 | WORKDIR $GOPATH/src/github.com/mcuadros/ascode 4 | COPY . . 5 | RUN GO111MODULE=on CGO_ENABLED=0 GOPROXY="https://proxy.golang.org" \ 6 | go build \ 7 | -ldflags "$(make goldflags)" \ 8 | -o /bin/ascode . 9 | 10 | FROM alpine:latest 11 | RUN apk --no-cache add ca-certificates 12 | COPY --from=builder /bin/ascode /bin/ascode 13 | CMD ["ascode"] -------------------------------------------------------------------------------- /starlark/types/testdata/examples/attribute.star: -------------------------------------------------------------------------------- 1 | # When a Resource has an Attribute means that the value it's only available 2 | # during the `apply` phase of Terraform. So in, AsCode an attribute behaves 3 | # like a poor-man pointer. 4 | 5 | aws = tf.provider("aws") 6 | 7 | ami = aws.resource.ami("ubuntu") 8 | 9 | instance = aws.resource.instance("foo") 10 | instance.ami = ami.id 11 | 12 | print(instance.ami) 13 | # Output: 14 | # ${aws_ami.ubuntu.id} -------------------------------------------------------------------------------- /starlark/types/testdata/examples/provider_collection.star: -------------------------------------------------------------------------------- 1 | tf.provider("aws", "2.13.0", "qux") 2 | tf.provider("aws", "2.13.0", "bar") 3 | tf.provider("google") 4 | 5 | # providers can be access by indexing 6 | aws_names = tf.provider["aws"].keys() 7 | print("aws providers:", sorted(aws_names)) 8 | 9 | # or by the get method 10 | google_names = tf.provider.get("google").keys() 11 | print("google providers:", google_names) 12 | 13 | # Output: 14 | # aws providers: ["bar", "qux"] 15 | # google providers: ["id_1"] -------------------------------------------------------------------------------- /starlark/runtime/testdata/load.star: -------------------------------------------------------------------------------- 1 | # test relative loading 2 | load("includes/foo.star", "foo") 3 | 4 | # evaluate and correct base_path 5 | mod = evaluate("includes/foo.star") 6 | print(mod.foo) 7 | 8 | # module constructor 9 | module("foo") 10 | 11 | # test defined modules 12 | load("encoding/json", "json") 13 | load("encoding/base64", "base64") 14 | load("encoding/csv", "csv") 15 | load("encoding/yaml", "yaml") 16 | load("math", "math") 17 | load("re", "re") 18 | load("time", "time") 19 | load("http", "http") 20 | load("url", "url") -------------------------------------------------------------------------------- /starlark/types/testdata/examples/tf_provider.star: -------------------------------------------------------------------------------- 1 | aws = tf.provider("aws", "2.13.0", "aliased") 2 | helm = tf.provider("helm", "1.0.0") 3 | ignition = tf.provider("ignition") 4 | 5 | print("terraform =", tf.version) 6 | for type in sorted(list(tf.provider)): 7 | for name in tf.provider[type]: 8 | p = tf.provider[type][name] 9 | print(" %s (%s) = %s" % (type, name, p.__version__)) 10 | 11 | # Output: 12 | # terraform = 0.12.23 13 | # aws (aliased) = 2.13.0 14 | # helm (id_1) = 1.0.0 15 | # ignition (id_2) = 1.2.1 -------------------------------------------------------------------------------- /starlark/types/testdata/examples/resource_collection_search.star: -------------------------------------------------------------------------------- 1 | aws = tf.provider("aws") 2 | aws.resource.instance("foo", instance_type="t2.micro") 3 | aws.resource.instance("bar", instance_type="a1.medium") 4 | aws.resource.instance("qux", instance_type="t2.micro") 5 | 6 | r = aws.resource.instance.search("bar") 7 | print("Instance type of `bar`: %s" % r[0].instance_type) 8 | 9 | r = aws.resource.instance.search("instance_type", "t2.micro") 10 | print("Instances with 't2.micro`: %d" % len(r)) 11 | 12 | # Output: 13 | # Instance type of `bar`: a1.medium 14 | # Instances with 't2.micro`: 2 15 | -------------------------------------------------------------------------------- /starlark/types/testdata/examples/tf_overview.star: -------------------------------------------------------------------------------- 1 | tf.backend = backend("gcs", bucket="tf-state") 2 | 3 | aws = tf.provider("aws", "2.54.0", region="us-west-2") 4 | aws.resource.instance("foo", instance_type="t2.micro") 5 | 6 | print(hcl(tf)) 7 | 8 | # Output: 9 | # terraform { 10 | # backend "gcs" { 11 | # bucket = "tf-state" 12 | # } 13 | # } 14 | # 15 | # provider "aws" { 16 | # alias = "id_1" 17 | # version = "2.54.0" 18 | # region = "us-west-2" 19 | # } 20 | # 21 | # resource "aws_instance" "foo" { 22 | # provider = aws.id_1 23 | # instance_type = "t2.micro" 24 | # } 25 | -------------------------------------------------------------------------------- /cmd/common.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | 8 | "github.com/mcuadros/ascode/starlark/runtime" 9 | "github.com/mcuadros/ascode/terraform" 10 | ) 11 | 12 | func init() { 13 | log.SetOutput(ioutil.Discard) 14 | } 15 | 16 | type commonCmd struct { 17 | PluginDir string `long:"plugin-dir" description:"directory containing plugin binaries" default:"$HOME/.terraform.d/plugins"` 18 | 19 | runtime *runtime.Runtime 20 | } 21 | 22 | func (c *commonCmd) init() { 23 | c.runtime = runtime.NewRuntime(&terraform.PluginManager{ 24 | Path: os.ExpandEnv(c.PluginDir)}, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /starlark/types/testdata/examples/provider.star: -------------------------------------------------------------------------------- 1 | def print_provider_info(p): 2 | print("Provider %s[%s] (%s)" % (p.__type__, p.__name__, p.__version__)) 3 | print(" Defines Data Sources: %d" % len(dir(p.data))) 4 | print(" Defines Resources: %d" % len(dir(p.resource))) 5 | print(" Configuration: %s" % p.__dict__) 6 | 7 | provider = tf.provider("google", "3.13.0") 8 | provider.project = "acme-app" 9 | provider.region = "us-central1" 10 | 11 | print_provider_info(provider) 12 | # Output: 13 | # Provider google[id_1] (3.13.0) 14 | # Defines Data Sources: 60 15 | # Defines Resources: 263 16 | # Configuration: {"project": "acme-app", "region": "us-central1"} 17 | 18 | -------------------------------------------------------------------------------- /cmd/repl.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/jessevdk/go-flags" 4 | 5 | // Command descriptions used in the flags.Parser.AddCommand. 6 | const ( 7 | REPLCmdShortDescription = "Run as interactive shell." 8 | REPLCmdLongDescription = REPLCmdShortDescription + "\n\n" + 9 | "The REPL shell provides the same capabilities as the regular `run`\n" + 10 | "command." 11 | ) 12 | 13 | // REPLCmd implements the command `repl`. 14 | type REPLCmd struct { 15 | commonCmd 16 | } 17 | 18 | // Execute honors the flags.Commander interface. 19 | func (c *REPLCmd) Execute(args []string) error { 20 | c.init() 21 | c.runtime.REPL() 22 | 23 | return nil 24 | } 25 | 26 | var _ flags.Commander = &REPLCmd{} 27 | -------------------------------------------------------------------------------- /starlark/module/filepath/testdata/test.star: -------------------------------------------------------------------------------- 1 | load('path/filepath', 'filepath') 2 | load('assert.star', 'assert') 3 | 4 | assert.eq(filepath.separator, "/") 5 | 6 | matches = filepath.glob("/tmp/*") 7 | assert.eq(True, len(matches) > 0) 8 | 9 | assert.eq(True, len(filepath.abs("foo")) > 3) 10 | 11 | assert.eq(filepath.clean("bar///foo.md"), "bar/foo.md") 12 | assert.eq(filepath.base("bar/foo.md"), "foo.md") 13 | assert.eq(filepath.dir("bar/foo.md"), "bar") 14 | assert.eq(filepath.ext("foo.md"), ".md") 15 | 16 | assert.eq(filepath.is_abs("foo"), False) 17 | assert.eq(filepath.is_abs("/foo"), True) 18 | 19 | assert.eq(filepath.join(["foo", "bar"]), "foo/bar") 20 | assert.eq(filepath.rel("/a", "/a/b/c"), "b/c") -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: 5 | - created 6 | 7 | jobs: 8 | release: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | goos: [linux, windows, darwin] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Set GO_LDFLAGS 19 | run: | 20 | echo "GO_LDFLAGS=$(make goldflags)" >> $GITHUB_ENV 21 | 22 | - name: Release binaries 23 | uses: mcuadros/go-release-action@master 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | goos: ${{ matrix.goos }} 27 | goarch: amd64 28 | ldflags: ${{ env.GO_LDFLAGS }} -------------------------------------------------------------------------------- /starlark/module/docker/image_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/qri-io/starlib/testdata" 7 | "go.starlark.net/resolve" 8 | "go.starlark.net/starlark" 9 | "go.starlark.net/starlarktest" 10 | ) 11 | 12 | func TestImage(t *testing.T) { 13 | resolve.AllowFloat = true 14 | resolve.AllowLambda = true 15 | thread := &starlark.Thread{Load: testdata.NewLoader(LoadModule, ModuleName)} 16 | starlarktest.SetReporter(thread, t) 17 | 18 | // Execute test file 19 | _, err := starlark.ExecFile(thread, "testdata/test.star", nil, nil) 20 | if err != nil { 21 | if ee, ok := err.(*starlark.EvalError); ok { 22 | t.Error(ee.Backtrace()) 23 | } else { 24 | t.Error(err) 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /starlark/types/testdata/examples/backend_local.star: -------------------------------------------------------------------------------- 1 | # new instance of a local backend 2 | # https://www.terraform.io/docs/backends/types/local.html 3 | b = backend("local") 4 | b.path = "terraform.tfstate" 5 | 6 | # it reads the state 7 | s = b.state() 8 | 9 | for provider in sorted(list(s)): 10 | print("%s:" % provider) 11 | for resource in sorted(list(s[provider]["resource"])): 12 | count = len(s[provider]["resource"][resource]) 13 | print(" %s (%d)" % (resource, count)) 14 | 15 | # Output: 16 | # google: 17 | # container_cluster (1) 18 | # container_node_pool (1) 19 | # helm: 20 | # release (5) 21 | # kubernetes: 22 | # cluster_role_binding (1) 23 | # namespace (1) 24 | # secret (1) 25 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Push 2 | on: 3 | release: 4 | types: 5 | - created 6 | jobs: 7 | docker: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Build image 14 | uses: mr-smithers-excellent/docker-build-push@v2 15 | with: 16 | image: mcuadros/ascode 17 | registry: docker.io 18 | username: mcuadros 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | 21 | - name: Tag image 22 | uses: mr-smithers-excellent/docker-build-push@v2 23 | with: 24 | image: mcuadros/ascode 25 | registry: docker.io 26 | tag: latest 27 | username: mcuadros 28 | password: ${{ secrets.DOCKER_PASSWORD }} -------------------------------------------------------------------------------- /starlark/types/testdata/nested.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | p = tf.provider("aws", "2.13.0") 4 | d = p.data.ami() 5 | 6 | assert.eq(type(d.filter), "ResourceCollection") 7 | 8 | bar = d.filter(name="bar", values=["qux"]) 9 | 10 | assert.eq(str(bar), "Resource") 11 | assert.eq(bar.name, "bar") 12 | assert.eq(bar.values, ["qux"]) 13 | 14 | assert.eq(len(d.filter), 1) 15 | assert.eq(d.filter[0], bar) 16 | 17 | qux = d.filter() 18 | qux.name = "qux" 19 | qux.values = ["bar"] 20 | 21 | assert.eq(qux.name, "qux") 22 | assert.eq(qux.values, ["bar"]) 23 | 24 | assert.eq(len(d.filter), 2) 25 | assert.eq(d.filter[1], qux) 26 | 27 | d.filter[1].values = ["baz"] 28 | assert.eq(qux.values, ["baz"]) 29 | 30 | assert.ne(d.filter[0], d.filter[1]) -------------------------------------------------------------------------------- /starlark/types/testdata/evaluate.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | bar = "bar" 4 | 5 | # context by kwargs 6 | module = evaluate("evaluate/test.star", bar=bar) 7 | assert.eq(str(module), '') 8 | assert.eq(module.foo, bar) 9 | 10 | # context by dict 11 | module = evaluate("evaluate/test.star", {"bar": bar}) 12 | assert.eq(str(module), '') 13 | assert.eq(module.foo, bar) 14 | 15 | # context dict overrided by kwargs 16 | module = evaluate("evaluate/test.star", {"bar": bar}, bar="foo") 17 | assert.eq(str(module), '') 18 | assert.eq(module.foo, "foo") 19 | 20 | # context dict with non strings 21 | def contextNonString(): evaluate("evaluate/test.star", {1: bar}) 22 | assert.fails(contextNonString, "expected string keys in dict, got int at index 0") 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | go-version: [1.15.x, 1.16.x] 15 | platform: [ubuntu-latest, macos-latest, windows-latest] 16 | runs-on: ${{ matrix.platform }} 17 | steps: 18 | 19 | - name: Install Go 20 | uses: actions/setup-go@v1 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | 24 | - name: Checkout code 25 | uses: actions/checkout@v2 26 | 27 | - name: Allow skip provisioner tests 28 | uses: allenevans/set-env@v2.0.0 29 | with: 30 | ALLOW_PROVISIONER_SKIP: 1 31 | 32 | - name: Test 33 | run: go test ./... -------------------------------------------------------------------------------- /starlark/types/testdata/backend.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | b = backend("gcs") 4 | 5 | # resource 6 | assert.eq(b.__kind__, "backend") 7 | assert.eq(b.__type__, "gcs") 8 | assert.eq(type(b), "Backend") 9 | assert.eq(str(b), "Backend") 10 | 11 | # attr 12 | b.bucket = "tf-state-prod" 13 | b.prefix = "terraform/state" 14 | 15 | # attr names 16 | assert.eq("__provider__" in dir(b), False) 17 | assert.eq("__name__" in dir(b), False) 18 | assert.eq("depends_on" in dir(b), False) 19 | assert.eq("add_provisioner" in dir(b), False) 20 | assert.eq("state" in dir(b), True) 21 | assert.eq("bucket" in dir(b), True) 22 | 23 | # hcl 24 | assert.eq(hcl(b), "" + 25 | 'terraform {\n' + \ 26 | ' backend "gcs" {\n' + \ 27 | ' bucket = "tf-state-prod"\n' + \ 28 | ' prefix = "terraform/state"\n' + \ 29 | ' }\n' + \ 30 | '}\n\n') 31 | -------------------------------------------------------------------------------- /_scripts/query-go-mod.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/rogpeppe/go-internal/modfile" 12 | ) 13 | 14 | func main() { 15 | path := os.Args[1] 16 | pkg := os.Args[2] 17 | 18 | f, err := readGoMod(path) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | for _, r := range f.Require { 24 | if r.Mod.Path != pkg { 25 | continue 26 | } 27 | 28 | parts := strings.Split(r.Mod.Version, "-") 29 | if len(parts) > 1 { 30 | fmt.Println(parts[len(parts)-1]) 31 | } 32 | } 33 | } 34 | 35 | func readGoMod(path string) (*modfile.File, error) { 36 | content, err := ioutil.ReadFile(filepath.Join(path, "go.mod")) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return modfile.ParseLax("", content, nil) 42 | } 43 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/jessevdk/go-flags" 7 | "github.com/mcuadros/ascode/cmd" 8 | ) 9 | 10 | var version string 11 | var build string 12 | 13 | func main() { 14 | parser := flags.NewNamedParser("ascode", flags.Default) 15 | parser.LongDescription = "AsCode - Terraform Alternative Syntax." 16 | parser.AddCommand("run", cmd.RunCmdShortDescription, cmd.RunCmdLongDescription, &cmd.RunCmd{}) 17 | parser.AddCommand("repl", cmd.REPLCmdShortDescription, cmd.REPLCmdLongDescription, &cmd.REPLCmd{}) 18 | parser.AddCommand("version", cmd.VersionCmdShortDescription, cmd.VersionCmdLongDescription, &cmd.VersionCmd{}) 19 | 20 | if _, err := parser.Parse(); err != nil { 21 | if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { 22 | os.Exit(0) 23 | } 24 | 25 | os.Exit(1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /starlark/module/filepath/filepath_test.go: -------------------------------------------------------------------------------- 1 | package filepath 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/qri-io/starlib/testdata" 8 | "go.starlark.net/resolve" 9 | "go.starlark.net/starlark" 10 | "go.starlark.net/starlarktest" 11 | ) 12 | 13 | func TestFile(t *testing.T) { 14 | if filepath.Separator != '/' { 15 | // TODO(mcuadros): do proper testing on windows. 16 | t.Skip("skiping filepath test for Windows") 17 | } 18 | 19 | resolve.AllowFloat = true 20 | thread := &starlark.Thread{Load: testdata.NewLoader(LoadModule, ModuleName)} 21 | starlarktest.SetReporter(thread, t) 22 | 23 | // Execute test file 24 | _, err := starlark.ExecFile(thread, "testdata/test.star", nil, nil) 25 | if err != nil { 26 | if ee, ok := err.(*starlark.EvalError); ok { 27 | t.Error(ee.Backtrace()) 28 | } else { 29 | t.Error(err) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /starlark/types/testdata/examples/resource_collection_call.star: -------------------------------------------------------------------------------- 1 | aws = tf.provider("aws") 2 | 3 | # Resources can be defined in different ways... 4 | 5 | # you can set the resource attributes... 6 | v = aws.resource.instance() 7 | v.instance_type = "t2.micro" 8 | v.tags = {"name": "HelloWorld"} 9 | 10 | # or using a dict in the constructor... 11 | d = aws.resource.instance({ 12 | "instance_type": "t2.micro", 13 | "tags": {"name": "HelloWorld"}, 14 | }) 15 | 16 | # or even using kwargs 17 | k = aws.resource.instance(instance_type="t2.micro", tags={"name": "HelloWorld"}) 18 | 19 | # and all the resources are equivalent: 20 | print(v.__dict__) 21 | print(d.__dict__) 22 | print(k.__dict__) 23 | 24 | # Output: 25 | # {"instance_type": "t2.micro", "tags": {"name": "HelloWorld"}} 26 | # {"instance_type": "t2.micro", "tags": {"name": "HelloWorld"}} 27 | # {"instance_type": "t2.micro", "tags": {"name": "HelloWorld"}} 28 | -------------------------------------------------------------------------------- /starlark/module/os/os_test.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/qri-io/starlib/testdata" 8 | "go.starlark.net/resolve" 9 | "go.starlark.net/starlark" 10 | "go.starlark.net/starlarktest" 11 | ) 12 | 13 | func TestFile(t *testing.T) { 14 | if filepath.Separator != '/' { 15 | // TODO(mcuadros): do proper testing on windows. 16 | t.Skip("skiping os test for Windows") 17 | } 18 | 19 | resolve.AllowFloat = true 20 | resolve.AllowGlobalReassign = true 21 | resolve.AllowLambda = true 22 | 23 | thread := &starlark.Thread{Load: testdata.NewLoader(LoadModule, ModuleName)} 24 | starlarktest.SetReporter(thread, t) 25 | 26 | // Execute test file 27 | _, err := starlark.ExecFile(thread, "testdata/test.star", nil, nil) 28 | if err != nil { 29 | if ee, ok := err.(*starlark.EvalError); ok { 30 | t.Error(ee.Backtrace()) 31 | } else { 32 | t.Error(err) 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /starlark/module/url/url_test.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/qri-io/starlib/testdata" 8 | "go.starlark.net/resolve" 9 | "go.starlark.net/starlark" 10 | "go.starlark.net/starlarktest" 11 | ) 12 | 13 | func TestFile(t *testing.T) { 14 | if filepath.Separator != '/' { 15 | // TODO(mcuadros): do proper testing on windows. 16 | t.Skip("skiping os test for Windows") 17 | } 18 | 19 | resolve.AllowFloat = true 20 | resolve.AllowGlobalReassign = true 21 | resolve.AllowLambda = true 22 | 23 | thread := &starlark.Thread{Load: testdata.NewLoader(LoadModule, ModuleName)} 24 | starlarktest.SetReporter(thread, t) 25 | 26 | // Execute test file 27 | _, err := starlark.ExecFile(thread, "testdata/test.star", nil, nil) 28 | if err != nil { 29 | if ee, ok := err.(*starlark.EvalError); ok { 30 | t.Error(ee.Backtrace()) 31 | } else { 32 | t.Error(err) 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /starlark/types/testdata/provisioner.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | # constructor 4 | foo = provisioner("file", source="conf/myapp.conf", destination="/etc/myapp.conf") 5 | assert.eq(foo.source, "conf/myapp.conf") 6 | assert.eq(foo.destination, "/etc/myapp.conf") 7 | 8 | 9 | file = provisioner("file") 10 | assert.eq(file.__kind__, "provisioner") 11 | assert.eq(file.__type__, "file") 12 | 13 | # attr 14 | file.content = "foo" 15 | assert.eq(file.content, "foo") 16 | 17 | # attr names 18 | assert.eq("__provider__" in dir(file), False) 19 | assert.eq("__name__" in dir(file), False) 20 | assert.eq("depends_on" in dir(file), False) 21 | assert.eq("add_provisioner" in dir(file), False) 22 | assert.eq("content" in dir(file), True) 23 | 24 | # hcl 25 | assert.eq(hcl(file), "" + 26 | 'provisioner "file" {\n' + \ 27 | ' content = "foo"\n' + \ 28 | '}\n') 29 | 30 | 31 | # type 32 | assert.eq(type(file), "Provisioner") 33 | assert.eq(str(file), "Provisioner") 34 | -------------------------------------------------------------------------------- /starlark/types/testdata/validate.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | helm = tf.provider("helm", "1.0.0", "default") 4 | helm.kubernetes.token = "foo" 5 | 6 | # require scalar arguments 7 | helm.resource.release() 8 | errors = validate(helm) 9 | assert.eq(len(errors), 2) 10 | assert.eq(errors[0].pos, "testdata/validate.star:7:22") 11 | assert.eq(errors[1].pos, "testdata/validate.star:7:22") 12 | 13 | # require list arguments 14 | google = tf.provider("google") 15 | r = google.resource.organization_iam_custom_role(role_id="foo", org_id="bar", title="qux") 16 | r.permissions = ["foo"] 17 | assert.eq(len(validate(google)), 0) 18 | 19 | r.permissions.pop() 20 | assert.eq(len(validate(google)), 1) 21 | 22 | # require blocks 23 | google = tf.provider("google") 24 | r = google.resource.compute_global_forwarding_rule(target="foo", name="bar") 25 | r.metadata_filters() 26 | assert.eq(len(validate(google)), 2) 27 | 28 | errors = validate(google) 29 | for e in errors: print(e.pos, e.msg) -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | release: 4 | types: 5 | - created 6 | jobs: 7 | gh-pages: 8 | env: 9 | GOPATH: ${{ github.workspace }} 10 | WORKING_PATH: ${{ github.workspace }}/src/github.com/${{ github.repository }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | with: 16 | path: ${{env.WORKING_PATH}} 17 | 18 | - name: Install Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.16.x 22 | 23 | - name: Setup Hugo 24 | uses: peaceiris/actions-hugo@v2 25 | with: 26 | hugo-version: '0.68.2' 27 | extended: true 28 | 29 | - name: Setup Outline 30 | run: go get github.com/mcuadros/outline 31 | 32 | - name: Hugo Build 33 | run: make hugo-build 34 | working-directory: ${{env.WORKING_PATH}} 35 | 36 | - name: Push to gh-pages 37 | uses: peaceiris/actions-gh-pages@v3 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: src/github.com/${{ github.repository }}/_site/public 41 | cname: ascode.run -------------------------------------------------------------------------------- /starlark/module/docker/testdata/test.star: -------------------------------------------------------------------------------- 1 | load('docker', 'docker') 2 | load('assert.star', 'assert') 3 | 4 | attr = docker.image("mcuadros/ascode", "latest") 5 | assert.eq(attr.name, "docker.io/mcuadros/ascode") 6 | assert.eq(attr.domain, "docker.io") 7 | assert.eq(attr.path, "mcuadros/ascode") 8 | assert.eq(dir(attr), ["domain", "name", "path", "tags", "version"]) 9 | 10 | image = docker.image("fedora", "latest") 11 | assert.eq(image.name, "docker.io/library/fedora") 12 | assert.eq(image.domain, "docker.io") 13 | assert.eq(image.path, "library/fedora") 14 | assert.eq(image.version(), "latest") 15 | 16 | semver = docker.image("fedora", ">=22 <30") 17 | assert.eq(semver.name, "docker.io/library/fedora") 18 | assert.eq(semver.version(), "29") 19 | assert.eq(semver.version(True), "docker.io/library/fedora:29") 20 | 21 | prometheus = docker.image("quay.io/prometheus/prometheus", "1.8.x") 22 | assert.eq(prometheus.name, "quay.io/prometheus/prometheus") 23 | assert.eq(prometheus.version(), "v1.8.2") 24 | assert.eq(len(prometheus.tags()) > 0, True) 25 | 26 | tagNotFound = docker.image("fedora", "not-found") 27 | assert.fails(lambda: tagNotFound.version(), 'tag "not-found" not found in repository') 28 | 29 | -------------------------------------------------------------------------------- /starlark/types/testdata/state.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | b = backend("local") 4 | 5 | b.path = "fixtures/modules/terraform.tfstate" 6 | 7 | s = b.state() 8 | assert.ne(s, None) 9 | assert.ne(s["null"], None) 10 | assert.eq(s["null"]["resource"]["resource"]["foo"].triggers["foo"], "foo-value") 11 | assert.eq(b.state("module.moduleA")["null"]["resource"]["resource"]["bar"].triggers["bar"], "bar-value") 12 | assert.eq(b.state("module.moduleA.module.moduleB")["null"]["resource"]["resource"]["qux"].triggers["qux"], "qux-value") 13 | 14 | c = backend("local") 15 | c.path = "fixtures/state/terraform.tfstate" 16 | 17 | s = c.state() 18 | assert.ne(s["google"]["data"]["client_config"], None) 19 | assert.eq(s["google"]["data"]["client_config"]["default"].id, "2020-03-19 15:06:27.25614138 +0000 UTC") 20 | assert.eq(s["google"]["data"]["client_config"]["default"].project, "project-foo") 21 | 22 | cluster = s["google"]["resource"]["container_cluster"]["primary"] 23 | assert.eq(cluster.addons_config.network_policy_config.disabled, True) 24 | 25 | release = s["helm"]["resource"]["release"]["nats-operator"] 26 | assert.eq(release.set[0].name, "cluster.auth.enabled") 27 | assert.eq(release.set[1].name, "image.tag") 28 | 29 | -------------------------------------------------------------------------------- /starlark/module/url/testdata/test.star: -------------------------------------------------------------------------------- 1 | load('url', 'url') 2 | load('assert.star', 'assert') 3 | 4 | 5 | assert.eq(url.query_escape("/foo&bar qux"), "%2Ffoo%26bar+qux") 6 | assert.eq(url.query_unescape("%2Ffoo%26bar+qux"), "/foo&bar qux") 7 | assert.fails(lambda: url.query_unescape("%ssf"), 'invalid URL escape "%ss"') 8 | 9 | assert.eq(url.path_escape("/foo&bar qux"), "%2Ffoo&bar%20qux") 10 | assert.eq(url.path_unescape("%2Ffoo&bar%20qux"), "/foo&bar qux") 11 | assert.fails(lambda: url.path_unescape("%ssf"), 'invalid URL escape "%ss"') 12 | 13 | r = url.parse("http://qux:bar@bing.com/search?q=dotnet#foo") 14 | assert.eq(r.scheme, "http") 15 | assert.eq(r.opaque, "") 16 | assert.eq(r.username, "qux") 17 | assert.eq(r.password, "bar") 18 | assert.eq(r.host, "bing.com") 19 | assert.eq(r.path, "/search") 20 | assert.eq(r.raw_query, "q=dotnet") 21 | assert.eq(r.fragment, "foo") 22 | 23 | r = url.parse("http://qux:@bing.com/search?q=dotnet#foo") 24 | assert.eq(r.username, "qux") 25 | assert.eq(r.password, "") 26 | 27 | r = url.parse("http://qux@bing.com/search?q=dotnet#foo") 28 | assert.eq(r.username, "qux") 29 | assert.eq(r.password, None) 30 | 31 | r = url.parse("http://bing.com/search?q=dotnet#foo") 32 | assert.eq(r.username, None) 33 | assert.eq(r.password, None) 34 | 35 | assert.fails(lambda: url.parse("%ssf"), 'invalid URL escape "%ss"') 36 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/jessevdk/go-flags" 8 | ) 9 | 10 | // Command descriptions used in the flags.Parser.AddCommand. 11 | const ( 12 | VersionCmdShortDescription = "Version prints information about this binary." 13 | VersionCmdLongDescription = VersionCmdShortDescription + "\n\n" + 14 | "Includes build information about AsCode like version and build\n" + 15 | "date but also versions from the Go runtime and other dependencies." 16 | ) 17 | 18 | var ( 19 | version string 20 | commit string 21 | build string 22 | terraformVersion string 23 | starlarkVersion string 24 | starlibVersion string 25 | ) 26 | 27 | // VersionCmd implements the command `version`. 28 | type VersionCmd struct{} 29 | 30 | // Execute honors the flags.Commander interface. 31 | func (c *VersionCmd) Execute(args []string) error { 32 | fmt.Printf("Go Version: %s\n", runtime.Version()) 33 | fmt.Printf("AsCode Version: %s\n", version) 34 | fmt.Printf("AsCode Commit: %s\n", commit) 35 | fmt.Printf("AsCode Build Date: %s\n", build) 36 | fmt.Printf("Terraform Version: %s\n", terraformVersion) 37 | fmt.Printf("Starlark Version: %s\n", starlarkVersion) 38 | fmt.Printf("Starlib Version: %s\n", starlibVersion) 39 | 40 | return nil 41 | } 42 | 43 | var _ flags.Commander = &VersionCmd{} 44 | -------------------------------------------------------------------------------- /_documentation/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Documentation' 3 | weight: 1 4 | --- 5 | 6 | ## AsCode - Terraform Alternative Syntax 7 | 8 | **AsCode** is a tool to define infrastructure as code using the [Starlark](https://github.com/google/starlark-go/blob/master/doc/spec.md) language on top of [Terraform](https://github.com/hashicorp/terraform). It allows to describe your infrastructure using an expressive language in Terraform without writing a single line of HCL, meanwhile, you have the complete ecosystem of [providers](https://www.terraform.io/docs/providers/index.html) 9 | 10 | ### Why? 11 | 12 | Terraform is a great tool, with support for almost everything you can imagine, making it the industry leader. Terraform is based on HCL, a JSON-alike declarative language, with minimal control flow functionality. IMHO, to unleash the power of the IaC, a powerful, expressive language should be used, where basic elements like loops or functions are first-class citizens. 13 | 14 | ### What is Starlark? 15 | 16 | Starlark is a dialect of Python intended to be used as a configuration language. A Starlark interpreter is typically embedded within a larger application, and this application may define additional domain-specific functions and data types beyond those provided by the core language. For example, Starlark is embedded within (and was originally developed for) the Bazel build tool, and Bazel's build language is based on Starlark. -------------------------------------------------------------------------------- /starlark/types/testdata/examples/resource.star: -------------------------------------------------------------------------------- 1 | # Create a new instance of the latest Ubuntu 14.04 on an 2 | # t2.micro node with an AWS Tag naming it "HelloWorld" 3 | 4 | aws = tf.provider("aws", "2.54.0") 5 | aws.region = "us-west-2" 6 | 7 | ubuntu_filter = "ubuntu/images/*/ubuntu-xenial-16.04-amd64-server-*" 8 | canonical = "099720109477" 9 | 10 | ami = aws.data.ami("ubuntu") 11 | ami.most_recent = True 12 | ami.filter(name="name", values=[ubuntu_filter]) 13 | ami.filter(name="virtualization-type", values=["hvm"]) 14 | ami.owners = [canonical] 15 | 16 | 17 | instance = aws.resource.instance("web") 18 | instance.instance_type = "t2.micro" 19 | instance.ami = ami.id 20 | instance.tags = { 21 | "name": "HelloWorld" 22 | } 23 | 24 | print(hcl(tf)) 25 | # Output: 26 | # provider "aws" { 27 | # alias = "id_1" 28 | # version = "2.54.0" 29 | # region = "us-west-2" 30 | # } 31 | # 32 | # data "aws_ami" "ubuntu" { 33 | # provider = aws.id_1 34 | # most_recent = true 35 | # owners = ["099720109477"] 36 | # 37 | # filter { 38 | # name = "name" 39 | # values = ["ubuntu/images/*/ubuntu-xenial-16.04-amd64-server-*"] 40 | # } 41 | # 42 | # filter { 43 | # name = "virtualization-type" 44 | # values = ["hvm"] 45 | # } 46 | # } 47 | # 48 | # resource "aws_instance" "web" { 49 | # provider = aws.id_1 50 | # ami = "${data.aws_ami.ubuntu.id}" 51 | # instance_type = "t2.micro" 52 | # tags = { name = "HelloWorld" } 53 | # } 54 | -------------------------------------------------------------------------------- /starlark/module/os/testdata/test.star: -------------------------------------------------------------------------------- 1 | load('os', 'os') 2 | load('assert.star', 'assert') 3 | 4 | assert.ne(os.temp_dir(), "") 5 | 6 | key = "HELLO" 7 | value = "world" 8 | 9 | os.setenv(key, value) 10 | assert.eq(os.getenv(key), value) 11 | 12 | content = "hello world\n" 13 | 14 | home = os.getenv("HOME") 15 | os.chdir(home) 16 | assert.eq(os.getwd(), home) 17 | 18 | temp = os.temp_dir() 19 | os.chdir(temp) 20 | 21 | assert.eq(os.write_file("plain.txt", content), None) 22 | assert.eq(os.read_file("plain.txt"), content) 23 | 24 | assert.eq(os.write_file("perms.txt", content=content, perms=0o777), None) 25 | assert.eq(os.read_file("perms.txt"), content) 26 | 27 | os.mkdir("foo", 0o755) 28 | os.remove("foo") 29 | 30 | os.mkdir_all("foo/bar", 0o755) 31 | 32 | os.rename("foo", "bar") 33 | os.remove_all("bar") 34 | 35 | def deleteNotExistant(): os.remove("foo") 36 | assert.fails(deleteNotExistant, "remove foo: no such file or directory") 37 | 38 | # test command 39 | temp = os.temp_dir() + "/example-dir" 40 | os.mkdir_all(temp + "/foo/bar", 0o755) 41 | os.chdir(temp) 42 | 43 | assert.eq(os.command("ls -1"), "foo") 44 | 45 | # test command dir 46 | assert.eq(os.command("ls -1", dir="foo"), "bar") 47 | 48 | # test command shell and env 49 | assert.eq(os.command("echo $FOO", shell=True, env=["FOO=foo"]), "foo") 50 | 51 | # test command combined 52 | assert.ne(os.command("not-exists || true", shell=True, combined=True), "") 53 | 54 | # test command error 55 | assert.fails(lambda: os.command("not-exists"), "executable file not found") 56 | 57 | os.remove_all(temp) 58 | -------------------------------------------------------------------------------- /_documentation/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://ascode.run" 2 | languageCode = "en-us" 3 | title = "AsCode - Terraform Alternative Syntax" 4 | theme = "hugo-ascode-theme" 5 | 6 | pygmentsCodeFences = true 7 | pygmentsCodefencesGuessSyntax = true 8 | pygmentsUseClasses = true 9 | 10 | [[menu.main]] 11 | name = "Home" 12 | url = "/" 13 | weight = 1 14 | 15 | [[menu.main]] 16 | name = "Docs" 17 | url = "/docs/" 18 | weight = 2 19 | 20 | [[menu.main]] 21 | name = "GitHub" 22 | url = "https://github.com/mcuadros/ascode" 23 | weight = 3 24 | 25 | [markup.goldmark.renderer] 26 | unsafe = true 27 | autoHeadingIDType = "github" 28 | 29 | [params] 30 | version="" 31 | google_analytics_id="" 32 | homepage_button_link = '/docs' 33 | homepage_button_text = 'Read The Docs' 34 | homepage_intro = 'AsCode allows you to describe your infrastructure using an expressive language in Terraform without writing a single line of HCL.' 35 | homepage_image = '/images/terminal.gif' 36 | 37 | [params.homepage_meta_tags] 38 | meta_description = "AsCode allows you to describe your infrastructure using an expressive language in Terraform without writing a single line of HCL." 39 | meta_og_title = "AsCode - Terraform Alternative Syntax" 40 | meta_og_type = "website" 41 | meta_og_url = "https://ascode.run" 42 | meta_og_image = "https://ascode.run/images/og.png" 43 | meta_og_description = "AsCode - Terraform Alternative Syntax." 44 | 45 | [params.logo] 46 | mobile = "/images/logo-mobile.svg" 47 | standard = "/images/logo.svg" 48 | -------------------------------------------------------------------------------- /starlark/types/testdata/terraform.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | assert.eq(type(tf), "Terraform") 4 | assert.ne(tf.version, "") 5 | assert.eq("aws" in tf.provider, False) 6 | 7 | # attr names 8 | assert.eq("version" in dir(tf), True) 9 | assert.eq("backend" in dir(tf), True) 10 | assert.eq("provider" in dir(tf), True) 11 | 12 | # provider 13 | qux = tf.provider("aws", "2.13.0", "qux", region="qux") 14 | bar = tf.provider("aws", "2.13.0", "bar", region="bar") 15 | assert.eq(bar.region, "bar") 16 | 17 | assert.eq(len(tf.provider["aws"]), 2) 18 | assert.eq("foo" in tf.provider["aws"], False) 19 | assert.eq(tf.provider["aws"]["bar"] == None, False) 20 | assert.eq(tf.provider["aws"]["bar"], bar) 21 | assert.eq(tf.provider["aws"]["bar"].region, "bar") 22 | 23 | # type 24 | assert.eq(type(tf.provider), "ProviderCollection") 25 | 26 | # backend 27 | assert.eq(tf.backend, None) 28 | 29 | tf.backend = backend("local") 30 | tf.backend.path = "foo" 31 | assert.eq(str(tf.backend), "Backend") 32 | 33 | def backendWrongType(): tf.backend = "foo" 34 | assert.fails(backendWrongType, "unexpected value string at backend") 35 | assert.eq(str(tf.backend), "Backend") 36 | 37 | # pop provider 38 | baz = tf.provider("aws", "2.13.0", "baz", region="baz") 39 | pop = tf.provider["aws"].pop("baz") 40 | 41 | # hcl 42 | assert.eq(hcl(tf), "" + 43 | 'terraform {\n' + \ 44 | ' backend "local" {\n' + \ 45 | ' path = "foo"\n' + \ 46 | ' }\n' + \ 47 | '}\n' + \ 48 | '\n' + \ 49 | 'provider "aws" {\n' + \ 50 | ' alias = "qux"\n' + \ 51 | ' version = "2.13.0"\n' + \ 52 | ' region = "qux"\n' + \ 53 | '}\n' + \ 54 | '\n' + \ 55 | 'provider "aws" {\n' + \ 56 | ' alias = "bar"\n' + \ 57 | ' version = "2.13.0"\n' + \ 58 | ' region = "bar"\n' + \ 59 | '}\n\n') -------------------------------------------------------------------------------- /starlark/types/examples_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | stdos "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "go.starlark.net/starlark" 13 | ) 14 | 15 | func TestExamples(t *testing.T) { 16 | pwd, _ := stdos.Getwd() 17 | defer func() { 18 | stdos.Chdir(pwd) 19 | }() 20 | 21 | err := stdos.Chdir(filepath.Join(pwd, "testdata", "examples")) 22 | assert.NoError(t, err) 23 | 24 | tests, err := filepath.Glob("*.star") 25 | assert.NoError(t, err) 26 | 27 | for _, test := range tests { 28 | if test == "evaluable.star" { 29 | continue 30 | } 31 | 32 | doTestExample(t, test) 33 | } 34 | } 35 | 36 | func doTestExample(t *testing.T, filename string) { 37 | var output string 38 | printer := func(_ *starlark.Thread, msg string) { 39 | if output != "" { 40 | output += "\n" 41 | } 42 | 43 | output += msg 44 | } 45 | 46 | doTestPrint(t, filename, printer) 47 | expected := strings.TrimSpace(getExpectedFromExample(t, filename)) 48 | 49 | assert.Equalf(t, strings.TrimSpace(output), expected, filename) 50 | } 51 | 52 | func getExpectedFromExample(t *testing.T, filename string) string { 53 | f, err := os.Open(filename) 54 | assert.NoError(t, err) 55 | defer f.Close() 56 | 57 | var expected []string 58 | scanner := bufio.NewScanner(f) 59 | var capture bool 60 | for scanner.Scan() { 61 | line := scanner.Text() 62 | if line == "# Output:" { 63 | capture = true 64 | continue 65 | } 66 | 67 | if !capture { 68 | continue 69 | } 70 | 71 | if len(line) >= 2 { 72 | line = line[2:] 73 | } else { 74 | line = "" 75 | } 76 | 77 | expected = append(expected, line) 78 | 79 | } 80 | 81 | return strings.Join(expected, "\n") 82 | } 83 | -------------------------------------------------------------------------------- /_documentation/starlark/dialect-differences.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Dialect differences' 3 | weight: 11 4 | --- 5 | 6 | The list below summarizes features of the Go implementation that are 7 | known to differ from the Java implementation of Starlark used by Bazel. 8 | Some of these features may be controlled by global options to allow 9 | applications to mimic the Bazel dialect more closely. Our goal is 10 | eventually to eliminate all such differences on a case-by-case basis. 11 | See [Starlark spec issue 20](https://github.com/bazelbuild/starlark/issues/20). 12 | 13 | * Integers are represented with infinite precision. 14 | * Integer arithmetic is exact. 15 | * Floating-point literals are supported (option: `-float`). 16 | * The `float` built-in function is provided (option: `-float`). 17 | * Real division using `float / float` is supported (option: `-float`). 18 | * String interpolation supports the `[ioxXeEfFgGc]` conversions. 19 | * `def` statements may be nested (option: `-nesteddef`). 20 | * `lambda` expressions are supported (option: `-lambda`). 21 | * String elements are bytes. 22 | * Non-ASCII strings are encoded using UTF-8. 23 | * Strings support octal and hex byte escapes. 24 | * Strings have the additional methods `elem_ords`, `codepoint_ords`, and `codepoints`. 25 | * The `chr` and `ord` built-in functions are supported. 26 | * The `set` built-in function is provided (option: `-set`). 27 | * `set & set` and `set | set` compute set intersection and union, respectively. 28 | * `assert` is a valid identifier. 29 | * Dot expressions may appear on the left side of an assignment: `x.f = 1`. 30 | * `type(x)` returns `"builtin_function_or_method"` for built-in functions. 31 | * `if`, `for`, and `while` are permitted at top level (option: `-globalreassign`). 32 | * top-level rebindings are permitted (option: `-globalreassign`). -------------------------------------------------------------------------------- /starlark/test/assert.star.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | const assertStarFile = ` 4 | # Predeclared built-ins for this module: 5 | # 6 | # error(msg): report an error in Go's test framework without halting execution. 7 | # This is distinct from the built-in fail function, which halts execution. 8 | # catch(f): evaluate f() and returns its evaluation error message, if any 9 | # matches(str, pattern): report whether str matches regular expression pattern. 10 | # module(**kwargs): a constructor for a module. 11 | # _freeze(x): freeze the value x and everything reachable from it. 12 | # 13 | # Clients may use these functions to define their own testing abstractions. 14 | 15 | def _eq(x, y): 16 | if x != y: 17 | error("%r != %r" % (x, y)) 18 | 19 | def _ne(x, y): 20 | if x == y: 21 | error("%r == %r" % (x, y)) 22 | 23 | def _true(cond, msg = "assertion failed"): 24 | if not cond: 25 | error(msg) 26 | 27 | def _lt(x, y): 28 | if not (x < y): 29 | error("%s is not less than %s" % (x, y)) 30 | 31 | def _contains(x, y): 32 | if y not in x: 33 | error("%s does not contain %s" % (x, y)) 34 | 35 | def _fails(f, pattern): 36 | "assert_fails asserts that evaluation of f() fails with the specified error." 37 | msg = catch(f) 38 | if msg == None: 39 | error("evaluation succeeded unexpectedly (want error matching %r)" % pattern) 40 | elif not matches(pattern, msg): 41 | error("regular expression (%s) did not match error (%s)" % (pattern, msg)) 42 | 43 | freeze = _freeze # an exported global whose value is the built-in freeze function 44 | 45 | assert = module( 46 | "assert", 47 | fail = error, 48 | eq = _eq, 49 | ne = _ne, 50 | true = _true, 51 | lt = _lt, 52 | contains = _contains, 53 | fails = _fails, 54 | ) 55 | ` 56 | -------------------------------------------------------------------------------- /_documentation/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Quick Install' 3 | weight: 1 4 | --- 5 | 6 | AsCode is written in [Go](https://golang.org/) with support for multiple platforms. 7 | 8 | The latest release can be found at [GitHub Releases.](https://github.com/mcuadros/ascode/releases), currently provides pre-built binaries for the following: 9 | 10 | - Linux 11 | - macOS (Darwin) 12 | - Windows 13 | 14 | ## Binary (Cross-platform) 15 | 16 | Download the appropriate version for your platform from [GitHub Releases.](https://github.com/mcuadros/ascode/releases). Once downloaded, the binary can be run from anywhere. You don’t need to install it into a global location. 17 | 18 | Ideally, you should install it somewhere in your PATH for easy use. `/usr/local/bin` is the most probable location. 19 | 20 | ### Linux 21 | ```sh 22 | wget https://github.com/mcuadros/ascode/releases/download/{{< param "version" >}}/ascode-{{< param "version" >}}-linux-amd64.tar.gz 23 | tar -xvzf ascode-{{< param "version" >}}-linux-amd64.tar.gz 24 | mv ascode /usr/local/bin/ 25 | ``` 26 | 27 | ### macOS (Darwin) 28 | ```sh 29 | wget https://github.com/mcuadros/ascode/releases/download/{{< param "version" >}}/ascode-{{< param "version" >}}-darwin-amd64.tar.gz 30 | tar -xvzf ascode-{{< param "version" >}}-darwin-amd64.tar.gz 31 | mv ascode /usr/local/bin/ 32 | ``` 33 | 34 | ## Source 35 | 36 | ### Prerequisite Tools 37 | 38 | - [Git](https://git-scm.com/) 39 | - [Go](https://golang.org/) (at least Go 1.12) 40 | 41 | ### Clone from GitHub 42 | 43 | AsCode uses the [Go Modules](https://github.com/golang/go/wiki/Modules), so the easiest way to get started is to clone AsCode in a directory outside of the `$GOPATH`, as in the following example: 44 | 45 | ```sh 46 | git clone https://github.com/mcuadros/ascode.git $HOME/ascode-src 47 | cd $HOME/ascode-src 48 | go install ./... 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /starlark/types/fixtures/modules/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "0.12.23", 4 | "serial": 7, 5 | "lineage": "48134dd4-a519-06f6-25db-30ad46535186", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.moduleA", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "bar", 13 | "provider": "provider.null", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "3670246898818245761", 19 | "triggers": { 20 | "bar": "bar-value" 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | { 27 | "mode": "managed", 28 | "type": "null_resource", 29 | "name": "foo", 30 | "provider": "provider.null", 31 | "instances": [ 32 | { 33 | "schema_version": 0, 34 | "attributes": { 35 | "id": "3013276421099581140", 36 | "triggers": { 37 | "bar": "bar-value", 38 | "foo": "foo-value", 39 | "qux": "qux-value" 40 | } 41 | }, 42 | "dependencies": [ 43 | "module.moduleA.module.moduleB.null_resource.qux", 44 | "module.moduleA.null_resource.bar" 45 | ] 46 | } 47 | ] 48 | }, 49 | { 50 | "module": "module.moduleA.module.moduleB", 51 | "mode": "managed", 52 | "type": "null_resource", 53 | "name": "qux", 54 | "provider": "provider.null", 55 | "instances": [ 56 | { 57 | "schema_version": 0, 58 | "attributes": { 59 | "id": "901669157451578259", 60 | "triggers": { 61 | "qux": "qux-value" 62 | } 63 | } 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /_documentation/starlark/module-execution.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Module execution' 3 | weight: 8 4 | --- 5 | 6 | Each Starlark file defines a _module_, which is a mapping from the 7 | names of global variables to their values. 8 | When a Starlark file is executed, whether directly by the application 9 | or indirectly through a `load` statement, a new Starlark thread is 10 | created, and this thread executes all the top-level statements in the 11 | file. 12 | Because if-statements and for-loops cannot appear outside of a function, 13 | control flows from top to bottom. 14 | 15 | If execution reaches the end of the file, module initialization is 16 | successful. 17 | At that point, the value of each of the module's global variables is 18 | frozen, rendering subsequent mutation impossible. 19 | The module is then ready for use by another Starlark thread, such as 20 | one executing a load statement. 21 | Such threads may access values or call functions defined in the loaded 22 | module. 23 | 24 | A Starlark thread may carry state on behalf of the application into 25 | which it is embedded, and application-defined functions may behave 26 | differently depending on this thread state. 27 | Because module initialization always occurs in a new thread, thread 28 | state is never carried from a higher-level module into a lower-level 29 | one. 30 | The initialization behavior of a module is thus independent of 31 | whichever module triggered its initialization. 32 | 33 | If a Starlark thread encounters an error, execution stops and the error 34 | is reported to the application, along with a backtrace showing the 35 | stack of active function calls at the time of the error. 36 | If an error occurs during initialization of a Starlark module, any 37 | active `load` statements waiting for initialization of the module also 38 | fail. 39 | 40 | Starlark provides no mechanism by which errors can be handled within 41 | the language. 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | GitHub license 7 | 8 | 9 | GitHub tag (latest SemVer) 10 | 11 | 12 | GitHub Action Test 13 | 14 | 15 | Go Report Card 16 | 17 |

18 |
19 | 20 | [**AsCode**](https://ascode.run) is a tool to define infrastructure as code using the [Starlark](https://github.com/google/starlark-go/blob/master/doc/spec.md) language on top of [Terraform](https://github.com/hashicorp/terraform). It allows to describe your infrastructure using an expressive language in Terraform without writing a single line of HCL, meanwhile, you have the complete ecosystem of [providers](https://www.terraform.io/docs/providers/index.html) 21 | 22 |

23 | 24 |

25 | 26 | ## Table of Contents 27 | 28 | - [Quick Install](https://ascode.run/docs/install/) 29 | - [Getting Started](https://ascode.run/docs/getting-started/) 30 | - [Examples](https://ascode.run/docs/example/) 31 | - [GitHub Action](https://ascode.run/docs/github-action/) 32 | 33 | ## License 34 | 35 | GPL-3.0, see [LICENSE](LICENSE) 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mcuadros/ascode 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Masterminds/goutils v1.1.1 // indirect 7 | github.com/Masterminds/semver/v3 v3.1.1 8 | github.com/containers/image/v5 v5.10.5 9 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e // indirect 10 | github.com/fatih/color v1.10.0 // indirect 11 | github.com/gobs/args v0.0.0-20210311043657-b8c0b223be93 12 | github.com/google/uuid v1.2.0 // indirect 13 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect 14 | github.com/hashicorp/errwrap v1.1.0 // indirect 15 | github.com/hashicorp/go-hclog v0.11.0 16 | github.com/hashicorp/go-multierror v1.1.1 // indirect 17 | github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26 18 | github.com/hashicorp/hcl/v2 v2.3.0 19 | github.com/hashicorp/terraform v0.12.23 20 | github.com/imdario/mergo v0.3.12 // indirect 21 | github.com/jessevdk/go-flags v1.4.0 22 | github.com/kr/text v0.2.0 // indirect 23 | github.com/mitchellh/cli v1.1.2 24 | github.com/mitchellh/copystructure v1.1.1 // indirect 25 | github.com/oklog/ulid/v2 v2.0.2 26 | github.com/posener/complete v1.2.3 // indirect 27 | github.com/prometheus/procfs v0.0.5 // indirect 28 | github.com/qri-io/starlib v0.4.2 29 | github.com/sergi/go-diff v1.1.0 // indirect 30 | github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect 31 | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect 32 | github.com/stretchr/testify v1.7.0 33 | github.com/zclconf/go-cty v1.8.1 34 | go.starlark.net v0.0.0-20210406145628-7a1108eaa012 35 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect 36 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect 37 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect 38 | gopkg.in/yaml.v2 v2.4.0 // indirect 39 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 40 | ) 41 | 42 | replace github.com/hashicorp/hcl2 => github.com/mcuadros/hcl2 v0.0.0-20190711172820-dd3dbf62a554 43 | -------------------------------------------------------------------------------- /_examples/runtime.star: -------------------------------------------------------------------------------- 1 | # Runtime Modules 2 | # AsCode comes with a variety of modules available like `http`, `math`, 3 | # `encoding/json`, etc. All this [modules](/docs/reference/) are available 4 | # runtime through the [`load`](/docs/starlark/statements/#load-statements) 5 | # statement. This example shows the usage of this modules and some others. 6 | # 7 | 8 | # ## Basic Module 9 | # The load statement expects at least two arguments; the first is the name of 10 | # the module, and the same the symbol to extract to it. The runtime modules 11 | # always define a symbol called equals to the last part of the module name. 12 | load("encoding/base64", "base64") 13 | load("http", "http") 14 | 15 | # This modules are very usuful to do basic operations such as encoding of 16 | # strings, like in this case to `base64` or to make HTTP requests. 17 | dec = base64.encode("ascode is amazing") 18 | 19 | msg = http.get("https://httpbin.org/base64/%s" % dec) 20 | print(msg.body()) 21 | 22 | # ### Output 23 | """sh 24 | ascode is amazing 25 | """ 26 | 27 | # ## Advanced Modules 28 | # Also, AsCode has some more specif modules, like the `docker` module. The 29 | # docker modules allow you to manipulate docker image names. 30 | load("docker", "docker") 31 | 32 | # A docker image tag can be defined using semver, instead of using the infamous 33 | # 'latest' tag, or fixing a particular version. This allows us to be up-to-date 34 | # without breaking our deployment. 35 | golang = docker.image("golang", "1.13.x") 36 | 37 | # We can use this in the definition of resources, allowing use to upgrade 38 | # the version of our containers in every `terraform apply` 39 | p = tf.provider("docker", "2.7.0") 40 | container = p.resource.container("golang", image=golang.version(full=True)) 41 | 42 | # version queries the docker repository and returns the correct tag. 43 | print(hcl(container)) 44 | 45 | # ### Output 46 | """hcl 47 | resource "docker_container" "foo" { 48 | provider = docker.id_01E4KHW2RSW0FQM93KN5W70Y42 49 | image = "docker.io/library/golang:1.13.9" 50 | } 51 | """ -------------------------------------------------------------------------------- /terraform/plugins_test.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/hashicorp/terraform/plugin/discovery" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestPluginManager_Provider(t *testing.T) { 15 | path, err := ioutil.TempDir("", "provider") 16 | assert.NoError(t, err) 17 | 18 | pm := &PluginManager{Path: path} 19 | cli, meta, err := pm.Provider("github", "2.1.0", false) 20 | assert.NoError(t, err) 21 | assert.NotNil(t, cli) 22 | assert.Equal(t, meta.Version, discovery.VersionStr("2.1.0")) 23 | 24 | cli, meta, err = pm.Provider("github", "2.1.0", true) 25 | assert.NoError(t, err) 26 | assert.NotNil(t, cli) 27 | assert.Equal(t, meta.Version, discovery.VersionStr("2.1.0")) 28 | 29 | cli, meta, err = pm.Provider("github", "2.1.0", false) 30 | assert.NoError(t, err) 31 | assert.NotNil(t, cli) 32 | assert.Equal(t, meta.Version, discovery.VersionStr("2.1.0")) 33 | } 34 | 35 | func TestPluginManager_ProviderDefault(t *testing.T) { 36 | path, err := ioutil.TempDir("", "provider") 37 | assert.NoError(t, err) 38 | 39 | pm := &PluginManager{Path: path} 40 | cli, meta, err := pm.Provider("github", "", false) 41 | assert.NoError(t, err) 42 | assert.NotNil(t, cli) 43 | assert.NotEqual(t, meta.Version, discovery.VersionStr("2.1.0")) 44 | 45 | cli, meta, err = pm.Provider("github", "", true) 46 | assert.NoError(t, err) 47 | assert.NotNil(t, cli) 48 | assert.NotEqual(t, meta.Version, discovery.VersionStr("2.1.0")) 49 | 50 | fmt.Println(meta.Path) 51 | assert.Equal(t, strings.Index(meta.Path, path), 0) 52 | } 53 | 54 | func TestPluginManager_ProvisionerDefault(t *testing.T) { 55 | if os.Getenv("ALLOW_PROVISIONER_SKIP") != "" && !IsTerraformBinaryAvailable() { 56 | t.Skip("terraform binary now available in $PATH") 57 | } 58 | 59 | path, err := ioutil.TempDir("", "provisioner") 60 | assert.NoError(t, err) 61 | 62 | pm := &PluginManager{Path: path} 63 | cli, meta, err := pm.Provisioner("file") 64 | assert.NoError(t, err) 65 | 66 | assert.NotNil(t, cli) 67 | assert.Equal(t, strings.Index(meta.Path, "terraform-TFSPACE-internal-plugin-"), 0) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /_scripts/example-to-md.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func main() { 13 | md, err := exampleToMD(os.Args[1], os.Args[2]) 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | fmt.Println(md) 19 | } 20 | 21 | func exampleToMD(filename string, weight string) (string, error) { 22 | b := bytes.NewBuffer(nil) 23 | f, err := os.Open(filename) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | var isComment, isCodeBlock, isPrint bool 29 | var preLine string 30 | scanner := bufio.NewScanner(f) 31 | for scanner.Scan() { 32 | 33 | line := scanner.Text() 34 | curIsComment := len(line) > 0 && line[0] == '#' 35 | if isCodeBlock && len(preLine) == 0 && curIsComment { 36 | isPrint = false 37 | } 38 | 39 | if len(preLine) >= 3 && preLine[:3] == `"""` { 40 | isPrint = false 41 | } 42 | 43 | if isPrint { 44 | 45 | preLine = strings.Trim(preLine, "#") 46 | fmt.Fprintln(b, preLine) 47 | isPrint = false 48 | } 49 | 50 | preLine = line 51 | if curIsComment { 52 | line = strings.TrimSpace(line[1:]) 53 | 54 | isComment = true 55 | if isCodeBlock { 56 | fmt.Fprintln(b, "```\n\n") 57 | isCodeBlock = false 58 | } 59 | 60 | if b.Len() == 0 { 61 | fmt.Fprintf(b, 62 | "---\ntitle: '%s'\nweight: %s\n---\n\n", 63 | line, weight, 64 | ) 65 | continue 66 | } 67 | 68 | isPrint = true 69 | continue 70 | } 71 | 72 | if len(line) == 0 && isComment { 73 | isPrint = true 74 | continue 75 | } 76 | 77 | if isComment { 78 | isComment = false 79 | isCodeBlock = true 80 | 81 | if len(line) >= 3 && line[:3] == `"""` { 82 | fmt.Fprintln(b, "```"+line[3:]) 83 | continue 84 | } 85 | 86 | fmt.Fprintln(b, "\n\n```python") 87 | 88 | } 89 | 90 | isPrint = true 91 | } 92 | 93 | if isCodeBlock { 94 | fmt.Fprintln(b, "```") 95 | } 96 | 97 | if err := scanner.Err(); err != nil { 98 | fmt.Fprintln(os.Stderr, "reading standard input:", err) 99 | } 100 | 101 | return b.String(), nil 102 | } 103 | -------------------------------------------------------------------------------- /starlark/types/testdata/hcl.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | helm = tf.provider("helm", "1.0.0", "default") 4 | helm.kubernetes.token = "foo" 5 | 6 | # hcl 7 | assert.eq(hcl(helm), "" + 8 | 'provider "helm" {\n' + \ 9 | ' alias = "default"\n' + \ 10 | ' version = "1.0.0"\n\n' + \ 11 | ' kubernetes {\n' + \ 12 | ' token = "foo"\n' + \ 13 | ' }\n' + \ 14 | '}\n\n') 15 | 16 | google = tf.provider("google", "3.16.0", "default") 17 | sa = google.resource.service_account("sa") 18 | sa.account_id = "service-account" 19 | 20 | m = google.resource.storage_bucket_iam_member(sa.account_id+"-admin") 21 | m.bucket = "main-storage" 22 | m.role = "roles/storage.objectAdmin" 23 | m.member = "serviceAccount:%s" % sa.email 24 | 25 | addr = google.resource.compute_global_address("test") 26 | addr.purpose = "VPC_PEERING" 27 | addr.address_type = "INTERNAL" 28 | addr.prefix_length = 16 29 | 30 | # hcl with interpoaltion 31 | assert.eq(hcl(google), "" + 32 | 'provider "google" {\n' + \ 33 | ' alias = "default"\n' + \ 34 | ' version = "3.16.0"\n' + \ 35 | '}\n' + \ 36 | '\n' + \ 37 | 'resource "google_compute_global_address" "test" {\n' + \ 38 | ' provider = google.default\n' + \ 39 | ' address_type = "INTERNAL"\n' + \ 40 | ' prefix_length = 16\n' + \ 41 | ' purpose = "VPC_PEERING"\n' + \ 42 | '}\n' + \ 43 | '\n' + \ 44 | 'resource "google_service_account" "sa" {\n' + \ 45 | ' provider = google.default\n' + \ 46 | ' account_id = "service-account"\n' + \ 47 | '}\n' + \ 48 | '\n' + \ 49 | 'resource "google_storage_bucket_iam_member" "service-account-admin" {\n' + \ 50 | ' provider = google.default\n' + \ 51 | ' bucket = "main-storage"\n' + \ 52 | ' member = "serviceAccount:${google_service_account.sa.email}"\n' + \ 53 | ' role = "roles/storage.objectAdmin"\n' + \ 54 | '}\n\n') 55 | 56 | # hcl with prefixed provider 57 | google = tf.provider("google", "3.16.0", "alias") 58 | google.set_prefix(True) 59 | 60 | sa = google.resource.service_account("sa") 61 | sa.account_id = "service-account" 62 | assert.eq(hcl(google), "" + 63 | 'provider "google" {\n' + \ 64 | ' alias = "alias"\n' + \ 65 | ' version = "3.16.0"\n' + \ 66 | '}\n' + \ 67 | '\n' + \ 68 | 'resource "google_service_account" "alias-sa" {\n' + \ 69 | ' provider = google.alias\n' + \ 70 | ' account_id = "service-account"\n' + \ 71 | '}\n\n') -------------------------------------------------------------------------------- /starlark/types/evaluate.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "go.starlark.net/starlark" 8 | "go.starlark.net/starlarkstruct" 9 | ) 10 | 11 | // BuiltinEvaluate returns a starlak.Builtin function to evalute Starlark files. 12 | // 13 | // outline: types 14 | // functions: 15 | // evaluate(filename, predeclared=None) dict 16 | // Evaluates a Starlark file and returns its global context. Kwargs may 17 | // be used to set predeclared. 18 | // examples: 19 | // evaluable.star 20 | // evaluate.star 21 | // params: 22 | // filename string 23 | // Name of the file to execute. 24 | // predeclared dict 25 | // Defines the predeclared context for the execution. Execution does 26 | // not modify this dictionary 27 | // 28 | func BuiltinEvaluate(predeclared starlark.StringDict) starlark.Value { 29 | return starlark.NewBuiltin("evaluate", func(t *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 30 | var raw starlark.String 31 | 32 | switch len(args) { 33 | case 2: 34 | dict, ok := args.Index(1).(*starlark.Dict) 35 | if !ok { 36 | return nil, fmt.Errorf("expected dict, got %s", args.Index(1).Type()) 37 | } 38 | 39 | for i, key := range dict.Keys() { 40 | if _, ok := key.(starlark.String); ok { 41 | continue 42 | } 43 | 44 | return nil, fmt.Errorf("expected string keys in dict, got %s at index %d", key.Type(), i) 45 | } 46 | 47 | kwargs = append(dict.Items(), kwargs...) 48 | fallthrough 49 | case 1: 50 | var ok bool 51 | raw, ok = args.Index(0).(starlark.String) 52 | if !ok { 53 | return nil, fmt.Errorf("expected string, got %s", args.Index(0).Type()) 54 | } 55 | default: 56 | return nil, fmt.Errorf("unexpected positional arguments count") 57 | } 58 | 59 | for _, kwarg := range kwargs { 60 | predeclared[kwarg.Index(0).(starlark.String).GoString()] = kwarg.Index(1) 61 | } 62 | 63 | filename := raw.GoString() 64 | if base, ok := t.Local("base_path").(string); ok { 65 | filename = filepath.Join(base, filename) 66 | } 67 | 68 | _, file := filepath.Split(filename) 69 | name := file[:len(file)-len(filepath.Ext(file))] 70 | 71 | global, err := starlark.ExecFile(t, filename, nil, predeclared) 72 | return &starlarkstruct.Module{ 73 | Name: name, 74 | Members: global, 75 | }, err 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/hashicorp/hcl/v2/hclwrite" 9 | "github.com/jessevdk/go-flags" 10 | "go.starlark.net/starlark" 11 | ) 12 | 13 | // Command descriptions used in the flags.Parser.AddCommand. 14 | const ( 15 | RunCmdShortDescription = "Run parses, resolves, and executes a Starlark file." 16 | RunCmdLongDescription = RunCmdShortDescription + "\n\n" + 17 | "When a provider is instantiated is automatically installed, at the \n" + 18 | "default location (~/.terraform.d/plugins), this can be overrided \n" + 19 | "using the flag `--plugin-dir=`. \n\n" + 20 | "The Starlark file can be \"transpiled\" to a HCL file using the flag \n" + 21 | "`--to-hcl=`. This file can be used directly with Terraform init \n" + 22 | "and plan commands.\n" 23 | ) 24 | 25 | // RunCmd implements the command `run`. 26 | type RunCmd struct { 27 | commonCmd 28 | 29 | ToHCL string `long:"to-hcl" description:"dumps resources to a hcl file"` 30 | PrintHCL bool `long:"print-hcl" description:"prints resources to a hcl file"` 31 | NoValidate bool `long:"no-validate" description:"skips the validation of the resources"` 32 | PositionalArgs struct { 33 | File string `positional-arg-name:"file" description:"starlark source file"` 34 | } `positional-args:"true" required:"1"` 35 | } 36 | 37 | // Execute honors the flags.Commander interface. 38 | func (c *RunCmd) Execute(args []string) error { 39 | c.init() 40 | 41 | _, err := c.runtime.ExecFile(c.PositionalArgs.File) 42 | if err != nil { 43 | if err, ok := err.(*starlark.EvalError); ok { 44 | fmt.Println(err.Backtrace()) 45 | os.Exit(1) 46 | return nil 47 | } 48 | 49 | return err 50 | } 51 | 52 | c.validate() 53 | return c.dumpToHCL() 54 | } 55 | 56 | func (c *RunCmd) validate() { 57 | if c.NoValidate { 58 | return 59 | } 60 | 61 | errs := c.runtime.Terraform.Validate() 62 | for _, err := range errs { 63 | fmt.Fprintln(os.Stderr, err) 64 | } 65 | 66 | if len(errs) != 0 { 67 | os.Exit(1) 68 | } 69 | } 70 | 71 | func (c *RunCmd) dumpToHCL() error { 72 | if c.ToHCL == "" && !c.PrintHCL { 73 | return nil 74 | } 75 | 76 | f := hclwrite.NewEmptyFile() 77 | c.runtime.Terraform.ToHCL(f.Body()) 78 | 79 | if c.PrintHCL { 80 | os.Stdout.Write(f.Bytes()) 81 | } 82 | 83 | if c.ToHCL == "" { 84 | return nil 85 | } 86 | 87 | return ioutil.WriteFile(c.ToHCL, f.Bytes(), 0644) 88 | } 89 | 90 | var _ flags.Commander = &RunCmd{} 91 | -------------------------------------------------------------------------------- /starlark/types/testdata/hcl_integration.star: -------------------------------------------------------------------------------- 1 | load("os", "os") 2 | load("assert.star", "assert") 3 | 4 | aws = tf.provider("aws", "2.13.0") 5 | aws.region = "us-west-2" 6 | 7 | # Based on: 8 | # https://www.terraform.io/docs/providers/aws/r/instance.html#example 9 | vpc = aws.resource.vpc() 10 | vpc.cidr_block = "172.16.0.0/16" 11 | vpc.tags = {"Name": "tf-example"} 12 | 13 | subnet = aws.resource.subnet() 14 | subnet.vpc_id = vpc.id 15 | subnet.cidr_block = "172.16.0.0/24" 16 | subnet.availability_zone = "us-west-2a" 17 | subnet.tags = {"Name": "tf-example"} 18 | 19 | iface = aws.resource.network_interface() 20 | iface.subnet_id = subnet.id 21 | iface.private_ips = ["172.16.10.100"] 22 | iface.tags = {"Name": "primary_network_iterface"} 23 | 24 | ubuntu = aws.data.ami() 25 | ubuntu.most_recent = True 26 | ubuntu.filter(name = "name", values = ["ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"]) 27 | ubuntu.filter(name = "virtualization-type", values = ["hvm"]) 28 | ubuntu.owners = ["099720109477"] 29 | 30 | instance = aws.resource.instance() 31 | instance.ami = ubuntu.id 32 | instance.instance_type = "t2.micro" 33 | instance.credit_specification.cpu_credits = "unlimited" 34 | instance.network_interface = [{ 35 | "network_interface_id": iface.id, 36 | "device_index": 0 37 | }] 38 | 39 | # Based on: 40 | # https://www.terraform.io/docs/providers/aws/r/autoscaling_group.html#mixed-instances-policy 41 | template = aws.resource.launch_template() 42 | template.name_prefix = "example" 43 | template.image_id = ubuntu.id 44 | template.instance_type = "c5.large" 45 | 46 | group = aws.resource.autoscaling_group() 47 | group.availability_zones = ["us-east-1a"] 48 | group.min_size = 1 49 | group.max_size = 1 50 | group.desired_capacity = 1 51 | group.mixed_instances_policy = { 52 | "launch_template": { 53 | "launch_template_specification": { 54 | "launch_template_id": template.id, 55 | }, 56 | "override": [ 57 | {"instance_type": "c4.large"}, 58 | {"instance_type": "c3.large"} 59 | ], 60 | }, 61 | } 62 | 63 | # Based on: 64 | # https://learn.hashicorp.com/terraform/getting-started/dependencies.html#implicit-and-explicit-dependencies 65 | bucket = aws.resource.s3_bucket() 66 | bucket.bucket = "terraform-getting-started-guide" 67 | bucket.acl = "private" 68 | 69 | example = aws.resource.instance() 70 | example.ami = "ami-2757f631" 71 | example.instance_type = "t2.micro" 72 | example.depends_on(bucket) 73 | 74 | assert.eq(hcl(tf).replace("\r", ""), os.read_file("fixtures/aws.tf").replace("\r", "")) 75 | -------------------------------------------------------------------------------- /_documentation/github-action.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'GitHub Action' 3 | weight: 40 4 | --- 5 | 6 | AsCode Github Action allows to execute AsCode `run` command in response to a GitHub event such as updating a pull request or pushing a new commit to a specific branch. 7 | 8 | This used in combination with the [Terraform GitHub Actions](https://www.terraform.io/docs/github-actions/getting-started.html) allows to execute the different terraform commands `init`, `plan` and `apply` inside of a [GitHub Workflow](https://help.github.com/en/actions/configuring-and-managing-workflows). 9 | 10 | ## Parameters 11 | 12 | | Parameter | **Mandatory**/**Optional** | Description | 13 | | --------- | -------- | ----------- | 14 | | file | **Mandatory** | Starlark file to execute. Default value: `main.star` | 15 | | hcl | **Mandatory** | HCL output file. Default value: `generated.tf` | 16 | 17 | ## Recommended Workflow 18 | 19 | 20 | ```yaml 21 | name: 'Terraform & AsCode' 22 | on: 23 | push: 24 | branches: 25 | - master 26 | pull_request: 27 | 28 | jobs: 29 | terraform: 30 | name: 'Deploy' 31 | runs-on: ubuntu-latest 32 | env: 33 | TF_VERSION: latest 34 | TF_WORKING_DIR: . 35 | steps: 36 | - name: 'Checkout' 37 | uses: actions/checkout@master 38 | 39 | - name: 'AsCode Run' 40 | uses: mcuadros/ascode@gh-action 41 | 42 | - name: 'Terraform Init' 43 | uses: hashicorp/terraform-github-actions@master 44 | with: 45 | tf_actions_version: ${{ env.TF_VERSION }} 46 | tf_actions_subcommand: 'init' 47 | tf_actions_working_dir: ${{ env.TF_WORKING_DIR }} 48 | tf_actions_comment: true 49 | 50 | - name: 'Terraform Validate' 51 | uses: hashicorp/terraform-github-actions@master 52 | with: 53 | tf_actions_version: ${{ env.TF_VERSION }} 54 | tf_actions_subcommand: 'validate' 55 | tf_actions_working_dir: ${{ env.TF_WORKING_DIR }} 56 | tf_actions_comment: true 57 | 58 | - name: 'Terraform Plan' 59 | uses: hashicorp/terraform-github-actions@master 60 | with: 61 | tf_actions_version: ${{ env.TF_VERSION }} 62 | tf_actions_subcommand: 'plan' 63 | tf_actions_working_dir: ${{ env.TF_WORKING_DIR }} 64 | tf_actions_comment: true 65 | 66 | - name: 'Terraform Apply' 67 | uses: hashicorp/terraform-github-actions@master 68 | if: github.event_name == 'push' 69 | with: 70 | tf_actions_version: ${{ env.TF_VERSION }} 71 | tf_actions_subcommand: 'apply' 72 | tf_actions_working_dir: ${{ env.TF_WORKING_DIR }} 73 | ``` -------------------------------------------------------------------------------- /starlark/types/testdata/provider.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | p = tf.provider("aws", "2.13.0") 4 | assert.eq(p.__kind__, "provider") 5 | assert.eq(p.__type__, "aws") 6 | assert.eq(p.__name__, "id_1") 7 | assert.eq(p.__version__, "2.13.0") 8 | 9 | # attr names 10 | assert.eq("__provider__" in dir(p), False) 11 | assert.eq("depends_on" in dir(p), False) 12 | assert.eq("add_provisioner" in dir(p), False) 13 | assert.eq("__version__" in dir(p), True) 14 | assert.eq("data" in dir(p), True) 15 | assert.eq("resource" in dir(p), True) 16 | 17 | # attr 18 | assert.eq(len(dir(p.data)), 133) 19 | assert.eq(len(dir(p.resource)), 508) 20 | 21 | resources = dir(p.resource) 22 | assert.contains(resources, "instance") 23 | 24 | # types 25 | assert.eq(type(p), "Provider") 26 | assert.eq(type(p.resource), "ResourceCollectionGroup") 27 | assert.eq(type(p.resource.instance), "ResourceCollection") 28 | assert.eq(type(p.resource.instance()), "Resource") 29 | assert.eq(type(p.data.ami().filter()), "Resource") 30 | 31 | # string 32 | assert.eq(str(p), "Provider") 33 | assert.eq(str(p.resource), "ResourceCollectionGroup") 34 | assert.eq(str(p.resource.instance), "ResourceCollection") 35 | assert.eq(str(p.resource.instance()), "Resource") 36 | assert.eq(str(p.data.ami().filter()), "Resource") 37 | 38 | 39 | assert.eq(len(p.resource.instance), 2) 40 | 41 | p.region = "us-west-2" 42 | assert.eq(p.region, "us-west-2") 43 | 44 | alias = tf.provider("aws", "2.13.0", "alias") 45 | assert.eq(alias.__name__, "alias") 46 | assert.eq(alias.__version__, "2.13.0") 47 | 48 | kwargs = tf.provider("aws", region="foo") 49 | assert.eq(kwargs.region, "foo") 50 | 51 | # ResourceCollectionGroup 52 | assert.eq("__kind__" in dir(p.resource), True) 53 | assert.eq(p.resource.__kind__, "resource") 54 | assert.eq("__provider__" in dir(p.resource), True) 55 | assert.eq(p.resource.__provider__, p) 56 | 57 | # compare 58 | assert.ne(p, kwargs) 59 | assert.ne(p, kwargs) 60 | 61 | foo = p.resource.instance("foo", ami="valueA") 62 | bar = p.resource.instance("bar", ami="valueA", disable_api_termination=False) 63 | qux = p.resource.instance("qux", ami="valueB", disable_api_termination=True) 64 | 65 | result = p.resource.instance.search("id", "foo") 66 | assert.eq(len(result), 1) 67 | assert.eq(result[0], foo) 68 | 69 | assert.eq(len(p.resource.instance.search("ami", "valueA")), 2) 70 | assert.eq(len(p.resource.instance.search("disable_api_termination", True)), 1) 71 | assert.eq(len(p.resource.instance.search("disable_api_termination", False)), 1) 72 | 73 | assert.eq(p.resource.instance.search("foo")[0], foo) 74 | 75 | -------------------------------------------------------------------------------- /starlark/types/fixtures/aws.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | alias = "id_1" 3 | version = "2.13.0" 4 | region = "us-west-2" 5 | } 6 | 7 | data "aws_ami" "id_5" { 8 | provider = aws.id_1 9 | most_recent = true 10 | owners = ["099720109477"] 11 | 12 | filter { 13 | name = "name" 14 | values = ["ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"] 15 | } 16 | 17 | filter { 18 | name = "virtualization-type" 19 | values = ["hvm"] 20 | } 21 | } 22 | 23 | resource "aws_autoscaling_group" "id_8" { 24 | provider = aws.id_1 25 | availability_zones = ["us-east-1a"] 26 | desired_capacity = 1 27 | max_size = 1 28 | min_size = 1 29 | 30 | mixed_instances_policy { 31 | launch_template { 32 | launch_template_specification { 33 | launch_template_id = "${aws_launch_template.id_7.id}" 34 | } 35 | 36 | override { 37 | instance_type = "c4.large" 38 | } 39 | 40 | override { 41 | instance_type = "c3.large" 42 | } 43 | } 44 | } 45 | } 46 | 47 | resource "aws_instance" "id_6" { 48 | provider = aws.id_1 49 | ami = "${data.aws_ami.id_5.id}" 50 | instance_type = "t2.micro" 51 | 52 | credit_specification { 53 | cpu_credits = "unlimited" 54 | } 55 | 56 | network_interface { 57 | device_index = 0 58 | network_interface_id = "${aws_network_interface.id_4.id}" 59 | } 60 | } 61 | 62 | resource "aws_instance" "id_10" { 63 | provider = aws.id_1 64 | ami = "ami-2757f631" 65 | instance_type = "t2.micro" 66 | depends_on = [aws_s3_bucket.id_9] 67 | } 68 | 69 | resource "aws_launch_template" "id_7" { 70 | provider = aws.id_1 71 | image_id = "${data.aws_ami.id_5.id}" 72 | instance_type = "c5.large" 73 | name_prefix = "example" 74 | } 75 | 76 | resource "aws_network_interface" "id_4" { 77 | provider = aws.id_1 78 | private_ips = ["172.16.10.100"] 79 | subnet_id = "${aws_subnet.id_3.id}" 80 | tags = { Name = "primary_network_iterface" } 81 | } 82 | 83 | resource "aws_s3_bucket" "id_9" { 84 | provider = aws.id_1 85 | acl = "private" 86 | bucket = "terraform-getting-started-guide" 87 | } 88 | 89 | resource "aws_subnet" "id_3" { 90 | provider = aws.id_1 91 | availability_zone = "us-west-2a" 92 | cidr_block = "172.16.0.0/24" 93 | tags = { Name = "tf-example" } 94 | vpc_id = "${aws_vpc.id_2.id}" 95 | } 96 | 97 | resource "aws_vpc" "id_2" { 98 | provider = aws.id_1 99 | cidr_block = "172.16.0.0/16" 100 | tags = { Name = "tf-example" } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /starlark/types/testdata/attribute.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | aws = tf.provider("aws", "2.13.0") 4 | 5 | ami = aws.data.ami() 6 | 7 | # attribute of scalar 8 | web = aws.resource.instance() 9 | web.ami = ami.id 10 | assert.eq(type(web.ami), "Attribute") 11 | assert.eq(str(web.ami), "${data.aws_ami.id_2.id}") 12 | assert.eq(web.ami.__resource__, ami) 13 | assert.eq(web.ami.__type__, "string") 14 | 15 | # attr names 16 | assert.eq("__resource__" in dir(web.ami), True) 17 | assert.eq("__type__" in dir(web.ami), True) 18 | 19 | # attribute of set 20 | table = aws.data.dynamodb_table() 21 | assert.eq(str(table.ttl), "${data.aws_dynamodb_table.id_4.ttl}") 22 | assert.eq(str(table.ttl[0]), "${data.aws_dynamodb_table.id_4.ttl.0}") 23 | assert.eq(str(table.ttl[0].attribute_name), "${data.aws_dynamodb_table.id_4.ttl.0.attribute_name}") 24 | 25 | # attribute of list 26 | instance = aws.data.instance() 27 | assert.eq(str(instance.credit_specification), "${data.aws_instance.id_5.credit_specification}") 28 | assert.eq(str(instance.credit_specification[0]), "${data.aws_instance.id_5.credit_specification.0}") 29 | assert.eq(str(instance.credit_specification[0].cpu_credits), "${data.aws_instance.id_5.credit_specification.0.cpu_credits}") 30 | 31 | # attribute of block 32 | attribute = str(aws.resource.instance().root_block_device.volume_size) 33 | assert.eq(attribute, "${aws_instance.id_6.root_block_device.0.volume_size}") 34 | 35 | # attribute on data source 36 | assert.eq(str(aws.resource.instance().id), "${aws_instance.id_7.id}") 37 | 38 | # attribute on resource 39 | assert.eq(str(aws.data.ami().id), "${data.aws_ami.id_8.id}") 40 | 41 | gcp = tf.provider("google", "3.13.0") 42 | 43 | # attribute on list with MaxItem:1 44 | cluster = gcp.resource.container_cluster("foo") 45 | assert.eq(str(cluster.master_auth.client_certificate), "${google_container_cluster.foo.master_auth.0.client_certificate}") 46 | 47 | # attr non-object 48 | assert.fails(lambda: web.ami.foo, "Attribute it's not a object") 49 | 50 | # fn wrapping 51 | assert.eq(str(fn("base64encode", web.ami)), "${base64encode(data.aws_ami.id_2.id)}") 52 | 53 | # attribute of dict 54 | k8s = tf.provider("kubernetes") 55 | 56 | secret = k8s.data.secret("foo") 57 | assert.eq(str(secret.data["qux"]), "${data.kubernetes_secret.foo.data.qux}") 58 | assert.eq(str(secret.data["qux"][0]), "${data.kubernetes_secret.foo.data.qux.0}") 59 | 60 | # ref 61 | secret = k8s.resource.secret("foo") 62 | secret.metadata.name = "foo" 63 | secret.type = "kubernetes.io/dockerconfigjson" 64 | assert.eq(str(ref(secret, "type")), "${kubernetes_secret.foo.type}") 65 | assert.eq(str(ref(secret.metadata, "name")), "${kubernetes_secret.foo.metadata.0.name}") 66 | 67 | # ref to non-exits 68 | assert.fails(lambda: ref(secret, "foo"), "Resource has no .foo field") -------------------------------------------------------------------------------- /_documentation/starlark/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Language definition' 3 | weight: 10 4 | --- 5 | 6 | Starlark is a dialect of Python intended for use as a configuration 7 | language. A Starlark interpreter is typically embedded within a larger 8 | application, and this application may define additional 9 | domain-specific functions and data types beyond those provided by the 10 | core language. For example, Starlark is embedded within (and was 11 | originally developed for) the [Bazel build tool](https://bazel.build), 12 | and [Bazel's build language](https://docs.bazel.build/versions/2.0.0/skylark/language.html) is based on Starlark. 13 | 14 | This document describes the Go implementation of Starlark 15 | at go.starlark.net/starlark. 16 | The language it defines is similar but not identical to 17 | [the Java-based implementation](https://github.com/bazelbuild/starlark) 18 | used by Bazel. 19 | We identify places where their behaviors differ, and an 20 | [appendix](/docs/starlark/dialect-differences/) provides a summary of those 21 | differences. 22 | We plan to converge both implementations on a single specification. 23 | 24 | This document is maintained by Alan Donovan . 25 | It was influenced by the Python specification, 26 | Copyright 1990–2017, Python Software Foundation, 27 | and the Go specification, Copyright 2009–2017, The Go Authors. 28 | 29 | Starlark was designed and implemented in Java by Laurent Le Brun, 30 | Dmitry Lomov, Jon Brandvin, and Damien Martin-Guillerez, standing on 31 | the shoulders of the Python community. 32 | The Go implementation was written by Alan Donovan and Jay Conrod; 33 | its scanner was derived from one written by Russ Cox. 34 | 35 | ## Overview 36 | 37 | Starlark is an untyped dynamic language with high-level data types, 38 | first-class functions with lexical scope, and automatic memory 39 | management or _garbage collection_. 40 | 41 | Starlark is strongly influenced by Python, and is almost a subset of 42 | that language. In particular, its data types and syntax for 43 | statements and expressions will be very familiar to any Python 44 | programmer. 45 | However, Starlark is intended not for writing applications but for 46 | expressing configuration: its programs are short-lived and have no 47 | external side effects and their main result is structured data or side 48 | effects on the host application. 49 | As a result, Starlark has no need for classes, exceptions, reflection, 50 | concurrency, and other such features of Python. 51 | 52 | Starlark execution is _deterministic_: all functions and operators 53 | in the core language produce the same execution each time the program 54 | is run; there are no sources of random numbers, clocks, or unspecified 55 | iterators. This makes Starlark suitable for use in applications where 56 | reproducibility is paramount, such as build tools. 57 | -------------------------------------------------------------------------------- /_documentation/reference/reference.md.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "functionName" }} 2 | {{- if ne .Receiver "" -}} 3 | # 4 | {{- end -}} 5 | ### def {{ if ne .Receiver "" -}} 6 | {{ .Receiver }}. 7 | {{- end -}} 8 | {{- (index (split .Signature "(") 0) -}} 9 | {{- end -}} 10 | {{- define "function" }} 11 | 12 | {{ template "functionName" . }} 13 | ```go 14 | {{if ne .Receiver "" -}}{{.Receiver}}.{{- end }}{{ .Signature }} 15 | ``` 16 | 17 | {{- if ne .Description "" }} 18 | {{ .Description }} 19 | {{- end -}} 20 | 21 | 22 | {{- if gt (len .Params) 0 }} 23 | 24 | ###### Arguments 25 | 26 | | name | type | description | 27 | |------|------|-------------| 28 | {{ range .Params -}} 29 | | `{{ .Name }}` | `{{ .Type }}` | {{ (replace_all .Description "\n" "") }} | 30 | {{ end -}} 31 | 32 | {{- end -}} 33 | 34 | {{- if gt (len .Examples) 0 }} 35 | ###### Examples 36 | {{ range .Examples -}} 37 | {{ .Description }} 38 | ```python 39 | {{ .Code }} 40 | ``` 41 | {{ end -}} 42 | 43 | {{- end -}} 44 | 45 | {{- end -}} 46 | {{- define "indexFunctionName" -}} 47 | {{- $receiver := "" -}} 48 | {{ if ne .Receiver "types" -}}{{ $receiver = printf "%s." .Receiver }}{{- end -}} 49 | {{- $name := printf "def %s" (index (split .Signature "(") 0) -}} 50 | {{- $anchor := printf "def %s%s" $receiver (index (split .Signature "(") 0) -}} 51 | * [{{ $name }}({{ (index (split .Signature "(") 1) }}](#{{ sanitizeAnchor $anchor }}) 52 | {{- end -}} 53 | 54 | {{- range . -}} 55 | --- 56 | title: "{{ .Path }}" 57 | summary: "{{ (replace_all .Description "\n" "") }}" 58 | toc: true 59 | --- 60 | 61 | ## Overview 62 | {{ if ne .Description "" }}{{ .Description }}{{ end }} 63 | 64 | {{- if gt (len .Functions) 0 }} 65 | ## Functions 66 | {{ range .Functions -}} 67 | {{ template "function" . }} 68 | {{ end -}} 69 | {{- end }} 70 | 71 | {{ if gt (len .Types) 0 }} 72 | ## Types 73 | {{ range .Types -}} 74 | 75 | 76 | ### type {{ .Name }} 77 | {{ if ne .Description "" }}{{ .Description }}{{ end -}} 78 | {{ if gt (len .Fields) 0 }} 79 | 80 | ###### Properties 81 | 82 | | name | type | description | 83 | |------|------|-------------| 84 | {{ range .Fields -}} 85 | | `{{ .Name }}` | `{{ .Type }}` | {{ (replace_all .Description "\n" "") }} | 86 | {{ end -}} 87 | 88 | {{ end }} 89 | 90 | {{ if gt (len .Examples) 0 }} 91 | ###### Examples 92 | {{ range .Examples -}} 93 | {{ .Description }} 94 | ```python 95 | {{ .Code }} 96 | ``` 97 | {{ end -}} 98 | 99 | 100 | {{ end -}} 101 | 102 | {{ if gt (len .Methods) 0 }} 103 | 104 | ###### Methods 105 | 106 | {{- range .Methods -}} 107 | {{ template "function" . }} 108 | {{ end -}} 109 | {{- if gt (len .Operators) 0 }} 110 | 111 | ###### Operators 112 | 113 | | operator | description | 114 | |----------|-------------| 115 | {{ range .Operators -}} 116 | | {{ .Opr }} | {{ .Description }} | 117 | {{ end }} 118 | 119 | {{ end }} 120 | 121 | {{ end }} 122 | {{- end -}} 123 | {{- end -}} 124 | {{ end }} -------------------------------------------------------------------------------- /starlark/types/terraform.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/terraform/version" 7 | "github.com/mcuadros/ascode/terraform" 8 | "go.starlark.net/starlark" 9 | ) 10 | 11 | // Terraform is a representation of Terraform as a starlark.Value 12 | // 13 | // outline: types 14 | // types: 15 | // Terraform 16 | // Terraform holds all the configuration defined by a script. A global 17 | // variable called `tf` contains a unique instance of Terraform. 18 | // 19 | // examples: 20 | // tf_overview.star 21 | // 22 | // fields: 23 | // version string 24 | // Terraform version. 25 | // backend Backend 26 | // Backend used to store the state, if None a `local` backend it's 27 | // used. 28 | // provider ProviderCollection 29 | // Dict with all the providers defined by provider type. 30 | // 31 | type Terraform struct { 32 | b *Backend 33 | p *ProviderCollection 34 | } 35 | 36 | // NewTerraform returns a new instance of Terraform 37 | func NewTerraform(pm *terraform.PluginManager) *Terraform { 38 | return &Terraform{ 39 | p: NewProviderCollection(pm), 40 | } 41 | } 42 | 43 | var _ starlark.Value = &Terraform{} 44 | var _ starlark.HasAttrs = &Terraform{} 45 | var _ starlark.HasSetField = &Terraform{} 46 | 47 | // Attr honors the starlark.HasAttrs interface. 48 | func (t *Terraform) Attr(name string) (starlark.Value, error) { 49 | switch name { 50 | case "version": 51 | return starlark.String(version.String()), nil 52 | case "provider": 53 | return t.p, nil 54 | case "backend": 55 | if t.b == nil { 56 | return starlark.None, nil 57 | } 58 | 59 | return t.b, nil 60 | } 61 | 62 | return starlark.None, nil 63 | } 64 | 65 | // SetField honors the starlark.HasSetField interface. 66 | func (t *Terraform) SetField(name string, val starlark.Value) error { 67 | if name != "backend" { 68 | errmsg := fmt.Sprintf("terraform has no .%s field or method", name) 69 | return starlark.NoSuchAttrError(errmsg) 70 | } 71 | 72 | if b, ok := val.(*Backend); ok { 73 | t.b = b 74 | return nil 75 | } 76 | 77 | return fmt.Errorf("unexpected value %s at %s", val.Type(), name) 78 | } 79 | 80 | // AttrNames honors the starlark.HasAttrs interface. 81 | func (t *Terraform) AttrNames() []string { 82 | return []string{"provider", "backend", "version"} 83 | } 84 | 85 | // Freeze honors the starlark.Value interface. 86 | func (t *Terraform) Freeze() {} // immutable 87 | 88 | // Hash honors the starlark.Value interface. 89 | func (t *Terraform) Hash() (uint32, error) { 90 | return 0, fmt.Errorf("unhashable type: %s", t.Type()) 91 | } 92 | 93 | // String honors the starlark.Value interface. 94 | func (t *Terraform) String() string { 95 | return "terraform" 96 | } 97 | 98 | // Truth honors the starlark.Value interface. 99 | func (t *Terraform) Truth() starlark.Bool { 100 | return t.p.Len() != 0 101 | } 102 | 103 | // Type honors the starlark.Value interface. 104 | func (t *Terraform) Type() string { 105 | return "Terraform" 106 | } 107 | -------------------------------------------------------------------------------- /_scripts/goldflags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/go-git/go-git/v5" 13 | "github.com/go-git/go-git/v5/plumbing" 14 | "github.com/rogpeppe/go-internal/modfile" 15 | ) 16 | 17 | func main() { 18 | pkg := os.Args[1] 19 | path := os.Args[2] 20 | 21 | f, err := readGoMod(path) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | flags, err := getFlags(f, path, os.Args[3:]) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | if pkg != "main" { 32 | pkg = filepath.Join(f.Module.Mod.Path, pkg) 33 | } 34 | 35 | fmt.Printf(renderLDFLAGS(pkg, flags)) 36 | } 37 | 38 | func getFlags(f *modfile.File, path string, pkgs []string) (map[string]string, error) { 39 | var err error 40 | flags := make(map[string]string, 0) 41 | flags["build"] = time.Now().Format(time.RFC3339) 42 | flags["version"], flags["commit"], err = readVersion(path) 43 | 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | for _, v := range pkgs { 49 | parts := strings.SplitN(v, "=", 2) 50 | key := parts[0] 51 | pkg := parts[1] 52 | 53 | flags[key] = getPackageVersion(f, pkg) 54 | } 55 | 56 | return flags, nil 57 | } 58 | 59 | func readVersion(path string) (string, string, error) { 60 | r, err := git.PlainOpen(path) 61 | if err != nil { 62 | return "", "", err 63 | } 64 | 65 | ref, err := r.Head() 66 | if err != nil { 67 | return "", "", err 68 | } 69 | 70 | if !ref.Name().IsBranch() { 71 | ref, err = findTag(r, ref.Hash()) 72 | if err != nil { 73 | return "", "", err 74 | } 75 | } 76 | 77 | return ref.Name().Short(), ref.Hash().String()[:7], nil 78 | } 79 | 80 | func findTag(r *git.Repository, h plumbing.Hash) (*plumbing.Reference, error) { 81 | tagrefs, err := r.Tags() 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | var match *plumbing.Reference 87 | err = tagrefs.ForEach(func(t *plumbing.Reference) error { 88 | if t.Hash() == h { 89 | match = t 90 | } 91 | 92 | return nil 93 | }) 94 | 95 | return match, err 96 | } 97 | 98 | func getVersionFromBranch(ref *plumbing.Reference) string { 99 | name := ref.Name().Short() 100 | pattern := "dev-%s" 101 | if name != "master" { 102 | pattern = fmt.Sprintf("dev-%s-%%s", name) 103 | } 104 | 105 | hash := ref.Hash().String()[:7] 106 | return fmt.Sprintf(pattern, hash) 107 | } 108 | 109 | func readGoMod(path string) (*modfile.File, error) { 110 | content, err := ioutil.ReadFile(filepath.Join(path, "go.mod")) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | return modfile.ParseLax("", content, nil) 116 | } 117 | 118 | func getPackageVersion(f *modfile.File, pkg string) string { 119 | for _, r := range f.Require { 120 | if r.Mod.Path == pkg { 121 | return r.Mod.Version 122 | } 123 | } 124 | return "" 125 | } 126 | 127 | func renderLDFLAGS(pkg string, flags map[string]string) string { 128 | output := make([]string, 0) 129 | for k, v := range flags { 130 | output = append(output, fmt.Sprintf("-X %s.%s=%s", pkg, k, v)) 131 | } 132 | 133 | return strings.Join(output, " ") 134 | } 135 | -------------------------------------------------------------------------------- /starlark/types/type_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.starlark.net/starlark" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/zclconf/go-cty/cty" 10 | ) 11 | 12 | func TestNewTypeFromStarlark(t *testing.T) { 13 | testCases := []struct { 14 | typ string 15 | cty cty.Type 16 | }{ 17 | {"bool", cty.Bool}, 18 | {"int", cty.Number}, 19 | {"float", cty.Number}, 20 | {"string", cty.String}, 21 | } 22 | 23 | for _, tc := range testCases { 24 | typ, err := NewTypeFromStarlark(tc.typ) 25 | assert.NoError(t, err) 26 | assert.Equal(t, typ.Cty(), tc.cty) 27 | } 28 | } 29 | func TestNewTypeFromStarlark_NonScalar(t *testing.T) { 30 | typ := MustTypeFromStarlark("list") 31 | assert.True(t, typ.Cty().IsListType()) 32 | 33 | typ = MustTypeFromStarlark("dict") 34 | assert.True(t, typ.Cty().IsMapType()) 35 | 36 | typ = MustTypeFromStarlark("ResourceCollection") 37 | assert.True(t, typ.Cty().IsListType()) 38 | 39 | typ = MustTypeFromStarlark("Resource") 40 | assert.True(t, typ.Cty().IsMapType()) 41 | } 42 | 43 | func TestNewTypeFromCty(t *testing.T) { 44 | testCases := []struct { 45 | typ string 46 | cty cty.Type 47 | }{ 48 | {"string", cty.String}, 49 | {"int", cty.Number}, 50 | {"bool", cty.Bool}, 51 | {"list", cty.List(cty.String)}, 52 | {"dict", cty.Map(cty.String)}, 53 | {"set", cty.Set(cty.String)}, 54 | {"tuple", cty.Tuple([]cty.Type{})}, 55 | } 56 | 57 | for _, tc := range testCases { 58 | typ, err := NewTypeFromCty(tc.cty) 59 | assert.NoError(t, err) 60 | assert.Equal(t, typ.Starlark(), tc.typ) 61 | } 62 | } 63 | 64 | func TestTypeValidate(t *testing.T) { 65 | testCases := []struct { 66 | t string 67 | v starlark.Value 68 | err bool 69 | }{ 70 | {"string", starlark.String("foo"), false}, 71 | {"int", starlark.String("foo"), true}, 72 | {"int", starlark.MakeInt(42), false}, 73 | {"int", starlark.MakeInt64(42), false}, 74 | {"string", starlark.MakeInt(42), true}, 75 | {"int", starlark.Float(42.), false}, 76 | } 77 | 78 | for i, tc := range testCases { 79 | typ := MustTypeFromStarlark(tc.t) 80 | err := typ.Validate(tc.v) 81 | if tc.err { 82 | assert.Error(t, err, i) 83 | } else { 84 | assert.NoError(t, err, i) 85 | } 86 | } 87 | } 88 | 89 | func TestTypeValidate_Dict(t *testing.T) { 90 | typ := MustTypeFromCty(cty.Map(cty.String)) 91 | dict := starlark.NewDict(1) 92 | dict.SetKey(starlark.String("foo"), starlark.MakeInt(42)) 93 | 94 | err := typ.Validate(dict) 95 | assert.NoError(t, err) 96 | } 97 | 98 | func TestTypeValidate_List(t *testing.T) { 99 | typ := MustTypeFromCty(cty.List(cty.String)) 100 | err := typ.Validate(starlark.NewList([]starlark.Value{ 101 | starlark.String("foo"), 102 | starlark.String("bar"), 103 | })) 104 | 105 | assert.NoError(t, err) 106 | } 107 | 108 | func TestTypeValidate_ListError(t *testing.T) { 109 | typ := MustTypeFromCty(cty.List(cty.Number)) 110 | err := typ.Validate(starlark.NewList([]starlark.Value{ 111 | starlark.MakeInt(42), 112 | starlark.String("bar"), 113 | })) 114 | 115 | assert.Errorf(t, err, "index 1: expected int, got string") 116 | } 117 | -------------------------------------------------------------------------------- /starlark/types/provider_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | stdos "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/mcuadros/ascode/starlark/module/os" 12 | "github.com/mcuadros/ascode/starlark/test" 13 | "github.com/mcuadros/ascode/terraform" 14 | "go.starlark.net/resolve" 15 | "go.starlark.net/starlark" 16 | "go.starlark.net/starlarkstruct" 17 | ) 18 | 19 | var id int 20 | 21 | func init() { 22 | NameGenerator = func() string { 23 | id++ 24 | return fmt.Sprintf("id_%d", id) 25 | } 26 | 27 | resolve.AllowLambda = true 28 | resolve.AllowNestedDef = true 29 | resolve.AllowFloat = true 30 | resolve.AllowSet = true 31 | resolve.AllowGlobalReassign = true 32 | } 33 | 34 | func TestProvider(t *testing.T) { 35 | doTest(t, "testdata/provider.star") 36 | } 37 | 38 | func TestProvisioner(t *testing.T) { 39 | if stdos.Getenv("ALLOW_PROVISIONER_SKIP") != "" { 40 | t.Skip("terraform binary now available in $PATH") 41 | } 42 | 43 | doTest(t, "testdata/provisioner.star") 44 | } 45 | 46 | func TestNestedBlock(t *testing.T) { 47 | doTest(t, "testdata/nested.star") 48 | } 49 | 50 | func TestResource(t *testing.T) { 51 | doTest(t, "testdata/resource.star") 52 | } 53 | 54 | func TestHCL(t *testing.T) { 55 | doTest(t, "testdata/hcl.star") 56 | } 57 | 58 | func TestHCLIntegration(t *testing.T) { 59 | doTest(t, "testdata/hcl_integration.star") 60 | } 61 | 62 | func doTest(t *testing.T, filename string) { 63 | doTestPrint(t, filename, nil) 64 | } 65 | 66 | func doTestPrint(t *testing.T, filename string, print func(*starlark.Thread, string)) { 67 | id = 0 68 | 69 | dir, _ := filepath.Split(filename) 70 | pm := &terraform.PluginManager{".providers"} 71 | 72 | log.SetOutput(ioutil.Discard) 73 | thread := &starlark.Thread{Load: load, Print: print} 74 | thread.SetLocal("base_path", dir) 75 | thread.SetLocal(PluginManagerLocal, pm) 76 | 77 | test.SetReporter(thread, t) 78 | 79 | predeclared := starlark.StringDict{} 80 | predeclared["tf"] = NewTerraform(pm) 81 | predeclared["provisioner"] = BuiltinProvisioner() 82 | predeclared["backend"] = BuiltinBackend() 83 | predeclared["hcl"] = BuiltinHCL() 84 | predeclared["validate"] = BuiltinValidate() 85 | predeclared["fn"] = BuiltinFunctionAttribute() 86 | predeclared["ref"] = BuiltinRef() 87 | predeclared["evaluate"] = BuiltinEvaluate(predeclared) 88 | predeclared["struct"] = starlark.NewBuiltin("struct", starlarkstruct.Make) 89 | predeclared["module"] = starlark.NewBuiltin("module", starlarkstruct.MakeModule) 90 | 91 | _, err := starlark.ExecFile(thread, filename, nil, predeclared) 92 | if err != nil { 93 | if err, ok := err.(*starlark.EvalError); ok { 94 | t.Fatal(err.Backtrace()) 95 | } 96 | t.Fatal(err) 97 | } 98 | } 99 | 100 | // load implements the 'load' operation as used in the evaluator tests. 101 | func load(thread *starlark.Thread, module string) (starlark.StringDict, error) { 102 | if module == "assert.star" { 103 | return test.LoadAssertModule() 104 | } 105 | 106 | if module == os.ModuleName { 107 | return os.LoadModule() 108 | } 109 | 110 | return nil, fmt.Errorf("load not implemented") 111 | } 112 | -------------------------------------------------------------------------------- /starlark/types/value_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.starlark.net/starlark" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/zclconf/go-cty/cty" 10 | ) 11 | 12 | func TestMustValue(t *testing.T) { 13 | 14 | dict := starlark.NewDict(1) 15 | dict.SetKey(starlark.String("foo"), starlark.MakeInt(42)) 16 | 17 | testCases := []struct { 18 | v starlark.Value 19 | cty cty.Type 20 | value cty.Value 21 | native interface{} 22 | }{ 23 | { 24 | starlark.String("foo"), 25 | cty.String, 26 | cty.StringVal("foo"), 27 | "foo", 28 | }, 29 | { 30 | starlark.MakeInt(42), 31 | cty.Number, 32 | cty.NumberIntVal(42), 33 | int64(42), 34 | }, 35 | { 36 | starlark.Float(42), 37 | cty.Number, 38 | cty.NumberFloatVal(42), 39 | 42., 40 | }, 41 | { 42 | starlark.Bool(true), 43 | cty.Bool, 44 | cty.True, 45 | true, 46 | }, 47 | { 48 | starlark.NewList([]starlark.Value{starlark.String("foo")}), 49 | cty.List(cty.NilType), 50 | cty.ListVal([]cty.Value{cty.StringVal("foo")}), 51 | []interface{}{"foo"}, 52 | }, 53 | { 54 | dict, 55 | cty.Map(cty.NilType), 56 | cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(42)}), 57 | map[string]interface{}{"foo": int64(42)}, 58 | }, 59 | } 60 | 61 | for _, tc := range testCases { 62 | value := MustValue(tc.v) 63 | assert.Equal(t, value.Type().Cty(), tc.cty) 64 | assert.Equal(t, value.Starlark(), tc.v) 65 | assert.Equal(t, value.Cty(), tc.value) 66 | assert.Equal(t, value.Interface(), tc.native) 67 | } 68 | } 69 | 70 | func TestValuesSet(t *testing.T) { 71 | values := NewValues() 72 | val := values.Set("foo", MustValue(starlark.MakeInt(42))) 73 | 74 | assert.Equal(t, val.Name, "foo") 75 | assert.Equal(t, val.Interface(), int64(42)) 76 | 77 | val = values.Set("foo", MustValue(starlark.MakeInt(84))) 78 | assert.Equal(t, val.Interface(), int64(84)) 79 | } 80 | 81 | func TestValuesGet(t *testing.T) { 82 | values := NewValues() 83 | values.Set("foo", MustValue(starlark.MakeInt(42))) 84 | values.Set("foo", MustValue(starlark.MakeInt(42*2))) 85 | 86 | val := values.Get("foo") 87 | assert.Equal(t, val.Interface(), int64(42*2)) 88 | 89 | val.Value = MustValue(starlark.MakeInt(42 * 3)) 90 | 91 | val = values.Get("foo") 92 | assert.Equal(t, val.Interface(), int64(42*3)) 93 | 94 | } 95 | 96 | func TestValuesHash(t *testing.T) { 97 | a := NewValues() 98 | a.Set("foo", MustValue(starlark.MakeInt(42))) 99 | a.Set("bar", MustValue(starlark.MakeInt(42*32))) 100 | 101 | hashA, err := a.Hash() 102 | assert.NoError(t, err) 103 | assert.Equal(t, hashA, uint32(0x98f6840f)) 104 | 105 | b := NewValues() 106 | b.Set("bar", MustValue(starlark.MakeInt(42*32))) 107 | b.Set("foo", MustValue(starlark.MakeInt(42))) 108 | 109 | hashB, err := b.Hash() 110 | assert.NoError(t, err) 111 | assert.Equal(t, hashA, hashB) 112 | } 113 | 114 | func TestValuesToStringDict(t *testing.T) { 115 | a := NewValues() 116 | a.Set("foo", MustValue(starlark.MakeInt(42))) 117 | a.Set("bar", MustValue(starlark.MakeInt(42*32))) 118 | 119 | dict := make(starlark.StringDict, 0) 120 | a.ToStringDict(dict) 121 | 122 | assert.Len(t, dict, 2) 123 | } 124 | 125 | func TestValuesForEach(t *testing.T) { 126 | a := NewValues() 127 | a.Set("foo", MustValue(starlark.MakeInt(42))) 128 | a.Set("bar", MustValue(starlark.MakeInt(42*32))) 129 | 130 | var result []string 131 | a.ForEach(func(v *NamedValue) error { 132 | result = append(result, v.Name) 133 | return nil 134 | }) 135 | 136 | assert.Equal(t, result, []string{"bar", "foo"}) 137 | } 138 | -------------------------------------------------------------------------------- /starlark/types/provisioner.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/terraform/plugin" 7 | "github.com/hashicorp/terraform/plugin/discovery" 8 | "github.com/mcuadros/ascode/terraform" 9 | "go.starlark.net/starlark" 10 | ) 11 | 12 | // BuiltinProvisioner returns a starlak.Builtin function capable of instantiate 13 | // new Provisioner instances. 14 | // 15 | // outline: types 16 | // functions: 17 | // provisioner(type) Provisioner 18 | // Instantiates a new Provisioner 19 | // 20 | // params: 21 | // type string 22 | // Provisioner type. 23 | // 24 | func BuiltinProvisioner() starlark.Value { 25 | return starlark.NewBuiltin("provisioner", MakeProvisioner) 26 | } 27 | 28 | // MakeProvisioner defines the Provisioner constructor. 29 | func MakeProvisioner( 30 | t *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, 31 | ) (starlark.Value, error) { 32 | 33 | pm := t.Local(PluginManagerLocal).(*terraform.PluginManager) 34 | 35 | var name starlark.String 36 | switch len(args) { 37 | case 1: 38 | var ok bool 39 | name, ok = args.Index(0).(starlark.String) 40 | if !ok { 41 | return nil, fmt.Errorf("expected string, got %s", args.Index(0).Type()) 42 | } 43 | default: 44 | return nil, fmt.Errorf("unexpected positional arguments count") 45 | } 46 | 47 | p, err := NewProvisioner(pm, name.GoString(), t.CallStack()) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return p, p.loadKeywordArgs(kwargs) 53 | } 54 | 55 | // Provisioner represents a Terraform provider of a specif type. 56 | // 57 | // outline: types 58 | // types: 59 | // Provisioner 60 | // Provisioner represents a Terraform provider of a specif type. As 61 | // written in the terraform documentation: "*Provisioners are a Last Resort*" 62 | // 63 | // fields: 64 | // __kind__ string 65 | // Kind of the provisioner. Fixed value `provisioner` 66 | // __type__ string 67 | // Type of the resource. Eg.: `aws_instance 68 | // 69 | // Arguments defined by the provisioner schema, thus can be of any 70 | // scalar type. 71 | // Resource 72 | // Blocks defined by the provisioner schema, thus are nested resources, 73 | // containing other arguments and/or blocks. 74 | // 75 | type Provisioner struct { 76 | provisioner *plugin.GRPCProvisioner 77 | meta discovery.PluginMeta 78 | *Resource 79 | } 80 | 81 | // NewProvisioner returns a new Provisioner for the given type. 82 | func NewProvisioner(pm *terraform.PluginManager, typ string, cs starlark.CallStack) (*Provisioner, error) { 83 | cli, meta, err := pm.Provisioner(typ) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | rpc, err := cli.Client() 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | raw, err := rpc.Dispense(plugin.ProvisionerPluginName) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | provisioner := raw.(*plugin.GRPCProvisioner) 99 | response := provisioner.GetSchema() 100 | 101 | defer cli.Kill() 102 | return &Provisioner{ 103 | provisioner: provisioner, 104 | meta: meta, 105 | 106 | Resource: NewResource(NameGenerator(), typ, ProvisionerKind, response.Provisioner, nil, nil, cs), 107 | }, nil 108 | } 109 | 110 | // Type honors the starlark.Value interface. It shadows p.Resource.Type. 111 | func (p *Provisioner) Type() string { 112 | return "Provisioner" 113 | } 114 | 115 | func (p *Provisioner) String() string { 116 | return fmt.Sprintf("Provisioner<%s>", p.typ) 117 | } 118 | -------------------------------------------------------------------------------- /starlark/types/type.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/zclconf/go-cty/cty" 8 | "go.starlark.net/starlark" 9 | ) 10 | 11 | // Type is a helper to manipulate and transform starlark.Type and cty.Type 12 | type Type struct { 13 | typ string 14 | cty cty.Type 15 | } 16 | 17 | // MustTypeFromStarlark returns a Type from a given starlark type string. 18 | // Panics if error. 19 | func MustTypeFromStarlark(typ string) *Type { 20 | t, err := NewTypeFromStarlark(typ) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | return t 26 | } 27 | 28 | // NewTypeFromStarlark returns a Type from a given starlark type string. 29 | func NewTypeFromStarlark(typ string) (*Type, error) { 30 | complex := strings.SplitN(typ, "<", 2) 31 | if len(complex) == 2 { 32 | typ = complex[0] 33 | } 34 | 35 | t := &Type{} 36 | t.typ = typ 37 | 38 | switch typ { 39 | case "bool": 40 | t.cty = cty.Bool 41 | case "int", "float": 42 | t.cty = cty.Number 43 | case "string": 44 | t.cty = cty.String 45 | case "list", "ResourceCollection": 46 | t.cty = cty.List(cty.NilType) 47 | case "dict", "Resource": 48 | t.cty = cty.Map(cty.NilType) 49 | case "Attribute": 50 | t.cty = cty.String 51 | default: 52 | return nil, fmt.Errorf("unexpected %q type", typ) 53 | } 54 | 55 | return t, nil 56 | } 57 | 58 | // MustTypeFromCty returns a Type froma given cty.Type. Panics if error. 59 | func MustTypeFromCty(typ cty.Type) *Type { 60 | t, err := NewTypeFromCty(typ) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | return t 66 | } 67 | 68 | // NewTypeFromCty returns a Type froma given cty.Type. 69 | func NewTypeFromCty(typ cty.Type) (*Type, error) { 70 | t := &Type{} 71 | t.cty = typ 72 | 73 | switch typ { 74 | case cty.String: 75 | t.typ = "string" 76 | case cty.Number: 77 | t.typ = "int" 78 | case cty.Bool: 79 | t.typ = "bool" 80 | } 81 | 82 | if typ.IsMapType() || typ.IsObjectType() { 83 | t.typ = "dict" 84 | } 85 | 86 | if typ.IsListType() { 87 | t.typ = "list" 88 | } 89 | 90 | if typ.IsSetType() { 91 | t.typ = "set" 92 | } 93 | 94 | if typ.IsTupleType() { 95 | t.typ = "tuple" 96 | } 97 | 98 | return t, nil 99 | } 100 | 101 | // Starlark returns the type as starlark type string. 102 | func (t *Type) Starlark() string { 103 | return t.typ 104 | } 105 | 106 | // Cty returns the type as cty.Type. 107 | func (t *Type) Cty() cty.Type { 108 | return t.cty 109 | } 110 | 111 | // Validate validates a value against the type. 112 | func (t *Type) Validate(v starlark.Value) error { 113 | switch v.(type) { 114 | case starlark.String: 115 | if t.cty == cty.String { 116 | return nil 117 | } 118 | case starlark.Int, starlark.Float: 119 | if t.cty == cty.Number { 120 | return nil 121 | } 122 | case starlark.Bool: 123 | if t.cty == cty.Bool { 124 | return nil 125 | } 126 | case *Attribute: 127 | if t.cty == v.(*Attribute).t { 128 | return nil 129 | } 130 | 131 | vt := v.(*Attribute).InnerType().Starlark() 132 | return fmt.Errorf("expected %s, got %s", t.typ, vt) 133 | case *starlark.List: 134 | if t.cty.IsListType() || t.cty.IsSetType() { 135 | return t.validateListType(v.(*starlark.List), t.cty.ElementType()) 136 | } 137 | case *starlark.Dict: 138 | if t.cty.IsMapType() || t.cty.IsObjectType() { 139 | return nil 140 | } 141 | } 142 | 143 | return fmt.Errorf("expected %s, got %s", t.typ, v.Type()) 144 | } 145 | 146 | func (t *Type) validateListType(l *starlark.List, expected cty.Type) error { 147 | for i := 0; i < l.Len(); i++ { 148 | if err := MustTypeFromCty(expected).Validate(l.Index(i)); err != nil { 149 | return fmt.Errorf("index %d: %s", i, err) 150 | } 151 | } 152 | 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BASE_PATH := $(realpath -s $(dir $(abspath $(firstword $(MAKEFILE_LIST))))) 2 | 3 | # Documentation 4 | OUTLINE_CMD ?= outline 5 | DOCUMENTATION_PATH ?= $(BASE_PATH)/_documentation 6 | DOCUMENTATION_REFERENCE_PATH ?= $(DOCUMENTATION_PATH)/reference 7 | DOCUMENTATION_REFERENCE_TEMPLATE ?= $(DOCUMENTATION_REFERENCE_PATH)/reference.md.tmpl 8 | DOCUMENTATION_INLINE_EXAMPLES_PATH ?= starlark/types/testdata/examples 9 | 10 | RUNTIME_MODULES = \ 11 | github.com/mcuadros/ascode/starlark/module/docker \ 12 | github.com/mcuadros/ascode/starlark/module/os \ 13 | github.com/mcuadros/ascode/starlark/types \ 14 | github.com/mcuadros/ascode/starlark/module/filepath \ 15 | github.com/mcuadros/ascode/starlark/module/url \ 16 | github.com/qri-io/starlib/encoding/base64 \ 17 | github.com/qri-io/starlib/encoding/csv \ 18 | github.com/qri-io/starlib/encoding/json \ 19 | github.com/qri-io/starlib/encoding/yaml \ 20 | github.com/qri-io/starlib/re \ 21 | github.com/qri-io/starlib/time \ 22 | github.com/qri-io/starlib/math \ 23 | github.com/qri-io/starlib/http 24 | 25 | QUERY_GO_MOD_CMD = go run _scripts/query-go-mod.go 26 | STARLIB_PKG ?= github.com/qri-io/starlib 27 | STARLIB_COMMIT ?= $(shell $(QUERY_GO_MOD_CMD) . $(STARLIB_PKG)) 28 | STARLIB_PKG_LOCATION = $(GOPATH)/src/$(STARLIB_PKG) 29 | 30 | # Examples 31 | EXAMPLE_TO_MD_CMD = go run _scripts/example-to-md.go 32 | EXAMPLES = functions.star runtime.star 33 | EXAMPLES_PATH = $(BASE_PATH)/_examples 34 | DOCUMENTATION_EXAMPLES_PATH = $(DOCUMENTATION_PATH)/example 35 | 36 | # Build Info 37 | GO_LDFLAGS_CMD = go run _scripts/goldflags.go 38 | GO_LDFLAGS_PACKAGE = cmd 39 | GO_LDFLAGS_PACKAGES = \ 40 | starlarkVersion=go.starlark.net \ 41 | starlibVersion=github.com/qri-io/starlib \ 42 | terraformVersion=github.com/hashicorp/terraform 43 | 44 | GITHUB_REF ?= $(shell cat .git/HEAD | cut -d \ -f 2) 45 | GIT_REF = $(GITHUB_REF) 46 | GIT_REF_SHORT = $(shell echo $(GIT_REF) | cut -d / -f 3) 47 | 48 | # Site 49 | HUGO_SITE_PATH ?= $(BASE_PATH)/_site 50 | HUGO_SITE_CONTENT_PATH ?= $(HUGO_SITE_PATH)/content 51 | HUGO_SITE_TEMPLATE_PATH ?= $(HUGO_SITE_PATH)/themes/hugo-ascode-theme 52 | HUGO_THEME_URL ?= https://github.com/mcuadros/hugo-ascode-theme 53 | HUGO_PARAMS_VERSION ?= $(GIT_REF_SHORT) 54 | export HUGO_PARAMS_VERSION 55 | 56 | # Rules 57 | .PHONY: documentation clean hugo-server hugo-build goldflags examples 58 | 59 | documentation: $(RUNTIME_MODULES) 60 | $(RUNTIME_MODULES): $(DOCUMENTATION_RUNTIME_PATH) $(STARLIB_PKG_LOCATION) 61 | $(OUTLINE_CMD) package \ 62 | -t $(DOCUMENTATION_REFERENCE_TEMPLATE) \ 63 | -d $(DOCUMENTATION_INLINE_EXAMPLES_PATH) \ 64 | $@ \ 65 | > $(DOCUMENTATION_REFERENCE_PATH)/`basename $@`.md 66 | 67 | $(DOCUMENTATION_REFERENCE_PATH): 68 | mkdir -p $@ 69 | 70 | $(STARLIB_PKG_LOCATION): 71 | git clone https://$(STARLIB_PKG) $@; \ 72 | cd $@; \ 73 | git checkout $(STARLIB_COMMIT); \ 74 | cd $(BASE_PATH); 75 | 76 | examples: $(EXAMPLES) 77 | 78 | $(EXAMPLES): 79 | $(EXAMPLE_TO_MD_CMD) \ 80 | $(EXAMPLES_PATH)/$@ $(shell ls -1 $(DOCUMENTATION_EXAMPLES_PATH) | wc -l) \ 81 | > $(DOCUMENTATION_EXAMPLES_PATH)/$@.md 82 | 83 | goldflags: 84 | @$(GO_LDFLAGS_CMD) $(GO_LDFLAGS_PACKAGE) . $(GO_LDFLAGS_PACKAGES) 85 | 86 | hugo-build: $(HUGO_SITE_PATH) documentation examples 87 | hugo --minify --source $(HUGO_SITE_PATH) --config $(DOCUMENTATION_PATH)/config.toml 88 | 89 | hugo-server: $(HUGO_SITE_PATH) documentation examples 90 | hugo server --source $(HUGO_SITE_PATH) --config $(DOCUMENTATION_PATH)/config.toml 91 | 92 | $(HUGO_SITE_PATH): $(HUGO_SITE_TEMPLATE_PATH) 93 | mkdir -p $@ \ 94 | mkdir -p $(HUGO_SITE_CONTENT_PATH) 95 | mkdir -p $(HUGO_SITE_TEMPLATE_PATH) 96 | ln -s $(DOCUMENTATION_PATH) $(HUGO_SITE_CONTENT_PATH)/docs 97 | ln -s $(DOCUMENTATION_PATH)/_home.md $(HUGO_SITE_CONTENT_PATH)/_index.md 98 | 99 | 100 | $(HUGO_SITE_TEMPLATE_PATH): 101 | git clone $(HUGO_THEME_URL) $(HUGO_SITE_TEMPLATE_PATH) 102 | 103 | clean: 104 | rm -rf $(HUGO_SITE_PATH) -------------------------------------------------------------------------------- /_examples/functions.star: -------------------------------------------------------------------------------- 1 | # Using functions 2 | # This example illustrates how with the usage through the usage of functions, 3 | # we can simplify and improve the readability of our infrastructure declaration. 4 | # 5 | 6 | # Instantiates a new AWS provider, `aws` will be available in the context of 7 | # the functions. 8 | aws = tf.provider("aws", region="us-west-2") 9 | 10 | # Every instance requires an `ami` data source; this data source contains a 11 | # very specif configuration like the ID of the owner or a name pattern. So 12 | # we define a dictionary with the different values we want to use. 13 | ami_names_owners = { 14 | "ubuntu": ["ubuntu/images/*/ubuntu-xenial-16.04-amd64-server-*", "099720109477"], 15 | "ecs": ["*amazon-ecs-optimized", "591542846629"], 16 | } 17 | 18 | # `get_ami` returns the ami for the given `distro`. It searches in the 19 | # `ResouceCollection` of the `ami` data source, if finds the `ami` it simply 20 | # returns it; if not creates a new one using the data from the `ami_names_owners` 21 | # dictionary. 22 | def get_ami(distro): 23 | amis = aws.data.ami.search(distro) 24 | if len(amis) != 0: 25 | return amis[0] 26 | 27 | data = ami_names_owners[distro] 28 | 29 | ami = aws.data.ami(distro) 30 | ami.most_recent = True 31 | ami.filter(name="name", values=[data[0]]) 32 | ami.filter(name="virtualization-type", values=["hvm"]) 33 | ami.owners = [data[1]] 34 | 35 | return ami 36 | 37 | # `new_instance` instantiates a new `instance` for the given name, distro and 38 | # type, the type has a default value `t2.micro`. The `distro` value is resolved 39 | # to an `ami` resource using the previously defined function `get_ami`. 40 | def new_instance(name, distro, type="t2.micro"): 41 | instance = aws.resource.instance(name) 42 | instance.instance_type = type 43 | instance.ami = get_ami(distro).id 44 | 45 | # Now using a basic `for` loop we can instantiate 5 different web servers, 46 | # where the even machines are using ubuntu and the odd ones ecs. 47 | for i in range(5): 48 | distro = "ubuntu" 49 | if i % 2: 50 | distro = "ecs" 51 | 52 | new_instance("web_%d" % i, distro) 53 | 54 | # ### Output 55 | # If we execute this script with the flag `--print-hcl` the result shuld be 56 | # something like this: 57 | 58 | """hcl 59 | provider "aws" { 60 | alias = "id_01E4KEA5ZAA1PYERQ8KM5D04GC" 61 | version = "2.13.0" 62 | region = "us-west-2" 63 | } 64 | 65 | data "aws_ami" "ubuntu" { 66 | provider = aws.id_01E4KEA5ZAA1PYERQ8KM5D04GC 67 | most_recent = true 68 | owners = ["099720109477"] 69 | 70 | filter { 71 | name = "name" 72 | values = ["ubuntu/images/*/ubuntu-xenial-16.04-amd64-server-*"] 73 | } 74 | 75 | filter { 76 | name = "virtualization-type" 77 | values = ["hvm"] 78 | } 79 | } 80 | 81 | data "aws_ami" "ecs" { 82 | provider = aws.id_01E4KEA5ZAA1PYERQ8KM5D04GC 83 | most_recent = true 84 | owners = ["591542846629"] 85 | 86 | filter { 87 | name = "name" 88 | values = ["*amazon-ecs-optimized"] 89 | } 90 | 91 | filter { 92 | name = "virtualization-type" 93 | values = ["hvm"] 94 | } 95 | } 96 | 97 | resource "aws_instance" "web_0" { 98 | provider = aws.id_01E4KEA5ZAA1PYERQ8KM5D04GC 99 | ami = "${data.aws_ami.ubuntu.id}" 100 | instance_type = "t2.micro" 101 | } 102 | 103 | resource "aws_instance" "web_1" { 104 | provider = aws.id_01E4KEA5ZAA1PYERQ8KM5D04GC 105 | ami = "${data.aws_ami.ecs.id}" 106 | instance_type = "t2.micro" 107 | } 108 | 109 | resource "aws_instance" "web_2" { 110 | provider = aws.id_01E4KEA5ZAA1PYERQ8KM5D04GC 111 | ami = "${data.aws_ami.ubuntu.id}" 112 | instance_type = "t2.micro" 113 | } 114 | 115 | resource "aws_instance" "web_3" { 116 | provider = aws.id_01E4KEA5ZAA1PYERQ8KM5D04GC 117 | ami = "${data.aws_ami.ecs.id}" 118 | instance_type = "t2.micro" 119 | } 120 | 121 | resource "aws_instance" "web_4" { 122 | provider = aws.id_01E4KEA5ZAA1PYERQ8KM5D04GC 123 | ami = "${data.aws_ami.ubuntu.id}" 124 | instance_type = "t2.micro" 125 | } 126 | """ 127 | -------------------------------------------------------------------------------- /starlark/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | osfilepath "path/filepath" 6 | 7 | "github.com/mcuadros/ascode/starlark/module/docker" 8 | "github.com/mcuadros/ascode/starlark/module/filepath" 9 | "github.com/mcuadros/ascode/starlark/module/os" 10 | "github.com/mcuadros/ascode/starlark/module/url" 11 | "github.com/mcuadros/ascode/starlark/types" 12 | "github.com/mcuadros/ascode/terraform" 13 | "github.com/qri-io/starlib/encoding/base64" 14 | "github.com/qri-io/starlib/encoding/csv" 15 | "github.com/qri-io/starlib/encoding/json" 16 | "github.com/qri-io/starlib/encoding/yaml" 17 | "github.com/qri-io/starlib/http" 18 | "github.com/qri-io/starlib/math" 19 | "github.com/qri-io/starlib/re" 20 | "github.com/qri-io/starlib/time" 21 | "go.starlark.net/repl" 22 | "go.starlark.net/resolve" 23 | "go.starlark.net/starlark" 24 | "go.starlark.net/starlarkstruct" 25 | ) 26 | 27 | func init() { 28 | resolve.AllowRecursion = true 29 | resolve.AllowFloat = true 30 | resolve.AllowGlobalReassign = true 31 | resolve.AllowLambda = true 32 | resolve.AllowNestedDef = true 33 | resolve.AllowSet = true 34 | } 35 | 36 | // LoadModuleFunc is a concurrency-safe and idempotent function that returns 37 | // the module when is called from the `load` funcion. 38 | type LoadModuleFunc func() (starlark.StringDict, error) 39 | 40 | // Runtime represents the AsCode runtime, it defines the available modules, 41 | // the predeclared globals and handles how the `load` function behaves. 42 | type Runtime struct { 43 | Terraform *types.Terraform 44 | pm *terraform.PluginManager 45 | predeclared starlark.StringDict 46 | modules map[string]LoadModuleFunc 47 | moduleCache map[string]*moduleCache 48 | 49 | path string 50 | } 51 | 52 | // NewRuntime returns a new Runtime for the given terraform.PluginManager. 53 | func NewRuntime(pm *terraform.PluginManager) *Runtime { 54 | tf := types.NewTerraform(pm) 55 | predeclared := starlark.StringDict{} 56 | predeclared["tf"] = tf 57 | predeclared["provisioner"] = types.BuiltinProvisioner() 58 | predeclared["backend"] = types.BuiltinBackend() 59 | predeclared["validate"] = types.BuiltinValidate() 60 | predeclared["hcl"] = types.BuiltinHCL() 61 | predeclared["fn"] = types.BuiltinFunctionAttribute() 62 | predeclared["ref"] = types.BuiltinRef() 63 | predeclared["evaluate"] = types.BuiltinEvaluate(predeclared) 64 | predeclared["struct"] = starlark.NewBuiltin("struct", starlarkstruct.Make) 65 | predeclared["module"] = starlark.NewBuiltin("module", starlarkstruct.MakeModule) 66 | 67 | return &Runtime{ 68 | Terraform: tf, 69 | pm: pm, 70 | moduleCache: make(map[string]*moduleCache), 71 | modules: map[string]LoadModuleFunc{ 72 | filepath.ModuleName: filepath.LoadModule, 73 | os.ModuleName: os.LoadModule, 74 | docker.ModuleName: docker.LoadModule, 75 | 76 | "encoding/json": json.LoadModule, 77 | "encoding/base64": base64.LoadModule, 78 | "encoding/csv": csv.LoadModule, 79 | "encoding/yaml": yaml.LoadModule, 80 | "math": math.LoadModule, 81 | "re": re.LoadModule, 82 | "time": time.LoadModule, 83 | "http": http.LoadModule, 84 | "url": url.LoadModule, 85 | }, 86 | predeclared: predeclared, 87 | } 88 | } 89 | 90 | // ExecFile parses, resolves, and executes a Starlark file. 91 | func (r *Runtime) ExecFile(filename string) (starlark.StringDict, error) { 92 | fullpath, _ := osfilepath.Abs(filename) 93 | r.path, _ = osfilepath.Split(fullpath) 94 | 95 | thread := &starlark.Thread{Name: "thread", Load: r.load} 96 | r.setLocals(thread) 97 | 98 | return starlark.ExecFile(thread, filename, nil, r.predeclared) 99 | } 100 | 101 | // REPL executes a read, eval, print loop. 102 | func (r *Runtime) REPL() { 103 | thread := &starlark.Thread{Name: "thread", Load: r.load} 104 | r.setLocals(thread) 105 | 106 | repl.REPL(thread, r.predeclared) 107 | } 108 | 109 | func (r *Runtime) setLocals(t *starlark.Thread) { 110 | t.SetLocal("base_path", r.path) 111 | t.SetLocal(types.PluginManagerLocal, r.pm) 112 | } 113 | 114 | func (r *Runtime) load(t *starlark.Thread, module string) (starlark.StringDict, error) { 115 | if m, ok := r.modules[module]; ok { 116 | return m() 117 | } 118 | 119 | filename := osfilepath.Join(r.path, module) 120 | return r.loadFile(t, filename) 121 | } 122 | 123 | type moduleCache struct { 124 | globals starlark.StringDict 125 | err error 126 | } 127 | 128 | func (r *Runtime) loadFile(thread *starlark.Thread, module string) (starlark.StringDict, error) { 129 | e, ok := r.moduleCache[module] 130 | if e == nil { 131 | if ok { 132 | // request for package whose loading is in progress 133 | return nil, fmt.Errorf("cycle in load graph") 134 | } 135 | 136 | // Add a placeholder to indicate "load in progress". 137 | r.moduleCache[module] = nil 138 | 139 | thread := &starlark.Thread{Name: "exec " + module, Load: thread.Load} 140 | globals, err := starlark.ExecFile(thread, module, nil, r.predeclared) 141 | 142 | e = &moduleCache{globals, err} 143 | r.moduleCache[module] = e 144 | } 145 | 146 | return e.globals, e.err 147 | } 148 | -------------------------------------------------------------------------------- /_documentation/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Getting Started' 3 | weight: 2 4 | --- 5 | 6 | This page explains the basics of using AsCode to define your infrastructure in Terraform. It assumes that you have already [installed](/docs/install) AsCode. 7 | 8 | ```sh 9 | > ascode --help 10 | Usage: 11 | ascode [OPTIONS] 12 | 13 | AsCode - Terraform Alternative Syntax. 14 | 15 | Help Options: 16 | -h, --help Show this help message 17 | 18 | Available commands: 19 | repl Run as interactive shell. 20 | run Run parses, resolves, and executes a Starlark file. 21 | version Version prints information about this binary. 22 | ``` 23 | 24 | ## The `repl` command 25 | 26 | The `repl` command provides a handy `REPL` interface for debugging and tinkering with AsCode. 27 | 28 | For example you can explore the API of a resource printing the list of arguments: 29 | 30 | ```sh 31 | > ascode repl 32 | >>> print(dir(helm.resource)) 33 | ["__kind__", "__provider__", "release", "repository"] 34 | ``` 35 | 36 | Or to validate how a resource will be rendered: 37 | ```sh 38 | > ascode repl 39 | >>> aws = tf.provider("aws") 40 | >>> web = aws.resource.instance("web", instance_type="t2.micro") 41 | >>> print(hcl(web)) 42 | resource "aws_instance" "web" { 43 | provider = aws.id_01E4JV722PS2WPKK7WQ2NMZY6D 44 | instance_type = "t2.micro" 45 | } 46 | ``` 47 | 48 | ## The `run` command 49 | 50 | The `run` command executes a valid Starlack program. Using the `--print-hcl` and `--to-hcl`, an HCL encoded version of the `tf` object will be printed or saved to a given file, respectively. 51 | 52 | This is the first step to deploy any infrastructure defined with AsCode, using `run` and generating a valid `.tf` file, we can use the standard Terraform tooling to deploy our infrastructure using `terraform init`, `terraform plan` and `terraform apply`. 53 | 54 | To learn about writing Starlark programs, please refer to the [Language definition](/docs/starlark/) and the [API Reference](/docs/reference/) sections of this documentation. 55 | 56 | 57 | ### Basic Example 58 | 59 | The goal of the example is to create, in DigitalOcean, one `s-1vcpu-1gb` instance called `web` in the `nyc2` region: 60 | 61 | > To run this example, you need `terraform` correctly installed on your system. 62 | 63 | ```sh 64 | > mkdir example; cd example 65 | > echo 'do = tf.provider("digitalocean")' > main.star 66 | > echo 'web = do.resource.droplet("web", name="web", size="s-1vcpu-1gb")' >> main.star 67 | > echo 'web.region = "nyc2"' >> main.star 68 | > echo 'web.image = "ubuntu-18-04-x64"' >> main.star 69 | ``` 70 | 71 | Now we are ready to run our Starlark program and generate a valid `HCL` file: 72 | 73 | ```sh 74 | > ascode run main.star --to-hcl main.tf 75 | > cat main.tf 76 | provider "digitalocean" { 77 | alias = "id_01E4JXQD8HKW7XEQ7R5S8SP8AQ" 78 | version = "1.15.1" 79 | } 80 | 81 | resource "digitalocean_droplet" "web" { 82 | provider = digitalocean.id_01E4JXQD8HKW7XEQ7R5S8SP8AQ 83 | image = "ubuntu-18-04-x64" 84 | name = "web" 85 | region = "nyc2" 86 | size = "s-1vcpu-1gb" 87 | } 88 | 89 | 90 | ``` 91 | 92 | And now as it's usual in terraform we can run `init`, `plan` or/and `apply` 93 | 94 | ```sh 95 | > terraform init 96 | ... 97 | > terraform plan 98 | Terraform will perform the following actions: 99 | 100 | # digitalocean_droplet.web will be created 101 | + resource "digitalocean_droplet" "web" { 102 | + backups = false 103 | + disk = (known after apply) 104 | + id = (known after apply) 105 | + image = "ubuntu-18-04-x64" 106 | + ipv4_address = (known after apply) 107 | + ipv4_address_private = (known after apply) 108 | + ipv6 = false 109 | + ipv6_address = (known after apply) 110 | + ipv6_address_private = (known after apply) 111 | + locked = (known after apply) 112 | + memory = (known after apply) 113 | + monitoring = false 114 | + name = "web" 115 | + price_hourly = (known after apply) 116 | + price_monthly = (known after apply) 117 | + private_networking = false 118 | + region = "nyc2" 119 | + resize_disk = true 120 | + size = "s-1vcpu-1gb" 121 | + status = (known after apply) 122 | + urn = (known after apply) 123 | + vcpus = (known after apply) 124 | + volume_ids = (known after apply) 125 | } 126 | 127 | Plan: 1 to add, 0 to change, 0 to destroy. 128 | 129 | > terraform apply 130 | ... 131 | ``` 132 | 133 | ## The `version` command 134 | 135 | The `version` command prints a report about the versions of the different 136 | dependencies, and AsCode itself used to compile the binary. 137 | 138 | ``` 139 | > ascode version 140 | Go Version: go1.14.1 141 | AsCode Version: v0.0.1 142 | AsCode Commit: 6a682e4 143 | AsCode Build Date: 2020-03-29T12:43:52+02:00 144 | Terraform Version: v0.12.23 145 | Starlark Version: v0.0.0-20200306205701-8dd3e2ee1dd5 146 | ``` -------------------------------------------------------------------------------- /starlark/test/assert.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Bazel Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package test defines utilities for testing Starlark programs. 6 | // 7 | // Clients can call LoadAssertModule to load a module that defines 8 | // several functions useful for testing. See assert.star for its 9 | // definition. 10 | // 11 | // The assert.error function, which reports errors to the current Go 12 | // testing.T, requires that clients call SetTest(thread, t) before use. 13 | package test 14 | 15 | import ( 16 | "fmt" 17 | "go/build" 18 | "os" 19 | "path/filepath" 20 | "regexp" 21 | "strings" 22 | "sync" 23 | 24 | "go.starlark.net/starlark" 25 | "go.starlark.net/starlarkstruct" 26 | ) 27 | 28 | const localKey = "Reporter" 29 | 30 | // A Reporter is a value to which errors may be reported. 31 | // It is satisfied by *testing.T. 32 | type Reporter interface { 33 | Error(args ...interface{}) 34 | } 35 | 36 | // SetReporter associates an error reporter (such as a testing.T in 37 | // a Go test) with the Starlark thread so that Starlark programs may 38 | // report errors to it. 39 | func SetReporter(thread *starlark.Thread, r Reporter) { 40 | thread.SetLocal(localKey, r) 41 | } 42 | 43 | // GetReporter returns the Starlark thread's error reporter. 44 | // It must be preceded by a call to SetReporter. 45 | func GetReporter(thread *starlark.Thread) Reporter { 46 | r, ok := thread.Local(localKey).(Reporter) 47 | if !ok { 48 | panic("internal error: starlarktest.SetReporter was not called") 49 | } 50 | return r 51 | } 52 | 53 | var ( 54 | once sync.Once 55 | assert starlark.StringDict 56 | assertErr error 57 | ) 58 | 59 | // LoadAssertModule loads the assert module. 60 | // It is concurrency-safe and idempotent. 61 | func LoadAssertModule() (starlark.StringDict, error) { 62 | once.Do(func() { 63 | predeclared := starlark.StringDict{ 64 | "error": starlark.NewBuiltin("error", errorFn), 65 | "catch": starlark.NewBuiltin("catch", catch), 66 | "matches": starlark.NewBuiltin("matches", matches), 67 | "module": starlark.NewBuiltin("module", starlarkstruct.MakeModule), 68 | "_freeze": starlark.NewBuiltin("freeze", freeze), 69 | } 70 | filename := DataFile("starlark/test", "assert.star") 71 | thread := new(starlark.Thread) 72 | assert, assertErr = starlark.ExecFile(thread, filename, assertStarFile, predeclared) 73 | }) 74 | return assert, assertErr 75 | } 76 | 77 | // catch(f) evaluates f() and returns its evaluation error message 78 | // if it failed or None if it succeeded. 79 | func catch(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 80 | var fn starlark.Callable 81 | if err := starlark.UnpackArgs("catch", args, kwargs, "fn", &fn); err != nil { 82 | return nil, err 83 | } 84 | if _, err := starlark.Call(thread, fn, nil, nil); err != nil { 85 | return starlark.String(err.Error()), nil 86 | } 87 | return starlark.None, nil 88 | } 89 | 90 | // matches(pattern, str) reports whether string str matches the regular expression pattern. 91 | func matches(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 92 | var pattern, str string 93 | if err := starlark.UnpackArgs("matches", args, kwargs, "pattern", &pattern, "str", &str); err != nil { 94 | return nil, err 95 | } 96 | ok, err := regexp.MatchString(pattern, str) 97 | if err != nil { 98 | return nil, fmt.Errorf("matches: %s", err) 99 | } 100 | return starlark.Bool(ok), nil 101 | } 102 | 103 | // error(x) reports an error to the Go test framework. 104 | func errorFn(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 105 | if len(args) != 1 { 106 | return nil, fmt.Errorf("error: got %d arguments, want 1", len(args)) 107 | } 108 | buf := new(strings.Builder) 109 | stk := thread.CallStack() 110 | stk.Pop() 111 | fmt.Fprintf(buf, "%sError: ", stk) 112 | if s, ok := starlark.AsString(args[0]); ok { 113 | buf.WriteString(s) 114 | } else { 115 | buf.WriteString(args[0].String()) 116 | } 117 | GetReporter(thread).Error(buf.String()) 118 | return starlark.None, nil 119 | } 120 | 121 | // freeze(x) freezes its operand. 122 | func freeze(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 123 | if len(kwargs) > 0 { 124 | return nil, fmt.Errorf("freeze does not accept keyword arguments") 125 | } 126 | if len(args) != 1 { 127 | return nil, fmt.Errorf("freeze got %d arguments, wants 1", len(args)) 128 | } 129 | args[0].Freeze() 130 | return args[0], nil 131 | } 132 | 133 | // DataFile returns the effective filename of the specified 134 | // test data resource. The function abstracts differences between 135 | // 'go build', under which a test runs in its package directory, 136 | // and Blaze, under which a test runs in the root of the tree. 137 | var DataFile = func(pkgdir, filename string) string { 138 | // Check if we're being run by Bazel and change directories if so. 139 | // TEST_SRCDIR and TEST_WORKSPACE are set by the Bazel test runner, so that makes a decent check 140 | testSrcdir := os.Getenv("TEST_SRCDIR") 141 | testWorkspace := os.Getenv("TEST_WORKSPACE") 142 | if testSrcdir != "" && testWorkspace != "" { 143 | return filepath.Join(testSrcdir, "net_starlark_go", pkgdir, filename) 144 | } 145 | 146 | return filepath.Join(build.Default.GOPATH, "src/github.com/mcuadros/ascode", pkgdir, filename) 147 | } 148 | -------------------------------------------------------------------------------- /starlark/types/validate.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "go.starlark.net/starlark" 8 | "go.starlark.net/starlarkstruct" 9 | ) 10 | 11 | // ValidationError is an error returned by Validabler.Validate. 12 | type ValidationError struct { 13 | // Msg reason of the error 14 | Msg string 15 | // CallStack of the instantiation of the value being validated. 16 | CallStack starlark.CallStack 17 | } 18 | 19 | // NewValidationError returns a new ValidationError. 20 | func NewValidationError(cs starlark.CallStack, format string, args ...interface{}) *ValidationError { 21 | return &ValidationError{ 22 | Msg: fmt.Sprintf(format, args...), 23 | CallStack: cs, 24 | } 25 | } 26 | 27 | func (e *ValidationError) Error() string { 28 | return fmt.Sprintf("%s: %s", e.CallStack.At(1).Pos, e.Msg) 29 | } 30 | 31 | // Value returns the error as a starlark.Value. 32 | func (e *ValidationError) Value() starlark.Value { 33 | values := []starlark.Tuple{ 34 | {starlark.String("msg"), starlark.String(e.Msg)}, 35 | {starlark.String("pos"), starlark.String(e.CallStack.At(1).Pos.String())}, 36 | } 37 | 38 | return starlarkstruct.FromKeywords(starlarkstruct.Default, values) 39 | } 40 | 41 | // ValidationErrors represents a list of ValidationErrors. 42 | type ValidationErrors []*ValidationError 43 | 44 | // Value returns the errors as a starlark.Value. 45 | func (e ValidationErrors) Value() starlark.Value { 46 | values := make([]starlark.Value, len(e)) 47 | for i, err := range e { 48 | values[i] = err.Value() 49 | } 50 | 51 | return starlark.NewList(values) 52 | } 53 | 54 | // Validabler defines if the resource is validable. 55 | type Validabler interface { 56 | Validate() ValidationErrors 57 | } 58 | 59 | // BuiltinValidate returns a starlak.Builtin function to validate objects 60 | // implementing the Validabler interface. 61 | // 62 | // outline: types 63 | // functions: 64 | // validate(resource) list 65 | // Returns a list with validating errors if any. A validating error is 66 | // a struct with two fields: `msg` and `pos` 67 | // params: 68 | // resource 69 | // resource to be validated. 70 | // 71 | func BuiltinValidate() starlark.Value { 72 | return starlark.NewBuiltin("validate", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { 73 | if args.Len() != 1 { 74 | return nil, fmt.Errorf("exactly one argument is required") 75 | } 76 | 77 | value := args.Index(0) 78 | v, ok := value.(Validabler) 79 | if !ok { 80 | return nil, fmt.Errorf("value type %s doesn't support validation", value.Type()) 81 | } 82 | 83 | errors := v.Validate() 84 | return errors.Value(), nil 85 | }) 86 | } 87 | 88 | // Validate honors the Validabler interface. 89 | func (t *Terraform) Validate() (errs ValidationErrors) { 90 | if t.b != nil { 91 | errs = append(errs, t.b.Validate()...) 92 | } 93 | 94 | errs = append(errs, t.p.Validate()...) 95 | return 96 | } 97 | 98 | // Validate honors the Validabler interface. 99 | func (d *Dict) Validate() (errs ValidationErrors) { 100 | for _, v := range d.Keys() { 101 | p, _, _ := d.Get(v) 102 | t, ok := p.(Validabler) 103 | if !ok { 104 | continue 105 | } 106 | 107 | errs = append(errs, t.Validate()...) 108 | } 109 | 110 | return 111 | } 112 | 113 | // Validate honors the Validabler interface. 114 | func (p *Provider) Validate() (errs ValidationErrors) { 115 | errs = append(errs, p.Resource.Validate()...) 116 | errs = append(errs, p.dataSources.Validate()...) 117 | errs = append(errs, p.resources.Validate()...) 118 | 119 | return 120 | } 121 | 122 | // Validate honors the Validabler interface. 123 | func (g *ResourceCollectionGroup) Validate() (errs ValidationErrors) { 124 | names := make(sort.StringSlice, len(g.collections)) 125 | var i int 126 | for name := range g.collections { 127 | names[i] = name 128 | i++ 129 | } 130 | 131 | sort.Sort(names) 132 | for _, name := range names { 133 | errs = append(errs, g.collections[name].Validate()...) 134 | } 135 | 136 | return 137 | } 138 | 139 | // Validate honors the Validabler interface. 140 | func (c *ResourceCollection) Validate() (errs ValidationErrors) { 141 | if c.nestedblock != nil { 142 | l := c.Len() 143 | max, min := c.nestedblock.MaxItems, c.nestedblock.MinItems 144 | if max != 0 && l > max { 145 | errs = append(errs, NewValidationError(c.parent.CallStack(), 146 | "%s: max. length is %d, current len %d", c, max, l, 147 | )) 148 | } 149 | 150 | if l < min { 151 | errs = append(errs, NewValidationError(c.parent.CallStack(), 152 | "%s: min. length is %d, current len %d", c, min, l, 153 | )) 154 | } 155 | } 156 | 157 | for i := 0; i < c.Len(); i++ { 158 | errs = append(errs, c.Index(i).(*Resource).Validate()...) 159 | } 160 | 161 | return 162 | } 163 | 164 | // Validate honors the Validabler interface. 165 | func (r *Resource) Validate() ValidationErrors { 166 | return append( 167 | r.doValidateAttributes(), 168 | r.doValidateBlocks()..., 169 | ) 170 | } 171 | 172 | func (r *Resource) doValidateAttributes() (errs ValidationErrors) { 173 | for k, attr := range r.block.Attributes { 174 | if attr.Optional { 175 | continue 176 | } 177 | 178 | v := r.values.Get(k) 179 | if attr.Required { 180 | fails := v == nil 181 | if !fails { 182 | if l, ok := v.Starlark().(*starlark.List); ok && l.Len() == 0 { 183 | fails = true 184 | } 185 | } 186 | 187 | if fails { 188 | errs = append(errs, NewValidationError(r.CallStack(), "%s: attr %q is required", r, k)) 189 | } 190 | } 191 | } 192 | 193 | return 194 | } 195 | 196 | func (r *Resource) doValidateBlocks() (errs ValidationErrors) { 197 | for k, block := range r.block.BlockTypes { 198 | v := r.values.Get(k) 199 | if block.MinItems > 0 && v == nil { 200 | errs = append(errs, NewValidationError(r.CallStack(), "%s: attr %q is required", r, k)) 201 | continue 202 | } 203 | 204 | if v == nil { 205 | continue 206 | } 207 | 208 | errs = append(errs, v.Starlark().(Validabler).Validate()...) 209 | } 210 | 211 | return 212 | } 213 | -------------------------------------------------------------------------------- /terraform/plugins.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "archive/zip" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | 15 | "github.com/hashicorp/go-hclog" 16 | "github.com/hashicorp/go-plugin" 17 | "github.com/hashicorp/terraform/addrs" 18 | "github.com/hashicorp/terraform/command" 19 | tfplugin "github.com/hashicorp/terraform/plugin" 20 | "github.com/hashicorp/terraform/plugin/discovery" 21 | "github.com/mitchellh/cli" 22 | ) 23 | 24 | // PluginManager is a wrapper around the terraform tools to download and execute 25 | // terraform plugins, like providers and provisioners. 26 | type PluginManager struct { 27 | Path string 28 | } 29 | 30 | // Provider returns a client and the metadata for a given provider and version, 31 | // first try to locate the provider in the local path, if not found, it 32 | // downloads it from terraform registry. If forceLocal just tries to find 33 | // the binary in the local filesystem. 34 | func (m *PluginManager) Provider(provider, version string, forceLocal bool) (*plugin.Client, discovery.PluginMeta, error) { 35 | meta, ok := m.getLocal("provider", provider, version) 36 | if !ok && !forceLocal { 37 | meta, ok, _ = m.getProviderRemoteDirectDownload(provider, version) 38 | if ok { 39 | return client(meta), meta, nil 40 | } 41 | 42 | var err error 43 | meta, _, err = m.getProviderRemote(provider, version) 44 | if err != nil { 45 | return nil, discovery.PluginMeta{}, err 46 | } 47 | 48 | } 49 | 50 | return client(meta), meta, nil 51 | } 52 | 53 | // Provisioner returns a client and the metadata for a given provisioner, it 54 | // try to locate it at the local Path, if not try to execute it from the 55 | // built-in plugins in the terraform binary. 56 | func (m *PluginManager) Provisioner(provisioner string) (*plugin.Client, discovery.PluginMeta, error) { 57 | if !IsTerraformBinaryAvailable() { 58 | return nil, discovery.PluginMeta{}, ErrTerraformNotAvailable 59 | } 60 | 61 | meta, ok := m.getLocal("provisioner", provisioner, "") 62 | if ok { 63 | return client(meta), meta, nil 64 | } 65 | 66 | // fallback to terraform internal provisioner. 67 | cmdLine, _ := command.BuildPluginCommandString("provisioner", provisioner) 68 | cmdArgv := strings.Split(cmdLine, command.TFSPACE) 69 | 70 | // override the internal to the terraform binary. 71 | cmdArgv[0] = "terraform" 72 | 73 | meta = discovery.PluginMeta{ 74 | Name: provisioner, 75 | Path: strings.Join(cmdArgv, command.TFSPACE), 76 | } 77 | 78 | return client(meta), meta, nil 79 | } 80 | 81 | func client(m discovery.PluginMeta) *plugin.Client { 82 | logger := hclog.New(&hclog.LoggerOptions{ 83 | Name: "plugin", 84 | Level: hclog.Error, 85 | Output: os.Stderr, 86 | }) 87 | 88 | cmdArgv := strings.Split(m.Path, command.TFSPACE) 89 | 90 | return plugin.NewClient(&plugin.ClientConfig{ 91 | Cmd: exec.Command(cmdArgv[0], cmdArgv[1:]...), 92 | HandshakeConfig: tfplugin.Handshake, 93 | VersionedPlugins: tfplugin.VersionedPlugins, 94 | Managed: true, 95 | Logger: logger, 96 | AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 97 | AutoMTLS: true, 98 | }) 99 | } 100 | 101 | const releaseTemplateURL = "https://releases.hashicorp.com/terraform-provider-%s/%s/terraform-provider-%[1]s_%[2]s_%s_%s.zip" 102 | 103 | func (m *PluginManager) getProviderRemoteDirectDownload(provider, v string) (discovery.PluginMeta, bool, error) { 104 | url := fmt.Sprintf(releaseTemplateURL, provider, v, runtime.GOOS, runtime.GOARCH) 105 | if err := m.downloadURL(url); err != nil { 106 | return discovery.PluginMeta{}, false, err 107 | } 108 | 109 | meta, ok := m.getLocal("provider", provider, v) 110 | return meta, ok, nil 111 | } 112 | 113 | func (m *PluginManager) downloadURL(url string) error { 114 | resp, err := http.Get(url) 115 | if err != nil { 116 | return fmt.Errorf("error downloading %s file: %w", url, err) 117 | } 118 | 119 | if resp.StatusCode != http.StatusOK { 120 | return fmt.Errorf("invalid URL: %s", url) 121 | } 122 | 123 | defer resp.Body.Close() 124 | file, err := ioutil.TempFile("", "ascode") 125 | if err != nil { 126 | return err 127 | 128 | } 129 | 130 | if _, err := io.Copy(file, resp.Body); err != nil { 131 | return fmt.Errorf("error downloading %s file: %w", url, err) 132 | } 133 | 134 | file.Close() 135 | defer os.Remove(file.Name()) 136 | 137 | archive, err := zip.OpenReader(file.Name()) 138 | if err != nil { 139 | panic(err) 140 | } 141 | 142 | defer archive.Close() 143 | 144 | for _, f := range archive.File { 145 | file := filepath.Join(m.Path, f.Name) 146 | 147 | if !strings.HasPrefix(file, filepath.Clean(m.Path)+string(os.PathSeparator)) { 148 | return fmt.Errorf("invalid path") 149 | } 150 | 151 | output, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | r, err := f.Open() 157 | if err != nil { 158 | return err 159 | } 160 | 161 | if _, err := io.Copy(output, r); err != nil { 162 | return err 163 | } 164 | 165 | output.Close() 166 | r.Close() 167 | } 168 | 169 | return nil 170 | } 171 | 172 | const defaultVersionContraint = "> 0" 173 | 174 | func (m *PluginManager) getProviderRemote(provider, v string) (discovery.PluginMeta, bool, error) { 175 | if v == "" { 176 | v = defaultVersionContraint 177 | } 178 | 179 | m.getProviderRemoteDirectDownload(provider, v) 180 | installer := &discovery.ProviderInstaller{ 181 | Dir: m.Path, 182 | PluginProtocolVersion: discovery.PluginInstallProtocolVersion, 183 | Ui: cli.NewMockUi(), 184 | } 185 | 186 | addr := addrs.NewLegacyProvider(provider) 187 | meta, _, err := installer.Get(addr, discovery.ConstraintStr(v).MustParse()) 188 | if err != nil { 189 | return discovery.PluginMeta{}, false, err 190 | } 191 | 192 | return meta, true, nil 193 | } 194 | 195 | func (m *PluginManager) getLocal(kind, provider, version string) (discovery.PluginMeta, bool) { 196 | set := discovery.FindPlugins(kind, []string{m.Path}) 197 | set = set.WithName(provider) 198 | if len(set) == 0 { 199 | return discovery.PluginMeta{}, false 200 | } 201 | 202 | if version != "" { 203 | set = set.WithVersion(discovery.VersionStr(version).MustParse()) 204 | } 205 | 206 | if len(set) == 0 { 207 | return discovery.PluginMeta{}, false 208 | } 209 | 210 | return set.Newest(), true 211 | } 212 | 213 | // ErrTerraformNotAvailable error used when `terraform` binary in not in the 214 | // path and we try to use a provisioner. 215 | var ErrTerraformNotAvailable = fmt.Errorf("provisioner error: executable file 'terraform' not found in $PATH") 216 | 217 | // IsTerraformBinaryAvailable determines if Terraform binary is available in 218 | // the path of the system. Terraform binary is a requirement for executing 219 | // provisioner plugins, since they are built-in on the Terrafrom binary. :( 220 | // 221 | // https://github.com/hashicorp/terraform/issues/20896#issuecomment-479054649 222 | func IsTerraformBinaryAvailable() bool { 223 | _, err := exec.LookPath("terraform") 224 | return err == nil 225 | } 226 | -------------------------------------------------------------------------------- /starlark/types/value.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/hashicorp/terraform/configs/configschema" 8 | "github.com/zclconf/go-cty/cty" 9 | "go.starlark.net/starlark" 10 | ) 11 | 12 | // Value is helper to manipulate and transform starlark.Value to go types and 13 | // cty.Value. 14 | type Value struct { 15 | t Type 16 | v starlark.Value 17 | } 18 | 19 | // MustValue returns a Value from a starlark.Value, it panics if error. 20 | func MustValue(v starlark.Value) *Value { 21 | value, err := NewValue(v) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | return value 27 | } 28 | 29 | // NewValue returns a Value from a starlark.Value. 30 | func NewValue(v starlark.Value) (*Value, error) { 31 | t, err := NewTypeFromStarlark(v.Type()) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &Value{t: *t, v: v}, nil 37 | } 38 | 39 | // Starlark returns the starlark.Value. 40 | func (v *Value) Starlark() starlark.Value { 41 | return v.v 42 | } 43 | 44 | // Type returns the Type of the value. 45 | func (v *Value) Type() *Type { 46 | return &v.t 47 | } 48 | 49 | // Cty returns the cty.Value. 50 | func (v *Value) Cty() cty.Value { 51 | switch v.t.Starlark() { 52 | case "string": 53 | return cty.StringVal(v.Interface().(string)) 54 | case "int": 55 | return cty.NumberIntVal(v.Interface().(int64)) 56 | case "float": 57 | return cty.NumberFloatVal(v.Interface().(float64)) 58 | case "bool": 59 | return cty.BoolVal(v.Interface().(bool)) 60 | case "list": 61 | list := v.v.(*starlark.List) 62 | if list.Len() == 0 { 63 | return cty.ListValEmpty(v.t.Cty()) 64 | } 65 | 66 | values := make([]cty.Value, list.Len()) 67 | for i := 0; i < list.Len(); i++ { 68 | values[i] = MustValue(list.Index(i)).Cty() 69 | } 70 | 71 | return cty.ListVal(values) 72 | case "dict": 73 | dict := v.v.(*starlark.Dict) 74 | values := make(map[string]cty.Value) 75 | for _, t := range dict.Items() { 76 | key := fmt.Sprintf("%s", MustValue(t.Index(0)).Interface()) 77 | values[key] = MustValue(t.Index(1)).Cty() 78 | } 79 | 80 | return cty.MapVal(values) 81 | case "Attribute": 82 | return cty.StringVal(v.v.(*Attribute).GoString()) 83 | default: 84 | panic(fmt.Sprintf("unhandled: %s", v.t.Starlark())) 85 | } 86 | } 87 | 88 | // Interface returns the value as a Go value. 89 | func (v *Value) Interface() interface{} { 90 | switch cast := v.v.(type) { 91 | case starlark.Bool: 92 | return bool(cast) 93 | case starlark.String: 94 | return string(cast) 95 | case starlark.Int: 96 | i, _ := cast.Int64() 97 | return i 98 | case starlark.Float: 99 | return float64(cast) 100 | case *ResourceCollection: 101 | return MustValue(cast.List).Interface() 102 | case *starlark.List: 103 | out := make([]interface{}, cast.Len()) 104 | for i := 0; i < cast.Len(); i++ { 105 | out[i] = MustValue(cast.Index(i)).Interface() 106 | } 107 | 108 | return out 109 | 110 | case *starlark.Dict: 111 | values := make(map[string]interface{}) 112 | for _, t := range cast.Items() { 113 | key := fmt.Sprintf("%s", MustValue(t.Index(0)).Interface()) 114 | values[key] = MustValue(t.Index(1)).Interface() 115 | } 116 | 117 | return values 118 | default: 119 | return v 120 | } 121 | } 122 | 123 | // Hash honors the starlark.Value interface. 124 | func (v *Value) Hash() (uint32, error) { 125 | switch value := v.v.(type) { 126 | case *starlark.List: 127 | // Use same algorithm as Python. 128 | var x, mult uint32 = 0x345678, 1000003 129 | for i := 0; i < value.Len(); i++ { 130 | y, err := value.Index(i).Hash() 131 | if err != nil { 132 | return 0, err 133 | } 134 | x = x ^ y*mult 135 | mult += 82520 + uint32(value.Len()+value.Len()) 136 | } 137 | return x, nil 138 | default: 139 | return value.Hash() 140 | } 141 | } 142 | 143 | // NamedValue represents a Value with a given name. 144 | type NamedValue struct { 145 | Name string 146 | *Value 147 | } 148 | 149 | // Values is a list of NamedValues. 150 | type Values struct { 151 | names sort.StringSlice 152 | values map[string]*NamedValue 153 | } 154 | 155 | // NewValues return a new instance of Values 156 | func NewValues() *Values { 157 | return &Values{values: make(map[string]*NamedValue)} 158 | } 159 | 160 | // Set sets a name and a value and returns it as a NamedValue. 161 | func (a *Values) Set(name string, v *Value) *NamedValue { 162 | if e, ok := a.values[name]; ok { 163 | e.Value = v 164 | return e 165 | } 166 | 167 | e := &NamedValue{Name: name, Value: v} 168 | a.values[name] = e 169 | a.names = append(a.names, name) 170 | return e 171 | } 172 | 173 | // Has returns true if Values contains a NamedValue with this name. 174 | func (a Values) Has(name string) bool { 175 | _, ok := a.values[name] 176 | return ok 177 | } 178 | 179 | // Get returns the NamedValue with the given name, if any. 180 | func (a Values) Get(name string) *NamedValue { 181 | if e, ok := a.values[name]; ok { 182 | return e 183 | } 184 | 185 | return nil 186 | } 187 | 188 | // Hash honors the starlark.Value interface. 189 | func (a Values) Hash() (uint32, error) { 190 | // Same algorithm as Tuple.hash, but with different primes. 191 | var x, m uint32 = 9199, 7207 192 | 193 | err := a.ForEach(func(v *NamedValue) error { 194 | namehash, _ := starlark.String(v.Name).Hash() 195 | x = x ^ 3*namehash 196 | y, err := v.Hash() 197 | if err != nil { 198 | return err 199 | } 200 | 201 | x = x ^ y*m 202 | m += 6203 203 | return nil 204 | }) 205 | 206 | if err != nil { 207 | return 0, err 208 | } 209 | 210 | return x, nil 211 | } 212 | 213 | // ToStringDict adds a name/value entry to d for each field of the struct. 214 | func (a Values) ToStringDict(d starlark.StringDict) { 215 | sort.Sort(a.names) // we sort the list before hash it. 216 | for _, name := range a.names { 217 | d[name] = a.values[name].Starlark() 218 | } 219 | } 220 | 221 | // ForEach call cb for each value on Values, it stop the iteration an error 222 | // is returned. 223 | func (a Values) ForEach(cb func(*NamedValue) error) error { 224 | sort.Sort(a.names) // we sort the list before hash it. 225 | 226 | for _, name := range a.names { 227 | if err := cb(a.values[name]); err != nil { 228 | return err 229 | } 230 | } 231 | 232 | return nil 233 | } 234 | 235 | // List return a list of NamedValues sorted by name. 236 | func (a Values) List() []*NamedValue { 237 | sort.Sort(a.names) // we sort the list before hash it. 238 | 239 | list := make([]*NamedValue, len(a.names)) 240 | for i, name := range a.names { 241 | list[i] = a.values[name] 242 | } 243 | 244 | return list 245 | } 246 | 247 | // Len return the length. 248 | func (a Values) Len() int { 249 | return len(a.values) 250 | } 251 | 252 | // Cty returns the cty.Value based on a given schema. 253 | func (a Values) Cty(schema *configschema.Block) cty.Value { 254 | values := make(map[string]cty.Value) 255 | for key, value := range schema.Attributes { 256 | v := value.EmptyValue() 257 | if a.Has(key) { 258 | v = a.Get(key).Cty() 259 | } 260 | 261 | values[key] = v 262 | } 263 | 264 | return cty.ObjectVal(values) 265 | } 266 | 267 | // Dict is a starlark.Dict HCLCompatible. 268 | type Dict struct { 269 | *starlark.Dict 270 | } 271 | 272 | // NewDict returns a new empty Dict. 273 | func NewDict() *Dict { 274 | return &Dict{starlark.NewDict(0)} 275 | } 276 | -------------------------------------------------------------------------------- /_documentation/starlark/name-binding-and-variables.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Name binding and variables' 3 | weight: 4 4 | --- 5 | 6 | After a Starlark file is parsed, but before its execution begins, the 7 | Starlark interpreter checks statically that the program is well formed. 8 | For example, `break` and `continue` statements may appear only within 9 | a loop; a `return` statement may appear only within a 10 | function; and `load` statements may appear only outside any function. 11 | 12 | _Name resolution_ is the static checking process that 13 | resolves names to variable bindings. 14 | During execution, names refer to variables. Statically, names denote 15 | places in the code where variables are created; these places are 16 | called _bindings_. A name may denote different bindings at different 17 | places in the program. The region of text in which a particular name 18 | refers to the same binding is called that binding's _scope_. 19 | 20 | Four Starlark constructs bind names, as illustrated in the example below: 21 | `load` statements (`a` and `b`), 22 | `def` statements (`c`), 23 | function parameters (`d`), 24 | and assignments (`e`, `h`, including the augmented assignment `e += 1`). 25 | Variables may be assigned or re-assigned explicitly (`e`, `h`), or implicitly, as 26 | in a `for`-loop (`f`) or comprehension (`g`, `i`). 27 | 28 | ```python 29 | load("lib.star", "a", b="B") 30 | 31 | def c(d): 32 | e = 0 33 | for f in d: 34 | print([True for g in f]) 35 | e += 1 36 | 37 | h = [2*i for i in a] 38 | ``` 39 | 40 | The environment of a Starlark program is structured as a tree of 41 | _lexical blocks_, each of which may contain name bindings. 42 | The tree of blocks is parallel to the syntax tree. 43 | Blocks are of five kinds. 44 | 45 | 46 | At the root of the tree is the _predeclared_ block, 47 | which binds several names implicitly. 48 | The set of predeclared names includes the universal 49 | constant values `None`, `True`, and `False`, and 50 | various built-in functions such as `len` and `list`; 51 | these functions are immutable and stateless. 52 | An application may pre-declare additional names 53 | to provide domain-specific functions to that file, for example. 54 | These additional functions may have side effects on the application. 55 | Starlark programs cannot change the set of predeclared bindings 56 | or assign new values to them. 57 | 58 | Nested beneath the predeclared block is the _module_ block, 59 | which contains the bindings of the current module. 60 | Bindings in the module block (such as `c`, and `h` in the 61 | example) are called _global_ and may be visible to other modules. 62 | The module block is empty at the start of the file 63 | and is populated by top-level binding statements. 64 | 65 | Nested beneath the module block is the _file_ block, 66 | which contains bindings local to the current file. 67 | Names in this block (such as `a` and `b` in the example) 68 | are bound only by `load` statements. 69 | The sets of names bound in the file block and in the module block do not overlap: 70 | it is an error for a load statement to bind the name of a global, 71 | or for a top-level statement to assign to a name bound by a load statement. 72 | 73 | A file block contains a _function_ block for each top-level 74 | function, and a _comprehension_ block for each top-level comprehension. 75 | Bindings in either of these kinds of block, 76 | and in the file block itself, are called _local_. 77 | (In the example, the bindings for `e`, `f`, `g`, and `i` are all local.) 78 | Additional functions and comprehensions, and their blocks, may be 79 | nested in any order, to any depth. 80 | 81 | If name is bound anywhere within a block, all uses of the name within 82 | the block are treated as references to that binding, 83 | even if the use appears before the binding. 84 | This is true even at the top level, unlike Python. 85 | The binding of `y` on the last line of the example below makes `y` 86 | local to the function `hello`, so the use of `y` in the print 87 | statement also refers to the local `y`, even though it appears 88 | earlier. 89 | 90 | ```python 91 | y = "goodbye" 92 | 93 | def hello(): 94 | for x in (1, 2): 95 | if x == 2: 96 | print(y) # prints "hello" 97 | if x == 1: 98 | y = "hello" 99 | ``` 100 | 101 | It is a dynamic error to evaluate a reference to a local variable 102 | before it has been bound: 103 | 104 | ```python 105 | def f(): 106 | print(x) # dynamic error: local variable x referenced before assignment 107 | x = "hello" 108 | ``` 109 | 110 | The same is true for global variables: 111 | 112 | ```python 113 | print(x) # dynamic error: global variable x referenced before assignment 114 | x = "hello" 115 | ``` 116 | 117 | It is a static error to bind a global variable already explicitly bound in the file: 118 | 119 | ```python 120 | x = 1 121 | x = 2 # static error: cannot reassign global x declared on line 1 122 | ``` 123 | 124 | 129 | 130 | If a name was pre-bound by the application, the Starlark program may 131 | explicitly bind it, but only once. 132 | 133 | An augmented assignment statement such as `x += y` is considered both a 134 | reference to `x` and a binding use of `x`, so it may not be used at 135 | top level. 136 | 137 | Implementation note: 138 | The Go implementation of Starlark permits augmented assignments to appear 139 | at top level if the `-globalreassign` flag is enabled. 140 | 141 | A function may refer to variables defined in an enclosing function. 142 | In this example, the inner function `f` refers to a variable `x` 143 | that is local to the outer function `squarer`. 144 | `x` is a _free variable_ of `f`. 145 | The function value (`f`) created by a `def` statement holds a 146 | reference to each of its free variables so it may use 147 | them even after the enclosing function has returned. 148 | 149 | ```python 150 | def squarer(): 151 | x = [0] 152 | def f(): 153 | x[0] += 1 154 | return x[0]*x[0] 155 | return f 156 | 157 | sq = squarer() 158 | print(sq(), sq(), sq(), sq()) # "1 4 9 16" 159 | ``` 160 | 161 | An inner function cannot assign to a variable bound in an enclosing 162 | function, because the assignment would bind the variable in the 163 | inner function. 164 | In the example below, the `x += 1` statement binds `x` within `f`, 165 | hiding the outer `x`. 166 | Execution fails because the inner `x` has not been assigned before the 167 | attempt to increment it. 168 | 169 | ```python 170 | def squarer(): 171 | x = 0 172 | def f(): 173 | x += 1 # dynamic error: local variable x referenced before assignment 174 | return x*x 175 | return f 176 | 177 | sq = squarer() 178 | ``` 179 | 180 | (Starlark has no equivalent of Python's `nonlocal` or `global` 181 | declarations, but as the first version of `squarer` showed, this 182 | omission can be worked around by using a list of a single element.) 183 | 184 | 185 | A name appearing after a dot, such as `split` in 186 | `get_filename().split('/')`, is not resolved statically. 187 | The [dot expression](/docs/starlark/expressions/#dot-expressions) `.split` is a dynamic operation 188 | on the value returned by `get_filename()`. 189 | 190 | -------------------------------------------------------------------------------- /starlark/module/docker/image.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "sync" 11 | 12 | "github.com/Masterminds/semver/v3" 13 | "github.com/containers/image/v5/docker" 14 | "github.com/containers/image/v5/docker/reference" 15 | "github.com/containers/image/v5/types" 16 | "go.starlark.net/starlark" 17 | "go.starlark.net/starlarkstruct" 18 | ) 19 | 20 | const ( 21 | // ModuleName defines the expected name for this Module when used 22 | // in starlark's load() function, eg: load('docker', 'docker') 23 | ModuleName = "docker" 24 | 25 | imageFuncName = "image" 26 | latestTag = "lastest" 27 | ) 28 | 29 | var ( 30 | once sync.Once 31 | dockerModule starlark.StringDict 32 | ) 33 | 34 | // LoadModule loads the os module. 35 | // It is concurrency-safe and idempotent. 36 | // 37 | // outline: docker 38 | // The docker modules allow you to manipulate docker image names. 39 | // path: docker 40 | func LoadModule() (starlark.StringDict, error) { 41 | once.Do(func() { 42 | dockerModule = starlark.StringDict{ 43 | "docker": &starlarkstruct.Module{ 44 | Name: "docker", 45 | Members: starlark.StringDict{ 46 | imageFuncName: starlark.NewBuiltin(imageFuncName, Image), 47 | }, 48 | }, 49 | } 50 | }) 51 | 52 | return dockerModule, nil 53 | } 54 | 55 | type sString = starlark.String 56 | 57 | // image represents a docker container image. 58 | // 59 | // outline: docker 60 | // types: 61 | // Image 62 | // Represents a docker container image. 63 | // 64 | // fields: 65 | // name string 66 | // Image name. Eg.: `docker.io/library/fedora` 67 | // domain string 68 | // Registry domain. Eg.: `docker.io`. 69 | // path string 70 | // Repository path. Eg.: `library/fedora` 71 | // 72 | // methods: 73 | // tags() list 74 | // List of all the tags for this container image. 75 | // version() string 76 | // Return the highest tag matching the image constraint. 77 | // params: 78 | // full bool 79 | // If `true` returns the image name plus the tag. Eg.: `docker.io/library/fedora:29` 80 | type image struct { 81 | tags []string 82 | ref types.ImageReference 83 | constraint string 84 | sString 85 | } 86 | 87 | // Image returns a starlak.Builtin function capable of instantiate 88 | // new Image instances. 89 | // 90 | // outline: docker 91 | // functions: 92 | // image(image, constraint) Image 93 | // Returns a new `Image` based on a given image and constraint. 94 | // 95 | // params: 96 | // image string 97 | // Container image name. Eg.: `ubuntu` or `quay.io/prometheus/prometheus`. 98 | // constraint string 99 | // [Semver](https://github.com/Masterminds/semver/#checking-version-constraints) contraint. Eg.: `1.2.*` 100 | // 101 | func Image( 102 | thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, 103 | ) (starlark.Value, error) { 104 | 105 | var image, constraint string 106 | err := starlark.UnpackArgs(imageFuncName, args, kwargs, "image", &image, "constraint", &constraint) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return newImage(image, constraint) 112 | } 113 | 114 | func newImage(name, constraint string) (*image, error) { 115 | ref, err := reference.ParseNormalizedNamed(name) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | if !reference.IsNameOnly(ref) { 121 | return nil, errors.New("no tag or digest allowed in reference") 122 | } 123 | 124 | dref, err := docker.NewReference(reference.TagNameOnly(ref)) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | return &image{ 130 | ref: dref, 131 | constraint: constraint, 132 | sString: starlark.String(ref.Name()), 133 | }, nil 134 | } 135 | 136 | func (i *image) Attr(name string) (starlark.Value, error) { 137 | switch name { 138 | case "name": 139 | return starlark.String(i.ref.DockerReference().Name()), nil 140 | case "domain": 141 | name := i.ref.DockerReference() 142 | return starlark.String(reference.Domain(name)), nil 143 | case "path": 144 | name := i.ref.DockerReference() 145 | return starlark.String(reference.Path(name)), nil 146 | case "tags": 147 | return starlark.NewBuiltin("tags", i.builtinTagsFunc), nil 148 | case "version": 149 | return starlark.NewBuiltin("version", i.builtinVersionFunc), nil 150 | } 151 | 152 | return nil, nil 153 | } 154 | 155 | func (i *image) AttrNames() []string { 156 | return []string{"name", "domain", "path", "tags", "version"} 157 | } 158 | 159 | func (i *image) builtinVersionFunc( 160 | _ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, 161 | ) (starlark.Value, error) { 162 | 163 | var full bool 164 | starlark.UnpackArgs("version", args, kwargs, "full", &full) 165 | 166 | v, err := i.getVersion() 167 | if err != nil { 168 | return starlark.None, err 169 | } 170 | 171 | if full { 172 | v = fmt.Sprintf("%s:%s", i.ref.DockerReference().Name(), v) 173 | } 174 | 175 | return starlark.String(v), nil 176 | } 177 | 178 | func (i *image) builtinTagsFunc( 179 | _ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, 180 | ) (starlark.Value, error) { 181 | return i.getTags() 182 | } 183 | 184 | func (i *image) getTags() (*starlark.List, error) { 185 | if len(i.tags) != 0 { 186 | return listToStarlark(i.tags), nil 187 | } 188 | 189 | var err error 190 | i.tags, err = docker.GetRepositoryTags(context.TODO(), imageSystemContext(), i.ref) 191 | if err != nil { 192 | return nil, fmt.Errorf("error listing repository tags: %v", err) 193 | } 194 | 195 | i.tags = sortTags(i.tags) 196 | return listToStarlark(i.tags), nil 197 | } 198 | 199 | func (i *image) getVersion() (string, error) { 200 | if i.constraint == latestTag { 201 | return latestTag, nil 202 | } 203 | 204 | _, err := i.getTags() 205 | if err != nil { 206 | return "", err 207 | } 208 | 209 | if len(i.tags) == 0 { 210 | return "", fmt.Errorf("no tags form this image") 211 | } 212 | 213 | c, err := semver.NewConstraint(i.constraint) 214 | if err != nil { 215 | return i.doGetVersionExactTag(i.constraint) 216 | } 217 | 218 | return i.doGetVersionWithConstraint(c) 219 | } 220 | 221 | func (i *image) doGetVersionWithConstraint(c *semver.Constraints) (string, error) { 222 | // it assumes tags are always sorted from higher to lower 223 | for _, tag := range i.tags { 224 | v, err := semver.NewVersion(tag) 225 | if err == nil { 226 | if c.Check(v) { 227 | return tag, nil 228 | } 229 | } 230 | } 231 | 232 | return "", nil 233 | } 234 | 235 | func (i *image) doGetVersionExactTag(expected string) (string, error) { 236 | for _, tag := range i.tags { 237 | if tag == expected { 238 | return tag, nil 239 | } 240 | } 241 | 242 | return "", fmt.Errorf("tag %q not found in repository", expected) 243 | } 244 | 245 | func sortTags(tags []string) []string { 246 | versions, others := listToVersion(tags) 247 | sort.Sort(sort.Reverse(semver.Collection(versions))) 248 | return versionToList(versions, others) 249 | } 250 | 251 | func listToStarlark(input []string) *starlark.List { 252 | output := make([]starlark.Value, len(input)) 253 | for i, v := range input { 254 | output[i] = starlark.String(v) 255 | } 256 | 257 | return starlark.NewList(output) 258 | } 259 | 260 | func listToVersion(input []string) ([]*semver.Version, []string) { 261 | versions := make([]*semver.Version, 0) 262 | other := make([]string, 0) 263 | 264 | for _, text := range input { 265 | v, err := semver.NewVersion(text) 266 | if err == nil && v.Prerelease() == "" { 267 | versions = append(versions, v) 268 | continue 269 | } 270 | 271 | other = append(other, text) 272 | } 273 | 274 | return versions, other 275 | } 276 | 277 | func versionToList(versions []*semver.Version, other []string) []string { 278 | output := make([]string, 0) 279 | for _, v := range versions { 280 | output = append(output, v.Original()) 281 | } 282 | 283 | return append(output, other...) 284 | } 285 | 286 | func imageSystemContext() *types.SystemContext { 287 | cfgFile := os.Getenv("DOCKER_CONFIG_FILE") 288 | if cfgFile == "" { 289 | if cfgPath := os.Getenv("DOCKER_CONFIG"); cfgPath != "" { 290 | cfgFile = filepath.Join(cfgPath, "config.json") 291 | } 292 | } 293 | 294 | return &types.SystemContext{ 295 | AuthFilePath: cfgFile, 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /starlark/module/url/url.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "net/url" 5 | "sync" 6 | 7 | "go.starlark.net/starlark" 8 | "go.starlark.net/starlarkstruct" 9 | ) 10 | 11 | const ( 12 | // ModuleName defines the expected name for this Module when used 13 | // in starlark's load() function, eg: load('io/ioutil', 'json') 14 | ModuleName = "url" 15 | 16 | pathEscapeFuncName = "path_escape" 17 | pathUnescapeFuncName = "path_unescape" 18 | queryEscapeFuncName = "query_escape" 19 | queryUnescapeFuncName = "query_unescape" 20 | parseFuncName = "parse" 21 | ) 22 | 23 | var ( 24 | once sync.Once 25 | ioutilModule starlark.StringDict 26 | ) 27 | 28 | // LoadModule loads the url module. 29 | // It is concurrency-safe and idempotent. 30 | // 31 | // outline: url 32 | // url parses URLs and implements query escaping. 33 | // path: url 34 | func LoadModule() (starlark.StringDict, error) { 35 | once.Do(func() { 36 | ioutilModule = starlark.StringDict{ 37 | "url": &starlarkstruct.Module{ 38 | Name: "url", 39 | Members: starlark.StringDict{ 40 | pathEscapeFuncName: starlark.NewBuiltin(pathEscapeFuncName, PathEscape), 41 | pathUnescapeFuncName: starlark.NewBuiltin(pathUnescapeFuncName, PathUnescape), 42 | queryEscapeFuncName: starlark.NewBuiltin(queryEscapeFuncName, QueryEscape), 43 | queryUnescapeFuncName: starlark.NewBuiltin(queryUnescapeFuncName, QueryUnescape), 44 | parseFuncName: starlark.NewBuiltin(parseFuncName, Parse), 45 | }, 46 | }, 47 | } 48 | }) 49 | 50 | return ioutilModule, nil 51 | } 52 | 53 | // PathEscape escapes the string so it can be safely placed inside a URL path 54 | // segment, replacing special characters (including /) with %XX sequences as 55 | // needed. 56 | // 57 | // outline: url 58 | // functions: 59 | // path_escape(s) 60 | // escapes the string so it can be safely placed inside a URL path 61 | // segment, replacing special characters (including /) with %XX 62 | // sequences as needed. 63 | // params: 64 | // s string 65 | func PathEscape(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 66 | var s string 67 | 68 | err := starlark.UnpackArgs(pathEscapeFuncName, args, kwargs, "s", &s) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return starlark.String(url.PathEscape(s)), nil 74 | } 75 | 76 | // PathUnescape does the inverse transformation of PathEscape, converting each 77 | // 3-byte encoded substring of the form "%AB" into the hex-decoded byte 0xAB. It 78 | // returns an error if any % is not followed by two hexadecimal digits. 79 | // PathUnescape is identical to QueryUnescape except that it does not unescape 80 | // '+' to ' ' (space). 81 | // 82 | // outline: url 83 | // functions: 84 | // path_unescape(s) 85 | // does the inverse transformation of path_escape, converting each 86 | // 3-byte encoded substring of the form "%AB" into the hex-decoded byte 87 | // 0xAB. It returns an error if any % is not followed by two hexadecimal 88 | // digits. path_unescape is identical to query_unescape except that it 89 | // does not unescape '+' to ' ' (space). 90 | // params: 91 | // s string 92 | func PathUnescape(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 93 | var s string 94 | 95 | err := starlark.UnpackArgs(pathUnescapeFuncName, args, kwargs, "s", &s) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | output, err := url.PathUnescape(s) 101 | return starlark.String(output), err 102 | } 103 | 104 | // QueryEscape escapes the string so it can be safely placed inside a URL query. 105 | // 106 | // outline: url 107 | // functions: 108 | // path_escape(s) 109 | // escapes the string so it can be safely placed inside a URL query. 110 | // params: 111 | // s string 112 | func QueryEscape(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 113 | var s string 114 | 115 | err := starlark.UnpackArgs(queryEscapeFuncName, args, kwargs, "s", &s) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return starlark.String(url.QueryEscape(s)), nil 121 | } 122 | 123 | // QueryUnescape does the inverse transformation of QueryEscape, converting each 124 | // 3-byte encoded substring of the form "%AB" into the hex-decoded byte 0xAB. 125 | // It returns an error if any % is not followed by two hexadecimal digits. 126 | // 127 | // outline: url 128 | // functions: 129 | // path_unescape(s) 130 | // does the inverse transformation of query_escape, converting each 131 | // 3-byte encoded substring of the form "%AB" into the hex-decoded byte 132 | // 0xAB. It returns an error if any % is not followed by two hexadecimal 133 | // digits. 134 | // params: 135 | // s string 136 | func QueryUnescape(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 137 | var s string 138 | 139 | err := starlark.UnpackArgs(queryUnescapeFuncName, args, kwargs, "s", &s) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | output, err := url.QueryUnescape(s) 145 | return starlark.String(output), err 146 | } 147 | 148 | type sString = starlark.String 149 | 150 | // URL represents a parsed URL (technically, a URI reference). 151 | // 152 | // outline: url 153 | // types: 154 | // URL 155 | // Represents a parsed URL (technically, a URI reference). 156 | // 157 | // fields: 158 | // scheme string 159 | // opaque string 160 | // Encoded opaque data. 161 | // username string 162 | // Username information. 163 | // password string 164 | // Password information. 165 | // host string 166 | // Host or host:port. 167 | // path string 168 | // Path (relative paths may omit leading slash). 169 | // raw_query string 170 | // Encoded query values, without '?'. 171 | // fragment string 172 | // Fragment for references, without '#'. 173 | // 174 | type URL struct { 175 | url url.URL 176 | sString 177 | } 178 | 179 | // Parse parses rawurl into a URL structure. 180 | // 181 | // outline: url 182 | // functions: 183 | // parse(rawurl) URL 184 | // Parse parses rawurl into a URL structure. 185 | // 186 | // params: 187 | // rawurl string 188 | // rawurl may be relative (a path, without a host) or absolute 189 | // (starting with a scheme). Trying to parse a hostname and path 190 | // without a scheme is invalid but may not necessarily return an 191 | // error, due to parsing ambiguities. 192 | func Parse( 193 | thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, 194 | ) (starlark.Value, error) { 195 | 196 | var rawurl string 197 | err := starlark.UnpackArgs(parseFuncName, args, kwargs, "rawurl", &rawurl) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | url, err := url.Parse(rawurl) 203 | if err != nil { 204 | return starlark.None, err 205 | } 206 | 207 | return &URL{ 208 | url: *url, 209 | sString: starlark.String(url.String()), 210 | }, nil 211 | } 212 | 213 | func (u *URL) Attr(name string) (starlark.Value, error) { 214 | switch name { 215 | case "scheme": 216 | return starlark.String(u.url.Scheme), nil 217 | case "opaque": 218 | return starlark.String(u.url.Opaque), nil 219 | case "username": 220 | if u.url.User == nil { 221 | return starlark.None, nil 222 | } 223 | 224 | return starlark.String(u.url.User.Username()), nil 225 | case "password": 226 | if u.url.User == nil { 227 | return starlark.None, nil 228 | } 229 | 230 | password, provided := u.url.User.Password() 231 | if !provided { 232 | return starlark.None, nil 233 | } 234 | 235 | return starlark.String(password), nil 236 | case "host": 237 | return starlark.String(u.url.Host), nil 238 | case "path": 239 | return starlark.String(u.url.Path), nil 240 | case "raw_query": 241 | return starlark.String(u.url.RawQuery), nil 242 | case "fragment": 243 | return starlark.String(u.url.Fragment), nil 244 | } 245 | 246 | return nil, nil 247 | } 248 | 249 | func (*URL) AttrNames() []string { 250 | return []string{ 251 | "scheme", "opaque", "username", "password", "host", "path", 252 | "raw_query", "fragment", 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /starlark/types/attribute.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | // sTring alias required to avoid name collision with the method String. 11 | type sString = starlark.String 12 | 13 | // BuiltinRef returns a starlak.Builtin function to generate a reference to a 14 | // resource argument. 15 | // 16 | // outline: types 17 | // functions: 18 | // ref(resource, argument) string 19 | // Returns a reference to a resource argument. 20 | // params: 21 | // resource 22 | // resource to be referenced. 23 | // field string 24 | // field to be referenced. 25 | // 26 | // examples: 27 | // ref.star 28 | // 29 | func BuiltinRef() starlark.Value { 30 | return starlark.NewBuiltin("ref", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 31 | var resource *Resource 32 | var argument string 33 | err := starlark.UnpackArgs("ref", args, kwargs, "resource", &resource, "argument", &argument) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | attr, ok := resource.block.Attributes[argument] 39 | if !ok { 40 | return nil, fmt.Errorf("%s has no .%s field", resource, argument) 41 | } 42 | 43 | return NewAttribute(resource, attr.Type, argument), nil 44 | }) 45 | } 46 | 47 | // Attribute is a reference to an argument of a Resource. Used mainly 48 | // for Computed arguments of Resources. 49 | // 50 | // outline: types 51 | // types: 52 | // Attribute 53 | // Attribute is a reference to an argument of a Resource. Used mainly 54 | // for Computed arguments of Resources. 55 | // 56 | // Attribute behaves as the type of the argument represented, this means 57 | // that them can be assigned to other resource arguments of the same 58 | // type. And, if the type is a list are indexable. 59 | // 60 | // examples: 61 | // attribute.star 62 | // 63 | // fields: 64 | // __resource__ Resource 65 | // Resource of the attribute. 66 | // __type__ string 67 | // Type of the attribute. Eg.: `string` 68 | type Attribute struct { 69 | r *Resource 70 | t cty.Type 71 | name string 72 | path string 73 | 74 | sString 75 | } 76 | 77 | var _ starlark.Value = &Attribute{} 78 | var _ starlark.HasAttrs = &Attribute{} 79 | var _ starlark.Indexable = &Attribute{} 80 | var _ starlark.Comparable = &Attribute{} 81 | 82 | // NewAttribute returns a new Attribute for a given value or block of a Resource. 83 | // The path is calculated traversing the parents of the given Resource. 84 | func NewAttribute(r *Resource, t cty.Type, name string) *Attribute { 85 | var parts []string 86 | var path string 87 | 88 | child := r 89 | 90 | for { 91 | if child.parent.kind == ProviderKind { 92 | if child.kind == ResourceKind { 93 | path = fmt.Sprintf("%s.%s", child.typ, child.Name()) 94 | } else { 95 | path = fmt.Sprintf("%s.%s.%s", child.kind, child.typ, child.Name()) 96 | } 97 | 98 | break 99 | } 100 | 101 | parts = append(parts, child.typ) 102 | child = child.parent 103 | } 104 | 105 | for i := len(parts) - 1; i >= 0; i-- { 106 | path += "." + parts[i] 107 | } 108 | 109 | // handling of MaxItems equals 1 110 | block, ok := r.parent.block.BlockTypes[r.typ] 111 | if ok && block.MaxItems == 1 { 112 | name = "0." + name 113 | } 114 | 115 | return NewAttributeWithPath(r, t, name, path+"."+name) 116 | } 117 | 118 | // NewAttributeWithPath returns a new Attribute for a given value or block of a Resource. 119 | func NewAttributeWithPath(r *Resource, t cty.Type, name, path string) *Attribute { 120 | return &Attribute{ 121 | r: r, 122 | t: t, 123 | name: name, 124 | path: path, 125 | sString: starlark.String(fmt.Sprintf("${%s}", path)), 126 | } 127 | } 128 | 129 | // Type honors the starlark.Value interface. 130 | func (c *Attribute) Type() string { 131 | return fmt.Sprintf("Attribute<%s>", MustTypeFromCty(c.t).Starlark()) 132 | } 133 | 134 | // InnerType returns the inner Type represented by this Attribute. 135 | func (c *Attribute) InnerType() *Type { 136 | t, _ := NewTypeFromCty(c.t) 137 | return t 138 | } 139 | 140 | // Attr honors the starlark.HasAttrs interface. 141 | func (c *Attribute) Attr(name string) (starlark.Value, error) { 142 | switch name { 143 | case "__resource__": 144 | return c.r, nil 145 | case "__type__": 146 | return starlark.String(MustTypeFromCty(c.t).Starlark()), nil 147 | } 148 | 149 | if !c.t.IsObjectType() { 150 | return nil, fmt.Errorf("%s it's not a object", c.Type()) 151 | } 152 | 153 | if !c.t.HasAttribute(name) { 154 | errmsg := fmt.Sprintf("%s has no .%s field", c.Type(), name) 155 | return nil, starlark.NoSuchAttrError(errmsg) 156 | } 157 | 158 | path := fmt.Sprintf("%s.%s", c.path, name) 159 | return NewAttributeWithPath(c.r, c.t.AttributeType(name), name, path), nil 160 | } 161 | 162 | func (c *Attribute) String() string { 163 | return c.sString.GoString() 164 | } 165 | 166 | // AttrNames honors the starlark.HasAttrs interface. 167 | func (c *Attribute) AttrNames() []string { 168 | return []string{"__resource__", "__type__"} 169 | } 170 | 171 | func (c *Attribute) doNested(name, path string, t cty.Type, index int) *Attribute { 172 | return &Attribute{ 173 | r: c.r, 174 | t: t, 175 | name: c.name, 176 | } 177 | 178 | } 179 | 180 | // Index honors the starlark.Indexable interface. 181 | func (c *Attribute) Index(i int) starlark.Value { 182 | path := fmt.Sprintf("%s.%d", c.path, i) 183 | 184 | if c.t.IsSetType() { 185 | return NewAttributeWithPath(c.r, *c.t.SetElementType(), c.name, path) 186 | } 187 | 188 | if c.t.IsListType() { 189 | return NewAttributeWithPath(c.r, *c.t.ListElementType(), c.name, path) 190 | } 191 | 192 | if c.t.IsMapType() { 193 | return NewAttributeWithPath(c.r, c.t, c.name, path) 194 | } 195 | 196 | return starlark.None 197 | } 198 | 199 | // Get honors the starlark.Mapping interface. 200 | func (c *Attribute) Get(key starlark.Value) (v starlark.Value, found bool, err error) { 201 | switch vKey := key.(type) { 202 | case starlark.Int: 203 | if !c.t.IsSetType() && !c.t.IsListType() && !c.t.IsMapType() { 204 | return nil, false, fmt.Errorf("%s does not support index", c.name) 205 | } 206 | 207 | index, _ := vKey.Int64() 208 | return c.Index(int(index)), true, nil 209 | case starlark.String: 210 | if !c.t.IsMapType() { 211 | return nil, false, fmt.Errorf("%s it's not a dict", c.name) 212 | } 213 | 214 | path := fmt.Sprintf("%s.%s", c.path, vKey.GoString()) 215 | return NewAttributeWithPath(c.r, c.t, c.name, path), true, nil 216 | default: 217 | return nil, false, fmt.Errorf("%s: unexpected key type %s", c.name, key.Type()) 218 | } 219 | } 220 | 221 | // Len honors the starlark.Indexable interface. 222 | func (c *Attribute) Len() int { 223 | if !c.t.IsSetType() && !c.t.IsListType() { 224 | return 0 225 | } 226 | 227 | return 1024 228 | } 229 | 230 | // BuiltinFunctionAttribute returns a built-in function that wraps Attributes 231 | // in HCL functions. 232 | // 233 | // outline: types 234 | // functions: 235 | // fn(name, target) Attribute 236 | // Fn wraps an Attribute in a HCL function. Since the Attributes value 237 | // are only available in the `apply` phase of Terraform, the only method 238 | // to manipulate this values is using the Terraform 239 | // [HCL functions](https://www.terraform.io/docs/configuration/functions.html). 240 | // 241 | // 242 | // params: 243 | // name string 244 | // Name of the HCL function to be applied. Eg.: `base64encode` 245 | // target Attribute 246 | // Target Attribute of the HCL function. 247 | // 248 | func BuiltinFunctionAttribute() starlark.Value { 249 | // TODO(mcuadros): implement multiple arguments support. 250 | return starlark.NewBuiltin("fn", func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 251 | var function starlark.String 252 | var computed *Attribute 253 | switch len(args) { 254 | case 2: 255 | var ok bool 256 | function, ok = args.Index(0).(starlark.String) 257 | if !ok { 258 | return nil, fmt.Errorf("expected string, got %s", args.Index(0).Type()) 259 | } 260 | 261 | computed, ok = args.Index(1).(*Attribute) 262 | if !ok { 263 | return nil, fmt.Errorf("expected Attribute, got %s", args.Index(1).Type()) 264 | } 265 | default: 266 | return nil, fmt.Errorf("unexpected positional arguments count") 267 | } 268 | 269 | path := fmt.Sprintf("%s(%s)", function.GoString(), computed.path) 270 | return NewAttributeWithPath(computed.r, computed.t, computed.name, path), nil 271 | }) 272 | } 273 | -------------------------------------------------------------------------------- /starlark/types/testdata/resource.star: -------------------------------------------------------------------------------- 1 | load("assert.star", "assert") 2 | 3 | ignition = tf.provider("ignition", "1.1.0") 4 | 5 | 6 | 7 | # attr 8 | qux = ignition.data.user() 9 | qux.uid = 42 10 | assert.eq(qux.uid, 42) 11 | 12 | qux.uid *= 2 13 | assert.eq(qux.uid, 84) 14 | 15 | # attr names 16 | assert.eq("uid" in dir(qux), True) 17 | 18 | # attr names in data sources 19 | assert.eq("depends_on" in dir(qux), False) 20 | assert.eq("add_provisioner" in dir(qux), False) 21 | assert.eq("__provider__" in dir(qux), True) 22 | assert.eq("__type__" in dir(qux), True) 23 | assert.eq("__name__" in dir(qux), True) 24 | assert.eq("__kind__" in dir(qux), True) 25 | assert.eq("__dict__" in dir(qux), True) 26 | 27 | # attr not-set 28 | assert.eq(qux.name, None) 29 | 30 | # attr not-exists 31 | assert.fails(lambda: qux.foo, "Resource has no .foo field or method") 32 | 33 | # attr id 34 | assert.eq(type(qux.id), "Attribute") 35 | assert.eq(str(qux.id), "${data.ignition_user.id_2.id}") 36 | aws = tf.provider("aws", "2.13.0") 37 | 38 | # attr output assignation 39 | def invalidOutput(): aws.data.instance().public_dns = "foo" 40 | assert.fails(invalidOutput, "Resource: can't set computed public_dns attribute") 41 | 42 | 43 | # attr output in asignation 44 | web = aws.resource.instance() 45 | web.ami = web.id 46 | def invalidType(): web.get_password_data = web.id 47 | assert.fails(invalidType, "expected bool, got string") 48 | 49 | group = aws.resource.autoscaling_group() 50 | 51 | # attr names in resources 52 | assert.eq("depends_on" in dir(web), True) 53 | assert.eq("add_provisioner" in dir(web), True) 54 | assert.eq("__provider__" in dir(web), True) 55 | assert.eq("__type__" in dir(web), True) 56 | assert.eq("__name__" in dir(web), True) 57 | assert.eq("__kind__" in dir(web), True) 58 | assert.eq("__dict__" in dir(web), True) 59 | 60 | # attr optional computed 61 | assert.eq(str(group.name), "${aws_autoscaling_group.id_6.name}") 62 | 63 | group.name = "foo" 64 | assert.eq(group.name, "foo") 65 | 66 | # attr resource 67 | group.mixed_instances_policy = { 68 | "launch_template": { 69 | "launch_template_specification": { 70 | "launch_template_id": "bar", 71 | }, 72 | }, 73 | } 74 | 75 | assert.eq(group.mixed_instances_policy.launch_template.launch_template_specification.launch_template_id, "bar") 76 | 77 | # attr collections 78 | assert.eq("__provider__" in dir(web.network_interface), True) 79 | assert.eq("__type__" in dir(web.network_interface), True) 80 | assert.eq("__kind__" in dir(web.network_interface), True) 81 | 82 | web.network_interface = [ 83 | {"network_interface_id": "foo"}, 84 | {"network_interface_id": "bar"}, 85 | ] 86 | 87 | assert.eq(len(web.network_interface), 2) 88 | assert.eq(web.network_interface[0].network_interface_id, "foo") 89 | assert.eq(web.network_interface[1].network_interface_id, "bar") 90 | 91 | # attr collections clears list 92 | web.network_interface = [ 93 | {"network_interface_id": "qux"}, 94 | ] 95 | 96 | assert.eq(len(web.network_interface), 1) 97 | assert.eq(web.network_interface[0].network_interface_id, "qux") 98 | 99 | # attr collection non list 100 | def attrCollectionNonList(): web.network_interface = {} 101 | assert.fails(attrCollectionNonList, "expected list, got dict") 102 | 103 | # attr collection non dict elements 104 | def attrCollectionNonDictElement(): web.network_interface = [{}, 42] 105 | assert.fails(attrCollectionNonDictElement, "1: expected dict, got int") 106 | 107 | # comparasion simple values 108 | assert.eq(ignition.data.disk(), ignition.data.disk()) 109 | assert.ne(ignition.data.disk(device="foo"), ignition.data.disk()) 110 | 111 | # comparasion with nested 112 | y = ignition.data.disk() 113 | x = ignition.data.disk() 114 | 115 | y.partition(start=42) 116 | assert.ne(x, y) 117 | 118 | x.partition(start=42) 119 | assert.eq(x, y) 120 | 121 | # comparasion with list 122 | assert.ne(ignition.data.user(groups=["foo"]), ignition.data.user()) 123 | assert.eq(ignition.data.user(groups=["foo"]), ignition.data.user(groups=["foo"])) 124 | 125 | # constructor with name 126 | quux = ignition.data.user("quux") 127 | assert.eq(str(quux.id), "${data.ignition_user.quux.id}") 128 | 129 | # constructor from kwargs 130 | bar = ignition.data.user(uid=42, system=True) 131 | assert.eq(bar.uid, 42) 132 | assert.eq(bar.system, True) 133 | 134 | # constructor from kwargs with name 135 | fred = ignition.data.user("fred", uid=42, system=True) 136 | assert.eq(fred.uid, 42) 137 | assert.eq(fred.system, True) 138 | assert.eq(str(fred.id), "${data.ignition_user.fred.id}") 139 | 140 | # constructor from dict 141 | foo = ignition.data.user({"uid": 42, "system": True}) 142 | assert.eq(foo.uid, 42) 143 | assert.eq(foo.system, True) 144 | 145 | # constructor from dict with name 146 | baz = ignition.data.user("baz", {"uid": 42, "system": True}) 147 | assert.eq(baz.uid, 42) 148 | assert.eq(baz.system, True) 149 | assert.eq(str(baz.id), "${data.ignition_user.baz.id}") 150 | 151 | # constructor from dict with name and kwargs 152 | baz = ignition.data.user("baz", {"uid": 42, "system": True}, uid=84) 153 | assert.eq(baz.uid, 84) 154 | assert.eq(baz.system, True) 155 | assert.eq(str(baz.id), "${data.ignition_user.baz.id}") 156 | 157 | 158 | assert.eq(bar, foo) 159 | assert.eq(foo, ignition.data.user(foo.__dict__)) 160 | 161 | # constructor errors 162 | def consNonDict(): ignition.data.user(1) 163 | assert.fails(consNonDict, "resource: expected string or dict, got int") 164 | 165 | def consNonNameDict(): ignition.data.user(1, 1) 166 | assert.fails(consNonNameDict, "resource: expected string, got int") 167 | 168 | def consNameDict(): ignition.data.user("foo", 1) 169 | assert.fails(consNameDict, "resource: expected dict, got int") 170 | 171 | def consKwargsNonName(): ignition.data.user(1, uid=42) 172 | assert.fails(consKwargsNonName, "resource: expected string or dict, got int") 173 | 174 | # full coverage 175 | user = ignition.data.user() 176 | user.name = "foo" 177 | user.uid = 42 178 | user.groups = ["foo", "bar"] 179 | user.system = True 180 | 181 | assert.eq(str(user), "Resource") 182 | assert.eq(user.__dict__, { 183 | "name": "foo", 184 | "uid": 42, 185 | "groups": ["foo", "bar"], 186 | "system": True, 187 | }) 188 | 189 | disk = ignition.data.disk() 190 | 191 | root = disk.partition() 192 | root.label = "root" 193 | root.start = 2048 194 | root.size = 4 * 1024 * 1024 195 | 196 | home = disk.partition() 197 | home.label = "home" 198 | home.start = root.size + root.start 199 | home.size = 4 * 1024 * 1024 200 | 201 | assert.eq(disk.__dict__, { 202 | "partition": [{ 203 | "label": "root", 204 | "start": 2048, 205 | "size": 4194304 206 | }, { 207 | "start": 4196352, 208 | "size": 4194304, 209 | "label": "home" 210 | }] 211 | }) 212 | 213 | 214 | # depends_on 215 | instanceA = aws.resource.instance() 216 | instanceB = aws.resource.instance() 217 | instanceA.depends_on(instanceB) 218 | 219 | def dependsOnNonResource(): instanceA.depends_on(42) 220 | assert.fails(dependsOnNonResource, "expected Resource<\\[data|resource\\]>, got int") 221 | 222 | def dependsOnNestedResource(): instanceA.depends_on(disk.partition()) 223 | assert.fails(dependsOnNestedResource, "expected Resource<\\[data|resource\\]>, got Resource") 224 | 225 | def dependsOnItself(): instanceA.depends_on(instanceA) 226 | assert.fails(dependsOnItself, "can't depend on itself") 227 | 228 | # __provider__ 229 | assert.eq(web.__provider__, aws) 230 | assert.eq(baz.__provider__, ignition) 231 | assert.eq(instanceA.__provider__, aws) 232 | assert.eq(home.__provider__, ignition) 233 | assert.eq(aws.resource.instance.__provider__, aws) 234 | 235 | # __kind__ 236 | assert.eq(ignition.data.user().__kind__, "data") 237 | assert.eq(aws.resource.instance.__kind__, "resource") 238 | assert.eq(aws.resource.instance().__kind__, "resource") 239 | assert.eq(aws.resource.autoscaling_group().mixed_instances_policy.__kind__, "nested") 240 | assert.eq(web.network_interface.__kind__, "nested") 241 | 242 | # __type__ 243 | assert.eq(ignition.data.user().__type__, "ignition_user") 244 | assert.eq(aws.resource.instance.__type__, "aws_instance") 245 | assert.eq(aws.resource.instance().__type__, "aws_instance") 246 | assert.eq(aws.resource.autoscaling_group().mixed_instances_policy.__type__, "mixed_instances_policy") 247 | assert.eq(web.network_interface.__type__, "network_interface") 248 | 249 | # __name__ 250 | assert.eq(ignition.data.user().__name__, "id_30") 251 | assert.eq(aws.resource.instance().__name__, "id_31") 252 | assert.eq(ignition.data.user("given").__name__, "given") 253 | 254 | # __call__ 255 | assert.eq(ignition.data.user().__name__, "id_32") 256 | assert.eq(ignition.data.user("foo").__name__, "foo") 257 | assert.eq(ignition.data.user(uid=42).uid, 42) 258 | assert.eq(ignition.data.user({"uid": 42}).uid, 42) 259 | 260 | foo = ignition.data.user("foo", {"uid": 42}) 261 | assert.eq(foo.__name__, "foo") 262 | assert.eq(foo.uid, 42) 263 | 264 | foo = ignition.data.user("foo", uid=42) 265 | assert.eq(foo.__name__, "foo") 266 | assert.eq(foo.uid, 42) -------------------------------------------------------------------------------- /_documentation/starlark/lexical-elements.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Lexical elements' 3 | weight: 2 4 | --- 5 | 6 | A Starlark program consists of one or more modules. 7 | Each module is defined by a single UTF-8-encoded text file. 8 | 9 | A complete grammar of Starlark can be found in [grammar.txt](https://github.com/google/starlark-go/blob/master/syntax/grammar.txt). 10 | That grammar is presented piecemeal throughout this document 11 | in boxes such as this one, which explains the notation: 12 | 13 | ```grammar {.good} 14 | Grammar notation 15 | 16 | - lowercase and 'quoted' items are lexical tokens. 17 | - Capitalized names denote grammar productions. 18 | - (...) implies grouping. 19 | - x | y means either x or y. 20 | - [x] means x is optional. 21 | - {x} means x is repeated zero or more times. 22 | - The end of each declaration is marked with a period. 23 | ``` 24 | 25 | The contents of a Starlark file are broken into a sequence of tokens of 26 | five kinds: white space, punctuation, keywords, identifiers, and literals. 27 | Each token is formed from the longest sequence of characters that 28 | would form a valid token of each kind. 29 | 30 | ```grammar {.good} 31 | File = {Statement | newline} eof . 32 | ``` 33 | 34 | *White space* consists of spaces (U+0020), tabs (U+0009), carriage 35 | returns (U+000D), and newlines (U+000A). Within a line, white space 36 | has no effect other than to delimit the previous token, but newlines, 37 | and spaces at the start of a line, are significant tokens. 38 | 39 | *Comments*: A hash character (`#`) appearing outside of a string 40 | literal marks the start of a comment; the comment extends to the end 41 | of the line, not including the newline character. 42 | Comments are treated like other white space. 43 | 44 | *Punctuation*: The following punctuation characters or sequences of 45 | characters are tokens: 46 | 47 | ```text 48 | + - * / // % = 49 | += -= *= /= //= %= == != 50 | ^ < > << >> & | 51 | ^= <= >= <<= >>= &= |= 52 | . , ; : ~ ** 53 | ( ) [ ] { } 54 | ``` 55 | 56 | *Keywords*: The following tokens are keywords and may not be used as 57 | identifiers: 58 | 59 | ```text 60 | and elif in or 61 | break else lambda pass 62 | continue for load return 63 | def if not while 64 | ``` 65 | 66 | The tokens below also may not be used as identifiers although they do not 67 | appear in the grammar; they are reserved as possible future keywords: 68 | 69 | 70 | 71 | ```text 72 | as finally nonlocal 73 | assert from raise 74 | class global try 75 | del import with 76 | except is yield 77 | ``` 78 | 79 | Implementation note: 80 | The Go implementation permits `assert` to be used as an identifier, 81 | and this feature is widely used in its tests. 82 | 83 | *Identifiers*: an identifier is a sequence of Unicode letters, decimal 84 | digits, and underscores (`_`), not starting with a digit. 85 | Identifiers are used as names for values. 86 | 87 | Examples: 88 | 89 | ```text 90 | None True len 91 | x index starts_with arg0 92 | ``` 93 | 94 | *Literals*: literals are tokens that denote specific values. Starlark 95 | has string, integer, and floating-point literals. 96 | 97 | ```text 98 | 0 # int 99 | 123 # decimal int 100 | 0x7f # hexadecimal int 101 | 0o755 # octal int 102 | 0b1011 # binary int 103 | 104 | 0.0 0. .0 # float 105 | 1e10 1e+10 1e-10 106 | 1.1e10 1.1e+10 1.1e-10 107 | 108 | "hello" 'hello' # string 109 | '''hello''' """hello""" # triple-quoted string 110 | r'hello' r"hello" # raw string literal 111 | ``` 112 | 113 | Integer and floating-point literal tokens are defined by the following grammar: 114 | 115 | ```grammar {.good} 116 | int = decimal_lit | octal_lit | hex_lit | binary_lit . 117 | decimal_lit = ('1' … '9') {decimal_digit} | '0' . 118 | octal_lit = '0' ('o'|'O') octal_digit {octal_digit} . 119 | hex_lit = '0' ('x'|'X') hex_digit {hex_digit} . 120 | binary_lit = '0' ('b'|'B') binary_digit {binary_digit} . 121 | 122 | float = decimals '.' [decimals] [exponent] 123 | | decimals exponent 124 | | '.' decimals [exponent] 125 | . 126 | decimals = decimal_digit {decimal_digit} . 127 | exponent = ('e'|'E') ['+'|'-'] decimals . 128 | 129 | decimal_digit = '0' … '9' . 130 | octal_digit = '0' … '7' . 131 | hex_digit = '0' … '9' | 'A' … 'F' | 'a' … 'f' . 132 | binary_digit = '0' | '1' . 133 | ``` 134 | 135 | ## String literals 136 | 137 | A Starlark string literal denotes a string value. 138 | In its simplest form, it consists of the desired text 139 | surrounded by matching single- or double-quotation marks: 140 | 141 | ```python 142 | "abc" 143 | 'abc' 144 | ``` 145 | 146 | Literal occurrences of the chosen quotation mark character must be 147 | escaped by a preceding backslash. So, if a string contains several 148 | of one kind of quotation mark, it may be convenient to quote the string 149 | using the other kind, as in these examples: 150 | 151 | ```python 152 | 'Have you read "To Kill a Mockingbird?"' 153 | "Yes, it's a classic." 154 | 155 | "Have you read \"To Kill a Mockingbird?\"" 156 | 'Yes, it\'s a classic.' 157 | ``` 158 | 159 | ### String escapes 160 | 161 | Within a string literal, the backslash character `\` indicates the 162 | start of an _escape sequence_, a notation for expressing things that 163 | are impossible or awkward to write directly. 164 | 165 | The following *traditional escape sequences* represent the ASCII control 166 | codes 7-13: 167 | 168 | ``` 169 | \a \x07 alert or bell 170 | \b \x08 backspace 171 | \f \x0C form feed 172 | \n \x0A line feed 173 | \r \x0D carriage return 174 | \t \x09 horizontal tab 175 | \v \x0B vertical tab 176 | ``` 177 | 178 | A *literal backslash* is written using the escape `\\`. 179 | 180 | An *escaped newline*---that is, a backslash at the end of a line---is ignored, 181 | allowing a long string to be split across multiple lines of the source file. 182 | 183 | ```python 184 | "abc\ 185 | def" # "abcdef" 186 | ``` 187 | 188 | An *octal escape* encodes a single byte using its octal value. 189 | It consists of a backslash followed by one, two, or three octal digits [0-7]. 190 | It is error if the value is greater than decimal 255. 191 | 192 | ```python 193 | '\0' # "\x00" a string containing a single NUL byte 194 | '\12' # "\n" octal 12 = decimal 10 195 | '\101-\132' # "A-Z" 196 | '\119' # "\t9" = "\11" + "9" 197 | ``` 198 | 199 | Implementation note: 200 | The Java implementation encodes strings using UTF-16, 201 | so an octal escape encodes a single UTF-16 code unit. 202 | Octal escapes for values above 127 are therefore not portable across implementations. 203 | There is little reason to use octal escapes in new code. 204 | 205 | A *hex escape* encodes a single byte using its hexadecimal value. 206 | It consists of `\x` followed by exactly two hexadecimal digits [0-9A-Fa-f]. 207 | 208 | ```python 209 | "\x00" # "\x00" a string containing a single NUL byte 210 | "(\x20)" # "( )" ASCII 0x20 = 32 = space 211 | 212 | red, reset = "\x1b[31m", "\x1b[0m" # ANSI terminal control codes for color 213 | "(" + red + "hello" + reset + ")" # "(hello)" with red text, if on a terminal 214 | ``` 215 | 216 | Implementation note: 217 | The Java implementation does not support hex escapes. 218 | 219 | An ordinary string literal may not contain an unescaped newline, 220 | but a *multiline string literal* may spread over multiple source lines. 221 | It is denoted using three quotation marks at start and end. 222 | Within it, unescaped newlines and quotation marks (or even pairs of 223 | quotation marks) have their literal meaning, but three quotation marks 224 | end the literal. This makes it easy to quote large blocks of text with 225 | few escapes. 226 | 227 | ``` 228 | haiku = ''' 229 | Yesterday it worked. 230 | Today it is not working. 231 | That's computers. Sigh. 232 | ''' 233 | ``` 234 | 235 | Regardless of the platform's convention for text line endings---for 236 | example, a linefeed (\n) on UNIX, or a carriage return followed by a 237 | linefeed (\r\n) on Microsoft Windows---an unescaped line ending in a 238 | multiline string literal always denotes a line feed (\n). 239 | 240 | Starlark also supports *raw string literals*, which look like an 241 | ordinary single- or double-quotation preceded by `r`. Within a raw 242 | string literal, there is no special processing of backslash escapes, 243 | other than an escaped quotation mark (which denotes a literal 244 | quotation mark), or an escaped newline (which denotes a backslash 245 | followed by a newline). This form of quotation is typically used when 246 | writing strings that contain many quotation marks or backslashes (such 247 | as regular expressions or shell commands) to reduce the burden of 248 | escaping: 249 | 250 | ```python 251 | "a\nb" # "a\nb" = 'a' + '\n' + 'b' 252 | r"a\nb" # "a\\nb" = 'a' + '\\' + '\n' + 'b' 253 | 254 | "a\ 255 | b" # "ab" 256 | r"a\ 257 | b" # "a\\\nb" 258 | ``` 259 | 260 | It is an error for a backslash to appear within a string literal other 261 | than as part of one of the escapes described above. 262 | 263 | TODO: define indent, outdent, semicolon, newline, eof 264 | -------------------------------------------------------------------------------- /_documentation/starlark/value-concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Value concepts' 3 | weight: 5 4 | toc: true 5 | --- 6 | 7 | ## Overview 8 | 9 | Starlark has eleven core [data types](/docs/starlark/data-types/). An application 10 | that embeds the Starlark intepreter may define additional types that 11 | behave like Starlark values. All values, whether core or 12 | application-defined, implement a few basic behaviors: 13 | 14 | ```text 15 | str(x) -- return a string representation of x 16 | type(x) -- return a string describing the type of x 17 | bool(x) -- convert x to a Boolean truth value 18 | ``` 19 | 20 | ## Identity and mutation 21 | 22 | Starlark is an imperative language: programs consist of sequences of 23 | statements executed for their side effects. 24 | For example, an assignment statement updates the value held by a 25 | variable, and calls to some built-in functions such as `print` change 26 | the state of the application that embeds the interpreter. 27 | 28 | Values of some data types, such as `NoneType`, `bool`, `int`, `float`, and 29 | `string`, are _immutable_; they can never change. 30 | Immutable values have no notion of _identity_: it is impossible for a 31 | Starlark program to tell whether two integers, for instance, are 32 | represented by the same object; it can tell only whether they are 33 | equal. 34 | 35 | Values of other data types, such as `list`, `dict`, and `set`, are 36 | _mutable_: they may be modified by a statement such as `a[i] = 0` or 37 | `items.clear()`. Although `tuple` and `function` values are not 38 | directly mutable, they may refer to mutable values indirectly, so for 39 | this reason we consider them mutable too. Starlark values of these 40 | types are actually _references_ to variables. 41 | 42 | Copying a reference to a variable, using an assignment statement for 43 | instance, creates an _alias_ for the variable, and the effects of 44 | operations applied to the variable through one alias are visible 45 | through all others. 46 | 47 | ```python 48 | x = [] # x refers to a new empty list variable 49 | y = x # y becomes an alias for x 50 | x.append(1) # changes the variable referred to by x 51 | print(y) # "[1]"; y observes the mutation 52 | ``` 53 | 54 | Starlark uses _call-by-value_ parameter passing: in a function call, 55 | argument values are assigned to function parameters as if by 56 | assignment statements. If the values are references, the caller and 57 | callee may refer to the same variables, so if the called function 58 | changes the variable referred to by a parameter, the effect may also 59 | be observed by the caller: 60 | 61 | ```python 62 | def f(y): 63 | y.append(1) # changes the variable referred to by x 64 | 65 | x = [] # x refers to a new empty list variable 66 | f(x) # f's parameter y becomes an alias for x 67 | print(x) # "[1]"; x observes the mutation 68 | ``` 69 | 70 | 71 | As in all imperative languages, understanding _aliasing_, the 72 | relationship between reference values and the variables to which they 73 | refer, is crucial to writing correct programs. 74 | 75 | ## Freezing a value 76 | 77 | Starlark has a feature unusual among imperative programming languages: 78 | a mutable value may be _frozen_ so that all subsequent attempts to 79 | mutate it fail with a dynamic error; the value, and all other values 80 | reachable from it, become _immutable_. 81 | 82 | Immediately after execution of a Starlark module, all values in its 83 | top-level environment are frozen. Because all the global variables of 84 | an initialized Starlark module are immutable, the module may be published to 85 | and used by other threads in a parallel program without the need for 86 | locks. For example, the Bazel build system loads and executes BUILD 87 | and .bzl files in parallel, and two modules being executed 88 | concurrently may freely access variables or call functions from a 89 | third without the possibility of a race condition. 90 | 91 | ## Hashing 92 | 93 | The `dict` and `set` data types are implemented using hash tables, so 94 | only _hashable_ values are suitable as keys of a `dict` or elements of 95 | a `set`. Attempting to use a non-hashable value as the key in a hash 96 | table results in a dynamic error. 97 | 98 | The hash of a value is an unspecified integer chosen so that two equal 99 | values have the same hash, in other words, `x == y => hash(x) == hash(y)`. 100 | A hashable value has the same hash throughout its lifetime. 101 | 102 | Values of the types `NoneType`, `bool`, `int`, `float`, and `string`, 103 | which are all immutable, are hashable. 104 | 105 | Values of mutable types such as `list`, `dict`, and `set` are not 106 | hashable. These values remain unhashable even if they have become 107 | immutable due to _freezing_. 108 | 109 | A `tuple` value is hashable only if all its elements are hashable. 110 | Thus `("localhost", 80)` is hashable but `([127, 0, 0, 1], 80)` is not. 111 | 112 | Values of the types `function` and `builtin_function_or_method` are also hashable. 113 | Although functions are not necessarily immutable, as they may be 114 | closures that refer to mutable variables, instances of these types 115 | are compared by reference identity (see [Comparisons](/docs/starlark/expressions/#comparisons)), 116 | so their hash values are derived from their identity. 117 | 118 | 119 | ## Sequence types 120 | 121 | Many Starlark data types represent a _sequence_ of values: lists, 122 | tuples, and sets are sequences of arbitrary values, and in many 123 | contexts dictionaries act like a sequence of their keys. 124 | 125 | We can classify different kinds of sequence types based on the 126 | operations they support. 127 | Each is listed below using the name of its corresponding interface in 128 | the interpreter's Go API. 129 | 130 | * `Iterable`: an _iterable_ value lets us process each of its elements in a fixed order. 131 | Examples: `dict`, `set`, `list`, `tuple`, but not `string`. 132 | * `Sequence`: a _sequence of known length_ lets us know how many elements it 133 | contains without processing them. 134 | Examples: `dict`, `set`, `list`, `tuple`, but not `string`. 135 | * `Indexable`: an _indexed_ type has a fixed length and provides efficient 136 | random access to its elements, which are identified by integer indices. 137 | Examples: `string`, `tuple`, and `list`. 138 | * `SetIndexable`: a _settable indexed type_ additionally allows us to modify the 139 | element at a given integer index. Example: `list`. 140 | * `Mapping`: a mapping is an association of keys to values. Example: `dict`. 141 | 142 | Although all of Starlark's core data types for sequences implement at 143 | least the `Sequence` contract, it's possible for an application 144 | that embeds the Starlark interpreter to define additional data types 145 | representing sequences of unknown length that implement only the `Iterable` contract. 146 | 147 | Strings are not iterable, though they do support the `len(s)` and 148 | `s[i]` operations. Starlark deviates from Python here to avoid a common 149 | pitfall in which a string is used by mistake where a list containing a 150 | single string was intended, resulting in its interpretation as a sequence 151 | of bytes. 152 | 153 | Most Starlark operators and built-in functions that need a sequence 154 | of values will accept any iterable. 155 | 156 | It is a dynamic error to mutate a sequence such as a list, set, or 157 | dictionary while iterating over it. 158 | 159 | ```python 160 | def increment_values(dict): 161 | for k in dict: 162 | dict[k] += 1 # error: cannot insert into hash table during iteration 163 | 164 | dict = {"one": 1, "two": 2} 165 | increment_values(dict) 166 | ``` 167 | 168 | 169 | ## Indexing 170 | 171 | Many Starlark operators and functions require an index operand `i`, 172 | such as `a[i]` or `list.insert(i, x)`. Others require two indices `i` 173 | and `j` that indicate the start and end of a sub-sequence, such as 174 | `a[i:j]`, `list.index(x, i, j)`, or `string.find(x, i, j)`. 175 | All such operations follow similar conventions, described here. 176 | 177 | Indexing in Starlark is *zero-based*. The first element of a string 178 | or list has index 0, the next 1, and so on. The last element of a 179 | sequence of length `n` has index `n-1`. 180 | 181 | ```python 182 | "hello"[0] # "h" 183 | "hello"[4] # "o" 184 | "hello"[5] # error: index out of range 185 | ``` 186 | 187 | For sub-sequence operations that require two indices, the first is 188 | _inclusive_ and the second _exclusive_. Thus `a[i:j]` indicates the 189 | sequence starting with element `i` up to but not including element 190 | `j`. The length of this sub-sequence is `j-i`. This convention is known 191 | as *half-open indexing*. 192 | 193 | ```python 194 | "hello"[1:4] # "ell" 195 | ``` 196 | 197 | Either or both of the index operands may be omitted. If omitted, the 198 | first is treated equivalent to 0 and the second is equivalent to the 199 | length of the sequence: 200 | 201 | ```python 202 | "hello"[1:] # "ello" 203 | "hello"[:4] # "hell" 204 | ``` 205 | 206 | It is permissible to supply a negative integer to an indexing 207 | operation. The effective index is computed from the supplied value by 208 | the following two-step procedure. First, if the value is negative, the 209 | length of the sequence is added to it. This provides a convenient way 210 | to address the final elements of the sequence: 211 | 212 | ```python 213 | "hello"[-1] # "o", like "hello"[4] 214 | "hello"[-3:-1] # "ll", like "hello"[2:4] 215 | ``` 216 | 217 | Second, for sub-sequence operations, if the value is still negative, it 218 | is replaced by zero, or if it is greater than the length `n` of the 219 | sequence, it is replaced by `n`. In effect, the index is "truncated" to 220 | the nearest value in the range `[0:n]`. 221 | 222 | ```python 223 | "hello"[-1000:+1000] # "hello" 224 | ``` 225 | 226 | This truncation step does not apply to indices of individual elements: 227 | 228 | ```python 229 | "hello"[-6] # error: index out of range 230 | "hello"[-5] # "h" 231 | "hello"[4] # "o" 232 | "hello"[5] # error: index out of range 233 | ``` 234 | 235 | --------------------------------------------------------------------------------