Skip to content

Commit fa0132f

Browse files
committed
Adds feature to improve module isolation
1 parent 89f4ac7 commit fa0132f

13 files changed

+392
-104
lines changed

doc/100-General/10-Changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic
2828
* [#495](https://github.com/Icinga/icinga-powershell-framework/pull/495) Adds feature to check the sign status for the local Icinga Agent certificate and notifying the user, in case the certificate is not yet signed by the Icinga CA
2929
* [#496](https://github.com/Icinga/icinga-powershell-framework/pull/496) Improves REST-Api default timeout for internal plugin execution calls from 30s to 120s
3030
* [#498](https://github.com/Icinga/icinga-powershell-framework/pull/498) Adds feature for thread queuing optimisation and frozen thread detection for REST calls
31+
* [#514](https://github.com/Icinga/icinga-powershell-framework/pull/514) Adds support for Icinga for Windows module isolation
3132

3233
## 1.8.0 (2022-02-08)
3334

doc/900-Developer-Guide/00-General.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ The following entries are set by default within the `Protected` space:
121121
| DebugMode | Enables the debug mode of Icinga for Windows, printing additional details during operations or tasks |
122122
| Minimal | Changes certain behavior regarding check execution and internal error handling |
123123

124+
## Private and Public Functions
125+
126+
Icinga for Windows will by default only export `Functions` and `Cmdlets`, in case they are either located within the `root .psm1` file, inside a folder called `public` or in case `Global:` is added before the function:
127+
128+
```powershell
129+
function Global:Invoke-MyCommand()
130+
```
131+
132+
In addition, all commands with the alias `Invoke-IcingaCheck` are automatically exported for plugins. This ensures that each module is isolated from each other and functions with the same name can be used within different modules, without overwriting existing ones. This ensures a better integrity of the module itself.
133+
134+
Last but not least, each module is created with a compilation file which is created during the first run of the module and used later on. This ensures en even faster response and reduced load on the system.
135+
124136
## Using Icinga for Windows Dev Tools
125137

126138
Maintaining the entire structure above seems to be complicated at the beginning, especially when considering to update the `NestedModules` section whenever you make changes. To mitigate this, Icinga for Windows provides a bunch of Cmdlets to help with the process
@@ -139,7 +151,7 @@ The command ships with a bunch of configurations to modify the created `.psd1` i
139151

140152
### Publish/Update Components
141153

142-
Once you have started to write your own code, you can use the Cmdlet `Publish-IcingaForWindowsComponent` to update the `NestedModules` attribute inside the `.psd1` file automatically, including the documentation in case the module is of type plugin.
154+
Once you have started to write your own code, you can use the Cmdlet `Publish-IcingaForWindowsComponent` to compile and add requires functions for the calls, including the documentation in case the module is of type plugin.
143155

144156
In addition, you ca create a `.zip` file for this module which can be integrated directly into the [Repository Manager](../120-Repository-Manager/01-Add-Repositories.md). By default, created `.zip` files will be created in your home folder, the path can how ever be changed while executing the command.
145157

@@ -149,6 +161,8 @@ In addition, you ca create a `.zip` file for this module which can be integrated
149161
| ReleasePackagePath | String | The path on where the `.zip` file will be created in. Defaults to the current users home folder |
150162
| CreateReleasePackage | Switch | This will toggle the `.zip` file creation of the specified package |
151163

164+
Please note that using `Publish-IcingaForWindowsComponent` is mandatory, before you can use the module on target systems. Each Icinga for Windows module is isolated from the general environment and allows to overwrite certain functions locally, inside the module instead of having to worry about them being overwritten on other, critical areas.
165+
152166
### Testing Your Component
153167

154168
In order to validate if your module can be loaded and is working properly, you can use the command `Test-IcingaForWindowsComponent`. In addition to an import check, it will also validate the code styling and give you an overview if and how many issues there are with your code.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
function Get-IcingaForWindowsComponentPublicFunctions()
2+
{
3+
param (
4+
$FileObject = $null,
5+
[string]$ModuleName = ''
6+
);
7+
8+
[array]$ExportFunctions = @();
9+
10+
# First first if we are inside a public space
11+
if ((Test-IcingaForWindowsComponentPublicFunctions -FileObject $FileObject -ModuleName $ModuleName) -eq $FALSE) {
12+
$FileData = (Read-IcingaPowerShellModuleFile -File $FileObject.FullName);
13+
14+
foreach ($entry in $FileData.FunctionList) {
15+
if ($entry.Contains('Global:')) {
16+
$ExportFunctions += $entry.Replace('Global:', '');
17+
}
18+
}
19+
20+
$ExportFunctions += $FileData.ExportFunction;
21+
22+
return $ExportFunctions;
23+
}
24+
25+
$FileData = (Read-IcingaPowerShellModuleFile -File $FileObject.FullName);
26+
$ExportFunctions += $FileData.FunctionList;
27+
$ExportFunctions += $FileData.ExportFunction;
28+
29+
# If we are, add all functions we found
30+
return $ExportFunctions;
31+
}

lib/core/dev/New-IcingaForWindowsComponent.psm1

+30-17
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,19 @@ function New-IcingaForWindowsComponent()
7575
'plugins' {
7676
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'plugins') | Out-Null;
7777
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'provider') | Out-Null;
78+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'provider\public') | Out-Null;
79+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'provider\private') | Out-Null;
7880
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib') | Out-Null;
81+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\public') | Out-Null;
82+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\private') | Out-Null;
7983

8084
break;
8185
};
8286
'apiendpoint' {
8387
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'endpoint') | Out-Null;
8488
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib') | Out-Null;
89+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\public') | Out-Null;
90+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\private') | Out-Null;
8591

8692
[string]$RegisterFunction = ([string]::Format('Register-IcingaRESTAPIEndpoint{0}', $TextInfo.ToTitleCase($Name.ToLower())));
8793
[string]$RegisterFunctionFile = (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('endpoint\{0}.psm1', $RegisterFunction)));
@@ -124,6 +130,8 @@ function New-IcingaForWindowsComponent()
124130
'daemon' {
125131
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'daemon') | Out-Null;
126132
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib') | Out-Null;
133+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\public') | Out-Null;
134+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\private') | Out-Null;
127135
New-Item `
128136
-ItemType File `
129137
-Path (Join-Path -Path (Join-Path -Path $ModuleDir -ChildPath 'daemon') -ChildPath ([string]::Format('Start-IcingaForWindowsDaemon{0}.psm1', $TextInfo.ToTitleCase($Name.ToLower())))) | Out-Null;
@@ -182,6 +190,8 @@ function New-IcingaForWindowsComponent()
182190
};
183191
'library' {
184192
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib') | Out-Null;
193+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\public') | Out-Null;
194+
New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\private') | Out-Null;
185195

186196
break;
187197
}
@@ -193,23 +203,26 @@ function New-IcingaForWindowsComponent()
193203
-Force | Out-Null;
194204

195205
Write-IcingaForWindowsComponentManifest -Name $Name -ModuleConfig @{
196-
'$MODULENAME$' = ([string]::Format('Windows {0}', $Name));
197-
'$GUID$' = (New-Guid);
198-
'$ROOTMODULE$' = ([string]::Format('{0}.psm1', $ModuleName));
199-
'$AUTHOR$' = $Author;
200-
'$COMPANYNAME$' = $CompanyName;
201-
'$COPYRIGHT$' = $Copyright;
202-
'$MODULEVERSION$' = $ModuleVersion.ToString();
203-
'$VMODULEVERSION$' = ([string]::Format('v{0}', $ModuleVersion.ToString()));
204-
'$DESCRIPTION$' = $Description;
205-
'$REQUIREDMODULES$' = $RequiredModules;
206-
'$NESTEDMODULES$' = '';
207-
'$TAGS$' = $Tags;
208-
'$PROJECTURI$' = $ProjectUri;
209-
'$LICENSEURI$' = $LicenseUri;
210-
'$COMPONENTTYPE$' = $ComponentType;
211-
'$DAEMONFUNCTION$' = $DaemonFunction;
212-
'$APIENDPOINT$' = $EndpointName;
206+
'$MODULENAME$' = ([string]::Format('Windows {0}', $Name));
207+
'$GUID$' = (New-Guid);
208+
'$ROOTMODULE$' = ([string]::Format('{0}.psm1', $ModuleName));
209+
'$AUTHOR$' = $Author;
210+
'$COMPANYNAME$' = $CompanyName;
211+
'$COPYRIGHT$' = $Copyright;
212+
'$MODULEVERSION$' = $ModuleVersion.ToString();
213+
'$VMODULEVERSION$' = ([string]::Format('v{0}', $ModuleVersion.ToString()));
214+
'$DESCRIPTION$' = $Description;
215+
'$REQUIREDMODULES$' = $RequiredModules;
216+
'$NESTEDMODULES$' = '';
217+
'$FUNCTIONSTOEXPORT$' = '';
218+
'$VARIABLESTOEXPORT$' = '';
219+
'$ALIASESTOEXPORT$' = '';
220+
'$TAGS$' = $Tags;
221+
'$PROJECTURI$' = $ProjectUri;
222+
'$LICENSEURI$' = $LicenseUri;
223+
'$COMPONENTTYPE$' = $ComponentType;
224+
'$DAEMONFUNCTION$' = $DaemonFunction;
225+
'$APIENDPOINT$' = $EndpointName;
213226
};
214227

215228
Set-Content `

lib/core/dev/Publish-IcingaForWindowsComponent.psm1

+24-4
Original file line numberDiff line numberDiff line change
@@ -66,28 +66,48 @@ function Publish-IcingaForWindowsComponent()
6666

6767
[ScriptBlock]$ManifestScriptBlock = [ScriptBlock]::Create('return ' + $ManifestScript);
6868
$ModuleManifestData = (& $ManifestScriptBlock);
69-
$ModuleList = @();
7069
$ModuleFiles = Get-ChildItem -Path $ModuleDir -Recurse -Filter '*.psm1';
70+
[array]$FunctionList = @();
71+
[array]$CmdletList = @();
72+
[array]$VariableList = @();
73+
[array]$AliasList = @();
74+
[string]$CompiledFolder = (Join-Path -Path $ModuleDir -ChildPath 'compiled');
75+
[string]$CompiledFile = [string]::Format('{0}.ifw_compilation.psm1', $ModuleName.ToLower());
76+
[string]$CompiledFileInclude = [string]::Format('.\compiled\{0}', $CompiledFile);
77+
[string]$CompiledFilePath = (Join-Path -Path $CompiledFolder -ChildPath $CompiledFile);
7178

7279
foreach ($entry in $ModuleFiles) {
73-
if ($entry.Name -eq ([string]::Format('{0}.psm1', $ModuleName))) {
80+
# Ensure the compilation file never includes itself
81+
if ($entry.FullName -eq $CompiledFilePath) {
7482
continue;
7583
}
7684

77-
$ModuleList += $entry.FullName.Replace($ModuleDir, '.');
85+
$FunctionList += Get-IcingaForWindowsComponentPublicFunctions -FileObject $entry -ModuleName $ModuleName;
86+
$FileConfig = (Read-IcingaPowerShellModuleFile -File $entry.FullName);
87+
$VariableList += $FileConfig.VariableList;
88+
$AliasList += $FileConfig.AliasList;
89+
$CmdletList += $FileConfig.ExportCmdlet;
7890
}
7991

92+
if ((Test-Path -Path $CompiledFolder) -eq $FALSE) {
93+
New-Item -Path $CompiledFolder -ItemType Directory -Force | Out-Null;
94+
}
95+
96+
Copy-ItemSecure -Path (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\compilation.psm1.template') -Destination $CompiledFilePath -Force | Out-Null;
97+
8098
if ($NoOutput) {
8199
Disable-IcingaFrameworkConsoleOutput;
82100
}
83101

84-
Write-IcingaForWindowsComponentManifest -Name $Name -ModuleList $ModuleList;
102+
Write-IcingaForWindowsComponentManifest -Name $Name -ModuleList @( $CompiledFileInclude ) -FunctionList $FunctionList -VariableList $VariableList -AliasList $AliasList -CmdletList $CmdletList;
85103

86104
if ($ModuleManifestData.PrivateData.Type -eq 'plugins') {
87105
Publish-IcingaPluginConfiguration -ComponentName $Name;
88106
Publish-IcingaPluginDocumentation -ModulePath $ModuleDir;
89107
}
90108

109+
Copy-ItemSecure -Path (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\compilation.psm1.template') -Destination $CompiledFilePath -Force | Out-Null;
110+
91111
if ($CreateReleasePackage) {
92112
if ([string]::IsNullOrEmpty($ReleasePackagePath)) {
93113
$ReleasePackagePath = Join-Path -Path $ENV:HOMEDRIVE -ChildPath $ENV:HOMEPATH;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
function Test-IcingaForWindowsComponentPublicFunctions()
2+
{
3+
param (
4+
$FileObject = $null,
5+
[string]$ModuleName = ''
6+
);
7+
8+
if ($null -eq $FileObject -Or [string]::IsNullOrEmpty($ModuleName)) {
9+
return $FALSE;
10+
}
11+
12+
# If we load the main .psm1 file of this module, add all functions inside to the public space
13+
if ($FileObject.Name -eq ([string]::Format('{0}.psm1', $ModuleName))) {
14+
return $TRUE;
15+
}
16+
17+
[int]$RelativPathStartIndex = $FileObject.FullName.IndexOf($ModuleName) + $ModuleName.Length;
18+
$ModuleFileRelativePath = $FileObject.FullName.SubString($RelativPathStartIndex, $FileObject.FullName.Length - $RelativPathStartIndex);
19+
20+
if ($ModuleFileRelativePath.Contains('\public\') -Or $ModuleFileRelativePath.Contains('\plugins\') -Or $ModuleFileRelativePath.Contains('\endpoint\') -Or $ModuleFileRelativePath.Contains('\daemon\')) {
21+
return $TRUE;
22+
}
23+
24+
return $FALSE;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
function Update-IcingaForWindowsManifestArray()
2+
{
3+
param (
4+
[string]$ManifestFile = '',
5+
[array]$ArrayVariableValues = @(),
6+
[string]$ArrayVariableName = ''
7+
);
8+
9+
if ([string]::IsNullOrEmpty($ArrayVariableName)) {
10+
return;
11+
}
12+
13+
# Remove duplicate entries
14+
$ArrayVariableValues = $ArrayVariableValues | Select-Object -Unique;
15+
16+
[array]$ManifestContent = Get-Content -Path $ManifestFile -ErrorAction SilentlyContinue;
17+
18+
if ($null -eq $ManifestContent -Or $ManifestContent.Count -eq 0) {
19+
Write-IcingaConsoleWarning 'The manifest file "{0}" could not be loaded for updating array element "{1}2' -Objects $ManifestFile, $ArrayVariableName;
20+
return;
21+
}
22+
23+
$ContentString = New-Object -TypeName 'System.Text.StringBuilder';
24+
[bool]$UpdatedArrayContent = $FALSE;
25+
26+
foreach ($entry in $ManifestContent) {
27+
[string]$ManifestLine = $entry;
28+
29+
if ($UpdatedArrayContent -And $entry -Like '*)*') {
30+
$UpdatedArrayContent = $FALSE;
31+
continue;
32+
}
33+
34+
if ($UpdatedArrayContent) {
35+
continue;
36+
}
37+
38+
if ($entry -Like ([string]::Format('*{0}*', $ArrayVariableName.ToLower()))) {
39+
if ($entry -NotLike '*)*') {
40+
$UpdatedArrayContent = $TRUE;
41+
}
42+
$ContentString.AppendLine(([string]::Format(' {0} = @(', $ArrayVariableName))) | Out-Null;
43+
44+
if ($ArrayVariableValues.Count -ne 0) {
45+
[array]$NestedModules = (ConvertFrom-IcingaArrayToString -Array $ArrayVariableValues -AddQuotes -UseSingleQuotes).Split(',');
46+
[int]$ModuleIndex = 0;
47+
foreach ($module in $NestedModules) {
48+
if ([string]::IsNullOrEmpty($module)) {
49+
continue;
50+
}
51+
52+
$ModuleIndex += 1;
53+
54+
if ($ModuleIndex -ne $NestedModules.Count) {
55+
if ($ModuleIndex -eq 1) {
56+
$ManifestLine = [string]::Format(' {0},', $module);
57+
} else {
58+
$ManifestLine = [string]::Format(' {0},', $module);
59+
}
60+
} else {
61+
if ($ModuleIndex -eq 1) {
62+
$ManifestLine = [string]::Format(' {0}', $module);
63+
} else {
64+
$ManifestLine = [string]::Format(' {0}', $module);
65+
}
66+
}
67+
68+
$ContentString.AppendLine($ManifestLine) | Out-Null;
69+
}
70+
}
71+
72+
$ContentString.AppendLine(' )') | Out-Null;
73+
continue;
74+
}
75+
76+
if ([string]::IsNullOrEmpty($ManifestLine.Replace(' ', '')) -Or $ManifestLine -eq "`r`n" -Or $ManifestLine -eq "`n") {
77+
continue;
78+
}
79+
80+
$ContentString.AppendLine($ManifestLine) | Out-Null;
81+
}
82+
83+
Write-IcingaFileSecure -File $ManifestFile -Value $ContentString.ToString();
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
function Write-IcingaForWindowsComponentCompilationFile()
2+
{
3+
param (
4+
[string]$ScriptRootPath = '',
5+
[string]$CompiledFilePath = ''
6+
);
7+
8+
# Get the current location and leave this folder
9+
Set-Location -Path $ScriptRootPath;
10+
Set-Location -Path '..';
11+
12+
# Store the location of the current file
13+
14+
# Now as we are inside the module root, get the name of the module and the path
15+
[string]$ModulePath = Get-Location;
16+
[string]$ModuleName = $ModulePath.Split('\')[-1];
17+
18+
# Fetch all '.psm1' files from this module content
19+
[array]$ModuleFiles = Get-ChildItem -Path $ModulePath -Recurse -Filter '*.psm1';
20+
# Get all public functions
21+
[array]$FunctionList = @();
22+
[array]$VariableList = @();
23+
[array]$AliasList = @();
24+
[array]$CmdletList = @();
25+
# Variable to store all of our module files
26+
[string]$CompiledModule = '';
27+
28+
foreach ($entry in $ModuleFiles) {
29+
# Ensure the compilation file never includes itself
30+
if ($entry.FullName -eq $CompiledFilePath) {
31+
continue;
32+
}
33+
34+
$FunctionList += Get-IcingaForWindowsComponentPublicFunctions -FileObject $entry -ModuleName $ModuleName;
35+
$FileConfig = (Read-IcingaPowerShellModuleFile -File $entry.FullName);
36+
$VariableList += $FileConfig.VariableList;
37+
$AliasList += $FileConfig.AliasList;
38+
$CmdletList += $FileConfig.ExportCmdlet;
39+
$CompiledModule += (Get-Content -Path $entry.FullName -Raw -Encoding 'UTF8');
40+
$CompiledModule += "`r`n";
41+
}
42+
43+
if ((Test-Path -Path $CompiledFilePath) -eq $FALSE) {
44+
New-Item -Path $CompiledFilePath -ItemType File -Force | Out-Null;
45+
}
46+
47+
$CompiledModule += "`r`n";
48+
$CompiledModule += [string]::Format(
49+
"Export-ModuleMember -Cmdlet @( {0} ) -Function @( {1} ) -Variable @( {2} ) -Alias @( {3} );",
50+
((ConvertFrom-IcingaArrayToString -Array ($CmdletList | Select-Object -Unique) -AddQuotes -UseSingleQuotes)),
51+
((ConvertFrom-IcingaArrayToString -Array ($FunctionList | Select-Object -Unique) -AddQuotes -UseSingleQuotes)),
52+
((ConvertFrom-IcingaArrayToString -Array ($VariableList | Select-Object -Unique) -AddQuotes -UseSingleQuotes)),
53+
((ConvertFrom-IcingaArrayToString -Array ($AliasList | Select-Object -Unique) -AddQuotes -UseSingleQuotes))
54+
);
55+
56+
Set-Content -Path $CompiledFilePath -Value $CompiledModule -Encoding 'UTF8';
57+
58+
Import-Module -Name $ModulePath -Force;
59+
Import-Module -Name $ModulePath -Force -Global;
60+
}

0 commit comments

Comments
 (0)