Für Entwickler, Architekten, Projektleiter und...

Read more

In der Welt der Softwareentwicklung ist die...

Read more

QUIBIQ spendet für den guten Zweck – und für...

Read more

Eine bestimmte Antwort auf einen HTTP Request zu...

Read more

In einer Welt, die von stetigem Wandel geprägt...

Read more

In einem unserer Kundenprojekte, war das Ziel eine...

Read more

QUIBIQ Hamburg wird mit dem Hamburger...

Read more

Zwei Tage lang wurde vom 14.-15.11 wieder das...

Read more

Was ist ein Excel-Plugin – und wann ist es...

Read more

Wir expandieren, bringen Kunden und Talente besser...

Read more

How-to: DevOps REST API: Library Variablengruppen automatisiert updaten

DevOps bietet eine Vielzahl von Rest-Endpunkten. In diesem Artikel wird auf das automatische Updaten von Library Variablengruppen via Rest-API, Script und Pipelines eingegangen. Als Script-Sprache wird Python verwendet, alternativ könnte natürlich auch PowerShell benutzt werden.

1. Vorwort
DevOps bietet eine Vielzahl von Rest-Endpunkten. In diesem Artikel wird auf das automatische Updaten von Library Variablengruppen via Rest-API, Script und Pipelines eingegangen. Als Script-Sprache wird Python verwendet, alternativ könnte auch PowerShell benutzt werden.

2. DevOps Rest-API
Es folgt ein allgemeiner Überblick zur Benutzung der DevOps-API.

2.1 Dokumentation
Es steht eine Großzahl von Endpunkten zur Verfügung. Die Microsoft-Dokumentation bietet einen guten Überblick inklusive Beschreibung zu allen Möglichkeiten.
https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-6.1

2.2 Sicherheit
Um Zugriff auf die Endpunkte zu bekommen, muss zuerst ein Personal Access Token erstellt werden. Innerhalb von DevOps unter "User Settings" kann dieser wie folgt generiert werden:

Beim Erstellen des Tokens können die gewünschten Berechtigungen verteilt werden, zu Testzwecken wird "Full Access" gewählt. Zu berücksichtigen ist, dass dieser Token nicht unbegrenzt, max. für ein Jahr, nutzbar ist:

Sobald auf "Create" geklickt wird, ist der Token erstellt. Er ist nur einmalig bei der Erstellung sichtbar und sollte sofort gespeichert werden –> idealerweise auf KeePass oder in einem anderen Passwort-Manager.

3. Projekt erstellen
Es wird ein Projekt mit dem Namen "quiteq", sowie einer Variablengruppe mit dem Namen "quiteq-variables" erstellt und es werden einige Testvariablen eingefügt. Der Projektname ist selber frei wählbar.

3. Test mit Postman
Bei vorhandenem Token und einer Variablengruppe ist bereits alles für einen ersten Test bereit. Dieser Test beinhaltet einen einfachen GET- und PUT-Request via Postman.

3.1 GET
Folgendes muss in Postman konfiguriert werden, um die Daten zu erhalten:

Die URL ist wie folgt aufgebaut: https://dev.azure.com/<organization>/<project>/_apis/distributedtask/variablegroups/<groupId>?api-version=6.1-preview.2

Organisation und Projekt sind direkt in DevOps in der URL ersichtlich, die groupId kann in der Variablengrupp in der URL gelesen werden. Alternativ kann sie auch weggelassen werden, um alle Gruppen inklusive ID zu erhalten. Weiterhin muss in Postman unter Authorization "Basic Auth" mit dem zuvor erstellten Token gespeichert sein. Ein "Username" wird nicht benötigt.

Folgendes Resultat wird nun als Antwort empfangen:

 

{

 

    "variables": {

        "testStr": {

            "value""testvalue"

        },

        "testNr": {

            "value""1"

        }

    },

    "id"3,

    "type""Vsts",

    "name""quiteq-variables",

    "description""",

    "createdBy": {

        "displayName""Patrick Hettich",

        "id""b58867c9-19e8-49b8-abc8-5e2649e8ee36",

        "uniqueName""patrick.hettich@quibiq.ch"

    },

    "createdOn""2021-07-01T09:43:56.9133333Z",

    "modifiedBy": {

        "displayName""Patrick Hettich",

        "id""b58867c9-19e8-49b8-abc8-5e2649e8ee36",

        "uniqueName""patrick.hettich@quibiq.ch"

    },

    "modifiedOn""2021-07-01T09:43:56.9133333Z",

    "isShared"false,

    "variableGroupProjectReferences": [

        {

            "projectReference": {

                "id""4bc9fe15-af95-4f56-9a57-61c6110e9b30",

                "name""quiteq"

            },

            "name""quiteq-variables",

            "description"""

        }

    ]

}

3.2 PUT
Diese empfangenen Daten können nun in abgeänderter Form via PUT-Request zurückgesendet werden, um die Variablengruppe anzupassen. In Postman muss nun der Request, sowie folgender Body als Raw JSON mitgegeben werden. Die restlichen Einstellungen, inklusiver URL bleiben gleich wie zuvor:

{

    "variables": {

        "testStrUpdated": {

            "value""updated value"

        },

        "testNr": {

            "value""100"

        },

        "newValue": {

            "value""new Value from Postman"

        }

    },

    "id"3,

    "type""Vsts",

    "name""quiteq-variables",

    "description""",

    "isShared"false,

    "variableGroupProjectReferences": [

        {

            "projectReference": {

                "id""4bc9fe15-af95-4f56-9a57-61c6110e9b30",

                "name""quiteq"

            },

            "name""quiteq-variables",

            "description"""

        }

    ]

}

Nach dem Absenden sollte bereits eine Antwort mit den neuen Werten empfangen werden. Das Resultat kann aber auch direkt in DevOps überprüft werden:

3.3 Secret Variablen
Bei geheimen Variablen wird beim GET-Request der Wert NULL gesetzt:

"secretVariable": {

  "value"null,

  "isSecret"true

}

Dies gilt vor allem zu berücksichtigen, wenn das empfangene JSON verwendet wird, um die Gruppe wieder anzupassen. Bei erneutem PUT wird alles ersetzt, inklusive geheimer Variable mit dem Wert Null. Natürlich kann eine geheime Variable in dem JSON gesetzt werden. Sicherheitstechnisch ist das aber nicht ideal.

"secretVariable": {

  "value""SecretPassword",
 
"isSecret"true

}

4. Script
Das Script wird via Python erstellt. Es folgt eine Erklärung der zu übergebenen Parameter und den Funktionen.

4.1 Solution
Je nach Anforderung ist die Solution anders aufgebaut, in diesem Beispiel gehen wir davon aus, dass auf die Stages DEV, TEST, QA und PROD deployed wird, somit wird diese folgendermassen aufgebaut:

Wichtig ist hierbei, dass bei allen Files folgende zwei Properties gesetzt werden. Somit werden die Files beim Build in die Artifacts geladen.:

 

4.2 Parameter
Theoretisch könnte natürlich alles hartcodiert werden. Um das Script jedoch etwas dynamisch zu gestalten, werden folgende Parameter via Pipeline übergeben:

DevOps Token, in Punkt 2.2 erstellt.
basic_auth = sys.argv[1]
Pfad zu den JSON-Files, für jede Stage kann diese angepasst werden.
base_path = sys.argv[2]
Link zu DevOps, dieser wird wie folgt aufgebaut:
<ORGANISATION>/<PROJECT-NAME>/_apis/distributedtask/variablegroups/
base_uri = sys.argv[3]  
API-Version, diese ist zurzeit:api-version=6.1-preview.2
api_version = sys.argv[4]  

4.3 Funktionen
Es werden drei Funktionen erstellt:

def getEntries():

    entries = os.listdir(base_path)

    for entry in entries:

        if(entry.endswith('.json')):

            fullPath = f'{base_path}\{entry}'

            with open(fullPath, encoding='utf-8-sig') as f:

                strContent = f.read().replace('\n', '')

                dictContent = json.loads(strContent)

                updateVariables(dictContent)

Mit Hilfe des Base-Path werden hier alle JSON-Files gelesen, \n wird entfernt und der Fileinhalt als Python-Dictionary an die Funktion updateVariables weitergegeben.

def updateVariables(jsonDict):

    id = jsonDict['id']

    base64_auth = b64Encode(f':{basic_auth}')

    jsonBody = json.dumps(jsonDict)

    url = f'{base_uri}{id}?{api_version}'

    req = urllib.request.Request(url, data=bytes(

        jsonBody.encode('utf-8')), method='PUT')

    req.add_header('Authorization', f'Basic {base64_auth}')

    req.add_header('content-type', 'application/json')

    withurllib.request.urlopen(req) as res:

        response_data = json.loads(res.read().decode('utf-8'))

        print(response_data)

Diese Funktion nimmt das Dictionary, liest daraus die ID, welche auf die Variablengruppe verweist, ruft die Funktion zum Umwandeln des Schlüssels in Base64 auf, wandelt das Dictionary zurück in JSON und sendet dann einen PUT-Request mit den Daten.

def b64Encode(string_val):

    bytes_val = string_val.encode('utf-8')

    bytes_base64 = base64.b64encode(bytes_val)

    string_base64 = bytes_base64.decode('utf-8')

    return string_base64

Der Key, der von DevOps generiert und als Parameter übergeben wurde, wird in Base64 umgewandelt.
 

4.4 Weiteres
Das JSON wird zuerst in ein Python-Dictionary und dann wiederum in JSON umgewandelt. Der Grund: je nach Einrückung können in dem JSON-File die Daten nicht korrekt an DevOps gesendet werden. Der Token von DevOps muss in Base64 konvertiert werden. Es werden jedoch in updateVariables vor den Key Doppelpunkte angefügt, da Basic-Authentication folgendes Format verlangt: <USERNAME>:<PASSWORD>

4.4 Komplettes Script

############################################################################

# Author            Patrick Hettich QUIBIQ AG

# Date              12.07.2021

# Description       Script to update Variable-Groups in Library.

############################################################################

importsys

importurllib.request

importos

importjson

importbase64

 

# Key created in DevOps

basic_auth = sys.argv[1]

# Base-Path where the JSON-Files are located, different for each stage

base_path = sys.argv[2] 

# Base-Uri, built like following:

# # <ORGANISATION>/<PROJECT-NAME>/_apis/distributedtask/variablegroups/

base_uri = sys.argv[3]

# Version of the API, currently: api-version=6.1-preview.2

api_version = sys.argv[4]  

 

# Encodes the Value given to Base64

def b64Encode(string_val):

    bytes_val = string_val.encode('utf-8')

    bytes_base64 = base64.b64encode(bytes_val)

    string_base64 = bytes_base64.decode('utf-8')

    return string_base64

 

# Updates the Variablegroup with the values given

def updateVariables(jsonDict):

    id = jsonDict['id']

    base64_auth = b64Encode(f':{basic_auth}')

    jsonBody = json.dumps(jsonDict)

    url = f'{base_uri}{id}?{api_version}'

    req = urllib.request.Request(url, data=bytes(

        jsonBody.encode('utf-8')), method='PUT')

    req.add_header('Authorization', f'Basic {base64_auth}')

    req.add_header('content-type', 'application/json')

    withurllib.request.urlopen(req) as res:

        response_data = json.loads(res.read().decode('utf-8'))

        print(response_data)

 

# Gets all entries from the given Base-Path

def getEntries():

    entries = os.listdir(base_path)

    for entry in entries:

        if(entry.endswith('.json')):

            fullPath = f'{base_path}\{entry}'

            with open(fullPath, encoding='utf-8-sig') as f:

                strContent = f.read().replace('\n', '')

                dictContent = json.loads(strContent)

                updateVariables(dictContent)

 

getEntries()

5. Pipelines erstellen
Nachdem das Script erstellt wurde, müssen noch Pipelines erstellt werden – zum einen eine Build Pipeline, um die Files in die Artifacts zu laden, zum andere eine Release-Pipeline, die dann alle Stages beinhaltet. In unserem Beispiel werden wir im Release nur auf die Stage DEV deployen.

5.1 Build Pipeline
Zuerst wird eine klassische Pipeline erstellt. Folgende Tasks werden benötigt:

  • Copy Files-Task
  • Publish Artifact

Die Daten müssen in das $(build.artifactstagingdirectory) verschoben werden, dies geschieht mithilfe einem Copy Files-Task.
Folgende Werte werden dabei gesetzt:

Source Folder: $(system.defaultworkingdirectory)

Contents:         **\quiteq\**
(Je nach Aufbau der Solution ist dies ein wenig anders, soll aber zum Script und den JSON-Files verweisen.)

Target Folder:  $(build.artifactstagingdirectory)

"Publish Artifact" folgt dem ersten Task. Der Name des Artifacts kann beliebig angepasst werden, für diese Demonstration wird er so belassen, wie er ist.

Sollte die Pipeline nun erfolgreich durchlaufen, werden die Artefakte published und consumed sein:

Dies kann mit einem Klick auf den oben markierten Button bestätigt werden:

5.2 Release Pipeline
In diesem Beispiel wird nun eine Release-Pipeline für das Ausführen des Python-Scripts auf DEV erstellt. Hierbei müssen die Artifacts der Build-Pipeline hinzugefügt werden:

Es wird eine Stage DEV erstellt und konfiguriert. Diese Stage beinhaltet einen einzigen Task "Python Script", der sich in einem Agent Job befindet:
Folgende Werte müssen angegeben werden:

Script Path:      $(System.DefaultWorkingDirectory)/_quiteq-CI/drop/quiteq/update_varGroups.py

Arguments:      <DevOps Key> $(System.DefaultWorkingDirectory)/_quiteq-CI/drop/quiteq/DEV https://patrickhettich.visualstudio.com/quiteq/_apis/distributedtask/variablegroups/ api-version=6.0-preview.2
(Die Reihenfolge muss zu den Parametern im Script übereinstimmen und alle Argumente werden mit einem Leerzeichen getrennt)

Beim Durchlauf der Pipeline können die Variablengruppen überprüft werden. An sich genügt es schon, auf das "Date modified" zu schauen, alternativ können die einzelnen Variablen verglichen werden.

6. Schlusswort

Ein Vorteil dieser automatisierten Updates liegt darin, dass sich die Variablen in einem Repository befinden. Das bedeutet: Bei versehentlichem Löschen sind nicht sofort alle Einträge weg. Weiterhin ist mit guten Kommentaren in den Commits klar, warum welcher Eintrag erfasst, bearbeitet oder gelöscht wurde.

Zu berücksichtigen gilt jedoch, dass es nicht ideal ist, mit dieser Methode Passwörter oder sonstig geheime Daten zu updaten. Eine Lösung wäre das Speichern solcher Informationen in einem Azure Key Vault oder einer separaten Variablengruppe. Weiterhin ist nur ein PUT- und kein Patch-Request möglich, welche ausnahmslos alle vorhanden Daten ersetzt. Das bedeutet, dass nicht manuell Variablen direkt auf DevOps konfiguriert werden dürfen.
 

© QUIBIQ GmbH · Imprint · Data protection