Skip to content

Commit b60b770

Browse files
committed
Add mgrctl commands for GPG key management
1 parent 464fb47 commit b60b770

10 files changed

Lines changed: 412 additions & 7 deletions

File tree

mgradm/cmd/gpg/add/gpg.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2025 SUSE LLC
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

@@ -39,6 +39,7 @@ func newCmd(globalFlags *types.GlobalFlags, run utils.CommandFunc[gpgAddFlags])
3939
var flags gpgAddFlags
4040
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, run)
4141
},
42+
Deprecated: "please use `mgrctl gpg upload` instead",
4243
}
4344

4445
gpgAddKeyCmd.Flags().BoolP("force", "f", false, L("Import without asking confirmation"))

mgradm/cmd/gpg/gpg.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2025 SUSE LLC
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

@@ -15,10 +15,11 @@ import (
1515
// NewCommand import gpg keys from 3rd party repository.
1616
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
1717
gpgKeyCmd := &cobra.Command{
18-
Use: "gpg",
19-
GroupID: "tool",
20-
Short: L("Manage GPG keys for 3rd party repositories"),
21-
Args: cobra.ExactArgs(1),
18+
Use: "gpg",
19+
GroupID: "tool",
20+
Short: L("Manage GPG keys for 3rd party repositories"),
21+
Args: cobra.ExactArgs(1),
22+
Deprecated: "please use `mgrctl gpg` instead",
2223
}
2324

2425
gpgKeyCmd.AddCommand(gpgadd.NewCommand(globalFlags))

mgradm/cmd/gpg/list/gpg.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2025 SUSE LLC
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

@@ -36,6 +36,7 @@ func newCmd(globalFlags *types.GlobalFlags, run utils.CommandFunc[gpgListFlags])
3636
var flags gpgListFlags
3737
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, run)
3838
},
39+
Deprecated: "please use `mgrctl gpg list` instead",
3940
}
4041

4142
gpgListKeyCmd.Flags().BoolP("system", "s", false, L("List keys from system keyring"))

mgrctl/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/api"
1515
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/cp"
1616
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
17+
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/gpg"
1718
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/proxy"
1819
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/ssh"
1920
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/term"
@@ -57,6 +58,7 @@ func NewUyunictlCommand() *cobra.Command {
5758
rootCmd.AddCommand(completion.NewCommand(globalFlags))
5859
rootCmd.AddCommand(proxy.NewCommand(globalFlags))
5960
rootCmd.AddCommand(ssh.NewCommand(globalFlags))
61+
rootCmd.AddCommand(gpg.NewCommand(globalFlags))
6062

6163
rootCmd.AddCommand(utils.GetConfigHelpCommand())
6264

mgrctl/cmd/gpg/gpg.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package gpg
6+
7+
import (
8+
"github.com/spf13/cobra"
9+
"github.com/uyuni-project/uyuni-tools/shared/api"
10+
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
11+
"github.com/uyuni-project/uyuni-tools/shared/types"
12+
"github.com/uyuni-project/uyuni-tools/shared/utils"
13+
)
14+
15+
type apiFlags struct {
16+
api.ConnectionDetails `mapstructure:"api"`
17+
}
18+
19+
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
20+
var flags apiFlags
21+
22+
gpgCmd := &cobra.Command{
23+
Use: "gpg",
24+
Short: L("GPG key management commands"),
25+
}
26+
27+
gpgUploadKeyCmd := &cobra.Command{
28+
Use: "upload [path or URL]",
29+
Short: L("Upload a GPG key to the server"),
30+
Long: L(`Uploads an armored GPG key to the server from a local file or a remote URL.`),
31+
RunE: func(cmd *cobra.Command, args []string) error {
32+
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runGpgKeyUpload)
33+
},
34+
Args: cobra.ExactArgs(1),
35+
}
36+
37+
gpgListKeysCmd := &cobra.Command{
38+
Use: "list",
39+
Short: L("List all GPG keys on the server"),
40+
Long: L(`Retrieves a list of registered and/or previously uploaded GPG keys.`),
41+
RunE: func(cmd *cobra.Command, args []string) error {
42+
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runGpgKeyList)
43+
},
44+
}
45+
46+
gpgRemoveKeyCmd := &cobra.Command{
47+
Use: "remove [fingerprint]",
48+
Short: L("Remove a GPG key from the server"),
49+
Long: L(`Removes a key identified by its fingerprint from the server's GPG keyring.`),
50+
RunE: func(cmd *cobra.Command, args []string) error {
51+
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runGpgKeyRemove)
52+
},
53+
Args: cobra.ExactArgs(1),
54+
}
55+
56+
gpgCmd.AddCommand(gpgUploadKeyCmd)
57+
gpgCmd.AddCommand(gpgListKeysCmd)
58+
gpgCmd.AddCommand(gpgRemoveKeyCmd)
59+
api.AddAPIFlags(gpgCmd)
60+
61+
return gpgCmd
62+
}

mgrctl/cmd/gpg/list.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package gpg
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/rs/zerolog/log"
11+
"github.com/spf13/cobra"
12+
"github.com/uyuni-project/uyuni-tools/shared/api"
13+
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
14+
"github.com/uyuni-project/uyuni-tools/shared/types"
15+
"github.com/uyuni-project/uyuni-tools/shared/utils"
16+
)
17+
18+
type ListGpgKeysResponse struct {
19+
KeyType int64 `mapstructure:"keyType"`
20+
KeySize int64 `mapstructure:"keySize"`
21+
Fingerprint string `mapstructure:"fingerprint"`
22+
Names []string `mapstructure:"names"`
23+
}
24+
25+
func gpgKeyList(client *api.APIClient) error {
26+
response, err := api.GetChecked[[]ListGpgKeysResponse](
27+
client,
28+
"admin/gpg/listGpgKeys",
29+
"admin.gpg.list_gpg_keys",
30+
)
31+
32+
if err != nil {
33+
return utils.Errorf(err, L("error listing GPG keys"))
34+
}
35+
36+
if !response.Success {
37+
return fmt.Errorf(L("failed to list GPG keys: %s"), response.Message)
38+
}
39+
40+
if len(response.Result) == 0 {
41+
fmt.Println("No GPG keys stored.")
42+
}
43+
44+
for keyIdx, key := range response.Result {
45+
fmt.Printf("[%d]\tFingerprint:\t%s\n", keyIdx, key.Fingerprint)
46+
47+
typeName := "unknown"
48+
49+
switch key.KeyType {
50+
case 1:
51+
typeName = "rsa"
52+
case 16:
53+
typeName = "elgamal"
54+
case 17:
55+
typeName = "dsa"
56+
case 18:
57+
typeName = "ecdh"
58+
case 19:
59+
typeName = "ecdsa"
60+
case 22:
61+
typeName = "eddsa"
62+
case 25:
63+
typeName = "x25519"
64+
}
65+
66+
fmt.Printf("\tKey type:\t%s%d\n", typeName, key.KeySize)
67+
68+
for nameIdx, name := range key.Names {
69+
fmt.Printf("\t[%d]\t%s\n", nameIdx, name)
70+
}
71+
fmt.Printf("\n")
72+
}
73+
74+
return nil
75+
}
76+
77+
func runGpgKeyList(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, _ []string) error {
78+
log.Debug().Msgf("Requesting GPG keys from the server...")
79+
client, err := api.Init(&flags.ConnectionDetails)
80+
if err == nil {
81+
err = client.Login()
82+
}
83+
if err != nil {
84+
return utils.Errorf(err, L("unable to login to the server"))
85+
}
86+
87+
return gpgKeyList(client)
88+
}

mgrctl/cmd/gpg/remove.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package gpg
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/rs/zerolog/log"
11+
"github.com/spf13/cobra"
12+
"github.com/uyuni-project/uyuni-tools/shared/api"
13+
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
14+
"github.com/uyuni-project/uyuni-tools/shared/types"
15+
"github.com/uyuni-project/uyuni-tools/shared/utils"
16+
)
17+
18+
func gpgKeyRemove(client *api.APIClient, fingerprint string) error {
19+
response, err := api.PostChecked[float64](
20+
client,
21+
"admin/gpg/removeGpgKey",
22+
"admin.gpg.remove_gpg_key",
23+
map[string]interface{}{
24+
"fingerprint": fingerprint,
25+
},
26+
)
27+
28+
if err != nil {
29+
return utils.Errorf(err, L("error removing GPG key"))
30+
}
31+
32+
if !response.Success {
33+
return fmt.Errorf(L("failed to remove GPG key: %s"), response.Message)
34+
}
35+
36+
if int(response.Result) == 1 {
37+
fmt.Println(L("GPG key successfully removed"))
38+
} else {
39+
fmt.Println(L("unable to remove GPG key, server returned an error"))
40+
}
41+
42+
return nil
43+
}
44+
45+
func runGpgKeyRemove(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
46+
fingerprint := args[0]
47+
48+
log.Debug().Msgf("Requesting GPG key deletion from the server...")
49+
50+
client, err := api.Init(&flags.ConnectionDetails)
51+
if err == nil {
52+
err = client.Login()
53+
}
54+
if err != nil {
55+
return utils.Errorf(err, L("unable to login to the server"))
56+
}
57+
58+
return gpgKeyRemove(client, fingerprint)
59+
}

mgrctl/cmd/gpg/upload.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package gpg
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"os"
11+
"strings"
12+
13+
"github.com/rs/zerolog/log"
14+
"github.com/spf13/cobra"
15+
"github.com/uyuni-project/uyuni-tools/shared/api"
16+
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
17+
"github.com/uyuni-project/uyuni-tools/shared/types"
18+
"github.com/uyuni-project/uyuni-tools/shared/utils"
19+
)
20+
21+
const armorHeader = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
22+
23+
func gpgKeyUpload(client *api.APIClient, key string) error {
24+
response, err := api.PostChecked[float64](
25+
client,
26+
"admin/gpg/uploadGpgKey",
27+
"admin.gpg.upload_gpg_key",
28+
map[string]interface{}{
29+
"gpgKey": key,
30+
},
31+
)
32+
33+
if err != nil {
34+
return utils.Errorf(err, L("error uploading GPG key"))
35+
}
36+
37+
if !response.Success {
38+
return fmt.Errorf(L("failed to upload GPG key: %s"), response.Message)
39+
}
40+
41+
if int(response.Result) == 1 {
42+
fmt.Println(L("GPG key successfully uploaded"))
43+
} else {
44+
fmt.Println(L("unable to upload GPG key, server returned an error"))
45+
}
46+
47+
return nil
48+
}
49+
50+
func readKey(source string) (string, error) {
51+
var data []byte
52+
var err error
53+
54+
if _, err = os.Stat(source); err == nil {
55+
log.Debug().Msgf("Reading GPG key from file %s", source)
56+
data, err = os.ReadFile(source)
57+
if err != nil {
58+
return "", utils.Errorf(err, L("failed to read key file %s"), source)
59+
}
60+
} else {
61+
log.Debug().Msgf("Downloading GPG key from %s", source)
62+
data, err = utils.GetURLBody(source)
63+
if err != nil {
64+
return "", utils.Errorf(err, L("failed to download key from %s"), source)
65+
}
66+
}
67+
68+
key := string(data)
69+
// Armored GPG keys start with this header.
70+
if !strings.Contains(key, armorHeader) {
71+
return "", errors.New(L("the provided key is not an armored GPG key"))
72+
}
73+
74+
return key, nil
75+
}
76+
77+
func runGpgKeyUpload(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
78+
source := args[0]
79+
80+
key, err := readKey(source)
81+
if err != nil {
82+
return err
83+
}
84+
85+
log.Debug().Msgf("Uploading GPG key...")
86+
client, err := api.Init(&flags.ConnectionDetails)
87+
if err == nil {
88+
err = client.Login()
89+
}
90+
if err != nil {
91+
return utils.Errorf(err, L("unable to login to the server"))
92+
}
93+
94+
return gpgKeyUpload(client, key)
95+
}

0 commit comments

Comments
 (0)