Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: CPF Merge #703

Merged
merged 36 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a9938d5
feat: implement an `intialize` lifecycle phase
isc-shuliu Jan 13, 2025
2e53f8f
feat: implement the CPF resource processor
isc-shuliu Jan 13, 2025
d3462df
chore: update changelog
isc-shuliu Jan 13, 2025
c2f9a7e
test: add test case for CPF merge
isc-shuliu Jan 13, 2025
9418f0b
refactor: modernize the code in %Initialize lifecycle
isc-shuliu Jan 14, 2025
56f4b33
refactor: change code based on feedback
isc-shuliu Jan 14, 2025
e580764
refactor: remove preloading in `LoadNewModule` but keep in `%Reload`
isc-shuliu Jan 14, 2025
9e083ea
fix: move preload from %Reload into %Initialize
isc-shuliu Jan 14, 2025
6f7c6cd
refactor: move cpf merge to before preload
isc-shuliu Jan 14, 2025
15bb05a
feat: implement CPF merge
isc-shuliu Jan 15, 2025
a8671c2
test: update cpf test cases
isc-shuliu Jan 15, 2025
a40a7e1
fix: fix incorrect file extension casing in integration test
isc-shuliu Jan 15, 2025
e7c7c12
test: typo fix
isc-shuliu Jan 15, 2025
0f5b5ba
fix: use $zf(-100) instead of Config.CPF:Merge as a workaround
isc-shuliu Jan 15, 2025
2d3eac7
test: re-add the test case for roles creation
isc-shuliu Jan 15, 2025
65bddb7
test: add more test cases for CPF merge
isc-shuliu Jan 15, 2025
b34b001
refactor: create datatypes for `Phase`, `When` and `CustomPhase`
isc-shuliu Jan 16, 2025
a651785
feat: allow running CPF before/after any phase
isc-shuliu Jan 16, 2025
cc34aa0
Merge branch 'v0.10.x' into v0.10.x-feat-init-cpf-merge
isc-shuliu Jan 21, 2025
83898eb
feat: phases with capital letters in the middle aren't all custom phases
isc-shuliu Jan 21, 2025
8ea47d5
feat: support CPF merge in custom phases
isc-shuliu Jan 21, 2025
77face7
test: adjust test cases for cpf merge during different phases
isc-shuliu Jan 21, 2025
37c9641
chore: update changelog about breaking compatibility with preload
isc-shuliu Jan 22, 2025
91baf3c
fix: fix a bug causing unconfigure to contain a trialing space
isc-shuliu Jan 22, 2025
280505e
refactor: unify error reporting for nonexistent CPF resource
isc-shuliu Jan 22, 2025
c6c680c
docs: improve documentation for CPF resource processor
isc-shuliu Jan 22, 2025
1b74d17
fix(ci): fix a bug in CI where $system is expanded in heredoc
isc-shuliu Jan 22, 2025
ddd5d47
refactor: add OnCustomPhase to Abstract ResourceProcessor
isc-shuliu Jan 22, 2025
687f1da
refactor: allow more versatile custom phase handling
isc-shuliu Jan 23, 2025
6818598
refactor: move custom phase props & methods to a mixin class
isc-shuliu Jan 23, 2025
dfe545a
fix(ci): bump version of upload artifact
isc-shuliu Jan 23, 2025
b8fc645
fix: check whether resource processors exists before using it
isc-shuliu Jan 23, 2025
70170b2
Merge branch 'v0.10.x' into v0.10.x-feat-init-cpf-merge
isc-shuliu Jan 23, 2025
51e9a50
fix: catch up fix
isc-shuliu Jan 23, 2025
94be840
feat: omit CustomPhase property in CPF processor which is inherited
isc-shuliu Jan 29, 2025
e2af5aa
Merge branch 'v0.10.x' into v0.10.x-feat-init-cpf-merge
isc-shuliu Jan 29, 2025
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #609 Added support for `-export-deps` when running the "Package" phase of lifecycle
- #541 Added support for ORAS repository
- #704 Added support for passing in env files via `-env /path/to/env1.json;/path/to/env2.json` syntax
- #702 Added a new lifecycle phase `Initialize` which is used for preload
- #702 Added a `<CPF/>` resource, which can be used for CPF merge before/after a specified lifecycle phase or in a custom lifecycle phase.

### Changed
-
- #702 Preload now happens as part of the new `Initialize` lifecycle phase. `zpm "<module> reload -only"` will no longer auto compile resources in `/preload` directory.

### Fixed
- #474: When loading a .tgz/.tar.gz package, automatically locate the top-most module.xml in case there is nested directory structure (e.g., GitHub releases)
Expand Down
7 changes: 7 additions & 0 deletions src/cls/IPM/DataType/CustomPhaseName.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Class %IPM.DataType.CustomPhaseName Extends %Library.String [ ClassType = datatype ]
{

/// The maximum number of characters the string can contain.
Parameter MAXLEN As INTEGER = 255;

}
14 changes: 14 additions & 0 deletions src/cls/IPM/DataType/PhaseName.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Class %IPM.DataType.PhaseName Extends %Library.String [ ClassType = datatype ]
{

/// The maximum number of characters the string can contain.
Parameter MAXLEN As INTEGER = 50;

/// Used for enumerated (multiple-choice) attributes.
/// <var>VALUELIST</var> is either a null string ("") or a delimiter
/// separated list (where the delimiter is the first character) of logical values.
/// If a non-null value is present, then the attribute is restricted to values
/// in the list, and the validation code simply checks to see if the value is in the list.
Parameter VALUELIST = ",Clean,Initialize,Reload,*,Validate,ExportData,Compile,Activate,Document,MakeDeployed,Test,Package,Verify,Publish,Configure,Unconfigure";

}
14 changes: 14 additions & 0 deletions src/cls/IPM/DataType/PhaseWhen.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Class %IPM.DataType.PhaseWhen Extends %Library.String [ ClassType = datatype ]
{

/// The maximum number of characters the string can contain.
Parameter MAXLEN As INTEGER = 50;

/// Used for enumerated (multiple-choice) attributes.
/// <var>VALUELIST</var> is either a null string ("") or a delimiter
/// separated list (where the delimiter is the first character) of logical values.
/// If a non-null value is present, then the attribute is restricted to values
/// in the list, and the validation code simply checks to see if the value is in the list.
Parameter VALUELIST = ",Before,After";

}
24 changes: 24 additions & 0 deletions src/cls/IPM/DataType/ResourceDirectory.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Class %IPM.DataType.ResourceDirectory Extends %Library.String [ ClassType = datatype ]
{

Parameter MAXLEN = 255;

/// Tests if the logical value <var>%val</var>, which is a string, is valid.
/// The validation is based on the class parameter settings used for the class attribute this data type is associated with.
/// In this case, <a href="#MINLEN">MINLEN</a>, <a href="#MAXLEN">MAXLEN</a>, <a href="#VALUELIST">VALUELIST</a>, and <a href="#PATTERN">PATTERN</a>.
ClassMethod IsValid(%val As %RawString) As %Status [ ServerOnly = 0 ]
{
If $Extract(%val) = "/" {
Return $$$ERROR($$$GeneralError, "Resource directory cannot start with a slash.")
isc-shuliu marked this conversation as resolved.
Show resolved Hide resolved
}
Set segments = $ListFromString(%val, "/")
Set ptr = 0
While $ListNext(segments, ptr, seg) {
If seg = ".." {
Return $$$ERROR($$$GeneralError, "For security reasons, resource directory cannot contain '..'.")
}
}
Return $$$OK
}

}
63 changes: 42 additions & 21 deletions src/cls/IPM/Lifecycle/Base.cls
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Property PhaseList As %List;

/// $ListBuild list of phases in this lifecycle. <br />
/// For each phase name, an instance method named "%<phase name>" must be defined in the class with a return type of %Status.
Parameter PHASES = {$ListBuild("Clean","Reload","*","Validate","ExportData","Compile","Activate","Document","MakeDeployed","Test","Package","Verify", "Publish", "Configure","Unconfigure")};
Parameter PHASES = {$ListBuild("Clean","Initialize", "Reload","*","Validate","ExportData","Compile","Activate","Document","MakeDeployed","Test","Package","Verify", "Publish", "Configure","Unconfigure")};

Property Payload As %Stream.Object [ Private ];

Expand Down Expand Up @@ -121,7 +121,7 @@ ClassMethod GetDefaultResourceProcessorProc(pLifecycleClass As %Dictionary.Class
ClassMethod GetCompletePhases(pPhases As %List) As %List
{
// If there is only phase, and the phase is not found in standard phases, it is a custom phase. Return as-is.
If ($ListLength(pPhases) = 1) && ('$ListFind(..#PHASES, $List(pPhases,1))) {
If ($ListLength(pPhases) = 1) && ('$ListFind(..#PHASES, ..MatchSinglePhase($List(pPhases,1)))) {
Return pPhases
}
For i=1:1:$ListLength(pPhases) {
Expand All @@ -142,18 +142,19 @@ ClassMethod GetCompletePhasesForOne(pOnePhase As %String) As %List

Quit $Case(pOnePhase,
"clean": $ListBuild("Clean"),
"reload": $ListBuild("Reload","*"),
"validate": $ListBuild("Reload","*","Validate"),
"initialize": $ListBuild("Initialize"),
"reload": $ListBuild("Initialize","Reload","*"),
"validate": $ListBuild("Initialize","Reload","*","Validate"),
"exportdata": $ListBuild("ExportData"),
"compile": $ListBuild("Reload","*","Validate","Compile"),
"activate": $ListBuild("Reload","*","Validate","Compile","Activate"),
"compile": $ListBuild("Initialize","Reload","*","Validate","Compile"),
"activate": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate"),
"document": $ListBuild("Document"),
"makedeployed": $ListBuild("MakeDeployed"),
"test": $ListBuild("Reload","*","Validate","Compile","Activate","Test"),
"package": $ListBuild("Reload","*","Validate","Compile","Activate","Package"),
"verify": $ListBuild("Reload","*","Validate","Compile","Activate","Package","Verify"),
"register": $ListBuild("Reload","*","Validate","Compile","Activate","Package","Register"),
"publish": $ListBuild("Reload","*","Validate","Compile","Activate","Package","Register","Publish"),
"test": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Test"),
"package": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Package"),
"verify": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Package","Verify"),
"register": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Package","Register"),
"publish": $ListBuild("Initialize","Reload","*","Validate","Compile","Activate","Package","Register","Publish"),
"configure": $ListBuild("Configure"),
"unconfigure": $ListBuild("Unconfigure"),
: ""
Expand All @@ -166,6 +167,7 @@ ClassMethod MatchSinglePhase(pOnePhase As %String) As %String
Set phase = $ZCONVERT(pOnePhase, "L")
Quit $Case(phase,
"clean": "Clean",
"initialize": "Initialize",
"reload": "Reload",
"validate": "Validate",
"exportdata": "ExportData",
Expand Down Expand Up @@ -505,6 +507,35 @@ Method %Unconfigure(ByRef pParams) As %Status
Quit tSC
}

Method %Initialize(ByRef pParams) As %Status
{
Set status = $$$OK
Try {
Set key = ""
For {
Set resource = ..Module.Resources.GetNext(.key)
Quit:key=""
If $IsObject(resource.Processor) {
Set status = $$$ADDSC(status,resource.Processor.OnPhase("Initialize",.pParams))
}
}

Set preloadRoot = $Get(pParams("RootDirectory"))_"preload"
Set verbose = $Get(pParams("Verbose"))
If ##class(%File).DirectoryExists(preloadRoot) {
Set tSC = $System.OBJ.ImportDir(preloadRoot, "*", $Select(verbose:"d",1:"-d")_$Select($Tlevel:"/multicompile=0", 1: ""), , 1, .tImported)
If $$$ISERR(tSC) { Quit }
Set tSC = ##class(%IPM.Utils.LegacyCompat).UpdateSuperclassAndCompile(.tImported)
If $$$ISERR(tSC) { Quit }
} ElseIf verbose {
Write !,"Skipping preload - directory does not exist."
}
} Catch ex {
Set status = ex.AsStatus()
}
Quit status
}

Method %Reload(ByRef pParams) As %Status
{
Set tSC = $$$OK
Expand Down Expand Up @@ -581,16 +612,6 @@ Method %Reload(ByRef pParams) As %Status

$$$ThrowOnError(..InstallPythonRequirements(tRoot, .pParams))

Set tPreloadRoot = tRoot_"preload"
If ##class(%File).DirectoryExists(tPreloadRoot) {
Set tSC = $System.OBJ.ImportDir(tPreloadRoot, "*", $Select(tVerbose:"d",1:"-d")_$Select($Tlevel:"/multicompile=0", 1: ""), , 1, .tImported)
If $$$ISERR(tSC) { Quit }
Set tSC = ##class(%IPM.Utils.LegacyCompat).UpdateSuperclassAndCompile(.tImported)
If $$$ISERR(tSC) { Quit }
} ElseIf tVerbose {
Write !,"Skipping preload - directory does not exist."
}

// Reload the module definition
Set tSC = ..Module.%Reload()
If $$$ISERR(tSC) {
Expand Down
88 changes: 88 additions & 0 deletions src/cls/IPM/ResourceProcessor/CPF.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
Include %IPM.Formatting

Class %IPM.ResourceProcessor.CPF Extends %IPM.ResourceProcessor.Abstract
{

/// Comma-separated list of resource attribute names that this processor uses
Parameter ATTRIBUTES As STRING = "Name,Directory,Phase,CustomPhase,When";

/// Description of resource processor class (shown in UI)
Parameter DESCRIPTION As STRING = "Merges the specified CPF file in the ""Initialize"" lifecycle phase.";
isc-shuliu marked this conversation as resolved.
Show resolved Hide resolved

/// Directory containing the CPF file to merge
Property Directory As %IPM.DataType.ResourceDirectory [ InitialExpression = "cpf" ];

/// FileN ame of the CPF merge file
Property Name As %IPM.DataType.ResourceName [ Required ];

/// The phase before/after which the CPF file should be merged
Property Phase As %IPM.DataType.PhaseName [ InitialExpression = "Initialize" ];

/// If provided, the Phase property will be ignored. The CPF merge will happen during this custom phase.
Property CustomPhase As %IPM.DataType.CustomPhaseName;
isc-shuliu marked this conversation as resolved.
Show resolved Hide resolved

/// When to merge the CPF file: Before or After the specified phase. This only applies to the standard phases.
Property When As %IPM.DataType.PhaseWhen [ InitialExpression = "Before" ];

Method OnBeforePhase(pPhase As %String, ByRef pParams) As %Status
{
If (..When = "Before") && (..Phase = pPhase) && (..CustomPhase = "") {
Quit ..DoMerge(.pParams)
}
Quit $$$OK
}

Method OnAfterPhase(pPhase As %String, ByRef pParams) As %Status
{
If (..When = "After") && (..Phase = pPhase) && (..CustomPhase = "") {
Quit ..DoMerge(.pParams)
}
Quit $$$OK
}

Method DoMerge(ByRef pParams) As %Status
{
Try {
Set verbose = $GET(pParams("Verbose"))
Set root = ..ResourceReference.Module.Root
Set sourcesRoot = ..ResourceReference.Module.SourcesRoot
// Use Construct first, rather than NormalizeFilename, so we don't have to deal with leading/trailing slashes
Set dir = $SELECT($$$isWINDOWS: $REPLACE(..Directory, "/", "\"), 1: ..Directory)
Set dir = ##class(%File).Construct(root, sourcesRoot, dir)
Set filename = ##class(%File).NormalizeFilename(..Name, dir)
If (filename = "") || ('##class(%File).Exists(filename)) {
$$$ThrowStatus($$$ERROR($$$GeneralError, $$$FormatText("CPF file '%1' not found in directory '%2'", ..Name, dir)))
}

Set stream = ##class(%Stream.FileCharacter).%New()
$$$ThrowOnError(stream.LinkToFile(filename))
If verbose {
Write !, "Merging CPF file: ", filename, !
Do stream.OutputToDevice()
}
Do ..MergeCPF(filename)
} Catch ex {
Return ex.AsStatus()
}
Return $$$OK
}

ClassMethod MergeCPF(file As %String)
{
// TODO The $zf(-100) callout is much slower than ##class(Config.CPF).Merge()
// Figure out why ##class(Config.CPF).Merge() doesn't work
// c.f. https://github.com/intersystems/ipm/pull/703#discussion_r1917290136

Set args($INCREMENT(args)) = "merge"
Set args($INCREMENT(args)) = ##class(%SYS.System).GetInstanceName()
Set args($INCREMENT(args)) = file

// Somehow, if the STDOUT is not set, the merge will silently fail
Set flags = "/SHELL/LOGCMD/STDOUT=""zf100stdout""/STDERR=""zf100stderr"""
Set status = $ZF(-100, flags, "iris", .args)
If status '= 0 {
$$$ThrowStatus($$$ERROR($$$GeneralError, "Error merging CPF file. $zf(-100) exited with "_status))
}
}

}
6 changes: 3 additions & 3 deletions src/cls/IPM/Storage/InvokeReference.cls
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ Property Class As %String(MAXLEN = 255, XMLPROJECTION = "ATTRIBUTE") [ Required

Property Method As %String(MAXLEN = 255, XMLPROJECTION = "ATTRIBUTE") [ Required ];

Property Phase As %String(MAXLEN = 255, XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = "Configure" ];
Property Phase As %IPM.DataType.PhaseName(XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = "Configure" ];

/// If provided, the Phase property will be ignored. This CustomPhase will be used and no corresponding lifecycle is required.
Property CustomPhase As %String(MAXLEN = 255, XMLPROJECTION = "ATTRIBUTE");
Property CustomPhase As %IPM.DataType.CustomPhaseName(XMLPROJECTION = "ATTRIBUTE");

Property When As %String(MAXLEN = 255, VALUELIST = ",Before,After", XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = "After", SqlFieldName = _WHEN ];
Property When As %IPM.DataType.PhaseWhen(XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = "After", SqlFieldName = _WHEN ];

Property CheckStatus As %Boolean(XMLPROJECTION = "ATTRIBUTE") [ InitialExpression = 0 ];

Expand Down
30 changes: 27 additions & 3 deletions src/cls/IPM/Storage/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,23 @@ Method GetCustomPhases(Output pPhases)
Set key = ""
For {
Set tInvoke = ..Invokes.GetNext(.key)
Quit:(key = "")
If key = "" {
Quit
}
If (tInvoke.CustomPhase '= "") {
Set pPhases($$$lcase(tInvoke.CustomPhase)) = tInvoke.CustomPhase
}
}
For {
Set tResource = ..Resources.GetNext(.key)
If key = "" {
Quit
}
Set tProcessor = tResource.Processor
If ($ClassName(tProcessor) = "%IPM.ResourceProcessor.CPF") && (tProcessor.CustomPhase '= "") {
isc-shuliu marked this conversation as resolved.
Show resolved Hide resolved
Set pPhases($$$lcase(tProcessor.CustomPhase)) = tProcessor.CustomPhase
}
}
}

/// Execute multiple lifecycle phases in sequence. Execution is terminated if one fails.
Expand Down Expand Up @@ -350,7 +362,20 @@ ClassMethod ExecutePhases(pModuleName As %String, pPhases As %List, pIsComplete
}
Quit:$$$ISERR(tSC)

If 'tIsCustomPhase {
If tIsCustomPhase {
// Handle CPF merges in custom phases
isc-shuliu marked this conversation as resolved.
Show resolved Hide resolved
Set tKey = ""
For {
Set tResource = tModule.Resources.GetNext(.tKey)
If tKey = "" {
Quit
}
Set tProcessor = tResource.Processor
If ($ClassName(tProcessor) = "%IPM.ResourceProcessor.CPF") && (tProcessor.CustomPhase = tOnePhase) {
$$$ThrowOnError(tProcessor.DoMerge(.pParams))
isc-shuliu marked this conversation as resolved.
Show resolved Hide resolved
}
}
} Else {
// Lifecycle before / (phase) / after
$$$ThrowOnError(tLifecycle.OnBeforePhase(tOnePhase,.pParams))
$$$ThrowOnError($Method(tLifecycle,"%"_tOnePhase,.pParams))
Expand Down Expand Up @@ -1612,4 +1637,3 @@ Storage Default
}

}

11 changes: 0 additions & 11 deletions src/cls/IPM/Utils/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1224,17 +1224,6 @@ ClassMethod LoadNewModule(pDirectory As %String, ByRef pParams, pRepository As %
}
}

Set tPath = pDirectory_"preload"
If ##class(%File).DirectoryExists(tPath) {
// Load first, update legacy superclasses, then compile
Set tSC = $system.OBJ.ImportDir(tPath,,$Select(tVerbose:"d",1:"-d")_$Select($TLevel:"/multicompile=0",1:""),,1,.tImported)
$$$ThrowOnError(tSC)
Set tSC = ##class(%IPM.Utils.LegacyCompat).UpdateSuperclassAndCompile(.tImported)
$$$ThrowOnError(tSC)
} Else {
Write:tVerbose !,"Skipping preload - directory does not exist."
}

Set tSC = $system.OBJ.Load(pDirectory_"module.xml",$Select(tVerbose:"d",1:"-d"),,.tLoadedList)
$$$ThrowOnError(tSC)
Set tSC = tModule.%Reload()
Expand Down
Loading
Loading