Extending the Swagger-Codegen project

Cut that Google Cloud Budget in HALF!

Building our own generator to migrate off of Kubernetes

WAIT!? MIGRATE OFF OF THE KUBERNETES?! R U EVEN CL00d SKALE!?

Yes, I still think that Kubernetes is a pretty neat bit of software. However, Kubernetes is expensive for someone trying to host a side project (especially one that no one uses!). Hopefully, we can turn something like this:

yikes!!!!

Into something like this:

can’t really get smaller than $0

Nice! First, let’s review the current and projected architecture.

This probably took ~10 days in total. Yuck.

As you can see, there are a lot of things that I chose to manage when I decided on Kubernetes. I’ve spent at least 2 days figuring out secret management. I faced some issues with the Terraform provider for Helm and the whole process eventually worked itself out. I don’t think I even got the whole certificate chain process working quite right. In any case, the whole set of infrastructure is eating away at my Google Cloud Free Credits

Zero users and nearly halfway done. FeelsBadMan.

To survive, we must shrink.

Unfortunately, Google Cloud functions doesn’t have support for custom host names (fx.vastorchard.com CNAME us-central-1-vastorchard.cloudfunctions.net). We also don’t have access to any sort of API Gateway (Apigee is ~$5000 a year rofl) and I think creating a compute instance for Kong kinda defeats the whole point of the functions. This means we will be embedding our auth layer into our functions. This isn’t a big deal but something tells me this will suck when I want to switch auth providers.

Lifecycle of a Google Cloud Function

  • Write snippet of code in request/response format
  • ZIP it up + place it in a Google Cloud Bucket
  • Reconfigure your function to use that new function

As stated earlier, I am using Terraform to manage all of my infrastructure. It look like I’ll just need to use a combination of the cloud function resource and bucket object resource. However, Kubernetes made Secret injection easy at runtime. With Google Cloud Functions, I will likely have to build secrets into my function. Swagger codegen generators support a config.json file that can be used to support these kinds of additional configuration. However, the swagger codegen project likely shouldn’t care if Terraform is deploying the generated ZIP file so I won’t be including that in the generator.

# config.json
{
"Authorization": "auth0",
"AuthorizationConfig": {
"Auth0Application": "vastorchard.auth0.com"
},
"Secrets": ["github-private-key"]
}

It is also worth noting that our function will be limited to 540 seconds . Using the PetStore example, I was able to run the generator in about two seconds.

1.62s user 0.17s system 133% cpu 1.343 total

Initial Design

Basically, I want a developer to write three things:

  • Swagger Template (focusing on the contract)
  • Business Logic (make the thing do the thing!)
  • Secret files (this will have to do for now)

The idea is that the swagger.Wrapper object is an opinionated object that will handle Authorization (Auth0 in my case). I should be able to throw anything into it (like a tracing context). It will also hold secrets.

Getting Started

We can follow the documentation for creating our own generator. Nothing to note here, just follow the steps and you’ll be good. I ran into some problems with swagger-codegen missing the 2.4.X version of their jar in Maven central so I opted to use 2.3.X .

Out of the box, you’ll generate the following structure:

$ find .
.
./src
./src/io
./src/io/swagger
./src/io/swagger/client
./src/io/swagger/client/api
./src/io/swagger/client/api/PetApi.sample
./src/io/swagger/client/api/StoreApi.sample
./src/io/swagger/client/api/UserApi.sample
./src/io/swagger/client/model
./src/io/swagger/client/model/ApiResponse.sample
./src/io/swagger/client/model/Category.sample
./src/io/swagger/client/model/Order.sample
./src/io/swagger/client/model/Pet.sample
./src/io/swagger/client/model/Tag.sample
./src/io/swagger/client/model/User.sample

which are generated with the included api.mustache and model.mustache templates. Based on the files above, each “API” will probably end up with our functions and the Models will end up with Go structs.

Let’s reference the existing Go-Server templates. If we change api.mustache to the following:

package {{package}}

import (
"{{package}}/models"
)

{{#operations}}
# classname: {{classname}}
{{#operation}}
func (s swagger.Wrapper) {{operationId}}({{#allParams}}{{paramName}} {{dataType}}, {{/allParams}}) (res http.ResponseWriter) {
return res
}
{{/operation}}
{{/operations}}

We will end up with end up with:

$ cat myClient/src/io/swagger/client/api/PetApi.sample
package io.swagger.client.api
import (
"io.swagger.client.api/models"
)
# classname: PetApi
func (s swagger.Wrapper) addPet(body Pet, ) (res http.ResponseWriter) {
return res
}
func (s swagger.Wrapper) deletePet(petId Long, apiKey String, ) (res http.ResponseWriter) {
return res
}
...

which is pretty darn close to what we want! Looks like we will have to figure out how to fix the last parameter’s comma. Now is a probably a good time to finalize package structure, too.

Context Switch: The Wrapping Code

We will spend more time on the generator when we are more certain which files we need to be generated. This means exploration of how a Google Cloud Functions works.

Unfortunately, Google Cloud Functions only supports Node.js packages. This means that we will end up zipping up a Node module. Fortunately, there is an example using Go. This example’s module is actually some C++ code that execs our Go program. Because this code replaces the existing Node.js server, we get some insight into the inner workings of Cloud Functions. We can see several endpoints:

  • /load which is probably for some upstream health checking
  • /ready which also looks to be used for some upstream health checking
  • /execute (our function) which would be built into this application with the provided example code.

We will have to layer our middleware after the /execute endpoint. Just like the picture above, this code will handle authentication and unmarshalling parameters. Remember, writing code around these assumptions is dangerous in that the contracts (expected endpoints) are not necessarily finalized.

It looks like the request is sent over one of the files passed to the wrapping Go code. This is an interesting design decision when compared to OpenFaaS (where a Function Watchdog converts HTTP requests to STDIN and Headers are available as environment variables). Seeing some convergence on a “minimal wrapper” in the FaaS space would be neat to see. Shoutout to CNCF’s working group for “serverless.”

More Challenges

Swagger codegen doesn’t have an easy way to create a file per function. Files are created:

  • per API (this is done by tags)
  • per Model
  • per Supporting File

As we are limited by APIs, we will need to create a tag per operationId. This means that taking an existing swagger spec might not be compatible with our generator. While not the greatest for adoption, we can do this for the meantime until a better solution is suggested.

Another interesting thing I’ve found in my exploration is that Google Cloud Functions don’t have a way to supply parameters in the path of an HTTP request (example: /book/$ISBN/pages/$PAGENUMBER). This might also prove to be difficult when migrating an existing RESTful API to Google Cloud Functions. Query parameters should still be supported.

MVP

We will need the minimum supporting files that were defined in the cloud-functions-go repository.

supportingFiles.add(new SupportingFile(
"Makefile.mustache", "",
"Makefile"));
supportingFiles.add(new SupportingFile(
"package.json", "",
"package.json"));
supportingFiles.add(new SupportingFile(
"index.js", "",
"index.js"));
// execer
supportingFiles.add(new SupportingFile(
"local_modules/execer/binding.gyp", "local_modules/execer",
"binding.gyp"));
supportingFiles.add(new SupportingFile(
"local_modules/execer/execer.cc", "local_modules/execer",
"execer.cc"));
supportingFiles.add(new SupportingFile(
"local_modules/execer/index.js", "local_modules/execer",
"index.js"));
supportingFiles.add(new SupportingFile(
"local_modules/execer/package.json", "local_modules/execer",
"package.json"));

This will give us a way to generate their hello world.

We also need to edit the generated class to extend the AbstractGoCodegen class.

public class FxgenGenerator extends AbstractGoCodegen implements CodegenConfig { ... }

By doing this, we end up with the type mapping from Swagger to Go ( String -> string). Because both Go Client and Go Server generators would need the same mappings, the would-be duplicated code is placed in this class. If you are building your own generator, start with one of these Abstract<Language>Codegen classes (or create one yourself so your server and client implementations don’t repeat code).

#struggle

Yet again, I’ve wasted an hour to a dynamically linked file :( CGO_ENABLED=0 does the trick for that one.

$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

I also found out about Go build tags. The included Makefile uses -tags node. I haven’t found an article bring up why or how to use tags, but from my exploration, the tags are used to include certain files in your build. Here is an example from the nodego module.

// +build !node

Because Google cloud functions uses file descriptors as the way to communicate between gateway and function, we would need to replicate that same setup locally to test the function. Alternatively, we can listen on port and use HTTP to invoke our function. After all, there are plenty more tools to interact with HTTP than with file descriptors.

There were times when updating function resulted in the following:

WTF?

while this might be scary for some of you in production, it might be ok for products that can recover from failure to communicate with backend services.

Writing the Makefile took some time. The first challenge was figuring out Makefiles matching rules. I ended up with the following logic.

FUNCTIONS = $(shell ls | sed -e 's/-pkg.go/.function/' | sed -e 's/-wrap.go/.function/' | sort | uniq )

all: ${FUNCTIONS}

%.function: %.go %.js %.zip
echo done with $<

First, we get a list of all things that need to be built by using some shell commands. We turn strings that look like createPet-pkg.go into the string createPet.function. Make will then take these “arguments” and try and match against targets (like %.function). From here, we can use the this same logic to build our dependency tree (compile go files, compile javascript, zip up the function). Useful Makefile variables are $<, $@, and $*.

Function Baseline + Pricing

To debug the above issues, I have removed all logic from the endpoint. The only code left is the Node.js exec code and any of the Go server http request handling. The code below shows what you the minimum amount of time your function will be running should you choose Go as your language. Add this number to

2 to 6 ms baseline

On the Google Cloud Function pricing page:

Compute time is measured in 100ms increments, rounded up to the nearest increment. For example, a function executing for 260ms would be billed as 300ms.

That should leave you with ~94 milliseconds if you want to maximize work for the lowest billable unit.

Final Product

At the time of publishing this article, I haven’t finished the Authentication piece. I’ll try and add this in the coming weeks. Ideally, I can add Service Binding for talking to Databases or other services exposed on Cloud Providers.

If you run the following code, you should be left with *-pkg.go files like the following. Fill the function in and enjoy!

func AddPet(body Pet) ( error) {
return nil
}