Butter Days: Day 10.75
This is Day 10.75 of Butter Days, from my mate’s place again.
I didn’t plan to post this, but I wanted to write down my CLI attempts and why they have failed so far. In writing it down though, things actually started to make more sense by the end.
See Day 1 of Butter Days for context on what I’m ultimately trying to build.
OpenAPI CLI Generator
Now that I’ve fixed the certificates, I’m trying to use this OpenAPI CLI Generator project to generate a CLI for this API, directly from the openapi spec.
I didn’t write this down as I went because I was impatient and was intermittently poking around trying to get it working, but here are the highlights:
- The AWS OpenAPI specs that I found are actually swagger 2.0 specs, but the CLI generator takes openapi 3.0 specs. Apparently openapi 3.0 is just the next version after swagger 2.0, just renamed.
- There are converters from the old spec to the new spec that I was able to use successfully.
- I ran into bad escaping issues, because characters in many of the strings in
that spec weren’t escaped properly when I tried to load them (e.g.
\+
should have been\\+
, etc.). It’s possible that was the 2.0 -> 3.0 converter’s fault. - I also ran into issues where quotes weren’t properly closed. I’m not sure whether this was the 2.0 -> 3.0 conversion’s fault or the 3.0 -> golang code generator’s fault.
- The biggest issue, which I’m now stuck on, is that the CLI generator library
doesn’t support XML, and that’s what the AWS API returns. The issue seems to
be that the library is trying to unmarshal XML into a generic
map[string]interface{}
object (a.k.a. map from string to “holds values of any type”) but this doesn’t work in golang for xml. In fact it seems like this is not possible generally because XML can have many things in it, like duplicate fields or attributes, that are hard to generically represent in a map.
XML Woes
Just to see if I can even get something working, I’m going to autogenerate the
golang struct needed to store the ListUsersResponse
XML
object. This is a common thing in golang,
and it exists for JSON as well. The idea
is that you just pass in an XML or JSON object, and it shows you the generated
golang code that would need to store that object. Unlike python where you can
just pull it into a generic map, golang likes to have more structure around how
things get loaded.
For example, when I pass this object to the converter:
<ListUsersResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ListUsersResult>
<IsTruncated>false</IsTruncated>
<Users>
<member>
<Path>/</Path>
<PasswordLastUsed>2019-08-31T04:27:09Z</PasswordLastUsed>
<Arn>arn:aws:iam::000000000000:user/shaun.verch</Arn>
<UserName>shaun.verch</UserName>
<UserId>AAAAAAAAAAAAAAAAAAAAA</UserId>
<CreateDate>2017-09-26T21:42:46Z</CreateDate>
</member>
</Users>
</ListUsersResult>
<ResponseMetadata>
<RequestId>47a297d4-ebe2-4d1e-b636-2d4a3d0b7318</RequestId>
</ResponseMetadata>
</ListUsersResponse>
I get this code:
type ListUsersResponse struct {
XMLName xml.Name `xml:"ListUsersResponse"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
ListUsersResult struct {
Text string `xml:",chardata"`
IsTruncated string `xml:"IsTruncated"`
Users struct {
Text string `xml:",chardata"`
Member struct {
Text string `xml:",chardata"`
Path string `xml:"Path"`
PasswordLastUsed string `xml:"PasswordLastUsed"`
Arn string `xml:"Arn"`
UserName string `xml:"UserName"`
UserId string `xml:"UserId"`
CreateDate string `xml:"CreateDate"`
} `xml:"member"`
} `xml:"Users"`
} `xml:"ListUsersResult"`
ResponseMetadata struct {
Text string `xml:",chardata"`
RequestId string `xml:"RequestId"`
} `xml:"ResponseMetadata"`
}
Let’s just shove that into the code that was generated by that CLI library and see if it even works. If it does, I might be able to use the XML to golang struct generator in the CLI generator to actually get the whole thing working with XML, since the openapi spec has the structure of this response in it.
Getting One Response Working
Now that I have the generated struct, I want to get the CLI library using it.
I changed a line that looked like:
var decoded map[string]interface{}
To:
var decoded ListUsersResponse
This is really all it takes because the function that takes the decoded
object
takes an interface{}
, which means “any type that you pass it”, and the type
system works out the rest. I had a few compiler errors that I had to fix, but
after that I ran the cli and got this!
$ env HTTPS_PROXY=http://localhost:8080/ ./main get-listusers ListUsers 2010-05-08
{
"XMLName": {
"Space": "https://iam.amazonaws.com/doc/2010-05-08/",
"Local": "ListUsersResponse"
},
"Text": "\n \n \n",
"Xmlns": "https://iam.amazonaws.com/doc/2010-05-08/",
"ListUsersResult": {
"Text": "\n \n \n ",
"IsTruncated": "false",
"Users": {
"Text": "\n \n ",
"Member": {
"Text": "\n \n \n \n \n \n \n ",
"Path": "/",
"PasswordLastUsed": "2019-08-31T04:27:09Z",
"Arn": "arn:aws:iam::000000000000:user/shaun.verch",
"UserName": "shaun.verch",
"UserId": "AAAAAAAAAAAAAAAAAAAAA",
"CreateDate": "2017-09-26T21:42:46Z"
}
}
},
"ResponseMetadata": {
"Text": "\n \n ",
"RequestId": "cacfc5fe-f397-4aed-97f7-2c5cd588cac2"
}
}
Nice! The interface is a bit ugly, but it works! Now I’m querying the AWS API with completely auto generated code, which means that any effort I put in to fix this interface will immediately work (hopefully) with all the endpoints instead of just one.
Why Do Any Of This?
As one engineer working on this without the resources of an entire multi-billion dollar mega corporation, this is pretty important. AWS has over a hundred different services. The IAM service alone (which admittedly is one of the more complex services) has 140 subcommands according to the aws cli. That means there are on the order of a thousand different commands, so there is no hope of keeping up without some autogeneration.
I know that many libraries, such as the boto
AWS client for python, are
already autogenerated in a large
part from some
kind of spec. However, as far as I know they are AWS specific formats, so each
library and language would have to implement special parsing for it to work.
Next Time
I’ve officially proven that I can use the OpenAPI spec to interact with the AWS API. Now, let’s refocus on the main goal.
I want a tool that can dump the current state of IAM (and eventually my entire AWS account). That goes back to the original motivation for doing this, where I want to alert on certain things about my account being misconfigured.
So what’s the best way to do that? Well, first I should generate a client for AWS, instead of using this CLI library. That was fun, but my autogenerated code will be generated against the actual library functions, not the CLI.
I’m going to generate a rust reqwest
based library with the
openapi-generator, see what
that looks like, and see if I can auto generate the functions to dump the
account state.
I also want to look into generating a fake “schema” of the cloud provider based on the response and request formats. For example, if I know what “ListUsers” returns, I can maybe formulate an idea of what the “Users” collection contains in it.