Octopus.Script exported 2023-09-11 by mcasperson belongs to ‘Octopus’ category.
Serialize an Octopus project as a Terraform module and upload the resulting package to the Octopus built in feed.
This step uses naming conventions to exclude resources from the generated module:
- Variables starting with
Private.
are excluded - Runbooks starting with
__
are excluded - The environment called
Sync
is removed from any variable scopes
Because serializing Terraform modules is done via the API, the values of any secret variables are not available, and are not included in the module generated by this step.
However, by following a variable naming and scoping convention, it is possible to export and then apply a project in a Terraform module recreating secret variables, without ever including the secrets in the exported module.
The project to be exported must define all secret variables with a unique name and a single value. For example, the secret variable Test.Database.Password
can be scoped to the Test
environment and the secret variable Production.Database.Password
can be scoped to the Production
environment. You can not have a single secret variable called Database.Password
with two values for the different environments though.
To collapse the unique secret variables into a single variable used by steps, it is possible to create a non-secret variable called Database.Password
with two values #{Test.Database.Password}
and #{Production.Database.Password}
scoped to appropriate environments.
In this way steps can still reference a single variable called Database.Password
, but all secret variables have unique names and only one value.
All secret variables are then scoped to an additional environment called Sync
, which means all secret variables are exposed to runbooks run in the Step
environment. The Sync
environment is used to apply the Terraform module exported by this step, Apply a Terraform template
step to perform variable replacements with secret variables.
The secret values in the Terraform module then have default values set to the Octostache template referencing the secret variable. For example, the Octopus variables in the Terraform module have default values like #{Test.Database.Password}
and #{Production.Database.Password}
. These templates are replaced at runtime by the Apply a Terraform template
step, run in the Sync
environment, effectively injecting the secret values back into the newly created project.
This allows secret variables to be recreated with their original values, without ever exporting the secret values.
Parameters
When steps based on the template are included in a project’s deployment process, the parameters below can be set.
Ignore All Changes
SerializeProject.Exported.Project.IgnoreAllChanges = False
Selecting this option creates a Terraform module with the “lifecycle.ignore_changes” option set to “all” for all project resources. This allows the resources to be created if they do not exist, but won’t update them if the module is reapplied. This value effectively enables the “Ignore Variable Changes” option.
Ignore Variable Changes
SerializeProject.Exported.Project.IgnoreVariableChanges = True
Selecting this option creates a Terraform module with the “lifecycle.ignore_changes” option set to “all” for Octopus variables (i.e. “octopusdeploy_variable” resources)”. This allows Octopus variables to be created if they do not exist, but won’t update Octopus variable values if the module is reapplied. This value effectively enabled if the “Ignore All Changes” option is enabled.
Ignore CaC Settings
SerializeProject.Exported.Project.IgnoreCacValues = False
Enable this option to exclude any Config-as-Code managed resources from the exported module, such as non-secret variables, deployment process, and CaC defined project settings. This option is useful when you are exporting CaC enabled projects and do not wish to include any settings in the exported module that are managed by Git. Disable this option, and enable the “Exclude CaC Settings” option to essentially convert CaC projects to regular projects.
Exclude CaC Settings
SerializeProject.Exported.Project.ExcludeCacProjectValues = False
Enable this option to exclude Config-as-Code settings from the exported module, such as Git credentials and the version controlled flag. Enable this option, and disable the “Ignore CaC Settings” option to essentially convert CaC projects to regular projects.
Terraform Backend
SerializeProject.ThisInstance.Terraform.Backend = s3
The backed to define in the Terraform module.
Octopus Server URL
SerializeProject.ThisInstance.Server.Url = #{Octopus.Web.ServerUri}
The URL of the Octopus Server hosting the project to be serialized.
Octopus API Key
SerializeProject.ThisInstance.Api.Key =
The Octopus API Key
Octopus Space ID
SerializeProject.Exported.Space.Id = #{Octopus.Space.Id}
The Space ID containing the project to be exported
Octopus Project Name
SerializeProject.Exported.Project.Name = #{Octopus.Project.Name}
The name of the project to serialize.
Octopus Upload Space ID
SerializeProject.Octopus.UploadSpace.Id =
The ID of the space to upload the Terraform package to. Leave this blank to upload to the space defined in the Octopus Space ID
parameter.
Ignored Accounts
SerializeProject.Exported.Project.IgnoredAccounts =
A comma separated list of accounts that will not be included in the Terraform module. These accounts are often those used by Runbooks that are not included in the module, and so do not need to be referenced.
Ignored Library Variables Sets
Exported.Project.IgnoredLibraryVariableSet =
A comma separated list of library variables sets that will not be included in the Terraform module. These library variable sets are often those used by Runbooks that are not included in the module, and so do not need to be referenced.
Include Step Templates
SerializeProject.Exported.Project.IncludeStepTemplates = False
Enable this option to export step templates referenced by a project. Disable this option to have step templates detached in projects instead.
Link Tenants and Create Tenant Variables
SerializeProject.Exported.Project.LookupProjectLinkTenants = False
Enable this option to have each project link any tenants and create project tenant variables.
Script body
Steps based on this template will execute the following Python script.
import argparse
import os
import stat
import re
import socket
import subprocess
import sys
from datetime import datetime
from urllib.parse import urlparse
import urllib.request
from itertools import chain
import platform
from urllib.request import urlretrieve
import zipfile
import json
import tarfile
import random, time
# If this script is not being run as part of an Octopus step, return variables from environment variables.
# Periods are replaced with underscores, and the variable name is converted to uppercase
if "get_octopusvariable" not in globals():
def get_octopusvariable(variable):
return os.environ[re.sub('\\.', '_', variable.upper())]
# If this script is not being run as part of an Octopus step, print directly to std out.
if "printverbose" not in globals():
def printverbose(msg):
print(msg)
def printverbose_noansi(output):
"""
Strip ANSI color codes and print the output as verbose
:param output: The output to print
"""
if not output:
return
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
output_no_ansi = re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', output)
printverbose(output_no_ansi)
def get_octopusvariable_quiet(variable):
"""
Gets an octopus variable, or an empty string if it does not exist.
:param variable: The variable name
:return: The variable value, or an empty string if the variable does not exist
"""
try:
return get_octopusvariable(variable)
except:
return ''
def retry_with_backoff(fn, retries=5, backoff_in_seconds=1):
x = 0
while True:
try:
return fn()
except Exception as e:
print(e)
if x == retries:
raise
sleep = (backoff_in_seconds * 2 ** x +
random.uniform(0, 1))
time.sleep(sleep)
x += 1
def execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi):
"""
The execute method provides the ability to execute external processes while capturing and returning the
output to std err and std out and exit code.
"""
process = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=cwd,
env=env)
stdout, stderr = process.communicate()
retcode = process.returncode
if print_args is not None:
print_output(' '.join(args))
if print_output is not None:
print_output(stdout)
print_output(stderr)
return stdout, stderr, retcode
def is_windows():
return platform.system() == 'Windows'
def init_argparse():
parser = argparse.ArgumentParser(
usage='%(prog)s [OPTION] [FILE]...',
description='Serialize an Octopus project to a Terraform module'
)
parser.add_argument('--ignore-all-changes',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.IgnoreAllChanges') or get_octopusvariable_quiet(
'Exported.Project.IgnoreAllChanges') or 'false',
help='Set to true to set the "lifecycle.ignore_changes" ' +
'setting on each exported resource to "all"')
parser.add_argument('--ignore-variable-changes',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.IgnoreVariableChanges') or get_octopusvariable_quiet(
'Exported.Project.IgnoreVariableChanges') or 'false',
help='Set to true to set the "lifecycle.ignore_changes" ' +
'setting on each exported octopus variable to "all"')
parser.add_argument('--terraform-backend',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.ThisInstance.Terraform.Backend') or get_octopusvariable_quiet(
'ThisInstance.Terraform.Backend') or 'pg',
help='Set this to the name of the Terraform backend to be included in the generated module.')
parser.add_argument('--server-url',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.ThisInstance.Server.Url') or get_octopusvariable_quiet(
'ThisInstance.Server.Url'),
help='Sets the server URL that holds the project to be serialized.')
parser.add_argument('--api-key',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.ThisInstance.Api.Key') or get_octopusvariable_quiet(
'ThisInstance.Api.Key'),
help='Sets the Octopus API key.')
parser.add_argument('--space-id',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Space.Id') or get_octopusvariable_quiet(
'Exported.Space.Id') or get_octopusvariable_quiet('Octopus.Space.Id'),
help='Set this to the space ID containing the project to be serialized.')
parser.add_argument('--project-name',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.Name') or get_octopusvariable_quiet(
'Exported.Project.Name') or get_octopusvariable_quiet(
'Octopus.Project.Name'),
help='Set this to the name of the project to be serialized.')
parser.add_argument('--upload-space-id',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Octopus.UploadSpace.Id') or get_octopusvariable_quiet(
'Octopus.UploadSpace.Id') or get_octopusvariable_quiet('Octopus.Space.Id'),
help='Set this to the space ID of the Octopus space where ' +
'the resulting package will be uploaded to.')
parser.add_argument('--ignore-cac-managed-values',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.IgnoreCacValues') or get_octopusvariable_quiet(
'Exported.Project.IgnoreCacValues') or 'false',
help='Set this to true to exclude cac managed values like non-secret variables, ' +
'deployment processes, and project versioning into the Terraform module. ' +
'Set to false to have these values embedded into the module.')
parser.add_argument('--exclude-cac-project-settings',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.ExcludeCacProjectValues') or get_octopusvariable_quiet(
'Exported.Project.ExcludeCacProjectValues') or 'false',
help='Set this to true to exclude CaC settings like git connections from the exported module.')
parser.add_argument('--ignored-library-variable-sets',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.IgnoredLibraryVariableSet') or get_octopusvariable_quiet(
'Exported.Project.IgnoredLibraryVariableSet'),
help='A comma separated list of library variable sets to ignore.')
parser.add_argument('--ignored-accounts',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.IgnoredAccounts') or get_octopusvariable_quiet(
'Exported.Project.IgnoredAccounts'),
help='A comma separated list of accounts to ignore.')
parser.add_argument('--include-step-templates',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.IncludeStepTemplates') or get_octopusvariable_quiet(
'Exported.Project.IncludeStepTemplates') or 'false',
help='Set this to true to include step templates in the exported module. ' +
'This disables the default behaviour of detaching step templates.')
parser.add_argument('--lookup-project-link-tenants',
action='store',
default=get_octopusvariable_quiet(
'SerializeProject.Exported.Project.LookupProjectLinkTenants') or get_octopusvariable_quiet(
'Exported.Project.LookupProjectLinkTenants') or 'false',
help='Set this option to link tenants and create tenant project variables.')
return parser.parse_known_args()
def get_latest_github_release(owner, repo, filename):
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
releases = urllib.request.urlopen(url).read()
contents = json.loads(releases)
download = [asset for asset in contents.get('assets') if asset.get('name') == filename]
if len(download) != 0:
return download[0].get('browser_download_url')
return None
def ensure_octo_cli_exists():
if is_windows():
print("Checking for the Octopus CLI")
try:
stdout, _, exit_code = execute(['octo.exe', 'help'])
printverbose(stdout)
if not exit_code == 0:
raise "Octo CLI not found"
return ""
except:
print("Downloading the Octopus CLI")
urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.win-x64.zip',
'OctopusTools.zip')
with zipfile.ZipFile('OctopusTools.zip', 'r') as zip_ref:
zip_ref.extractall(os.getcwd())
return os.getcwd()
else:
print("Checking for the Octopus CLI for Linux")
try:
stdout, _, exit_code = execute(['octo', 'help'])
printverbose(stdout)
if not exit_code == 0:
raise "Octo CLI not found"
return ""
except:
print("Downloading the Octopus CLI for Linux")
urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.linux-x64.tar.gz',
'OctopusTools.tar.gz')
with tarfile.open('OctopusTools.tar.gz') as file:
file.extractall(os.getcwd())
os.chmod(os.path.join(os.getcwd(), 'octo'), stat.S_IRWXO | stat.S_IRWXU | stat.S_IRWXG)
return os.getcwd()
def ensure_octoterra_exists():
if is_windows():
print("Checking for the Octoterra tool for Windows")
try:
stdout, _, exit_code = execute(['octoterra.exe', '-version'])
printverbose(stdout)
if not exit_code == 0:
raise "Octoterra not found"
return ""
except:
print("Downloading Octoterra CLI for Windows")
retry_with_backoff(lambda: urlretrieve(
"https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport/releases/latest/download/octoterra_windows_amd64.exe",
'octoterra.exe'), 10, 30)
return os.getcwd()
else:
print("Checking for the Octoterra tool for Linux")
try:
stdout, _, exit_code = execute(['octoterra', '-version'])
printverbose(stdout)
if not exit_code == 0:
raise "Octoterra not found"
return ""
except:
print("Downloading Octoterra CLI for Linux")
retry_with_backoff(lambda: urlretrieve(
"https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport/releases/latest/download/octoterra_linux_amd64",
'octoterra'), 10, 30)
os.chmod(os.path.join(os.getcwd(), 'octoterra'), stat.S_IRWXO | stat.S_IRWXU | stat.S_IRWXG)
return os.getcwd()
octocli_path = ensure_octo_cli_exists()
octoterra_path = ensure_octoterra_exists()
parser, _ = init_argparse()
# Variable precondition checks
if len(parser.server_url) == 0:
print("--server-url, ThisInstance.Server.Url, or SerializeProject.ThisInstance.Server.Url must be defined")
sys.exit(1)
if len(parser.api_key) == 0:
print("--api-key, ThisInstance.Api.Key, or ThisInstance.Api.Key must be defined")
sys.exit(1)
print("Octopus URL: " + parser.server_url)
print("Octopus Space ID: " + parser.space_id)
# Build the arguments to ignore library variable sets
ignores_library_variable_sets = parser.ignored_library_variable_sets.split(',')
ignores_library_variable_sets_args = [['-excludeLibraryVariableSet', x] for x in ignores_library_variable_sets]
# Build the arguments to ignore accounts
ignored_accounts = parser.ignored_accounts.split(',')
ignored_accounts = [['-excludeAccounts', x] for x in ignored_accounts]
os.mkdir(os.getcwd() + '/export')
export_args = [os.path.join(octoterra_path, 'octoterra'),
# the url of the instance
'-url', parser.server_url,
# the api key used to access the instance
'-apiKey', parser.api_key,
# add a postgres backend to the generated modules
'-terraformBackend', parser.terraform_backend,
# dump the generated HCL to the console
'-console',
# dump the project from the current space
'-space', parser.space_id,
# the name of the project to serialize
'-projectName', parser.project_name,
# ignoreProjectChanges can be set to ignore all changes to the project, variables, runbooks etc
'-ignoreProjectChanges=' + parser.ignore_all_changes,
# use data sources to lookup external dependencies (like environments, accounts etc) rather
# than serialize those external resources
'-lookupProjectDependencies',
# for any secret variables, add a default value set to the octostache value of the variable
# e.g. a secret variable called "database" has a default value of "#{database}"
'-defaultSecretVariableValues',
# Any value that can't be replaced with an Octostache template, add a dummy value
'-dummySecretVariableValues',
# detach any step templates, allowing the exported project to be used in a new space
'-detachProjectTemplates=' + str(not parser.include_step_templates),
# allow the downstream project to move between project groups
'-ignoreProjectGroupChanges',
# allow the downstream project to change names
'-ignoreProjectNameChanges',
# CaC enabled projects will not export the deployment process, non-secret variables, and other
# CaC managed project settings if ignoreCacManagedValues is true. It is usually desirable to
# set this value to true, but it is false here because CaC projects created by Terraform today
# save some variables in the database rather than writing them to the Git repo.
'-ignoreCacManagedValues=' + parser.ignore_cac_managed_values,
# Excluding CaC values means the resulting module does not include things like git credentials.
# Setting excludeCaCProjectSettings to true and ignoreCacManagedValues to false essentially
# converts a CaC project back to a database project.
'-excludeCaCProjectSettings=' + parser.exclude_cac_project_settings,
# This value is always true. Either this is an unmanaged project, in which case we are never
# reapplying it; or it is a variable configured project, in which case we need to ignore
# variable changes, or it is a shared CaC project, in which case we don't use Terraform to
# manage variables.
'-ignoreProjectVariableChanges=' + parser.ignore_variable_changes,
# To have secret variables available when applying a downstream project, they must be scoped
# to the Sync environment. But we do not need this scoping in the downstream project, so the
# Sync environment is removed from any variable scopes when serializing it to Terraform.
'-excludeVariableEnvironmentScopes', 'Sync',
# Exclude any variables starting with "Private."
'-excludeProjectVariableRegex', 'Private\\..*',
# Capture the octopus endpoint, space ID, and space name as output vars. This is useful when
# querying th Terraform state file to know which space and instance the resources were
# created in. The scripts used to update downstream projects in bulk work by querying the
# Terraform state, finding all the downstream projects, and using the space name to only process
# resources that match the current tenant (because space names and tenant names are the same).
# The output variables added by this option are octopus_server, octopus_space_id, and
# octopus_space_name.
'-includeOctopusOutputVars',
# Where steps do not explicitly define a worker pool and reference the default one, this
# option explicitly exports the default worker pool by name. This means if two spaces have
# different default pools, the exported project still uses the pool that the original project
# used.
'-lookUpDefaultWorkerPools',
# Link any tenants that were originally link to the project and create project tenant variables
'-lookupProjectLinkTenants=' + parser.lookup_project_link_tenants,
# Add support for experimental step templates
'-experimentalEnableStepTemplates=' + parser.include_step_templates,
# The directory where the exported files will be saved
'-dest', os.getcwd() + '/export',
# This is a management runbook that we do not wish to export
'-excludeRunbookRegex', '__ .*'] + list(chain(*ignores_library_variable_sets_args)) + list(
chain(*ignored_accounts))
print("Exporting Terraform module")
_, _, octoterra_exit = execute(export_args)
if not octoterra_exit == 0:
print("Octoterra failed. Please check the logs for more information.")
sys.exit(1)
date = datetime.now().strftime('%Y.%m.%d.%H%M%S')
print("Creating Terraform module package")
if is_windows():
execute([os.path.join(octocli_path, 'octo.exe'),
'pack',
'--format', 'zip',
'--id', re.sub('[^0-9a-zA-Z]', '_', parser.project_name),
'--version', date,
'--basePath', os.getcwd() + '\\export',
'--outFolder', os.getcwd()])
else:
_, _, _ = execute([os.path.join(octocli_path, 'octo'),
'pack',
'--format', 'zip',
'--id', re.sub('[^0-9a-zA-Z]', '_', parser.project_name),
'--version', date,
'--basePath', os.getcwd() + '/export',
'--outFolder', os.getcwd()])
print("Uploading Terraform module package")
if is_windows():
_, _, _ = execute([os.path.join(octocli_path, 'octo.exe'),
'push',
'--apiKey', parser.api_key,
'--server', parser.server_url,
'--space', parser.upload_space_id,
'--package', os.getcwd() + "\\" +
re.sub('[^0-9a-zA-Z]', '_', parser.project_name) + '.' + date + '.zip',
'--replace-existing'])
else:
_, _, _ = execute([os.path.join(octocli_path, 'octo'),
'push',
'--apiKey', parser.api_key,
'--server', parser.server_url,
'--space', parser.upload_space_id,
'--package', os.getcwd() + "/" +
re.sub('[^0-9a-zA-Z]', '_', parser.project_name) + '.' + date + '.zip',
'--replace-existing'])
print("##octopus[stdout-default]")
print("Done")
Provided under the Apache License version 2.0.
To use this template in Octopus Deploy, copy the JSON below and paste it into the Library → Step templates → Import dialog.
{
"Id": "e9526501-09d5-490f-ac3f-5079735fe041",
"Name": "Octopus - Serialize Project to Terraform",
"Description": "Serialize an Octopus project as a Terraform module and upload the resulting package to the Octopus built in feed.\n\nThis step uses naming conventions to exclude resources from the generated module:\n\n* Variables starting with `Private.` are excluded\n* Runbooks starting with `__ ` are excluded\n* The environment called `Sync` is removed from any variable scopes\n\nBecause serializing Terraform modules is done via the API, the values of any secret variables are not available, and are not included in the module generated by this step.\n\nHowever, by following a variable naming and scoping convention, it is possible to export and then apply a project in a Terraform module recreating secret variables, without ever including the secrets in the exported module.\n\nThe project to be exported must define all secret variables with a unique name and a single value. For example, the secret variable `Test.Database.Password` can be scoped to the `Test` environment and the secret variable `Production.Database.Password` can be scoped to the `Production` environment. You can not have a single secret variable called `Database.Password` with two values for the different environments though.\n\nTo collapse the unique secret variables into a single variable used by steps, it is possible to create a non-secret variable called `Database.Password` with two values `#{Test.Database.Password}` and `#{Production.Database.Password}` scoped to appropriate environments.\n\nIn this way steps can still reference a single variable called `Database.Password`, but all secret variables have unique names and only one value.\n\nAll secret variables are then scoped to an additional environment called `Sync`, which means all secret variables are exposed to runbooks run in the `Step` environment. The `Sync` environment is used to apply the Terraform module exported by this step, `Apply a Terraform template` step to perform variable replacements with secret variables.\n\nThe secret values in the Terraform module then have default values set to the Octostache template referencing the secret variable. For example, the Octopus variables in the Terraform module have default values like `#{Test.Database.Password}` and `#{Production.Database.Password}`. These templates are replaced at runtime by the `Apply a Terraform template` step, run in the `Sync` environment, effectively injecting the secret values back into the newly created project.\n\nThis allows secret variables to be recreated with their original values, without ever exporting the secret values. ",
"Version": 10,
"ExportedAt": "2023-09-11T21:06:55.101Z",
"ActionType": "Octopus.Script",
"Author": "mcasperson",
"Packages": [],
"Parameters": [
{
"Id": "ca62a702-6eb3-4d01-b645-73fbb2a1ea86",
"Name": "SerializeProject.Exported.Project.IgnoreAllChanges",
"Label": "Ignore All Changes",
"HelpText": "Selecting this option creates a Terraform module with the \"lifecycle.ignore_changes\" option set to \"all\" for all project resources. This allows the resources to be created if they do not exist, but won't update them if the module is reapplied. This value effectively enables the \"Ignore Variable Changes\" option.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
}
},
{
"Id": "05fafe0b-b05f-4e7b-85c3-857b62dc4182",
"Name": "SerializeProject.Exported.Project.IgnoreVariableChanges",
"Label": "Ignore Variable Changes",
"HelpText": "Selecting this option creates a Terraform module with the \"lifecycle.ignore_changes\" option set to \"all\" for Octopus variables (i.e. \"octopusdeploy_variable\" resources)\". This allows Octopus variables to be created if they do not exist, but won't update Octopus variable values if the module is reapplied. This value effectively enabled if the \"Ignore All Changes\" option is enabled.",
"DefaultValue": "True",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
}
},
{
"Id": "3b8f35b6-fc1a-442b-ae0e-3036f5436a7a",
"Name": "SerializeProject.Exported.Project.IgnoreCacValues",
"Label": "Ignore CaC Settings",
"HelpText": "Enable this option to exclude any Config-as-Code managed resources from the exported module, such as non-secret variables, deployment process, and CaC defined project settings. This option is useful when you are exporting CaC enabled projects and do not wish to include any settings in the exported module that are managed by Git. Disable this option, and enable the \"Exclude CaC Settings\" option to essentially convert CaC projects to regular projects.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
}
},
{
"Id": "5c315650-9ba8-48b8-a02c-269315277fea",
"Name": "SerializeProject.Exported.Project.ExcludeCacProjectValues",
"Label": "Exclude CaC Settings",
"HelpText": "Enable this option to exclude Config-as-Code settings from the exported module, such as Git credentials and the version controlled flag. Enable this option, and disable the \"Ignore CaC Settings\" option to essentially convert CaC projects to regular projects.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
}
},
{
"Id": "9c18e779-bddc-4f74-81c6-9d75babc9c9c",
"Name": "SerializeProject.ThisInstance.Terraform.Backend",
"Label": "Terraform Backend",
"HelpText": "The [backed](https://developer.hashicorp.com/terraform/language/settings/backends/configuration) to define in the Terraform module.",
"DefaultValue": "s3",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "f96cd929-1c18-4f7c-9121-82904e64834e",
"Name": "SerializeProject.ThisInstance.Server.Url",
"Label": "Octopus Server URL",
"HelpText": "The URL of the Octopus Server hosting the project to be serialized.",
"DefaultValue": "#{Octopus.Web.ServerUri}",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "95e0f611-d1f2-4317-ad5e-9131de73bbbe",
"Name": "SerializeProject.ThisInstance.Api.Key",
"Label": "Octopus API Key",
"HelpText": "The Octopus API Key",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "Sensitive"
}
},
{
"Id": "9473de1f-a633-41d0-ba2c-6b622ce65551",
"Name": "SerializeProject.Exported.Space.Id",
"Label": "Octopus Space ID",
"HelpText": "The Space ID containing the project to be exported",
"DefaultValue": "#{Octopus.Space.Id}",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "9d8af8e2-307a-4149-a28e-f75cec9ee044",
"Name": "SerializeProject.Exported.Project.Name",
"Label": "Octopus Project Name",
"HelpText": "The name of the project to serialize.",
"DefaultValue": "#{Octopus.Project.Name}",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "1b8ce71f-0931-4966-805a-e6d0ec12e3a0",
"Name": "SerializeProject.Octopus.UploadSpace.Id",
"Label": "Octopus Upload Space ID",
"HelpText": "The ID of the space to upload the Terraform package to. Leave this blank to upload to the space defined in the `Octopus Space ID` parameter.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "aec82033-cae1-4a18-a315-c70468f71539",
"Name": "SerializeProject.Exported.Project.IgnoredAccounts",
"Label": "Ignored Accounts",
"HelpText": "A comma separated list of accounts that will not be included in the Terraform module. These accounts are often those used by Runbooks that are not included in the module, and so do not need to be referenced.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "e45abab5-cb8f-4af2-b3e9-3cde057907df",
"Name": "Exported.Project.IgnoredLibraryVariableSet",
"Label": "Ignored Library Variables Sets",
"HelpText": "A comma separated list of library variables sets that will not be included in the Terraform module. These library variable sets are often those used by Runbooks that are not included in the module, and so do not need to be referenced.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
},
{
"Id": "e456bc3f-a537-4982-8963-a091d3f31cf0",
"Name": "SerializeProject.Exported.Project.IncludeStepTemplates",
"Label": "Include Step Templates",
"HelpText": "Enable this option to export step templates referenced by a project. Disable this option to have step templates detached in projects instead.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
}
},
{
"Id": "d9dacd25-5b0f-4f3e-89e2-08eefc0ffb89",
"Name": "SerializeProject.Exported.Project.LookupProjectLinkTenants",
"Label": "Link Tenants and Create Tenant Variables",
"HelpText": "Enable this option to have each project link any tenants and create project tenant variables.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
}
}
],
"Properties": {
"Octopus.Action.RunOnServer": "true",
"Octopus.Action.Script.ScriptBody": "import argparse\nimport os\nimport stat\nimport re\nimport socket\nimport subprocess\nimport sys\nfrom datetime import datetime\nfrom urllib.parse import urlparse\nimport urllib.request\nfrom itertools import chain\nimport platform\nfrom urllib.request import urlretrieve\nimport zipfile\nimport json\nimport tarfile\nimport random, time\n\n# If this script is not being run as part of an Octopus step, return variables from environment variables.\n# Periods are replaced with underscores, and the variable name is converted to uppercase\nif \"get_octopusvariable\" not in globals():\n def get_octopusvariable(variable):\n return os.environ[re.sub('\\\\.', '_', variable.upper())]\n\n# If this script is not being run as part of an Octopus step, print directly to std out.\nif \"printverbose\" not in globals():\n def printverbose(msg):\n print(msg)\n\n\ndef printverbose_noansi(output):\n \"\"\"\n Strip ANSI color codes and print the output as verbose\n :param output: The output to print\n \"\"\"\n if not output:\n return\n\n # https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python\n output_no_ansi = re.sub(r'\\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])', '', output)\n printverbose(output_no_ansi)\n\n\ndef get_octopusvariable_quiet(variable):\n \"\"\"\n Gets an octopus variable, or an empty string if it does not exist.\n :param variable: The variable name\n :return: The variable value, or an empty string if the variable does not exist\n \"\"\"\n try:\n return get_octopusvariable(variable)\n except:\n return ''\n\n\ndef retry_with_backoff(fn, retries=5, backoff_in_seconds=1):\n x = 0\n while True:\n try:\n return fn()\n except Exception as e:\n\n print(e)\n\n if x == retries:\n raise\n\n sleep = (backoff_in_seconds * 2 ** x +\n random.uniform(0, 1))\n time.sleep(sleep)\n x += 1\n\n\ndef execute(args, cwd=None, env=None, print_args=None, print_output=printverbose_noansi):\n \"\"\"\n The execute method provides the ability to execute external processes while capturing and returning the\n output to std err and std out and exit code.\n \"\"\"\n process = subprocess.Popen(args,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True,\n cwd=cwd,\n env=env)\n stdout, stderr = process.communicate()\n retcode = process.returncode\n\n if print_args is not None:\n print_output(' '.join(args))\n\n if print_output is not None:\n print_output(stdout)\n print_output(stderr)\n\n return stdout, stderr, retcode\n\n\ndef is_windows():\n return platform.system() == 'Windows'\n\n\ndef init_argparse():\n parser = argparse.ArgumentParser(\n usage='%(prog)s [OPTION] [FILE]...',\n description='Serialize an Octopus project to a Terraform module'\n )\n parser.add_argument('--ignore-all-changes',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.IgnoreAllChanges') or get_octopusvariable_quiet(\n 'Exported.Project.IgnoreAllChanges') or 'false',\n help='Set to true to set the \"lifecycle.ignore_changes\" ' +\n 'setting on each exported resource to \"all\"')\n parser.add_argument('--ignore-variable-changes',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.IgnoreVariableChanges') or get_octopusvariable_quiet(\n 'Exported.Project.IgnoreVariableChanges') or 'false',\n help='Set to true to set the \"lifecycle.ignore_changes\" ' +\n 'setting on each exported octopus variable to \"all\"')\n parser.add_argument('--terraform-backend',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.ThisInstance.Terraform.Backend') or get_octopusvariable_quiet(\n 'ThisInstance.Terraform.Backend') or 'pg',\n help='Set this to the name of the Terraform backend to be included in the generated module.')\n parser.add_argument('--server-url',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.ThisInstance.Server.Url') or get_octopusvariable_quiet(\n 'ThisInstance.Server.Url'),\n help='Sets the server URL that holds the project to be serialized.')\n parser.add_argument('--api-key',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.ThisInstance.Api.Key') or get_octopusvariable_quiet(\n 'ThisInstance.Api.Key'),\n help='Sets the Octopus API key.')\n parser.add_argument('--space-id',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Space.Id') or get_octopusvariable_quiet(\n 'Exported.Space.Id') or get_octopusvariable_quiet('Octopus.Space.Id'),\n help='Set this to the space ID containing the project to be serialized.')\n parser.add_argument('--project-name',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.Name') or get_octopusvariable_quiet(\n 'Exported.Project.Name') or get_octopusvariable_quiet(\n 'Octopus.Project.Name'),\n help='Set this to the name of the project to be serialized.')\n parser.add_argument('--upload-space-id',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Octopus.UploadSpace.Id') or get_octopusvariable_quiet(\n 'Octopus.UploadSpace.Id') or get_octopusvariable_quiet('Octopus.Space.Id'),\n help='Set this to the space ID of the Octopus space where ' +\n 'the resulting package will be uploaded to.')\n parser.add_argument('--ignore-cac-managed-values',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.IgnoreCacValues') or get_octopusvariable_quiet(\n 'Exported.Project.IgnoreCacValues') or 'false',\n help='Set this to true to exclude cac managed values like non-secret variables, ' +\n 'deployment processes, and project versioning into the Terraform module. ' +\n 'Set to false to have these values embedded into the module.')\n parser.add_argument('--exclude-cac-project-settings',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.ExcludeCacProjectValues') or get_octopusvariable_quiet(\n 'Exported.Project.ExcludeCacProjectValues') or 'false',\n help='Set this to true to exclude CaC settings like git connections from the exported module.')\n parser.add_argument('--ignored-library-variable-sets',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.IgnoredLibraryVariableSet') or get_octopusvariable_quiet(\n 'Exported.Project.IgnoredLibraryVariableSet'),\n help='A comma separated list of library variable sets to ignore.')\n parser.add_argument('--ignored-accounts',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.IgnoredAccounts') or get_octopusvariable_quiet(\n 'Exported.Project.IgnoredAccounts'),\n help='A comma separated list of accounts to ignore.')\n parser.add_argument('--include-step-templates',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.IncludeStepTemplates') or get_octopusvariable_quiet(\n 'Exported.Project.IncludeStepTemplates') or 'false',\n help='Set this to true to include step templates in the exported module. ' +\n 'This disables the default behaviour of detaching step templates.')\n parser.add_argument('--lookup-project-link-tenants',\n action='store',\n default=get_octopusvariable_quiet(\n 'SerializeProject.Exported.Project.LookupProjectLinkTenants') or get_octopusvariable_quiet(\n 'Exported.Project.LookupProjectLinkTenants') or 'false',\n help='Set this option to link tenants and create tenant project variables.')\n\n return parser.parse_known_args()\n\n\ndef get_latest_github_release(owner, repo, filename):\n url = f\"https://api.github.com/repos/{owner}/{repo}/releases/latest\"\n releases = urllib.request.urlopen(url).read()\n contents = json.loads(releases)\n\n download = [asset for asset in contents.get('assets') if asset.get('name') == filename]\n\n if len(download) != 0:\n return download[0].get('browser_download_url')\n\n return None\n\n\ndef ensure_octo_cli_exists():\n if is_windows():\n print(\"Checking for the Octopus CLI\")\n try:\n stdout, _, exit_code = execute(['octo.exe', 'help'])\n printverbose(stdout)\n if not exit_code == 0:\n raise \"Octo CLI not found\"\n return \"\"\n except:\n print(\"Downloading the Octopus CLI\")\n urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.win-x64.zip',\n 'OctopusTools.zip')\n with zipfile.ZipFile('OctopusTools.zip', 'r') as zip_ref:\n zip_ref.extractall(os.getcwd())\n return os.getcwd()\n else:\n print(\"Checking for the Octopus CLI for Linux\")\n try:\n stdout, _, exit_code = execute(['octo', 'help'])\n printverbose(stdout)\n if not exit_code == 0:\n raise \"Octo CLI not found\"\n return \"\"\n except:\n print(\"Downloading the Octopus CLI for Linux\")\n urlretrieve('https://download.octopusdeploy.com/octopus-tools/9.0.0/OctopusTools.9.0.0.linux-x64.tar.gz',\n 'OctopusTools.tar.gz')\n with tarfile.open('OctopusTools.tar.gz') as file:\n file.extractall(os.getcwd())\n os.chmod(os.path.join(os.getcwd(), 'octo'), stat.S_IRWXO | stat.S_IRWXU | stat.S_IRWXG)\n return os.getcwd()\n\n\ndef ensure_octoterra_exists():\n if is_windows():\n print(\"Checking for the Octoterra tool for Windows\")\n try:\n stdout, _, exit_code = execute(['octoterra.exe', '-version'])\n printverbose(stdout)\n if not exit_code == 0:\n raise \"Octoterra not found\"\n return \"\"\n except:\n print(\"Downloading Octoterra CLI for Windows\")\n retry_with_backoff(lambda: urlretrieve(\n \"https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport/releases/latest/download/octoterra_windows_amd64.exe\",\n 'octoterra.exe'), 10, 30)\n return os.getcwd()\n else:\n print(\"Checking for the Octoterra tool for Linux\")\n try:\n stdout, _, exit_code = execute(['octoterra', '-version'])\n printverbose(stdout)\n if not exit_code == 0:\n raise \"Octoterra not found\"\n return \"\"\n except:\n print(\"Downloading Octoterra CLI for Linux\")\n retry_with_backoff(lambda: urlretrieve(\n \"https://github.com/OctopusSolutionsEngineering/OctopusTerraformExport/releases/latest/download/octoterra_linux_amd64\",\n 'octoterra'), 10, 30)\n os.chmod(os.path.join(os.getcwd(), 'octoterra'), stat.S_IRWXO | stat.S_IRWXU | stat.S_IRWXG)\n return os.getcwd()\n\n\noctocli_path = ensure_octo_cli_exists()\noctoterra_path = ensure_octoterra_exists()\nparser, _ = init_argparse()\n\n# Variable precondition checks\nif len(parser.server_url) == 0:\n print(\"--server-url, ThisInstance.Server.Url, or SerializeProject.ThisInstance.Server.Url must be defined\")\n sys.exit(1)\n\nif len(parser.api_key) == 0:\n print(\"--api-key, ThisInstance.Api.Key, or ThisInstance.Api.Key must be defined\")\n sys.exit(1)\n\nprint(\"Octopus URL: \" + parser.server_url)\nprint(\"Octopus Space ID: \" + parser.space_id)\n\n# Build the arguments to ignore library variable sets\nignores_library_variable_sets = parser.ignored_library_variable_sets.split(',')\nignores_library_variable_sets_args = [['-excludeLibraryVariableSet', x] for x in ignores_library_variable_sets]\n\n# Build the arguments to ignore accounts\nignored_accounts = parser.ignored_accounts.split(',')\nignored_accounts = [['-excludeAccounts', x] for x in ignored_accounts]\n\nos.mkdir(os.getcwd() + '/export')\n\nexport_args = [os.path.join(octoterra_path, 'octoterra'),\n # the url of the instance\n '-url', parser.server_url,\n # the api key used to access the instance\n '-apiKey', parser.api_key,\n # add a postgres backend to the generated modules\n '-terraformBackend', parser.terraform_backend,\n # dump the generated HCL to the console\n '-console',\n # dump the project from the current space\n '-space', parser.space_id,\n # the name of the project to serialize\n '-projectName', parser.project_name,\n # ignoreProjectChanges can be set to ignore all changes to the project, variables, runbooks etc\n '-ignoreProjectChanges=' + parser.ignore_all_changes,\n # use data sources to lookup external dependencies (like environments, accounts etc) rather\n # than serialize those external resources\n '-lookupProjectDependencies',\n # for any secret variables, add a default value set to the octostache value of the variable\n # e.g. a secret variable called \"database\" has a default value of \"#{database}\"\n '-defaultSecretVariableValues',\n # Any value that can't be replaced with an Octostache template, add a dummy value\n '-dummySecretVariableValues',\n # detach any step templates, allowing the exported project to be used in a new space\n '-detachProjectTemplates=' + str(not parser.include_step_templates),\n # allow the downstream project to move between project groups\n '-ignoreProjectGroupChanges',\n # allow the downstream project to change names\n '-ignoreProjectNameChanges',\n # CaC enabled projects will not export the deployment process, non-secret variables, and other\n # CaC managed project settings if ignoreCacManagedValues is true. It is usually desirable to\n # set this value to true, but it is false here because CaC projects created by Terraform today\n # save some variables in the database rather than writing them to the Git repo.\n '-ignoreCacManagedValues=' + parser.ignore_cac_managed_values,\n # Excluding CaC values means the resulting module does not include things like git credentials.\n # Setting excludeCaCProjectSettings to true and ignoreCacManagedValues to false essentially\n # converts a CaC project back to a database project.\n '-excludeCaCProjectSettings=' + parser.exclude_cac_project_settings,\n # This value is always true. Either this is an unmanaged project, in which case we are never\n # reapplying it; or it is a variable configured project, in which case we need to ignore\n # variable changes, or it is a shared CaC project, in which case we don't use Terraform to\n # manage variables.\n '-ignoreProjectVariableChanges=' + parser.ignore_variable_changes,\n # To have secret variables available when applying a downstream project, they must be scoped\n # to the Sync environment. But we do not need this scoping in the downstream project, so the\n # Sync environment is removed from any variable scopes when serializing it to Terraform.\n '-excludeVariableEnvironmentScopes', 'Sync',\n # Exclude any variables starting with \"Private.\"\n '-excludeProjectVariableRegex', 'Private\\\\..*',\n # Capture the octopus endpoint, space ID, and space name as output vars. This is useful when\n # querying th Terraform state file to know which space and instance the resources were\n # created in. The scripts used to update downstream projects in bulk work by querying the\n # Terraform state, finding all the downstream projects, and using the space name to only process\n # resources that match the current tenant (because space names and tenant names are the same).\n # The output variables added by this option are octopus_server, octopus_space_id, and\n # octopus_space_name.\n '-includeOctopusOutputVars',\n # Where steps do not explicitly define a worker pool and reference the default one, this\n # option explicitly exports the default worker pool by name. This means if two spaces have\n # different default pools, the exported project still uses the pool that the original project\n # used.\n '-lookUpDefaultWorkerPools',\n # Link any tenants that were originally link to the project and create project tenant variables\n '-lookupProjectLinkTenants=' + parser.lookup_project_link_tenants,\n # Add support for experimental step templates\n '-experimentalEnableStepTemplates=' + parser.include_step_templates,\n # The directory where the exported files will be saved\n '-dest', os.getcwd() + '/export',\n # This is a management runbook that we do not wish to export\n '-excludeRunbookRegex', '__ .*'] + list(chain(*ignores_library_variable_sets_args)) + list(\n chain(*ignored_accounts))\n\nprint(\"Exporting Terraform module\")\n_, _, octoterra_exit = execute(export_args)\n\nif not octoterra_exit == 0:\n print(\"Octoterra failed. Please check the logs for more information.\")\n sys.exit(1)\n\ndate = datetime.now().strftime('%Y.%m.%d.%H%M%S')\n\nprint(\"Creating Terraform module package\")\nif is_windows():\n execute([os.path.join(octocli_path, 'octo.exe'),\n 'pack',\n '--format', 'zip',\n '--id', re.sub('[^0-9a-zA-Z]', '_', parser.project_name),\n '--version', date,\n '--basePath', os.getcwd() + '\\\\export',\n '--outFolder', os.getcwd()])\nelse:\n _, _, _ = execute([os.path.join(octocli_path, 'octo'),\n 'pack',\n '--format', 'zip',\n '--id', re.sub('[^0-9a-zA-Z]', '_', parser.project_name),\n '--version', date,\n '--basePath', os.getcwd() + '/export',\n '--outFolder', os.getcwd()])\n\nprint(\"Uploading Terraform module package\")\nif is_windows():\n _, _, _ = execute([os.path.join(octocli_path, 'octo.exe'),\n 'push',\n '--apiKey', parser.api_key,\n '--server', parser.server_url,\n '--space', parser.upload_space_id,\n '--package', os.getcwd() + \"\\\\\" +\n re.sub('[^0-9a-zA-Z]', '_', parser.project_name) + '.' + date + '.zip',\n '--replace-existing'])\nelse:\n _, _, _ = execute([os.path.join(octocli_path, 'octo'),\n 'push',\n '--apiKey', parser.api_key,\n '--server', parser.server_url,\n '--space', parser.upload_space_id,\n '--package', os.getcwd() + \"/\" +\n re.sub('[^0-9a-zA-Z]', '_', parser.project_name) + '.' + date + '.zip',\n '--replace-existing'])\n\nprint(\"##octopus[stdout-default]\")\n\nprint(\"Done\")\n",
"Octopus.Action.Script.ScriptSource": "Inline",
"Octopus.Action.Script.Syntax": "Python"
},
"Category": "Octopus",
"HistoryUrl": "https://github.com/OctopusDeploy/Library/commits/master/step-templates//opt/buildagent/work/75443764cd38076d/step-templates/octopus-serialize-project-to-terraform.json",
"Website": "/step-templates/e9526501-09d5-490f-ac3f-5079735fe041",
"Logo": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAC1QTFRFT6Tl////L5Pg8vj9Y67omsvwPJrisdfzfbzs5fL7y+T32Ov5isLucLXqvt31CJPHWwAABMJJREFUeNrs3deW4jAMAFDF3U75/89dlp0ZhiU4blJEjvQ8hYubLJsA00UCBCIQgQhEIAIRiEAEIhCBCEQgAhGIQAQiEIEIhD8kJm+t+QprfdKfB9HbYpx6CWfspj8HMi+gMgHL/AmQA8W3JTKH+ALFvzCeL0RbpyoCPE9IJeNOSQwh5Z3qd6yRGWQ2qi2cZQWxqj1WzQYSjeoJmJlAklOd4VlArOqPhQEkqBERToeMcfRJBkC0Uep8CfBpjz4JsHJ0zF3dkEWNje0kiB/sUC6eApndaIiCMyAa1PiwJ0AWhRGJHJJQHG2dC7h1rNbO1QOxSA7lNCkkKrQIpJCAB1GREILYIC1NAiwbpKFJgGWDNExcwGstfExcZBCHC6nOglshHtmhViLIig1RNBCN7qjtW8C0Z1UvJcC1Z9XmwMBzzvobmgAyEzgq91dtEEsBsQSQQAFZCSBAATEEEApHZbrVBIkkEIUPSVeB+KtALA0kXQUSrwKZBCIQBnk8Y4i5CsReBeKvkqLM+BCSDWJlrZFvGk9SRTHshkgjZCGAaArIxm3H3grhVzFlW2msfl1ca79UJ1bofYvsDHHlNdTZnlh5MghuPd5NdBDUNZHyCkfktIh03XzALGRPlBDPac7qgWjHZzWcmF5zmmkhidMQ6boKiDXcDTUEaylZqCGJ0Vjvu/fLJtHqhSANEvqb2OYqkOUqEHuVMbJcZdZCGiPhKhC4yjqiIjEE7XThMp8fAWII3mY3kUIQD+AMKQTzPiBhgQ63HlT/KSvgtoi0dq5mCPah1UIE0eh3sT0NhOByvKeAkFzi8PgQomumFhsyOxpIzZN4gLOj5plVwNpR0b2AuePWKBEHQu24pSsJA+LVCeHHQxZ1SiyDIdqok8IOhSSnTottHEQTdyt4ettAj4KkzA4dMikk2Dht2S5ptm1vswnPDxn0YyDZ5oDM3iToo2T5voWaYe+Q+vdjH80QyAzZhCgcDtLMI1Tmtz9w++XHgziHQHJJu/OZ3bs9Xn8gQ72NcP3dKqEfkp10F51xhoIi2I91R+LurXV/5q7pH+wx061CzO16oSQleMyr8fXvwMA0Pro8432DPD/ySx8XrHfSuDAM8n6UhnjQabaiXf5Bq/lREHvEeNtn1rJ08+C/uXkQZHeguxAPC3UvtcJYUogLzZX5hhZZvS6onG5lxXtzWGaygwb79vT/IXhdlNibwlKYOR6T8xjI7W8n+xV7T+GH4tMzWwR+lZhRkJYSsC0thpmCYqyngOz3rN2FLBZ2wZflBCggUHF0Vnp88JKienzIXLSEZCZqU7IKr/gQW9yx3pzV7Y9kvWZWTRRIqDmTtRUnU7b2lLcTYmoqHqnmiO1poER0SPkAeZMAZxaJx0Y3TCdAclsIqDz03ALcyxfTCZBsthoGXWmigGyVhWPLFJJfuuKQWycoEFdXbH4dJJoJxNR1eD/kshz6yn48cF8yW8sFoitflB1w6Q8n+/15Za7oA17/pYNmYgP5fmWm8L1NOHPWgK8kuFew1/JXtOA0yJCv7ah7X8ObUuT5kObU30+fDZm8+zqP+HTIpK0xQ796b5Kv2hSIQAQiEIEIRCACEYhABCIQgQhEIAIRiEAEIpBf8UeAAQAEjtYmlDTcCgAAAABJRU5ErkJggg==",
"$Meta": {
"Type": "ActionTemplate"
}
}
Page updated on Monday, September 11, 2023