Things they don't tell you – Tagging containers in Azure DevOps
Tags: azure, devops, docker
Published: 2021-09-15 03:38:33


---
title: Things they don't tell you – Tagging containers in Azure DevOps
published: true
description: Here's an interesting gotcha that has kept us occupied for a little while. Our client wanted us to build an Azure DevOps pipeline that would build a container, tag it, &hellip; Continue reading &#8220;Things they don't tell you – Tagging containers in Azure DevOps&#8221;
tags: azure, devops, docker
canonical_url: https://blog.wiseowls.co.nz/index.php/2021/09/15/tagging-containers-in-azure-devops/
cover_image: https://blog.wiseowls.co.nz/wp-content/uploads/2021/09/tom-radetzki-idRpmZVNs30-unsplash-scaled.jpg
---

Here’s an interesting gotcha that has kept us occupied for a little while. Our client wanted us to build an Azure DevOps pipeline that would build a container, tag it, and launch the image to do more work. As the result was not really worth pushing up to image registries, we decided to go fully local.

## Setting up agent pool

Our client had further constraint that prevented them from using managed agents so first thing we had to do was to define a [local pool](https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues?view=azure-devops&tabs=yaml%2Cbrowser). The process was uneventful, so we thought we’re off to a good start.

Creating Azure DevOps pipeline
------------------------------

Our first stab yielded a pipeline definition along the following lines:

```yaml
trigger:
  - none

jobs:
  - job: test
    pool:
      name: local-linux-pool
    displayName: Build Cool software
    steps:
      - task: Bash@3
        displayName: Prune leftover containers
        inputs:
          targetType: inline
          script: |
            docker system prune -f -a

      - task: Docker@2
        displayName: Build worker container
        inputs:          
          command: build
          Dockerfile: 'container/Dockerfile'
          tags: |
            supercool/test-app # tagging container would simplify our next step and avoid us headaches of trying to figure out correct ID

      - bash: |
          docker run --name builderContainer -it --rm supercool/test-app:latest # we assume latest is the correct tag here
```


Nothing fancy here. We clean the environment before each run (this would be optional but helped troubleshooting). Then we build a container from a Dockerfile we found in source control. To make sure we run the right thing on next step we want to tag it.

But then it went sideways…
--------------------------

```bash
Unable to find image 'supercool/test-app:latest' locally
docker: Error response from daemon: pull access denied for test-app, repository does not exist or may require 'docker login': denied: requested access to the resource is denied.
```


This of course means docker could not detect a local image and went off to pull it from the default registry. And we don’t want that!

Upon further inspection we found command line that builds container DOES NOT tag it!

```bash
/usr/bin/docker build \
		-f /myagent/_work/1/s/container/Dockerfile \
		--label com.azure.dev.image.system.teamfoundationcollectionuri=https://dev.azure.com/thisorgdoesnotexist/ \
		--label com.azure.dev.image.system.teamproject=test \
		--label com.azure.dev.image.build.repository.name=test \
		--label com.azure.dev.image.build.sourceversion=3d7a6ed84b0e9538b1b29206fd1651b44c0f74d8 \
		--label com.azure.dev.image.build.repository.uri=https://thisorgdoesnotexist@dev.azure.com/thisorgdoesnotexist/test/_git/test \
		--label com.azure.dev.image.build.sourcebranchname=main \
		--label com.azure.dev.image.build.definitionname=test \
		--label com.azure.dev.image.build.buildnumber=20262388.5 \
		--label com.azure.dev.image.build.builduri=vstfs:///Build/Build/11 \
		--label image.base.ref.name=alpine:3.12 \
		--label image.base.digest=sha256:a296b4c6f6ee2b88f095b61e95c7dde451ba25598835b4978c9256d8c8ace48a \
		/myagent/_work/1/s/container

```


How does this happen?
---------------------

Luckily, [DockerV2](https://github.com/microsoft/azure-pipelines-tasks/tree/master/Tasks/DockerV2) is Open Source and freely available on GitHub. Looking at the code, we notice an interesting [error message](https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/DockerV2/task.json#L177): `"NotAddingAnyTagsToBuild": "Not adding any tags to the built image as no repository is specified."` Is that our clue? Seems like it may be. Let’s keep digging.

Further inspection [reveals](https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/DockerV2/dockerbuild.ts#L65) for tags to get applied, task must be able to infer image name:

```typescript
 if (imageNames && imageNames.length > 0) {
        ....
        tagArguments.push(imageName);
        ....
    }
    else {
        tl.debug(tl.loc('NotAddingAnyTagsToBuild'));
    }
```


And that information must come from a `repository` input parameter.

> Repository – (Optional) Name of repository within the container registry corresponding to the Docker registry service connection specified as input for `containerRegistry`
> 
> Looking at the [documentation](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/docker?view=azure-devops), this makes no sense

A further peek into the source code, however, reveals that developers have [kindly thought about](https://github.com/microsoft/azure-pipelines-tasks/blob/master/common-npm-packages/docker-common-v2/containerconnection.ts#L102) local tagging:

```typescript
public getQualifiedImageNamesFromConfig(repository: string, enforceDockerNamingConvention?: boolean) {
        let imageNames: string[] = [];
        if (repository) {
            let regUrls = this.getRegistryUrlsFromDockerConfig();
            if (regUrls && regUrls.length > 0) {

                // not our case, skipping for brevity
            }
            else {
                // in case there is no login information found and a repository is specified, the intention
                // might be to tag the image to refer locally.
                let imageName = repository;
                if (enforceDockerNamingConvention) {
                    imageName = imageUtils.generateValidImageName(imageName);
                }
                
                imageNames.push(imageName);
            }
        }

        return imageNames;
    }
```


Now we can solve it
-------------------

Adding `repository` input to our task without specifying `containerRegistry` should get us the desired result:

```yaml
...
      - task: Docker@2
        displayName: Build worker container
        inputs:          
          command: build
          Dockerfile: 'container/Dockerfile'
          repository: supercool/test-app # moving this from tag input to repository does magic!
...
```


Looking at the logs we seem to have won:

```bash
/usr/bin/docker build \
		-f /myagent/_work/1/s/container/Dockerfile \
		#... ADO labels go here, irrelevant
		-t supercool/test-app \
		/myagent/_work/1/s/container
```


Conclusion
----------

This scenario, however far-fetched it may appear, seems to be fully supported. At least on the code level. Documentation is lacking a little bit, but I understand how this nuance may be hard to convey in 1-2 paragraphs when there’s so much else to cover.