diff --git a/Dockerfile b/Dockerfile index 0d81fa9..ed0aa99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,5 +35,11 @@ WORKDIR /go/src/${MODULE_NAME} ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH RUN /bin/bash -c "curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh" +RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash + +# Set work directory. +RUN mkdir -p /go/src/${MODULE_NAME} +WORKDIR /go/src/${MODULE_NAME} +COPY . /go/src/${MODULE_NAME} RUN ["bundle", "install", "--gemfile", "./Gemfile"] \ No newline at end of file diff --git a/docker/linux/Dockerfile b/docker/linux/Dockerfile index 540680a..b8a3f70 100644 --- a/docker/linux/Dockerfile +++ b/docker/linux/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:16.04 +FROM ubuntu:18.04 # To make it easier for build and release pipelines to run apt-get, # configure apt to not require confirmation (assume the -y argument by default) @@ -6,16 +6,17 @@ ENV DEBIAN_FRONTEND=noninteractive RUN echo "APT::Get::Assume-Yes \"true\";" > /etc/apt/apt.conf.d/90assumeyes RUN apt-get update \ -&& apt-get install -y --no-install-recommends \ + && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ jq \ git \ iputils-ping \ - libcurl3 \ - libicu55 \ + libcurl4 \ + libicu60 \ libunwind8 \ - netcat + netcat \ + libssl1.0 WORKDIR /azp diff --git a/docker/linux/start.sh b/docker/linux/start.sh index ee931df..e40c567 100644 --- a/docker/linux/start.sh +++ b/docker/linux/start.sh @@ -73,9 +73,6 @@ curl -LsS $AZP_AGENTPACKAGE_URL | tar -xz & wait $! source ./env.sh -trap 'cleanup; exit 130' INT -trap 'cleanup; exit 143' TERM - print_header "3. Configuring Azure Pipelines agent..." ./config.sh --unattended \ @@ -88,11 +85,15 @@ print_header "3. Configuring Azure Pipelines agent..." --replace \ --acceptTeeEula & wait $! -# remove the administrative token before accepting work -rm $AZP_TOKEN_FILE - print_header "4. Running Azure Pipelines agent..." -# `exec` the node runtime so it's aware of TERM and INT signals -# AgentService.js understands how to handle agent self-update and restart -exec ./externals/node/bin/node ./bin/AgentService.js interactive \ No newline at end of file +trap 'cleanup; exit 130' SIGINT +trap 'cleanup; exit 143' SIGTERM +trap 'cleanup; exit 144' SIGQUIT + +# To be aware of TERM and INT signals call run.sh +# Running it with the --once flag at the end will shut down the agent after the build is executed +./run.sh $* & +wait $! + +cleanup \ No newline at end of file diff --git a/docker/windows/start.ps1 b/docker/windows/start.ps1 index c9cda43..8615af7 100644 --- a/docker/windows/start.ps1 +++ b/docker/windows/start.ps1 @@ -54,9 +54,6 @@ if (-not (Test-Path Env:AZP_URL)) { --work "$(if (Test-Path Env:AZP_WORK) { ${Env:AZP_WORK} } else { '_work' })" ` --replace - # remove the administrative token before accepting work - Remove-Item $Env:AZP_TOKEN_FILE - Write-Host "4. Running Azure Pipelines agent..." -ForegroundColor Cyan .\run.cmd diff --git a/test/azure_devops_agent_aci_test.go b/test/azure_devops_agent_aci_test.go index ba7e08a..c5d1aae 100644 --- a/test/azure_devops_agent_aci_test.go +++ b/test/azure_devops_agent_aci_test.go @@ -198,6 +198,12 @@ func TestDeployAzureDevOpsLinuxAgentsInVirtualNetwork(t *testing.T) { t.Fatalf("Cannot create Azure DevOps agent pool for the test: %v", err) } + // At the end of the test, clean up any resources that were created + defer test_structure.RunTestStage(t, "teardown", func() { + terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder) + terraform.Destroy(t, terraformOptions) + }) + // Deploy the example test_structure.RunTestStage(t, "setup", func() { terraformOptions := configureTerraformOptions(t, fixtureFolder) @@ -226,12 +232,6 @@ func TestDeployAzureDevOpsLinuxAgentsInVirtualNetwork(t *testing.T) { t.Fatalf("Test failed. Expected number of agents is %d. Actual number of agents is %d", expectedAgentsCount, actualAgentsCount) } }) - - // At the end of the test, clean up any resources that were created - test_structure.RunTestStage(t, "teardown", func() { - terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder) - terraform.Destroy(t, terraformOptions) - }) } // This function tests the deployment of Azure DevOps Linux and Windows agents @@ -272,6 +272,12 @@ func TestDeployAzureDevOpsLinuxAndWindowsAgents(t *testing.T) { t.Fatalf("Cannot create Azure DevOps Linux agent pool for the test: %v", err) } + // At the end of the test, clean up any resources that were created + defer test_structure.RunTestStage(t, "teardown", func() { + terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder) + terraform.Destroy(t, terraformOptions) + }) + // Deploy the example test_structure.RunTestStage(t, "setup", func() { terraformOptions := configureTerraformOptions(t, fixtureFolder) @@ -312,12 +318,6 @@ func TestDeployAzureDevOpsLinuxAndWindowsAgents(t *testing.T) { t.Fatalf("Test failed. Expected number of Windows agents is %d. Actual number of Windows agents is %d", expectedWindowsAgentsCount, actualWindowsAgentsCount) } }) - - // At the end of the test, clean up any resources that were created - test_structure.RunTestStage(t, "teardown", func() { - terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder) - terraform.Destroy(t, terraformOptions) - }) } // This function tests the deployment of Azure DevOps Linux agents into an existing resource group @@ -347,6 +347,12 @@ func TestDeployAzureDevOpsLinuxAgentsIntoExistingResourceGroup(t *testing.T) { t.Fatalf("Cannot create Azure DevOps agent pool for the test: %v", err) } + // At the end of the test, clean up any resources that were created + defer test_structure.RunTestStage(t, "teardown", func() { + terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder) + terraform.Destroy(t, terraformOptions) + }) + // Deploy the example test_structure.RunTestStage(t, "setup", func() { terraformOptions := configureTerraformOptions(t, fixtureFolder) @@ -375,11 +381,102 @@ func TestDeployAzureDevOpsLinuxAgentsIntoExistingResourceGroup(t *testing.T) { t.Fatalf("Test failed. Expected number of agents is %d. Actual number of agents is %d", expectedAgentsCount, actualAgentsCount) } }) +} + +// This function tests the deployment of Azure DevOps Linux +func TestAgentCleanUp(t *testing.T) { + t.Parallel() + + fixtureFolder := "./fixture/agent-pool-cleanup" + + // generate a random suffix for the test + rand.Seed(time.Now().UnixNano()) + randomInt := rand.Intn(9999) + randomSuffix := strconv.Itoa(randomInt) + + // create random agent pool name + testPoolName := fmt.Sprintf("e2e-agents-%s", randomSuffix) + os.Setenv("TF_VAR_linux_azure_devops_pool_name", testPoolName) + os.Setenv("TF_VAR_windows_azure_devops_pool_name", testPoolName) + + devopsOrganizationName := os.Getenv("TF_VAR_azure_devops_org_name") + devopsPersonalAccessToken := os.Getenv("TF_VAR_azure_devops_personal_access_token") + devopsOrganizationURL := fmt.Sprintf("https://dev.azure.com/%s", devopsOrganizationName) + + // create the Linux agents pool + defer deleteAzureDevOpsAgentTestPool(testPoolName, devopsOrganizationURL, devopsPersonalAccessToken) + err := createAzureDevOpsAgentTestPool(testPoolName, devopsOrganizationURL, devopsPersonalAccessToken) + if err != nil { + t.Fatalf("Cannot create Azure DevOps Linux agent pool for the test: %v", err) + } // At the end of the test, clean up any resources that were created - test_structure.RunTestStage(t, "teardown", func() { + defer test_structure.RunTestStage(t, "teardown", func() { + terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder) + terraform.Destroy(t, terraformOptions) + }) + + // Deploy the example + test_structure.RunTestStage(t, "setup", func() { + terraformOptions := configureTerraformOptions(t, fixtureFolder) + + // Save the options so later test stages can use them + test_structure.SaveTerraformOptions(t, fixtureFolder, terraformOptions) + + // This will init and apply the resources and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + }) + + // Check whether the length of output meets the requirement + test_structure.RunTestStage(t, "validate", func() { + // add wait time for ACI to get connectivity + time.Sleep(45 * time.Second) + + // ensure deployment was successful for agents + expectedAgentsCount := 2 + actualAgentsCount, err := getAgentsCount(testPoolName, devopsOrganizationURL, devopsPersonalAccessToken) + + if err != nil { + t.Fatalf("Cannot retrieve the number of agents that were deployed: %v", err) + } + + if expectedAgentsCount != actualAgentsCount { + t.Fatalf("Test failed. Expected number of agents is %d. Actual number of agents is %d", expectedAgentsCount, actualAgentsCount) + } + + // Update agent count terraformOptions := test_structure.LoadTerraformOptions(t, fixtureFolder) + terraformOptions.Vars["agents_count"] = "1" + terraform.Apply(t, terraformOptions) + // add wait time for ACI to update + time.Sleep(45 * time.Second) + + expectedAgentsCount = 1 + actualAgentsCount, err = getAgentsCount(testPoolName, devopsOrganizationURL, devopsPersonalAccessToken) + + if err != nil { + t.Fatalf("Cannot retrieve the number of agents that were deployed: %v", err) + } + + if expectedAgentsCount != actualAgentsCount { + t.Fatalf("Test failed. Expected number of agents is %d. Actual number of agents is %d", expectedAgentsCount, actualAgentsCount) + } + + // destroying infrastructure: + terraformOptions = test_structure.LoadTerraformOptions(t, fixtureFolder) terraform.Destroy(t, terraformOptions) + + // after clean up, new expected count = 0 + expectedAgentsCount = 0 + actualAgentsCount, err = getAgentsCount(testPoolName, devopsOrganizationURL, devopsPersonalAccessToken) + + if err != nil { + t.Fatalf("Cannot retrieve the number of agents that were deployed: %v", err) + } + + if expectedAgentsCount != actualAgentsCount { + t.Fatalf("Test failed. Expected number of agents is %d. Actual number of agents is %d", expectedAgentsCount, actualAgentsCount) + } }) } diff --git a/test/fixture/agent-pool-cleanup/build.sh b/test/fixture/agent-pool-cleanup/build.sh new file mode 100755 index 0000000..2705386 --- /dev/null +++ b/test/fixture/agent-pool-cleanup/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +cd ../../../docker + +az login --service-principal --username "$ARM_CLIENT_ID" --password "$ARM_CLIENT_SECRET" --tenant "microsoft.onmicrosoft.com" + +while true; +do + echo "checking for acr..." + sleep 1 + created=$(az acr check-name -n $acr_name --query [nameAvailable] --output tsv) + [[ $created == 'false' ]] && break +done + +# add wait time for role propagation +sleep 60 + +az configure --defaults acr=$acr_name +az acr build -t "aci-devops-agent:0.2-linux" linux >> acr.txt \ No newline at end of file diff --git a/test/fixture/agent-pool-cleanup/main.tf b/test/fixture/agent-pool-cleanup/main.tf new file mode 100644 index 0000000..5f54147 --- /dev/null +++ b/test/fixture/agent-pool-cleanup/main.tf @@ -0,0 +1,73 @@ +data "azurerm_client_config" "current" {} + +resource "random_string" "suffix" { + length = 4 + special = false + upper = false + keepers = { + id = data.azurerm_client_config.current.object_id + } +} + +resource "azurerm_resource_group" "rg" { + name = "rg-terraform-azure-devops-agents-e2e-tests-${random_string.suffix.result}" + location = var.location +} + +resource "azurerm_container_registry" "acr" { + name = "acr${random_string.suffix.result}" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + sku = "Basic" + admin_enabled = true +} + +resource "azurerm_role_assignment" "acr_push" { + scope = azurerm_container_registry.acr.id + role_definition_name = "AcrPush" + principal_id = data.azurerm_client_config.current.object_id +} + +resource "null_resource" "build" { + provisioner "local-exec" { + command = "./build.sh" + + environment = { + acr_name = azurerm_container_registry.acr.name + } + working_dir = "${path.module}/" + } + + depends_on = [azurerm_role_assignment.acr_push] +} + + +module "aci-devops-agent" { + source = "../../../" + enable_vnet_integration = false + create_resource_group = false + + linux_agents_configuration = { + agent_name_prefix = "linux-agent-${random_string.suffix.result}" + count = var.agents_count, + docker_image = "${azurerm_container_registry.acr.login_server}/${var.linux_agent_docker_image}" + docker_tag = var.linux_agent_docker_tag + agent_pool_name = var.linux_azure_devops_pool_name + cpu = 1 + memory = 4 + user_assigned_identity_ids = [] + use_system_assigned_identity = false + } + + image_registry_credential = { + username = azurerm_container_registry.acr.admin_username + password = azurerm_container_registry.acr.admin_password + server = azurerm_container_registry.acr.login_server + } + + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + azure_devops_org_name = var.azure_devops_org_name + azure_devops_personal_access_token = var.azure_devops_personal_access_token + depends_on = [null_resource.build] +} diff --git a/test/fixture/agent-pool-cleanup/provider.tf b/test/fixture/agent-pool-cleanup/provider.tf new file mode 100644 index 0000000..49f3ab5 --- /dev/null +++ b/test/fixture/agent-pool-cleanup/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 2.0" + } + } +} + +provider "azurerm" { + features {} +} \ No newline at end of file diff --git a/test/fixture/agent-pool-cleanup/variables.tf b/test/fixture/agent-pool-cleanup/variables.tf new file mode 100644 index 0000000..f68ae15 --- /dev/null +++ b/test/fixture/agent-pool-cleanup/variables.tf @@ -0,0 +1,45 @@ +variable "azure_devops_org_name" { + type = string + description = "The name of the Azure DevOps organization in which the containerized agents will be deployed (e.g. https://dev.azure.com/YOUR_ORGANIZATION_NAME, must exist)" +} + +variable "azure_devops_personal_access_token" { + type = string + description = "The personal access token to use to connect to Azure DevOps (see https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/v2-windows?view=azure-devops#permissions)" +} + +variable "linux_azure_devops_pool_name" { + type = string + description = "The name of the Azure DevOps agent pool in which the linux containerized agents will be deployed (must exist)" + default = "linux-e2e-agents" +} + +variable "windows_azure_devops_pool_name" { + type = string + description = "The name of the Azure DevOps agent pool in which the windows containerized agents will be deployed (must exist)" + default = "windows-e2e-agents" +} + +variable "location" { + type = string + description = "The Azure location to use" + default = "westeurope" +} + +variable "linux_agent_docker_image" { + type = string + description = "The Docker image to use for the Linux agent" + default = "aci-devops-agent" +} + +variable "linux_agent_docker_tag" { + type = string + description = "The Docker tag to use for the Linux agent" + default = "0.2-linux" +} + +variable "agents_count" { + type = number + description = "The number of agents to create" + default = 2 +}