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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #938 Added flag -export-python-deps to package command
- #462: The `repo` command for repository configuration now supports secret input terminal mode for passwords with the `-password-stdin` flag
- #935: Adding a generic JFrog Artifactory tarball resource processor for bundling artifact with a package and deploying it to a final location on install.
- #967: Added package pinning commands (`pin` and `unpin`) to lock module versions and prevent accidental modification. The `list` command now displays the `-Pinned` status, and the new `list -p` modifier shows only pinned modules.

### Changed
- #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies
Expand Down
68 changes: 65 additions & 3 deletions src/cls/IPM/Main.cls
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ reinstall -env /path/to/env1.json;/path/to/env2.json example-package
<modifier name="showtime" aliases="st" description="If specified, show the time of last update for each module" />
<modifier name="showupstream" aliases="su" description="If specified, show the latest version for each module in configured repos if it's different than the local version." />
<modifier name="repository" aliases="repo" value="true" description="If specified, only show modules installed that belong to the provided repository." />

<modifier name="pinned" aliases="p" description="If specified, lists pinned IPM modules." />
</command>

<command name="list-dependents" aliases="dependents">
Expand Down Expand Up @@ -758,6 +758,22 @@ generate /my/path -export 00000,PacketName2,IgnorePacket2^00000,PacketName3,Igno
</example>
</command>

<command name="pin" dataPrefix="P">
<description>Marks specific module as pinned to prevent future automatic ZPM operations.</description>
<example description="Marks specific module as pinned to prevent future automatic upgrades.">
pin HS.JSON
</example>
<parameter name="module" required="true" description="Name of the module to pin." />
</command>

<command name="unpin" dataPrefix="P">
<description>Removes the pinned status from an installed module, allowing future ZPM operations.</description>
<example description="Removes the pinned status from an installed module, allowing future ZPM operations.">
unpin HS.JSON
</example>
<parameter name="module" required="true" description="Name of the module unpin." />
</command>

</commands>
}

Expand Down Expand Up @@ -992,6 +1008,10 @@ ClassMethod ShellInternal(
do ..ModuleVersion(.tCommandInfo)
} elseif (tCommandInfo = "information") {
do ..Information(.tCommandInfo)
} elseif (tCommandInfo = "pin") {
do ..Pin(.tCommandInfo)
} elseif (tCommandInfo = "unpin") {
do ..UnPin(.tCommandInfo)
}
} catch pException {
if (pException.Code = $$$ERCTRLC) {
Expand Down Expand Up @@ -1200,7 +1220,9 @@ ClassMethod GetListModules(
$$$ThrowOnError(tSC)
set name = tRes.Name
set list = list + 1
set list(list) = $listbuild(name, tRes.VersionString, tRes.ExternalName, tRes.DeveloperMode, tRes.Root)
set IsModulePinned=$select(##class(%IPM.Storage.PinnedModule).IsModulePinned(name):"-Pinned",1:"")

set list(list) = $listbuild(name, tRes.VersionString_IsModulePinned, tRes.ExternalName, tRes.DeveloperMode, tRes.Root)
if hasLastUpdated {
set list(list) = list(list) _ $listbuild(tRes.LastUpdated)
}
Expand Down Expand Up @@ -2248,6 +2270,10 @@ ClassMethod Install(ByRef pCommandInfo) [ Internal ]
$$$ThrowStatus($$$ERROR($$$GeneralError, "No repositories are configured and enabled in this namespace."))
}

//verify the module is pinned
if ##class(%IPM.Storage.PinnedModule).IsModulePinned(tModuleName) {
$$$ThrowStatus($$$ERROR($$$GeneralError, "The module "_tModuleName_" is pinned unable to install"))
}
set tVersion = $get(pCommandInfo("parameters","version"))
set tKeywords = $$$GetModifier(pCommandInfo,"keywords")
set tSynchronous = $$$HasModifier(pCommandInfo,"synchronous")
Expand Down Expand Up @@ -2340,6 +2366,10 @@ ClassMethod Reinstall(ByRef pCommandInfo) [ Internal ]
$$$ThrowOnError(..CheckModuleNamespace())
}

//verify the module is pinned
if ##class(%IPM.Storage.PinnedModule).IsModulePinned(tModuleName) {
$$$ThrowStatus($$$ERROR($$$GeneralError, "The module "_tModuleName_" is pinned unable to reinstall"))
}
set tVersionString = tModule.Version.ToString()
write !,"Reinstalling ",tModuleName," ",tVersionString
set pCommandInfo("parameters","version") = tVersionString
Expand All @@ -2354,11 +2384,16 @@ ClassMethod Uninstall(ByRef pCommandInfo) [ Internal ]
$$$ThrowOnError(##class(%IPM.Utils.Module).UninstallAll(tForce,.tParams))
return
} else {
set tModuleName = pCommandInfo("parameters","module")
set tModuleName = $get(pCommandInfo("parameters","module"))
if (tModuleName = $$$IPMModuleName) {
$$$ThrowOnError(..CheckModuleNamespace())
}
set tRecurse = $$$HasModifier(pCommandInfo,"recurse") // Recursively uninstall unneeded dependencies

//verify the module is pinned
if ##class(%IPM.Storage.PinnedModule).IsModulePinned(tModuleName) {
$$$ThrowStatus($$$ERROR($$$GeneralError, "The module "_tModuleName_" is pinned unable to uninstall"))
}
$$$ThrowOnError(##class(%IPM.Storage.Module).Uninstall(tModuleName,tForce,tRecurse,.tParams))
}
}
Expand Down Expand Up @@ -2552,6 +2587,13 @@ ClassMethod ActiveNamespacesClose(qHandle As %Binary) As %Status [ PlaceAfter =
ClassMethod ListInstalled(ByRef pCommandInfo) [ Private ]
{
set tSearchString = $get(pCommandInfo("parameters","searchString"),"")
// list pinned modules
if $data(pCommandInfo("modifiers","pinned")) {
merge tModifiers = pCommandInfo("modifiers")
do ##class(%IPM.Storage.PinnedModule).GetAllPinnedModules(.list)
do ..DisplayModules(.list,,,, .tModifiers)
quit $$$OK
}
if (''$data(pCommandInfo("modifiers","tree"))) {
// Show tree of dependencies as well.
// Modules that are dependencies for no other are shown at the top level.
Expand Down Expand Up @@ -3858,6 +3900,10 @@ ClassMethod Update(ByRef pCommandInfo)
if '##class(%IPM.Storage.Module).NameExists(moduleName) && '##class(%IPM.Storage.Module).ExternalNameExists(moduleName) {
$$$ThrowStatus($$$ERROR($$$GeneralError,"Module "_moduleName_" has not been installed. Please use the install or load commands to install the module before calling update."))
}
//verify the module is pinned
if ##class(%IPM.Storage.PinnedModule).IsModulePinned(moduleName) {
$$$ThrowStatus($$$ERROR($$$GeneralError, "The module "_moduleName_" is pinned unable to update."))
}
if verbose {
if path '= "" {
write !,"Resolving version of module specified by -path flag: ",path," to update",!
Expand All @@ -3881,4 +3927,20 @@ ClassMethod Update(ByRef pCommandInfo)
}
}

ClassMethod Pin(ByRef pCommandInfo) As %Status
{
set sc = ##class(%IPM.Storage.PinnedModule).PinModule(pCommandInfo("parameters","module"))
$$$ThrowOnError(sc)
write "Module "_pCommandInfo("parameters","module")_" is now pinned"
return 1
}

ClassMethod UnPin(ByRef pCommandInfo) As %Status
{
set sc = ##class(%IPM.Storage.PinnedModule).UnPinModule(pCommandInfo("parameters","module"))
$$$ThrowOnError(sc)
write "Module "_pCommandInfo("parameters","module")_" is now unpinned"
return 1
}

}
96 changes: 96 additions & 0 deletions src/cls/IPM/Storage/PinnedModule.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
Class %IPM.Storage.PinnedModule Extends (%Persistent, %JSON.Adaptor, %XML.Adaptor)
{

Parameter DEFAULTGLOBAL = "^IPM.Storage.PinnedModule";

Parameter DOMAIN = "ZPM";

Property Name As %String(MAXLEN = 128) [ Required ];

Property PinnedVersion As %String(MAXLEN = 32) [ Required ];

Property PinnedAt As %TimeStamp [ Required ];

Property PinnedBy As %String;

Index PinnedModuleIdx On Name [ IdKey, Unique ];

ClassMethod IsModulePinned(pModule As %String = "") As %Boolean [ CodeMode = expression ]
{
$select($$$lcase(pModule)'="": ..%ExistsId($$$lcase(pModule)),1: 0)
}

ClassMethod GetAllPinnedModules(Output list)
{
set tResult = ##class(%SQL.Statement).%ExecDirect(,"select Name, PinnedVersion from %IPM_Storage.PinnedModule")
$$$ThrowSQLIfError(tResult.%SQLCODE, tResult.%Message)
kill list
set width=0
while tResult.%Next(.tSC) {
$$$ThrowOnError(tSC)
set name=$zconvert(tResult.%Get("Name"),"l")
set list($increment(list))=$listbuild(name,$zconvert(tResult.%Get("PinnedVersion"),"l")_"-pinned")
if $length(name)>width set width=$length(name)
}
set list("width") = width
}

/// Pin Only Installed Modules
ClassMethod PinModule(pModule As %String = "") As %Status
{
set pModule=$$$lcase(pModule)
if ..IsModulePinned(pModule) {
return $$$ERROR($$$GeneralError,"Module "_pModule_" already pinned")
}
&SQL(SELECT ID INTO :id FROM %IPM_Storage.ModuleItem where name=:pModule)
if id = ""{
return $$$ERROR($$$GeneralError,"Module "_pModule_" not found")
}
set moduleObj = ##class(%IPM.Storage.Module).%OpenId(id)
set pinModule = ..%New()
set pinModule.Name= moduleObj.Name
set pinModule.PinnedVersion = moduleObj.VersionString
set pinModule.PinnedAt = $zdatetime($ztimestamp,3)
set pinModule.PinnedBy = $username
set sc = pinModule.%Save()
if $$$ISERR(sc) {
return sc
}
return $$$OK
}

/// UnPin Only Installed Modules
ClassMethod UnPinModule(pModule As %String = "") As %Status
{
set pModule = $$$lcase(pModule)
if '..IsModulePinned(pModule) {
return $$$ERROR($$$GeneralError,"Module "_pModule_" not currently pinned.")
}
return ..%DeleteId(pModule)
}

Storage Default
{
<Data name="PinnedModuleDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>PinnedVersion</Value>
</Value>
<Value name="3">
<Value>PinnedAt</Value>
</Value>
<Value name="4">
<Value>PinnedBy</Value>
</Value>
</Data>
<DataLocation>^IPM.Storage.PinnedModuleD</DataLocation>
<DefaultData>PinnedModuleDefaultData</DefaultData>
<IdLocation>^IPM.Storage.PinnedModuleD</IdLocation>
<IndexLocation>^IPM.Storage.PinnedModuleI</IndexLocation>
<StreamLocation>^IPM.Storage.PinnedModuleS</StreamLocation>
<Type>%Storage.Persistent</Type>
}

}
2 changes: 2 additions & 0 deletions src/cls/IPM/Utils/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,8 @@ ClassMethod UninstallAll(
do ##class(%IPM.Main).GetListModules(,,.modList)
for i = 1:1:modList {
set name = $listget(modList(i),1)
;prevent from uninstall the pinned module
continue:##class(%IPM.Storage.PinnedModule).IsModulePinned(name)
set simpleModList(name) = ""
// list of modules must be from here because modules without dependencies won't appear in %IPM_Storage.ModuleItem_Dependencies
set tModuleNames(name) = ""
Expand Down
110 changes: 110 additions & 0 deletions tests/integration_tests/Test/PM/Integration/TestCLIPinCommand.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
Class Test.PM.Integration.TestCLIPinCommand Extends %UnitTest.TestCase
{

/// Test.PM.Integration.Base
Parameter ModuleFolder = "pin";

/// lightweight library used for python pip installation and uninstallation.
Parameter TestZPMModule = "web-fslog";

// test pin command

Method TestCLIPinCommand()
{
set sc = $$$OK
set sc = ..InstallZPMModule()
do $$$AssertStatusOK(sc, "installing the module "_..#TestZPMModule)
set tCommand = "pin "_..#TestZPMModule
set sc = ..RunCommand(tCommand)
do $$$AssertStatusOK(sc, "pinned the module "_..#TestZPMModule)
do ..RePinThePinnedMoudle()
do ..PreventFromInstall()
do ..PreventFromReInstall()
do ..PreventFromUnInstall()
do ..PreventFromUpdate()
do ..CLIUnPinCommand()
quit sc
}

/// test unpin command
Method CLIUnPinCommand()
{
set sc = $$$OK
set tCommand ="unpin "_..#TestZPMModule
set sc = ..RunCommand(tCommand)
do $$$AssertStatusOK(sc, "unpin the module "_..#TestZPMModule)
do ..UnInstallZPMModule()
do $$$AssertStatusOK(sc, "uninstalled the module "_..#TestZPMModule)
do ..UnPinTheUnpinnedModule()
quit sc
}

Method UnPinTheUnpinnedModule()
{
set tCommand ="unpin x-textpacakge"
set sc = ..RunCommand(tCommand)
do $$$AssertStatusNotOK(sc, "Unpin the unpinned module x-textpacakge")
}

Method RePinThePinnedMoudle()
{
set tCommand ="pin "_..#TestZPMModule
set sc = ..RunCommand(tCommand)
do $$$AssertStatusNotOK(sc, "pin the already pinned module "_..#TestZPMModule)
}

/// list -p : display all pinned modules
Method ListPinnedModules()
{
set sc = ..RunCommand("list -p")
do $$$AssertStatusOK(sc, "list all the pinned "_..#TestZPMModule)
return sc
}

Method PreventFromInstall()
{
set sc = ..RunCommand("install "_..#TestZPMModule)
do $$$AssertStatusNotOK(sc, "prevent from install the module"_..#TestZPMModule)
return sc
}

Method PreventFromReInstall()
{
set sc = ..RunCommand("reinstall "_..#TestZPMModule)
do $$$AssertStatusNotOK(sc, "prevent from reinstall the module"_..#TestZPMModule)
return sc
}

Method PreventFromUnInstall()
{
set sc = ..RunCommand("uninstall "_..#TestZPMModule)
do $$$AssertStatusNotOK(sc, "prevent from uninstall the module"_..#TestZPMModule)
return sc
}

Method PreventFromUpdate()
{
set sc = ..RunCommand("update "_..#TestZPMModule)
do $$$AssertStatusNotOK(sc, "prevent from update the module"_..#TestZPMModule)
}

Method InstallZPMModule(pModule As %String = {..#TestZPMModule})
{
set tCommand="install "_pModule
return ..RunCommand(tCommand)
}

Method UnInstallZPMModule(pModule As %String = "")
{
set tCommand="uninstall "_pModule
return ..RunCommand(tCommand)
}

Method RunCommand(pCommand As %String)
{
set sc= ##class(%IPM.Main).Shell(pCommand)
do $$$LogMessage("Run command: "_pCommand)
return sc
}

}
Loading
Loading