diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f7f567..bd436bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,10 @@ 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 +- #578: Added functionality to record and display IPM history of install, uninstall, load, and update - #961: Adding creation of a lock file for a module by using the `-create-lockfile` flag on install. +- #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 - #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies @@ -27,7 +29,7 @@ lock contention by bypassing IRIS compiler. - Have better caching of results for module searches by collapsing search expressions (reducing expressions that are intersections). ### 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 @@ -37,6 +39,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/Main.cls b/src/cls/IPM/Main.cls index 8734ec9a..0cbe438d 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -179,10 +179,10 @@ This command is an alias for `module-action module-name makedeployed` Delete package from repository unpublish MyModuleName all -unpublish MyModuleName 1.0.0 +unpublish MyRepo/MyModuleName 1.0.0 - + @@ -2154,6 +2154,8 @@ ClassMethod ShowModulesForRepository( set list(list, "AllVersions") = tRes.AllVersions } } + // Throw an error if there are no results and tSC is set + $$$ThrowOnError(tSC) set list("width") = width write ! do ..DisplayModules(.list) @@ -2506,25 +2508,29 @@ 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_" version "_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" + 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 599c54f0..3d72a754 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,11 +126,53 @@ 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 + 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 = $piece(name, "/", *) + + // 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 { + // Note: pass empty namespace since 'name' already includes the full path + set moduleXMLText = ..GetModuleXML(..Location, name _ ":" _ tag, "", ..Username, ..Password, ..Token, ..TokenAuthMethod) + + if (moduleXMLText '= "") { + // Parse module.xml + 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 + } + + // Check if search name matches either Name or ExternalName (case-insensitive) + 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 { + continue + } + } else { + // No module.xml available - skip this module + continue + } + } + } + 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 @@ -162,48 +204,32 @@ 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 + // Get ORAS client set client = ..GetClient(..Location, ..Username, ..Password, ..Token, ..TokenAuthMethod) - #; Parse search criteria - set name = $$$lcase(pSearchCriteria.Name) + // Parse search criteria 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 + // Collect list of 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" - 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)) - } - - #; Handle results - set json = "" - while 'response.Data.AtEnd { - set json = json _ response.Data.Read() - } - set data = ##class(%DynamicAbstractObject).%FromJSON(json) + // 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 + // 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) { + // Handle results + set data = ##class(%DynamicAbstractObject).%FromJSON(response.Data) set iter = data.repositories.%GetIterator() // Iterate through each package // key is the index @@ -213,22 +239,38 @@ Method ListModules(pSearchCriteria As %IPM.Repo.SearchCriteria) As %ListOfObject if (package="") { continue } - #; filter by module name if requested - if (name'="") && (package'=name) { - 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) { + continue + } } - #; collect all versions for this package in tList + + // 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) + // Endpoint failed, but if the name is specified, still try to find it + 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." + set msg = msg _ $char(10,13) _ "Response Code: "_response.StatusCode _ " - " _ response.ReasonPhrase + $$$ThrowStatus($$$ERROR($$$GeneralError, msg)) + } } return tList } -// ** ORAS FUNCTIONS ** - +/// ** ORAS FUNCTIONS ** /// Returns an authenticated ORAS client ClassMethod GetClient( registry As %String, @@ -391,8 +433,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 cf39ef63..e24f111a 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( @@ -1132,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/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/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/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 e250f20b..1278df4d 100644 --- a/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls +++ b/tests/integration_tests/Test/PM/Integration/PublishExternalName.cls @@ -9,31 +9,31 @@ 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 _ "/0.0.1")) + 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") } } 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")) +} + } 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