README ¶
This example was created using goa framework. Using goa we have created a service provider that exposes a resource for managing 'Bottles' allowing the consumer to create and read bottles.
Overview
goa is a framework for building micro-services and REST APIs in Go using a unique design-first approach.
This example service provider APIs have been designed using goa's DSL and leveraging the goa framework the server, client, swagger documentation and a CLI were generated automatically. This is where the real power of goa resides, allowing service providers to focus on improving their services while still offering good integration capabilities.
The code containing the API design is located in the 'design' folder. This file contains all the information related to the API from where goa will feed and generate the code desired (e,g: server api, client lib, documentation, cli, etc). In this example, everything inside the api folder was auto-generated by goa excluding the design/design.go file.
How to
The following describes the contents of this example and how to use goa to auto-generate code from the API design DSL.
Pre-requisites
- Go go1.10.1
- goa latest from master
Running goagen
The folder located at design folder contains the API design described using goa DSL. This folder is used by goa to auto-generate the desired files and ib this example the command used is as follows:
$ cd $GOPATH/github.com/dikhan/terraform-provider-openapi/examples/goa/api
$ goagen bootstrap -d github.com/dikhan/terraform-provider-openapi/examples/goa/api/design
Running the above command will auto-generate the application code, application scaffolding, swagger file and a client package and tool. In order to run goagen the binary has to be installed in your path, for that follow goa installation instructions)
The output displayed after running goagen will be something like:
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
app/test
app/test/bottle_testing.go
main.go
bottle.go
tool/cellar-cli
tool/cellar-cli/main.go
tool/cli
tool/cli/commands.go
client
client/client.go
client/bottle.go
client/user_types.go
client/media_types.go
swagger
swagger/swagger.json
swagger/swagger.yaml
As you can see, goa can make you save a lot of time. The server side code, client library, API documentation and even a CLI was created for free by ust running one command. The server side code generated handles all the things related to transport and connections, however the actual logic needs to be implemented by the service provider of course.
In this example, the auto-generated bottle.go file has been updated to use an in-memory db (just a simple map) so we are able to use the terraform-provider-openapi against the API and manage the resources created.
Running the example goa API
Building and running container 'manual approach'
A dockerfile is provided to run the example in a container.
To build the container:
$ cd $GOPATH/github.com/dikhan/terraform-provider-openapi/examples/goa/api
$ docker build -t goa-service-provider-api .
To run the container:
docker run -p 9090:9090 goa-service-provider-api
Alternatively, a make target is provided to do both steps in one command:
$ cd $GOPATH/github.com/dikhan/terraform-provider-openapi
$ make local-env
Building and running container via make target 'faster approach'
Alternatively, a make target is provided to do the above in one command:
$ cd $GOPATH/github.com/dikhan/terraform-provider-openapi
$ make local-env
This command will bring up the example APIs provided for development purposes.
Running terraform-provider-openapi against this API
- Install terraform-provider-api and the symlink for 'goa' provider:
$ cd $GOPATH/github.com/dikhan/terraform-provider-openapi/
$ PROVIDER_NAME="goa" make install
- Execute terraform passing the following env variables:
$ cd $GOPATH/github.com/dikhan/terraform-provider-openapi/examples/goa/api
$ terraform init && OTF_VAR_goa_SWAGGER_URL="http://localhost:9090/swagger/swagger.yaml" OTF_INSECURE_SKIP_VERIFY=true terraform plan
Alternatively, a make target is also provided to achieve the same output but executing the following:
$ cd $GOPATH/github.com/dikhan/terraform-provider-openapi/
$ make local-env
$ make run-terraform-example-goa
Below is an output of the terraform execution:
$ cd $GOPATH/github.com/dikhan/terraform-provider-openapi/examples/goa
$ terraform init && OTF_VAR_goa_SWAGGER_URL="http://localhost:9090/swagger/swagger.yaml" terraform plan
Initializing provider plugins...
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ goa_bottles.my_bottle
id: <computed>
name: "Name of bottle"
rating: "3"
vintage: "2653"
Plan: 1 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
Things to know when using goa and the terraform-provider-openapi
goa handles data types in a particular way, having two models that describe the request and the responses respectively:
- Payload: content posted as part of the request. The request payload describes the shape of the request body.
- MediaType: content returned as part of the response. A response media type defines the shape of response bodies.
This is a valid separation of concern when exposing the API models as requests body models are completely decoupled from response body models giving more flexibility on what is intended to be exposed for consumption as well as representation of the final object created. However, in order for terraform to know what values are computed and which ones are meant to be provided by the user when creating a resource, a readOnly flag needs to be attached to the attributes that are computed as described below:
Attribute("id", String, "Unique bottle ID", func() {
// This makes the id attribute read-only, this means that the value will be computed by the API and therefore parameter
// is not expected from the user when invoking the API
ReadOnly()
})
The above will end up being rendered when executing goagen into the following definition:
definitions:
BottlePayload:
description: BottlePayload is the type used to create bottles
example:
id: Enim sapiente expedita sit.
name: x
rating: 4
vintage: 2653
properties:
id:
description: Unique bottle ID
example: Enim sapiente expedita sit.
readOnly: true // <-- This is the important bit, here terraform-provider-openapi will understand that this value is computed
type: string
This way terraform will know how to differentiate between expected fields that should be populated by the user and expected fields that should be computed by the API, avoiding inecessary conflicts between the main.tf file and local terraform state.
Since writing both Payload and MediaType might be a bit redundant, goa has a reference DSL that basically will inherit the attribute properties from the data type being referened:
Reference(BottlePayload)
Below is a full example on how both Payload and MediaType can be described reducing as much as possible the boiler plate needed:
var BottlePayload = Type("BottlePayload", func() {
Description("BottlePayload is the type used to create bottles")
Attribute("id", String, "Unique bottle ID", func() {
// This makes the id attribute read-only, this means that the value will be computed by the API and therefore parameter
// is not expected from the user when invoking the API
ReadOnly()
})
Attribute("name", String, "Name of bottle", func() {
MinLength(1)
})
Attribute("vintage", Integer, "Vintage of bottle", func() {
Minimum(1900)
})
Attribute("rating", Integer, "Rating of bottle", func() {
Minimum(1)
Maximum(5)
})
Required("name", "vintage", "rating")
})
var BottleMedia = MediaType("application/vnd.gophercon.goa.bottle", func() {
TypeName("bottle")
// Reusing BottlePayload reduces the boiler plate having to define attribute properties in one place only
Reference(BottlePayload)
Attributes(func() {
Attribute("id")
Attribute("name")
Attribute("vintage")
Attribute("rating")
Required("id", "name", "vintage", "rating")
})
View("default", func() {
Attribute("id")
Attribute("name")
Attribute("vintage")
Attribute("rating")
})
})
The design file contains a fill example for the reference.