Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [0.10.7] - Unreleased

### Added
- #992: Implement automatic history purge logic
- #973: Enables CORS and JWT configuration for WebApplications in module.xml
- #1031: Add support for user-configurable ModuleRoot for IPM module installation

### Fixed
- #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace
Expand Down
1 change: 1 addition & 0 deletions preload/cls/IPM/Installer.cls
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ ClassMethod ZPMInit(
$$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("UseStandalonePip", "", 0))
$$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("SemVerPostRelease", 0, 0))
$$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("DefaultLogEntryLimit",20, 0))
$$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("ModuleRoot",##class(%SYSTEM.Util).DataDirectory() _ "ipm/", 0))
quit $$$OK
}

Expand Down
26 changes: 20 additions & 6 deletions src/cls/IPM/Repo/UniversalSettings.cls
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,9 @@ Parameter UseStandalonePip = "UseStandalonePip";
/// Default value is 0, where 1.0.0-anystring is considered a pre-release of 1.0.0, hence 1.0.0-anystring < 1.0.0
Parameter SemVerPostRelease = "SemVerPostRelease";

/// Configuration setting name used to determine the number of days
/// to retain IPM history records before they are eligible for cleanup.
Parameter HistoryRetain = "history_retain";
Parameter ModuleRoot = "ModuleRoot";

Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit,HistoryRetain";
Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit,ModuleRoot";

/// Returns configArray, that includes all configurable settings
ClassMethod GetAll(Output configArray) As %Status
Expand Down Expand Up @@ -104,6 +102,9 @@ ClassMethod UpdateOne(
write "Config key = """_key_""" not found",!
quit
}
if key="ModuleRoot" {
do ..CheckDirPermission(value)
}
set sc = ..SetValue($parameter(..%ClassName(1),key), value)
if $$$ISOK(sc) {
write "Key """_key_""" succesfully updated",!
Expand Down Expand Up @@ -185,9 +186,22 @@ ClassMethod SetAnalyticsAvailable(
return ..SetValue(..#analytics, +val, overwrite)
}

ClassMethod GetHistoryRetain() As %Integer
ClassMethod GetMouleRoot() As %String
{
return ..GetValue(..#ModuleRoot)
}

ClassMethod CheckDirPermission(directory As %String = "")
{
return ..GetValue(..#HistoryRetain)
if directory="" {
$$$ThrowOnError($$$ERROR($$$DirectoryNameRequired))
}
set directory = ##class(%File).NormalizeDirectory(directory)
if '##class(%File).DirectoryExists(directory) {
$$$ThrowOnError($$$ERROR($$$DirectoryNotFound))
} elseif '##class(%File).Writeable(directory) {
$$$ThrowOnError($$$ERROR($$$GeneralError,"No write permission on directory "_directory))
}
}

}
6 changes: 5 additions & 1 deletion src/cls/IPM/ResourceProcessor/Abstract.cls
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,11 @@ Method %Evaluate(pAttrValue As %String) As %String [ Internal ]
set tAttrValue = ##class(%IPM.Utils.Module).%EvaluateMacro(tAttrValue)
set attrValue = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(tAttrValue)
set customParams("packagename") = ..ResourceReference.Module.Name
set customParams("ipmDir") = ##class(%Library.File).NormalizeDirectory($system.Util.DataDirectory() _ "ipm/" _ ..ResourceReference.Module.Name _ "/" _ ..ResourceReference.Module.VersionString)
set ipmDir = ##class(%IPM.Repo.UniversalSettings).GetMouleRoot()
if ipmDir="" {
set ipmDir = $system.Util.DataDirectory() _ "ipm/"
}
set customParams("ipmDir") = ##class(%Library.File).NormalizeDirectory(ipmDir _ ..ResourceReference.Module.Name _ "/" _ ..ResourceReference.Module.VersionString)
set attrValue = ##class(%IPM.Storage.ModuleSetting.Default).EvaluateAttribute(attrValue,.customParams)
set root = ..ResourceReference.Module.Root
if (root '= "") {
Expand Down
15 changes: 13 additions & 2 deletions src/cls/IPM/Utils/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,19 @@ ClassMethod LoadModuleFromArchive(
try {
set tVerbose = $get(pParams("Verbose"))

// Modules have a well-defined location inside the archive
set tTargetDirectory = ##class(%Library.File).NormalizeDirectory(##class(%SYSTEM.Util).DataDirectory() _ "ipm/" _ pModuleName _ "/" _ pModuleVersion)
// Determine the base installation directory for IPM modules.
// Precedence:
// 1. User-configured 'ModuleRoot' via UniversalSettings.
// 2. Default system location (IRIS data directory /ipm/).
set ipmRoot = ##class(%IPM.Repo.UniversalSettings).GetMouleRoot()
if ipmRoot'="" {
set impRoot = ##class(%File).NormalizeDirectory(ipmRoot)
}
else {
set ipmRoot = ##class(%SYSTEM.Util).DataDirectory() _ "ipm/"
}
set ipmModuleRootDir = ipmRoot_ pModuleName _ "/" _ pModuleVersion
set tTargetDirectory = ##class(%Library.File).NormalizeDirectory(ipmModuleRootDir)
if ##class(%File).DirectoryExists(tTargetDirectory) {
// Delete it.
set tSC = ##class(%IPM.Utils.File).RemoveDirectoryTree(tTargetDirectory)
Expand Down
119 changes: 98 additions & 21 deletions tests/unit_tests/Test/PM/Unit/UniversalSettings.cls
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,123 @@ Parameter TestIndx As STRING = "TestIndx";

Method TestSetValueOverwrite()
{
Do ##class(%IPM.Repo.UniversalSettings).SetValue(..#TestIndx, "new value a", )
Set value = ##class(%IPM.Repo.UniversalSettings).GetValue(..#TestIndx)
Do $$$AssertEquals(value, "new value a")
do ##class(%IPM.Repo.UniversalSettings).SetValue(..#TestIndx, "new value a", )
set value = ##class(%IPM.Repo.UniversalSettings).GetValue(..#TestIndx)
do $$$AssertEquals(value, "new value a")

Do ##class(%IPM.Repo.UniversalSettings).SetValue(..#TestIndx, "new value b", 1)
Set value = ##class(%IPM.Repo.UniversalSettings).GetValue(..#TestIndx)
Do $$$AssertEquals(value, "new value b")
do ##class(%IPM.Repo.UniversalSettings).SetValue(..#TestIndx, "new value b", 1)
set value = ##class(%IPM.Repo.UniversalSettings).GetValue(..#TestIndx)
do $$$AssertEquals(value, "new value b")
}

Method TestSetValueNoOverwrite()
{
Do ##class(%IPM.Repo.UniversalSettings).SetValue(..#TestIndx, "new value c", 0)
Set value = ##class(%IPM.Repo.UniversalSettings).GetValue(..#TestIndx)
Do $$$AssertEquals(value, "default value")
do ##class(%IPM.Repo.UniversalSettings).SetValue(..#TestIndx, "new value c", 0)
set value = ##class(%IPM.Repo.UniversalSettings).GetValue(..#TestIndx)
do $$$AssertEquals(value, "default value")
}

/// Run by <B>RunTest</B> immediately after each test method in the test class is run.<br>
/// <dl>
/// <dt><i>testname</i>
/// <dd>Name of the test to be run. Required.
/// </dl>
/// <dd>Name of the test to be run. Required.
/// </dl>
Method OnAfterOneTest(testname As %String) As %Status
{
New $NAMESPACE
Set $NAMESPACE = "%SYS"
Kill ^IPM.settings(..#TestIndx)
Quit $$$OK
new $namespace
set $namespace = "%SYS"
kill ^IPM.settings(..#TestIndx)
quit $$$OK
}

/// Run by <B>RunTest</B> immediately before each test method in the test class is run.<br>
/// <dl>
/// <dt><i>testname</i>
/// <dd>Name of the test to be run. Required.
/// </dl>
/// <dd>Name of the test to be run. Required.
/// </dl>
Method OnBeforeOneTest(testname As %String) As %Status
{
New $NAMESPACE
Set $NAMESPACE = "%SYS"
Set ^IPM.settings(..#TestIndx) = "default value"
Quit $$$OK
new $namespace
set $namespace = "%SYS"
set ^IPM.settings(..#TestIndx) = "default value"
quit $$$OK
}

Method TestModuleRootConfiguration()
{
#define NormalizeDirectory(%dir) ##class(%File).NormalizeDirectory(%dir)
// Define the system default for comparison
//ZPMInit method setup - set the global to default ipm directory
do ##class(%IPM.Repo.UniversalSettings).SetValue("ModuleRoot",##class(%SYSTEM.Util).DataDirectory() _ "ipm/", 1)

do $$$LogMessage("Running on "_$namespace_ "namespace")

do ##class(%IPM.Main).GetVersion("zpm",.out)
if $get(out)="" {
do $$$LogMessage("No registry configured. So started configuring https://pm.community.intersystems.com")
set status = ..RunCommand("repo -remote -n registry -url https://pm.community.intersystems.com/ -user """" -pass """"")
do $$$AssertStatusOK(status, "Registory configured successfully")
}
set defaultRoot = $$$NormalizeDirectory(##class(%SYSTEM.Util).DataDirectory() _ "ipm/")

set moduleObj = ##class(%IPM.Storage.Module).NameOpen("objectscript-math")
if $isobject(moduleObj) {
set status = ..RunCommand("uninstall objectscript-math")
do $$$AssertStatusOK(status, "Module 'objectscript-math' uninstalled for testing the module root check.")
}

do $$$LogMessage("Phase 1: Verifying installation using default system paths.")
set status = ..RunCommand("install objectscript-math")
do $$$AssertStatusOK(status, "Module 'objectscript-math' installed successfully using default paths.")

set moduleObj = ##class(%IPM.Storage.Module).NameOpen("objectscript-math")
set moduleRoot = $$$NormalizeDirectory($piece(moduleObj.Root, "objectscript-math"))

do $$$AssertTrue(moduleRoot=defaultRoot, "Verified: Module root '"_moduleRoot_"' matches system default: '"_defaultRoot_"'")

do $$$LogMessage("Phase 2: Testing permission validation for restricted directories.")
try {
// Attempting to set a restricted directory should trigger the validation logic you wrote
do ##class(%IPM.Repo.UniversalSettings).UpdateOne("ModuleRoot", "/usr")
} catch ex {
// Fallback if the method throws an exception instead of returning a status
do $$$AssertTrue(1, "Correctly blocked setting ModuleRoot to a restricted directory (/usr).")
}

do $$$LogMessage("Phase 3: Configuring a custom valid ModuleRoot.")
set customPath = "/tmp/" // Ensure it is treated as a directory
set status = ##class(%IPM.Repo.UniversalSettings).UpdateOne("ModuleRoot", customPath)
do $$$AssertStatusOK(status, "Successfully updated ModuleRoot to custom path: " _ customPath)

do $$$LogMessage("Verify the ModuleRoot directory.")

do $$$LogMessage("Module Dir: "_##class(%IPM.Repo.UniversalSettings).GetValue("ModuleRoot"))

do $$$LogMessage("Phase 4: Reinstalling module to verify path redirection.")
set status = ..RunCommand("uninstall objectscript-math")
do $$$AssertStatusOK(status, "Uninstalled 'objectscript-math' to prepare for relocation test.")

set status = ..RunCommand("install objectscript-math")
do $$$AssertStatusOK(status, "Reinstalled 'objectscript-math' successfully.")

set moduleObj = ##class(%IPM.Storage.Module).NameOpen("objectscript-math")
set moduleRoot = $$$NormalizeDirectory($piece(moduleObj.Root, "objectscript-math"))

// Verify the root has actually changed
do $$$AssertTrue(moduleRoot'=defaultRoot, "Verified: Module is no longer in default root.")
do $$$AssertTrue(moduleRoot[customPath, "Verified: Module is now installed in custom root: " _ moduleRoot)

set status = ##class(%IPM.Repo.UniversalSettings).ResetToDefault("ModuleRoot")
if $$$ISOK(status) {
do $$$AssertStatusOK(status, "reset the ModuleRoot directory: "_##class(%IPM.Repo.UniversalSettings).GetValue("ModuleRoot"))
}
}

Method RunCommand(command) As %Status
{
set status = ##class(%IPM.Main).Shell(command)
do $$$LogMessage("Run command: "_command)
return status
}

}