Refactoring
to
maintainability

The clash of duck typing and api design

2021-03-01
Python Azure

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.


Return to Index

Unless otherwise stated, all posts are my own and not associated with any other particular person or company.

For all code - MIT license
All other text - Creative Commons Attribution-ShareAlike 4.0 International License
Creative Commons BY-SA license image