diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..764cd8f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "submodules/htmltoadf"]
+ path = submodules/htmltoadf
+ url = https://github.com/wouterken/htmltoadf.git
diff --git a/AtlassianPowerKit-Admin/AtlassianPowerKit-Admin.psd1 b/AtlassianPowerKit-Admin/AtlassianPowerKit-Admin.psd1
new file mode 100644
index 0000000..50e56ea
--- /dev/null
+++ b/AtlassianPowerKit-Admin/AtlassianPowerKit-Admin.psd1
@@ -0,0 +1,144 @@
+#
+# Module manifest for module 'AtlassianPowerKit'
+#
+# Generated by: MarkCulhane
+#
+# Generated on: 16/06/2024
+#
+
+@{
+
+ # Script module or binary module file associated with this manifest.
+ RootModule = 'AtlassianPowerKit-Admin.psm1'
+
+ # Version number of this module.
+ ModuleVersion = '1.0.0'
+
+ # Supported PSEditions
+ # CompatiblePSEditions = @()
+
+ # ID used to uniquely identify this module
+ GUID = 'c93fe103-590b-4a69-b4e1-09fcc140ef99'
+
+ # Author of this module
+ Author = 'Mark Culhane'
+
+ # Company or vendor of this module
+ CompanyName = 'ZOAK Technologies'
+
+ # Copyright statement for this module
+ Copyright = '(c) ZOAK PTY LTD. All rights reserved.'
+
+ # Description of the functionality provided by this module
+ Description = 'Atlassian Cloud PowerShell Module for handy functions to interact with Attlassian Cloud APIs.'
+
+ # Minimum version of the PowerShell engine required by this module
+ PowerShellVersion = '7.3'
+
+ # Name of the PowerShell host required by this module
+ # PowerShellHostName = ''
+
+ # Minimum version of the PowerShell host required by this module
+ # PowerShellHostVersion = ''
+
+ # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+ # DotNetFrameworkVersion = ''
+
+ # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+ # ClrVersion = ''
+
+ # Processor architecture (None, X86, Amd64) required by this module
+ # ProcessorArchitecture = ''
+
+ # Modules that must be imported into the global environment prior to importing this module
+ # RequiredModules = @('..\AtlassianPowerKit-Shared\AtlassianPowerKit-Shared.psd1', '..\AtlassianPowerKit-Jira\AtlassianPowerKit-Jira.psd1')
+
+ # Assemblies that must be loaded prior to importing this module
+ # RequiredAssemblies = @()
+
+ # Script files (.ps1) that are run in the caller's environment prior to importing this module.
+ # ScriptsToProcess = @()
+
+ # Type files (.ps1xml) to be loaded when importing this module
+ # TypesToProcess = @()
+
+ # Format files (.ps1xml) to be loaded when importing this module
+ # FormatsToProcess = @()
+
+ # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+ # NestedModules = @('..\AtlassianPowerKit-Shared\AtlassianPowerKit-Shared.psd1', '..\AtlassianPowerKit-Jira\AtlassianPowerKit-Jira.psd1', '..\AtlassianPowerKit-Confluence\AtlassianPowerKit-Confluence.psd1')
+
+ # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
+ FunctionsToExport = @(
+ 'New-JiraIssueType',
+ 'Import-JiraIssueTypes',
+ 'Test-ExistingConfigJSON',
+ 'Import-JSONConfigExport',
+ 'Get-OSMDeploymentConfigsJIRA',
+ 'Get-OSMConfigAsMarkdown',
+ 'Get-JiraProjectIssueTypes',
+ 'Get-JiraCloudIssueTypeSchema'
+ 'Get-JiraCloudIssueTypeMetadata',
+ 'Get-JiraOSMFilterList',
+ 'Export-ProjectProformaFormTemplates'
+ )
+
+ # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
+ CmdletsToExport = @()
+
+ # Variables to export from this module
+ VariablesToExport = @()
+
+ # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
+ AliasesToExport = @()
+
+ # DSC resources to export from this module
+ # DscResourcesToExport = @()
+
+ # List of all modules packaged with this module
+ # ModuleList = @()
+
+ # List of all files packaged with this module
+ # FileList = @()
+
+ # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+ PrivateData = @{
+
+ PSData = @{
+
+ # Tags applied to this module. These help with module discovery in online galleries.
+ # Tags = @()
+
+ # A URL to the license for this module.
+ # LicenseUri = ''
+
+ # A URL to the main website for this project.
+ # ProjectUri = ''
+
+ # A URL to an icon representing this module.
+ # IconUri = ''
+
+ # ReleaseNotes of this module
+ # ReleaseNotes = ''
+
+ # Prerelease string of this module
+ # Prerelease = ''
+
+ # Flag to indicate whether the module requires explicit user acceptance for install/update/save
+ # RequireLicenseAcceptance = $false
+
+ # External dependent modules of this module
+ # ExternalModuleDependencies = @()
+
+ } # End of PSData hashtable
+
+ } # End of PrivateData hashtable
+
+ # HelpInfo URI of this module
+ # HelpInfoURI = ''
+
+ # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
+ # DefaultCommandPrefix = ''
+
+}
+
diff --git a/AtlassianPowerKit-Admin/AtlassianPowerKit-Admin.psm1 b/AtlassianPowerKit-Admin/AtlassianPowerKit-Admin.psm1
new file mode 100644
index 0000000..739c67b
--- /dev/null
+++ b/AtlassianPowerKit-Admin/AtlassianPowerKit-Admin.psm1
@@ -0,0 +1,455 @@
+<#
+.SYNOPSIS
+ Atlassian Cloud PowerShell Module - AtlassianPowerKit-JIRAGRCosmDeploy - module for creating new issue types, workflows, fields, screens, screen schemes, issue type screen schemes, and issue type screen scheme associations using the Atlassian Jira Cloud REST API.See https://developer.atlassian.com/cloud/jira/platform/rest/v3/ for more information.
+
+.DESCRIPTION
+ Atlassian Cloud PowerShell Module - AtlassianPowerKit-JIRAGRCosmDeploy
+ - Dependencies: AtlassianPowerKit-Shared
+ - New-AtlassianAPIEndpoint
+ For list of functions and cmdlets, run Get-Command -Module AtlassianPowerKit-JIRAGRCosmDeploy.psm1
+
+.EXAMPLE
+ New-JiraIssueType -JiraCloudProjectKey 'OSM' -JiraIssueTypeName 'Test Issue Type' -JiraIssueTypeDescription 'This is a test issue type.' -JiraIssueTypeAvatarId '10000'
+
+.LINK
+GitHub: https://github.com/OrganisationServiceManagement/AtlassianPowerKit.git
+
+#>
+
+$ErrorActionPreference = 'Stop'; $DebugPreference = 'Continue'
+# Directory of this file
+Import-Module "$env:OSM_INSTALL\AtlassianPowerKit\AtlassianPowerKit-Shared\AtlassianPowerKit-Shared.psd1" -Force
+Import-Module "$env:OSM_INSTALL\AtlassianPowerKit\AtlassianPowerKit-Jira\AtlassianPowerKit-Jira.psd1" -Force
+$RETRY_AFTER = 10
+
+# Function to create a new Jira Issue Typess
+function New-JiraIssueType {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$JiraIssueTypeName,
+ [Parameter(Mandatory = $true)]
+ [string]$JiraIssueTypeDescription,
+ [Parameter(Mandatory = $true)]
+ [string]$JiraIssueTypeAvatarId,
+ [Parameter(Mandatory = $true)]
+ [int]$JiraIssueHierarchyLevel,
+ [Parameter(Mandatory = $false)]
+ [string]$ExistingJiraIssueTypeList = $null
+ )
+ # First check if the issue type already exists by name
+ $ExistingJiraIssueType = $null
+ if ( ! $ExistingJiraIssueTypeList ) {
+ Write-Debug 'Getting existing Jira Issue Type list as it was not provided...'
+ $ExistingJiraIssueTypeList = Invoke-RestMethod -Method Get -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issuetype" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders)
+ }
+ Write-Debug "Existing Jira Issue Type Count: $($ExistingJiraIssueTypeList.Count)"
+ $ExistingJiraIssueType = $ExistingJiraIssueTypeList | ConvertFrom-Json | Where-Object { $_.name -eq "$JiraIssueTypeName" }
+ Write-Debug "Existing Jira Issue Type Count [MATCH]: $ExistingJiraIssueType"
+ if ($ExistingJiraIssueType) {
+ Write-Debug "Issue type $($JiraIssueType.name) already exists. Returning existing issue type."
+ # Ensure the ExistingJiraIssueType object is a single object
+ if ($ExistingJiraIssueType.Count -gt 1) {
+ Write-Warn "Multiple issue types found with the name: $JiraIssueTypeName. Returning the first issue type."
+ $ExistingJiraIssueType = $ExistingJiraIssueType[0]
+ }
+ } else {
+ # Create a JSON object for the new issue type using the $JiraIssueType fields: name, description, hierarchyLevel, avatarId (removing the other fields)
+ $NewJiraIssueType = @{
+ name = $JiraIssueTypeName
+ description = $JiraIssueTypeDescription
+ heirarchyLevel = $JiraIssueHierarchyLevel
+ }
+ Write-Debug "Creating issue type $($JiraIssueType.name)...: "
+ $NewJiraIssueType | ConvertTo-Json -Depth 10 | Write-Debug
+ $CreatedJiraIssueType = Invoke-RestMethod -Method Post -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issuetype" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Body $($NewJiraIssueType | ConvertTo-Json -Depth 10) -ContentType 'application/json'
+ Write-Debug "Issue type $($CreatedJiraIssueType.name) created."
+ # Update the Avatar for the new issue type
+ $JiraIssueAvatarUpdateBody = @{
+ avatarId = $JiraIssueTypeAvatarId
+ }
+ $JiraIssueAvatarUpdateBody = $JiraIssueAvatarUpdateBody | ConvertTo-Json -Depth 10
+ $CreatedJiraIssueType = Invoke-RestMethod -Method Put -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issuetype/$($CreatedJiraIssueType.id)" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Body $($NewJiraIssueType | ConvertTo-Json -Depth 10) -ContentType 'application/json'
+ Write-Debug "Issue type $JiraIssueTypeName avatar updated."
+ $ExistingJiraIssueType = $CreatedJiraIssueType
+ }
+ Return $ExistingJiraIssueType | ConvertTo-Json -Depth 100 -Compress
+}
+
+function Set-OrgAdminUser {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$ORG_ADMIN_USER
+ )
+ $ATLASSIAN_ADMIN_API = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/user?username=$ORG_ADMIN_USER"
+
+}
+
+
+# Function to load issue types from a JSON file
+function Import-JiraIssueTypes {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$JiraIssueTypesJSONFile
+ )
+ $ExistingJiraIssueTypes = Invoke-RestMethod -Method Get -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issuetype" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) | ConvertTo-Json -Depth 100 -Compress
+ $ImportIssueList = Get-Content -Path $JiraIssueTypesJSONFile | ConvertFrom-Json -AsHashtable
+ $DeployedIssueTypes = $ImportIssueList | ForEach-Object {
+ $NewIssueType = $_
+ New-JiraIssueType -JiraIssueTypeName $NewIssueType.name -JiraIssueTypeDescription $NewIssueType.description -JiraIssueTypeAvatarId $NewIssueType.avatarId -JiraIssueHierarchyLevel $NewIssueType.heirarchyLevel -ExistingJiraIssueTypeList $ExistingJiraIssueTypes
+ }
+ Return $DeployedIssueTypes | ConvertFrom-Json -AsHashtable -NoEnumerate | ConvertTo-Json -Depth 100 -Compress
+}
+
+# Function to create
+function Test-ExistingConfigJSON {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$CONFIG_FILE_PATHPATTERN
+ )
+ $CONFIG_FILE = Get-ChildItem -Path $CONFIG_FILE_PATHPATTERN | Where-Object { $_.LastWriteTime -gt (Get-Date).AddHours(-12) }
+ if ($CONFIG_FILE) {
+ $CONFIG_FILE
+ } else {
+ $null
+ }
+}
+# Returns JSON path that can be loaded with Get-Content $(Import-JSONConfigExport) | ConvertFrom-Json -AsHashtable -NoEnumerate
+function Import-JSONConfigExport {
+
+ $FULL_CONFIG_OUTPUT_JSONFILE = $null
+ # Advise user of the age of the existing JSON export and ask if they want to use it defaulting to 'Yes'
+ while (! $FULL_CONFIG_OUTPUT_JSONFILE) {
+ $EXISTING_JSON_EXPORT_LIST = Get-ChildItem -Path "$OUTPUT_PATH\FULL-$PROFILE_NAME-*.json" | Sort-Object -Property LastWriteTime -Descending
+ if ($EXISTING_JSON_EXPORT_LIST -and $EXISTING_JSON_EXPORT_LIST.Count -gt 0) {
+ # If LastWriteTime is less than 12 hours ago, use it
+ if ($EXISTING_JSON_EXPORT_LIST[0].LastWriteTime -gt (Get-Date).AddHours(-12)) {
+ $LATEST_EXISTING_JSON_EXPORT = $EXISTING_JSON_EXPORT_LIST[0]
+ Write-Debug "Fresh, existing JSON export: $($LATEST_EXISTING_JSON_EXPORT.FullName)"
+ $FULL_CONFIG_OUTPUT_JSONFILE = $LATEST_EXISTING_JSON_EXPORT.FullName
+ }
+ } else {
+ Write-Debug "$($MyInvocation.MyCommand.Name): Creating new FULL DEPLOYMENT CONFIG json file using: Get-OSMDeploymentConfigsJIRA -PROFILE_NAME $PROFILE_NAME"
+ $RAW_CONFIG_JSON = Get-OSMDeploymentConfigsJIRA -PROFILE_NAME $PROFILE_NAME | ConvertFrom-Json -Depth 100
+ $FULL_CONFIG_OUTPUT_JSONFILE = "$OUTPUT_PATH\FULL-$PROFILE_NAME-$(Get-Date -Format 'yyyyMMdd-HHmm').json"
+ $RAW_CONFIG_JSON | ConvertTo-Json -Depth 100 | Out-File -FilePath $FULL_CONFIG_OUTPUT_JSONFILE -Force | Out-Null
+ Write-Debug "Output written to $FULL_CONFIG_OUTPUT_JSONFILE"
+ }
+ }
+ Return $FULL_CONFIG_OUTPUT_JSONFILE
+}
+
+
+# Listing issue Types
+function Get-OSMConfigAsMarkdown {
+ param (
+ [Parameter(Mandatory = $false)]
+ [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\JIRA",
+ [Parameter(Mandatory = $false)]
+ [string]$PROFILE_NAME = $env:AtlassianPowerKit_PROFILE_NAME
+ )
+ $OUTPUT_FILE = "$OUTPUT_PATH\$PROFILE_NAME-OSM-Config_$(Get-Date -Format 'yyyyMMdd-HHmm').md"
+ $INPUT_JSON_FILE = Import-JSONConfigExport
+ $RAW_CONFIG_JSON = Get-Content -Path "$INPUT_JSON_FILE" -Raw | ConvertFrom-Json -Depth 100
+ # Write the markdown file
+ $RAW_CONFIG_JSON | ForEach-Object {
+ if ($_ -ne $null) {
+ $PROJECT_NAME = if ($null -ne $_.PROJECT_NAME) { $_.PROJECT_NAME } else { 'Unknown Project' }
+ $PROJECT_KEY = if ($null -ne $_.PROJECT_KEY) { $_.PROJECT_KEY } else { 'Unknown Key' }
+ $PROJECT_ISSUE_TYPE_SCHEMA = if ($null -ne $_.PROJECT_ISSUE_TYPE_SCHEMA -and $null -ne $_.PROJECT_ISSUE_TYPE_SCHEMA.self) { $null -ne $_.PROJECT_ISSUE_TYPE_SCHEMA } else { @{ self = '#' } }
+ $PROJECT_ISSUE_TYPES = if ($null -ne $_.PROJECT_ISSUE_TYPES) { $_.PROJECT_ISSUE_TYPES } else { @() }
+ $PROJECT_REQUEST_TYPES = if ($null -ne $_.PROJECT_REQUEST_TYPES) { $_.PROJECT_REQUEST_TYPES } else { @() }
+ $PROJECT_WORKFLOWS_SCHEMES = if ($null -ne $_.PROJECT_WORKFLOWS_SCHEMES) { $_.PROJECT_WORKFLOWS_SCHEMES } else { @() }
+ # Write output for project details
+ Write-Output "## [$PROJECT_NAME]($($PROJECT_ISSUE_TYPE_SCHEMA.self)) - [Show All Instances](https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/jira/servicedesk/projects/$PROJECT_KEY/issues/?jql=project%20%3D%20$PROJECT_KEY)"
+ # Write Issue Types
+ Write-Output '### Issue Types'
+ $PROJECT_ISSUE_TYPES | ForEach-Object {
+ if ($_ -ne $null -and $null -ne $_.Name -and $null -ne $_.self) {
+ Write-Output "- [$($_.Name)]($($_.self)) - [Show All Instances](https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/jira/servicedesk/projects/$PROJECT_KEY/issues/?jql=project%20%3D%20$PROJECT_KEY%20AND%20issuetype%20%3D%20%22$($_.name.replace(' ','%20'))%22)"
+ } else {
+ Write-Output '- Invalid or missing issue type'
+ }
+ }
+ # Write Request Types
+ Write-Output '### Request Types'
+ $PROJECT_REQUEST_TYPES | ForEach-Object {
+ if ($_ -ne $null -and $null -ne $_.Name -and $null -ne $_.self) {
+ Write-Output "- [$($_.Name)]($($_.self)) - [Show All Instances](https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint).atlassian.net/jira/servicedesk/projects/$PROJECT_KEY/issues/?jql=project%20%3D%20$PROJECT_KEY%20AND%20issuetype%20%3D%20%22$($_.name.replace(' ','%20'))%22)"
+ } else {
+ Write-Output '- Invalid or missing request type'
+ }
+ }
+ # Write Workflow Schemes
+ Write-Output '### Workflow Schemes'
+ $PROJECT_WORKFLOWS_SCHEMES | ForEach-Object {
+ if ($_ -ne $null -and $null -ne $_.Name -and $null -ne $_.self) {
+ Write-Output "- [$($_.Name)]($($_.self)) - [Show All Instances](https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint).atlassian.net/jira/servicedesk/projects/$PROJECT_KEY/issues/?jql=project%20%3D%20$PROJECT_KEY%20AND%20issuetype%20%3D%20%22$($_.name.replace(' ','%20'))%22)"
+ }
+ }
+ }
+ } | Out-File -FilePath $OUTPUT_FILE -Force
+ Write-Debug "Output written to $OUTPUT_FILE"
+ $JSON_RETURN = @{ OUTPUT_FILE = $OUTPUT_FILE
+ OUTPUT_PATH = $OUTPUT_PATH
+ STATUS = 'SUCCESS'
+ PROFILE_NAME = $PROFILE_NAME
+ }
+
+ Return $JSON_RETURN | ConvertTo-Json -Depth 100 -Compress
+}
+
+function Export-ProjectProformaFormTemplates {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$PROJECT_KEY,
+ [Parameter(Mandatory = $false)]
+ [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\JIRA"
+ )
+ Import-Module "$env:OSM_INSTALL\AtlassianPowerKit\AtlassianPowerKit-Jira\AtlassianPowerKit-Jira.psd1" -Force | Out-Null
+ $TIME_STAMP = Get-Date -Format 'yyyyMMdd-HHmm'
+ $PROFILE_NAME = $env:AtlassianPowerKit_PROFILE_NAME
+ $FORMLIST_FILE = "$OUTPUT_PATH\$PROFILE_NAME-$PROJECT_KEY-ProformaFormList-$TIME_STAMP.json"
+ # Get List of Forms for the project
+ $FORMS_ALL = Get-FormsForJiraProject -PROJECT_KEY $PROJECT_KEY
+ $FORM_LIST = $FORMS_ALL | ConvertFrom-Json | Where-Object { $_.name -notcontains 'z_Archive' }
+ $FORM_LIST | ConvertTo-Json -Depth 100 | Out-File -FilePath $FORMLIST_FILE -Force | Out-Null
+ $FORM_LIST | ForEach-Object {
+ $FORM_ID = $_.id
+ $FORM_NAME = $_.name
+ Write-Debug "======================= Processing Form: $FORM_NAME"
+ $FORM_ENDPOINT = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/proforma/1.0/form/$FORM_ID/schema"
+ $REST_RESULTS = Invoke-RestMethod -Uri $FORM_ENDPOINT -Method Get -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders)
+ $REST_RESULTS | ConvertTo-Json -Depth 100 | Write-Debug
+ exit
+ $FORM_TEMPLATE | ConvertTo-Json -Depth 100 | Out-File -FilePath "$OUTPUT_PATH\$PROFILE_NAME-$PROJECT_KEY-$FORM_NAME-FormTemplate-$(Get-Date -Format 'yyyyMMdd-HHmm').json" -Force
+ }
+
+ $PROFORMA_API_ENDPOINT = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/proforma/1.0"
+ Return $OUTPUT_FILE
+
+}
+
+# Function Get Deployment
+function Get-OSMDeploymentConfigsJIRA {
+ param (
+ [Parameter(Mandatory = $false)]
+ [string]$PROFILE_NAME = $env:AtlassianPowerKit_PROFILE_NAME,
+ [Parameter(Mandatory = $false)]
+ [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$PROFILE_NAME\JIRA"
+ )
+ $OUTPUT_FILE = "$OUTPUT_PATH\FULL-$PROFILE_NAME-$(Get-Date -Format 'yyyyMMdd-HHmm').json"
+ Write-Host "Processing profile: $PROFILE_NAME"
+ # If there is a env:AtlassianPowerKit_PROFILE_NAME-ProjectList-*.json that was created in the last 12 hours, use it
+ $PROFILE_PROJECT_LIST = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-ProjectList-*.json"
+ if ($PROFILE_PROJECT_LIST) {
+ $PROJECT_LIST = Get-Content $PROFILE_PROJECT_LIST.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate
+ } else {
+ $PROJECT_LIST = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraProjectList' | ConvertFrom-Json -AsHashtable -NoEnumerate
+ }
+ #$PROJECT_LIST | ForEach-Object { Write-Host "Project: $($_.name) - $($_.key)" }
+ #$PROJECT_LIST | ConvertTo-Json -Depth 100 | Write-Debug
+ $OSM_PROJECT_LIST = $PROJECT_LIST | Where-Object { $_.key -match '.*OSM.*' -and $_.key -notin @('CUBOSM') }
+
+ $JIRA_PROJECTS = $OSM_PROJECT_LIST | ForEach-Object {
+ $PROJECT_NAME = $($_.name)
+ $PROJECT_KEY = $($_.key)
+ # PROJECT_PROPERTIES
+ $PROFILE_PROJECT_PROPERTIES = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-ProjectProperties-*.json"
+ if ($PROFILE_PROJECT_PROPERTIES) {
+ $PROFILE_PROJECT_PROPERTIES = Get-Content $PROFILE_PROJECT_PROPERTIES.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate
+ } else {
+ $PROFILE_PROJECT_PROPERTIES = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraProjectProperties' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate
+ }
+ # PROJECT_ISSUE_TYPE_SCHEMA
+ $PROJECT_ISSUE_TYPE_SCHEMA = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-IssueTypeSchema-*.json"
+ if ($PROJECT_ISSUE_TYPE_SCHEMA) {
+ $PROJECT_ISSUE_TYPE_SCHEMA = Get-Content $PROJECT_ISSUE_TYPE_SCHEMA.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate
+ } else {
+ $PROJECT_ISSUE_TYPE_SCHEMA = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraCloudIssueTypeSchema' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate
+ }
+ #
+ # PROJECT_ISSUE_TYPES
+ $PROJECT_ISSUE_TYPES = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-ProjectIssueTypes-*.json"
+ if ($PROJECT_ISSUE_TYPES) {
+ $PROJECT_ISSUE_TYPES = Get-Content $PROJECT_ISSUE_TYPES.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate
+ } else {
+ $PROJECT_ISSUE_TYPES = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraProjectIssueTypes' -FunctionParameters @{ PROJECT_KEY_OR_ID = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate
+ }
+ $PROJECT_REQUEST_TYPES = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-RequestTypeSchema-*.json"
+ if ($PROJECT_REQUEST_TYPES) {
+ $PROJECT_REQUEST_TYPES = Get-Content $PROJECT_REQUEST_TYPES.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate
+ } else {
+ $PROJECT_REQUEST_TYPES = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraServiceDeskRequestTypes' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate
+ }
+
+ # FORMS
+ $PROJECT_FORMS = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-Forms-*.json"
+ if ($PROJECT_FORMS) {
+ $PROJECT_FORMS = Get-Content $PROJECT_FORMS.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate
+ } else {
+ $PROJECT_FORMS = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-FormsForJiraProject' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate
+ }
+
+ # $FORMS = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-FormsForJiraProject' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY }
+ # WORKFLOW_SCHEMES
+ $PROJECT_WORKFLOWS_SCHEMES = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-ProjectWorkflowSchemes-*.json"
+ if ($PROJECT_WORKFLOWS_SCHEMES) {
+ $PROJECT_WORKFLOWS_SCHEMES = Get-Content $PROJECT_WORKFLOWS_SCHEMES.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate
+ } else {
+ $PROJECT_WORKFLOWS_SCHEMES = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraProjectWorkflowSchemes' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate
+ }
+
+ # Return object
+ [PSCustomObject]@{
+ PROJECT_NAME = $PROJECT_NAME
+ PROJECT_KEY = $PROJECT_KEY
+ PROJECT_ISSUE_TYPE_SCHEMA = $PROJECT_ISSUE_TYPE_SCHEMA
+ PROJECT_ISSUE_TYPES = $PROJECT_ISSUE_TYPES
+ PROJECT_REQUEST_TYPES = $PROJECT_REQUEST_TYPES
+ PROJECT_WORKFLOWS_SCHEMES = $PROJECT_WORKFLOWS_SCHEMES
+ }
+ }
+ $JIRA_PROJECTS | ConvertTo-Json -Depth 100 -Compress | Out-File -FilePath $OUTPUT_FILE -Force | Out-Null
+ Return $OUTPUT_FILE
+}
+
+# Funtion to list project properties (JIRA entities)
+function Get-JiraProjectIssueTypes {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$PROJECT_KEY_OR_ID,
+ [Parameter(Mandatory = $false)]
+ [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\JIRA"
+ )
+ # If the AtlassianPowerKit-J
+ if ($PROJECT_KEY_OR_ID -match '^\d+$') {
+ $PROJECT_ID = $PROJECT_KEY_OR_ID
+ } else {
+ # Get the most recent auda-ProjectList-*.json in the $OUTPUT_PATH or run Get-JiraProjectList and check again for the file
+ $PROJECT_LIST_FILE = Get-ChildItem -Path $OUTPUT_PATH -Filter "$env:AtlassianPowerKit_PROFILE_NAME-ProjectList-*.json" | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1
+ While (-not $PROJECT_LIST_FILE) {
+ Write-Debug 'No Project List file found, running Get-JiraProjectList...'
+ Get-JiraProjectList -OUTPUT_PATH $OUTPUT_PATH
+ $PROJECT_LIST_FILE = Get-ChildItem -Path $OUTPUT_PATH -Filter "$env:AtlassianPowerKit_PROFILE_NAME-ProjectList-*.json" | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1
+ }
+ $PROJECT_ID = (Get-Content -Path $PROJECT_LIST_FILE.FullName | ConvertFrom-Json | Where-Object { $_.key -eq $PROJECT_KEY_OR_ID }).id
+ }
+ $FILENAME = "$env:AtlassianPowerKit_PROFILE_NAME-$PROJECT_KEY_OR_ID-IssueTypes-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
+ if (-not (Test-Path $OUTPUT_PATH)) {
+ New-Item -ItemType Directory -Path $OUTPUT_PATH -Force | Out-Null
+ }
+ $OUTPUT_FILE = "$OUTPUT_PATH\$FILENAME"
+ # Use Get-PaginatedResults to get all issues types for the project
+ Write-Debug "Getting Jira Project Issue Types for project: $PROJECT_ID ..."
+ $REST_RESULTS = Get-PaginatedJSONResults -URI "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issuetype/project?projectId=$PROJECT_ID" -Method Get
+ Write-Debug "Jira Project Issue Types for project: $PROJECT_ID received... writing to file..."
+ $REST_RESULTS | ConvertTo-Json -Depth 50 | Out-File -FilePath $OUTPUT_FILE
+ Write-Debug "Jira Project Issue Types written to: $OUTPUT_FILE"
+ return $REST_RESULTS | ConvertTo-Json -Depth 100 -Compress
+}
+
+# Function to get issue type metadata for a Jira Cloud project
+function Get-JiraCloudIssueTypeMetadata {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$PROJECT_KEY,
+ [Parameter(Mandatory = $false)]
+ [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\JIRA"
+ )
+ $FILENAME = "$env:AtlassianPowerKit_PROFILE_NAME-$PROJECT_KEY-IssueTypeMetadata-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
+ $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issue/createmeta/$PROJECT_KEY&expand=projects.issuetypes.fields" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
+ ConvertTo-Json $REST_RESULTS -Depth 50 | Out-File -FilePath "$OUTPUT_PATH\$FILENAME"
+ Write-Debug "Issue Type Metadata JSON file created: $OUTPUT_PATH\$FILENAME"
+ Return $REST_RESULTS
+}
+
+# Fuction to get Issue Type schema for a Jira Cloud project
+function Get-JiraCloudIssueTypeSchema {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$PROJECT_KEY_OR_ID,
+ [Parameter(Mandatory = $false)]
+ [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\JIRA"
+ )
+ # if the project key is passed, get the project ID (key is Alpha-numeric, ID is numeric)
+ if ($PROJECT_KEY_OR_ID -match '^\d+$') {
+ Write-Debug "Project ID passed: $PROJECT_KEY_OR_ID"
+ $PROJECT_ID = $PROJECT_KEY_OR_ID
+ $PROJECT_KEY = (Get-JiraProjectByKey -PROJECT_KEY $PROJECT_KEY_OR_ID | ConvertFrom-Json -AsHashtable -NoEnumerate).key
+ } else {
+ Write-Debug "Project Key passed: $PROJECT_KEY_OR_ID ... getting project ID..."
+ $PROJECT_OBJECT = Get-JiraProjectByKey -PROJECT_KEY $PROJECT_KEY_OR_ID | ConvertFrom-Json -AsHashtable -NoEnumerate
+ #ConvertTo-Json $PROJECT_OBJECT -Depth 50 | Write-Debug
+ if ($PROJECT_OBJECT.id) {
+ $PROJECT_ID = $PROJECT_OBJECT.id
+ } else {
+ Write-Error "Project ID not found for project key: $PROJECT_KEY_OR_ID"
+ }
+ }
+ $FILENAME = "$env:AtlassianPowerKit_PROFILE_NAME-$PROJECT_KEY-IssueTypeSchema-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
+ $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issuetypescheme/project?projectId=$PROJECT_ID" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
+ ConvertTo-Json $REST_RESULTS -Depth 50 | Out-File -FilePath "$OUTPUT_PATH\$FILENAME"
+ Write-Debug "Issue Type Schema JSON file created: $OUTPUT_PATH\$FILENAME"
+ return $REST_RESULTS.values | ConvertTo-Json -Depth 50 -Compress
+}
+
+function Get-FilterJQL {
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$FILTER_ID
+ )
+ # While response code is 429, wait and try again
+ try {
+ $REST_RESPONSE = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/filter/$($FILTER_ID)" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json'
+ } catch {
+ # Catch 429 errors and wait for the retry-after time
+ if ($_.Exception.Response.StatusCode -eq 429) {
+ Write-Warn "429 error, waiting for $RETRY_AFTER seconds..."
+ Start-Sleep -Seconds $RETRY_AFTER
+ $REST_RESPONSE = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/filter/$($FILTER_ID)" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json'
+ } else {
+ Write-Debug "$($MyInvocation.MyCommand.Name): Error getting filter JQL: $($_.Exception.Message)'
+ Write-Error 'Error getting filter JQL: $($_.Exception.Message)"
+ }
+ }
+ Return $REST_RESPONSE.jql
+}
+
+function Get-JiraOSMFilterList {
+ param (
+ [Parameter(Mandatory = $false)]
+ [string[]]$PROJECT_KEYS = @('GRCOSM')
+ )
+ $FILTERS_SEARCH_URL = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/filter/search"
+ $PROJECT_LIST = Get-JiraProjectList | ConvertFrom-Json
+ # Get Project ID project with key GRCOSM
+ $SEARCH_TERMS_FOR_FILTERS = @(
+ @{ 'Name' = 'filterName'; 'Value' = 'osm' })
+ $PROJECT_ID_SEARCHES = $PROJECT_KEYS | ForEach-Object {
+ $PROJECT_KEY = $_
+ $PROJECT_ID = $PROJECT_LIST | Where-Object { $_.key -eq $PROJECT_KEY } | Select-Object -ExpandProperty id
+ if ($PROJECT_ID) {
+ Return @{ 'Name' = 'projectId'; 'Value' = $PROJECT_ID }
+ }
+ }
+ $SEARCH_TERMS_FOR_FILTERS += $PROJECT_ID_SEARCHES
+ # Write-Debug 'Searching for filters with search terms: '
+ # $SEARCH_TERMS_FOR_FILTERS | ConvertTo-Json -Depth 100 | Write-Debug
+ # Write-Debug 'Attempting to get results using Get-PaginatedJSONResults...'
+ $FILTER_RESULTS = $SEARCH_TERMS_FOR_FILTERS | ForEach-Object {
+ $ONE_FILTER_SEARCH_URL = "$FILTERS_SEARCH_URL" + '?' + $_.Name + '=' + $_.Value
+ Get-PaginatedJSONResults -URI $ONE_FILTER_SEARCH_URL -METHOD Get -RESPONSE_JSON_OBJECT_FILTER_KEY 'values' | ConvertFrom-Json -AsHashtable
+ }
+ Write-Debug 'Filter results received... processing...'
+ $i = 1
+ $FILTER_RESULTS = $FILTER_RESULTS | Group-Object id | ForEach-Object { $_.Group | Select-Object -First 1 }
+ $FILTER_RESULTS | ForEach-Object {
+ $FILTER_ID = $_.id
+ $FILTER_NAME = $_.name
+ $FILTER_JQL = "'" + $(Get-FilterJQL -FILTER_ID $FILTER_ID) + "'"
+ Write-Debug "Filter in parsed results [$i]: $FILTER_NAME - $FILTER_ID - $FILTER_JQL"
+ $i++
+ }
+ $FILTER_RESULTS_JSON = $FILTER_RESULTS | ConvertTo-Json -Depth 50 -Compress
+ return $FILTER_RESULTS_JSON
+}
diff --git a/AtlassianPowerKit-Confluence/AtlassianPowerKit-Confluence.psd1 b/AtlassianPowerKit-Confluence/AtlassianPowerKit-Confluence.psd1
index 0eac7fd..74784be 100644
--- a/AtlassianPowerKit-Confluence/AtlassianPowerKit-Confluence.psd1
+++ b/AtlassianPowerKit-Confluence/AtlassianPowerKit-Confluence.psd1
@@ -80,7 +80,7 @@
'Get-ConfluencePageByID',
'Get-ConfluencePageByTitle',
'Remove-AttachmentsFromConfPage',
- 'Set-ConfluencePageContent',
+ 'Set-AttachmentForConfluencePage',
'Set-ConfluenceYearMonthStructure',
'Set-ConfluenceSpacePropertyByID'
)
diff --git a/AtlassianPowerKit-Confluence/AtlassianPowerKit-Confluence.psm1 b/AtlassianPowerKit-Confluence/AtlassianPowerKit-Confluence.psm1
index 634be51..86239f0 100644
--- a/AtlassianPowerKit-Confluence/AtlassianPowerKit-Confluence.psm1
+++ b/AtlassianPowerKit-Confluence/AtlassianPowerKit-Confluence.psm1
@@ -1,4 +1,31 @@
$ErrorActionPreference = 'Stop'; $DebugPreference = 'Continue'
+
+function Export-ConfluencePageToMarkDown {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$CONFLUENCE_SPACE_KEY,
+ [Parameter(Mandatory = $true)]
+ [int64]$CONFLUENCE_PAGE_ID
+ )
+ Write-Debug 'Exporting Confluence page to markdown...not done yet'
+ ## Check if node module @atlaskit/adf-utils is installed
+ #$NODE_MODULE = 'atlaskit/adf-utils'
+ #$NODE_MODULE_PATH = "$($env:OSM_HOME)\node_modules\$NODE_MODULE"
+ #if (-not (Test-Path $NODE_MODULE_PATH)) {
+ # #Check if node is installed
+ # $NODE_PATH = Get-Command -Name 'node' -ErrorAction SilentlyContinue
+ # if (-not $NODE_PATH) {
+ # Write-Error 'Node.js is not installed. Please install Node.js and run `npm install @atlaskit/adf-utils` to install the required node module.'
+ # }
+ # else {
+ # Push-Location $env:OSM_HOME\AtlassianPowerKit\AtlassianPowerKit-Confluence
+ # npm install $NODE_MODULE
+ # Pop-Location
+ # }
+ #}
+
+}
function Export-ConfluencePage {
param (
[Parameter(Mandatory = $true)]
@@ -6,30 +33,47 @@ function Export-ConfluencePage {
[Parameter(Mandatory = $true)]
[int64]$CONFLUENCE_PAGE_ID,
[Parameter(Mandatory = $false)]
- [string]$CONFLUENCE_PAGE_FORMAT = 'storage'
+ [string]$CONFLUENCE_PAGE_FORMAT = 'atlas_doc_format'
)
+ Write-Debug "Recommended format is 'atlas_doc_format', see: https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/"
$CONFLUENCE_PAGE_ENDPOINT = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/wiki/api/v2/pages/$($CONFLUENCE_PAGE_ID)?body-format=$($CONFLUENCE_PAGE_FORMAT)"
Write-Debug "Exporting page format: $CONFLUENCE_PAGE_FORMAT for page ID: $CONFLUENCE_PAGE_ID ... URL: $CONFLUENCE_PAGE_ENDPOINT ..."
try {
$REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_PAGE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
- Write-Debug $REST_RESULTS.getType()
- Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10)
- }
- catch {
+ # Print properties of the REST_RESULTS object
+ #$REST_RESULTS | Get-Member -MemberType Properties -Force | ForEach-Object {
+ # Write-Debug "Property: $($_.Name), Value: $($REST_RESULTS.$($_.Name))"
+ #}
+ } catch {
Write-Error "Error exporting page format: $CONFLUENCE_PAGE_FORMAT for page ID: $CONFLUENCE_PAGE_ID"
}
$CONFLUENCE_PAGE_DIR = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\CONFLUENCE"
- $CONFLUENCE_PAGE_CONTENT = $REST_RESULTS.body.$CONFLUENCE_PAGE_FORMAT
+ if ($CONFLUENCE_PAGE_FORMAT -eq 'atlas_doc_format') {
+ $CONFLUENCE_PAGE_CONTENT = $REST_RESULTS.body.atlas_doc_format.value
+ $FILE_EXTENSION = 'json'
+ } elseif ($CONFLUENCE_PAGE_FORMAT -eq 'storage') {
+ $CONFLUENCE_PAGE_CONTENT = $REST_RESULTS.body.storage.value
+ $FILE_EXTENSION = 'xml'
+ } else {
+ $CONFLUENCE_PAGE_CONTENT = $REST_RESULTS | ConvertTo-Json -Depth 100
+ $FILE_EXTENSION = 'json'
+ }
$CONFLUENCE_PAGE_TITLE = $REST_RESULTS.title
$CONFLUENCE_PAGE_TITLE_FILENAME = $CONFLUENCE_PAGE_TITLE -replace ' ', '_' -replace ':', '-' -replace '[^a-zA-Z0-9_-]', ''
$CURRENT_DATE_TIME = Get-Date -Format 'yyyyMMdd-HHmmss'
- $OUTFILE = "$CONFLUENCE_PAGE_DIR\$($CONFLUENCE_PAGE_TITLE_FILENAME)_$CURRENT_DATE_TIME.$($EXPORT_EXTENSION_MAP[$CONFLUENCE_PAGE_FORMAT])"
+ $OUTFILE = "$CONFLUENCE_PAGE_DIR\$($CONFLUENCE_PAGE_TITLE_FILENAME)-$CURRENT_DATE_TIME.$FILE_EXTENSION"
if (-not (Test-Path $CONFLUENCE_PAGE_DIR)) {
New-Item -ItemType Directory -Path $CONFLUENCE_PAGE_DIR -Force | Out-Null
}
Write-Debug 'Confluence Page Content:'
- $CONFLUENCE_PAGE_CONTENT.Value | Set-Content -Path $OUTFILE -Encoding UTF8 -Force
- return $OUTFILE
+ $CONFLUENCE_PAGE_CONTENT | Set-Content -Path $OUTFILE -Encoding UTF8 -Force
+ $RETURN_OBJ = @{
+ CONFLUENCE_PAGE_TITLE = $CONFLUENCE_PAGE_TITLE
+ CONFLUENCE_PAGE_ID = $CONFLUENCE_PAGE_ID
+ CONFLUENCE_PAGE_FORMAT = $CONFLUENCE_PAGE_FORMAT
+ FILE_NAME = $OUTFILE
+ }
+ return $RETURN_OBJ | ConvertTo-Json
}
function Export-ConfluencePageAllChildren {
@@ -38,10 +82,10 @@ function Export-ConfluencePageAllChildren {
[string]$CONFLUENCE_SPACE_KEY,
[Parameter(Mandatory = $true)]
[int64]$CONFLUENCE_PAGE_ID,
- [Parameter(Mandatory = $true)]
- [string]$CONFLUENCE_PAGE_FORMAT,
[Parameter(Mandatory = $false)]
- [int]$DepthLimit = 0,
+ [string]$CONFLUENCE_PAGE_FORMAT = 'atlas_doc_format',
+ [Parameter(Mandatory = $false)]
+ [int]$DepthLimit = 10,
[Parameter(Mandatory = $false)]
[int]$DepthCount = 0
)
@@ -99,8 +143,7 @@ function Export-ConfluencePageWord {
$response = Invoke-WebRequest -Uri $CONFLUENCE_PAGE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
# Save the content directly to the file
[System.IO.File]::WriteAllBytes($FILE_NAME, $response.Content)
- }
- catch {
+ } catch {
Write-Error "Exception.Message: $($_.Exception.Message)"
}
$FILE_STRING = "$($directoryPath.FullName)\$CONFLUENCE_PAGE_TITLE.doc"
@@ -161,12 +204,10 @@ function Export-ConfluencePageWord {
$templateDoc.SaveAs("$directoryPath\$CONFLUENCE_PAGE_TITLE-Templated.pdf", 17)
$sourceDoc.Close()
$templateDoc.Close()
- }
- catch {
+ } catch {
Write-Debug 'AtlassianPowerKit-Confluence.psm1:Export-ConfluencePageWord - Errored!'
Write-Error "Exception.Message: $($_.Exception.Message)"
- }
- finally {
+ } finally {
$wordApp.Quit()
$wordApp2.Quit()
}
@@ -212,8 +253,7 @@ function Export-ConfluencePageStorageFormat {
#Write-Debug "Rest Result Fields, Recursive: $($REST_RESULTS | Get-Member -MemberType Properties -Force)"
#Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10)
Write-Debug '++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'
- }
- catch {
+ } catch {
$functionName = (Get-PSCallStack)[0].FunctionName
Write-Debug "$functionName errored: $($_.Exception.Message)"
Write-Error "$functionName errored: $($_.Exception.Message)"
@@ -246,7 +286,7 @@ function Export-ConfluencePageStorageFormatForChildren {
[Parameter(Mandatory = $true)]
[string]$CONFLUENCE_PARENT_PAGE_TITLE,
[Parameter(Mandatory = $false)]
- [int]$DepthLimit = 0,
+ [int]$DepthLimit = 10,
[Parameter(Mandatory = $false)]
[int]$DepthCount = 0
)
@@ -284,8 +324,7 @@ function Get-ConfluencePageByID {
Write-Debug "Confluence Page Endpoint: $CONFLUENCE_PAGE_ENDPOINT"
try {
$REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_PAGE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
- }
- catch {
+ } catch {
Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String)
Write-Error "Get-ConfluencePageByID: $($_.Exception.Message)"
}
@@ -312,8 +351,7 @@ function Get-ConfluencePageByTitle {
$REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_PAGE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
#Write-Debug $REST_RESULTS.getType()
Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10)
- }
- catch {
+ } catch {
Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String)
Write-Error "Error updating field: $($_.Exception.Message)"
}
@@ -326,8 +364,7 @@ function Get-ConfluenceSpaceList {
$CONFLUENCE_SPACES_ENDPOINT = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/wiki/api/v2/spaces"
try {
$REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_SPACES_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json'
- }
- catch {
+ } catch {
Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__
Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription
}
@@ -348,8 +385,7 @@ function Get-ConfluenceSpacePropertiesBySpaceID {
$REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_SPACE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json'
Write-Debug $REST_RESULTS.getType()
Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10)
- }
- catch {
+ } catch {
Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__
Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription
}
@@ -369,8 +405,7 @@ function Get-ConfluenceChildPages {
$REST_RESULTS = Invoke-RestMethod -Uri $GET_CHILD_PAGE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
Write-Debug $REST_RESULTS.getType()
Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10)
- }
- catch {
+ } catch {
Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__
Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription
}
@@ -390,16 +425,14 @@ function Remove-AttachmentsFromConfPage {
$REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_PAGE_ATTACHMENTS_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
Write-Debug $REST_RESULTS.getType()
Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10)
- }
- catch {
+ } catch {
Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__
Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription
}
$REST_RESULTS.results | ForEach-Object {
if ($EXCLUDE_ATTACHMENT_NAMES -contains $_.title) {
Write-Debug "Excluding attachment: $($_.title)"
- }
- else {
+ } else {
$CONFLUENCE_PAGE_ATTACHMENT_DELETE_ENDPOINT = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/wiki/api/v2/attachments/$($_.id)"
Write-Debug "Deleting attachment: $($_.title)"
Invoke-RestMethod -Uri $CONFLUENCE_PAGE_ATTACHMENT_DELETE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Delete
@@ -407,6 +440,68 @@ function Remove-AttachmentsFromConfPage {
}
}
+function Set-AttachmentForConfluencePage {
+ param (
+ [Parameter(Mandatory = $true)]
+ [int64]$CONFLUENCE_PAGE_ID,
+ [Parameter(Mandatory = $true)]
+ [string]$ATTACHMENT_FILE_PATH
+ )
+
+ # Validate the file path
+ if (-not (Test-Path $ATTACHMENT_FILE_PATH)) {
+ Write-Error "Attachment file does not exist: $ATTACHMENT_FILE_PATH"
+ return
+ }
+
+ # API endpoint
+ $CONFLUENCE_PAGE_V1_ATTACHMENTS_ENDPOINT = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/wiki/rest/api/content/$CONFLUENCE_PAGE_ID/child/attachment"
+
+ # Request headers
+ $REQUEST_HEADERS = @{
+ 'Authorization' = "Basic $env:AtlassianPowerKit_AtlassianAPIAuthString"
+ 'X-Atlassian-Token' = 'no-check'
+ }
+
+ # File preparation
+ $FileName = [System.IO.Path]::GetFileName($ATTACHMENT_FILE_PATH)
+ $Boundary = [System.Guid]::NewGuid().ToString()
+ $FileContent = [System.IO.File]::ReadAllBytes($ATTACHMENT_FILE_PATH)
+
+ # Construct multipart form-data
+ $Body = @(
+ "--$Boundary"
+ "Content-Disposition: form-data; name=`"file`"; filename=`"$FileName`""
+ 'Content-Type: application/pdf'
+ ''
+ ([System.Text.Encoding]::UTF8.GetString($FileContent))
+ "--$Boundary--"
+ ) -join "`r`n"
+
+ try {
+ # POST the attachment
+ $REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_PAGE_V1_ATTACHMENTS_ENDPOINT `
+ -Headers $REQUEST_HEADERS `
+ -Method Post `
+ -ContentType "multipart/form-data; boundary=$Boundary" `
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($Body))
+
+ # Output results
+ Write-Debug 'Attachment uploaded successfully.'
+ Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10)
+ return $REST_RESULTS | ConvertTo-Json
+ } catch {
+ # Handle exceptions
+ Write-Error "Failed to upload attachment: $($_.Exception.Message)"
+ if ($_.Exception.Response) {
+ Write-Error "Response Status Code: $($_.Exception.Response.StatusCode)"
+ Write-Error "Response Status Description: $($_.Exception.Response.StatusDescription)"
+ }
+ }
+}
+
+
+
# Function to set confluence space properties by space ID
function Set-ConfluenceSpacePropertyByID {
param (
@@ -426,89 +521,13 @@ function Set-ConfluenceSpacePropertyByID {
$REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_SPACE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Put -ContentType 'application/json' -Body $CONFLUENCE_SPACE_PROPERTIES
Write-Debug $REST_RESULTS.getType()
Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10)
- }
- catch {
+ } catch {
Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__
Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription
}
$REST_RESULTS
}
-## INPROGRESS
-# Function set Confluence page content using a storage format file and page ID
-# function Set-ConfluencePageContent {
-# param (
-# [Parameter(Mandatory = $true)]
-# [string]$CONFLUENCE_SPACE_KEY,
-# [Parameter(Mandatory = $true)]
-# [int64]$CONFLUENCE_PAGE_ID,
-# [Parameter(Mandatory = $false)]
-# [string]$CONFLUENCE_PAGE_STORAGE_FILE
-# )
-# $MyFunctionName = (Get-PSCallStack)[0].FunctionName
-# $VERSION_MESSAGE = "Updated via AtlassianPowerKit $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
-# Get-ChildItem -Path . -Recurse -Filter 'Naive-ConflunceStorageValidator.psd1' | Import-Module -Force
-# $CONFLUENCE_PAGE_ENDPOINT = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/wiki/api/v2/pages/$($CONFLUENCE_PAGE_ID)"
-# if ($CONFLUENCE_PAGE_STORAGE_FILE) {
-# Write-Debug "$MyFunctionName- Using file: $CONFLUENCE_PAGE_STORAGE_FILE to update page ID: $CONFLUENCE_PAGE_ID...ignoring -CONFLUENCE_PAGE_STORAGE_FILE_CONTENT if it was provided..."
-# }
-# else {
-# Write-Error 'You must provide either a file path to a storage format file or the content of the storage format file to update the page with parameter -CONFLUENCE_PAGE_STORAGE_FILE or -CONFLUENCE_PAGE_STORAGE_FILE_CONTENT'
-# }
-# Write-Debug "$MyFunctionName Getting existing page title and version number..."
-# $REST_RESULTS = Invoke-RestMethod -Uri $CONFLUENCE_PAGE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
-# #$REST_RESULTS | ConvertTo-Json -Depth 40 | Write-Debug
-# $CONFLUENCE_PAGE_TITLE = $REST_RESULTS.title
-# $CURRENT_VERSION = $REST_RESULTS.version.number
-# $CLEANED_FILE_CONTENT = Get-Content -Path $CONFLUENCE_PAGE_STORAGE_FILE -Raw -Encoding UTF8
-# #Write-Debug "Making backup of current page ID: $CONFLUENCE_PAGE_ID..."
-# # Remove pretty formatting, whitepassed, and newlines
-# function Get-ConfluenceStoragePayload {
-# param (
-# [Parameter(Mandatory = $true)]
-# [string]$CleanedXmlContent,
-# [Parameter(Mandatory = $true)]
-# [string]$Title,
-# [Parameter(Mandatory = $true)]
-# [int]$Version,
-# [Parameter(Mandatory = $true)]
-# [int64]$PageId
-# )
-
-# $body = @{
-# representation = 'storage'
-# value = $CleanedXmlContent
-# }
-
-# $payload = @{
-# id = $PageId
-# title = $Title
-# version = @{
-# number = $Version
-# message = $VERSION_MESSAGE
-# }
-# body = @{
-# storage = $body
-# }
-# status = 'current'
-# }
-
-# return $payload | ConvertTo-Json -Depth 10 -Compress
-# }
-# $NEW_VERSION = $CURRENT_VERSION + 1
-# Write-Debug "$MyFunctionName - Page Payload: $PAGE_PAYLOAD"
-# try {
-# #Invoke-RestMethod -Uri $CONFLUENCE_PAGE_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
-# Invoke-RestMethod -Uri $CONFLUENCE_PAGE_ENDPOINT -Method Put -ContentType 'application/json' -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Body $(Get-ConfluenceStoragePayload -CleanedXmlContent $CLEANED_FILE_CONTENT -Title $CONFLUENCE_PAGE_TITLE -Version $NEW_VERSION -PageId $CONFLUENCE_PAGE_ID)
-# }
-# catch {
-# Write-Debug $(Prepare-ConfluenceStoragePayload -CleanedXmlContent $CLEANED_FILE_CONTENT -Title $CONFLUENCE_PAGE_TITLE -Version $NEW_VERSION -PageId $CONFLUENCE_PAGE_ID)
-# Write-Debug "$MyFunctionName - StatusCode: $($_.Exception.Response.StatusCode.value__)"
-# Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String)
-# Write-Error "Error updating field: $($_.Exception.Message)"
-# }
-# }
-# Function to take a CONFLUENCE_PAGE_ID, validate it is in 'YYYY (.*)' format, get a list of Child Pages, and if it doesn't already exist, create a new child page with the title 'YYYY[1-12] (.*)', move any existing child pages that title match 'YYYYMM.*' to the new child page, and then move the new child page to the top of the list
function Set-ConfluenceYearMonthStructure {
param (
[Parameter(Mandatory = $true)]
@@ -524,10 +543,10 @@ function Set-ConfluenceYearMonthStructure {
throw "Page does not exist: $CONFLUENCE_PAGE_ID"
}
$CONFLUENCE_PAGE_TITLE = $CONFLUENCE_PAGE.title
- if ($CONFLUENCE_PAGE_TITLE -notmatch '^\d{4} (.*)') {
+ if ($CONFLUENCE_PAGE_TITLE -notmatch '^\d { 4 } (.*)') {
throw "Confluence page title does not match 'YYYY (.*)' format. This function is intended to be used on pages with titles in the format 'YYYY (.*)'"
}
- $MATCH = $CONFLUENCE_PAGE_TITLE -match '(\d{4}) - (.*)'
+ $MATCH = $CONFLUENCE_PAGE_TITLE -match '(\d { 4 }) - (.*)'
$CONFLUENCE_PAGE_YEAR = $MATCH[1]
$CONFLUENCE_STRUCTURE_NAME = $MATCH[2].Trim()
@@ -546,8 +565,7 @@ function Set-ConfluenceYearMonthStructure {
$CONFLUENCE_PAGE_MONTH_PAGE = New-ConfluencePage -CONFLUENCE_SPACE_KEY $CONFLUENCE_SPACE_KEY -CONFLUENCE_PAGE_TITLE $CONFLUENCE_PAGE_MONTH_TITLE
# Copy content from parent page
Set-ConfluencePageContent -CONFLUENCE_SPACE_KEY $CONFLUENCE_SPACE_KEY -CONFLUENCE_PAGE_ID $CONFLUENCE_PAGE_MONTH_PAGE.id -CONFLUENCE_PAGE_STORAGE_FILE $TEMP_FILE.FullName
- }
- else {
+ } else {
Write-Debug "Page already exists: $CONFLUENCE_PAGE_MONTH_TITLE"
}
} finally {
@@ -557,7 +575,7 @@ function Set-ConfluenceYearMonthStructure {
$CHILD_PAGES = Get-ConfluenceChildPages -CONFLUENCE_SPACE_KEY $CONFLUENCE_SPACE_KEY -PARENT_ID $CONFLUENCE_PAGE_ID
$CHILD_PAGES.results | ForEach-Object {
$CHILD_PAGE_TITLE = $_.title
- $CHILD_PAGE_TITLE_MATCH = $CHILD_PAGE_TITLE -match '(\d{4})(\d{2})(\d+.*)'
+ $CHILD_PAGE_TITLE_MATCH = $CHILD_PAGE_TITLE -match '(\d { 4 })(\d { 2 })(\d+.*)'
if ($CHILD_PAGE_TITLE_MATCH) {
$CHILD_PAGE_YEAR = $Matches[1]
$CHILD_PAGE_MONTH = $Matches[2]
diff --git a/AtlassianPowerKit-Confluence/Updating-Confluence-Pages.md b/AtlassianPowerKit-Confluence/Updating-Confluence-Pages.md
new file mode 100644
index 0000000..0c02c7d
--- /dev/null
+++ b/AtlassianPowerKit-Confluence/Updating-Confluence-Pages.md
@@ -0,0 +1,3 @@
+# Notes on updating Confluence pages
+
+- Using the Confluence REST API to update pages is a bit tricky. The API is not very well documented and the examples are not very helpful. Here are some notes on how to update Confluence pages using the REST API.
diff --git a/AtlassianPowerKit-GRCosm/AtlassianPowerKit-GRCosm.psm1 b/AtlassianPowerKit-GRCosm/AtlassianPowerKit-GRCosm.psm1
index 9585512..456904a 100644
--- a/AtlassianPowerKit-GRCosm/AtlassianPowerKit-GRCosm.psm1
+++ b/AtlassianPowerKit-GRCosm/AtlassianPowerKit-GRCosm.psm1
@@ -70,55 +70,89 @@ function Update-GRCosmConfRegister {
[Parameter(Mandatory = $true)]
[string]$CONFLUENCE_SPACE_KEY,
[Parameter(Mandatory = $true)]
- [string]$CONF_PAGE_ID,
+ [string]$CONFLUENCE_PAGE_ID,
[Parameter(Mandatory = $true)]
- [string]$FILTER_ID,
- [Parameter(Mandatory = $true)]
- [string]$REGISTER_STORAGE_TEMPLATE_PATH,
- [Parameter(Mandatory = $false)]
- [string]$TEMPLATE_PLACEHOLDER_MAP
+ [string]$FILTER_ID
)
- if (-not $TEMPLATE_PLACEHOLDER_MAP) {
- $TEMPLATE_PLACEHOLDER_MAP = @{
- 'GRCOSM_REGISTER_TABLE_DATA' = 'Get-JiraFilterResultsAsConfluenceTable -FILTER_ID $FILTER_ID'
- }
- }
- # Check template file exists
- if (-not (Test-Path $REGISTER_STORAGE_TEMPLATE_PATH)) {
- Write-Error "Update-GRCosmConfRegister: Template file not found: $REGISTER_STORAGE_TEMPLATE_PATH"
+ $FILTER_INFO = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/filter/$($FILTER_ID)" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
+
+ $WEB_UI_HEADERS = @{
+ Authorization = "Basic $env:AtlassianPowerKit_AtlassianAPIAuthString"
+ Accept = 'text/html'
}
- # Backup the Confluence page storage format
- $BACKUP_FILE = Export-ConfluencePageStorageFormat -CONFLUENCE_SPACE_KEY $CONFLUENCE_SPACE_KEY -CONFLUENCE_PAGE_ID $CONF_PAGE_ID
- Write-Debug "Backup file: $BACKUP_FILE"
+ $TIME_STAMP = Get-Date -Format 'yyyyMMdd_HHmmss'
+ $FILENAME = $FILTER_INFO.name.Replace(' ', '_').Replace(':', '-').Replace('/', '_').Replace('\', '_').Replace('[^a-zA-Z0-9]', '') + $TIME_STAMP
+ #FiLENAME is just GRC.*, exclude any prefix
+ $FILENAME = $FILENAME -replace '.*GRC', 'GRC'
+ $OUTPUT_FILE = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\$FILENAME"
+ $PUPPETEER_PATH = "$($env:OSM_HOME)\AtlassianPowerKit\AtlassianPowerKit-GRCosm"
+ #Push-Location $PUPPETEER_PATH
- # Split $BACKUP_FILE on _ and drop the last element
- $BACKUP_FILE_BASE = $($BACKUP_FILE -split '_2')[0]
+ $NodePath = Get-Command -Name 'node' | Select-Object -ExpandProperty Source
+ Write-Debug "NodePath: $NodePath"
+ $ScriptPath = "$PUPPETEER_PATH\generate-pdf.js"
+ $Arguments = @(
+ $ScriptPath, # First argument is the script path
+ "--url=https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/sr/jira.issueviews:searchrequest-printable/$FILTER_ID/SearchRequest-$FILTER_ID.html",
+ "--auth=Basic $env:AtlassianPowerKit_AtlassianAPIAuthString",
+ "--output=$OUTPUT_FILE",
+ '--format=A2',
+ '--landscape',
+ '--scale=0.75',
+ '--background'
+ )
- # Get JIRA filter data - Fields are determined by the JIRA filter
- # Write-Debug '############################################################################################'
- Write-Debug 'Update-GRCosmConfRegister: Getting JIRA filter results as Confluence table...'
- $CONF_REGISTER_TABLE_DATA = Get-JiraFilterResultsAsConfluenceTable -FILTER_ID $FILTER_ID
- Write-Debug 'Update-GRCosmConfRegister: Jira filter results as Confluence table returned'
- # Write-Debug "Type: $($CONF_REGISTER_TABLE_DATA.GetType())"
- # Write-Debug "Length: $($CONF_REGISTER_TABLE_DATA.Length)"
- # Write-Debug "Content: `n $($CONF_REGISTER_TABLE_DATA)"
- Write-Debug 'Update-GRCosmConfRegister: Getting Confluence template data...'
- # ([string]::join("",$CONTENT.Split("`n").Trim()))
- $UPDATED_PAGE_STORAGE_DATA = Get-Content $REGISTER_STORAGE_TEMPLATE_PATH -Raw
- Write-Debug 'Update-GRCosmConfRegister: Confluence template data returned'
- # Write-Debug "Type: $($UPDATED_PAGE_STORAGE_DATA.GetType())"
- # Write-Debug "Length: $($UPDATED_PAGE_STORAGE_DATA.Length)"
- # Write-Debug "Content: `n $($UPDATED_PAGE_STORAGE_DATA)"
- Write-Debug 'Update-GRCosmConfRegister: Replacing GRCOSM_REGISTER_TABLE_DATA PLA with JIRA filter results...'
- $UPDATED_PAGE_STORAGE_DATA = $UPDATED_PAGE_STORAGE_DATA -replace 'GRCOSM_REGISTER_TABLE_DATA', $CONF_REGISTER_TABLE_DATA
- Write-Debug 'Update-GRCosmConfRegister: GRCOSM_REGISTER_TABLE_DATA replaced'
- Write-Debug "Type: $($UPDATED_PAGE_STORAGE_DATA.GetType())"
- Write-Debug '############################ STORAGE FORMAT TO SEND ################################################'
- $UPDATED_PAGE_STORAGE_DATA | Out-File "$BACKUP_FILE_BASE-LATEST.xml" -Encoding UTF8 -Force
- Write-Debug "############################ STORAGE FORMAT TO SEND ################################################ `n `n `n"
- Write-Debug "$BACKUP_FILE_BASE-LATEST.xml"
- Set-ConfluencePageContent -CONFLUENCE_SPACE_KEY $CONFLUENCE_SPACE_KEY -CONFLUENCE_PAGE_ID $CONF_PAGE_ID -CONFLUENCE_PAGE_STORAGE_FILE "$BACKUP_FILE_BASE-LATEST.xml"
+ # Start the Node.js process and wait for it to complete
+ $Process = Start-Process -FilePath $NodePath `
+ -WorkingDirectory $PUPPETEER_PATH `
+ -ArgumentList $Arguments `
+ -NoNewWindow `
+ -PassThru `
+ -Wait
+ # Check the exit code
+ if ($Process.ExitCode -ne 0) {
+ Write-Error "Node.js script failed with exit code $($Process.ExitCode)"
+ }
+ else {
+ Write-Host 'Node.js script completed successfully.'
+ }
+ Import-Module "$($env:OSM_HOME)\AtlassianPowerKit\AtlassianPowerKit-Confluence\AtlassianPowerKit-Confluence.psd1" -Force | Out-Null
+ Write-Debug "Removing attachments from Confluence page: $CONFLUENCE_PAGE_ID"
+ Remove-AttachmentsFromConfPage -CONFLUENCE_PAGE_ID $CONFLUENCE_PAGE_ID | Out-Null
+ Write-Debug "Setting attachment for Confluence page: $CONFLUENCE_PAGE_ID to $OUTPUT_FILE.pdf"
+ Set-AttachmentForConfluencePage -CONFLUENCE_PAGE_ID $CONFLUENCE_PAGE_ID -ATTACHMENT_FILE_PATH "$OUTPUT_FILE.pdf" | Out-Null
+ Write-Debug "Attached file: $OUTPUT_FILE.pdf to Confluence page: $CONFLUENCE_PAGE_ID"
+ $CURRENT_PAGE_STORAGE_FORMAT_FILE = Export-ConfluencePageStorageFormat -CONFLUENCE_SPACE_KEY $CONFLUENCE_SPACE_KEY -CONFLUENCE_PAGE_ID $CONFLUENCE_PAGE_ID
+ $CURRENT_PAGE_STORAGE_FORMAT = Get-Content $CURRENT_PAGE_STORAGE_FORMAT_FILE
+ # Update ri:filename="GRCosm-_Asset_Register20250129_005159.pdf" to the uploaded file name
+ $UPDATED_PAGE_STORAGE_FORMAT = $CURRENT_PAGE_STORAGE_FORMAT -replace 'ri:filename=".*?"', "ri:filename=""$FILENAME.pdf"""
+ Write-Debug "Updated storage format for Confluence page: $CONFLUENCE_PAGE_ID, getting current page info..."
+ $CURRENT_PAGE_INFO = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/wiki/api/v2/pages/$CONFLUENCE_PAGE_ID" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get
+ # Step 4: Prepare the updated content
+ $UPDATEDBODY = @{
+ id = $CONFLUENCE_PAGE_ID
+ status = 'current'
+ title = $CURRENT_PAGE_INFO.title
+ body = @{
+ storage = @{
+ representation = 'storage'
+ value = $UPDATED_PAGE_STORAGE_FORMAT
+ }
+ }
+ version = @{
+ number = $CURRENT_PAGE_INFO.version.number + 1
+ }
+ } | ConvertTo-Json -Depth 40 -Compress
+ #$UPDATEDBODY = $UPDATEDBODY -replace '\\"', '"'
+ Write-Debug "Updated body: $UPDATEDBODY"
+
+ # Step 5: Update the Confluence page
+ Write-Debug "Pushing updated Confluence page: $CONFLUENCE_PAGE_ID"
+ $UPDATE_RESPONSE = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/wiki/api/v2/pages/$CONFLUENCE_PAGE_ID" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -ContentType 'application/json' -Method Put -Body $UPDATEDBODY
+ Write-Debug "Update response: $UPDATE_RESPONSE"
+ Write-Debug "Confluence page updated: $CONFLUENCE_PAGE_ID"
+ Return $UPDATE_RESPONSE | ConvertTo-Json
}
function Get-OSMPlaceholdersJira {
@@ -209,7 +243,8 @@ function Get-OSMPlaceholdersConfluence {
$placeholder | ForEach-Object {
# Write output in red
Write-Output "#### PLACEHOLDER FOUND!!! See: $($FILE.FullName): $_"
- $PLACEHOLDERS += , ($($FILE.NAME), $_) }
+ $PLACEHOLDERS += , ($($FILE.NAME), $_)
+ }
}
else {
Write-Debug "No placeholders found in file: $($FILE.FullName)"
diff --git a/AtlassianPowerKit-GRCosm/generate-pdf.js b/AtlassianPowerKit-GRCosm/generate-pdf.js
new file mode 100644
index 0000000..ad412c8
--- /dev/null
+++ b/AtlassianPowerKit-GRCosm/generate-pdf.js
@@ -0,0 +1,80 @@
+const puppeteer = require("puppeteer");
+const yargs = require("yargs");
+const fs = require("fs");
+
+(async () => {
+ const argv = yargs
+ .option("url", {
+ alias: "u",
+ description: "The URL of the page to print",
+ type: "string",
+ demandOption: true,
+ })
+ .option("auth", {
+ alias: "a",
+ description:
+ 'Base64-encoded Authorization header (e.g., "Basic username:api_token")',
+ type: "string",
+ demandOption: true,
+ })
+ .option("output", {
+ alias: "o",
+ description: "Output file path prefix (e.g., 'output' for output.pdf)",
+ type: "string",
+ default: "output",
+ })
+ .help()
+ .alias("help", "h").argv;
+
+ const { url, auth, output } = argv;
+
+ const browser = await puppeteer.launch({ headless: false }); // Use headless: true for production
+ const page = await browser.newPage();
+
+ try {
+ // Step 1: Set Authorization Header
+ await page.setExtraHTTPHeaders({
+ Authorization: auth,
+ "X-Atlassian-Token": "no-check",
+ });
+
+ // Debug: Log failed requests
+ page.on("requestfailed", (request) => {
+ console.error(
+ `Request failed: ${request.url()} - ${request.failure().errorText}`
+ );
+ });
+
+ // Step 2: Navigate to the page
+ console.log(`Navigating to URL: ${url}`);
+ await page.goto(url, { waitUntil: "networkidle2" });
+
+ // Debug: Log final URL
+ console.log(`Final URL: ${page.url()}`);
+
+ // Debug: Capture screenshot
+ console.log("Capturing screenshot...");
+ await page.screenshot({ path: "debug-screenshot.png", fullPage: true });
+
+ // Step 3: Generate the PDF
+ console.log("Generating PDF...");
+ const pdfPath = `${output}.pdf`;
+ await page.pdf({
+ path: pdfPath,
+ format: "A2",
+ landscape: true,
+ printBackground: true,
+ margin: {
+ top: "10mm",
+ right: "10mm",
+ bottom: "10mm",
+ left: "10mm",
+ },
+ });
+ console.log(`PDF saved as '${pdfPath}'`);
+ } catch (err) {
+ console.error("An error occurred:", err);
+ } finally {
+ await browser.close();
+ }
+})();
diff --git a/AtlassianPowerKit-GRCosm/templates/GRCosm-SharePointViewer-Template.confluence b/AtlassianPowerKit-GRCosm/templates/GRCosm-SharePointViewer-Template.confluence
new file mode 100644
index 0000000..a1e0134
--- /dev/null
+++ b/AtlassianPowerKit-GRCosm/templates/GRCosm-SharePointViewer-Template.confluence
@@ -0,0 +1,24 @@
+
$ROW_VAL
N/A
$ROW_VAL
N/A
Updated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Source: $($FILTER_INFO.name)
Updated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Source: $($FILTER_INFO.name)
$($_.label)
https://auda.atlassian.net/servicedesk/customer/portal/7/', '
'), + [Parameter(Mandatory = $false)] + [Switch]$DryRun = $false + ) + $ISSUES = Get-JiraCloudJQLQueryResult -JQL_STRING $JQL_STRING + $ISSUES.issues | ForEach-Object { + $ISSUE = $_ + $ISSUE_KEY = $ISSUE.key + Write-Debug "Updating fields for issue: $($_.key)" + if (! $DryRun) { + Set-JiraIssueField -ISSUE_KEY $ISSUE_KEY -Field_Ref $FIELD_REF -New_Value $NEW_VALUE -FIELD_TYPE $FIELD_TYPE + } else { Write-Debug "Dry Run: Set-JiraIssueField -ISSUE_KEY $ISSUE_KEY -Field_Ref $FIELD_REF -New_Value $NEW_VALUE" } } } +function Set-OSMRelationFieldBulkSQL { + param ( + [Parameter(Mandatory = $true)] + [string]$JQL_STRING, + [Parameter(Mandatory = $true)] + [string]$FieldRef + ) + + $ISSUES = Get-JiraCloudJQLQueryResult -JQL_STRING $JQL_STRING -ReturnJSONOnly -RETURN_FIELDS @('key', 'summary') + $ISSUES = $ISSUES | ConvertFrom-Json + $ISSUES | ForEach-Object { + $ISSUE_KEY = $_.key + $ISSUE_SUMMARY = $_.fields.summary + Write-Debug "Updating fields for issue: $ISSUE_KEY - $ISSUE_SUMMARY" + Set-OSMRelationFieldIssueKey -IssueKey $ISSUE_KEY -FieldRef $FieldRef + } +} + +function Set-OSMRelationFieldIssueKey { + param ( + [Parameter(Mandatory = $true)] + [string]$IssueKey, + [Parameter(Mandatory = $true)] + [string]$FieldRef + ) + $textInfo = (Get-Culture).TextInfo + $LINKED_ISSUES_HASHTABLE = @{} + $ISSUE_LINKS = Get-JiraIssueLinks -IssueKey $IssueKey + # For each link type, create a nested list of linked issues + $ISSUE_LINKS | ConvertFrom-Json | ForEach-Object { + # if the $_ contains an inwardIssue, the linked issue is the inwardIssue, otherwise it is the outwardIssue + if ($_.inwardIssue) { + $LINK_TYPE_NAME = $_.type.inward + $LINKED_ISSUE = $_.inwardIssue.key + } else { + $LINK_TYPE_NAME = $_.type.outward + $LINKED_ISSUE = $_.outwardIssue.key + } + $LINK_TYPE_NAME = $textInfo.ToTitleCase($LINK_TYPE_NAME.ToLower()) + Write-Debug "Link Type: $LINK_TYPE_NAME, Linked Issue: $LINKED_ISSUE" + if ($LINKED_ISSUES_HASHTABLE.ContainsKey($LINK_TYPE_NAME)) { + $LINKED_ISSUES_HASHTABLE[$LINK_TYPE_NAME] += @("https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/browse/$LINKED_ISSUE") + } else { + $LINKED_ISSUES_HASHTABLE[$LINK_TYPE_NAME] = @("https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/browse/$LINKED_ISSUE") + } + } + # Output the linked issues hashtable sorted by link type and convert to JSON + $JSON_STRING = $LINKED_ISSUES_HASHTABLE | ConvertTo-Json -Depth 10 + Write-Debug "JSON_STRING: $JSON_STRING" + $MARKDOWN_TEXT = ConvertTo-JSONMarkdownList -JSON_DATA_STRING $JSON_STRING + Write-Debug "MARKDOWN_TEXT: $MARKDOWN_TEXT" + #$MARKDOWN_TEXT | Write-Debug + $UPDATE_RESPONSE = Set-JiraIssueField -ISSUE_KEY $IssueKey -Field_Ref $FieldRef -New_Value $MARKDOWN_TEXT -FIELD_TYPE 'text' + return $UPDATE_RESPONSE +} + # function to get changes from a Jira issue change log that are from a value to null function Get-JiraIssueChangeNulls { param ( @@ -702,8 +1031,7 @@ function Get-JiraIssueChangeNulls { $NULL_CHANGE_ITEMS += $MAMMA.items | Where-Object { ($MAMMA.created -gt (Get-Date).AddMonths($CHECK_MONTHS)) -and ((-not $_.toString) -and ( -not $_.to)) -and (-not $_.field.StartsWith('BCMS')) -and (-not $EXCLUDED_FIELDS.Contains($_.field)) } - } - else { + } else { $NULL_CHANGE_ITEMS += $MAMMA.items | Where-Object { (($SELECTOR -eq $($_.fieldId)) -and ($INCLDUED_VALUES.Contains($_.toString))) #Write-Debug "Selector: $SELECTOR" @@ -728,14 +1056,14 @@ function Get-JiraIssueChangeNulls { $_ | Add-Member -MemberType NoteProperty -Name 'author' -Value $MAMMA.author.emailAddress #Write-Debug $_ | Select-Object -Property * -ExcludeProperty psobject $FINAL_ITEMS += $_ - # $fieldType = '' + # $FIELD_TYPE = '' # $fieldRef = '' # switch -regex ($_.field) { - # 'Service Categories' { $fieldType = 'multi-select'; $fieldRef = 'customfield_10316' } - # 'Sensitivity Classification' { $fieldType = 'single-select'; $fieldRef = 'customfield_10275' } - # Default { $fieldType = 'text' } + # 'Service Categories' { $FIELD_TYPE = 'multi-select'; $fieldRef = 'customfield_10316' } + # 'Sensitivity Classification' { $FIELD_TYPE = 'single-select'; $fieldRef = 'customfield_10275' } + # Default { $FIELD_TYPE = 'text' } # } - # Write-Debug "Set-JiraIssueField -ISSUE_KEY $($_.key) -Field_Ref $fieldRef -New_Value $($_.fromString) -FieldType $fieldType" + # Write-Debug "Set-JiraIssueField -ISSUE_KEY $($_.key) -Field_Ref $fieldRef -New_Value $($_.fromString) -FIELD_TYPE $FIELD_TYPE" } } $FINAL_ITEMS @@ -745,7 +1073,9 @@ function Get-JiraIssueChangeNulls { function Get-JiraStatuses { param ( [Parameter(Mandatory = $false)] - [switch]$WriteOutput = $false + [switch]$JSONOnly = $false, + [Parameter(Mandatory = $false)] + [switch]$GetDuplicates = $false ) $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/status" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json' function Get-DuplicateJiraStatusNames { @@ -793,23 +1123,57 @@ function Get-JiraStatuses { } return $JIRA_STATUSES } - if ($WriteOutput) { - $OUTPUT_FILE = "$env:OSM_HOME\$env:AtlassianPowerKit_PROFILE_NAME\JIRA\$env:AtlassianPowerKit_PROFILE_NAME-JIRAStatuses-$(Get-Date -Format 'yyyyMMdd-HHmmss').xlsx" + if (! $JSONOnly) { + $OUTPUT_FILE = "$env:OSM_HOME\$env:AtlassianPowerKit_PROFILE_NAME\JIRA\$env:AtlassianPowerKit_PROFILE_NAME-JIRAStatuses-$(Get-Date -Format 'yyyyMMdd-HHmmss').json" if (-not (Test-Path $OUTPUT_FILE)) { New-Item -ItemType File -Path $OUTPUT_FILE -Force | Out-Null } - $REST_RESULTS | ConvertTo-Csv -UseQuotes Never -Delimiter '-' -NoHeader | Out-File -FilePath $OUTPUT_FILE + $REST_RESULTS | ConvertTo-Json -Depth 100 | Out-File -FilePath $OUTPUT_FILE Write-Debug "Jira Statuses written to: $OUTPUT_FILE" } - $DUPLICATES = (Get-DuplicateJiraStatusNames -JIRA_STATUSES $REST_RESULTS | Where-Object { $_.duplicate -eq $true } | Sort-Object -Property name) - Write-Debug "Jira Statuses with duplicates: $($DUPLICATES.Count)" - Write-Debug 'Dulplicate list: ' - $DUPLICATES | ForEach-Object { - Write-Debug "$($_.name) - $($_.id) - $($_.duplicate) - $($_.duplicate_ids)" + if ($GetDuplicates) { + $DUPLICATES = (Get-DuplicateJiraStatusNames -JIRA_STATUSES $REST_RESULTS | Where-Object { $_.duplicate -eq $true } | Sort-Object -Property name) + Write-Debug "Jira Statuses with duplicates: $($DUPLICATES.Count)" + $DUPLICATES | ForEach-Object { + Write-Debug "$($_.name) - $($_.id) - $($_.duplicate) - $($_.duplicate_ids)" + } | Out-File -FilePath "$env:OSM_HOME\$env:AtlassianPowerKit_PROFILE_NAME\JIRA\$env:AtlassianPowerKit_PROFILE_NAME-JIRAStatusesDuplicates-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + Write-Debug 'Dulplicate list: ' + } - return $REST_RESULTS + return $REST_RESULTS | ConvertTo-Json -Depth 100 -Compress +} + +function Get-JiraProjectWorkflowSchemes { + param ( + [Parameter(Mandatory = $false)] + [string]$PROJECT_KEY = $false, + [Parameter(Mandatory = $false)] + [string]$OUTPUT_DIR = "$env:OSM_HOME\$env:AtlassianPowerKit_PROFILE_NAME\JIRA" + ) + if ($PROJECT_KEY) { + $OUTPUT_FILE = "$OUTPUT_DIR\$env:AtlassianPowerKit_PROFILE_NAME-$PROJECT_KEY-JIRAProjectWorkflowSchemes-$(Get-Date -Format 'yyyyMMdd-HHmmss').json" + Write-Debug "Project Key passed: $PROJECT_KEY ... getting project ID..." + $PROJECT_OBJECT = Get-JiraProjectByKey -PROJECT_KEY $PROJECT_KEY | ConvertFrom-Json -AsHashtable -NoEnumerate + ConvertTo-Json $PROJECT_OBJECT -Depth 50 | Write-Debug + if ($PROJECT_OBJECT.id) { + $PROJECT_ID = $PROJECT_OBJECT.id + } else { + Write-Error "Project ID not found for project key: $PROJECT_KEY" + } + $URL = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/workflowscheme/project?projectId=$PROJECT_ID" + } else { + Write-Debug 'No project key passed, getting all project workflow schemes...disables' + # $OUTPUT_FILE = "$OUTPUT_DIR\$env:AtlassianPowerKit_PROFILE_NAME-ALL-JIRAProjectWorkflowSchemes-$(Get-Date -Format 'yyyyMMdd-HHmmss').json" + # $URL = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/workflowscheme" + } + $WORKFLOW_SCHEMES = Invoke-RestMethod -Uri $URL -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json' + $WORKFLOW_SCHEMES | ConvertTo-Json -Depth 100 | Out-File -FilePath $OUTPUT_FILE + Write-Debug "Jira Project Workflow Schemes written to: $OUTPUT_FILE" + return $WORKFLOW_SCHEMES | ConvertTo-Json -Depth 100 -Compress } + + # Get-JiraActiveWorkflows function Get-JiraActiveWorkflows { $WORKFLOW_ENDPOINT = "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/workflow/search?isActive=true&expand=statuses" @@ -834,8 +1198,7 @@ function Get-JiraActiveWorkflows { if ($AMIBIGUOUS_FIELDS) { $AMIBIGUOUS_FIELDS = $AMIBIGUOUS_FIELDS -join ', ' $WORKFLOW | Add-Member -MemberType NoteProperty -Name 'AmbiguousDup' -Value $AMIBIGUOUS_FIELDS - } - else { + } else { $WORKFLOW | Add-Member -MemberType NoteProperty -Name 'AmbiguousDup' -Value 'No' } $OUTPUT_FILE = "$env:OSM_HOME\$env:AtlassianPowerKit_PROFILE_NAME\JIRA\$env:AtlassianPowerKit_PROFILE_NAME-JIRAWorkflows-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv" @@ -857,8 +1220,7 @@ function Get-JiraFieldDups { $JIRA_FIELDS | ForEach-Object { if ($JIRA_FIELD_NAMES -contains $_.name) { $DUPLICATE_FIELD_NAMES += $_.name - } - else { + } else { $JIRA_FIELD_NAMES += $_.name } } @@ -885,6 +1247,7 @@ function Get-JiraFields { $CSV_DATA | Out-File -FilePath $OUTPUT_FILE #$REST_RESULTS | ConvertTo-Json -Depth 10 | Out-File -FilePath $OUTPUT_FILE # Write results to a CSV file + Write-Debug "Jira Fields written to: $OUTPUT_FILE" } return $REST_RESULTS @@ -898,8 +1261,7 @@ function Get-DuplicateJiraFieldNames { $JIRA_FIELDS | ForEach-Object { if ($JIRA_FIELD_NAMES -contains $_.name) { $DUPLICATE_FIELD_NAMES += $_.name - } - else { + } else { $JIRA_FIELD_NAMES += $_.name } } @@ -914,8 +1276,7 @@ function Get-JSMServices { $REST_RESULTS = Invoke-RestMethod -Uri $JSM_SERVICES_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json' Write-Debug $REST_RESULTS.getType() Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) - } - catch { + } catch { Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) # This functions name is $MyInvocation.MyCommand.Name $ERROR_MESSAGE = "Error from $($MyInvocation.MyCommand.Name) - $($_.Exception.Message)" @@ -934,111 +1295,66 @@ function Get-JSMService { $REST_RESULTS = Invoke-RestMethod -Uri $JSM_SERVICES_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json' Write-Debug $REST_RESULTS.getType() Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) - } - catch { + } catch { Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) Write-Error "Error updating field: $($_.Exception.Message)" } } - -# Funtion to list project properties (JIRA entities) -function Get-JiraProjectIssuesTypes { +function Get-JiraProjectByKey { param ( [Parameter(Mandatory = $true)] - [string]$JiraCloudProjectKey, - [Parameter(Mandatory = $false)] - [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\JIRA" + [string]$PROJECT_KEY ) - $FILENAME = "$env:AtlassianPowerKit_PROFILE_NAME-$JiraCloudProjectKey-IssueTypes-$(Get-Date -Format 'yyyyMMdd-HHmmss').json" - if (-not (Test-Path $OUTPUT_PATH)) { - New-Item -ItemType Directory -Path $OUTPUT_PATH -Force | Out-Null - } - $OUTPUT_FILE = "$OUTPUT_PATH\$FILENAME" - Write-Debug "Output file: $OUTPUT_FILE" - # Initiate json file with { "Project": "$JiraCloudProjectKey", "JiraIssueTypes": [ - $OUTPUT_FILE_HEADER = "{ `"Project`": `"$JiraCloudProjectKey`", `"JiraIssueTypes`": [" - $OUTPUT_FILE_HEADER | Out-File -FilePath $OUTPUT_FILE - $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issue/createmeta/$JiraCloudProjectKey/issuetypes" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get - Write-Debug $REST_RESULTS.getType() - foreach ($issueType in $REST_RESULTS.issueTypes) { - #Write-Debug "############## Issue Type: $($issueType.name) ##############" - #Write-Debug "Issue Type: $($issueType | Get-Member -MemberType Properties)" - #Write-Debug "Issue Type ID: $($issueType.id)" - $ISSUE_FIELDS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issue/createmeta/$JiraCloudProjectKey/issuetypes/$($issueType.id)" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get - #Write-Debug (ConvertTo-Json $ISSUE_FIELDS -Depth 10) - #Write-Debug '######################################################################' - # Append ConvertTo-Json $ISSUE_FIELDS -Depth 10 to the $OUTPUT_FILE - # Create a JSON object in file to hold the issue type fields - "{""Issue Type"": ""$($issueType.name)"", ""FieldInfo"":" | Out-File -FilePath $OUTPUT_FILE -Append - $ISSUE_FIELDS | ConvertTo-Json -Depth 10 | Out-File -FilePath $OUTPUT_FILE -Append - # Add a comma to the end of the file to separate the issue types - ' - }, ' | Out-File -FilePath $OUTPUT_FILE -Append - } - # Remove the last comma from the file, replace with ]}, ensuring the entire line is written not repeated - $content = Get-Content $OUTPUT_FILE - $content[-1] = $content[-1] -replace ' -}, ', ' -}] }' - $PARSED = $content | ConvertFrom-Json - # Write the content back to the file ensuring JSON formatting is correc - $PARSED | ConvertTo-Json -Depth 30 | Set-Content $OUTPUT_FILE - Write-Debug 'Issue Types found: ' - $PARSED.JiraIssueTypes | ForEach-Object { - $CUSTOM_FIELD_COUNT = ($_.FieldInfo.fields | Where-Object { $_.key -like 'customfield*' }).Count - Write-Debug "$($_.'Issue Type') - Field Count: $($_.'FieldInfo'.total), Custom Field Count: $CUSTOM_FIELD_COUNT" - } - Write-Debug "See Issue Types JSON file created: $OUTPUT_FILE" + # Check for $($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-ProjectList-*.json" that was written in past 12 hours and use it to get the project ID, else run Get-JiraProjectList, then try again + $PROJECT_LIST_FILE = Get-ChildItem -Path "$($env:OSM_HOME)\$env:AtlassianPowerKit_PROFILE_NAME\JIRA" -Filter "$env:AtlassianPowerKit_PROFILE_NAME-ProjectList-*.json" | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 + While (-not $PROJECT_LIST_FILE) { + Write-Debug 'No Project List file found, running Get-JiraProjectList...' + Get-JiraProjectList | Out-Null + $PROJECT_LIST_FILE = Get-ChildItem -Path "$($env:OSM_HOME)\$env:AtlassianPowerKit_PROFILE_NAME\JIRA" -Filter "$env:AtlassianPowerKit_PROFILE_NAME-ProjectList-*.json" | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 + } + $PROJECT = (Get-Content -Path $PROJECT_LIST_FILE.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate) | Where-Object { $_.key -eq $PROJECT_KEY } + return $PROJECT | ConvertTo-Json -Depth 50 -Compress } - -# Function to get issue type metadata for a Jira Cloud project -function Get-JiraCloudIssueTypeMetadata { - param ( - [Parameter(Mandatory = $true)] - [string]$JiraCloudProjectKey - ) - $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issue/createmeta/$JiraCloudProjectKey&expand=projects.issuetypes.fields" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get - Write-Debug $REST_RESULTS.getType() - Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) -} - # Funtion to print the value project properties (JIRA entity) function Get-JiraProjectList { param ( [Parameter(Mandatory = $false)] - [string]$PROJECT_KEY + [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\JIRA" ) + $FILENAME = "$env:AtlassianPowerKit_PROFILE_NAME-ProjectList-$(Get-Date -Format 'yyyyMMdd-HHmmss').json" + $REST_RESULTS = @() $REST_RESPONSE = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/search" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get $REST_RESULTS += $REST_RESPONSE.values + Write-Debug 'Adding first page of projects to results...' while (!$REST_RESPONSE.isLast) { $REST_RESPONSE = Invoke-RestMethod -Uri $REST_RESPONSE.nextPage -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get + Write-Debug "Adding next page of projects to results...[$($REST_RESPONSE.startAt) / $($REST_RESPONSE.total)]" $REST_RESULTS += $REST_RESPONSE.values } - if ($PROJECT_KEY) { - $REST_RESULTS = $REST_RESULTS | Where-Object { $_.key -eq $PROJECT_KEY } - } - return $REST_RESULTS | ConvertTo-Json -Depth 50 + ConvertTo-Json $REST_RESULTS -Depth 50 | Out-File -FilePath "$OUTPUT_PATH\$FILENAME" + Write-Debug "Project List JSON file created: $OUTPUT_PATH\$FILENAME" + return $REST_RESULTS | ConvertTo-Json -Depth 50 -Compress } function Get-JiraProjectProperties { param ( [Parameter(Mandatory = $true)] - [string]$JiraCloudProjectKey + [string]$PROJECT_KEY ) - $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/$JiraCloudProjectKey/properties" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get - Write-Debug $REST_RESULTS.getType() - Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) + $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/$PROJECT_KEY/properties" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get + $REST_RESULTS | ConvertTo-Json -Depth 100 | Out-File -FilePath "$env:OSM_HOME\$env:AtlassianPowerKit_PROFILE_NAME\JIRA\$env:AtlassianPowerKit_PROFILE_NAME-$PROJECT_KEY-ProjectProperties-$(Get-Date -Format 'yyyyMMdd-HHmmss').json" + return $REST_RESULTS | ConvertTo-Json -Depth 100 -Compress } # Funtion to print the value of a specific project property (JIRA entity) function Get-JiraProjectProperty { param ( [Parameter(Mandatory = $true)] - [string]$JiraCloudProjectKey, + [string]$PROJECT_KEY, [Parameter(Mandatory = $true)] [string]$PROPERTY_KEY ) - $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/$JiraCloudProjectKey/properties/$PROPERTY_KEY" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get + $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/$PROJECT_KEY/properties/$PROPERTY_KEY" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get Write-Debug $REST_RESULTS.getType() Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) } @@ -1047,7 +1363,7 @@ function Get-JiraProjectProperty { function Set-JiraProjectProperty { param ( [Parameter(Mandatory = $true)] - [string]$JiraCloudProjectKey, + [string]$PROJECT_KEY, [Parameter(Mandatory = $true)] [string]$PROPERTY_KEY, [Parameter(Mandatory = $true)] @@ -1062,39 +1378,174 @@ function Set-JiraProjectProperty { $content = Get-Content $JSON_FILE # validate the JSON content $content | ConvertFrom-Json | Out-Null - } - catch { + } catch { Write-Debug "File not found or invalid JSON: $JSON_FILE" $content | ConvertFrom-Json | Out-Null return } - $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/$JiraCloudProjectKey/properties/$PROPERTY_KEY" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Put -Body $content -ContentType 'application/json' + $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/$PROJECT_KEY/properties/$PROPERTY_KEY" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Put -Body $content -ContentType 'application/json' Write-Debug $REST_RESULTS.getType() # Write all of the $REST_RESULTS to the console as PSObjects with all properties Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) Write-Debug '###############################################' - Write-Debug "Querying the property to confirm the value was set... $PROPERTY_KEY in $JiraCloudProjectKey via $($env:AtlassianPowerKit_AtlassianAPIEndpoint)" - Get-JiraProjectProperty -JiraCloudProjectKey $JiraCloudProjectKey -PROPERTY_KEY $PROPERTY_KEY + Write-Debug "Querying the property to confirm the value was set... $PROPERTY_KEY in $PROJECT_KEY via $($env:AtlassianPowerKit_AtlassianAPIEndpoint)" + Get-JiraProjectProperty -PROJECT_KEY $PROJECT_KEY -PROPERTY_KEY $PROPERTY_KEY Write-Debug '###############################################' } +function Set-JIRARegisterRefProperty { + param ( + [Parameter(Mandatory = $true)] + [string]$PROJECT_KEY, + [Parameter(Mandatory = $false)] + [string]$FILTER_ID # For JSON files just use Set-JiraProjectProperty -PROJECT_KEY $PROJECT_KEY -PROPERTY_KEY $PROPERTY_KEY -JSON_FILE $OUTPUT_FILE + ) + # Get the Register values + $REGISTER_JSON = Get-JiraCloudJQLQueryResult -JQL_STRING "filter=$FILTER_ID" -RETURN_FIELDS @('summary') -ReturnJSONOnly | ConvertFrom-Json + # Build a JSON property object with the with id: issue.key and value: issue.fields.summary + #Write-Debug "Register JSON: $REGISTER_JSON" + $REGISTER_ARRAY = $REGISTER_JSON | ForEach-Object { + Write-Debug "Register Object: $_" + $REGISTER_OBJECT = @{ + 'id' = $_.key + 'name' = $_.fields.summary + } + $REGISTER_OBJECT + } + Write-Debug "Register Array: $($REGISTER_ARRAY | ConvertTo-Json)" + $OUTPUT_FILE = "$env:OSM_HOME\$env:AtlassianPowerKit_PROFILE_NAME\JIRA\$env:AtlassianPowerKit_PROFILE_NAME-$PROJECT_KEY-$PROPERTY_KEY-Register-$(Get-Date -Format 'yyyyMMdd-HHmmss').json" + $REGISTER_ARRAY | ConvertTo-Json -Compress | Out-File -FilePath $OUTPUT_FILE + Set-JiraProjectProperty -PROJECT_KEY $PROJECT_KEY -PROPERTY_KEY $PROPERTY_KEY -JSON_FILE $OUTPUT_FILE + #Set-JiraProjectProperty -PROJECT_KEY $PROJECT_KEY -PROPERTY_KEY $PROPERTY_KEY -JSON_FILE (ConvertTo-Json $REGISTER_ARRAY -Depth 10) +} + # Funtion to delete a project property (JIRA entity) function Clear-JiraProjectProperty { param ( [Parameter(Mandatory = $true)] - [string]$JiraCloudProjectKey, + [string]$PROJECT_KEY, [Parameter(Mandatory = $true)] [string]$PROPERTY_KEY ) - $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/$JiraCloudProjectKey/properties/$PROPERTY_KEY" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Delete + $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/$PROJECT_KEY/properties/$PROPERTY_KEY" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Delete Write-Debug $REST_RESULTS.getType() Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) Write-Debug '###############################################' - Write-Debug "Querying the propertues to confirm the value was deleted... $PROPERTY_KEY in $JiraCloudProjectKey via $($env:AtlassianPowerKit_AtlassianAPIEndpoint)" - Get-JiraProjectProperties -JiraCloudProjectKey $JiraCloudProjectKey + Write-Debug "Querying the propertues to confirm the value was deleted... $PROPERTY_KEY in $PROJECT_KEY via $($env:AtlassianPowerKit_AtlassianAPIEndpoint)" + Get-JiraProjectProperties -JiraCloudProjectKey $PROJECT_KEY Write-Debug '###############################################' } +function Get-JIRAFieldContextList { + param ( + [Parameter(Mandatory = $true)] + [string]$FIELD_ID + ) + $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)//rest/api/3/field/$FIELD_ID/context" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json' + #Write-Debug $REST_RESULTS.getType() + #Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) + return $REST_RESULTS +} + +function Get-JIRAFieldContextOptionList { + param ( + [Parameter(Mandatory = $true)] + [string]$FIELD_ID, + [Parameter(Mandatory = $true)] + [string]$CONTEXT_ID + + ) + $REQUEST_RESULTS = $() + $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/field/$FIELD_ID/context/$CONTEXT_ID/option" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json' + $REQUEST_RESULTS += $REST_RESULTS.values + while (!$REST_RESULTS.isLast) { + $nextPageStart = $REST_RESULTS.startAt + $REST_RESULTS.maxResults + $APPEND_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/field/$FIELD_ID/context/$CONTEXT_ID/option?startAt=$nextPageStart" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json' + $REST_RESULTS.values += $APPEND_RESULTS.values + $REST_RESULTS.startAt = $APPEND_RESULTS.startAt + $REST_RESULTS.isLast = $APPEND_RESULTS.isLast + $REQUEST_RESULTS += $APPEND_RESULTS.values + } + $REQUEST_RESULTS | ConvertTo-Json -Depth 50 | Write-Debug + Write-Debug '^^^^^ Options found: ' + #ENSURE THE $REQUEST_RESULTS is flattened + return $REQUEST_RESULTS | ConvertTo-Json -Depth 50 -Compress +} + +function Set-JIRAFieldContextOptions { + param ( + [Parameter(Mandatory = $true)] + [string]$FIELD_ID, + [Parameter(Mandatory = $true)] + [string]$CONTEXT_ID, + [Parameter(Mandatory = $true)] + [string]$SOURCE_PROPERTIES_URL + ) + try { + $SOURCE_PROPERTIES = Invoke-RestMethod -Uri $SOURCE_PROPERTIES_URL -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get -ContentType 'application/json' + #Write-Debug $SOURCE_PROPERTIES.getType() + #Write-Debug (ConvertTo-Json $SOURCE_PROPERTIES -Depth 10) + } catch { + Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) + Write-Error "Error updating field: $($_.Exception.Message)" + } + + # Get the current options for the field context + $EXISTING_OPTIONS = Get-JIRAFieldContextOptionList -FIELD_ID $FIELD_ID -CONTEXT_ID $CONTEXT_ID | ConvertFrom-Json -Depth 10 + # ConvertFrom-Json -Depth 40 + #Write-Debug "Existing Options: $($EXISTING_OPTIONS.values.count)" + $RET_ARRAY = @() # Initialize an empty array to store results + + foreach ($HOLDME in $SOURCE_PROPERTIES.value) { + # Check if the option already exists + $EXISTING_OPTION = $EXISTING_OPTIONS | Where-Object { $_.value -eq $HOLDME.name } + + if ($EXISTING_OPTION) { + Write-Debug "Option found: $($HOLDME.name) - Updating it..." + $EXISTING_OPTION | ConvertTo-Json -Depth 10 | Write-Debug + + # Construct the JSON payload for updating + $JSON_PAYLOAD = @{ + 'id' = $EXISTING_OPTION.id + 'value' = $HOLDME.name + 'disabled' = $false + } | ConvertTo-Json -Depth 10 + + $METHOD = 'Put' + } else { + Write-Debug "Option not found: $($HOLDME.name) - Adding it..." + + # Construct the JSON payload for adding + $JSON_PAYLOAD = @{ + 'value' = $HOLDME.name + 'disabled' = $false + } | ConvertTo-Json -Depth 10 + + $METHOD = 'Post' + } + + # Construct the full payload + $FULL_PAYLOAD = "{`"options`": [$JSON_PAYLOAD]}" + + # Make the API request + $REST_RESULTS = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/field/$FIELD_ID/context/$CONTEXT_ID/option" ` + -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) ` + -Method $METHOD ` + -Body $FULL_PAYLOAD ` + -ContentType 'application/json' ` + -ErrorAction Continue + + # Store the result + $RET_ARRAY += $REST_RESULTS + } + + Write-Debug "Options processed for field context: $FIELD_ID - $CONTEXT_ID - ALL DONE!" + + # Return the results as JSON + return $RET_ARRAY | ConvertTo-Json -Depth 10 -Compress + +} + # Function to list all users for a JSM cloud project function Remove-RemoteIssueLink { param ( @@ -1114,8 +1565,7 @@ function Remove-RemoteIssueLink { Write-Debug "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issue/$($_.key)/remotelink?globalId=$GLOBAL_LINK_ID_ENCODED" Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/issue/$($_.key)/remotelink?globalId=$GLOBAL_LINK_ID_ENCODED" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Delete } - } - catch { + } catch { Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) Write-Error "Error updating field: $($_.Exception.Message)" } @@ -1169,16 +1619,13 @@ function Set-IssueLinkTypeByJQL { if ($CONFIRM -ne 'Y') { Write-Warning 'Operation cancelled...' return - } - else { + } else { Write-Warning 'Proceeding !' } - } - else { + } else { Write-Warning "Force flag set, removing all links of type: $CURRNT_LINK_TYPE from the JQL query results: $JQL_STRING" } - } - elseif ((! $CURRNT_LINK_TYPE) -and $NEW_LINK_TYPE_OR_NONE -and $JQL_STRING) { + } elseif ((! $CURRNT_LINK_TYPE) -and $NEW_LINK_TYPE_OR_NONE -and $JQL_STRING) { #JUST CREATE A NEW LINK # Read from user the target issue key (asking for it) if (! $TARGET_ISSUE_KEY) { @@ -1192,12 +1639,10 @@ function Set-IssueLinkTypeByJQL { return } Write-Debug "Creating link type: $NEW_LINK_TYPE_OR_NONE from JQL query results: $JQL_STRING to $TARGET_ISSUE_KEY" - } - else { + } else { Write-Debug "Updating link type: $CURRNT_LINK_TYPE to $NEW_LINK_TYPE_OR_NONE from JQL query results: $JQL_STRING" } - } - else { + } else { Write-Debug 'Issue links for JQL query results can be created, updated or deleted' Write-Debug 'To create a link, required parameters are JQL_STRING, NEW_LINK_TYPE_OR_NONE, LINK_DIRECTION_FOR_JQL and TARGET_ISSUE_KEY' Write-Debug 'To update or remove a link, required parameters are JQL_STRING, CURRNT_LINK_TYPE, NEW_LINK_TYPE_OR_NONE' @@ -1247,8 +1692,7 @@ function Set-IssueLinkTypeByJQL { if (! $LINK_EXISTS) { Write-Debug "Creating new link [type = $NEW_LINK_TYPE_OR_NONE] from $INWARD_ISSUE_KEY to $OUTWARD_ISSUE_KEY" Invoke-RestMethod -Uri $ISSUELINK_ENDPOINT -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Post -Body ($PAYLOAD | ConvertTo-Json -Depth 10) -ContentType 'application/json' - } - else { + } else { Write-Debug 'Link already exists... skipping <---------------------------------------' } } @@ -1261,8 +1705,7 @@ function Set-IssueLinkTypeByJQL { Write-Debug "Removing link: $LINK_ID" try { Invoke-RestMethod -Uri "$ISSUELINK_ENDPOINT/$LINK_ID" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Delete -ContentType 'application/json' - } - catch { + } catch { Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) Write-Error "Error updating field: $($_.Exception.Message)" } @@ -1278,8 +1721,7 @@ function Set-IssueLinkTypeByJQL { if ($LINK_DIRECTION_FOR_JQL -eq 'inward') { $INWARD_ISSUE_KEY = $THIS_ISSUE.key $OUTWARD_ISSUE_KEY = $TARGET_ISSUE_KEY - } - else { + } else { $INWARD_ISSUE_KEY = $TARGET_ISSUE_KEY $OUTWARD_ISSUE_KEY = $THIS_ISSUE.key } @@ -1294,13 +1736,11 @@ function Set-IssueLinkTypeByJQL { try { # First check if the new link type already exists New-JiraIssueLink -LINK_TYPE $NEW_LINK_TYPE_OR_NONE -INWARD_ISSUE_KEY $INWARD_ISSUE_KEY -OUTWARD_ISSUE_KEY $OUTWARD_ISSUE_KEY - } - catch { + } catch { Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) Write-Error "Error updating field: $($_.Exception.Message)" } - } - else { + } else { Write-Debug "Issue Key: $($THIS_ISSUE.key) - Link Type Name: $($_.type.name), no new link type specified, just removing..." } # Write-Debug "New was created: $($NEW_LINK | ConvertTo-Json -Depth 10)" @@ -1309,8 +1749,7 @@ function Set-IssueLinkTypeByJQL { Write-Debug "Removing link: $($CURRNT_LINK_FULL.type.name) [$($CURRNT_LINK_FULL.id)] from $($CURRNT_LINK_FULL.inwardIssue.key) to $($CURRNT_LINK_FULL.outwardIssue.key)" try { Remove-JiraIssueLink -LINK_ID $($CURRNT_LINK_FULL.id) - } - catch { + } catch { Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) Write-Error "Error updating field: $($_.Exception.Message)" } @@ -1331,7 +1770,7 @@ function Add-FormsFromJQLQueryResults { [switch]$InternalOnlyVisible = $false ) # /{issueIdOrKey}/form - $COMBINED_ISSUES_JSON = Get-JiraCloudJQLQueryResult -JQL_STRING $JQL_STRING -RETURN_FIELDS @('id', 'key') -ReturnJSONOnly -IncludeEmptyFields + $COMBINED_ISSUES_JSON = Get-JiraCloudJQLQueryResult -JQL_STRING $JQL_STRING -RETURN_FIELDS @('id', 'key') -ReturnJSONOnly $COMBINED_ISSUES = $COMBINED_ISSUES_JSON | ConvertFrom-Json Write-Debug "JQL Query results: $($COMBINED_ISSUES.Count)" $COMBINED_ISSUES | ForEach-Object { @@ -1348,8 +1787,7 @@ function Add-FormsFromJQLQueryResults { $PAYLOAD | ConvertTo-Json -Depth 10 | Write-Debug try { Invoke-RestMethod -Uri $ISSUE_FORM_ID_URL -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Post -Body ($PAYLOAD | ConvertTo-Json -Depth 10) -ContentType 'application/json' - } - catch { + } catch { Write-Debug "$($MyInvocation.InvocationName) - Failed to attach form ($FORM_ID) to issue: $($ISSUE.key)" Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) Write-Error "Error updating field: $($_.Exception.Message)" @@ -1373,13 +1811,11 @@ function Set-AttachedFormsExternal { try { #https://api.atlassian.com/jira/forms/cloud/{cloudId}/issue/{issueIdOrKey}/form/{formId}/action/external' \ Invoke-RestMethod -Uri "$ISSUE_FORM_ATTACHMENTS_URL/$($ATTACHED_FORM.id)/action/external" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Put - } - catch { + } catch { Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) Write-Error "Error updating field: $($_.Exception.Message)" } - } - else { + } else { Write-Debug "No form found for issue: $($ISSUE.key)" } } @@ -1390,7 +1826,7 @@ function Set-AttachedFormsExternalJQLQuery { [string]$JQL_STRING ) # /{issueIdOrKey}/form - $COMBINED_ISSUES_JSON = Get-JiraCloudJQLQueryResult -JQL_STRING $JQL_STRING -RETURN_FIELDS @('id', 'key') -ReturnJSONOnly -IncludeEmptyFields + $COMBINED_ISSUES_JSON = Get-JiraCloudJQLQueryResult -JQL_STRING $JQL_STRING -RETURN_FIELDS @('id', 'key') -ReturnJSONOnly $COMBINED_ISSUES = $COMBINED_ISSUES_JSON | ConvertFrom-Json Write-Debug "JQL Query results: $($COMBINED_ISSUES.Count)" $COMBINED_ISSUES | ForEach-Object { @@ -1401,13 +1837,30 @@ function Set-AttachedFormsExternalJQLQuery { function Get-FormsForJiraProject { param ( [Parameter(Mandatory = $true)] - [string]$JiraCloudProjectKey + [string]$PROJECT_KEY, + [Parameter(Mandatory = $false)] + [string]$OUTPUT_PATH = "$($env:OSM_HOME)\$($env:AtlassianPowerKit_PROFILE_NAME)\JIRA" ) + $FILENAME = "$env:AtlassianPowerKit_PROFILE_NAME-$PROJECT_KEY-Forms-$(Get-Date -Format 'yyyyMMdd-HHmmss').json" + $REST_RESULTS = @() + $REST_RESPONSE = Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/3/project/search" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get + $REST_RESULTS += $REST_RESPONSE.values + + Write-Debug 'Adding first page of projects to results...' + while (!$REST_RESPONSE.isLast) { + $REST_RESPONSE = Invoke-RestMethod -Uri $REST_RESPONSE.nextPage -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get + Write-Debug "Adding next page of projects to results...[$($REST_RESPONSE.startAt) / $($REST_RESPONSE.total)]" + $REST_RESULTS += $REST_RESPONSE.values + } + ConvertTo-Json $REST_RESULTS -Depth 50 | Out-File -FilePath "$OUTPUT_PATH\$FILENAME" # https://api.atlassian.com/jira/forms/cloud/{cloudId}/project/{projectIdOrKey}/form - $PROJECT_FORM_ID_URL = "https://api.atlassian.com/jira/forms/cloud/$($env:AtlassianPowerKit_CloudID)/project/$JiraCloudProjectKey/form" - $REST_RESULTS = Invoke-RestMethod -Uri $PROJECT_FORM_ID_URL -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get - Write-Debug $REST_RESULTS.getType() - Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) + $PROJECT_FORM_ID_URL = "https://api.atlassian.com/jira/forms/cloud/$($env:AtlassianPowerKit_CloudID)/project/$PROJECT_KEY/form" + $PROJECT_FORM_INDEX = Invoke-RestMethod -Uri $PROJECT_FORM_ID_URL -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get + $PROJECT_FORM_INDEX | ConvertTo-Json -Depth 30 | Out-File -FilePath "$OUTPUT_PATH\$FILENAME" + Write-Debug "Project Form Index JSON file created: $OUTPUT_PATH\$FILENAME" + return $PROJECT_FORM_INDEX | ConvertTo-Json -Depth 50 -Compress + + } function Get-FormsForJiraIssue { param ( @@ -1420,6 +1873,18 @@ function Get-FormsForJiraIssue { $REST_RESULTS | ConvertTo-Json -Depth 10 | Write-Debug } +function Reset-FormsFromJQLQueryResults { + param ( + [Parameter(Mandatory = $true)] + [string]$JQL_STRING, + [Parameter(Mandatory = $true)] + [string]$FORM_ID + ) + Write-Debug "Expecting to reset form: $FORM_ID from JQL query results: $JQL_STRING" + # Get JQL query results + Remove-FormsFromJQLQueryResults -JQL_STRING $JQL_STRING +} + # Function to remove forms from JQL query results function Remove-FormsFromJQLQueryResults { param ( @@ -1429,7 +1894,7 @@ function Remove-FormsFromJQLQueryResults { [switch]$DontReplace = $false ) # /{issueIdOrKey}/form - $COMBINED_ISSUES_JSON = Get-JiraCloudJQLQueryResult -JQL_STRING $JQL_STRING -RETURN_FIELDS @('id', 'key') -ReturnJSONOnly -IncludeEmptyFields + $COMBINED_ISSUES_JSON = Get-JiraCloudJQLQueryResult -JQL_STRING $JQL_STRING -RETURN_FIELDS @('id', 'key') -ReturnJSONOnly $COMBINED_ISSUES = $COMBINED_ISSUES_JSON | ConvertFrom-Json Write-Debug "JQL Query results: $($COMBINED_ISSUES.Count)" $COMBINED_ISSUES | ForEach-Object { @@ -1438,8 +1903,7 @@ function Remove-FormsFromJQLQueryResults { $ATTACHED_FORMS = Invoke-RestMethod -Uri "$ISSUE_FORM_ID_URL" -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Get if ($null -eq $ATTACHED_FORMS -or $ATTACHED_FORMS -eq 0) { Write-Debug "No form found for issue: $($ISSUE.key)" - } - else { + } else { Write-Debug "Issue Key: $($ISSUE.key) - ATTACHED_FORMS to remove: " Write-Debug "ATTACHED FORMS COUNT: $($ATTACHED_FORMS.Count)" $ATTACHED_FORMS | ConvertTo-Json -Depth 10 | Write-Debug @@ -1459,12 +1923,10 @@ function Remove-FormsFromJQLQueryResults { Write-Debug "Re-attaching form ($FORM_TEMPLATE_ID) to issue: $($ISSUE.key)" Invoke-RestMethod -Uri $ISSUE_FORM_ID_URL -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method Post -Body ($PAYLOAD | ConvertTo-Json -Depth 10) -ContentType 'application/json' Set-AttachedFormsExternal -ISSUE_KEY $($ISSUE.key) - } - else { + } else { Write-Debug "Not re-attaching form to issue: $($ISSUE.key)" } - } - catch { + } catch { Write-Debug ($_ | Select-Object -Property * -ExcludeProperty psobject | Out-String) Write-Error "Error updating field: $($_.Exception.Message)" } diff --git a/AtlassianPowerKit-Jira/Get-FixedADFJson.ps1 b/AtlassianPowerKit-Jira/Get-FixedADFJson.ps1 new file mode 100644 index 0000000..30f3ee7 --- /dev/null +++ b/AtlassianPowerKit-Jira/Get-FixedADFJson.ps1 @@ -0,0 +1,46 @@ +param ( + [Parameter(Mandatory = $true)] + [string]$INPUT_FILE_PATH, + [Parameter(Mandatory = $false)] + [switch]$DownloadSchema = $false +) +$ErrorActionPreference = 'Stop'; $DebugPreference = 'Continue' + +$JIRA_ADF_SCHEMA_URL = 'http://go.atlassian.com/adf-json-schema' +$JIRA_ADF_SCHEMA_FILE = "$env:OSM_INSTALL\AtlassianPowerKit\AtlassianPowerKit-Jira\JiraADFJsonSchema.json" + +function Get-SchemaDefinition { + param( + [string]$SchemaUrl + ) + Invoke-RestMethod -Uri $SchemaUrl -OutFile $JIRA_ADF_SCHEMA_FILE +} + +function Test-JSONFileAgainstSchemaFile { + param( + [Parameter(Mandatory = $true)] + [string]$JsonInputFilePath, + [Parameter(Mandatory = $true)] + [string]$SchemaFilePath + ) + Write-Debug "Validating JSON file: $JsonInputFilePath ..." + if (! (Test-Json -Path $JsonInputFilePath)) { + throw "Invalid JSON file: $JsonInputFilePath" + } + Write-Debug "`t ... $JsonInputFilePath is a valid JSON file." + Write-Debug "Validating JSON file against schema file: $SchemaFilePath ..." + if (! (Test-Json -Path $SchemaFilePath)) { + throw "Invalid JSON schema file: $SchemaFilePath" + } + Write-Debug "`t ... $SchemaFilePath is a valid JSON schema file." + Write-Debug 'Validating JSON file against schema file ...' + Test-Json -Path "$JsonInputFilePath" -SchemaFile "$SchemaFilePath" +} + +#################### MAIN #################### +if ($DownloadSchema) { + Get-SchemaDefinition -SchemaUrl $JIRA_ADF_SCHEMA_URL +} +if ($INPUT_FILE_PATH) { + Test-JSONFileAgainstSchemaFile -JsonInputFilePath $INPUT_FILE_PATH -SchemaFilePath $JIRA_ADF_SCHEMA_FILE +} diff --git a/AtlassianPowerKit-Jira/JIRA_ADF_README.md b/AtlassianPowerKit-Jira/JIRA_ADF_README.md new file mode 100644 index 0000000..119bd4e --- /dev/null +++ b/AtlassianPowerKit-Jira/JIRA_ADF_README.md @@ -0,0 +1,22 @@ +# Atlassian ADF + +- Atlassian Document Format (ADF) is a powerful, human-readable document format for representing structured content that is easy to write and to read. It is designed to be both easy to read as a human and easy to parse as a machine. ADF is used in Atlassian products like Confluence and Jira. + +## Key References + +- [ADF Schema Definition](http://go.atlassian.com/adf-json-schema) +- [ADF Structure Doc](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/) +- [ADF Web-Builder](https://developer.atlassian.com/cloud/jira/platform/apis/document/playground/) + +## VSCode JSON Schema Validation + +```json + "json.schemas": [ + { + "fileMatch": [ + "/OP-*.json" + ], + "url": "http://go.atlassian.com/adf-json-schema" + } + ] +``` diff --git a/AtlassianPowerKit-Jira/JiraADFJsonSchema.json b/AtlassianPowerKit-Jira/JiraADFJsonSchema.json new file mode 100644 index 0000000..e69de29 diff --git a/AtlassianPowerKit-Jira/SSG_REQ_FIELDMAP.json b/AtlassianPowerKit-Jira/SSG_REQ_FIELDMAP.json new file mode 100644 index 0000000..cb2935c --- /dev/null +++ b/AtlassianPowerKit-Jira/SSG_REQ_FIELDMAP.json @@ -0,0 +1,5 @@ +'{ + "Applicability Justification": "is caused by", + "Security Requirements": "addressed by", + "Dependencies": "met by" +}' \ No newline at end of file diff --git a/AtlassianPowerKit-Shared/AtlassianPowerKit-Shared.psd1 b/AtlassianPowerKit-Shared/AtlassianPowerKit-Shared.psd1 index ae16d46..bb340ea 100644 --- a/AtlassianPowerKit-Shared/AtlassianPowerKit-Shared.psd1 +++ b/AtlassianPowerKit-Shared/AtlassianPowerKit-Shared.psd1 @@ -74,11 +74,9 @@ 'Clear-AtlassianPowerKitProfileDirs', 'Clear-AtlassianPowerKitVault', 'Get-AtlassianPowerKitProfileList', - 'Get-CurrentAtlassianPowerKitProfile', + 'Get-PaginatedJSONResults', 'Get-LevenshteinDistance', - 'Get-PopulatedTemplate', - 'Get-RequisitePowerKitModules', - 'Register-AtlassianPowerKitProfile', + 'Register-AtlassianPowerKitProfileInVault', 'Set-AtlassianPowerKitProfile', 'Remove-AtlasianPowerKitProfile', 'Unlock-Vault' diff --git a/AtlassianPowerKit-Shared/AtlassianPowerKit-Shared.psm1 b/AtlassianPowerKit-Shared/AtlassianPowerKit-Shared.psm1 index 075be26..62cf510 100644 --- a/AtlassianPowerKit-Shared/AtlassianPowerKit-Shared.psm1 +++ b/AtlassianPowerKit-Shared/AtlassianPowerKit-Shared.psm1 @@ -9,7 +9,7 @@ - Get-AtlassianAPIEndpoint - Get-OpsgenieAPIEndpoint - Clear-AtlassianPowerKitGlobalVariables - - To list all functions in this module, run: Get-Command -Module AtlassianPowerKit-Shared + - To list all functions in this module, run: `Get-Command -Module AtlassianPowerKit-Shared` - Debug output is enabled by default. To disable, set $DisableDebug = $true before running functions. .EXAMPLE @@ -33,9 +33,15 @@ GitHub: https://github.com/markz0r/AtlassianPowerKit #> # Vault path: $env:LOCALAPPDATA\Microsoft\PowerShell\secretmanagement\localstore\ +$script:OSMAtlassianProfilesVaultPath = if ($env:OSMAtlassianProfilesVaultPath) { + $env:OSMAtlassianProfilesVaultPath +} else { + 'op://employee/OSMAtlassianProfiles/notesPlain' +} $ErrorActionPreference = 'Stop'; $DebugPreference = 'Continue' -$VAULT_NAME = 'AtlassianPowerKitProfileVault' -$VAULT_KEY_PATH = 'vault_key.xml' +$RETRY_AFTER = 60 +$ENVAR_PREFIX = 'AtlassianPowerKit_' +$REQUIRED_ENV_VARS = @('AtlassianAPIEndpoint', 'AtlassianAPIUserName', 'AtlassianAPIAuthString', 'PROFILE_NAME') function Clear-AtlassianPowerKitProfile { # Clear all environment variables starting with AtlassianPowerKit_ @@ -61,8 +67,7 @@ function Clear-AtlassianPowerKitProfileDirs { if ($itemsToArchive.Count -eq 0) { Write-Debug "Profile directory $dir. FullName has nothing to archive. Skipping..." - } - else { + } else { # Archiving items Compress-Archive -Path $itemsToArchive.FullName -DestinationPath $ARCHIVE_PATH -Force Write-Debug "Archiving $($dir.BaseName) to $ARCHIVE_NAME in $($dir.FullName)...." @@ -77,50 +82,106 @@ function Clear-AtlassianPowerKitProfileDirs { Write-Debug 'Profile directories cleared.' } -function Clear-AtlassianPowerKitVault { - Unregister-SecretVault -Name $script:VAULT_NAME - Write-Debug "Vault $script:VAULT_NAME cleared." - $VAULT_KEY = Get-VaultKey - $storeConfiguration = @{ - Authentication = 'Password' - Password = $VAULT_KEY - PasswordTimeout = 3600 - Interaction = 'None' - } - Reset-SecretStore @storeConfiguration -Force - Clear-AtlassianPowerKitProfile -} - -# Function to check if a profile is already loaded -function Get-CurrentAtlassianPowerKitProfile { - if ($env:AtlassianPowerKit_PROFILE_NAME -and $env:AtlassianPowerKit_CloudID) { - Write-Debug "Profile $($env:AtlassianPowerKit_PROFILE_NAME) appears loaded..." - #Write-Debug "Profile $($env:AtlassianPowerKit_PROFILE_NAME) is loaded." - return $env:AtlassianPowerKit_PROFILE_NAME +function Get-PaginatedJSONResults { + param ( + [Parameter(Mandatory = $true)] + [string]$URI, + [Parameter(Mandatory = $true)] + [string]$METHOD, + [Parameter(Mandatory = $false)] + [string]$POST_BODY, + [Parameter(Mandatory = $false)] + [string]$RESPONSE_JSON_OBJECT_FILTER_KEY, + [Parameter(Mandatory = $false)] + [string]$API_HEADERS = $env:AtlassianPowerKit_AtlassianAPIHeaders + ) + + function Get-PageResult { + param ( + [Parameter(Mandatory = $true)] + [string]$URI, + [Parameter(Mandatory = $false)] + [string]$METHOD = 'GET', + [Parameter(Mandatory = $false)] + [string]$ONE_POST_BODY + ) + try { + if ($METHOD -eq 'POST') { + $PAGE_RESULTS = Invoke-RestMethod -Uri $URI -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method $METHOD -Body $ONE_POST_BODY -ContentType 'application/json' + } else { + $PAGE_RESULTS = Invoke-RestMethod -Uri $URI -Headers $(ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders) -Method $METHOD -ContentType 'application/json' + #$PAGE_RESULTS | ConvertTo-Json -Depth 100 | Write-Debug + } + } catch { + # Catch 429 errors and wait for the retry-after time + if ($_.Exception.Response.StatusCode -eq 429) { + Write-Warn "429 error, waiting for $RETRY_AFTER seconds..." + Start-Sleep -Seconds $RETRY_AFTER + Get-PageResult -URI $URI -ONE_POST_BODY $ONE_POST_BODY + } else { + Write-Error "Error: $($_.Exception.Message)" + throw 'Get-PageResult failed' + } + } + if ($PAGE_RESULTS.isLast -eq $false) { + # if PAGE_RESULTS has a value for key 'nextPageToken' then set it + #Write-Debug 'More pages to get, getting next page...' + if ($PAGE_RESULTS.nextPageToken) { + #Write-Debug "Next page token: $($PAGE_RESULTS.nextPageToken)" + # Update if the method is POST, update the ONE_POST_BODY with the nextPageToken + if ($METHOD -eq 'POST') { + $ONE_POST_BODY = $ONE_POST_BODY | ConvertFrom-Json + $ONE_POST_BODY.nextPageToken = $PAGE_RESULTS.nextPageToken + #$ONE_POST_BODY = $ONE_POST_BODY | ConvertTo-Json + } else { + $URI = $URI + "&nextPageToken=$($PAGE_RESULTS.nextPageToken)" + } + } elseif ($PAGE_RESULTS.nextPage) { + Write-Debug "Next page: $($PAGE_RESULTS.nextPage)" + if ($METHOD -eq 'POST') { + Write-Error "$($MyInvocation.InvocationName) does not support POST method with nextPage. Exiting..." + } else { + $URI = $PAGE_RESULTS.nextPage + } + } + Get-PageResult -URI $URI -METHOD $METHOD + } + if ($RESPONSE_JSON_OBJECT_FILTER_KEY) { + $PAGE_RESULTS = $PAGE_RESULTS.$RESPONSE_JSON_OBJECT_FILTER_KEY + } + $PAGE_RESULTS } - else { - Write-Debug 'No profile loaded.' - return $false + if ($POST_BODY) { + $RESULTS_ARRAY = Get-PageResult -URI $URI -METHOD $METHOD -ONE_POST_BODY $POST_BODY + } else { + $RESULTS_ARRAY = Get-PageResult -URI $URI -METHOD $METHOD } + Write-Debug "$($MyInvocation.InvocationName) results:" + # $RESULTS_ARRAY | ConvertTo-Json -Depth 100 -Compress | Write-Debug + return $RESULTS_ARRAY | ConvertTo-Json -Depth 100 -Compress } function Get-AtlassianPowerKitProfileList { - Write-Debug 'Getting AtlassianPowerKit Profile List...' - if (!$(Get-SecretVault -Name $script:VAULT_NAME -ErrorAction SilentlyContinue)) { - Register-AtlassianPowerKitVault + $vaultPath = if ($env:OSMAtlassianProfilesVaultPath) { + $env:OSMAtlassianProfilesVaultPath + } else { + 'op://employee/OSMAtlassianProfiles/notesPlain' } - else { - Write-Debug 'Vault already registered, getting profiles...' - unlock-vault -VaultName $script:VAULT_NAME - $PROFILE_LIST = (Get-SecretInfo -Vault $script:VAULT_NAME -Name '*').Name - if ($PROFILE_LIST.Count -eq 0) { - Write-Debug 'No profiles found. Please create a new profile.' - return $false - } + + Write-Debug "Getting Atlassian profiles from vault path: $vaultPath" + + try { + $profileJson = op read $vaultPath + $profileMap = $profileJson | ConvertFrom-Json -ErrorAction Stop + $profileList = $profileMap.PSObject.Properties.Name + } catch { + Write-Warning "❌ Failed to read or parse profiles from 1Password: $_" + Write-Warning "Running 'New-AtlassianPowerKitProfile' to create a new profile." + return @() } - $env:AtlassianPowerKit_PROFILE_LIST_STRING = $PROFILE_LIST - Write-Debug "Profiles found: $($env:AtlassianPowerKit_PROFILE_LIST_STRING)" - return $PROFILE_LIST + + Write-Debug "Found profiles: $($profileList -join ', ')" + return $profileList } function Get-LevenshteinDistance { @@ -150,335 +211,464 @@ function Get-LevenshteinDistance { return $d[$s.Length][$t.Length] } -# Function Update-ContentPlaceholderAll, takes a file path and a hashtable of placeholders and values, returning the content with the placeholders replaced (not updating the file) -function Get-PopulatedTemplate { - param ( - [Parameter(Mandatory = $true)] - [string]$TemplateFilePath, - [Parameter(Mandatory = $false)] - [string[]]$InputJSON, - [Parameter(Mandatory = $false)] - [string[]]$InputConfStorageString - ) - # If neither JSON or ConfStorageString is provided error - if (-not $InputJSON -and -not $InputConfStorageString) { - Write-Debug 'No InputJSON or InputConfStorageString provided to , one is required. Exiting...' - Write-Error 'Get-PopulatedTemplate failed. Exiting...' - return $false - } - $TemplateContent = Get-Content -Path $TemplateFilePath -Raw | ConvertFrom-Json -Depth 20 - foreach ($item in $TemplateContent.placeholderMap) { - foreach ($key in $item.Keys) { - $value = $item[$key] - $valueDataType = $value.GetType().Name - - # Handle different types of values - if ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) { - $value = $value -join ', ' - } - - Write-Output "Key: $key, ValueDataType: $valueDataType, Value: $value" +# Function to set the Atlassian Cloud API headers +function Test-VaultProfileLoaded { + # Check if all of the $REQUIRED_ENV_VARS are set + $PROFILE_LOADED = $true + foreach ($envVar in $REQUIRED_ENV_VARS) { + $envVarNameKey = $ENVAR_PREFIX + $envVar + $ENV_STATE = Get-Item -Path "env:$envVarNameKey" -ErrorAction SilentlyContinue + if (!$ENV_STATE) { + Write-Debug "Profile is missing required environment variable: $envVarNameKey" + $PROFILE_LOADED = $false + break + } else { + #Write-Debug "Found required environment variable: $envVarNameKey already set." + $ENV_STATE = $null } } - return $Content + return $PROFILE_LOADED } -function Get-VaultKey { - if (-not (Test-Path $VAULT_KEY_PATH)) { - Write-Debug 'No vault key file found. Please register a vault first.' - return $false +function Set-AtlassianAPIHeaders { + # check if there is a profile loaded + if (!$(Test-VaultProfileLoaded)) { + Write-Debug "$($MyInvocation.InvocationName) failed. Profile not loaded. Exiting..." + throw "$($MyInvocation.InvocationName) failed. Profile not loaded. Exiting..." + } else { + $HEADERS = @{ + Authorization = "Basic $($env:AtlassianPowerKit_AtlassianAPIAuthString)" + Accept = 'application/json' + } + # Add atlassian headers to the profile data + $API_HEADERS = $HEADERS | ConvertTo-Json -Compress } - $VAULT_KEY = Import-Clixml -Path $VAULT_KEY_PATH - return $VAULT_KEY + Return $API_HEADERS } +function Set-AtlassianPowerKitProfile { + param ( + [Parameter(Mandatory = $false)] + [string]$ProfileName + ) + + $OSMprofile = $null + $runningInDocker = Test-Path -Path '/.dockerenv' -or $env:IN_DOCKER -function Register-AtlassianPowerKitVault { - # Register the secret vault - # Cheking if the vault is already registered - while (-not (Test-Path $VAULT_KEY_PATH)) { - Write-Debug 'No vault key file found. Removing any existing vaults and re-creating...' - Unregister-SecretVault -Name $script:VAULT_NAME -ErrorAction SilentlyContinue - # Create a random secure key to use as the vault key as protected data - $VAULT_KEY = $null - while (-not $VAULT_KEY -or $VAULT_KEY.Length -lt 16) { - $VAULT_KEY = [System.Convert]::ToBase64String([System.Security.Cryptography.RNGCryptoServiceProvider]::GetBytes(32)) - # Convert the key to a secure string and export it to a file - $VAULT_KEY = ConvertTo-SecureString -String $VAULT_KEY -AsPlainText -Force - $VAULT_KEY | Export-Clixml -Path $VAULT_KEY_PATH + # --- OPTION 1: Docker env --- + if ($runningInDocker -and $env:OSMAtlassianProfiles) { + try { + $profileMap = $env:OSMAtlassianProfiles | ConvertFrom-Json -ErrorAction Stop + } catch { + throw '❌ Invalid JSON in OSMAtlassianProfiles environment variable.' } - # Write the vault key to a temporary file - Write-Debug 'Vault key file created successfully.' - } - if (Get-SecretVault -Name $script:VAULT_NAME -ErrorAction SilentlyContinue) { - Write-Debug "Vault $script:VAULT_NAME already exists." - } - else { - Write-Debug "Registering vault $script:VAULT_NAME..." - $VAULT_KEY = Get-VaultKey - $storeConfiguration = @{ - Authentication = 'Password' - Password = $VAULT_KEY - PasswordTimeout = 3600 - Interaction = 'None' + + if (-not $ProfileName) { + $keys = $profileMap.PSObject.Properties.Name + if ($keys.Count -eq 1) { + $ProfileName = $keys[0] + Write-Host "ℹ️ Defaulting to only available profile: '$ProfileName'" + } elseif ($keys.Count -gt 1) { + Write-Host 'Available profiles:' + $keys | ForEach-Object { Write-Host " - $_" } + $ProfileName = Read-Host 'Enter the profile name to load' + } else { + throw '❌ No profiles found in OSMAtlassianProfiles environment variable.' + } } - Set-SecretVaultDefault -ClearDefault - Reset-SecretStore @storeConfiguration -Force - Register-SecretVault -Name $script:VAULT_NAME -ModuleName Microsoft.PowerShell.SecretStore -VaultParameters $storeConfiguration -DefaultVault -AllowClobber - Write-Debug "Vault $script:VAULT_NAME registered successfully." - Write-Debug "Checking if vault $script:VAULT_NAME is the default vault..." - if ((Get-SecretVault | Where-Object IsDefault).Name -ne $script:VAULT_NAME) { - Write-Debug "$script:VAULT_NAME is not the default vault. Setting as default..." - Set-SecretVaultDefault -Name $script:VAULT_NAME + + $OSMprofile = $profileMap.$ProfileName + if (-not $OSMprofile) { + throw "❌ Profile '$ProfileName' not found in Docker OSMAtlassianProfiles." } - #Set-SecretStoreConfiguration @storeConfiguration - #try { - # Set-SecretStorePassword -NewPassword $VAULT_KEY - #} - # catch { - # Write-Debug "Failed to set SecretStorePassword for $script:VAULT_NAME. Please check the vault key file." - # throw "ERROR: Failed to set SecretStorePassword for $script:VAULT_NAME. Please run again, if error continues please raise an issue ..." - # } - Write-Debug "Vault $script:VAULT_NAME configured successfully." - } - # Unlock the vault if it is locked - try { - $VAULT_KEY = Get-VaultKey - Unlock-Vault -VaultName $script:VAULT_NAME - } - catch { - Write-Debug "Failed to unlock vault $script:VAULT_NAME. Please check the vault key file." - Write-Debug "De-registering vault $script:VAULT_NAME... and resetting vault key file." - Unregister-SecretVault -Name $script:VAULT_NAME - Remove-Item -Path $VAULT_KEY_PATH -Force - Write-Debug "Vault $script:VAULT_NAME de-registered and vault key file removed, starting from scratch..." - throw "ERROR: Failed to unlock vault $script:VAULT_NAME. Please run again, if error continues please raise an issue ..." } -} -function Register-AtlassianPowerKitProfile { - param( - [Parameter(Mandatory = $true)] - [string] $ProfileName, - [Parameter(Mandatory = $true)] - [string] $AtlassianAPIEndpoint, - [Parameter(Mandatory = $true)] - [PSCredential] $AtlassianAPICredential, - [Parameter(Mandatory = $false)] - [string] $OpsgenieAPIEndpoint = 'api.opsgenie.com', - [Parameter(Mandatory = $false)] - [switch] $UseOpsgenieAPI = $false, - [Parameter(Mandatory = $false)] - [PSCredential] $OpsgenieAPICredential - ) - if (!$script:REGISTER_VAULT) { - Register-AtlassianPowerKitVault - } - # Function to write profile data to the vault - function Set-AtlassianPowerKitProfileData { - param ( - [Parameter(Mandatory = $true)] - [string] $ProfileName, - [Parameter(Mandatory = $true)] - [hashtable] $ProfileData - ) - Write-Debug "Writing profile data to vault for $ProfileName..." - Unlock-Vault -VaultName $script:VAULT_NAME + # --- OPTION 2: Host env --- + elseif ($env:OSMAtlassianProfiles) { try { - Set-Secret -Name $ProfileName -Secret $ProfileData -Vault $script:VAULT_NAME - } - catch { - Write-Debug "Update of vault failed for $ProfileName." - throw "Update of vault failed for $ProfileName." + $profileMap = $env:OSMAtlassianProfiles | ConvertFrom-Json -ErrorAction Stop + } catch { + throw '❌ Invalid JSON in OSMAtlassianProfiles environment variable.' } - Write-Debug "Vault entruy for $ProfileName updated successfully." - } - # Check if the profile already exists in the secret vault - if ($null -ne $env:AtlassianPowerKit_PROFILE_LIST_STRING -and $env:AtlassianPowerKit_PROFILE_LIST_STRING.Count -gt 0 -and $env:AtlassianPowerKit_PROFILE_LIST_STRING.Contains($ProfileName)) { - Write-Debug "Profile $ProfileName already exists." - # Ask user if they want to overwrite the profile - $overwrite = Read-Host -Prompt "Profile $ProfileName already exists. Do you want to overwrite it? (Y/N)" - if ($overwrite -eq 'Y') { - Write-Debug "Overwriting profile $ProfileName..." + + if (-not $ProfileName) { + $keys = $profileMap.PSObject.Properties.Name + if ($keys.Count -eq 1) { + $ProfileName = $keys[0] + Write-Host "ℹ️ Defaulting to only available profile: '$ProfileName'" + } elseif ($keys.Count -gt 1) { + Write-Host 'Available profiles:' + $keys | ForEach-Object { Write-Host " - $_" } + $ProfileName = Read-Host 'Enter the profile name to load' + } else { + throw '❌ No profiles found in OSMAtlassianProfiles environment variable.' + } } - else { - Write-Debug "Profile $ProfileName already exists." - return $false + + $OSMprofile = $profileMap.$ProfileName + if (-not $OSMprofile) { + throw "❌ Profile '$ProfileName' not found in host OSMAtlassianProfiles env." } } - #Write-Debug "Profile $ProfileName does not exist. Creating..." - Write-Debug "Preparing profile data for $ProfileName..." - $CredPair = "$($AtlassianAPICredential.UserName):$($AtlassianAPICredential.GetNetworkCredential().password)" - Write-Debug "CredPair: $CredPair" - $AtlassianAPIAuthToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($CredPair)) - $ProfileData = @{ - 'PROFILE_NAME' = $ProfileName - 'AtlassianAPIEndpoint' = $AtlassianAPIEndpoint - 'AtlassianAPIUserName' = $AtlassianAPICredential.UserName - 'AtlassianAPIAuthString' = $AtlassianAPIAuthToken - } - Write-Debug "Creating profile $ProfileName in $script:VAULT_NAME..." - Set-Secret -Name $ProfileName -Secret $ProfileData -Vault $script:VAULT_NAME - Write-Debug "Profile $ProfileName created successfully in $script:VAULT_NAME." - Write-Debug 'Clearing existing profiles selection...' - Clear-AtlassianPowerKitProfile - $LOADED_PROFILE = Set-AtlassianPowerKitProfile -SelectedProfileName $ProfileName - return $LOADED_PROFILE -} -# Function to set the Atlassian Cloud API headers -function Set-AtlassianAPIHeaders { - # check if there is a profile loaded - if (!$env:AtlassianPowerKit_PROFILE_NAME) { - Write-Debug 'No profile loaded. Please load a profile first.' - return $false - } + # --- OPTION 3: Host with 1Password fallback --- else { - Write-Debug "Profile $ProfileName loaded. Setting API headers and AtlassianAPIEndpoint..." - $HEADERS = @{ - Authorization = "Basic $($env:AtlassianPowerKit_AtlassianAPIAuthString)" - Accept = 'application/json' + try { + $vaultPath = if ($env:OSMAtlassianProfilesVaultPath) { + $env:OSMAtlassianProfilesVaultPath + } else { + 'op://employee/OSMAtlassianProfiles/notesPlain' + } + + $profileJson = op read $vaultPath + $profileMap = $profileJson | ConvertFrom-Json -ErrorAction Stop + + if (-not $ProfileName) { + $keys = $profileMap.PSObject.Properties.Name + if ($keys.Count -eq 1) { + $ProfileName = $keys[0] + Write-Host "ℹ️ Defaulting to only available profile: '$ProfileName'" + } elseif ($keys.Count -gt 1) { + Write-Host 'Available profiles:' + $keys | ForEach-Object { Write-Host " - $_" } + $ProfileName = Read-Host 'Enter the profile name to load' + } else { + throw '❌ No profiles found in 1Password store.' + } + } + + $OSMprofile = $profileMap.$ProfileName + if (-not $OSMprofile) { + throw "❌ Profile '$ProfileName' not found in 1Password." + } + + } catch { + throw "❌ Failed to read profile from 1Password: $_" } - # Add atlassian headers to the profile data - $env:AtlassianPowerKit_AtlassianAPIHeaders = $HEADERS | ConvertTo-Json - Write-Debug "Atlassian headers set for $($env:AtlassianPowerKit_PROFILE_NAME)." - Write-Debug "Headers are: $($env:AtlassianPowerKit_AtlassianAPIHeaders)" - Test-AtlassianPowerKitProfile - Write-Debug "Atlassian headers set and tested successfully for $($env:AtlassianPowerKit_PROFILE_NAME)." } + + # --- Apply profile values to ENV vars --- + $env:AtlassianPowerKit_PROFILE_NAME = $ProfileName + $env:AtlassianPowerKit_AtlassianAPIEndpoint = $OSMprofile.OSMAtlassianEndpoint + $env:AtlassianPowerKit_AtlassianAPIUserName = $OSMprofile.OSMAtlassianUsername + $env:AtlassianPowerKit_AtlassianAPIAuthString = $OSMprofile.OSMAtlassianAPIKey + $env:AtlassianPowerKit_AtlassianAPIHeaders = Set-AtlassianAPIHeaders + $env:AtlassianPowerKit_CloudID = $(Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/_edge/tenant_info").cloudId + + Write-Host "✅ Loaded profile '$ProfileName'." + return Get-Item -Path "env:$ENVAR_PREFIX*" } + function Set-AtlassianPowerKitProfile { param ( [Parameter(Mandatory = $true)] - [string]$SelectedProfileName + [string]$ProfileName ) - Write-Debug "Set-AtlassianPowerKitProfile - with: $SelectedProfileName ..." - # Load all profiles from the secret vault - if (!$(Get-SecretVault -Name $script:VAULT_NAME -ErrorAction SilentlyContinue)) { - Register-AtlassianPowerKitVault + + $OSMprofile = $null + $runningInDocker = Test-Path -Path '/.dockerenv' -or $env:IN_DOCKER + + # --- OPTION 1: Docker --- + if ($runningInDocker -and $env:OSMAtlassianProfiles) { + try { + $profileMap = $env:OSMAtlassianProfiles | ConvertFrom-Json -ErrorAction Stop + $OSMprofile = $profileMap.$ProfileName + } catch { + throw "Invalid JSON in OSMAtlassianProfiles env var: $_" + } + if (-not $profile) { + throw "Profile '$ProfileName' not found in Docker env OSMAtlassianProfiles." + } } - # Check if the profile exists - $PROFILE_LIST = Get-AtlassianPowerKitProfileList - if (!$PROFILE_LIST.Contains($SelectedProfileName)) { - Write-Debug "Profile $SelectedProfileName does not exists in the vault - we have: $PROFILE_LIST" - return $false + + # --- OPTION 2: Host with OSMAtlassianProfiles env --- + elseif ($env:OSMAtlassianProfiles) { + try { + $profileMap = $env:OSMAtlassianProfiles | ConvertFrom-Json -ErrorAction Stop + $OSMprofile = $profileMap.$ProfileName + } catch { + throw "Invalid JSON in OSMAtlassianProfiles env var: $_" + } + if (-not $OSMprofile) { + throw "Profile '$ProfileName' not found in host OSMAtlassianProfiles env." + } } + + # --- OPTION 3: Host with 1Password fallback --- else { - Write-Debug "Profile $SelectedProfileName exists in the vault, loading..." try { - # if vault is locked, unlock it - Unlock-Vault -VaultName $script:VAULT_NAME - $PROFILE_DATA = (Get-Secret -Name $SelectedProfileName -Vault $script:VAULT_NAME -AsPlainText) - #Create environment variables for each item in the profile data - $PROFILE_DATA.GetEnumerator() | ForEach-Object { - Write-Debug "Setting environment variable: $($_.Key) = $($_.Value)" - # Create environment variable concatenated with AtlassianPowerKit_ prefix - $SetEnvar = '$env:AtlassianPowerKit_' + $_.Key + " = `"$($_.Value)`"" - Invoke-Expression -Command $SetEnvar | Out-Null - Write-Debug "Environment variable set: $SetEnvar" + Write-Host "🔐 Loading Atlassian profile '$ProfileName' from 1Password..." + $opItemJson = op read "op://OSM/AtlassianProfile_$ProfileName/json" + $OSMprofile = $opItemJson | ConvertFrom-Json + } catch { + Write-Warning "⚠️ 1Password item for '$ProfileName' not found." + $confirm = Read-Host 'Do you want to create it now in 1Password? (Y/N)' + if ($confirm -ne 'Y') { throw 'Cannot proceed without valid profile.' } + + $endpoint = Read-Host 'Enter Atlassian Endpoint (e.g. example.atlassian.net)' + $username = Read-Host 'Enter Atlassian Username (email)' + $apiKey = Read-Host 'Enter Atlassian API Key (base64 encoded)' + + $newProfile = @{ + OSMAtlassianEndpoint = $endpoint + OSMAtlassianUsername = $username + OSMAtlassianAPIKey = $apiKey } - } - catch { - Write-Debug "Failed to load profile $SelectedProfileName. Please check the vault key file." - throw "Failed to load profile $SelectedProfileName. Please check the vault key file." + + # Save to 1Password + $tempFile = [System.IO.Path]::GetTempFileName() + $newProfile | ConvertTo-Json -Depth 3 | Set-Content $tempFile + & op item create --category 'Secure Note' --title "AtlassianProfile_$ProfileName" -InputFile $tempFile | Out-Null + Remove-Item $tempFile + + Write-Host "✅ Profile saved to 1Password as AtlassianProfile_$ProfileName. Please re-run the command." + return } - Set-AtlassianAPIHeaders - #Set-OpsgenieAPIHeaders - $env:AtlassianPowerKit_CloudID = $(Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/_edge/tenant_info").cloudId - Write-Debug "Profile $SelectedProfileName loaded successfully." } - $LOADED_PROFILE = Get-CurrentAtlassianPowerKitProfile - return $LOADED_PROFILE + + # --- Set ENVARS --- + $env:AtlassianPowerKit_PROFILE_NAME = $ProfileName + $env:AtlassianPowerKit_AtlassianAPIEndpoint = $profile.OSMAtlassianEndpoint + $env:AtlassianPowerKit_AtlassianAPIUserName = $profile.OSMAtlassianUsername + $env:AtlassianPowerKit_AtlassianAPIAuthString = $profile.OSMAtlassianAPIKey + $env:AtlassianPowerKit_AtlassianAPIHeaders = Set-AtlassianAPIHeaders + $env:AtlassianPowerKit_CloudID = $(Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/_edge/tenant_info").cloudId + + return Get-Item -Path "env:$ENVAR_PREFIX*" } +function New-AtlassianPowerKitProfile { + param ( + [Parameter(Mandatory = $false)] + [string]$opEntry = 'OSMAtlassianProfiles' + ) + $profileName = Read-Host 'Enter a unique Profile Name' + $endpoint = Read-Host 'Enter Atlassian Endpoint (e.g. example.atlassian.net)' + $username = Read-Host 'Enter Atlassian Username (email)' + $apiKeySecure = Read-Host 'Enter Atlassian API Key' -AsSecureString + + # Convert SecureString to plain text + $apiKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( + [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($apiKeySecure) + ) + + # Build temporary env for testing + $env:AtlassianPowerKit_PROFILE_NAME = $profileName + $env:AtlassianPowerKit_AtlassianAPIEndpoint = $endpoint + $env:AtlassianPowerKit_AtlassianAPIUserName = $username + $env:AtlassianPowerKit_AtlassianAPIAuthString = $apiKey + $env:AtlassianPowerKit_AtlassianAPIHeaders = Set-AtlassianAPIHeaders -# Function to test if AtlassianPowerKit profile authenticates successfully -function Test-AtlassianPowerKitProfile { - Write-Debug 'Testing Atlassian Cloud PowerKit Profile...' - #Write-Debug "API Headers: $($script:AtlassianAPIHeaders | Format-List * | Out-String)" - Write-Debug "API Endpoint: $($env:AtlassianPowerKit_AtlassianAPIEndpoint) ..." - Write-Debug "API Headers: $($env:AtlassianPowerKit_AtlassianAPIHeaders) ..." - $HEADERS = ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders - $TEST_ENDPOINT = 'https://' + $env:AtlassianPowerKit_AtlassianAPIEndpoint + '/rest/api/2/myself' try { - Write-Debug "Running: Invoke-RestMethod -Uri https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/2/myself -Headers $($env:AtlassianPowerKit_AtlassianAPIHeaders | ConvertFrom-Json -AsHashtable) -Method Get" - Invoke-RestMethod -Method Get -Uri $TEST_ENDPOINT -Headers $HEADERS | Write-Debug - #Write-Debug "Results: $($REST_RESULTS | ConvertTo-Json -Depth 10) ..." - Write-Debug 'Donennnne' - } - catch { - Write-Debug "Error: $_ ..." - throw 'Atlassian Cloud API Auth test failed.' + $cloudId = $(Invoke-RestMethod -Uri "https://$endpoint/_edge/tenant_info").cloudId + $env:AtlassianPowerKit_CloudID = $cloudId + Test-AtlassianPowerKitProfile | Out-Null + Write-Host "`n✅ Profile connection test succeeded." + } catch { + Write-Warning "`n❌ Connection test failed: $_" + return } - Write-Debug "Atlassian Cloud Auth test returned: $($REST_RESULTS.displayName) --- OK!" - # Test Opsgenie API if profile uses Opsgenie API - if ($env:AtlassianPowerKit_UseOpsgenieAPI) { - try { - Invoke-RestMethod -Uri "https://$($env:AtlassianPowerKit_OpsgenieAPIEndpoint)/v1/services?limit=1" -Headers $($env:AtlassianPowerKit_OpsgenieAPIHeaders | ConvertFrom-Json -AsHashtable) -Method Get - #Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) + try { + $existingJson = op read "op://OSM/$opEntry/notesPlain" + $profileMap = $existingJson | ConvertFrom-Json -ErrorAction Stop + if ($profileMap.$profileName) { + $overwrite = Read-Host "Profile '$profileName' already exists. Overwrite? (Y/N)" + if ($overwrite -ne 'Y') { + Write-Host '❌ Aborted.' + return + } } - catch { - Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__ - Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription - throw 'Opsgenie API Auth test failed.' + } catch { + Write-Host "🔐 1Password item '$opEntry' does not exist. Creating new profile store." + $profileMap = @{} + } + + # Add/overwrite profile entry + $profileMap.$profileName = @{ + OSMAtlassianEndpoint = $endpoint + OSMAtlassianUsername = $username + OSMAtlassianAPIKey = $apiKey + } + + # Write back to 1Password + $tempPath = [System.IO.Path]::GetTempFileName() + $profileMap | ConvertTo-Json -Depth 10 | Set-Content -Path $tempPath + + try { + if ($existingJson) { + & op item edit "$opEntry" notesPlain="$(Get-Content $tempPath -Raw)" | Out-Null + Write-Host "`n✅ Updated existing 1Password item '$opEntry'." + } else { + & op item create --category 'Secure Note' --title "$opEntry" notesPlain="$(Get-Content $tempPath -Raw)" | Out-Null + Write-Host "`n✅ Created new 1Password item '$opEntry'." } - Write-Debug 'Opsgenie Auth test --- OK!' + } finally { + Remove-Item -Path $tempPath -Force } } -function Unlock-Vault { +function Edit-AtlassianPowerKitProfile { param ( - [Parameter(Mandatory = $true)] - [string]$VaultName + [Parameter(Mandatory = $false)] + [string]$ProfileName, + [Parameter(Mandatory = $false)] + [string]$opEntry = 'OSMAtlassianProfiles' ) + + # Load profiles try { - if ((Get-SecretVault | Where-Object IsDefault).Name -ne $script:VAULT_NAME) { - Write-Debug "$script:VAULT_NAME is not the default vault. Setting as default..." - Set-SecretVaultDefault -Name $script:VAULT_NAME + $rawJson = op read "op://employee/$opEntry/notesPlain" + $profileMap = $rawJson | ConvertFrom-Json -ErrorAction Stop + } catch { + throw "❌ Failed to read or parse 1Password item '$opEntry'." + } + + # Default selection logic + if (-not $ProfileName) { + $keys = $profileMap.PSObject.Properties.Name + if ($keys.Count -eq 1) { + $ProfileName = $keys[0] + Write-Host "ℹ️ Defaulting to only available profile: '$ProfileName'" + } elseif ($keys.Count -gt 1) { + Write-Host 'Available profiles:' + $keys | ForEach-Object { Write-Host " - $_" } + $ProfileName = Read-Host 'Enter the profile name to edit' + } else { + throw "❌ No profiles found in $opEntry." } - # Attempt to get a non-existent secret. If the vault is locked, this will throw an error. - $VAULT_KEY = Get-VaultKey - Unlock-SecretStore -Password $VAULT_KEY } - catch { - # If an error is thrown, the vault is locked. - Write-Debug "Unlock-Vault failed: $_ ..." - throw 'Unlock-Vault failed Exiting' + + if (-not $profileMap.ContainsKey($ProfileName)) { + throw "❌ Profile '$ProfileName' not found." } - # If no error is thrown, the vault is unlocked. - Write-Debug 'Vault is unlocked.' -} -# Function to update the vault with the new profile data -function Update-AtlassianPowerKitVault { + $OSMProfileData = $profileMap.$ProfileName + + Write-Host "`nEditing profile '$ProfileName'..." + Write-Host 'Select field to update:' + Write-Host '1) Endpoint' + Write-Host '2) Username' + Write-Host '3) API Key' + + $choice = Read-Host 'Enter 1, 2, or 3' + switch ($choice) { + '1' { + $newValue = Read-Host 'Enter new Atlassian Endpoint' + $OSMProfileData.OSMAtlassianEndpoint = $newValue + } + '2' { + $newValue = Read-Host 'Enter new Atlassian Username' + $OSMProfileData.OSMAtlassianUsername = $newValue + } + '3' { + $newSecure = Read-Host 'Enter new Atlassian API Key' -AsSecureString + $newPlain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( + [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newSecure) + ) + $OSMProfileData.OSMAtlassianAPIKey = $newPlain + } + default { + Write-Host '❌ Invalid selection. Aborting.' + return + } + } + + # Save updated profile back + $profileMap.$ProfileName = $OSMProfileData + $tempPath = [System.IO.Path]::GetTempFileName() + $profileMap | ConvertTo-Json -Depth 10 | Set-Content -Path $tempPath + + try { + & op item edit "$opEntry" notesPlain="$(Get-Content $tempPath -Raw)" | Out-Null + Write-Host "`n✅ Profile '$ProfileName' updated successfully." + } finally { + Remove-Item $tempPath -Force + } +} +function Export-AtlassianPowerKitProfilesForDocker { param ( - [Parameter(Mandatory = $true)] - [string]$ProfileName, - [Parameter(Mandatory = $true)] - [hashtable]$ProfileData + [Parameter(Mandatory = $false)] + [string]$opEntry = 'OSMAtlassianProfiles', + [Parameter(Mandatory = $false)] + [switch]$AsShellExport ) - Write-Debug "Writing profile data to vault for $ProfileName..." - Unlock-Vault -VaultName $script:VAULT_NAME + try { - Set-Secret -Name $ProfileName -Secret $ProfileData -Vault $script:VAULT_NAME - } - catch { - Write-Debug "Update of vault failed for $ProfileName." - throw "Update of vault failed for $ProfileName." + $rawJson = op read "op://OSM/$opEntry/notesPlain" + $parsed = $rawJson | ConvertFrom-Json -ErrorAction Stop + } catch { + throw "Failed to read or parse 1Password item '$opEntry'." + } + + $compactJson = $parsed | ConvertTo-Json -Depth 10 -Compress + + if ($AsShellExport) { + Write-Output "export OSMAtlassianProfiles='$compactJson'" + } else { + return $compactJson } - Write-Debug "Vault entruy for $ProfileName updated successfully." } -function Remove-AtlasianPowerKitProfile { +# Function to test if AtlassianPowerKit profile authenticates successfully +function Test-AtlassianPowerKitProfile { + Write-Debug 'Testing Atlassian Cloud PowerKit Profile...' + ##Write-Debug "API Headers: $($script:AtlassianAPIHeaders | Format-List * | Out-String)' + #Write-Debug "API Endpoint: $($env:AtlassianPowerKit_AtlassianAPIEndpoint) ..." + #Write-Debug "API Headers: $($env:AtlassianPowerKit_AtlassianAPIHeaders) ..." + $HEADERS = ConvertFrom-Json -AsHashtable $env:AtlassianPowerKit_AtlassianAPIHeaders + $TEST_ENDPOINT = 'https://' + $env:AtlassianPowerKit_AtlassianAPIEndpoint + '/rest/api/2/myself' + try { + #Write-Debug "Running: Invoke-RestMethod -Uri https://$($env:AtlassianPowerKit_AtlassianAPIEndpoint)/rest/api/2/myself -Headers $($env:AtlassianPowerKit_AtlassianAPIHeaders | ConvertFrom-Json -AsHashtable) -Method Get" + $REST_RESPONSE = Invoke-RestMethod -Method Get -Uri $TEST_ENDPOINT -Headers $HEADERS -StatusCodeVariable REST_STATUS + #Write-Debug "Results: $($REST_RESULTS | ConvertTo-Json -Depth 10) ..." + Write-Debug "$($MyInvocation.InvocationName) returned status code: $($REST_STATUS)" + } catch { + Write-Debug "$($MyInvocation.InvocationName) failed: with $_" + Write-Debug 'Rest response: ' + $REST_RESPONSE | ConvertTo-Json -Depth 10 | Write-Debug + throw "$($MyInvocation.InvocationName) failed. Exiting..." + } + Return $true +} + +function Remove-AtlassianPowerKitProfile { param ( [Parameter(Mandatory = $true)] - [string]$ProfileName + [string]$ProfileName, + [Parameter(Mandatory = $false)] + [string]$opEntry = 'OSMAtlassianProfiles' ) - Write-Debug "Removing profile $ProfileName..." - if ($ProfileName -eq $env:AtlassianPowerKit_PROFILE_NAME) { - Clear-AtlassianPowerKitProfile + + try { + $rawJson = op read "op://employee/$opEntry/notesPlain" + $profileMap = $rawJson | ConvertFrom-Json -ErrorAction Stop + } catch { + throw "❌ Failed to read 1Password item '$opEntry'." } - Remove-Secret -Name $ProfileName -Vault $script:VAULT_NAME - Write-Debug "Profile $ProfileName removed." -} \ No newline at end of file + + if (-not $profileMap.ContainsKey($ProfileName)) { + Write-Host "⚠️ Profile '$ProfileName' not found. Nothing to remove." + return + } + + $confirm = Read-Host "Are you sure you want to delete profile '$ProfileName'? (Y/N)" + if ($confirm -ne 'Y') { + Write-Host '❌ Deletion aborted.' + return + } + + $profileMap.Remove($ProfileName) + + # Write back updated JSON + $tempPath = [System.IO.Path]::GetTempFileName() + $profileMap | ConvertTo-Json -Depth 10 | Set-Content -Path $tempPath + + try { + & op item edit "$opEntry" notesPlain="$(Get-Content $tempPath -Raw)" | Out-Null + Write-Host "`n✅ Profile '$ProfileName' removed from '$opEntry'." + } finally { + Remove-Item $tempPath -Force + } +} + diff --git a/AtlassianPowerKit-UsersAndGroups/AtlassianPowerKit-UsersAndGroups.psm1 b/AtlassianPowerKit-UsersAndGroups/AtlassianPowerKit-UsersAndGroups.psm1 index 21ef748..8816dc0 100644 --- a/AtlassianPowerKit-UsersAndGroups/AtlassianPowerKit-UsersAndGroups.psm1 +++ b/AtlassianPowerKit-UsersAndGroups/AtlassianPowerKit-UsersAndGroups.psm1 @@ -1,32 +1,32 @@ <# .SYNOPSIS - Atlassian Cloud PowerShell Module - Users and Groups - for handy functions to interact with Attlassian Cloud APIs. +Atlassian Cloud PowerShell Module - Users and Groups - for handy functions to interact with Attlassian Cloud APIs. .DESCRIPTION - Atlassian Cloud PowerShell Module - Users and Groups - - Dependencies: AtlassianPowerKit-Shared - - New-AtlassianAPIEndpoint - - Users and Groups Module Functions - - Get-AtlassianGroupMembers - - Get-AtlassianUser - - Show-JiraCloudJSMProjectRole - - To list all functions in this module, run: Get-Command -Module AtlassianPowerKit-UsersAndGroups - - Debug output is enabled by default. To disable, set $DisableDebug = $true before running functions. +Atlassian Cloud PowerShell Module - Users and Groups +- Dependencies: AtlassianPowerKit-Shared +- New-AtlassianAPIEndpoint +- Users and Groups Module Functions +- Get-AtlassianGroupMembers +- Get-AtlassianUser +- Show-JiraCloudJSMProjectRole +- To list all functions in this module, run: `Get-Command` -Module AtlassianPowerKit-UsersAndGroups +- Debug output is enabled by default. To disable, set $DisableDebug = $true before running functions. .EXAMPLE - Get-AtlassianGroupMembers -GROUP_NAME 'jira-administrators' +Get-AtlassianGroupMembers -GROUP_NAME 'jira-administrators' - This example gets all members of the 'jira-administrators' group. +This example gets all members of the 'jira-administrators' group. .EXAMPLE - Get-AtlassianUser -ACCOUNT_ID '5f7b7f7d7f7f7f7f7f7f7f7f7' +Get-AtlassianUser -ACCOUNT_ID '5f7b7f7d7f7f7f7f7f7f7f7f7' - This example gets the user details for the account ID '5f7b7f7d7f7f7f7f7f7f7f7f7'. +This example gets the user details for the account ID '5f7b7f7d7f7f7f7f7f7f7f7f7'. .EXAMPLE - Show-JiraCloudJSMProjectRole -JiraCloudJSMProjectKey 'OSM' +Show-JiraCloudJSMProjectRole -JiraCloudJSMProjectKey 'OSM' - This example gets all roles for the Jira Service Management (JSM) project with the key 'OSM'. +This example gets all roles for the Jira Service Management (JSM) project with the key 'OSM'. .LINK @@ -45,8 +45,7 @@ function Get-AtlassianGroupMembersBulk { $MEMBER_ENTRY_ARRAY = Get-AtlassianGroupMembers -GROUP_NAME $_.Key if ((!$MEMBER_ENTRY_ARRAY) -or $MEMBER_ENTRY_ARRAY.Count -eq 0) { Write-Output "No members found in group $($_.Key)" - } - else { + } else { Write-Debug "MEMBER_ENTRY_ARRAY TYPE = $($MEMBER_ENTRY_ARRAY.getType()), COUNT = $($MEMBER_ENTRY_ARRAY.Count)" $MEMBERS_LIST.add($_.Key, $MEMBER_ENTRY_ARRAY) } @@ -85,8 +84,7 @@ function Get-AtlassianGroups { try { $REST_RESULTS = Invoke-RestMethod -Uri $GROUPS_ENDPOINT -Headers $GROUP_ENDPOINT_HEADERS -Method Get -ContentType 'application/json' #Write-Debug $REST_RESULTS.getType() - } - catch { + } catch { Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__ Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription } @@ -112,8 +110,7 @@ function Get-AtlassianGroupMembers { Write-Debug "Group Members Endpoint: $GROUP_MEMBERS_ENDPOINT" try { $REST_RESULTS = Invoke-RestMethod -Uri $GROUP_MEMBERS_ENDPOINT -Headers $HEADERS -Method Get -ContentType 'application/json' - } - catch { + } catch { # If rate limiting, sleep for 20 seconds then retry if ($_.Exception.Response.StatusCode.value__ -eq 429) { Write-Output 'Rate limited. Sleeping for 20 seconds then retrying.' @@ -129,8 +126,7 @@ function Get-AtlassianGroupMembers { # Build an array of hashtables with the values, handle null values and no members if ($REST_RESULTS.total -eq 0) { Write-Output "No members found in group $GROUP_NAME" - } - else { + } else { Write-Debug "REST_RESULTS TYPE = $($REST_RESULTS.getType())" Write-Debug "REST_RESULTS COUNT = $($REST_RESULTS.Count)" # Build an array of hashtables with the values handle null values @@ -155,8 +151,7 @@ function Get-AllAtlassianUsers { Write-Debug "Headers: $HEADERS" try { $REST_RESULTS = Invoke-RestMethod -Uri $USERS_ENDPOINT -Headers $HEADERS -Method Get -ContentType 'application/json' - } - catch { + } catch { Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__ Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription } @@ -166,8 +161,7 @@ function Get-AllAtlassianUsers { # Build an array of hashtables with the values, handle null values and no members if ($REST_RESULTS.total -eq 0) { Write-Output 'No users found' - } - else { + } else { # Build an array of hashtables with the values handle null values $REST_RESULTS | ForEach-Object { $USERS_HASH_ARRAY += [PSCustomObject] @{ @@ -200,8 +194,7 @@ function Get-AtlassianUser { $REST_RESULTS = Invoke-RestMethod -Uri $USER_ENDPOINT -Headers $script:AtlassianAPIHeaders -Method Get -ContentType 'application/json' Write-Debug $REST_RESULTS.getType() Write-Debug (ConvertTo-Json $REST_RESULTS -Depth 10) - } - catch { + } catch { Write-Debug 'StatusCode:' $_.Exception.Response.StatusCode.value__ Write-Debug 'StatusDescription:' $_.Exception.Response.StatusDescription } diff --git a/AtlassianPowerKit.psm1 b/AtlassianPowerKit.psm1 index be9064f..576d0c9 100644 --- a/AtlassianPowerKit.psm1 +++ b/AtlassianPowerKit.psm1 @@ -1,25 +1,3 @@ -<# -.SYNOPSIS - Atlassian Cloud PowerKit module for interacting with Atlassian Cloud REST API. -.DESCRIPTION - Atlassian Cloud PowerKit module for interacting with Atlassian Cloud REST API. - - Dependencies: AtlassianPowerKit-Shared - - Functions: - - Use-AtlassianPowerKit: Interactive function to run any function in the module. - - Debug output is enabled by default. To disable, set $DisableDebug = $true before running functions. -.EXAMPLE - Use-AtlassianPowerKit - This example lists all functions in the AtlassianPowerKit module. -.EXAMPLE - Use-AtlassianPowerKit - Simply run the function to see a list of all functions in the module and nested modules. -.EXAMPLE - Get-DefinedPowerKitVariables - This example lists all variables defined in the AtlassianPowerKit module. -.LINK - GitHub: - -#> $ErrorActionPreference = 'Stop'; $DebugPreference = 'Continue' function Get-RequisitePowerKitModules { $AtlassianPowerKitRequiredModules = @('PowerShellGet', 'Microsoft.PowerShell.SecretManagement', 'Microsoft.PowerShell.SecretStore') @@ -31,8 +9,7 @@ function Get-RequisitePowerKitModules { Write-Debug "Module $_ not found. Installing..." Install-Module -Name $_ -Force -Scope CurrentUser | Write-Debug } - } - catch { + } catch { Write-Error "Module $_ not found and installation failed. Exiting." throw "Dependency module $_ unanable to install, try manual install, Exiting for now." } @@ -44,23 +21,23 @@ function Get-RequisitePowerKitModules { function Import-NestedModules { param ( [Parameter(Mandatory = $true)] - [string[]] $NESTED_MODULES + [string[]]$NESTED_MODULES ) $NESTED_MODULES | ForEach-Object { $MODULE_NAME = $_ - Write-Debug "Importing nested module: $MODULE_NAME" + #Write-Debug "Importing nested module: $MODULE_NAME" #Find-Module psd1 file in the subdirectory and import it $PSD1_FILE = Get-ChildItem -Path ".\$MODULE_NAME" -Filter "$MODULE_NAME.psd1" -Recurse -ErrorAction SilentlyContinue if (-not $PSD1_FILE) { Write-Error "Module $MODULE_NAME not found. Exiting." throw "Nested module $MODULE_NAME not found. Exiting." - } - elseif ($PSD1_FILE.Count -gt 1) { + } elseif ($PSD1_FILE.Count -gt 1) { Write-Error "Multiple module files found for $MODULE_NAME. Exiting." throw "Multiple module files found for $MODULE_NAME. Exiting." } + Write-Debug "Importing nested module: $PSD1_FILE" Import-Module $PSD1_FILE.FullName -Force - Write-Debug "Importing nested module: $PSD1_FILE, -- $($PSD1_FILE.BaseName)" + Write-Debug "Imported nested module: $PSD1_FILE, -- $($PSD1_FILE.BaseName)" #Write-Debug "Importing nested module: .\$($_.BaseName)\$($_.Name)" # Validate the module is imported if (-not (Get-Module -Name $MODULE_NAME)) { @@ -70,67 +47,132 @@ function Import-NestedModules { } return $NESTED_MODULES } - -function Test-OSMHomeDir { - # If the OSM_HOME environment variable is not set, set it to the current directory. - $new_home = $(Get-Item $pwd).FullName | Split-Path -Parent - if (-not $env:OSM_HOME) { - Write-Debug "Setting OSM_HOME to $new_home" - $env:OSM_HOME = $new_home - } - # Check the OSM_HOME environment variable directory exists - if (-not (Test-Path $env:OSM_HOME)) { - Write-Warning "OSM_HOME directory not found: $env:OSM_HOME" - Write-Warning "Changing OSM_HOME to $new_home" - $env:OSM_HOME = $new_home - } - if ($env:OSM_HOME -ne $new_home) { - Write-Warn "OSM_HOME is set to $env:OSM_HOME, but the script location indicates it should be $new_home. This may cause issues." +# Create OSM dirs +$OSM_DIRS = @('OSM_HOME', 'OSM_INSTALL') +function Confirm-OSMDirs { + $VALIDATED_DIRS = $OSM_DIRS | ForEach-Object { + # Check if $env: variable exists + $ENVAR_NAME = 'env:' + $_ + if (-not (Get-Item -Path $ENVAR_NAME -ErrorAction SilentlyContinue)) { + if ($IsLinux) { + $DIR_PATH = '/opt/osm' + } else { + $DIR_PATH = $(Get-ItemProperty -Path .).FullName + } + $SetEnvar = '$' + $ENVAR_NAME + ' = "' + $DIR_PATH + '"' + Invoke-Expression -Command $SetEnvar | Write-Debug + #Write-Debug "Envar set: $SetEnvar" + } + # Get the path from the $env: variable and create the directory if it does not exist + $EXISING_ENVAR = Get-Item -Path $ENVAR_NAME + $EXPECTED_DIR = $EXISING_ENVAR.Value + if (-not (Test-Path $EXPECTED_DIR)) { + New-Item -ItemType Directory -Path $EXPECTED_DIR -Force | Write-Debug + #Write-Debug "Directory created: $EXPECTED_DIR" + } else { + #Write-Debug "Good news, $ENVAR_NAME already set and directory already exists: $EXPECTED_DIR" + } + $ENVAR_NAME } - $ValidatedOSMHome = (Get-Item $env:OSM_HOME).FullName - return $ValidatedOSMHome + return $VALIDATED_DIRS } function Invoke-AtlassianPowerKitFunction { param ( [Parameter(Mandatory = $true)] - [string] $FunctionName, + [string]$FunctionName, [Parameter(Mandatory = $false)] - [hashtable] $FunctionParameters + [hashtable]$FunctionParameters ) $TEMP_DIR = "$env:OSM_HOME\$env:AtlassianPowerKit_PROFILE_NAME\.temp" if (-not (Test-Path $TEMP_DIR)) { - New-Item -ItemType Directory -Path $TEMP_DIR -Force | Out-Null + New-Item -ItemType Directory -Path $TEMP_DIR -Force | Write-Debug } - $TIMESTAMP = Get-Date -Format 'yyyyMMdd-HHmmss' - $LOG_FILE = "$TEMP_DIR\$FunctionName-$TIMESTAMP.log" $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() - $stopwatch.Start() - if ($FunctionParameters) { - $singleLineDefinition = $FunctionParameters.Keys | ForEach-Object { "- -> $_ = $($FunctionParameters.($_))" } - Write-Debug "Running function: $FunctionName with parameters: $singleLineDefinition" - & $FunctionName @FunctionParameters - } - else { - Invoke-Expression "$FunctionName" + $stopwatch.Start() | Write-Debug + + try { + if ($FunctionParameters) { + # Safely construct a debug message with hashtable keys and values + $singleLineDefinition = $FunctionParameters.Keys | ForEach-Object { '- ' + $_ + ": $($FunctionParameters[$_])" } + Write-Debug "Running function: $FunctionName with parameters: $singleLineDefinition" + + # Use Splatting (@) to pass parameters + $RETURN_OBJECT = & $FunctionName @FunctionParameters + } else { + Write-Debug "Running function: $FunctionName without parameters" + $RETURN_OBJECT = & $FunctionName + } + + # Stop timing the function execution + $stopwatch.Stop() | Out-Null + Write-Debug "Function $FunctionName completed - execution time: $($stopwatch.Elapsed.TotalSeconds) seconds" + + # Convert the returned object to JSON + $RETURN_JSON = $RETURN_OBJECT + Write-Debug "Returning JSON of size: $($RETURN_JSON.Length) characters" + } catch { + Write-Debug "Error occurred while invoking function: $FunctionName" + Write-Debug $_ + $RETURN_JSON = "{'error': 'An error occurred while executing the function.', 'details': '$($_.Exception.Message)'}" } - $stopwatch.Stop() - Write-Output "Function $FunctionName completed - execution time: $($stopwatch.Elapsed.TotalSeconds) seconds" - Write-Output "Log file: $LOG_FILE" - Write-Output 'To run again, use the command: ' - Write-Output "Invoke-AtlassianPowerKitFunction -FunctionName $FunctionName -FunctionParameters @{ $singleLineDefinition }" + + return $RETURN_JSON } function Show-AdminFunctions { param ( [Parameter(Mandatory = $false)] - [string[]] $AdminModules = @('AtlassianPowerKit-Shared', 'AtlassianPowerKit-UsersAndGroups') + [string[]]$AdminModules = @('AtlassianPowerKit-Shared', 'AtlassianPowerKit-UsersAndGroups') ) # Clear current screen Clear-Host Show-AtlassianPowerKitFunctions -NESTED_MODULES $AdminModules } +function Update-OSMPKFunctionsMarkDownDoc { + param ( + [Parameter(Mandatory = $true)] + [string[]]$NESTED_MODULES + ) + # Creates or updates a markdown document with all functions in the module, their descriptions, and parameters + $MARKDOWN_FILE = "$env:OSM_HOME\AtlassianPowerKit\AtlassianPowerKit-Functions.md" + if (-not (Test-Path $MARKDOWN_FILE)) { + New-Item -ItemType File -Path $MARKDOWN_FILE -Force | Write-Debug + } else { + Clear-Content -Path $MARKDOWN_FILE | Write-Debug + } + $NESTED_MODULES | ForEach-Object { + $MODULE_NAME = $_ + Write-Output "# Module: $MODULE_NAME" | Out-File -FilePath $MARKDOWN_FILE -Append + Write-Debug "Updating markdown document for module: $MODULE_NAME" + $MODULE_FUNCTIONS = (Get-Module -Name $MODULE_NAME -All).ExportedFunctions.Keys + $MODULE_FUNCTIONS | ForEach-Object { + $FUNCTION_NAME = $_ + $FUNCTION_PARAMS = $FUNCTION_NAME.Parameters + Write-Output "## Function: $FUNCTION_NAME" | Out-File -FilePath $MARKDOWN_FILE -Append + Write-Output '### Params' | Out-File -FilePath $MARKDOWN_FILE -Append + foreach ($PARAM in $FUNCTION_PARAMS) { + $PARAM_DETAILS = $FUNCTION_PARAMS[$PARAM] + $PARAM_NAME = $PARAM + $PARAM_TYPE = $PARAM_DETAILS.ParameterType.Name + $PARAM_MANDATORY = $PARAM_DETAILS.IsMandatory + $PARAM_DEFAULT = $PARAM_DETAILS.DefaultValue + $PARAM_DETAILS = " - **$PARAM_NAME** ($PARAM_TYPE)" + if ($PARAM_MANDATORY) { + $PARAM_DETAILS += ' - Mandatory' + } + if ($PARAM_DEFAULT) { + $PARAM_DETAILS += " - Default: $PARAM_DEFAULT" + } + Write-Output $PARAM_DETAILS | Out-File -FilePath $MARKDOWN_FILE -Append + } + $FUNCTION_DETAILS | Out-File -FilePath $MARKDOWN_FILE -Append + } + } + return $MARKDOWN_FILE +} + # Function display console interface to run any function in the module function Show-AtlassianPowerKitFunctions { param ( @@ -139,7 +181,8 @@ function Show-AtlassianPowerKitFunctions { ) $selectedFunction = $null # Remove AtlassianPowerKit-Shard and AtlassianPowerKit-UsersAndGroups from the nested modules - $NESTED_MODULES = $NESTED_MODULES | Where-Object { $_ -ne 'AtlassianPowerKit-UsersAndGroups' } + #$NESTED_MODULES = $NESTED_MODULES | Where-Object { $_ -ne 'AtlassianPowerKit-UsersAndGroups' -and $_ -ne 'AtlassianPowerKit-Shared' } + $NESTED_MODULES = $NESTED_MODULES | Where-Object { $_ -ne 'AtlassianPowerKit-Shared' } # List nested modules and their exported functions to the console in a readable format, grouped by module $colors = @('Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'Blue', 'Gray') $colorIndex = 0 @@ -161,7 +204,7 @@ function Show-AtlassianPowerKitFunctions { $colorIndex++ #Write-Debug $MODULE_NAME #Get-Module -Name $MODULE_NAME - $FunctionList = (Get-Module -Name $MODULE_NAME).ExportedFunctions.Keys + $FunctionList = (Get-Module -Name $MODULE_NAME -All).ExportedFunctions.Keys $FunctionList | ForEach-Object { $functionReferences += $_ Write-Host ' ' -NoNewline -BackgroundColor "Dark$color" @@ -192,185 +235,201 @@ function Show-AtlassianPowerKitFunctions { if ($selectedFunction -match '^\d+$') { Write-Debug "Selected function by num: $selectedFunction" $SelectedFunctionName = ($functionReferences[[int]$selectedFunction]) - } - elseif ($selectedFunction -match '^(?i)[a-z]*-[a-z]*$') { + } elseif ($selectedFunction -match '^(?i)[a-z]*-[a-z]*$') { # Test if the function exists $selectedFunction = $selectedFunction Write-Debug "Selected function by name: $selectedFunction" #Write-Debug "Function references: $($functionReferences.GetType())" if ($functionReferences.Contains($selectedFunction)) { $SelectedFunctionName = $selectedFunction - } - else { + } else { Write-Error "Function $selectedFunction does not exist in the function references." } } - # if selected function is Return, exit the function - if (!$SelectedFunctionName -or ($SelectedFunctionName -eq 0 -or $SelectedFunctionName -eq 'Return')) { - Write-Debug 'No function selected. Exiting' - return $null - } if ($SelectedFunctionName -eq 'A') { Show-AdminFunctions } + # if selected function is Return, exit the function + elseif (!$SelectedFunctionName -or ($SelectedFunctionName -eq 0 -or $SelectedFunctionName -eq 'Return')) { + #Write-Debug 'No function selected. Exiting' + return $null + } # Run the selected function timing the execution Write-Host "`n" Write-Host "Invoking AtlassingPowerKit Function: $SelectedFunctionName" -ForegroundColor Green return $SelectedFunctionName } - -# Function to create a new profile function New-AtlassianPowerKitProfile { - # Ask user to enter the profile name - $ProfileName = Read-Host 'Enter a profile name:' - $ProfileName = $ProfileName.ToLower().Trim() - if (!$ProfileName -or $ProfileName -eq '' -or $ProfileName.Length -gt 100) { - Write-Error 'Profile name cannot be empty, or more than 100 characters, Please try again.' - # Load the selected profile or create a new profile - Write-Debug "Profile name entered: $ProfileName" - Throw 'Profile name cannot be empty, taken or mor than 100 characters, Please try again.' - } - else { - try { - Register-AtlassianPowerKitProfile($ProfileName) - } - catch { - Write-Debug "Error: $($_.Exception.Message)" - throw "Register-AtlassianPowerKitProfile $ProfileName failed. Exiting." - } + param ( + [Parameter(Mandatory = $false)] + [string]$PROFILE_NAME = $null + ) + if (!$PROFILE_NAME) { + $PROFILE_NAME = Read-Host -Prompt 'Enter a name for the new profile' } -} + $PROFILE_NAME = $PROFILE_NAME.Trim().ToLower() + $API_ENDPOINT = Read-Host -Prompt 'Enter the Atlassian API endpoint (e.g. https://your-domain.atlassian.net)' + $API_CREDPAIR = Get-Credential -Message 'Enter your Atlassian API credentials (email and API token)' + $REGISTERED_PROFILE = Register-AtlassianPowerKitProfileInVault -ProfileName $PROFILE_NAME -AtlassianAPIEndpoint $API_ENDPOINT -AtlassianAPICredentialPair $API_CREDPAIR + $ENVAR_ARRAY = Import-AtlassianPowerKitProfile -selectedProfile $REGISTERED_PROFILE + return $ENVAR_ARRAY +} # Function to list availble profiles with number references for interactive selection or 'N' to create a new profile -function Show-AtlassianPowerKitProfileList { - #Get-AtlassianPowerKitProfileList - $PROFILE_LIST = Get-AtlassianPowerKitProfileList - $profileIndex = 0 - if (!$PROFILE_LIST) { - Write-Host 'Please create a new profile.' - New-AtlassianPowerKitProfile - #Write-Debug "Profile List: $(Get-AtlassianPowerKitProfileList)" - #Show-AtlassianPowerKitProfileList - } - else { - #Write-Debug "Profile list: $env:AtlassianPowerKit_PROFILE_LIST_STRING" - Write-Debug "Profile list string $PROFILE_LIST" - $PROFILE_LIST.split() | ForEach-Object { - Write-Host "[$profileIndex] $_" - $profileIndex++ +function Import-AtlassianPowerKitProfile { + param ( + [Parameter(Mandatory = $false)] + [switch]$NoVault = $false, + [Parameter(Mandatory = $false)] + [string]$selectedProfile = $false + ) + if ($NoVault) { + #Write-Debug "$($MyInvocation.InvocationName) -NoVault flag set, attempting to load profile from environment variables" + $ENVAR_ARRAY = Set-AtlassianPowerKitProfile -NoVault + } elseif ($selectedProfile -ne $false) { + #Write-Debug "$($MyInvocation.InvocationName) -ProfileName profided, attempting to load profile: $selectedProfile from the vault" + $ENVAR_ARRAY = Set-AtlassianPowerKitProfile -ProfileName $selectedProfile + if (!$ENVAR_ARRAY -or $ENVAR_ARRAY.Count -lt 3) { + Write-Host "Could not load profile: $selectedProfile from the vault. Requesting values to add it to vault." + $ENVAR_ARRAY = New-AtlassianPowerKitProfile -PROFILE_NAME $selectedProfile + } + } else { + #Write-Debug "$($MyInvocation.InvocationName) -NoVault flag not set and no profile selected, checking for existing profiles in the vault" + $VAULT_PROFILES = Get-AtlassianPowerKitProfileList + if ($VAULT_PROFILES) { + if ($VAULT_PROFILES.Count -eq 1) { + $selectedProfile = $VAULT_PROFILES[0] + Write-Debug "Only one profile found in the vault, selecting $selectedProfile" + $ENVAR_ARRAY = Set-AtlassianPowerKitProfile -ProfileName $selectedProfile + } else { + Write-Output 'Multiple profiles found in the vault but no profile provided, please use the -OSMProfile parameter to specify the desired profile' + foreach ($OSMProfileNAME in $VAULT_PROFILES) { + Write-Output " AtlassianPowerkit -OSMProfileNAME $OSMProfileNAME" + } + Throw 'Ambiguous profile state' + } + } else { + Write-Output 'No profiles found in the vault, please create a new profile.' + $ENVAR_ARRAY = New-AtlassianPowerKitProfile } - } - Write-Host '[N] Create a new profile' - Write-Host '[D] Delete a profile' - Write-Host '[A] Admin (danger) functions' - Write-Host '[Q / Return] Quit' - Write-Host '++++++++++++++++++++++++++++++++++++++++++++++++++++++++++' -ForegroundColor DarkGray - try { - # read input from the user and just break with no error if the input is not a number, 'N', 'R' or 'Q' - $selectedProfile = Read-Host 'Select a profile number or action' - } - catch { - return $null - } - if ((!$selectedProfile) -or ($selectedProfile -eq 'Q')) { - return $null - } - elseif ($selectedProfile -eq 'N') { - New-AtlassianPowerKitProfile - } - elseif ($selectedProfile -eq 'A') { - Show-AdminFunctions - } - elseif ($selectedProfile -eq 'D') { - Remove-AtlasianPowerKitProfile - } - else { - $selectedProfile = [int]$selectedProfile - $SELECTED_PROFILE_NAME = $PROFILE_LIST[$selectedProfile] - # Write-Debug "Selected profile index: $selectedProfile" - # Write-Debug "Selected profile name: $($PROFILE_LIST[$selectedProfile])" - #$LOADED_PROFILENAME = Set-AtlassianPowerKitProfile -SelectedProfileName $($PROFILE_LIST[$selectedProfile]) - return $SELECTED_PROFILE_NAME } + return $ENVAR_ARRAY } function AtlassianPowerKit { param ( [Parameter(Mandatory = $false)] - [string] $Profile, + [string]$OSMProfile, + [Parameter(Mandatory = $false)] + [switch]$ArchiveProfileDirs, [Parameter(Mandatory = $false)] - [switch] $ArchiveProfileDirs, + [switch]$ResetVault, [Parameter(Mandatory = $false)] - [switch] $ResetVault, + [string]$FunctionName, [Parameter(Mandatory = $false)] - [string] $FunctionName, + [hashtable]$FunctionParameters, [Parameter(Mandatory = $false)] - [hashtable] $FunctionParameterHashTable, + [switch]$ClearProfile, [Parameter(Mandatory = $false)] - [switch] $ClearProfile + [switch]$ListProfiles, + [Parameter(Mandatory = $false)] + [switch]$NoVault = $false, + [Parameter(Mandatory = $false)] + [string]$RemoveVaultProfile = $false, + [Parameter(Mandatory = $false)] + [switch]$NewVaultProfile = $false, + [Parameter(Mandatory = $false)] + [switch]$DocFunctions = $false ) if (!$env:AtlassianPowerKit_RequisiteModules) { $env:AtlassianPowerKit_RequisiteModules = Get-RequisitePowerKitModules Write-Debug 'AtlassianPowerKit_RequisiteModules - Required modules imported' } - $NESTED_MODULES = Import-NestedModules -NESTED_MODULES @('AtlassianPowerKit-Shared', 'AtlassianPowerKit-Jira', 'AtlassianPowerKit-Confluence', 'AtlassianPowerKit-GRCosm', 'AtlassianPowerKit-JSM', 'AtlassianPowerKit-UsersAndGroups') + $NESTED_MODULES = Import-NestedModules -NESTED_MODULES @('AtlassianPowerKit-Shared', 'AtlassianPowerKit-Jira', 'AtlassianPowerKit-Confluence', 'AtlassianPowerKit-GRCosm', 'AtlassianPowerKit-JSM', 'AtlassianPowerKit-UsersAndGroups', 'AtlassianPowerKit-Admin') try { #Push-Location -Path $PSScriptRoot -ErrorAction Continue Write-Debug "Starting AtlassianPowerKit, running from $((Get-Item -Path $PSScriptRoot).FullName)" - Write-Debug "OSM_HOME: $(Test-OSMHomeDir)" + #Write-Debug 'OSM Directories: ' + foreach ($OSM_DIR in Confirm-OSMDirs) { + Get-Item -Path "$OSM_DIR" | Write-Debug + } # If current directory is not the script root, push the script root to the stack if ($ResetVault) { - Clear-AtlassianPowerKitVault + Write-Debug '-ResetVault flagged Clearing the AtlassianPowerKit vault, ignoring any other parameters' + Clear-AtlassianPowerKitVault | Write-Debug return $true - } - if ($ArchiveProfileDirs) { - Clear-AtlassianPowerKitProfileDirs + } elseif ($ListProfiles) { + Write-Debug '$ListProfiles flaged, Listing AtlassianPowerKit profiles, ignoring any other parameters' + $PROFILE_LIST = Get-AtlassianPowerKitProfileList + return $PROFILE_LIST + } elseif ($ArchiveProfileDirs) { + Write-Debug '-ArchiveProfileDirs flagged, Clearing the AtlassianPowerKit profile directories, ignoring any other parameters' + Clear-AtlassianPowerKitProfileDirs | Write-Debug return $true - } - if ($ClearProfile) { - Clear-AtlassianPowerKitProfile + } elseif ($ClearProfile) { + Write-Debug '-ClearProfile flagged, Clearing the AtlassianPowerKit profile, ignoring any other parameters' + Clear-AtlassianPowerKitProfile | Write-Debug return $true + } elseif ($RemoveVaultProfile -ne $false) { + Write-Debug '-RemoveVaultProfile flagged, Removing the AtlassianPowerKit profile from the vault, ignoring any other parameters' + Remove-AtlassianPowerKitProfile -ProfileName $RemoveVaultProfile | Write-Debug + return $true + } elseif ($DocFunctions) { + Write-Debug '-DocFunctions flagged, Creating a markdown document of all AtlassianPowerKit functions, ignoring any other parameters' + Update-OSMPKFunctionsMarkDownDoc -NESTED_MODULES $NESTED_MODULES + return $true + } elseif ($NewVaultProfile) { + Write-Debug '-NewVaultProfile flagged, Creating a new AtlassianPowerKit profile in the vault, ignoring any other parameters' + New-AtlassianPowerKitProfile | Write-Debug + return $true + } elseif ($NoVault) { + Write-Debug '-NoVault flagged, attempting to load profile from environment variables' + $PROFILE_ARRAY = Import-AtlassianPowerKitProfile -NoVault + } elseif ($OSMProfile) { + Write-Debug "Profile provided: $OSMProfile" + $ProfileName = $OSMProfile.Trim().ToLower() + $PROFILE_ARRAY = Import-AtlassianPowerKitProfile -selectedProfile $ProfileName + } else { + Write-Debug 'No profile provided, checking if vault has only 1 profile' + $PROFILE_ARRAY = Import-AtlassianPowerKitProfile } - # If no profile name is provided, list the available profiles - $ProfileName = $null - if ($Profile) { - $ProfileName = $Profile.Trim().ToLower() - } - if (!$ProfileName) { - $ProfileName = $(Show-AtlassianPowerKitProfileList) + Write-Debug "Profile set to: $env:AtlassianPowerKit_PROFILE_NAME" + $PROFILE_ARRAY | ForEach-Object { + Write-Output " $_" | Out-Null } - $CURRENT_PROFILE = Set-AtlassianPowerKitProfile -SelectedProfileName $ProfileName - Write-Debug "Profile set to: $CURRENT_PROFILE" if (!$FunctionName) { $FunctionName = Show-AtlassianPowerKitFunctions -NESTED_MODULES $NESTED_MODULES } - # If function parameters are provided, splat them to the function - Write-Debug "AtlassianPowerKit Main - Running function: $FunctionName, with profile: $CURRENT_PROFILE" - if ($FunctionParameterHashTable) { - Write-Debug ' Parameters provided to the function via hashtable:' + #Write-Debug "Function selected: $FunctionName" + if ($FunctionParameters) { + Write-Debug '-FunctionParameters provided !' + if ($FunctionParameters.GetType() -ne [hashtable]) { + Write-Debug '-FunctionParameters must be a hashtable, e.g.:' + Write-Debug ' @{ key1 = "value1"; key2 = "value2" }' + throw 'Function parameters must be a hashtable. Exiting.' + } # Iterate through the hashtable and display the key value pairs as "-key value" - $FunctionParameterHashTable.GetEnumerator() | ForEach-Object { + $FunctionParameters.GetEnumerator() | ForEach-Object { Write-Debug " -$($_.Key) $_.Value" } - Invoke-AtlassianPowerKitFunction -FunctionName $FunctionName -FunctionParameters $FunctionParameterHashTable - } - elseif ($FunctionName) { - Write-Debug "AtlassianPowerKit Main: No parameters provided to the function, attempting to run the function without parameters: $FunctionName" - Invoke-AtlassianPowerKitFunction -FunctionName $FunctionName + $RET_VAL = Invoke-AtlassianPowerKitFunction -FunctionName $FunctionName -FunctionParameters $FunctionParameters + Write-Debug "AtlassianPowerKit Main: Received JSON of size: $($RETURN_JSON.Length) characters" + } elseif ($FunctionName) { + Write-Debug "$($MyInvocation.InvocationName) attempting to run function: $FunctionName without parameters - most functions will handle this by requesting user input" + $RET_VAL = Invoke-AtlassianPowerKitFunction -FunctionName $FunctionName + } - } - catch { + } catch { # Write call stack and sub-function error messages to the debug output - Write-Debug '++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ AtlassianPowerKit Main: ' + Write-Debug "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ $((Get-Item -Path $PSScriptRoot).FullName) $($MyInvocation.InvocationName) FAILED: " # Write full call stack to the debug output and error message to the console Get-PSCallStack - Write-Debug '++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ AtlassianPowerKit Main: ' + Write-Debug "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ $((Get-Item -Path $PSScriptRoot).FullName) $($MyInvocation.InvocationName)" Write-Error $_.Exception.Message } - finally { - #Clear-AtlassianPowerKitProfile - Pop-Location - #Remove-Item 'env:AtlassianPowerKit_*' -ErrorAction Continue - Write-Debug 'Gracefully exited AtlassianPowerKit' + if (!$RET_VAL) { + Write-Output 'Nothing to return, have a nice day.' + } else { + return $RET_VAL } } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cf8b65c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Use the official PowerShell image as the base image +FROM mcr.microsoft.com/powershell:latest + +# Set the working directory +WORKDIR /mnt/osm + +# Install Git and required PowerShell modules +#RUN apt-get update && \ +# apt-get install -y git && \ +# apt-get clean && \ +# rm -rf /var/lib/apt/lists/* && \ +# mkdir -p /mnt/osm && \ +# chmod 755 -R ./* +RUN pwd && \ + chmod 755 -R /mnt +RUN pwsh -Command "Set-PSRepository -Name PSGallery -InstallationPolicy Trusted" && \ + pwsh -Command "Install-Module -Name PowerShellGet -Force" && \ + pwsh -Command "Install-Module -Name Microsoft.PowerShell.SecretManagement,Microsoft.PowerShell.SecretStore -Force" + +# Set the entry point +ENTRYPOINT ["pwsh"] + diff --git a/README.md b/README.md index 40080da..5ad932d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # AtlassianPowerKit - Various functions in PowerShell to interact with Atlassian Cloud APIs +- Supports multiple profiles for different Atlassian Cloud accounts +- Docker image available for cross-platform support (Windows, macOS, Linux): + - [markz0r/atlassian-powerkit](https://hub.docker.com/r/markz0r/atlassian-powerkit) -## Quick Start +## Usage ```powershell git clone https://github.com/OrganisationServiceManagement/AtlassianPowerKit.git @@ -10,18 +13,33 @@ cd .\AtlassianPowerKit; Import-Module ".\AtlassianPowerKit.psd1" AtlassianPowerKit ``` -## Usage - ```powershell # Text UI AtlassianPowerKit # Direct invocation (after profile configured) -AtlassianPowerKitFunction -FunctionName "Get-JiraIssue" -FunctionParameters @{"Key"="TEST-1"} -Profile "zoak" +AtlassianPowerKit -FunctionName "Get-JiraIssue" -FunctionParameters @{"Key"="TEST-1"} -Profile "zoak" +``` + +```docker +# Windows +mkdir .\osm_home +docker run --rm -v ${PWD}\osm_home:/mnt/osm -v "$Env:LOCALAPPDATA\Microsoft\PowerShell\secretmanagement\:/root/.secretmanagement/" -it markz0r/atlassian-powerkit:latest + +# Linux +mkdir ./osm_home +docker run -it --rm -v ${PWD}/osm_home:/mnt/osm -v "$HOME/.local/share/powershell/secretmanagement/ " ``` -## Prerequisites +## Documentation -- Windows PowerShell 7.0 or later +- _[AtlassianPowerKit Wiki](../../wiki)_ + +## Dependencies + +- PowerShell 7.0 or later (Core is supported on Windows, macOS, and Linux) +- Alternatively, you can use the Docker image to run the module: + - https://hub.docker.com/r/markz0r/atlassian-powerkit + - `docker run --rm -v ${PWD}\osm_home:/mnt/osm -v "$Env:LOCALAPPDATA\Microsoft\PowerShell\secretmanagement\:/root/.secretmanagement/" -it markz0r/atlassian-powerkit:latest` ## Contributing @@ -34,3 +52,7 @@ See [LICENSE](LICENSE.md) file. ## Disclaimer This module is provided as-is without any warranty or support. Use it at your own risk. + +``` + +``` diff --git a/Run.ps1 b/Run.ps1 new file mode 100644 index 0000000..0e90f3c --- /dev/null +++ b/Run.ps1 @@ -0,0 +1,112 @@ +# Run.ps1 + +# Import necessary modules +Import-Module -Name Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore -Force +Set-Location "$env:OSM_INSTALL/AtlassianPowerKit" +Import-Module "$env:OSM_INSTALL/AtlassianPowerKit/AtlassianPowerKit.psd1" -Force +$env:SECRETSTORE_PATH = $env:OSM_HOME + +# Load Environment Variables from the host + + +# Check if arguments were passed to the script +if ($args.Count -gt 0) { + # Run AtlassianPowerKit with the provided arguments + AtlassianPowerKit @args +} else { + # Default command + Write-Output 'No arguments provided. Starting Atlassian PowerKit...' + AtlassianPowerKit +} + +#Function Get-DeploymentConfigs { +# param ( +# [Parameter(Mandatory = $true)] +# [string]$PROFILE_NAME +# ) +# Write-Host "Processing profile: $PROFILE_NAME" +# # If there is a env:AtlassianPowerKit_PROFILE_NAME-ProjectList-*.json that was created in the last 12 hours, use it +# $PROFILE_PROJECT_LIST = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-ProjectList-*.json" +# if ($PROFILE_PROJECT_LIST) { +# $PROJECT_LIST = Get-Content $PROFILE_PROJECT_LIST.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# else { +# $PROJECT_LIST = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraProjectList' | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# #$PROJECT_LIST | ForEach-Object { Write-Host "Project: $($_.name) - $($_.key)" } +# #$PROJECT_LIST | ConvertTo-Json -Depth 100 | Write-Debug +# $OSM_PROJECT_LIST = $PROJECT_LIST | Where-Object { $_.key -match '.*OSM.*' -and $_.key -notin @('CUBOSM') } + +# $JIRA_PROJECTS = $OSM_PROJECT_LIST | ForEach-Object { +# $PROJECT_NAME = $($_.name) +# $PROJECT_KEY = $($_.key) +# # PROJECT_PROPERTIES +# $PROFILE_PROJECT_PROPERTIES = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-ProjectProperties-*.json" +# if ($PROFILE_PROJECT_PROPERTIES) { +# $PROFILE_PROJECT_PROPERTIES = Get-Content $PROFILE_PROJECT_PROPERTIES.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# else { +# $PROFILE_PROJECT_PROPERTIES = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraProjectProperties' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# # PROJECT_ISSUE_TYPE_SCHEMA +# $PROJECT_ISSUE_TYPE_SCHEMA = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-IssueTypeSchema-*.json" +# if ($PROJECT_ISSUE_TYPE_SCHEMA) { +# $PROJECT_ISSUE_TYPE_SCHEMA = Get-Content $PROJECT_ISSUE_TYPE_SCHEMA.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# else { +# $PROJECT_ISSUE_TYPE_SCHEMA = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraCloudIssueTypeSchema' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } +# } +# # +# # PROJECT_ISSUE_TYPES +# $PROJECT_ISSUE_TYPES = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-ProjectIssuesTypes-*.json" +# if ($PROJECT_ISSUE_TYPES) { +# $PROJECT_ISSUE_TYPES = Get-Content $PROJECT_ISSUE_TYPES.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# else { +# $PROJECT_ISSUE_TYPES = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraProjectIssuesTypes' -FunctionParameters @{ PROJECT_KEY_OR_ID = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# $PROJECT_REQUEST_TYPES = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-RequestTypeSchema-*.json" +# if ($PROJECT_REQUEST_TYPES) { +# $PROJECT_REQUEST_TYPES = Get-Content $PROJECT_REQUEST_TYPES.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# else { +# $PROJECT_REQUEST_TYPES = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraServiceDeskRequestTypes' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate +# } + +# # FORMS +# $PROJECT_FORMS = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-Forms-*.json" +# if ($PROJECT_FORMS) { +# $PROJECT_FORMS = Get-Content $PROJECT_FORMS.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# else { +# $PROJECT_FORMS = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-FormsForJiraProject' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate +# } + +# # $FORMS = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-FormsForJiraProject' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } +# # WORKFLOW_SCHEMES +# $PROJECT_WORKFLOWS_SCHEMES = Test-ExistingConfigJSON -CONFIG_FILE_PATHPATTERN "$($env:OSM_HOME)\$PROFILE_NAME\$PROFILE_NAME-$PROJECT_KEY-ProjectWorkflowSchemes-*.json" +# if ($PROJECT_WORKFLOWS_SCHEMES) { +# $PROJECT_WORKFLOWS_SCHEMES = Get-Content $PROJECT_WORKFLOWS_SCHEMES.FullName | ConvertFrom-Json -AsHashtable -NoEnumerate +# } +# else { +# $PROJECT_WORKFLOWS_SCHEMES = AtlassianPowerKit -Profile $PROFILE_NAME -FunctionName 'Get-JiraProjectWorkflowSchemes' -FunctionParameters @{ PROJECT_KEY = $PROJECT_KEY } | ConvertFrom-Json -AsHashtable -NoEnumerate +# } + +# # Return object +# [PSCustomObject]@{ +# PROJECT_NAME = $PROJECT_NAME +# PROJECT_KEY = $PROJECT_KEY +# PROJECT_ISSUE_TYPE_SCHEMA = $PROJECT_ISSUE_TYPE_SCHEMA +# PROJECT_ISSUE_TYPES = $PROJECT_ISSUE_TYPES +# PROJECT_REQUEST_TYPES = $PROJECT_REQUEST_TYPES +# PROJECT_WORKFLOWS_SCHEMES = $PROJECT_WORKFLOWS_SCHEMES +# } +# } +# Return $JIRA_PROJECTS | ConvertTo-Json -Depth 100 -Compress +#} + + +#ED Conductig a jq; l search and returning issues. +# $JIRA_PROJECTS = Get-DeploymentConfigs -PROFILE_NAME 'OSM' +# $JIRA_PROJECTS | ForEach-Object { + diff --git a/build_and_push.ps1 b/build_and_push.ps1 new file mode 100644 index 0000000..7d339a7 --- /dev/null +++ b/build_and_push.ps1 @@ -0,0 +1 @@ +docker build -t markz0r/atlassian-powerkit:latest . \ No newline at end of file diff --git a/submodules/htmltoadf b/submodules/htmltoadf new file mode 160000 index 0000000..247f248 --- /dev/null +++ b/submodules/htmltoadf @@ -0,0 +1 @@ +Subproject commit 247f2481ec71cee3930d963245a190c1527c8dfc