This script demonstrates how to programmatically assign a group of tenants to a project in Octopus.
Usage
Provide values for:
- Octopus URL
- Octopus API Key
- Name of the space to use
- Name of the project to add
- A list of Environments to tie the project to
- The tenant tag you wish to filter on
- What if is true or false
- Max number of tenants to change
Note: In order for this script to execute correctly, please note the following:
- The projects provided must have the Multi-tenanted deployment setting enabled.
- The environments provided must exist.
- The optional tenant tags provided must exist.
- The script will make sure the environments specified are assigned to that tenant. Even if the tenant / project relationship already exists.
This script has guardrails in place to ensure you don’t make too many changes.
- The first is what if. Set that to
$true
and it will skip the save step. It will print out all the changes it would’ve made to the tenant. - The second is the max number of tenants to change. The search will still return the full list of tenants, however, it will only change, at most the number of tenants specified. For example, if you specified 10 tenants, and it found 25 to change, it would only change those 10. The next run will change the next 10, and then finally the last 5.
Script
PowerShell (REST API)
$octopusUrl = "https://your-octopus-url"
$octopusApiKey = "API-YOUR-KEY"
$spaceName = "YOUR SPACE NAME"
$projectName = "PROJECT NAME TO ADD"
$environmentNameList = "ENVIRONMENTS TO TIE TO" # "Development,Test"
$tenantTag = "TENANT TAG TO FILTER ON" #Format = [Tenant Tag Set Name]/[Tenant Tag] "Tenant Type/Customer"
$whatIf = $false # Set to true to test out changes before making them
$maxNumberOfTenants = 1 # The max number of tenants you wish to change in this run
$cachedResults = @{}
function Write-OctopusVerbose
{
param($message)
Write-Verbose $message
}
function Write-OctopusInformation
{
param($message)
Write-Host $message
}
function Write-OctopusSuccess
{
param($message)
Write-Host $message
}
function Write-OctopusWarning
{
param($message)
Write-Warning "$message"
}
function Write-OctopusCritical
{
param ($message)
Write-Error "$message"
}
function Invoke-OctopusApi
{
param
(
$octopusUrl,
$endPoint,
$spaceId,
$apiKey,
$method,
$item,
$ignoreCache
)
$octopusUrlToUse = $OctopusUrl
if ($OctopusUrl.EndsWith("/"))
{
$octopusUrlToUse = $OctopusUrl.Substring(0, $OctopusUrl.Length - 1)
}
if ([string]::IsNullOrWhiteSpace($SpaceId))
{
$url = "$octopusUrlToUse/api/$EndPoint"
}
else
{
$url = "$octopusUrlToUse/api/$spaceId/$EndPoint"
}
try
{
if ($null -ne $item)
{
$body = $item | ConvertTo-Json -Depth 10
Write-OctopusVerbose $body
Write-OctopusInformation "Invoking $method $url"
return Invoke-RestMethod -Method $method -Uri $url -Headers @{"X-Octopus-ApiKey" = "$ApiKey" } -Body $body -ContentType 'application/json; charset=utf-8'
}
if (($null -eq $ignoreCache -or $ignoreCache -eq $false) -and $method.ToUpper().Trim() -eq "GET")
{
Write-OctopusVerbose "Checking to see if $url is already in the cache"
if ($cachedResults.ContainsKey($url) -eq $true)
{
Write-OctopusVerbose "$url is already in the cache, returning the result"
return $cachedResults[$url]
}
}
else
{
Write-OctopusVerbose "Ignoring cache."
}
Write-OctopusVerbose "No data to post or put, calling bog standard Invoke-RestMethod for $url"
$result = Invoke-RestMethod -Method $method -Uri $url -Headers @{"X-Octopus-ApiKey" = "$ApiKey" } -ContentType 'application/json; charset=utf-8'
if ($cachedResults.ContainsKey($url) -eq $true)
{
$cachedResults.Remove($url)
}
Write-OctopusVerbose "Adding $url to the cache"
$cachedResults.add($url, $result)
return $result
}
catch
{
if ($null -ne $_.Exception.Response)
{
if ($_.Exception.Response.StatusCode -eq 401)
{
Write-OctopusCritical "Unauthorized error returned from $url, please verify API key and try again"
}
elseif ($_.Exception.Response.statusCode -eq 403)
{
Write-OctopusCritical "Forbidden error returned from $url, please verify API key and try again"
}
else
{
Write-OctopusVerbose -Message "Error calling $url $($_.Exception.Message) StatusCode: $($_.Exception.Response.StatusCode )"
}
}
else
{
Write-OctopusVerbose $_.Exception
}
}
Throw "There was an error calling the Octopus API please check the log for more details"
}
function Get-OctopusItemByName
{
param(
$itemName,
$itemType,
$endpoint,
$spaceId,
$defaultUrl,
$octopusApiKey
)
Write-OctopusInformation "Attempting to find $itemType with the name of $itemName"
$itemList = Invoke-OctopusApi -octopusUrl $defaultUrl -endPoint "$($endPoint)?partialName=$([uri]::EscapeDataString($itemName))&skip=0&take=100" -spaceId $spaceId -apiKey $octopusApiKey -method "GET"
$item = Get-FilteredOctopusItem -itemList $itemList -itemName $itemName
Write-OctopusInformation "Successfully found $itemName with id of $($item.Id)"
return $item
}
function Get-FilteredOctopusItem
{
param(
$itemList,
$itemName
)
if ($itemList.Items.Count -eq 0)
{
Write-OctopusCritical "Unable to find $itemName. Exiting with an exit code of 1."
Exit 1
}
$item = $itemList.Items | Where-Object { $_.Name -eq $itemName}
if ($null -eq $item)
{
Write-OctopusCritical "Unable to find $itemName. Exiting with an exit code of 1."
exit 1
}
return $item
}
function Test-OctopusObjectHasProperty
{
param(
$objectToTest,
$propertyName
)
$hasProperty = Get-Member -InputObject $objectToTest -Name $propertyName -MemberType Properties
if ($hasProperty)
{
Write-OctopusVerbose "$propertyName property found."
return $true
}
else
{
Write-OctopusVerbose "$propertyName property missing."
return $false
}
}
function Add-PropertyIfMissing
{
param(
$objectToTest,
$propertyName,
$propertyValue,
$overwriteIfExists)
if ((Test-OctopusObjectHasProperty -objectToTest $objectToTest -propertyName $propertyName) -eq $false)
{
$objectToTest | Add-Member -MemberType NoteProperty -Name $propertyName -Value $propertyValue
}
}
#https://local.octopusdemos.app/api/Spaces-102/tenants/tag-test?tags=Tenant%20Type%2FCustomer
$space = Get-OctopusItemByName -itemName $spaceName -itemType "Space" -endpoint "spaces" -spaceId $null -defaultUrl $octopusUrl -octopusApiKey $octopusApiKey
$spaceId = $space.Id
$project = Get-OctopusItemByName -itemName $projectName -itemType "Project" -endpoint "projects" -spaceId $spaceId -defaultUrl $octopusUrl -octopusApiKey $octopusApiKey
$projectId = $project.Id
$splitEnvironmentList = $environmentNameList -split ","
$environmentList = @()
foreach ($environmentName in $splitEnvironmentList)
{
$environment = Get-OctopusItemByName -itemName $environmentName -itemType "Environment" -endpoint "environments" -spaceId $spaceId -defaultUrl $octopusUrl -octopusApiKey $octopusApiKey
$environmentList += $environment.Id
}
$tenantList = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint "tenants?tags=$([uri]::EscapeDataString($tenantTag))&skip=0&take=10000" -spaceId $spaceId -method "GET" -item $null -ignoreCache $false
Write-OctopusInformation "Found $($tenantList.Items.Count) tenants matching the tenant tag $tenantTag"
$changeReport = @()
$itemsChanged = 1
foreach ($tenant in $tenantList.Items)
{
Write-OctopusInformation "Checking to see if $($tenant.Name) is assigned to $($project.Name)"
$tenantChanged = $false
if ((Test-OctopusObjectHasProperty -objectToTest $tenant.ProjectEnvironments -propertyName $projectId) -eq $false)
{
Write-OctopusInformation "The project $($project.Name) is not assigned to $($project.Name), adding it"
$changeReport += "Added $($project.Name) to $($tenant.Name) with environment ids $environmentList"
Add-PropertyIfMissing -objectToTest $tenant.ProjectEnvironments -propertyName $projectId -propertyValue $environmentList
$tenantChanged = $true
}
else
{
Write-OctopusInformation "Project $($project.Name) is assigned to the $($tenant.Name), let's make sure it has the environments as well"
foreach ($environmentId in $environmentList)
{
if ($tenant.ProjectEnvironments.$projectId -notcontains $environmentId)
{
$changeReport += "Added $environmentId to $($project.Name) association for $($tenant.Name)"
Write-OctopusInformation "Environment $environmentId is not assigned to $($project.Name) for $($tenant.Name), adding it"
$tenant.ProjectEnvironments.$projectId += $environmentId
$tenantChanged = $true
}
}
}
if ($tenantChanged -eq $false)
{
continue
}
if ($whatIf -eq $false)
{
Invoke-OctopusApi -endPoint "tenants/$($tenant.Id)" -spaceId $spaceId -apiKey $octopusApiKey -method "PUT" -item $tenant -ignoreCache $true -octopusUrl $octopusUrl
}
else
{
Write-OctopusInformation "What if set to true, skipping saving"
}
$itemsChanged += 1
if ($itemsChanged -gt $maxNumberOfTenants)
{
Write-OctopusInformation "Max number of tenants to change has been reached, exiting loop"
break
}
}
Write-OctopusInformation "Change Report:"
foreach ($item in $changeReport)
{
Write-OctopusInformation " $item"
}
PowerShell (Octopus.Client)
# Load assembly
Add-Type -Path 'C:\Octopus.Client\Octopus.Client.dll'
# Declare variables
$octopusUrl = "https://your-octopus-url"
$octopusApiKey = "API-YOUR-KEY"
$spaceName = "YOUR SPACE NAME"
$projectName = "PROJECT NAME TO ADD"
$environmentNameList = "ENVIRONMENTS TO TIE TO" # "Development,Test"
$tenantTag = "TENANT TAG TO FILTER ON" #Format = [Tenant Tag Set Name]/[Tenant Tag] "Tenant Type/Customer"
$whatIf = $false # Set to true to test out changes before making them
$maxNumberOfTenants = 1 # The max number of tenants you wish to change in this run
$tenantsUpdated = 0
# Create client object
$endpoint = New-Object Octopus.Client.OctopusServerEndpoint($octopusURL, $octopusAPIKey)
$repository = New-Object Octopus.Client.OctopusRepository($endpoint)
$client = New-Object Octopus.Client.OctopusClient($endpoint)
$space = $repository.Spaces.FindByName($spaceName)
$client = $client.ForSpace($space)
# Get project
$project = $client.Projects.FindByName($projectName)
# Get reference to environments
$environments = @()
foreach ($environmentName in $environmentNameList)
{
$environment = $client.Environments.FindByName($environmentName)
if ($null -ne $environment)
{
$environments += $environment
}
else
{
Write-Warning "Environment $environmentName not found!"
}
}
# Get tenants by tag
$tenants = $client.Tenants.FindAll("", @($tenantTag), 1000)
# Loop through returned tenants
foreach ($tenant in $tenants)
{
$tenantUpdated = $false
if (($null -eq $tenant.ProjectEnvironments) -or ($tenant.ProjectEnvironments.Count -eq 0))
{
# Add project/environments
$tenant.ConnectToProjectAndEnvironments($project, $environments)
$tenantUpdated = $true
}
else
{
# Get existing project connections
$projectEnvironments = $tenant.ProjectEnvironments | Where-Object {$_.Keys -eq $project.Id}
# Compare environment list
$missingEnvironments = @()
foreach ($environment in $environments)
{
if ($projectEnvironments.ContainsValue($environment.Id) -eq $false)
{
#$missingEnvironments += $environment.Id
$tenant.ProjectEnvironments[$project.Id].Add($environment.Id)
$tenantUpdated = $true
}
}
}
if ($tenantUpdated)
{
if ($whatIf)
{
$tenant
}
else
{
# Update tenant
$client.Tenants.Modify($tenant)
}
$tenantsUpdated ++
}
if ($tenantsUpdated -ge $maxNumberOfTenants)
{
# We out!
break
}
}
C#
#r "path\to\Octopus.Client.dll"
using Octopus.Client;
using Octopus.Client.Model;
using System;
using System.Linq;
// If using .net Core, be sure to add the NuGet package of System.Security.Permissions
var octopusURL = "https://your-octopus-url";
var octopusAPIKey = "API-YOUR-KEY";
var spaceName = "Default";
var projectName = "MyProject";
var environmentNameList = new string[] { "Environment", "List"};
string[] tenantTag = new string[] { "TagSet/Tag" }; // "TENANT TAG TO FILTER ON" Format = [Tenant Tag Set Name]/[Tenant Tag] "Tenant Type/Customer"
bool whatIf = false;
int maxNumberOfTenants = 1;
int tenantsUpdated = 0;
// Create repository object
var endpoint = new OctopusServerEndpoint(octopusURL, octopusAPIKey);
var repository = new OctopusRepository(endpoint);
var client = new OctopusClient(endpoint);
// Get space
var space = repository.Spaces.FindByName(spaceName);
var spaceRepository = client.ForSpace(space);
// Get project
var project = spaceRepository.Projects.FindByName(projectName);
// Get tenants by tag
var tenants = spaceRepository.Tenants.FindAll("", tenantTag, 1000);
// Get environment objects
var environments = new System.Collections.Generic.List<Octopus.Client.Model.EnvironmentResource>();
foreach (string environmentName in environmentNameList)
{
var environment = spaceRepository.Environments.FindByName(environmentName);
if (environment != null)
{
environments.Add(environment);
}
else
{
Console.WriteLine(string.Format("{0} not found!", environmentName));
}
}
// Loop through tenants
foreach (var tenant in tenants)
{
bool tenantUpdated = false;
if(tenant.ProjectEnvironments == null || tenant.ProjectEnvironments.Count == 0)
{
// Add project/environments
tenant.ConnectToProjectAndEnvironments(project, environments.ToArray());
tenantUpdated = true;
}
else
{
// Get project connected environments
System.Collections.Generic.Dictionary<string, ReferenceCollection> projectEnvironments = new System.Collections.Generic.Dictionary<string, Octopus.Client.Model.ReferenceCollection>(tenant.ProjectEnvironments.Where(e => e.Key == project.Id));
// Compare what's connected to list of environments to connect
foreach (var environment in environments)
{
if (!projectEnvironments[project.Id].Contains(environment.Id))
{
tenant.ProjectEnvironments[project.Id].Add(environment.Id);
tenantUpdated = true;
}
}
}
// Check to see if tenant was updated
if (tenantUpdated)
{
if (whatIf)
{
Console.WriteLine(tenant);
}
else
{
// Update tenant
spaceRepository.Tenants.Modify(tenant);
}
// Increment updated counter
tenantsUpdated++;
}
// Check to see if we've reached the max number of updated
if (tenantsUpdated == maxNumberOfTenants)
{
// Get outta here!
break;
}
}
Python3
import json
import requests
from requests.api import get, head
def get_octopus_resource(uri, headers, skip_count = 0):
items = []
skip_querystring = ""
if '?' in uri:
skip_querystring = '&skip='
else:
skip_querystring = '?skip='
response = requests.get((uri + skip_querystring + str(skip_count)), headers=headers)
response.raise_for_status()
# Get results of API call
results = json.loads(response.content.decode('utf-8'))
# Store results
if 'Items' in results.keys():
items += results['Items']
# Check to see if there are more results
if (len(results['Items']) > 0) and (len(results['Items']) == results['ItemsPerPage']):
skip_count += results['ItemsPerPage']
items += get_octopus_resource(uri, headers, skip_count)
else:
return results
# return results
return items
# Define Octopus server variables
octopus_server_uri = 'https://your-octopus-url'
octopus_api_key = 'API-YOUR-KEY'
headers = {'X-Octopus-ApiKey': octopus_api_key}
project_name = 'MyProject'
environment_name_list = ['Environment', 'List']
tenant_tag = 'TENANT TAG TO FILTER ON' #Format = [Tenant Tag Set Name]/[Tenant Tag] "Tenant Type/Customer"
max_number_tenants = 1
tenants_updated = 0
space_name = 'Default'
what_if = False
# Get space
uri = '{0}/spaces'.format(octopus_server_uri)
spaces = get_octopus_resource(uri, headers)
space = next((x for x in spaces if x['Name'] == space_name), None)
# Get project
uri = '{0}/{1}/projects'.format(octopus_server_uri, space['Id'])
projects = get_octopus_resource(uri, headers)
project = next((x for x in projects if x['Name'] == project_name), None)
# Get environments
environments = []
uri = '{0}/{1}/environments'.format(octopus_server_uri, space['Id'])
all_environments = get_octopus_resource(uri, headers)
for environment_name in environment_name_list:
environment = next((x for x in all_environments if x['Name'] == environment_name), None)
environments.append(environment['Id'])
# Get tenants by tag
uri = '{0}/{1}/tenants?tags={2}'.format(octopus_server_uri, space['Id'], tenant_tag)
tenants = get_octopus_resource(uri, headers)
# Loop through tenants
for tenant in tenants:
tenant_updated = False
if tenant['ProjectEnvironments'] == None or len(tenant['ProjectEnvironments']) == 0:
projectEnvironments = {
project['Id']: environments
}
# Attach to tenant
tenant['ProjectEnvironments'] = projectEnvironments
tenant_updated = True
else:
# Get current project environments
projectEnvironments = tenant['ProjectEnvironments']
# Loop through environments
for environment in environments:
#print (projectEnvironments[project['Id']])
if environment not in projectEnvironments[project['Id']]:
projectEnvironments[project['Id']].append(environment)
tenant['ProjectEnvironments'] = projectEnvironments
tenant_updated = True
if tenant_updated:
if what_if:
print(tenant)
else:
uri = '{0}/{1}/tenants/{2}'.format(octopus_server_uri, space['Id'], tenant['Id'])
response = requests.put(uri, headers=headers, json=tenant)
response.raise_for_status
tenants_updated = tenants_updated + 1
if tenants_updated == max_number_tenants:
break
Go
package main
import (
"fmt"
"log"
"net/url"
"github.com/OctopusDeploy/go-octopusdeploy/octopusdeploy"
)
func main() {
apiURL, err := url.Parse("https://your-octopus-url")
if err != nil {
log.Println(err)
}
APIKey := "API-YOUR-KEY"
spaceName := "Default"
projectName := "MyProject"
environmentNameList := []string{"Environment", "List"}
tenantTag := "TENANT TAG TO FILTER ON" //Format = [Tenant Tag Set Name]/[Tenant Tag] "Tenant Type/Customer"
whatIf := false
maxNumberOfTenants := 1
tenantsUpdated := 0
// Get reference to space
space := GetSpace(apiURL, APIKey, spaceName)
// Get project reference
project := GetProject(apiURL, APIKey, space, projectName)
// Get environment ids
environments := []string{}
for i := 0; i < len(environmentNameList); i++ {
environment := GetEnvironment(apiURL, APIKey, space, environmentNameList[i])
if nil != environment {
environments = append(environments, environment.ID)
}
}
// Get tenants
tenants := GetTenantsByTag(apiURL, APIKey, space, tenantTag)
// Loop through tenants
for i := 0; i < len(tenants); i++ {
tenantUpdated := false
if len(tenants[i].ProjectEnvironments) == 0 {
// Add everything
projectEnvironments := make(map[string][]string)
projectEnvironments[project.ID] = environments
tenants[i].ProjectEnvironments = projectEnvironments
tenantUpdated = true
} else {
projectEnvironments := tenants[i].ProjectEnvironments
for e := 0; e < len(environments); e++ {
if !contains(projectEnvironments[project.ID], environments[e]) {
// Add
existingEntries := []string{}
existingEntries = projectEnvironments[project.ID]
existingEntries = append(existingEntries, environments[e])
projectEnvironments[project.ID] = existingEntries
tenantUpdated = true
}
}
}
if tenantUpdated {
if whatIf {
fmt.Println(tenants[i])
} else {
client := octopusAuth(apiURL, APIKey, space.ID)
client.Tenants.Update(tenants[i])
}
tenantsUpdated++
}
if maxNumberOfTenants == tenantsUpdated {
break
}
}
}
func octopusAuth(octopusURL *url.URL, APIKey, space string) *octopusdeploy.Client {
client, err := octopusdeploy.NewClient(nil, octopusURL, APIKey, space)
if err != nil {
log.Println(err)
}
return client
}
func GetSpace(octopusURL *url.URL, APIKey string, spaceName string) *octopusdeploy.Space {
client := octopusAuth(octopusURL, APIKey, "")
spaceQuery := octopusdeploy.SpacesQuery{
Name: spaceName,
}
// Get specific space object
spaces, err := client.Spaces.Get(spaceQuery)
if err != nil {
log.Println(err)
}
for _, space := range spaces.Items {
if space.Name == spaceName {
return space
}
}
return nil
}
func GetProject(octopusURL *url.URL, APIKey string, space *octopusdeploy.Space, projectName string) *octopusdeploy.Project {
// Create client
client := octopusAuth(octopusURL, APIKey, space.ID)
projectsQuery := octopusdeploy.ProjectsQuery {
Name: projectName,
}
// Get specific project object
projects, err := client.Projects.Get(projectsQuery)
if err != nil {
log.Println(err)
}
for _, project := range projects.Items {
if project.Name == projectName {
return project
}
}
return nil
}
func GetEnvironment(octopusURL *url.URL, APIKey string, space *octopusdeploy.Space, environmentName string) *octopusdeploy.Environment {
// Get client for space
client := octopusAuth(octopusURL, APIKey, space.ID)
// Get environment
environmentsQuery := octopusdeploy.EnvironmentsQuery {
Name: environmentName,
}
environments, err := client.Environments.Get(environmentsQuery)
if err != nil {
log.Println(err)
}
// Loop through results
for _, environment := range environments.Items {
if environment.Name == environmentName {
return environment
}
}
return nil
}
func GetTenantsByTag(octopusURL *url.URL, APIKey string, space *octopusdeploy.Space, tagName string) []*octopusdeploy.Tenant {
// Create client
client := octopusAuth(octopusURL, APIKey, space.ID)
tenants, err := client.Tenants.GetAll()
if err != nil {
log.Println(err)
}
tenantsWithTag := []*octopusdeploy.Tenant{}
for i := 0; i < len(tenants); i++ {
if contains(tenants[i].TenantTags, tagName) {
tenantsWithTag = append(tenantsWithTag, tenants[i])
}
}
return tenantsWithTag
}
func contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}
Help us continuously improve
Please let us know if you have any feedback about this page.
Page updated on Sunday, January 1, 2023