Use PAT in env var/path to authenticate to Azure Pipelines (#82)

This commit is contained in:
jotaylo 2020-08-13 15:16:33 -07:00 коммит произвёл GitHub
Родитель 79373a9432
Коммит dd0b82f5d3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 95 добавлений и 14 удалений

Просмотреть файл

@ -1,9 +1,11 @@
import base64
import json import json
import requests import requests
import time import time
import argparse import argparse
import kfp import kfp
import adal import adal
import os
def info(msg, char="#", width=75): def info(msg, char="#", width=75):
@ -13,18 +15,18 @@ def info(msg, char="#", width=75):
print(char * width) print(char * width)
def send_complete_event(callbackinfo, status): # noqa: E501 def send_complete_event(callbackinfo, pat, status): # noqa: E501
callback_vars = json.loads(callbackinfo.replace("'", '"')) # noqa: E501 callback_vars = json.loads(callbackinfo.replace("'", '"')) # noqa: E501
url = r"{planUri}/{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/events?api-version=2.0-preview.1".format( # noqa: E501 url = r"{planUri}/{projectId}/_apis/distributedtask/hubs/{hubName}/plans/{planId}/events?api-version=2.0-preview.1".format( # noqa: E501
planUri=callback_vars["PlanUri"], projectId=callback_vars["ProjectId"], hubName=callback_vars["HubName"], planId=callback_vars["PlanId"]) # noqa: E501 planUri=callback_vars["PlanUri"], projectId=callback_vars["ProjectId"], hubName=callback_vars["HubName"], planId=callback_vars["PlanId"]) # noqa: E501
data = {'name': 'TaskCompleted', data = {'name': 'TaskCompleted',
'taskId': callback_vars["TaskInstanceId"], 'jobId': callback_vars["JobId"], 'result': status} # noqa: E501 'taskId': callback_vars["TaskInstanceId"], 'jobId': callback_vars["JobId"], 'result': status} # noqa: E501
header = {'Authorization': 'Bearer ' + callback_vars["AuthToken"]} header = {'Authorization': 'Basic ' + pat}
response = requests.post(url, json=data, headers=header) response = requests.post(url, json=data, headers=header)
print(response) print(response)
def get_compoenet_status(kfp_host_url, kfp_run_id, token=None): def get_component_status(kfp_host_url, kfp_run_id, token=None):
status = "Suceeded" status = "Suceeded"
client = kfp.Client(host=kfp_host_url, client = kfp.Client(host=kfp_host_url,
existing_token=token) existing_token=token)
@ -52,6 +54,21 @@ def get_access_token(tenant, clientId, client_secret):
return token['accessToken'] return token['accessToken']
def get_pat(pat_env, pat_path_env):
if pat_env:
# Read PAT from env var
pat = os.environ[pat_env]
elif pat_path_env:
# Read PAT from file
with open(os.environ[pat_path_env], 'r') as f:
pat = f.readline()
f.close
else:
raise Exception('Please provide a PAT via pat_env or pat_path_env')
pat = ":" + pat
return str(base64.b64encode(pat.encode("utf-8")), "utf-8")
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Azure DevOps Callback') parser = argparse.ArgumentParser(description='Azure DevOps Callback')
parser.add_argument('-hst', '--kfp_host_url', parser.add_argument('-hst', '--kfp_host_url',
@ -65,8 +82,14 @@ if __name__ == "__main__":
help='service_principal_id') help='service_principal_id')
parser.add_argument('-p', '--service_principal_password', parser.add_argument('-p', '--service_principal_password',
help='service_principal_password') help='service_principal_password')
parser.add_argument('-ppe', '--pat_path_env',
help='Name of environment variable containing the path to the Azure DevOps PAT') # noqa: E501
parser.add_argument('-pe', '--pat_env',
help='Name of environment variable containing the Azure DevOps PAT') # noqa: E501
args = parser.parse_args() args = parser.parse_args()
status = get_compoenet_status(args.kfp_host_url, args.run_id, get_access_token( # noqa: E501 pat = get_pat(args.pat_env, args.pat_path_env)
status = get_component_status(args.kfp_host_url, args.run_id, get_access_token( # noqa: E501
args.tenant_id, args.service_principal_id, args.service_principal_password)) # noqa: E501 args.tenant_id, args.service_principal_id, args.service_principal_password)) # noqa: E501
send_complete_event(args.azdocallback, status) send_complete_event(args.azdocallback, pat, status)

Просмотреть файл

@ -6,6 +6,8 @@ inputs:
- {name: tenant_id, type: Integer} - {name: tenant_id, type: Integer}
- {name: service_principal_id, type: String} - {name: service_principal_id, type: String}
- {name: service_principal_password, type: String} - {name: service_principal_password, type: String}
- {name: pat_env, type: String, default: '', description: 'Name of environment variable containing Azure DevOps PAT'}
- {name: pat_path_env, type: String, default: '', description: 'Name of environment variable containing path to Azure DevOps PAT'}
implementation: implementation:
container: container:
@ -17,5 +19,7 @@ implementation:
'--run_id', {inputValue: run_id}, '--run_id', {inputValue: run_id},
'--tenant_id', {inputValue: tenant_id}, '--tenant_id', {inputValue: tenant_id},
'--service_principal_id', {inputValue: service_principal_id}, '--service_principal_id', {inputValue: service_principal_id},
'--service_principal_password', {inputValue: service_principal_password} '--service_principal_password', {inputValue: service_principal_password},
'--pat_env', {inputValue: pat_env},
'--pat_path_env', {inputValue: pat_path_env}
] ]

Просмотреть файл

@ -6,6 +6,8 @@ inputs:
- {name: organization, type: String, description: 'Azure DevOps organization'} - {name: organization, type: String, description: 'Azure DevOps organization'}
- {name: project, type: String, description: 'Azure DevOps project'} - {name: project, type: String, description: 'Azure DevOps project'}
- {name: id, type: Integer, description: 'Azure Pipeline definition id'} - {name: id, type: Integer, description: 'Azure Pipeline definition id'}
- {name: pat_env, type: String, default: '', description: 'Name of environment variable containing Azure DevOps PAT'}
- {name: pat_path_env, type: String, default: '', description: 'Name of environment variable containing path to Azure DevOps PAT'}
- {name: source_branch, type: String, default: '', description: 'Source branch for the pipeline'} - {name: source_branch, type: String, default: '', description: 'Source branch for the pipeline'}
- {name: source_version, type: String, default: '', description: 'Source version for the pipeline'} - {name: source_version, type: String, default: '', description: 'Source version for the pipeline'}
- {name: parameters, type: String, default: '', description: 'Parameters for the pipeline'} - {name: parameters, type: String, default: '', description: 'Parameters for the pipeline'}
@ -19,6 +21,8 @@ implementation:
--organization, {inputValue: organization}, --organization, {inputValue: organization},
--project, {inputValue: project}, --project, {inputValue: project},
--id, {inputValue: id}, --id, {inputValue: id},
--pat_env, {inputValue: pat_env},
--pat_path_env, {inputValue: pat_path_env},
--source_branch, {inputValue: source_branch}, --source_branch, {inputValue: source_branch},
--source_version, {inputValue: source_version}, --source_version, {inputValue: source_version},
--parameters, {inputValue: parameters} --parameters, {inputValue: parameters}

Просмотреть файл

@ -9,6 +9,8 @@ This task enables you to queue an [Azure Pipelines](https://docs.microsoft.com/e
|organization|string|Y|The Azure DevOps organization that contains the pipeline to be queued. https[]()://dev.azure.com/`organization`/project/_build?definitionId=id| |organization|string|Y|The Azure DevOps organization that contains the pipeline to be queued. https[]()://dev.azure.com/`organization`/project/_build?definitionId=id|
|project|string|Y|The Azure DevOps project that contains the pipeline to be queued. https[]()://dev.azure.com/organization/`project`/_build?definitionId=id| |project|string|Y|The Azure DevOps project that contains the pipeline to be queued. https[]()://dev.azure.com/organization/`project`/_build?definitionId=id|
|id|string|Y|The id of the pipeline definition to queue. Shown in the url as *pipelineId* or *definitionId*. https[]()://dev.azure.com/organization/project/_build?definitionId=`id`| |id|string|Y|The id of the pipeline definition to queue. Shown in the url as *pipelineId* or *definitionId*. https[]()://dev.azure.com/organization/project/_build?definitionId=`id`|
|pat_env|string|one of pat_env or pat_path_env|The name of the environment variable containing the PAT for Azure Pipelines authentication|
|pat_path_env|string|one of pat_env or pat_path_env|The name of the environment variable containing a path to the PAT for Azure Pipelines authentication|
|sourch_branch|string||The branch of the source code for queuing the pipeline.| |sourch_branch|string||The branch of the source code for queuing the pipeline.|
|source_version|string||The version (e.g. commit id) of the source code for queuing the pipeline.| |source_version|string||The version (e.g. commit id) of the source code for queuing the pipeline.|
|parameters|string||Json serialized string of key-values pairs e.g. `{ 'x': '1', 'y': '2' }`. These values can be accessed as `$(x)` and `$(y)` in the Azure Pipelines pipeline.| |parameters|string||Json serialized string of key-values pairs e.g. `{ 'x': '1', 'y': '2' }`. These values can be accessed as `$(x)` and `$(y)` in the Azure Pipelines pipeline.|
@ -25,6 +27,7 @@ import kfp
from kfp import components from kfp import components
from kfp.dsl.extensions.kubernetes import use_secret from kfp.dsl.extensions.kubernetes import use_secret
import kfp.compiler as compiler import kfp.compiler as compiler
from kubernetes import client as k8s_client
component_root = os.path.join(os.path.dirname( component_root = os.path.join(os.path.dirname(
os.path.abspath(__file__)), ".") os.path.abspath(__file__)), ".")
@ -34,14 +37,24 @@ queue_pipeline_op = components.load_component_from_file(os.path.join(component_r
def queue_az_pipeline(): def queue_az_pipeline():
secret_name = "azdopat" secret_name = "azdopat"
secret_path = "/app/secrets" secret_path = "/app/secrets"
pat_path_env = "PAT_PATH"
secret_file_path_in_volume = "azdopat"
organization = # organization organization = # organization
project = # project project = # project
pipeline_id = # id pipeline_id = # id
queue_task = queue_pipeline_op( queue_task = queue_pipeline_op(
organization=organization, organization=organization,
project=project, project=project,
id=pipeline_id id=pipeline_id,
pat_path_env=pat_path_env
).apply(use_secret(secret_name=secret_name, secret_volume_mount_path=secret_path)) ).apply(use_secret(secret_name=secret_name, secret_volume_mount_path=secret_path))
# path separator workaround when compiling on windows
queue_task.container.add_env_variable(
k8s_client.V1EnvVar(
name=pat_path_env,
value=secret_path + "/" + secret_file_path_in_volume
))
kfp.Client().create_run_from_pipeline_func(queue_az_pipeline, arguments={}) kfp.Client().create_run_from_pipeline_func(queue_az_pipeline, arguments={})
``` ```

Просмотреть файл

@ -26,6 +26,7 @@ POSSIBILITY OF SUCH DAMAGE.
""" """
import argparse import argparse
import os
from azure.devops.connection import Connection from azure.devops.connection import Connection
from msrest.authentication import BasicAuthentication from msrest.authentication import BasicAuthentication
@ -79,6 +80,10 @@ def main():
parser.add_argument('-i', '--id', parser.add_argument('-i', '--id',
required=True, required=True,
help='Id of the pipeline definition') help='Id of the pipeline definition')
parser.add_argument('-ppe', '--pat_path_env',
help='Name of environment variable containing the path to the Azure DevOps PAT') # noqa: E501
parser.add_argument('-pe', '--pat_env',
help='Name of environment variable containing the Azure DevOps PAT') # noqa: E501
parser.add_argument( parser.add_argument(
'--source_branch', help='Source branch for the pipeline') '--source_branch', help='Source branch for the pipeline')
parser.add_argument( parser.add_argument(
@ -86,13 +91,19 @@ def main():
parser.add_argument( parser.add_argument(
'--parameters', help='Parameters for the pipeline') '--parameters', help='Parameters for the pipeline')
# Read PAT from file
with open("/app/secrets/azdopat", 'r') as f:
pat = f.readline()
f.close()
args = parser.parse_args() args = parser.parse_args()
if args.pat_env:
# Read PAT from env var
pat = os.environ[args.pat_env]
elif args.pat_path_env:
# Read PAT from file
with open(os.environ[args.pat_path_env], 'r') as f:
pat = f.readline()
f.close
else:
raise Exception('Please provide a PAT via pat_env or pat_path_env')
client = get_client(args.organization, pat) client = get_client(args.organization, pat)
build = define_build(args.id, build = define_build(args.id,
args.source_branch, args.source_branch,

Просмотреть файл

@ -7,7 +7,7 @@ import kfp.compiler as compiler
import kfp.components as components import kfp.components as components
from kfp.azure import use_azure_secret from kfp.azure import use_azure_secret
from kubernetes.client.models import V1EnvVar from kubernetes.client.models import V1EnvVar
from utils.kfp_helper import use_databricks_secret, use_image, use_kfp_host_secret from utils.kfp_helper import use_databricks_secret, use_image, use_kfp_host_secret, use_secret_var
persistent_volume_path = '/mnt/azure' persistent_volume_path = '/mnt/azure'
@ -63,7 +63,12 @@ def tacosandburritos_train(
run_id=dsl.RUN_ID_PLACEHOLDER, run_id=dsl.RUN_ID_PLACEHOLDER,
tenant_id="$(AZ_TENANT_ID)", tenant_id="$(AZ_TENANT_ID)",
service_principal_id="$(AZ_CLIENT_ID)", service_principal_id="$(AZ_CLIENT_ID)",
service_principal_password="$(AZ_CLIENT_SECRET)").apply(use_azure_secret()).apply(use_kfp_host_secret()).apply(use_image(exit_image_name)) # noqa: E501 service_principal_password="$(AZ_CLIENT_SECRET)",
pat_env="PAT_ENV"
).apply(use_azure_secret()
).apply(use_kfp_host_secret()
).apply(use_image(exit_image_name)
).apply(use_secret_var("azdopat", "PAT_ENV", "azdopat"))
with dsl.ExitHandler(exit_op=exit_handler_op): with dsl.ExitHandler(exit_op=exit_handler_op):

Просмотреть файл

@ -62,6 +62,27 @@ def use_kfp_host_secret(secret_name='kfp-host-secret'):
return _use_kfp_host_secret return _use_kfp_host_secret
def use_secret_var(secret_name, env_var_name, secret_key):
def _use_secret_var(task):
from kubernetes import client as k8s_client
(
task.container
.add_env_variable(
k8s_client.V1EnvVar(
name=env_var_name,
value_from=k8s_client.V1EnvVarSource(
secret_key_ref=k8s_client.V1SecretKeySelector(
name=secret_name,
key=secret_key
)
)
)
)
)
return task
return _use_secret_var
def use_image(image_name): def use_image(image_name):
def _use_image(task): def _use_image(task):
task.image = image_name task.image = image_name