@@ -23,14 +23,16 @@ func installCmd(_ *client.DatumCloudFactory) *cobra.Command {
2323 Short : "Install a datumctl plugin" ,
2424 Long : `Install a datumctl plugin from a plugin catalog or a GitHub Release.
2525
26- With no arguments, restores all plugins recorded in plugins.json to their
27- recorded versions. Use this to reproduce a plugin set on a new machine.
26+ With no arguments, restores all previously installed plugins to their recorded
27+ versions. Use this to reproduce a plugin set on a new machine.
2828
2929With a plugin name argument, installs from a registered catalog:
30- - name resolves against the official datum catalog first, then any
31- other registered catalog; a name found in more than one
32- catalog prints the options instead of guessing
33- - catalog/name installs from a specific registered catalog (e.g. datum/dns)
30+ - name resolves against the official datum catalog first, then
31+ any other registered catalog; a name found in more than
32+ one catalog prints the options instead of guessing
33+ - name@version installs a specific version of a catalog plugin
34+ - catalog/name installs from a specific registered catalog (e.g. datum/dns)
35+ - catalog/name@version installs a specific version from a specific catalog
3436
3537With an owner/repo argument, installs directly from a GitHub Release:
3638 - owner/repo installs the latest release
@@ -50,7 +52,11 @@ The plugin binary is written to the managed plugins directory
5052 # Install a specific version from GitHub
5153 datumctl plugin install datum-cloud/datumctl-dns@v1.2.0
5254
53- # Restore all plugins from plugins.json
55+ # Install a specific version of a catalog plugin
56+ datumctl plugin install dns@v1.2.0
57+ datumctl plugin install acme/deploy@v2.0.0
58+
59+ # Restore all previously installed plugins
5460 datumctl plugin install` ,
5561 RunE : func (cmd * cobra.Command , args []string ) error {
5662 pluginsDir , err := resolvePluginsDir (cmd )
@@ -87,31 +93,77 @@ The plugin binary is written to the managed plugins directory
8793 return cmd
8894}
8995
90- // installArg dispatches a single install argument to the right path based on
91- // catalog-aware addressing:
92- // - "owner/repo@version" -> direct GitHub release (the '@' is unambiguous)
93- // - "catalog/name" -> catalog-qualified, when the prefix is a
94- // registered catalog name
95- // - "owner/repo" -> direct GitHub release otherwise
96- // - "name" -> bare name, resolved across catalogs (default
97- // first); a collision prints the qualified options instead of guessing
98- func installArg (cmd * cobra.Command , pluginsDir string , reg * pluginstore.Registry , arg , currentVersion string ) error {
99- if strings .Contains (arg , "@" ) {
100- return installGitHubArg (cmd , pluginsDir , arg , currentVersion )
101- }
102- if strings .Contains (arg , "/" ) {
103- prefix , rest , _ := strings .Cut (arg , "/" )
96+ // installRouteKind identifies which install path an argument resolves to.
97+ type installRouteKind int
98+
99+ const (
100+ // routeGitHub installs directly from a GitHub release (owner/repo).
101+ routeGitHub installRouteKind = iota
102+ // routeCatalog installs a named plugin from a specific registered catalog.
103+ routeCatalog
104+ // routeBare resolves a bare plugin name across all registered catalogs.
105+ routeBare
106+ )
107+
108+ // installRoute is the parsed routing decision for a single install argument.
109+ // It is produced by routeInstallArg and consumed by installArg; keeping the
110+ // decision in a pure value makes the routing logic unit-testable without
111+ // performing real network installs.
112+ type installRoute struct {
113+ kind installRouteKind
114+ // ghSource is the "owner/repo" (or "github.com/owner/repo") string to hand
115+ // to the GitHub install path; only set for routeGitHub.
116+ ghSource string
117+ // catalog is the registered catalog name; only set for routeCatalog.
118+ catalog string
119+ // name is the plugin name (catalog/bare paths); unset for routeGitHub.
120+ name string
121+ // version is the trailing "@version" requested, or "" for latest. It is
122+ // threaded through every route.
123+ version string
124+ }
125+
126+ // routeInstallArg parses a single install argument into a routing decision.
127+ // Any trailing "@version" is split off FIRST, then routing is decided on the
128+ // remaining string so that version-pinning works uniformly across catalogs and
129+ // GitHub:
130+ // - "catalog/name" -> routeCatalog, when the prefix is a registered catalog
131+ // - "owner/repo" -> routeGitHub otherwise (the slash is owner/repo)
132+ // - "name" -> routeBare, resolved across catalogs (default first)
133+ //
134+ // The version is carried on the route so "catalog/name@v2", "name@v2", and
135+ // "owner/repo@v2" all pin the requested version through their respective paths.
136+ func routeInstallArg (reg * pluginstore.Registry , arg string ) installRoute {
137+ rest , version , _ := strings .Cut (arg , "@" )
138+ if strings .Contains (rest , "/" ) {
139+ prefix , name , _ := strings .Cut (rest , "/" )
104140 if reg .Find (prefix ) != nil {
105- return installFromCatalog ( cmd , pluginsDir , reg , prefix , rest , currentVersion )
141+ return installRoute { kind : routeCatalog , catalog : prefix , name : name , version : version }
106142 }
107- return installGitHubArg (cmd , pluginsDir , arg , currentVersion )
143+ return installRoute {kind : routeGitHub , ghSource : rest , version : version }
144+ }
145+ return installRoute {kind : routeBare , name : rest , version : version }
146+ }
147+
148+ // installArg dispatches a single install argument to the right path based on
149+ // catalog-aware addressing. See routeInstallArg for the routing rules.
150+ func installArg (cmd * cobra.Command , pluginsDir string , reg * pluginstore.Registry , arg , currentVersion string ) error {
151+ route := routeInstallArg (reg , arg )
152+ switch route .kind {
153+ case routeCatalog :
154+ return installFromCatalog (cmd , pluginsDir , reg , route .catalog , route .name , route .version , currentVersion )
155+ case routeBare :
156+ return installBareName (cmd , pluginsDir , reg , route .name , route .version , currentVersion )
157+ default :
158+ return installGitHubArg (cmd , pluginsDir , route .ghSource , route .version , currentVersion )
108159 }
109- return installBareName (cmd , pluginsDir , reg , arg , currentVersion )
110160}
111161
112- // installGitHubArg installs directly from a GitHub release (owner/repo[@version]).
113- func installGitHubArg (cmd * cobra.Command , pluginsDir , arg , currentVersion string ) error {
114- owner , repo , version , parseErr := parseSource (arg )
162+ // installGitHubArg installs directly from a GitHub release. ghSource is the
163+ // "owner/repo" (or "github.com/owner/repo") string with any "@version" already
164+ // split off; the requested version is passed separately.
165+ func installGitHubArg (cmd * cobra.Command , pluginsDir , ghSource , version , currentVersion string ) error {
166+ owner , repo , _ , parseErr := parseSource (ghSource )
115167 if parseErr != nil {
116168 return customerrors .NewUserError (parseErr .Error ())
117169 }
@@ -122,8 +174,10 @@ func installGitHubArg(cmd *cobra.Command, pluginsDir, arg, currentVersion string
122174 return saveAndReport (cmd , pluginsDir , pluginName , entry , binaryPath )
123175}
124176
125- // installFromCatalog installs a named plugin from a specific registered catalog.
126- func installFromCatalog (cmd * cobra.Command , pluginsDir string , reg * pluginstore.Registry , catalogName , pluginName , currentVersion string ) error {
177+ // installFromCatalog installs a named plugin from a specific registered
178+ // catalog. version pins a specific release, or "" installs the catalog's
179+ // recommended version.
180+ func installFromCatalog (cmd * cobra.Command , pluginsDir string , reg * pluginstore.Registry , catalogName , pluginName , version , currentVersion string ) error {
127181 cat := reg .Find (catalogName )
128182 if cat == nil {
129183 return customerrors .NewUserError (fmt .Sprintf ("catalog %q is not registered; run 'datumctl plugin index add %s <source>' first" , catalogName , catalogName ))
@@ -132,7 +186,7 @@ func installFromCatalog(cmd *cobra.Command, pluginsDir string, reg *pluginstore.
132186 if err != nil {
133187 return indexFetchUserError (err )
134188 }
135- idxEntry , name , binaryPath , installErr := installPlugin (cmd .Context (), pluginsDir , pluginName , "" , currentVersion , idx )
189+ idxEntry , name , binaryPath , installErr := installPlugin (cmd .Context (), pluginsDir , pluginName , version , currentVersion , idx )
136190 if installErr != nil {
137191 return customerrors .NewUserError (fmt .Sprintf ("install plugin %s/%s: %v" , catalogName , pluginName , installErr ))
138192 }
@@ -144,13 +198,14 @@ func installFromCatalog(cmd *cobra.Command, pluginsDir string, reg *pluginstore.
144198
145199// installBareName resolves a bare plugin name across all catalogs (default
146200// first) and installs the unique match, or surfaces the options on a collision.
147- func installBareName (cmd * cobra.Command , pluginsDir string , reg * pluginstore.Registry , name , currentVersion string ) error {
201+ // version pins a specific release, or "" installs the recommended version.
202+ func installBareName (cmd * cobra.Command , pluginsDir string , reg * pluginstore.Registry , name , version , currentVersion string ) error {
148203 matches := resolveBareName (cmd , pluginsDir , reg , name )
149204 switch len (matches ) {
150205 case 0 :
151206 return customerrors .NewUserError (fmt .Sprintf ("plugin %q not found in any registered catalog; run 'datumctl plugin search %s' to look for it" , name , name ))
152207 case 1 :
153- return installFromCatalog (cmd , pluginsDir , reg , matches [0 ].catalog .Name , name , currentVersion )
208+ return installFromCatalog (cmd , pluginsDir , reg , matches [0 ].catalog .Name , name , version , currentVersion )
154209 default :
155210 return customerrors .NewUserError (collisionError (name , matches ).Error ())
156211 }
@@ -199,7 +254,7 @@ func installAllFromManifest(cmd *cobra.Command, pluginsDir, currentVersion strin
199254 }
200255
201256 if len (manifest .Plugins ) == 0 {
202- fmt .Fprintln (cmd .OutOrStdout (), "No plugins recorded in plugins.json ." )
257+ fmt .Fprintln (cmd .OutOrStdout (), "No previously installed plugins to restore ." )
203258 return nil
204259 }
205260
0 commit comments