The clash of duck typing and api design
So today we have a small example of how, sometimes, properties of separate systems (and us humans are a system for this post) interconnect into making an odd behaviour happen.
The setup
So, let’s start with what we are trying to achieve: programmatically creating and triggering an Azure Devops Release Pipeline. Luckily for us, Microsoft provides a Python SDK that we can use to achieve this.
The code
Below is an example of code doing exactly that.
def __create_release(release_client, release_definition_id, project_name):
metadata = ReleaseStartMetadata()
metadata.definition_id = release_definition_id
return release_client.create_release(metadata, project_name)
def __trigger_release(release_client, project_name, release, full_stage_name):
stage = __get_stage(full_stage_name, release)
stage.status = "inProgress"
stage.variables = None
return release_client.update_release_environment(
stage, project_name, stage.release.id, stage.id
)
The ReleaseStartMetada
is one of the objects provided by the SDK. create_release
does exactly as advertise on the tin for a release pipeline. It returns an object of type Release
. __get_stage
is just a function that looks for a specific named stage on the release object. We update that stage to be “inProgress” to start the process. And then we update the release. There is a line there that solves the issue we found:
stage.variables = None
The API behaviour
When you crate a release, the object returned has all the information about it … including the variables that you have setup on the release pipeline. And as you can gather from what you see, those variables were originally passed back again on the update_release_environment
. Now if you get the variables and send them back again, where is the issue?
The issue resides on the behaviour around secret variables. When you get the data back on the Release
object after calling create_release
, and you get all variables, secrets variables end with a value of None
(because they are secret, of course). So when you pass the object back the secrets gets overriden with that value (None
). My colleague Chris Bimson, did suggest that probably a better option would have been to send an encrypted version of the secret variable, so even if you overrode the values, it would still have worked correctly.
The logs
Well, the above is not completely true: if the secrets are not marked to be overriden, the call to update_release_environment
actually fails, but because I had not yet set the logs properly, I made one of those errors in assuming that the error was on the create_release
(you know, assume makes an ass of me and me). So, I changed the properties of the variables to be able to be rewritten. Pffffft to me.
Remember, logs are important.
Even if is a small cli application, well setup log are your friend (of course, too much logging risk you being swamp in non-useful data).
Python
This is the last part of equation. create_release
returns a Release
object, but update_release_environment
actually takes a ReleaseEnvironmentUpdateMetadata
. As it happens, the member variables used for update_release_environment
are present on a Release
object, so ducktyping comes into effect. Had we been forced by a static type system to change the object, probably we will have realized exactly the issue earlier. I have nothing against duck typing (in fact, I find it quite powerful), and I like dynamic languages in some scenarios (like the one I am in), although there is always the irony of a dynamic language that has the sentence explicit is better than implicit
on their official Zen
The solution
Finally, we need to talk about that stage.variables = None
line being a solution to our issue. The behaviour of the API is that if you don’t pass any variable, it uses the default versions for the release. No overriding happens.
The best part of all of it was that, once we put the logging in place and we saw the exact place where it was failing, we quickly created an hypothesis of what was happening and were able to test it immediately.