Search code examples
pythonazure-devopspipazure-artifactsvscode-devcontainer

Sharing artifacts-keyring authentication and pip.conf with devcontainer to reach private Azure feed


We're using Azure DevOps at work and have used the Artifacts feed in there to share Python packages internally which is lovely.

I've been using WSL2 and artifacts-keyring to authenticate with DevOps and a pip.conf file to specify the feed URL as instructed in https://learn.microsoft.com/en-us/azure/devops/artifacts/quickstarts/python-cli?view=azure-devops#consume-python-packages which works great.

To develop Python and keep dependencies isolated while still having access to the private feed and authentication I've used Azure Devops Artifacts Helpers with virtualenv which have also worked like a charm.

Now we're trying more and more to use devcontainers to get even more isolation and ease of setup for new developers.

I've searched wide and far for a way to get access to the pip.conf URL:s and the artifacts-keyring authentication inside of my devcontainer. Is there any way that I can provide my container with these? I've tried all the different solutions I can find on Google but none of them work seamlessly and without PAT:s.

I do not want to use any PAT since I've already authenticated in WSL2.

I'm using WSL2 as the host i.e. I'm cloning the repo in WSL2 and then starting VScode and the devcontainer from there.

Is there anything related to keyring which I can mount inside the container so that it will see that the authentication is already done?

I could live with providing a copy of the pip.conf inside my repo which I could copy to the container on build, but to have to authenticate each time I rebuild my container is to much and so is using a PAT.

Kind Regards Carl


Solution

  • I ran into the same problem today. The trouble is that the token cache file, $HOME/.local/share/MicrosoftCredentialProvider/SessionTokenCache.dat, is being written within the storage local to the container which gets reset each time we rebuild the devcontainer. This causes us to have to click the https://microsoft.com/devicelogin link every time we rebuild our container and login again in our browser which is a huge time waster.

    I was able to resolve this by mounting my host's $HOME/.local/share/ into my devcontainer, so the SessionTokenCache.dat can survive past the rebuild. This is done by adding the following config in your devcontainer.json:

        "mounts": [
            "source=${localEnv:HOME}/.local/share/,target=/home/vscode/.local/share/,type=bind,consistency=cached"
        ],
    
    

    This assumes you have "remoteUser": "vscode" in your devcontainer.json otherwise the home location in the target will need adjustment.

    If you are using a Python devcontainer image, you may get an error that dotnet is a missing dependency for artifacts-keyring, but this can be resolved by adding a features configuration for dotnet to your devcontainer.json:

        "features": {
            "dotnet": {
                "version": "latest",
                "runtimeOnly": false
              }
        },
    

    If you are also transitioning from using pip.conf outside of venv to now having a venv, the next problem you may run into is that when a venv the pip.conf has to exist in the .venv folder (you may have customized this folder name). For this I run a simple cp ./pip.conf ./.venv/pip.conf to copy the file from the root of my checkout into my .venv folder.

    My full devcontainer.json:

    // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
    // https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/python-3
    {
        "name": "Python 3",
        "build": {
            "dockerfile": "Dockerfile",
            "context": "..",
            "args": { 
                // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
                // Append -bullseye or -buster to pin to an OS version.
                // Use -bullseye variants on local on arm64/Apple Silicon.
                "VARIANT": "3.8",
                // Options
                "NODE_VERSION": "lts/*"
            }
        },
        "features": {
            "dotnet": {
                "version": "latest",
                "runtimeOnly": false
              }
        },
    
        // Set *default* container specific settings.json values on container create.
        "settings": { 
            "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
            "python.testing.pytestEnabled": true,
            "python.testing.pytestPath": "${workspaceFolder}/.venv/bin/pytest",
            "python.testing.pytestArgs": [
                "tests"
            ],    
            "python.testing.unittestEnabled": false,
            "python.testing.nosetestsEnabled": false,
            "python.envFile":   "${workspaceFolder}/src/.env_local",
            "python.linting.enabled": false,
            "python.linting.pylintEnabled": false,
            "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
            "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
            "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
            "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
            "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
            "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
            "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
            "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
            "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
            "azureFunctions.deploySubpath": "${workspaceFolder}/src/api",
            "azureFunctions.scmDoBuildDuringDeployment": true,
            "azureFunctions.pythonVenv": "${workspaceFolder}/.venv",
            "azureFunctions.projectLanguage": "Python",
            "azureFunctions.projectRuntime": "~3",
            "azureFunctions.projectSubpath": "${workspaceFolder}/src/api",
            "debug.internalConsoleOptions": "neverOpen"
        },
        
        "runArgs": ["--env-file","${localWorkspaceFolder}/src/.env_local"],
    
        // Add the IDs of extensions you want installed when the container is created.
        "extensions": [
            "ms-python.python",
            "ms-python.vscode-pylance",
            "ms-azuretools.vscode-azurefunctions",
            "ms-vscode.azure-account",
            "ms-azuretools.vscode-docker",
            "DurableFunctionsMonitor.durablefunctionsmonitor",
            "eamodio.gitlens",
            "ms-dotnettools.csharp",
            "editorconfig.editorconfig",
            "littlefoxteam.vscode-python-test-adapter"
        ],
    
        "mounts": [
            "source=${localEnv:HOME}/.local/share/,target=/home/vscode/.local/share/,type=bind,consistency=cached"
        ],
    
        // Use 'forwardPorts' to make a list of ports inside the container available locally.
        "forwardPorts": [9090, 9091],
    
        // Use 'postCreateCommand' to run commands after the container is created.
        "postCreateCommand": "bash ./resetenv.sh",
    
        // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
        "remoteUser": "vscode"
    }
    

    The referenced resetenv.sh:

    #!/bin/bash
    
    pushd . > /dev/null
    SCRIPT_PATH="${BASH_SOURCE[0]}";
    if ([ -h "${SCRIPT_PATH}" ]) then
      while([ -h "${SCRIPT_PATH}" ]) do cd "$(dirname "$SCRIPT_PATH")"; SCRIPT_PATH=`readlink "${SCRIPT_PATH}"`; done
    fi
    cd "$(dirname ${SCRIPT_PATH})" > /dev/null
    SCRIPT_PATH=$(pwd);
    popd  > /dev/null
    
    
    pushd ${SCRIPT_PATH}
    deactivate
    python3 -m venv --clear .venv
     . .venv/bin/activate && pip install --upgrade pip && pip install twine keyring artifacts-keyring && cp ./pip.conf ./.venv/pip.conf && pip install -r deployment/requirements.txt -r deployment/api/requirements.txt 
     echo Env Reset
    

    full Dockerfile:

    # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/python-3/.devcontainer/base.Dockerfile
    
    # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
    ARG VARIANT="3.8"
    FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
    
    # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
    ARG NODE_VERSION="lts/*"
    RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
    
    # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
    # COPY requirements.txt /tmp/pip-tmp/
    # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
    #    && rm -rf /tmp/pip-tmp
    
    # [Optional] Uncomment this section to install additional OS packages.
    # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    #     && apt-get -y install --no-install-recommends <your-package-list-here>
    
    # [Optional] Uncomment this line to install global node packages.
    RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g azure-functions-core-tools@3 --unsafe-perm true" 2>&1
    
    # Instead of running Azurite from within this devcontainer, we run a docker container on the host to be shared by VSCode and VS
    # See https://github.com/VantageSoftware/azurite-forever
    #RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g azurite --unsafe-perm true" 2>&1
    

    The referenced .env_local is just a simple env file that is used to set secrets and other config as environment variables inside the devcontainer.