From 98a25fe5d23847bcd1f6b2fd84bce05cd55fca5f Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Wed, 10 Dec 2025 10:26:42 -0500 Subject: [PATCH 1/7] Add tests and -force flag to unpublish command --- CHANGELOG.md | 2 + src/cls/IPM/Main.cls | 36 +++--- src/cls/IPM/Storage/Module.cls | 22 ++-- .../Integration/ExternalNameModuleAction.cls | 30 ++--- .../PM/Integration/PublishExternalName.cls | 108 +++++++++++++++--- 5 files changed, 139 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e6015a3..11d352df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #950: Added support for listing installed Python packages using `list -python`, `list -py` and `list-installed -python` - #822: The CPF resource processor now supports system expressions and macros in CPF merge files - #578 Added functionality to record and display IPM history of install, uninstall, load, and update +- #959: External name can now be used interchangeably with (default) name for `install` and `update`, i.e. a module published with its (default) name can be installed using its external name. +- #951: The `unpublish` command will skip user confirmation prompt if the `-force` flag is provided. ### Changed - #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index db2b1f69..14111214 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -177,11 +177,11 @@ This command is an alias for `module-action module-name makedeployed` Delete package from repository -unpublish MyModuleName all -unpublish MyModuleName 1.0.0 +unpublishMyModuleName all +unpublish MyRepo/MyModuleName 1.0.0 - + @@ -2471,6 +2471,7 @@ ClassMethod RunOnePhase(ByRef pCommandInfo) [ Internal ] ClassMethod Unpublish(ByRef pCommandInfo) [ Internal ] { + #; merge ^mtempdc($zhorolog,"pCommandInfo") = pCommandInfo if ( ($get(pCommandInfo("parameters","module"))="") || ($get(pCommandInfo("parameters","version"))="") ) { set tCommandInfo("parameters","command") = "unpublish" do ..%Help(.tCommandInfo) @@ -2503,23 +2504,26 @@ ClassMethod Unpublish(ByRef pCommandInfo) [ Internal ] if (isEnabled) { set tResult = 0 - if ($$$lcase(tVersion)="all") { - write $$$FormattedLine($$$Red, "Deleting a package and all its versions is an irreversible action") - set tHelp = "Enter ""Yes"" if you want to delete all package versions." - set tMsg = "Are you sure you want to delete all versions of the package """_tModuleName_""" from registry """_tServer.Name_""" ("_tServer.URL_")?" - } else { - write $$$FormattedLine($$$Red, "Deleting a package version is an irreversible action") - set tHelp = "Enter ""Yes"" if you want to delete selected package version." - set tMsg = "Are you sure you want to delete the package """_tModuleName_" "_tVersion_""" from registry """_tServer.Name_""" ("_tServer.URL_")?" - } + set force = $data(pCommandInfo("modifiers","force")) + if 'force { + if ($$$lcase(tVersion)="all") { + write $$$FormattedLine($$$Red, "Deleting a package and all its versions is an irreversible action") + set tHelp = "Enter ""Yes"" if you want to delete all package versions." + set tMsg = "Are you sure you want to delete all versions of the package """_tModuleName_""" from registry """_tServer.Name_""" ("_tServer.URL_")?" + } else { + write $$$FormattedLine($$$Red, "Deleting a package version is an irreversible action") + set tHelp = "Enter ""Yes"" if you want to delete selected package version." + set tMsg = "Are you sure you want to delete the package """_tModuleName_" "_tVersion_""" from registry """_tServer.Name_""" ("_tServer.URL_")?" + } - set tResponse = ##class(%Library.Prompt).GetYesNo(tMsg,.tResult,.tHelp) + set tResponse = ##class(%Library.Prompt).GetYesNo(tMsg,.tResult,.tHelp) - if (tResponse '= $$$SuccessResponse) { - $$$ThrowStatus($$$ERROR($$$GeneralError,"Operation cancelled.")) + if (tResponse '= $$$SuccessResponse) { + $$$ThrowStatus($$$ERROR($$$GeneralError,"Operation cancelled.")) + } } - if (tResult) { + if (tResult || force) { $$$ThrowOnError(tManager.Unpublish(tServer.Name, tModuleName, tVersion)) write !!,"Package deleted" } diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index f512d430..c0418c65 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -16,7 +16,7 @@ Property VersionString As %String(MAXLEN = 100, XMLNAME = "Version") [ InitialEx /// Does not need comparison method to be code generated because that comparing VersionString is good enough. Property Version As %IPM.General.SemanticVersion(ForceCodeGenerate = 0, XMLPROJECTION = "NONE") [ Required ]; -Property ExternalName As %String(MAXLEN = 255); +Property ExternalName As %IPM.DataType.ModuleName; Index ExternalName On ExternalName [ Unique ]; @@ -159,12 +159,12 @@ Method HandleAllUpdateSteps( } /// For a given version, seed or execute update steps listed in the module's update package version class -/// +/// /// If seeding: For only steps that don't have a TimeStampEnd value or have an errored status, set it's TimeStampEnd to the current time -/// +/// /// If executing: Only start running steps at the first one found not to have a TimeStampEnd value or if an errored step is found /// After finding this step, run it and all subsequent steps, regardless of if they have been run or not. -/// +/// /// newStepToApply - Gets marked as true to tell us to run all remaining steps in the list, whether or not they have been run/seeded before /// Can carry this over to subsequent calls by passing the newStepToApply variable to those HandleUpdateStepsFromList() calls Method HandleUpdateStepsFromList( @@ -1336,7 +1336,7 @@ Method OverrideLifecycleClassSet(pValue As %Dictionary.Classname) As %Status /// This callback method is invoked by the %New method to /// provide notification that a new instance of an object is being created. -/// +/// ///

If this method returns an error then the object will not be created. ///

It is passed the arguments provided in the %New call. /// When customizing this method, override the arguments with whatever variables and types you expect to receive from %New(). @@ -1352,7 +1352,7 @@ Method %OnNew() As %Status [ Private, ServerOnly = 1 ] /// This callback method is invoked by the %Open method to /// provide notification that the object specified by oid is being opened. -/// +/// ///

If this method returns an error then the object will not be opened. Method %OnOpen() As %Status [ Private, ServerOnly = 1 ] { @@ -1382,7 +1382,7 @@ Method %OnOpen() As %Status [ Private, ServerOnly = 1 ] /// This callback method is invoked by the %ValidateObject method to /// provide notification that the current object is being validated. -/// +/// ///

If this method returns an error then %ValidateObject will fail. Method %OnValidateObject() As %Status [ Private, ServerOnly = 1 ] { @@ -1471,7 +1471,7 @@ Method %OnValidateObject() As %Status [ Private, ServerOnly = 1 ] /// either because %Save() was invoked on this object or on an object that references this object. /// %OnAddToSaveSet can modify the current object. It can also add other objects to the current /// SaveSet by invoking %AddToSaveSet or remove objects by calling %RemoveFromSaveSet. -/// +/// ///

If this method returns an error status then %Save() will fail and the transaction /// will be rolled back. Method %OnAddToSaveSet( @@ -1506,7 +1506,7 @@ Method %OnAddToSaveSet( } /// Get an instance of an XML enabled class.

-/// +/// /// You may override this method to do custom processing (such as initializing /// the object instance) before returning an instance of this class. /// However, this method should not be called directly from user code.
@@ -1736,9 +1736,9 @@ Method %Evaluate( /// This callback method is invoked by the %Save method to /// provide notification that the object is being saved. It is called before /// any data is written to disk. -/// +/// ///

insert will be set to 1 if this object is being saved for the first time. -/// +/// ///

If this method returns an error then the call to %Save will fail. Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ] { diff --git a/tests/integration_tests/Test/PM/Integration/ExternalNameModuleAction.cls b/tests/integration_tests/Test/PM/Integration/ExternalNameModuleAction.cls index 17d1eb7d..448fb537 100644 --- a/tests/integration_tests/Test/PM/Integration/ExternalNameModuleAction.cls +++ b/tests/integration_tests/Test/PM/Integration/ExternalNameModuleAction.cls @@ -9,27 +9,27 @@ Parameter ExternalName = "ext-name-mod-action"; Method TestExternalNameModuleAction() { - Set dir = ..GetModuleDir(..#Folder) - Set sc = ##class(%IPM.Main).Shell("load "_dir) - Do $$$AssertStatusOK(sc, "Successfully loaded module") + set dir = ..GetModuleDir(..#Folder) + set sc = ##class(%IPM.Main).Shell("load "_dir) + do $$$AssertStatusOK(sc, "Successfully loaded module") - Set sc = ##class(%IPM.Main).Shell(..#DefaultName_" greeting") - Do $$$AssertStatusOK(sc, "Successfully executed action by default name") + set sc = ##class(%IPM.Main).Shell(..#DefaultName_" greeting") + do $$$AssertStatusOK(sc, "Successfully executed action by default name") - Set sc = ##class(%IPM.Main).Shell(..#ExternalName_" greeting") - Do $$$AssertStatusOK(sc, "Successfully executed action by external name") + set sc = ##class(%IPM.Main).Shell(..#ExternalName_" greeting") + do $$$AssertStatusOK(sc, "Successfully executed action by external name") - Set sc = ##class(%IPM.Main).Shell(..#DefaultName_" package") - Do $$$AssertStatusOK(sc, "Successfully package by default name") + set sc = ##class(%IPM.Main).Shell(..#DefaultName_" package") + do $$$AssertStatusOK(sc, "Successfully package by default name") - Set sc = ##class(%IPM.Main).Shell(..#ExternalName_" package") - Do $$$AssertStatusOK(sc, "Successfully package by external name") + set sc = ##class(%IPM.Main).Shell(..#ExternalName_" package") + do $$$AssertStatusOK(sc, "Successfully package by external name") - Set sc = ##class(%IPM.Main).Shell("package "_..#DefaultName) - Do $$$AssertStatusOK(sc, "Successfully package by default name") + set sc = ##class(%IPM.Main).Shell("package "_..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully package by default name") - Set sc = ##class(%IPM.Main).Shell("package "_..#ExternalName) - Do $$$AssertStatusOK(sc, "Successfully package by external name") + set sc = ##class(%IPM.Main).Shell("package "_..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully package by external name") } } diff --git a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls index e250f20b..580cff68 100644 --- a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls +++ b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls @@ -9,31 +9,105 @@ Parameter Folder = "publish-external-name"; Method TestExternalName() { - Set moduleDir = ..GetModuleDir(..#Folder) + set moduleDir = ..GetModuleDir(..#Folder) - Set sc = ##class(%IPM.Main).Shell("repo -delete-all") - Do $$$AssertStatusOK(sc, "Deleted all repos") + set sc = ##class(%IPM.Main).Shell("repo -delete-all") + do $$$AssertStatusOK(sc, "Deleted all repos") - Set sc = ##class(%IPM.Main).Shell("repo -o -name oras -url http://oras:5000 -publish 1") - Do $$$AssertStatusOK(sc, "Added ORAS repo") + set sc = ##class(%IPM.Main).Shell("repo -o -name oras -url http://oras:5000 -publish 1") + do $$$AssertStatusOK(sc, "Added ORAS repo") - Set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir)) - Do $$$AssertStatusOK(sc, "Loaded module") + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir)) + do $$$AssertStatusOK(sc, "Loaded module") - Set sc = ##class(%IPM.Main).Shell($$$FormatText("publish %1 -use-ext", ..#DefaultName)) - Do $$$AssertStatusOK(sc, "Published module with default name") + set sc = ##class(%IPM.Main).Shell($$$FormatText("publish %1 -use-ext", ..#DefaultName)) + do $$$AssertStatusOK(sc, "Published module with default name") - Set sc = ##class(%IPM.Main).Shell($$$FormatText("uninstall %1", ..#DefaultName)) - Do $$$AssertStatusOK(sc, "Deleted module") + set sc = ##class(%IPM.Main).Shell($$$FormatText("uninstall %1", ..#DefaultName)) + do $$$AssertStatusOK(sc, "Deleted module") - Set sc = ##class(%IPM.Main).Shell($$$FormatText("install %1", ..#ExternalName)) - Do $$$AssertStatusOK(sc, "Installed module by external name") + set sc = ##class(%IPM.Main).Shell($$$FormatText("install %1", ..#ExternalName)) + do $$$AssertStatusOK(sc, "Installed module by external name") - Set sc = ##class(%IPM.Main).Shell("repo -delete-all") - Do $$$AssertStatusOK(sc, "Deleted all repos") + set sc = ##class(%IPM.Main).Shell("repo -delete-all") + do $$$AssertStatusOK(sc, "Deleted all repos") - Set sc = ##class(%IPM.Main).Shell("repo -reset-defaults") - Do $$$AssertStatusOK(sc, "Reset repos to default") + set sc = ##class(%IPM.Main).Shell("repo -reset-defaults") + do $$$AssertStatusOK(sc, "Reset repos to default") +} + +Method TestInterchangeableNames() +{ + // Set up repo + set sc = ##class(%IPM.Main).Shell("repo -o -name zot -url http://oras:5000") + do $$$AssertStatusOK(sc,"Zot repo configured succesfully") + // Make sure modules are not already published + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + + // Load module + set moduleDir = ..GetModuleDir(..#Folder) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir)) + do $$$AssertStatusOK(sc, "Loaded module") + + // Test 1: Publish with default name and install using external name + + // Publish module + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot") + do $$$AssertStatusOK(sc,"Published module successfully with default name") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module with external name") + + // Install using external name + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully installed module with external name") + + // Cleanup by unpublishing + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + + + // Test 2: Publish with external name using -use-ext flag and install using internal name + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext") + do $$$AssertStatusOK(sc,"Published module successfully with default name but -use-ext flag") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") + + // Install using internal name + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully installed module with default name") + + // Cleanup by unpublishing + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + + + // Test 3: Publish with external name without -use-ext flag and install using internal name + set sc = ##class(%IPM.Main).Shell("publish " _ ..#ExternalName _ " -verbose -r zot") + do $$$AssertStatusOK(sc,"Published module successfully with external name") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") + + // Install using internal name + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully installed module with default name") + + // Cleanup by unpublishing + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + + + // Cleanup by uninstalling module and removing zot repo + $$$ThrowOnError(##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName)) + $$$ThrowOnError(##class(%IPM.Main).Shell("repo -n zot -delete")) } } From 7cc4c3ec4c15d85fc633a2bf25b24ca0a4854eea Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Wed, 10 Dec 2025 11:02:29 -0500 Subject: [PATCH 2/7] Handle non-namespaced ORAS default-external name interchangeability --- src/cls/IPM/Repo/Oras/PackageService.cls | 72 +++++++++++++++++-- .../PM/Integration/PublishExternalName.cls | 40 ++++++++++- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/cls/IPM/Repo/Oras/PackageService.cls b/src/cls/IPM/Repo/Oras/PackageService.cls index 599c54f0..8f93c895 100644 --- a/src/cls/IPM/Repo/Oras/PackageService.cls +++ b/src/cls/IPM/Repo/Oras/PackageService.cls @@ -131,6 +131,44 @@ Method ListModulesFromTagString( set artifactMetadata = ##class(%IPM.Repo.Oras.ArtifactMetadata).%New() $$$ThrowOnError(artifactMetadata.%JSONImport(metadata)) + #; Filter by module name if requested - check both Name and ExternalName + if (searchCriteria.Name '= "") { + #; Optimization: First check if package name already matches (fast path) + set packageNameMatches = ($$$lcase(name) = $$$lcase(searchCriteria.Name)) + + if 'packageNameMatches { + #; Package name doesn't match - fetch module.xml to check Name and ExternalName + set moduleXMLText = ..GetModuleXML(..Location, name _ ":" _ tag, ..Namespace, ..Username, ..Password, ..Token, ..TokenAuthMethod) + + if (moduleXMLText '= "") { + #; Parse just Name and ExternalName from XML + set moduleObj = ..ParseModuleXMLForNameCheck(moduleXMLText, .sc) + + if $$$ISERR(sc) { + #; Skip this package if XML parsing fails + continue + } + + #; Check if search name matches either Name or ExternalName (case-insensitive) + set matchesName = ($$$lcase(moduleObj.Name) = $$$lcase(searchCriteria.Name)) + set matchesExternalName = 0 + + if (moduleObj.ExternalName '= "") { + set matchesExternalName = ($$$lcase(moduleObj.ExternalName) = $$$lcase(searchCriteria.Name)) + } + + #; Only include if at least one matches + if 'matchesName && 'matchesExternalName { + continue + } + } else { + #; No module.xml available - skip this package + continue + } + } + #; If packageNameMatches is true, proceed without additional checks + } + set tModRef = ##class(%IPM.Storage.ModuleInfo).%New() // `artifactMetadata.ImageTitle` can be different from `name`. E.g., when the module was simply "moved" from elsewhere under a different name. set tModRef.Name = name @@ -213,10 +251,6 @@ Method ListModules(pSearchCriteria As %IPM.Repo.SearchCriteria) As %ListOfObject if (package="") { continue } - #; filter by module name if requested - if (name'="") && (package'=name) { - continue - } #; collect all versions for this package in tList do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, package, .tList) } @@ -227,6 +261,36 @@ Method ListModules(pSearchCriteria As %IPM.Repo.SearchCriteria) As %ListOfObject return tList } +/// Lightweight parse of module.xml to extract Name and ExternalName only +/// Returns a Module object with Name and ExternalName populated +ClassMethod ParseModuleXMLForNameCheck( + moduleXML As %String, + Output sc As %Status) As %IPM.Storage.Module +{ + set sc = $$$OK + set moduleObj = "" + + try { + #; Create stream from XML text + set stream = ##class(%Stream.GlobalCharacter).%New() + do stream.Write(moduleXML) + + #; Parse XML into Module object + set reader = ##class(%XML.Reader).%New() + set sc = reader.OpenStream(stream) + $$$ThrowOnError(sc) + + do reader.Correlate("Module", "%IPM.Storage.Module") + do reader.Next(.moduleObj, .sc) + $$$ThrowOnError(sc) + + } catch e { + set sc = e.AsStatus() + } + + quit moduleObj +} + // ** ORAS FUNCTIONS ** /// Returns an authenticated ORAS client diff --git a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls index 580cff68..c5b1ba8b 100644 --- a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls +++ b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls @@ -69,6 +69,11 @@ Method TestInterchangeableNames() // Cleanup by unpublishing set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + // Verify cannot install now + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") // Test 2: Publish with external name using -use-ext flag and install using internal name @@ -86,9 +91,14 @@ Method TestInterchangeableNames() // Cleanup by unpublishing set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + // Verify cannot install now + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") - // Test 3: Publish with external name without -use-ext flag and install using internal name + // Test 3: Publish with external name without -use-ext flag (will appear in repo under internal name) and install using internal name set sc = ##class(%IPM.Main).Shell("publish " _ ..#ExternalName _ " -verbose -r zot") do $$$AssertStatusOK(sc,"Published module successfully with external name") @@ -101,8 +111,34 @@ Method TestInterchangeableNames() do $$$AssertStatusOK(sc, "Successfully installed module with default name") // Cleanup by unpublishing - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") + + + // Test 4: Publish with external name without -use-ext flag and install using external name + set sc = ##class(%IPM.Main).Shell("publish " _ ..#ExternalName _ " -verbose -r zot") + do $$$AssertStatusOK(sc,"Published module successfully with external name") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") + + // Install using internal name + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully installed module with external name") + + // Cleanup by unpublishing + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") + // Cleanup by uninstalling module and removing zot repo From 53429581b7d99fd0ce339c34213e77e3de313445 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Wed, 10 Dec 2025 11:51:19 -0500 Subject: [PATCH 3/7] Add name interchangeability for namespaced ORAS repos and fix namespace bug --- CHANGELOG.md | 7 +- src/cls/IPM/Repo/Oras/PackageService.cls | 93 +++++++------ .../Test/PM/Integration/OrasTag.cls | 124 +++++++++++++++--- .../PM/Integration/PublishExternalName.cls | 43 ++++++ 4 files changed, 206 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d352df..154d5de3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #935: Adding a generic JFrog Artifactory tarball resource processor for bundling artifact with a package and deploying it to a final location on install. - #950: Added support for listing installed Python packages using `list -python`, `list -py` and `list-installed -python` - #822: The CPF resource processor now supports system expressions and macros in CPF merge files -- #578 Added functionality to record and display IPM history of install, uninstall, load, and update -- #959: External name can now be used interchangeably with (default) name for `install` and `update`, i.e. a module published with its (default) name can be installed using its external name. +- #578: Added functionality to record and display IPM history of install, uninstall, load, and update +- #959: In ORAS repos, external name can now be used interchangeably with (default) name for `install` and `update`, i.e. a module published with its (default) name can be installed using its external name. - #951: The `unpublish` command will skip user confirmation prompt if the `-force` flag is provided. ### Changed @@ -24,7 +24,7 @@ to load using multicompile instead of trying to do own multi-threading of item l lock contention by bypassing IRIS compiler. ### Removed -- #938 Removed secret flag NewVersion handling in %Publish() +- #938: Removed secret flag NewVersion handling in %Publish() ### Fixed - #943: The `load` command when used with a GitHub repository URL accepts a `branch` argument again @@ -34,6 +34,7 @@ lock contention by bypassing IRIS compiler. - #965: FileCopy on a directory with a Name without the leading slash now works - #937: Publishing a module with a `` containing a `Path` no longer errors out - #957: Improved error messages for OS command execution. Now, when a command fails, the error message includes the full command and its return code. Also fixed argument separation for the Windows `attrib` command and removed misleading error handling for missing commands. +- #789: Fix error when listing modules for an ORAS repo with a specified namespace. ### Deprecated - #828: The `CheckStatus` flag for `` action has been deprecated. Default behavior is now to always check the status of the method if and only if the method signature returns %Library.Status diff --git a/src/cls/IPM/Repo/Oras/PackageService.cls b/src/cls/IPM/Repo/Oras/PackageService.cls index 8f93c895..3fa19166 100644 --- a/src/cls/IPM/Repo/Oras/PackageService.cls +++ b/src/cls/IPM/Repo/Oras/PackageService.cls @@ -133,12 +133,26 @@ Method ListModulesFromTagString( #; Filter by module name if requested - check both Name and ExternalName if (searchCriteria.Name '= "") { + #; Extract the package name without namespace prefix for comparison + #; Package name from ORAS may be "namespace/modulename" or just "modulename" + set packageNameWithoutNamespace = name + if (name [ "/") { + #; Extract everything after the last "/" + for i=$length(name):-1:1 { + if ($extract(name, i) = "/") { + set packageNameWithoutNamespace = $extract(name, i+1, *) + quit + } + } + } + #; Optimization: First check if package name already matches (fast path) - set packageNameMatches = ($$$lcase(name) = $$$lcase(searchCriteria.Name)) + set packageNameMatches = ($$$lcase(packageNameWithoutNamespace) = $$$lcase(searchCriteria.Name)) if 'packageNameMatches { #; Package name doesn't match - fetch module.xml to check Name and ExternalName - set moduleXMLText = ..GetModuleXML(..Location, name _ ":" _ tag, ..Namespace, ..Username, ..Password, ..Token, ..TokenAuthMethod) + #; Note: pass empty namespace since 'name' already includes the full path + set moduleXMLText = ..GetModuleXML(..Location, name _ ":" _ tag, "", ..Username, ..Password, ..Token, ..TokenAuthMethod) if (moduleXMLText '= "") { #; Parse just Name and ExternalName from XML @@ -200,63 +214,66 @@ Method ListModulesFromTagString( } } +/// Lists the modules available in this repository that satisfy the search criteria +/// Note: for ORAS repositories only, the internal name and external name can be used interchangeably Method ListModules(pSearchCriteria As %IPM.Repo.SearchCriteria) As %ListOfObjects(ELEMENTTYPE="%IPM.Storage.ModuleInfo") { #; Get ORAS client set client = ..GetClient(..Location, ..Username, ..Password, ..Token, ..TokenAuthMethod) #; Parse search criteria - set name = $$$lcase(pSearchCriteria.Name) set tVersionExpression = pSearchCriteria.VersionExpression set tSC = ##class(%IPM.General.SemanticVersionExpression).FromString(pSearchCriteria.VersionExpression, .tVersionExpression) $$$ThrowOnError(tSC) - #; If namespace is defined, add it to the package URI being searched for - if (name'="") && (..Namespace'="") { - set name = ..AppendURIs(..Namespace, name) - } - #; Get all modules set tList = ##class(%Library.ListOfObjects).%New() #; get all versions - // When no namespace is specified, we can only call "v2/_catalog" to get all the packages. In case of error, fail descriptively - if (..Namespace = "") { - set request = ..GetHttpRequest() - #; Make GET request - // response is a JSON structure like {"repositories":["package1", "package2", ...]} - set tSC=request.Get(..PathPrefix _ "/v2/_catalog") - $$$ThrowOnError(tSC) - set response=request.HttpResponse - if (response.StatusCode'=200) { - // TODO improve error processing - set msg = "Error: ORAS namespace is not set and the call to /v2/_catalog endpoint failed" + // Use /v2/_catalog to enumerate all packages, then filter appropriately + set request = ..GetHttpRequest() + #; Make GET request + // response is a JSON structure like {"repositories":["package1", "package2", ...]} + set tSC=request.Get(..PathPrefix _ "/v2/_catalog") + $$$ThrowOnError(tSC) + set response=request.HttpResponse + if (response.StatusCode'=200) { + // TODO improve error processing + set msg = "Error: Call to /v2/_catalog endpoint failed" + if (..Namespace = "") { set msg = msg _ $char(10,13) _ "Either set an ORAS namespace or ensure the ORAS server supports the /v2/_catalog endpoint" - set msg = msg _ $char(10,13) _ "Response Code: "_response.StatusCode _ " - " _ response.Data.Read() - $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) } + set msg = msg _ $char(10,13) _ "Response Code: "_response.StatusCode _ " - " _ response.Data.Read() + $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) + } - #; Handle results - set json = "" - while 'response.Data.AtEnd { - set json = json _ response.Data.Read() + #; Handle results + set json = "" + while 'response.Data.AtEnd { + set json = json _ response.Data.Read() + } + set data = ##class(%DynamicAbstractObject).%FromJSON(json) + set iter = data.repositories.%GetIterator() + // Iterate through each package + // key is the index + // package is the package name + // type is always "string" + while iter.%GetNext(.key, .package, .type) { + if (package="") { + continue } - set data = ##class(%DynamicAbstractObject).%FromJSON(json) - set iter = data.repositories.%GetIterator() - // Iterate through each package - // key is the index - // package is the package name - // type is always "string" - while iter.%GetNext(.key, .package, .type) { - if (package="") { + + #; If namespace is configured, only consider packages in that namespace + if (..Namespace '= "") { + set namespacePrefix = ..Namespace _ "/" + #; Skip packages not in this namespace + if ($extract(package, 1, $length(namespacePrefix)) '= namespacePrefix) { continue } - #; collect all versions for this package in tList - do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, package, .tList) } - } else { - // TODO: Make this work properly for the case where name is empty (searching in a repo with a namespace) - do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, name, .tList) + + #; collect all versions for this package in tList + do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, package, .tList) } return tList } diff --git a/tests/integration_tests/Test/PM/Integration/OrasTag.cls b/tests/integration_tests/Test/PM/Integration/OrasTag.cls index 9b8b7149..51690c74 100644 --- a/tests/integration_tests/Test/PM/Integration/OrasTag.cls +++ b/tests/integration_tests/Test/PM/Integration/OrasTag.cls @@ -5,34 +5,118 @@ Parameter TargetModuleName As STRING = "oras-tag"; Method TestOrasTagConversion() { - Set tModuleDir = ..GetModuleDir(..#TargetModuleName) + set moduleDir = ..GetModuleDir(..#TargetModuleName) - Set tSC = ##class(%IPM.Main).Shell("load -verbose " _ tModuleDir) - Do $$$AssertStatusOK(tSC,"Loaded module successfully") + set sc = ##class(%IPM.Main).Shell("load -verbose " _ moduleDir) + do $$$AssertStatusOK(sc,"Loaded module successfully") - Set tSC = ##class(%IPM.Main).Shell("repo -delete-all") - Do $$$AssertStatusOK(tSC,"Deleted repos successfully") + set sc = ##class(%IPM.Main).Shell("repo -delete-all") + do $$$AssertStatusOK(sc,"Deleted repos successfully") - Set tSC = ##class(%IPM.Main).Shell("repo -o -name oras -url http://oras:5000 -publish 1") - Do $$$AssertStatusOK(tSC,"Set up oras module successfully") + set sc = ##class(%IPM.Main).Shell("repo -o -name oras -url http://oras:5000 -publish 1") + do $$$AssertStatusOK(sc,"Set up oras module successfully") - Set tSC = ##class(%IPM.Main).Shell("publish oras-tag -r oras -verbose") - Do $$$AssertStatusOK(tSC,"Published module successfully") + set sc = ##class(%IPM.Main).Shell("publish oras-tag -r oras -verbose") + do $$$AssertStatusOK(sc,"Published module successfully") - Set tSC = ##class(%IPM.Main).Shell("uninstall oras-tag") - Do $$$AssertStatusOK(tSC,"Uninstalled module successfully") + set sc = ##class(%IPM.Main).Shell("uninstall oras-tag") + do $$$AssertStatusOK(sc,"Uninstalled module successfully") - Set tSC = ##class(%IPM.Main).Shell("install oras-tag") - If '$$$AssertStatusOK(tSC,"Installed module from ORAS registry successfully") { - Zwrite %objlasterror - Zwrite tSC - } + set sc = ##class(%IPM.Main).Shell("install oras-tag") + do $$$AssertStatusOK(sc,"Installed module from ORAS registry successfully") - Set tSC = ##class(%IPM.Main).Shell("repo -delete-all") - Do $$$AssertStatusOK(tSC,"Deleted repos successfully") + // Cleanup + set sc = ##class(%IPM.Main).Shell("unpublish oras/oras-tag all -force") + do $$$AssertStatusOK(sc,"Unpublished module successfully") - Set tSC = ##class(%IPM.Main).Shell("repo -reset-defaults") - Do $$$AssertStatusOK(tSC,"Reset repos to default successfully") + set sc = ##class(%IPM.Main).Shell("repo -delete-all") + do $$$AssertStatusOK(sc,"Deleted repos successfully") + + set sc = ##class(%IPM.Main).Shell("repo -reset-defaults") + do $$$AssertStatusOK(sc,"Reset repos to default successfully") +} + +Method TestOrasNamespace() +{ + set moduleDir = ..GetModuleDir(..#TargetModuleName) + + set sc = ##class(%IPM.Main).Shell("load -verbose " _ moduleDir) + do $$$AssertStatusOK(sc,"Loaded module successfully") + + set sc = ##class(%IPM.Main).Shell("repo -delete-all") + do $$$AssertStatusOK(sc,"Deleted repos successfully") + + // Set up oras module with namespace + set sc = ##class(%IPM.Main).Shell("repo -o -name oras -url http://oras:5000 -ns testnamespace -publish 1") + do $$$AssertStatusOK(sc,"Set up oras module with namespace successfully") + + // Publish module (should be stored as testnamespace/oras-tag) + set sc = ##class(%IPM.Main).Shell("publish oras-tag -r oras -verbose") + do $$$AssertStatusOK(sc,"Published module successfully to namespaced repo") + + // Uninstall and reinstall + set sc = ##class(%IPM.Main).Shell("uninstall oras-tag") + do $$$AssertStatusOK(sc,"Uninstalled module successfully") + + set sc = ##class(%IPM.Main).Shell("install oras-tag") + do $$$AssertStatusOK(sc,"Installed module from namespaced ORAS registry successfully") + + // Verify searching only shows modules in the namespace + set sc = ##class(%IPM.Main).Shell("repo -list-modules -n oras") + do $$$AssertStatusOK(sc,"Listed modules in namespaced repo successfully") + + // Reconfigure to no namespace to verify isolation + set sc = ##class(%IPM.Main).Shell("repo -n oras -ns """"") + do $$$AssertStatusOK(sc,"Reconfigured oras repo without namespace") + + // Publish to the non-namespaced repo (should be stored as oras-tag) + set sc = ##class(%IPM.Main).Shell("uninstall oras-tag") + do $$$AssertStatusOK(sc,"Uninstalled module before republishing") + + // Load module again + set sc = ##class(%IPM.Main).Shell("load -verbose " _ moduleDir) + do $$$AssertStatusOK(sc,"Reloaded module successfully") + + set sc = ##class(%IPM.Main).Shell("publish oras-tag -r oras -verbose") + do $$$AssertStatusOK(sc,"Published module to non-namespaced repo") + + // Reconfigure back to namespace + set sc = ##class(%IPM.Main).Shell("repo -n oras -ns testnamespace") + do $$$AssertStatusOK(sc,"Reconfigured oras repo with namespace again") + + // Verify we can install from the namespaced repo (should find testnamespace/oras-tag) + set sc = ##class(%IPM.Main).Shell("install oras/oras-tag") + do $$$AssertStatusOK(sc,"Installed module from namespaced repo using oras/oras-tag syntax") + + // Reconfigure to no namespace + set sc = ##class(%IPM.Main).Shell("repo -n oras -ns """"") + do $$$AssertStatusOK(sc,"Reconfigured oras repo without namespace again") + + // Verify we can also install from the non-namespaced repo + set sc = ##class(%IPM.Main).Shell("uninstall oras-tag") + do $$$AssertStatusOK(sc,"Uninstalled module successfully") + + set sc = ##class(%IPM.Main).Shell("install oras/oras-tag") + do $$$AssertStatusOK(sc,"Installed module from non-namespaced repo using oras/oras-tag syntax") + + // Cleanup - unpublish from both namespaced and non-namespaced + set sc = ##class(%IPM.Main).Shell("unpublish oras/oras-tag all -force") + do $$$AssertStatusOK(sc,"Unpublished module from non-namespaced repo") + + set sc = ##class(%IPM.Main).Shell("repo -n oras -ns testnamespace") + do $$$AssertStatusOK(sc,"Reconfigured oras repo with namespace for cleanup") + + set sc = ##class(%IPM.Main).Shell("unpublish oras/oras-tag all -force") + do $$$AssertStatusOK(sc,"Unpublished module from namespaced repo") + + set sc = ##class(%IPM.Main).Shell("uninstall oras-tag") + do $$$AssertStatusOK(sc,"Uninstalled module successfully") + + set sc = ##class(%IPM.Main).Shell("repo -delete-all") + do $$$AssertStatusOK(sc,"Deleted repos successfully") + + set sc = ##class(%IPM.Main).Shell("repo -reset-defaults") + do $$$AssertStatusOK(sc,"Reset repos to default successfully") } } diff --git a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls index c5b1ba8b..45a54113 100644 --- a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls +++ b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls @@ -140,6 +140,49 @@ Method TestInterchangeableNames() do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") + // Test 5: Test with namespace - publish with -use-ext and install using default name + // Reconfigure zot repo with namespace + set sc = ##class(%IPM.Main).Shell("repo -n zot -ns test") + do $$$AssertStatusOK(sc, "Reconfigured zot repo with namespace 'test'") + + // Publish with -use-ext (will be stored as test/external-name) + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext") + do $$$AssertStatusOK(sc, "Published module with -use-ext in namespaced repo") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + + // Install using default name (should find test/external-name and match via module.xml) + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully installed module with default name from namespaced repo") + + // Cleanup + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc, "Unpublished external name from namespaced repo") + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") + + + // Test 6: Test with namespace - publish without -use-ext and install using external name + // Publish without -use-ext (will be stored as test/default-name) + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot") + do $$$AssertStatusOK(sc, "Published module without -use-ext in namespaced repo") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + + // Install using external name (should find test/default-name and match via module.xml) + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully installed module with external name from namespaced repo") + + // Cleanup + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc, "Unpublished default name from namespaced repo") + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") + // Cleanup by uninstalling module and removing zot repo $$$ThrowOnError(##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName)) From 7d08c13540b4482f7b5d86751dd382b9cb47f6f3 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Thu, 11 Dec 2025 15:07:19 -0500 Subject: [PATCH 4/7] Refactor and fix minor issues --- src/cls/IPM/Main.cls | 3 +- src/cls/IPM/Repo/Oras/PackageService.cls | 94 +++++++++--------------- src/cls/IPM/Utils/Module.cls | 13 ++++ 3 files changed, 47 insertions(+), 63 deletions(-) diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index 14111214..bce616e7 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -177,7 +177,7 @@ This command is an alias for `module-action module-name makedeployed` Delete package from repository -unpublishMyModuleName all +unpublish MyModuleName all unpublish MyRepo/MyModuleName 1.0.0 @@ -2471,7 +2471,6 @@ ClassMethod RunOnePhase(ByRef pCommandInfo) [ Internal ] ClassMethod Unpublish(ByRef pCommandInfo) [ Internal ] { - #; merge ^mtempdc($zhorolog,"pCommandInfo") = pCommandInfo if ( ($get(pCommandInfo("parameters","module"))="") || ($get(pCommandInfo("parameters","version"))="") ) { set tCommandInfo("parameters","command") = "unpublish" do ..%Help(.tCommandInfo) diff --git a/src/cls/IPM/Repo/Oras/PackageService.cls b/src/cls/IPM/Repo/Oras/PackageService.cls index 3fa19166..f13b280b 100644 --- a/src/cls/IPM/Repo/Oras/PackageService.cls +++ b/src/cls/IPM/Repo/Oras/PackageService.cls @@ -107,13 +107,13 @@ Method ListModulesFromTagString( set allVersionsList = ..AggregatePlatformVersions($listfromstring(allTagsString, ", "), .aggregatedPlatformVersion) set pointer = 0 while $listnext(allVersionsList,pointer,moduleVersion) { - #; filter by version + // filter by version set tVersion = ##class(%IPM.General.SemanticVersion).FromString($$$Tag2Semver(moduleVersion)) if 'tVersion.Satisfies(semverExpr) { continue } - #; Special case: if we provided an explicit build number, require it. + // Special case: if we provided an explicit build number, require it. if ##class(%IPM.General.SemanticVersion).IsValid(searchCriteria.VersionExpression,.specificVersion) && (specificVersion.Build '= "") && (specificVersion.Build '= tVersion.Build) { continue } @@ -126,15 +126,15 @@ Method ListModulesFromTagString( set tag = tag _ $$$OrasTagPlatformSeparator _ platformVersion } - #; get metadata from annotations + // get metadata from annotations set metadata = ..GetPackageMetadata(..Location, name, tag, "", client) set artifactMetadata = ##class(%IPM.Repo.Oras.ArtifactMetadata).%New() $$$ThrowOnError(artifactMetadata.%JSONImport(metadata)) - #; Filter by module name if requested - check both Name and ExternalName + // Filter by module name if requested - check both Name and ExternalName if (searchCriteria.Name '= "") { - #; Extract the package name without namespace prefix for comparison - #; Package name from ORAS may be "namespace/modulename" or just "modulename" + // Extract the package name without namespace prefix for comparison + // Package name from ORAS may be "namespace/modulename" or just "modulename" set packageNameWithoutNamespace = name if (name [ "/") { #; Extract everything after the last "/" @@ -146,41 +146,47 @@ Method ListModulesFromTagString( } } - #; Optimization: First check if package name already matches (fast path) - set packageNameMatches = ($$$lcase(packageNameWithoutNamespace) = $$$lcase(searchCriteria.Name)) + // First check if package name already matches + if ($$$lcase(packageNameWithoutNamespace) = $$$lcase(searchCriteria.Name)) { + set packageNameMatches = 1 + } else { + set packageNameMatches = 0 + } if 'packageNameMatches { - #; Package name doesn't match - fetch module.xml to check Name and ExternalName - #; Note: pass empty namespace since 'name' already includes the full path + // Package name doesn't match - fetch module.xml to check namespaced Name and ExternalName + // Note: pass empty namespace since 'name' already includes the full path set moduleXMLText = ..GetModuleXML(..Location, name _ ":" _ tag, "", ..Username, ..Password, ..Token, ..TokenAuthMethod) if (moduleXMLText '= "") { - #; Parse just Name and ExternalName from XML - set moduleObj = ..ParseModuleXMLForNameCheck(moduleXMLText, .sc) + // Parse module.xml + set moduleObj = ##class(%IPM.Utils.Module).GetModuleObjectFromString(moduleXMLText, .found) - if $$$ISERR(sc) { - #; Skip this package if XML parsing fails + if 'found { continue } - #; Check if search name matches either Name or ExternalName (case-insensitive) - set matchesName = ($$$lcase(moduleObj.Name) = $$$lcase(searchCriteria.Name)) + // Check if search name matches either Name or ExternalName (case-insensitive) + set matchesName = 0 set matchesExternalName = 0 - if (moduleObj.ExternalName '= "") { - set matchesExternalName = ($$$lcase(moduleObj.ExternalName) = $$$lcase(searchCriteria.Name)) + if ($$$lcase(moduleObj.Name) = $$$lcase(searchCriteria.Name)) { + set matchesName = 1 + } + if (moduleObj.ExternalName '= "") && ($$$lcase(moduleObj.ExternalName) = $$$lcase(searchCriteria.Name)) { + set matchesExternalName = 1 } - #; Only include if at least one matches + // Only include if at least one matches if 'matchesName && 'matchesExternalName { continue } } else { - #; No module.xml available - skip this package + // No module.xml available - skip this module continue } } - #; If packageNameMatches is true, proceed without additional checks + // If packageNameMatches is true, proceed without additional checks } set tModRef = ##class(%IPM.Storage.ModuleInfo).%New() @@ -240,9 +246,6 @@ Method ListModules(pSearchCriteria As %IPM.Repo.SearchCriteria) As %ListOfObject if (response.StatusCode'=200) { // TODO improve error processing set msg = "Error: Call to /v2/_catalog endpoint failed" - if (..Namespace = "") { - set msg = msg _ $char(10,13) _ "Either set an ORAS namespace or ensure the ORAS server supports the /v2/_catalog endpoint" - } set msg = msg _ $char(10,13) _ "Response Code: "_response.StatusCode _ " - " _ response.Data.Read() $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) } @@ -263,53 +266,22 @@ Method ListModules(pSearchCriteria As %IPM.Repo.SearchCriteria) As %ListOfObject continue } - #; If namespace is configured, only consider packages in that namespace + // If namespace is configured, only consider packages in that namespace if (..Namespace '= "") { set namespacePrefix = ..Namespace _ "/" - #; Skip packages not in this namespace + // Skip packages not in this namespace if ($extract(package, 1, $length(namespacePrefix)) '= namespacePrefix) { continue } } - #; collect all versions for this package in tList + // collect all versions for this package in tList do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, package, .tList) } return tList } -/// Lightweight parse of module.xml to extract Name and ExternalName only -/// Returns a Module object with Name and ExternalName populated -ClassMethod ParseModuleXMLForNameCheck( - moduleXML As %String, - Output sc As %Status) As %IPM.Storage.Module -{ - set sc = $$$OK - set moduleObj = "" - - try { - #; Create stream from XML text - set stream = ##class(%Stream.GlobalCharacter).%New() - do stream.Write(moduleXML) - - #; Parse XML into Module object - set reader = ##class(%XML.Reader).%New() - set sc = reader.OpenStream(stream) - $$$ThrowOnError(sc) - - do reader.Correlate("Module", "%IPM.Storage.Module") - do reader.Next(.moduleObj, .sc) - $$$ThrowOnError(sc) - - } catch e { - set sc = e.AsStatus() - } - - quit moduleObj -} - -// ** ORAS FUNCTIONS ** - +/// ** ORAS FUNCTIONS ** /// Returns an authenticated ORAS client ClassMethod GetClient( registry As %String, @@ -472,8 +444,8 @@ ClassMethod GetModuleXML( annotations = manifest.get('annotations', {}) module_xml = annotations.get('com.intersystems.ipm.module.v1+xml') if module_xml: - # remove all whitespace - return "".join(module_xml.split()) + # remove extraneous whitespace + return module_xml.strip() except Exception as ex: print("Exception: %s" % str(ex)) diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index 62b89303..129a7e04 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -328,6 +328,19 @@ ClassMethod GetModuleObjectFromStream( return $get(moduleObj) } +/// Given a string, tries to correlate it to an instance of %IPM.Storage.Module. +/// Throws errors. +ClassMethod GetModuleObjectFromString( + moduleXML As %String, + Output found As %Boolean) As %IPM.Storage.Module +{ + #; Create stream from XML text + set stream = ##class(%Stream.GlobalCharacter).%New() + do stream.Write(moduleXML) + + return ..GetModuleObjectFromStream(stream, .found) +} + /// Returns a semantic version expression capturing all version requirements for a given module name in the current namespace. /// A list of modules to exclude may be provided (for example, if these modules would be updated at the same time). ClassMethod GetRequiredVersionExpression( From 1b0ae0525165d1fd196f302f1054d2dd8423c088 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Thu, 11 Dec 2025 16:11:04 -0500 Subject: [PATCH 5/7] Add support for update command for external name --- src/cls/IPM/Repo/Oras/PackageService.cls | 8 ++- src/cls/IPM/Utils/Module.cls | 2 +- .../PM/Integration/PublishExternalName.cls | 67 ++++++++++++++++++- .../{ => 0.0.1}/module.xml | 0 .../publish-external-name/0.0.2/module.xml | 11 +++ 5 files changed, 83 insertions(+), 5 deletions(-) rename tests/integration_tests/Test/PM/Integration/_data/publish-external-name/{ => 0.0.1}/module.xml (100%) create mode 100644 tests/integration_tests/Test/PM/Integration/_data/publish-external-name/0.0.2/module.xml diff --git a/src/cls/IPM/Repo/Oras/PackageService.cls b/src/cls/IPM/Repo/Oras/PackageService.cls index f13b280b..aa343957 100644 --- a/src/cls/IPM/Repo/Oras/PackageService.cls +++ b/src/cls/IPM/Repo/Oras/PackageService.cls @@ -160,8 +160,12 @@ Method ListModulesFromTagString( if (moduleXMLText '= "") { // Parse module.xml - set moduleObj = ##class(%IPM.Utils.Module).GetModuleObjectFromString(moduleXMLText, .found) - + try { + set moduleObj = ##class(%IPM.Utils.Module).GetModuleObjectFromString(moduleXMLText, .found) + } catch ex { + // if a single module.xml fails to parse, move onto the next one instead of failing completely + continue + } if 'found { continue } diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index 129a7e04..53b7cd47 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -1145,7 +1145,7 @@ ClassMethod LoadNewModule( if foundNewModuleObj { // Check that on an update command with -path flag, the module in the path matches the base module specified for update. - if (commandLineModuleName '= "") && (newModuleObj.Name '= commandLineModuleName) { + if (commandLineModuleName '= "") && (newModuleObj.Name '= commandLineModuleName) && (newModuleObj.ExternalName '= commandLineModuleName){ set msg = $$$FormatText("The specified path does not contain a module matching the specified module to update. Module in path = %1. Specified module = %2.",newModuleObj.Name,commandLineModuleName) $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) } diff --git a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls index 45a54113..cf9e0657 100644 --- a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls +++ b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls @@ -17,7 +17,7 @@ Method TestExternalName() set sc = ##class(%IPM.Main).Shell("repo -o -name oras -url http://oras:5000 -publish 1") do $$$AssertStatusOK(sc, "Added ORAS repo") - set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir)) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) do $$$AssertStatusOK(sc, "Loaded module") set sc = ##class(%IPM.Main).Shell($$$FormatText("publish %1 -use-ext", ..#DefaultName)) @@ -49,7 +49,7 @@ Method TestInterchangeableNames() // Load module set moduleDir = ..GetModuleDir(..#Folder) - set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir)) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) do $$$AssertStatusOK(sc, "Loaded module") // Test 1: Publish with default name and install using external name @@ -189,4 +189,67 @@ Method TestInterchangeableNames() $$$ThrowOnError(##class(%IPM.Main).Shell("repo -n zot -delete")) } +/// Test that update works with external name as well as default name +Method TestInterchangeableNameUpdate() +{ + set moduleDir = ..GetModuleDir(..#Folder) + // Test 1: Publish with default name, update with external name + set sc = ##class(%IPM.Main).Shell("repo -o -name zot -url http://oras:5000") + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.2")) + do $$$AssertStatusOK(sc, "Loaded module succesfully!") + + // Publish module + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot") + do $$$AssertStatusOK(sc,"Published module successfully with default name") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + + // Load version 0.0.1 + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module succesfully!") + + // Update to version 0.0.2 using external name + set sc = ##class(%IPM.Main).Shell("update " _ ..#ExternalName _ " 0.0.2") + do $$$AssertStatusOK(sc, "Updated module succesfully!") + + //Cleanup + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc, "Unpublished default name from repo") + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") + + // Test 2: Publish with external name, update with default name + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.2")) + do $$$AssertStatusOK(sc, "Loaded module succesfully!") + + // Publish module + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext") + do $$$AssertStatusOK(sc,"Published module successfully with external name") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + + // Load version 0.0.1 + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module succesfully!") + + // Update to version 0.0.2 using default name + set sc = ##class(%IPM.Main).Shell("update " _ ..#DefaultName _ " 0.0.2") + do $$$AssertStatusOK(sc, "Updated module succesfully!") + + //Cleanup + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc, "Unpublished default name from repo") + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") + + + // Cleanup by uninstalling module and removing zot repo + $$$ThrowOnError(##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName)) + $$$ThrowOnError(##class(%IPM.Main).Shell("repo -n zot -delete")) +} + } diff --git a/tests/integration_tests/Test/PM/Integration/_data/publish-external-name/module.xml b/tests/integration_tests/Test/PM/Integration/_data/publish-external-name/0.0.1/module.xml similarity index 100% rename from tests/integration_tests/Test/PM/Integration/_data/publish-external-name/module.xml rename to tests/integration_tests/Test/PM/Integration/_data/publish-external-name/0.0.1/module.xml diff --git a/tests/integration_tests/Test/PM/Integration/_data/publish-external-name/0.0.2/module.xml b/tests/integration_tests/Test/PM/Integration/_data/publish-external-name/0.0.2/module.xml new file mode 100644 index 00000000..5b2c2ecf --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/publish-external-name/0.0.2/module.xml @@ -0,0 +1,11 @@ + + + + + default-name + external-name + 0.0.2 + module + + + \ No newline at end of file From a544d64b745deaa878f866ff86ae84939fb4d027 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Wed, 17 Dec 2025 11:29:26 -0500 Subject: [PATCH 6/7] Address review comments --- src/cls/IPM/Main.cls | 2 + src/cls/IPM/Repo/Oras/PackageService.cls | 101 +++++++++-------------- src/cls/IPM/Storage/Module.cls | 2 +- 3 files changed, 44 insertions(+), 61 deletions(-) diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index bc753b2f..f0e993e1 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -2154,6 +2154,8 @@ ClassMethod ShowModulesForRepository( set list(list, "AllVersions") = tRes.AllVersions } } + // Also need to throw an error if there are no results and tSC is set + $$$ThrowOnError(tSC) set list("width") = width write ! do ..DisplayModules(.list) diff --git a/src/cls/IPM/Repo/Oras/PackageService.cls b/src/cls/IPM/Repo/Oras/PackageService.cls index aa343957..b012c158 100644 --- a/src/cls/IPM/Repo/Oras/PackageService.cls +++ b/src/cls/IPM/Repo/Oras/PackageService.cls @@ -135,23 +135,10 @@ Method ListModulesFromTagString( if (searchCriteria.Name '= "") { // Extract the package name without namespace prefix for comparison // Package name from ORAS may be "namespace/modulename" or just "modulename" - set packageNameWithoutNamespace = name - if (name [ "/") { - #; Extract everything after the last "/" - for i=$length(name):-1:1 { - if ($extract(name, i) = "/") { - set packageNameWithoutNamespace = $extract(name, i+1, *) - quit - } - } - } + set packageNameWithoutNamespace = $piece(name, "/", *) // First check if package name already matches - if ($$$lcase(packageNameWithoutNamespace) = $$$lcase(searchCriteria.Name)) { - set packageNameMatches = 1 - } else { - set packageNameMatches = 0 - } + set packageNameMatches = ($$$lcase(packageNameWithoutNamespace) = $$$lcase(searchCriteria.Name)) if 'packageNameMatches { // Package name doesn't match - fetch module.xml to check namespaced Name and ExternalName @@ -171,15 +158,8 @@ Method ListModulesFromTagString( } // Check if search name matches either Name or ExternalName (case-insensitive) - set matchesName = 0 - set matchesExternalName = 0 - - if ($$$lcase(moduleObj.Name) = $$$lcase(searchCriteria.Name)) { - set matchesName = 1 - } - if (moduleObj.ExternalName '= "") && ($$$lcase(moduleObj.ExternalName) = $$$lcase(searchCriteria.Name)) { - set matchesExternalName = 1 - } + set matchesName = ($$$lcase(moduleObj.Name) = $$$lcase(searchCriteria.Name)) + set matchesExternalName = ((moduleObj.ExternalName '= "") && ($$$lcase(moduleObj.ExternalName) = $$$lcase(searchCriteria.Name))) // Only include if at least one matches if 'matchesName && 'matchesExternalName { @@ -228,59 +208,60 @@ Method ListModulesFromTagString( /// Note: for ORAS repositories only, the internal name and external name can be used interchangeably Method ListModules(pSearchCriteria As %IPM.Repo.SearchCriteria) As %ListOfObjects(ELEMENTTYPE="%IPM.Storage.ModuleInfo") { - #; Get ORAS client + // Get ORAS client set client = ..GetClient(..Location, ..Username, ..Password, ..Token, ..TokenAuthMethod) - #; Parse search criteria + // Parse search criteria set tVersionExpression = pSearchCriteria.VersionExpression set tSC = ##class(%IPM.General.SemanticVersionExpression).FromString(pSearchCriteria.VersionExpression, .tVersionExpression) $$$ThrowOnError(tSC) - #; Get all modules + // Collect list of modules set tList = ##class(%Library.ListOfObjects).%New() - #; get all versions // Use /v2/_catalog to enumerate all packages, then filter appropriately + // Note: not every registry supports this endpoint, but without it, searching becomes much harder without using registry-specific endpoints set request = ..GetHttpRequest() - #; Make GET request + // Make GET request // response is a JSON structure like {"repositories":["package1", "package2", ...]} set tSC=request.Get(..PathPrefix _ "/v2/_catalog") $$$ThrowOnError(tSC) set response=request.HttpResponse - if (response.StatusCode'=200) { - // TODO improve error processing - set msg = "Error: Call to /v2/_catalog endpoint failed" - set msg = msg _ $char(10,13) _ "Response Code: "_response.StatusCode _ " - " _ response.Data.Read() - $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) - } - - #; Handle results - set json = "" - while 'response.Data.AtEnd { - set json = json _ response.Data.Read() - } - set data = ##class(%DynamicAbstractObject).%FromJSON(json) - set iter = data.repositories.%GetIterator() - // Iterate through each package - // key is the index - // package is the package name - // type is always "string" - while iter.%GetNext(.key, .package, .type) { - if (package="") { - continue - } - - // If namespace is configured, only consider packages in that namespace - if (..Namespace '= "") { - set namespacePrefix = ..Namespace _ "/" - // Skip packages not in this namespace - if ($extract(package, 1, $length(namespacePrefix)) '= namespacePrefix) { + if (response.StatusCode=200) { + // Handle results + set data = ##class(%DynamicAbstractObject).%FromJSON(response.Data) + set iter = data.repositories.%GetIterator() + // Iterate through each package + // key is the index + // package is the package name + // type is always "string" + while iter.%GetNext(.key, .package, .type) { + if (package="") { continue } - } - // collect all versions for this package in tList - do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, package, .tList) + // If namespace is configured, only consider packages in that namespace + if (..Namespace '= "") { + set namespacePrefix = ..Namespace _ "/" + // Skip packages not in this namespace + if ($extract(package, 1, $length(namespacePrefix)) '= namespacePrefix) { + continue + } + } + + // collect all versions for this package in tList + do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, package, .tList) + } + } else { + // Endpoint failed, but if the name is specified, still try to find it + if pSearchCriteria.Name '= "" { + do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, pSearchCriteria.Name, .tList) + } else { + // otherwise error out + set msg = "Error: Call to /v2/_catalog endpoint failed. This registry may not support it." + set msg = msg _ $char(10,13) _ "Response Code: "_response.StatusCode _ " - " _ response.ReasonPhrase + $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) + } } return tList } diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index 0f9ba459..ce8ba82c 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -16,7 +16,7 @@ Property VersionString As %IPM.DataType.VersionString(XMLNAME = "Version") [ Ini /// Does not need comparison method to be code generated because that comparing VersionString is good enough. Property Version As %IPM.General.SemanticVersion(ForceCodeGenerate = 0, XMLPROJECTION = "NONE") [ Required ]; -Property ExternalName As %IPM.DataType.ModuleName; +Property ExternalName As %String(MAXLEN = 255); Index ExternalName On ExternalName [ Unique ]; From 01ed1c4e2980d1f069512d0ba7c56e57e53baac5 Mon Sep 17 00:00:00 2001 From: isc-dchui Date: Fri, 19 Dec 2025 13:40:31 -0500 Subject: [PATCH 7/7] Address more review comments --- src/cls/IPM/Main.cls | 11 +- src/cls/IPM/Repo/Oras/PackageService.cls | 12 +- src/cls/IPM/Utils/Module.cls | 2 +- .../Test/PM/Integration/InstallModule.cls | 226 +++++++++++++++++- .../PM/Integration/PublishExternalName.cls | 216 ----------------- .../Test/PM/Integration/Update.cls | 70 ++++++ 6 files changed, 310 insertions(+), 227 deletions(-) diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index f0e993e1..0cbe438d 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -2154,7 +2154,7 @@ ClassMethod ShowModulesForRepository( set list(list, "AllVersions") = tRes.AllVersions } } - // Also need to throw an error if there are no results and tSC is set + // Throw an error if there are no results and tSC is set $$$ThrowOnError(tSC) set list("width") = width write ! @@ -2511,13 +2511,13 @@ ClassMethod Unpublish(ByRef pCommandInfo) [ Internal ] set force = $data(pCommandInfo("modifiers","force")) if 'force { if ($$$lcase(tVersion)="all") { - write $$$FormattedLine($$$Red, "Deleting a package and all its versions is an irreversible action") + write $$$FormattedLine($$$Red, "Deleting a package and all its versions is an irreversible action!") set tHelp = "Enter ""Yes"" if you want to delete all package versions." set tMsg = "Are you sure you want to delete all versions of the package """_tModuleName_""" from registry """_tServer.Name_""" ("_tServer.URL_")?" } else { - write $$$FormattedLine($$$Red, "Deleting a package version is an irreversible action") + write $$$FormattedLine($$$Red, "Deleting a package version is an irreversible action!") set tHelp = "Enter ""Yes"" if you want to delete selected package version." - set tMsg = "Are you sure you want to delete the package """_tModuleName_" "_tVersion_""" from registry """_tServer.Name_""" ("_tServer.URL_")?" + set tMsg = "Are you sure you want to delete the package """_tModuleName_" version "_tVersion_""" from registry """_tServer.Name_""" ("_tServer.URL_")?" } set tResponse = ##class(%Library.Prompt).GetYesNo(tMsg,.tResult,.tHelp) @@ -2529,7 +2529,8 @@ ClassMethod Unpublish(ByRef pCommandInfo) [ Internal ] if (tResult || force) { $$$ThrowOnError(tManager.Unpublish(tServer.Name, tModuleName, tVersion)) - write !!,"Package deleted" + set msg = "Package deleted" _ $case(force, 1:" forcefully without user confirmation", :"") + write !!, msg } } else { write !,"The package could not be deleted (the registry denied the request)",! diff --git a/src/cls/IPM/Repo/Oras/PackageService.cls b/src/cls/IPM/Repo/Oras/PackageService.cls index b012c158..3d72a754 100644 --- a/src/cls/IPM/Repo/Oras/PackageService.cls +++ b/src/cls/IPM/Repo/Oras/PackageService.cls @@ -140,8 +140,9 @@ Method ListModulesFromTagString( // First check if package name already matches set packageNameMatches = ($$$lcase(packageNameWithoutNamespace) = $$$lcase(searchCriteria.Name)) + // If packageNameMatches is true, proceed without additional checks + // Otherwise fetch module.xml to check namespaced Name and ExternalName if 'packageNameMatches { - // Package name doesn't match - fetch module.xml to check namespaced Name and ExternalName // Note: pass empty namespace since 'name' already includes the full path set moduleXMLText = ..GetModuleXML(..Location, name _ ":" _ tag, "", ..Username, ..Password, ..Token, ..TokenAuthMethod) @@ -170,7 +171,6 @@ Method ListModulesFromTagString( continue } } - // If packageNameMatches is true, proceed without additional checks } set tModRef = ##class(%IPM.Storage.ModuleInfo).%New() @@ -254,8 +254,12 @@ Method ListModules(pSearchCriteria As %IPM.Repo.SearchCriteria) As %ListOfObject } } else { // Endpoint failed, but if the name is specified, still try to find it - if pSearchCriteria.Name '= "" { - do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, pSearchCriteria.Name, .tList) + set name = pSearchCriteria.Name + if (name '= "") { + if ..Namespace '= "" { + set name = ..AppendURIs(..Namespace, name) + } + do ..ListModulesFromTagString(tVersionExpression, client, pSearchCriteria, name, .tList) } else { // otherwise error out set msg = "Error: Call to /v2/_catalog endpoint failed. This registry may not support it." diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index 13019c70..e24f111a 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -334,7 +334,7 @@ ClassMethod GetModuleObjectFromString( moduleXML As %String, Output found As %Boolean) As %IPM.Storage.Module { - #; Create stream from XML text + // Create stream from XML text set stream = ##class(%Stream.GlobalCharacter).%New() do stream.Write(moduleXML) diff --git a/tests/integration_tests/Test/PM/Integration/InstallModule.cls b/tests/integration_tests/Test/PM/Integration/InstallModule.cls index e6e14b13..4e4c4c65 100644 --- a/tests/integration_tests/Test/PM/Integration/InstallModule.cls +++ b/tests/integration_tests/Test/PM/Integration/InstallModule.cls @@ -1,6 +1,12 @@ Class Test.PM.Integration.InstallModule Extends Test.PM.Integration.Base { +Parameter DefaultName = "default-name"; + +Parameter ExternalName = "external-name"; + +Parameter Folder = "publish-external-name"; + Method OnBeforeAllTests() As %Status { // Using update-test modules for the test of making sure update steps are used correctly with zpm "install" @@ -14,7 +20,18 @@ Method OnAfterAllTests() As %Status // Remove test repository after tests have been run set sc = ##class(%IPM.Main).Shell("repo -delete -name update-test-modules") do $$$AssertStatusOK(sc,"Deleted update-test-modules repo successfully.") - return sc + + // Clean up zot registry - ensure packages are removed even if test failed + set sc = ##class(%IPM.Main).Shell("repo -o -name zot -url http://oras:5000") + set sc = ##class(%IPM.Main).Shell("unpublish zot/default-name all -force") + set sc = ##class(%IPM.Main).Shell("unpublish zot/external-name all -force") + // Also clean from namespaced path + set sc = ##class(%IPM.Main).Shell("repo -n zot -ns test") + set sc = ##class(%IPM.Main).Shell("unpublish zot/default-name all -force") + set sc = ##class(%IPM.Main).Shell("unpublish zot/external-name all -force") + set sc = ##class(%IPM.Main).Shell("repo -n zot -delete") + + return $$$OK } Method TestSimpleApp() @@ -198,4 +215,211 @@ Method ConfirmUpdateStepsAreSeeded(updateStepsToSeed As %DynamicArray) } } +/// Tests that external name and default name can be used interchangeably for installing +Method TestInterchangeableNames() +{ + // Set up repo + set sc = ##class(%IPM.Main).Shell("repo -o -name zot -url http://oras:5000") + do $$$AssertStatusOK(sc,"Zot repo configured succesfully") + + // Make sure modules are not already published + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + + // Load module + set moduleDir = ..GetModuleDir(..#Folder) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module") + + + // Test 1: Publish with default name and install using external name + + // Publish module + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot") + do $$$AssertStatusOK(sc,"Published module successfully with default name") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module with external name") + + // Install using external name + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully installed module with external name") + + // Cleanup by unpublishing and uninstalling + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + // Verify cannot install now + set sc = ##class(%IPM.Main).Shell("install -verbose zot/" _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Test 1: Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Test 1: Correctly failed to install module with external name") + + + // Test 2: Publish with external name using -use-ext flag and install using internal name + // Load and publish module + set moduleDir = ..GetModuleDir(..#Folder) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module") + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext") + do $$$AssertStatusOK(sc,"Published module successfully with default name but -use-ext flag") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") + + // Install using internal name + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully installed module with default name") + + // Cleanup by unpublishing and uninstalling + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + // Verify cannot install now + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Test 2: Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Test 2: Correctly failed to install module with external name") + + + // Test 3: Publish with external name without -use-ext flag (will appear in repo under internal name) and install using internal name + // Load and publish module + set moduleDir = ..GetModuleDir(..#Folder) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module") + set sc = ##class(%IPM.Main).Shell("publish " _ ..#ExternalName _ " -verbose -r zot") + do $$$AssertStatusOK(sc,"Published module successfully with external name") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") + + // Install using internal name + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully installed module with default name") + + // Cleanup by unpublishing and uninstalling + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + // Verify cannot install now + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Test 3: Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Test 3: Correctly failed to install module with external name") + + + // Test 4: Publish with external name without -use-ext flag and install using external name + // Load and publish module + set moduleDir = ..GetModuleDir(..#Folder) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module") + set sc = ##class(%IPM.Main).Shell("publish " _ ..#ExternalName _ " -verbose -r zot") + do $$$AssertStatusOK(sc,"Published module successfully with external name") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") + + // Install using internal name + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully installed module with external name") + + // Cleanup by unpublishing and uninstalling + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + // Verify cannot install now + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Test 4: Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Test 4: Correctly failed to install module with external name") + + + // Test 5: Test with namespace - publish with -use-ext and install using default name + // Reconfigure zot repo with namespace + set sc = ##class(%IPM.Main).Shell("repo -n zot -ns test") + do $$$AssertStatusOK(sc, "Reconfigured zot repo with namespace 'test'") + + // Load and publish module + set moduleDir = ..GetModuleDir(..#Folder) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module") + // Publish with -use-ext (will be stored as test/external-name) + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext") + do $$$AssertStatusOK(sc, "Published module with -use-ext in namespaced repo") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + + // Install using default name (should find test/external-name and match via module.xml) + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully installed module with default name from namespaced repo") + + // Cleanup by unpublishing and uninstalling + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + // Verify cannot install now + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Test 5: Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Test 5: Correctly failed to install module with external name") + + + // Test 6: Test with namespace - publish without -use-ext and install using external name + // Publish without -use-ext (will be stored as test/default-name) + // Load and publish module + set moduleDir = ..GetModuleDir(..#Folder) + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module") + set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot") + do $$$AssertStatusOK(sc, "Published module without -use-ext in namespaced repo") + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + + // Install using external name (should find test/default-name and match via module.xml) + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusOK(sc, "Successfully installed module with external name from namespaced repo") + + // Cleanup by unpublishing and uninstalling + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished default name succesfully") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc,"Unpublished external name succesfully") + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + // Verify cannot install now + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Test 6: Correctly failed to install module with default name") + set sc = ##class(%IPM.Main).Shell("install zot/" _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Test 6: Correctly failed to install module with external name") + + + // Cleanup by removing zot repo + $$$ThrowOnError(##class(%IPM.Main).Shell("repo -n zot -delete")) +} + } diff --git a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls index cf9e0657..1278df4d 100644 --- a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls +++ b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls @@ -36,220 +36,4 @@ Method TestExternalName() do $$$AssertStatusOK(sc, "Reset repos to default") } -Method TestInterchangeableNames() -{ - // Set up repo - set sc = ##class(%IPM.Main).Shell("repo -o -name zot -url http://oras:5000") - do $$$AssertStatusOK(sc,"Zot repo configured succesfully") - // Make sure modules are not already published - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") - do $$$AssertStatusOK(sc,"Unpublished default name succesfully") - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") - do $$$AssertStatusOK(sc,"Unpublished default name succesfully") - - // Load module - set moduleDir = ..GetModuleDir(..#Folder) - set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) - do $$$AssertStatusOK(sc, "Loaded module") - - // Test 1: Publish with default name and install using external name - - // Publish module - set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot") - do $$$AssertStatusOK(sc,"Published module successfully with default name") - - // Uninstall module - set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#ExternalName) - do $$$AssertStatusOK(sc, "Successfully uninstalled module with external name") - - // Install using external name - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusOK(sc, "Successfully installed module with external name") - - // Cleanup by unpublishing - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") - do $$$AssertStatusOK(sc,"Unpublished default name succesfully") - // Verify cannot install now - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install module with default name") - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") - - - // Test 2: Publish with external name using -use-ext flag and install using internal name - set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext") - do $$$AssertStatusOK(sc,"Published module successfully with default name but -use-ext flag") - - // Uninstall module - set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") - - // Install using internal name - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully installed module with default name") - - // Cleanup by unpublishing - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") - do $$$AssertStatusOK(sc,"Unpublished external name succesfully") - // Verify cannot install now - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install module with default name") - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") - - - // Test 3: Publish with external name without -use-ext flag (will appear in repo under internal name) and install using internal name - set sc = ##class(%IPM.Main).Shell("publish " _ ..#ExternalName _ " -verbose -r zot") - do $$$AssertStatusOK(sc,"Published module successfully with external name") - - // Uninstall module - set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") - - // Install using internal name - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully installed module with default name") - - // Cleanup by unpublishing - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") - do $$$AssertStatusOK(sc,"Unpublished external name succesfully") - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install module with default name") - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") - - - // Test 4: Publish with external name without -use-ext flag and install using external name - set sc = ##class(%IPM.Main).Shell("publish " _ ..#ExternalName _ " -verbose -r zot") - do $$$AssertStatusOK(sc,"Published module successfully with external name") - - // Uninstall module - set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully uninstalled module with default name") - - // Install using internal name - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusOK(sc, "Successfully installed module with external name") - - // Cleanup by unpublishing - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") - do $$$AssertStatusOK(sc,"Unpublished external name succesfully") - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install module with default name") - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install module with external name") - - - // Test 5: Test with namespace - publish with -use-ext and install using default name - // Reconfigure zot repo with namespace - set sc = ##class(%IPM.Main).Shell("repo -n zot -ns test") - do $$$AssertStatusOK(sc, "Reconfigured zot repo with namespace 'test'") - - // Publish with -use-ext (will be stored as test/external-name) - set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext") - do $$$AssertStatusOK(sc, "Published module with -use-ext in namespaced repo") - - // Uninstall module - set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully uninstalled module") - - // Install using default name (should find test/external-name and match via module.xml) - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully installed module with default name from namespaced repo") - - // Cleanup - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") - do $$$AssertStatusOK(sc, "Unpublished external name from namespaced repo") - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") - - - // Test 6: Test with namespace - publish without -use-ext and install using external name - // Publish without -use-ext (will be stored as test/default-name) - set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot") - do $$$AssertStatusOK(sc, "Published module without -use-ext in namespaced repo") - - // Uninstall module - set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully uninstalled module") - - // Install using external name (should find test/default-name and match via module.xml) - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusOK(sc, "Successfully installed module with external name from namespaced repo") - - // Cleanup - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") - do $$$AssertStatusOK(sc, "Unpublished default name from namespaced repo") - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") - - - // Cleanup by uninstalling module and removing zot repo - $$$ThrowOnError(##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName)) - $$$ThrowOnError(##class(%IPM.Main).Shell("repo -n zot -delete")) -} - -/// Test that update works with external name as well as default name -Method TestInterchangeableNameUpdate() -{ - set moduleDir = ..GetModuleDir(..#Folder) - // Test 1: Publish with default name, update with external name - set sc = ##class(%IPM.Main).Shell("repo -o -name zot -url http://oras:5000") - set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.2")) - do $$$AssertStatusOK(sc, "Loaded module succesfully!") - - // Publish module - set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot") - do $$$AssertStatusOK(sc,"Published module successfully with default name") - - // Uninstall module - set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully uninstalled module") - - // Load version 0.0.1 - set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) - do $$$AssertStatusOK(sc, "Loaded module succesfully!") - - // Update to version 0.0.2 using external name - set sc = ##class(%IPM.Main).Shell("update " _ ..#ExternalName _ " 0.0.2") - do $$$AssertStatusOK(sc, "Updated module succesfully!") - - //Cleanup - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") - do $$$AssertStatusOK(sc, "Unpublished default name from repo") - set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") - - // Test 2: Publish with external name, update with default name - set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.2")) - do $$$AssertStatusOK(sc, "Loaded module succesfully!") - - // Publish module - set sc = ##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext") - do $$$AssertStatusOK(sc,"Published module successfully with external name") - - // Uninstall module - set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) - do $$$AssertStatusOK(sc, "Successfully uninstalled module") - - // Load version 0.0.1 - set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) - do $$$AssertStatusOK(sc, "Loaded module succesfully!") - - // Update to version 0.0.2 using default name - set sc = ##class(%IPM.Main).Shell("update " _ ..#DefaultName _ " 0.0.2") - do $$$AssertStatusOK(sc, "Updated module succesfully!") - - //Cleanup - set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") - do $$$AssertStatusOK(sc, "Unpublished default name from repo") - set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) - do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") - - - // Cleanup by uninstalling module and removing zot repo - $$$ThrowOnError(##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName)) - $$$ThrowOnError(##class(%IPM.Main).Shell("repo -n zot -delete")) -} - } diff --git a/tests/integration_tests/Test/PM/Integration/Update.cls b/tests/integration_tests/Test/PM/Integration/Update.cls index 513c2a34..e4aa9b66 100644 --- a/tests/integration_tests/Test/PM/Integration/Update.cls +++ b/tests/integration_tests/Test/PM/Integration/Update.cls @@ -35,6 +35,12 @@ Parameter ModuleThreeVersion As STRING = "3.5.0"; Parameter UpdateTestRepo As STRING = "update-test-modules"; +Parameter DefaultName = "default-name"; + +Parameter ExternalName = "external-name"; + +Parameter Folder = "publish-external-name"; + Method OnBeforeAllTests() As %Status { // Clean up repo used for tests @@ -571,4 +577,68 @@ ClassMethod CleanUpDirectory(dir As %String) return } +/// Test that update works with external name as well as default name +Method TestInterchangeableNameUpdate() +{ + set moduleDir = ..GetModuleDir(..#Folder) + // Test 1: Publish with default name, update with external name + $$$ThrowOnError(##class(%IPM.Main).Shell("repo -o -name zot -url http://oras:5000")) + $$$ThrowOnError(##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.2"))) + + // Publish module + $$$ThrowOnError(##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot")) + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + + // Load version 0.0.1 + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module succesfully!") + + // Update to version 0.0.2 using external name + set sc = ##class(%IPM.Main).Shell("update " _ ..#ExternalName _ " 0.0.2") + do $$$AssertStatusOK(sc, "Updated module succesfully!") + + //Cleanup + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc, "Unpublished default name from repo") + set sc = ##class(%IPM.Main).Shell("install " _ ..#DefaultName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") + + // Test 2: Publish with external name, update with default name + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.2")) + do $$$AssertStatusOK(sc, "Loaded module succesfully!") + + // Publish module + $$$ThrowOnError(##class(%IPM.Main).Shell("publish " _ ..#DefaultName _ " -verbose -r zot -use-ext")) + + // Uninstall module + set sc = ##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName) + do $$$AssertStatusOK(sc, "Successfully uninstalled module") + + // Load version 0.0.1 + set sc = ##class(%IPM.Main).Shell($$$FormatText("load %1", moduleDir _ "/0.0.1")) + do $$$AssertStatusOK(sc, "Loaded module succesfully!") + + // Update to version 0.0.2 using default name + set sc = ##class(%IPM.Main).Shell("update " _ ..#DefaultName _ " 0.0.2") + do $$$AssertStatusOK(sc, "Updated module succesfully!") + + //Cleanup + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc, "Unpublished default name from repo") + set sc = ##class(%IPM.Main).Shell("install " _ ..#ExternalName) + do $$$AssertStatusNotOK(sc, "Correctly failed to install after unpublish") + + + // Cleanup by uninstalling module and removing zot repo + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#DefaultName _ " all -force") + do $$$AssertStatusOK(sc, "Unpublished default name from repo") + set sc = ##class(%IPM.Main).Shell("unpublish zot/" _ ..#ExternalName _ " all -force") + do $$$AssertStatusOK(sc, "Unpublished external name from repo") + $$$ThrowOnError(##class(%IPM.Main).Shell("uninstall " _ ..#DefaultName)) + $$$ThrowOnError(##class(%IPM.Main).Shell("repo -n zot -delete")) +} + }