diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cf33e9a..1ba11bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.10.7] - Unreleased ### Added +- #1141: New `Production` resource processor that allows managing auto-start and auto-update of interoperability productions. +- #1141: New `DecomposedProduction` resource processor for managing an interoperability production class constituted from PTD files. - #408: Modules can now list dependencies without specifying version; will be assumed to be "*" - #992: Implement automatic history purge logic - #973: Enables CORS and JWT configuration for WebApplications in module.xml diff --git a/src/cls/IPM/ResourceProcessor/DecomposedProduction.cls b/src/cls/IPM/ResourceProcessor/DecomposedProduction.cls new file mode 100644 index 00000000..ceb0e1cc --- /dev/null +++ b/src/cls/IPM/ResourceProcessor/DecomposedProduction.cls @@ -0,0 +1,143 @@ +/// Resource processor for IRIS Interoperability productions stored as decomposed PTD files. +/// The production class is fully constituted by importing PTD files from the ptd/ subdirectory. +Class %IPM.ResourceProcessor.DecomposedProduction Extends %IPM.ResourceProcessor.Abstract +{ + +Parameter DESCRIPTION As STRING = "Manages an IRIS Interoperability production class constituted from PTD files in the ptd/ subdirectory."; + +Parameter ATTRIBUTES As STRING = "Name"; + +/// The class name of the production to manage. +Property Name As %String(MAXLEN = "") [ Required ]; + +ClassMethod IsSupported() As %Boolean +{ + quit ##class(%Dictionary.ClassDefinition).%ExistsId("%Studio.SourceControl.Production") +} + +Method OnGetUniqueName(Output pUniqueName) +{ + set pUniqueName = ..Name _ ".CLS" +} + +Method OnBeforePhase( + pPhase As %String, + ByRef pParams) As %Status +{ + set sc = $$$OK + try { + set sc = ##super(pPhase, .pParams) + if $$$ISERR(sc) { + quit + } + + if (pPhase = "Validate") { + set sysReqs = ..ResourceReference.Module.SystemRequirements + if '$isobject(sysReqs) || (sysReqs.Interoperability '= "enabled") { + set sc = $$$ERROR($$$GeneralError, $$$FormatText("Module '%1' must declare Interoperability in SystemRequirements to use the DecomposedProduction resource processor.", ..ResourceReference.Module.Name)) + quit + } + + if '##class(%IPM.ResourceProcessor.DecomposedProduction).IsSupported() { + set sc = $$$ERROR($$$GeneralError, "DecomposedProduction resource processor is not supported on this InterSystems IRIS version.") + quit + } + } + } catch ex { + set sc = ex.AsStatus() + } + quit sc +} + +Method OnAfterPhase( + pPhase As %String, + ByRef pParams) As %Status +{ + set sc = $$$OK + try { + set sc = ##super(pPhase, .pParams) + if $$$ISERR(sc) { + quit + } + + if (pPhase = "Compile") { + // See %Studio.SourceControl.Production for details on how decomposed productions are represented on the file system + set ptdDir = ..ResourceReference.Module.Root _ "ptd/" _ $translate(..Name, ".", "_") _ "/" + set sc = ##class(%Studio.SourceControl.Production).ImportPTDsDir(ptdDir) + if $$$ISERR(sc) { + quit + } + } + } catch ex { + set sc = ex.AsStatus() + } + quit sc +} + +Method OnResolveChildren( + ByRef pResourceArray, + pCheckModuleOwnership As %Boolean) As %Status +{ + // the production class itself should NOT be controlled by another package resource + set pResourceArray(..Name _ ".CLS") = "" + quit $$$OK +} + +Method OnBeforeArtifact( + pExportDirectory As %String, + pWorkingDirectory As %String, + ByRef pParams) As %Status +{ + set sc = $$$OK + try { + if (pExportDirectory = pWorkingDirectory) { + quit + } + + if '..ResourceReference.Deploy { + set subDir = "ptd/" _ $translate(..Name, ".", "_") _ "/" + set sc = ##class(%IPM.Utils.File).CopyDir( + pExportDirectory _ subDir, + pWorkingDirectory _ subDir) + } + } catch ex { + set sc = ex.AsStatus() + } + quit sc +} + +Method OnPhase( + pPhase As %String, + ByRef pParams, + Output pResourceHandled As %Boolean = 0) As %Status +{ + set sc = $$$OK + try { + if (pPhase = "Clean") { + set pResourceHandled = 1 + + set sc = ##class(Ens.Director).GetProductionStatus(.runningName, .state) + if $$$ISERR(sc) { + quit + } + + if (state '= 0) && (runningName = ..Name) { + set timeout = ##class(Ens.Director).GetRunningProductionShutdownTimeout() + set sc = ##class(Ens.Director).StopProduction(timeout) + if $$$ISERR(sc) { + quit + } + } + + set sc = ##class(Ens.Director).DeleteProduction(..Name) + if $$$ISERR(sc) { + quit + } + } + } catch ex { + set sc = ex.AsStatus() + } + quit sc +} + +} diff --git a/src/cls/IPM/ResourceProcessor/Production.cls b/src/cls/IPM/ResourceProcessor/Production.cls new file mode 100644 index 00000000..c53c517b --- /dev/null +++ b/src/cls/IPM/ResourceProcessor/Production.cls @@ -0,0 +1,149 @@ +/// Resource processor for IRIS Interoperability productions. +/// Manages runtime state: autostart registration, auto-update on activate, and stop on uninstall. +Class %IPM.ResourceProcessor.Production Extends %IPM.ResourceProcessor.Abstract +{ + +Parameter DESCRIPTION As STRING = "Manages the runtime lifecycle of an IRIS Interoperability production: autostart, auto-update on activate, and stopping on uninstall."; + +Parameter ATTRIBUTES As STRING = "Name,AutoStart,AutoUpdate"; + +/// The class name of the production to manage. +Property Name As %String(MAXLEN = "") [ Required ]; + +/// If true, registers this production as the namespace autostart production and starts it during Activate. +Property AutoStart As %Boolean [ InitialExpression = 0 ]; + +/// If true, queues an update if the production needs updating during Activate. +Property AutoUpdate As %Boolean [ InitialExpression = 1 ]; + +Method OnGetUniqueName(Output pUniqueName) +{ + // an arbitrary extension chosen not to collide with future document types + set pUniqueName = ..Name _ ".ZPRODUCTION" +} + +Method OnBeforePhase( + pPhase As %String, + ByRef pParams) As %Status +{ + set sc = $$$OK + try { + set sc = ##super(pPhase, .pParams) + if $$$ISERR(sc) { + quit + } + + if (pPhase = "Validate") { + set sysReqs = ..ResourceReference.Module.SystemRequirements + if '$isobject(sysReqs) || (sysReqs.Interoperability '= "enabled") { + set sc = $$$ERROR($$$GeneralError, $$$FormatText("Module '%1' must declare to use the Production resource processor.", ..ResourceReference.Module.Name)) + quit + } + } + } catch ex { + set sc = ex.AsStatus() + } + quit sc +} + +Method OnAfterPhase( + pPhase As %String, + ByRef pParams) As %Status +{ + set sc = $$$OK + try { + set sc = ##super(pPhase, .pParams) + if $$$ISERR(sc) { + quit + } + + if (pPhase = "Activate") { + if ..AutoStart { + // Set this production as the namespace autostart production + set sc = ##class(Ens.Director).SetAutoStart(..Name) + if $$$ISERR(sc) { + quit + } + + set sc = ##class(Ens.Director).GetProductionStatus(.runningName, .state) + if $$$ISERR(sc) { + quit + } + + if state = 0 { + // StartProduction issues transaction control statements that break + // IPM's outer transaction, so run it in a worker job + set workmgr = $system.WorkMgr.%New() + if workmgr = "" { + set sc = %objlasterror + quit + } + set sc = workmgr.Queue("##class(Ens.Director).StartProduction", ..Name) + if $$$ISERR(sc) { + quit + } + set sc = workmgr.Sync() + if $$$ISERR(sc) { + quit + } + } + } + + if ..AutoUpdate { + if ##class(Ens.Director).ProductionNeedsUpdate() { + set timeout = ##class(Ens.Director).GetRunningProductionUpdateTimeout() + set workmgr = $system.WorkMgr.%New() + if workmgr = "" { + set sc = %objlasterror + quit + } + set sc = workmgr.Queue("##class(Ens.Director).UpdateProduction", timeout) + if $$$ISERR(sc) { + quit + } + } + } + } + } catch ex { + set sc = ex.AsStatus() + } + quit sc +} + +Method OnPhase( + pPhase As %String, + ByRef pParams, + Output pResourceHandled As %Boolean = 0) As %Status +{ + set sc = $$$OK + try { + if (pPhase = "Clean") { + set pResourceHandled = 1 + + set sc = ##class(Ens.Director).GetProductionStatus(.runningName, .state) + if $$$ISERR(sc) { + quit + } + + if (state '= 0) && (runningName = ..Name) { + set timeout = ##class(Ens.Director).GetRunningProductionShutdownTimeout() + set sc = ##class(Ens.Director).StopProduction(timeout) + if $$$ISERR(sc) { + quit + } + } + + if ..AutoStart { + set sc = ##class(Ens.Director).SetAutoStart("") + if $$$ISERR(sc) { + quit + } + } + } + } catch ex { + set sc = ex.AsStatus() + } + quit sc +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/DecomposedProduction.cls b/tests/integration_tests/Test/PM/Integration/DecomposedProduction.cls new file mode 100644 index 00000000..5600cde1 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/DecomposedProduction.cls @@ -0,0 +1,100 @@ +Class Test.PM.Integration.DecomposedProduction Extends Test.PM.Integration.Base +{ + +Parameter NEEDSREGISTRY As BOOLEAN = 0; + +Method OnBeforeAllTests() As %Status +{ + set status = ##class(%IPM.Main).Shell("repo -n integration-tests -fs -path /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/") + $$$ThrowOnError(status) + if '##class(%IPM.Storage.SystemRequirements).IsInteroperabilityEnabled() { + set status = ##class(%EnsembleMgr).EnableNamespace($namespace, 1) + $$$ThrowOnError(status) + } + quit $$$OK +} + +Method OnBeforeOneTest(testname As %String) As %Status +{ + if '##class(%IPM.ResourceProcessor.DecomposedProduction).IsSupported() { + do $$$AssertSkipped("DecomposedProduction requires %Studio.SourceControl.Production, which is not available on this IRIS version.") + } + quit $$$OK +} + +Method TestLoadFromDirectory() +{ + set status = $$$OK + try { + set moduleDir = ..GetModuleDir("decomposed-production") + set status = ##class(%IPM.Main).Shell("load -verbose " _ moduleDir) + do $$$AssertStatusOK(status, "Loaded module-a from directory.") + + do $$$AssertTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Sample.NewProduction"), "Production class Sample.NewProduction exists after load.") + + set prodConfig = ##class(Ens.Config.Production).%OpenId("Sample.NewProduction", , .openSC) + do $$$AssertStatusOK(openSC, "Ens.Config.Production record for Sample.NewProduction is accessible.") + do $$$AssertTrue($isobject(prodConfig), "Ens.Config.Production object is valid.") + + set status = ##class(%IPM.Main).Shell("uninstall module-a -r") + do $$$AssertStatusOK(status, "Uninstalled module-a.") + + do $$$AssertTrue('##class(%Dictionary.ClassDefinition).%ExistsId("Sample.NewProduction"), "Production class Sample.NewProduction removed after uninstall.") + } catch ex { + do $$$AssertStatusOK(ex.AsStatus(), "An exception occurred in TestLoadFromDirectory.") + } +} + +Method TestAutoStart() +{ + set status = $$$OK + try { + set moduleDir = ..GetModuleDir("decomposed-production") + set status = ##class(%IPM.Main).Shell("load -verbose " _ moduleDir) + do $$$AssertStatusOK(status, "Loaded module-a from directory.") + + do $$$AssertEquals($get(^Ens.AutoStart), "Sample.NewProduction") + + set status = ##class(%IPM.Main).Shell("uninstall module-a -r") + do $$$AssertStatusOK(status, "Uninstalled module-a.") + + do $$$AssertEquals($get(^Ens.AutoStart), "") + } catch ex { + do $$$AssertStatusOK(ex.AsStatus(), "An exception occurred in TestAutoStart.") + } +} + +Method TestPackagePublishInstall() +{ + set status = $$$OK + try { + set status = ##class(%IPM.Main).Shell("repo -o -name zot -url http://oras:5000") + do $$$AssertStatusOK(status, "Configured Zot registry.") + + set moduleDir = ..GetModuleDir("decomposed-production") + set status = ##class(%IPM.Main).Shell("load -verbose " _ moduleDir) + do $$$AssertStatusOK(status, "Loaded module-a from directory.") + + set status = ##class(%IPM.Main).Shell("publish module-a -r zot -verbose") + do $$$AssertStatusOK(status, "Published module-a to Zot registry.") + + set status = ##class(%IPM.Main).Shell("uninstall module-a -r") + do $$$AssertStatusOK(status, "Uninstalled module-a before reinstall from registry.") + + do $$$AssertNotTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Sample.NewProduction"), "Production class removed after uninstall.") + + set status = ##class(%IPM.Main).Shell("install module-a -verbose") + do $$$AssertStatusOK(status, "Installed module-a from Zot registry.") + + do $$$AssertTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Sample.NewProduction"), "Production class Sample.NewProduction exists after install from registry.") + + set status = ##class(%IPM.Main).Shell("uninstall module-a -r") + do $$$AssertStatusOK(status, "Final uninstall of module-a.") + } catch ex { + do $$$AssertStatusOK(ex.AsStatus(), "An exception occurred in TestPackagePublishInstall.") + } + // Always clean up the Zot repo config + do ##class(%IPM.Main).Shell("repo -n zot -delete") +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/module.xml b/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/module.xml new file mode 100644 index 00000000..305be3be --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/module.xml @@ -0,0 +1,12 @@ + + + + + module-a + 1.0.0 + + + + + + \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/ProdStgs-Sample_NewProduction.xml b/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/ProdStgs-Sample_NewProduction.xml new file mode 100644 index 00000000..e06f1ca0 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/ProdStgs-Sample_NewProduction.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + +Sample.NewProduction +1841-01-01 00:00:00.000 + + + + +ProductionSettings-Sample_NewProduction +ProductionSettings:Sample.NewProduction.PTD + + + + +]]> + + + +2 +]]> + diff --git a/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/Stgs-and another one38E0.xml b/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/Stgs-and another one38E0.xml new file mode 100644 index 00000000..078f8933 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/Stgs-and another one38E0.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + +Sample.NewProduction +1841-01-01 00:00:00.000 + + + + +Settings-and another one +Settings:and another one.PTD + + + + +]]> + + + + +]]> + diff --git a/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/Stgs-hello5480.xml b/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/Stgs-hello5480.xml new file mode 100644 index 00000000..1b329ec8 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/decomposed-production/ptd/Sample_NewProduction/Stgs-hello5480.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + +Sample.NewProduction +1841-01-01 00:00:00.000 + + + + +Settings-hello +Settings:hello.PTD + + + + +]]> + + + + +]]> +