mirror of
https://github.com/MAKS-IT-COM/maksit-cluster-console.git
synced 2026-09-30 00:38:10 +02:00
(feature): resolve release plugin secrets from Vault; fix table Ctrl+C copy
This commit is contained in:
parent
fee5ca8249
commit
d51c636636
14
CHANGELOG.md
14
CHANGELOG.md
@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.1] - 2026-08-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Release packaging can resolve enabled plugin `*Secret` values from **MaksIT Vault** when `useVault` is set in `scriptSettings.json` (PowerShell module or HTTP API; `Shared` application fallback).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Copying resource-table rows includes column headers, so Ctrl+C pastes into Excel as a TSV table.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Copying resource-table rows no longer pastes empty quoted cells after CPU/memory tooltips moved columns to templates.
|
||||||
|
|
||||||
## [0.5.0] - 2026-08-22
|
## [0.5.0] - 2026-08-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Version>0.5.0</Version>
|
<Version>0.5.1</Version>
|
||||||
<Product>MaksIT.ClusterConsole</Product>
|
<Product>MaksIT.ClusterConsole</Product>
|
||||||
<AssemblyTitle>MaksIT.ClusterConsole</AssemblyTitle>
|
<AssemblyTitle>MaksIT.ClusterConsole</AssemblyTitle>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@ -551,6 +551,8 @@
|
|||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
CanUserResizeColumns="True"
|
CanUserResizeColumns="True"
|
||||||
CanUserSortColumns="True"
|
CanUserSortColumns="True"
|
||||||
|
ClipboardCopyMode="IncludeHeader"
|
||||||
|
CopyingRowClipboardContent="OnResourceGridCopyingRowClipboardContent"
|
||||||
DoubleTapped="OnResourceGridDoubleTapped" />
|
DoubleTapped="OnResourceGridDoubleTapped" />
|
||||||
<GridSplitter Grid.Column="1" Classes="shell-split" ResizeDirection="Columns" />
|
<GridSplitter Grid.Column="1" Classes="shell-split" ResizeDirection="Columns" />
|
||||||
<DockPanel Name="DetailsPane" Grid.Column="2" Margin="8,0,8,8">
|
<DockPanel Name="DetailsPane" Grid.Column="2" Margin="8,0,8,8">
|
||||||
|
|||||||
@ -97,6 +97,29 @@ public partial class MainWindow : Window {
|
|||||||
page.BrowseFilesCommand.Execute(null);
|
page.BrowseFilesCommand.Execute(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnResourceGridCopyingRowClipboardContent(object? sender, DataGridRowClipboardEventArgs e) {
|
||||||
|
if (!e.IsColumnHeadersRow)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var i = 0; i < e.ClipboardRowContent.Count; i++) {
|
||||||
|
var cell = e.ClipboardRowContent[i];
|
||||||
|
e.ClipboardRowContent[i] = new DataGridClipboardCellContent(
|
||||||
|
cell.Item,
|
||||||
|
cell.Column,
|
||||||
|
ColumnHeaderText(cell.Column));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ColumnHeaderText(DataGridColumn column) {
|
||||||
|
if (column.Tag is string tag && !string.IsNullOrWhiteSpace(tag))
|
||||||
|
return tag;
|
||||||
|
if (column.Header is string header)
|
||||||
|
return header;
|
||||||
|
if (column.Header is Control { DataContext: ColumnFilterViewModel filter })
|
||||||
|
return filter.Header;
|
||||||
|
return column.Header?.ToString() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
private void RebuildColumns(MainViewModel viewModel) {
|
private void RebuildColumns(MainViewModel viewModel) {
|
||||||
var grid = this.FindControl<DataGrid>("ResourceGrid");
|
var grid = this.FindControl<DataGrid>("ResourceGrid");
|
||||||
if (grid is null)
|
if (grid is null)
|
||||||
@ -129,6 +152,7 @@ public partial class MainWindow : Window {
|
|||||||
CanUserSort = true,
|
CanUserSort = true,
|
||||||
CustomSortComparer = comparer,
|
CustomSortComparer = comparer,
|
||||||
CellTemplate = StatusCellTemplate(),
|
CellTemplate = StatusCellTemplate(),
|
||||||
|
ClipboardContentBinding = new Binding(nameof(ResourceRow.Status)) { Mode = BindingMode.OneWay },
|
||||||
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
|
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
|
||||||
MinWidth = 72
|
MinWidth = 72
|
||||||
};
|
};
|
||||||
@ -140,6 +164,7 @@ public partial class MainWindow : Window {
|
|||||||
CanUserSort = true,
|
CanUserSort = true,
|
||||||
CustomSortComparer = comparer,
|
CustomSortComparer = comparer,
|
||||||
CellTemplate = TextCellTemplate(header),
|
CellTemplate = TextCellTemplate(header),
|
||||||
|
ClipboardContentBinding = CellsBinding(header),
|
||||||
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
|
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
|
||||||
MinWidth = 72
|
MinWidth = 72
|
||||||
};
|
};
|
||||||
@ -151,10 +176,7 @@ public partial class MainWindow : Window {
|
|||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
Margin = new Thickness(6, 0)
|
Margin = new Thickness(6, 0)
|
||||||
};
|
};
|
||||||
text.Bind(TextBlock.TextProperty, new Binding(nameof(ResourceRow.Cells)) {
|
text.Bind(TextBlock.TextProperty, CellsBinding(header));
|
||||||
Mode = BindingMode.OneWay,
|
|
||||||
Converter = new DictionaryKeyConverter(header)
|
|
||||||
});
|
|
||||||
text.Bind(ToolTip.TipProperty, new Binding(nameof(ResourceRow.CellTips)) {
|
text.Bind(ToolTip.TipProperty, new Binding(nameof(ResourceRow.CellTips)) {
|
||||||
Mode = BindingMode.OneWay,
|
Mode = BindingMode.OneWay,
|
||||||
Converter = new DictionaryKeyConverter(header)
|
Converter = new DictionaryKeyConverter(header)
|
||||||
@ -176,6 +198,12 @@ public partial class MainWindow : Window {
|
|||||||
return text;
|
return text;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
private static Binding CellsBinding(string header) =>
|
||||||
|
new(nameof(ResourceRow.Cells)) {
|
||||||
|
Mode = BindingMode.OneWay,
|
||||||
|
Converter = new DictionaryKeyConverter(header)
|
||||||
|
};
|
||||||
|
|
||||||
private sealed class DictionaryKeyConverter(string key) : IValueConverter {
|
private sealed class DictionaryKeyConverter(string key) : IValueConverter {
|
||||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) {
|
||||||
if (value is IReadOnlyDictionary<string, string> cells && cells.TryGetValue(key, out var text))
|
if (value is IReadOnlyDictionary<string, string> cells && cells.TryGetValue(key, out var text))
|
||||||
|
|||||||
@ -205,6 +205,7 @@
|
|||||||
<Setter Property="HorizontalGridLinesBrush" Value="#33383e" />
|
<Setter Property="HorizontalGridLinesBrush" Value="#33383e" />
|
||||||
<Setter Property="VerticalGridLinesBrush" Value="#33383e" />
|
<Setter Property="VerticalGridLinesBrush" Value="#33383e" />
|
||||||
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
|
||||||
|
<Setter Property="ClipboardCopyMode" Value="IncludeHeader" />
|
||||||
<Setter Property="controls:DataGridColumns.IndependentResize" Value="True" />
|
<Setter Property="controls:DataGridColumns.IndependentResize" Value="True" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="DataGridRow:nth-child(even)">
|
<Style Selector="DataGridRow:nth-child(even)">
|
||||||
|
|||||||
@ -35,6 +35,7 @@ else {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||||
|
Initialize-RepoUtilsVaultSecrets -Settings $settings -Plugins $configuredPlugins
|
||||||
|
|
||||||
$releaseBanner = if ($null -ne $releaseExtension) {
|
$releaseBanner = if ($null -ne $releaseExtension) {
|
||||||
$releaseExtension.StepBanner
|
$releaseExtension.StepBanner
|
||||||
|
|||||||
@ -15,6 +15,7 @@ function Import-EngineModules {
|
|||||||
(Join-Path $modulesDir 'ScriptConfig.psm1'),
|
(Join-Path $modulesDir 'ScriptConfig.psm1'),
|
||||||
(Join-Path $modulesDir 'Logging.psm1'),
|
(Join-Path $modulesDir 'Logging.psm1'),
|
||||||
(Join-Path $engineModuleDir 'PluginSupport.psm1'),
|
(Join-Path $engineModuleDir 'PluginSupport.psm1'),
|
||||||
|
(Join-Path $engineModuleDir 'VaultSupport.psm1'),
|
||||||
(Join-Path $engineModuleDir 'EngineContext.psm1')
|
(Join-Path $engineModuleDir 'EngineContext.psm1')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ function Test-IsEngineRuntimeModuleName {
|
|||||||
'TestRunner',
|
'TestRunner',
|
||||||
'EngineContext',
|
'EngineContext',
|
||||||
'PluginSupport',
|
'PluginSupport',
|
||||||
|
'VaultSupport',
|
||||||
'ReleaseSupport',
|
'ReleaseSupport',
|
||||||
'TestSupport'
|
'TestSupport'
|
||||||
),
|
),
|
||||||
|
|||||||
413
utils/modules/Engine/VaultSupport.psm1
Normal file
413
utils/modules/Engine/VaultSupport.psm1
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
function Test-RepoUtilsUseVaultEnabled {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Settings -or -not ($Settings.PSObject.Properties.Name -contains 'useVault')) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $Settings.useVault
|
||||||
|
if ($value -is [bool]) {
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$value -eq 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertFrom-VaultConnectionSecret {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Raw
|
||||||
|
)
|
||||||
|
|
||||||
|
$trimmed = $Raw.Trim()
|
||||||
|
$separator = $trimmed.IndexOf('|')
|
||||||
|
if ($separator -lt 1 -or $separator -ge ($trimmed.Length - 1)) {
|
||||||
|
throw "Vault connection must be 'baseAddress|apiKey' (pipe separator). Example: http://172.16.0.14|your-key"
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseAddress = $trimmed.Substring(0, $separator).Trim().TrimEnd('/')
|
||||||
|
$apiKey = $trimmed.Substring($separator + 1).Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($baseAddress) -or [string]::IsNullOrWhiteSpace($apiKey)) {
|
||||||
|
throw "Vault connection is missing base address or API key."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
BaseAddress = $baseAddress
|
||||||
|
ApiKey = $apiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepoUtilsVaultScope {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
$organization = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_ORGANIZATION')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($organization) -and ($Settings.PSObject.Properties.Name -contains 'vaultOrganization')) {
|
||||||
|
$organization = [string]$Settings.vaultOrganization
|
||||||
|
}
|
||||||
|
|
||||||
|
$application = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_APPLICATION')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($application) -and ($Settings.PSObject.Properties.Name -contains 'vaultApplication')) {
|
||||||
|
$application = [string]$Settings.vaultApplication
|
||||||
|
}
|
||||||
|
|
||||||
|
$organization = if ($null -eq $organization) { '' } else { $organization.Trim() }
|
||||||
|
$application = if ($null -eq $application) { '' } else { $application.Trim() }
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($organization) -or [string]::IsNullOrWhiteSpace($application)) {
|
||||||
|
throw "useVault is true but vault organization/application are not set. Set vaultOrganization/vaultApplication in scriptSettings.json or MAKSIT_VAULT_ORGANIZATION / MAKSIT_VAULT_APPLICATION."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Organization = $organization
|
||||||
|
Application = $application
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepoUtilsVaultConnectionSecretName {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
if (($Settings.PSObject.Properties.Name -contains 'vaultConnectionSecret') -and
|
||||||
|
-not [string]::IsNullOrWhiteSpace([string]$Settings.vaultConnectionSecret)) {
|
||||||
|
return ([string]$Settings.vaultConnectionSecret).Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'MAKSIT_VAULT'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-RepoUtilsSecretNamesFromObject {
|
||||||
|
param(
|
||||||
|
$Object,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[System.Collections.Generic.HashSet[string]]$Names
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Object -or $Object -is [string] -or $Object -is [ValueType]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Object -is [System.Collections.IDictionary]) {
|
||||||
|
foreach ($key in @($Object.Keys)) {
|
||||||
|
$name = [string]$key
|
||||||
|
$value = $Object[$key]
|
||||||
|
if ($name -like '*Secret') {
|
||||||
|
$trimmed = ([string]$value).Trim()
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($trimmed) -and
|
||||||
|
-not [string]::Equals($trimmed, 'WebhookSecret', [System.StringComparison]::Ordinal)) {
|
||||||
|
[void]$Names.Add($trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Add-RepoUtilsSecretNamesFromObject -Object $value -Names $Names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Object -is [System.Collections.IEnumerable]) {
|
||||||
|
foreach ($item in @($Object)) {
|
||||||
|
Add-RepoUtilsSecretNamesFromObject -Object $item -Names $Names
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $Object.PSObject) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($property in $Object.PSObject.Properties) {
|
||||||
|
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $property.Value
|
||||||
|
if ($property.Name -like '*Secret') {
|
||||||
|
$trimmed = ([string]$value).Trim()
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($trimmed) -and
|
||||||
|
-not [string]::Equals($trimmed, 'WebhookSecret', [System.StringComparison]::Ordinal)) {
|
||||||
|
[void]$Names.Add($trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
Add-RepoUtilsSecretNamesFromObject -Object $value -Names $Names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-EnabledPluginSecretNames {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugins
|
||||||
|
)
|
||||||
|
|
||||||
|
$names = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||||
|
foreach ($plugin in @($Plugins)) {
|
||||||
|
if ($null -eq $plugin) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($plugin.PSObject.Properties.Name -contains 'enabled') -and ($plugin.enabled -eq $false)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
Add-RepoUtilsSecretNamesFromObject -Object $plugin -Names $names
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($names)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VaultNameFilterExpression {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
$escaped = $Name.Replace('\', '\\').Replace('"', '\"')
|
||||||
|
return "Name == `"$escaped`""
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-RepoUtilsVaultClientModule {
|
||||||
|
$modulePath = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_MODULE_PATH')
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($modulePath)) {
|
||||||
|
Import-Module $modulePath -Force -ErrorAction Stop
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Get-Command Connect-Vault -ErrorAction SilentlyContinue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module 'MaksIT.Vault.Client.PowerShell' -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-RepoUtilsVaultSecretMatch {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$OrganizationName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ApplicationName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[pscustomobject]$Connection,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet('Cmdlet', 'Rest')]
|
||||||
|
[string]$Mode
|
||||||
|
)
|
||||||
|
|
||||||
|
$orgFilter = Get-VaultNameFilterExpression -Name $OrganizationName
|
||||||
|
$appFilter = Get-VaultNameFilterExpression -Name $ApplicationName
|
||||||
|
$pageNumber = 1
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$items = @()
|
||||||
|
$hasNext = $false
|
||||||
|
|
||||||
|
if ($Mode -eq 'Cmdlet') {
|
||||||
|
$response = Search-VaultSecrets -PageNumber $pageNumber -PageSize 100 `
|
||||||
|
-OrganizationFilters $orgFilter -ApplicationFilters $appFilter
|
||||||
|
if ($null -ne $response -and $null -ne $response.Items) {
|
||||||
|
$items = @($response.Items)
|
||||||
|
}
|
||||||
|
if ($null -ne $response) {
|
||||||
|
$hasNext = [bool]$response.HasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$body = @{
|
||||||
|
pageNumber = $pageNumber
|
||||||
|
pageSize = 100
|
||||||
|
organizationFilters = $orgFilter
|
||||||
|
applicationFilters = $appFilter
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
$uri = "$($Connection.BaseAddress)/api/vault/secrets"
|
||||||
|
$headers = @{ 'X-API-KEY' = $Connection.ApiKey }
|
||||||
|
$response = Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -ContentType 'application/json' -Body $body
|
||||||
|
if ($null -ne $response.items) {
|
||||||
|
$items = @($response.items)
|
||||||
|
}
|
||||||
|
elseif ($null -ne $response.Items) {
|
||||||
|
$items = @($response.Items)
|
||||||
|
}
|
||||||
|
$hasNext = [bool]($response.hasNextPage)
|
||||||
|
if (-not $hasNext) {
|
||||||
|
$hasNext = [bool]($response.HasNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item in $items) {
|
||||||
|
$itemName = [string]$(if ($item.PSObject.Properties.Name -contains 'Name') { $item.Name } else { $item.name })
|
||||||
|
if ([string]::Equals($itemName, $SecretName, [System.StringComparison]::Ordinal)) {
|
||||||
|
return $item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $hasNext) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageNumber++
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepoUtilsVaultSecretValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Match,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[pscustomobject]$Connection,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet('Cmdlet', 'Rest')]
|
||||||
|
[string]$Mode
|
||||||
|
)
|
||||||
|
|
||||||
|
$organizationId = if ($Match.PSObject.Properties.Name -contains 'OrganizationId') { $Match.OrganizationId } else { $Match.organizationId }
|
||||||
|
$applicationId = if ($Match.PSObject.Properties.Name -contains 'ApplicationId') { $Match.ApplicationId } else { $Match.applicationId }
|
||||||
|
$secretId = if ($Match.PSObject.Properties.Name -contains 'Id') { $Match.Id } else { $Match.id }
|
||||||
|
|
||||||
|
if ($Mode -eq 'Cmdlet') {
|
||||||
|
$version = Get-VaultSecret -OrganizationId $organizationId -ApplicationId $applicationId -SecretId $secretId -SecretVersion 'current'
|
||||||
|
if ($null -eq $version -or [string]::IsNullOrWhiteSpace([string]$version.Value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
return [string]$version.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = "$($Connection.BaseAddress)/api/vault/organization/$organizationId/application/$applicationId/secret/$secretId" +
|
||||||
|
'?secretVersion=current'
|
||||||
|
$headers = @{ 'X-API-KEY' = $Connection.ApiKey }
|
||||||
|
$version = Invoke-RestMethod -Method Get -Uri $uri -Headers $headers
|
||||||
|
$value = if ($version.PSObject.Properties.Name -contains 'Value') { $version.Value } else { $version.value }
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
return [string]$value
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RepoUtilsVaultSecretValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$OrganizationName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ApplicationName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[pscustomobject]$Connection,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Mode
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($application in @($ApplicationName, 'Shared')) {
|
||||||
|
$match = Find-RepoUtilsVaultSecretMatch `
|
||||||
|
-OrganizationName $OrganizationName `
|
||||||
|
-ApplicationName $application `
|
||||||
|
-SecretName $SecretName `
|
||||||
|
-Connection $Connection `
|
||||||
|
-Mode $Mode
|
||||||
|
if ($null -eq $match) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = Get-RepoUtilsVaultSecretValue -Match $match -Connection $Connection -Mode $Mode
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
Write-Log -Level 'INFO' -Message "Resolved Vault secret '$SecretName' from $OrganizationName/$application"
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Initialize-RepoUtilsVaultSecrets {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugins
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-RepoUtilsUseVaultEnabled -Settings $Settings)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$connectionName = Get-RepoUtilsVaultConnectionSecretName -Settings $Settings
|
||||||
|
$raw = [Environment]::GetEnvironmentVariable($connectionName)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||||
|
throw "useVault is true but environment variable '$connectionName' is not set (expected baseAddress|apiKey)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = ConvertFrom-VaultConnectionSecret -Raw $raw
|
||||||
|
$scope = Get-RepoUtilsVaultScope -Settings $Settings
|
||||||
|
$secretNames = @(Get-EnabledPluginSecretNames -Plugins $Plugins)
|
||||||
|
|
||||||
|
Write-Log -Level 'INFO' -Message "Vault mode: loading $($secretNames.Count) plugin secret(s) for $($scope.Organization)/$($scope.Application)"
|
||||||
|
|
||||||
|
$mode = 'Rest'
|
||||||
|
try {
|
||||||
|
Import-RepoUtilsVaultClientModule
|
||||||
|
if (Get-Command Connect-Vault -ErrorAction SilentlyContinue) {
|
||||||
|
Connect-Vault -BaseAddress $connection.BaseAddress -ApiKey $connection.ApiKey
|
||||||
|
$mode = 'Cmdlet'
|
||||||
|
Write-Log -Level 'INFO' -Message 'Connected to Vault with MaksIT.Vault.Client.PowerShell'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level 'INFO' -Message 'MaksIT.Vault.Client.PowerShell not found; using Vault HTTP API'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Log -Level 'INFO' -Message "Vault PowerShell module not loaded ($($_.Exception.Message)); using Vault HTTP API"
|
||||||
|
$mode = 'Rest'
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($secretName in $secretNames) {
|
||||||
|
$value = Resolve-RepoUtilsVaultSecretValue `
|
||||||
|
-OrganizationName $scope.Organization `
|
||||||
|
-ApplicationName $scope.Application `
|
||||||
|
-SecretName $secretName `
|
||||||
|
-Connection $connection `
|
||||||
|
-Mode $mode
|
||||||
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
throw "Vault secret '$secretName' was not found for $($scope.Organization)/$($scope.Application) (or Shared) or has no current version."
|
||||||
|
}
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable($secretName, $value, 'Process')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function @(
|
||||||
|
'Test-RepoUtilsUseVaultEnabled',
|
||||||
|
'ConvertFrom-VaultConnectionSecret',
|
||||||
|
'Get-RepoUtilsVaultScope',
|
||||||
|
'Get-RepoUtilsVaultConnectionSecretName',
|
||||||
|
'Get-EnabledPluginSecretNames',
|
||||||
|
'Initialize-RepoUtilsVaultSecrets'
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user