Skip to content

Commit e0b6bdc

Browse files
committed
Add mgrctl commands for GPG key management
1 parent 5bd1e5c commit e0b6bdc

10 files changed

Lines changed: 396 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: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.Get[[]ListGpgKeysResponse](client, "admin/gpg/listGpgKeys")
27+
if err != nil {
28+
return utils.Errorf(err, L("error uploading GPG key"))
29+
}
30+
31+
if !response.Success {
32+
return fmt.Errorf(L("failed to upload GPG key: %s"), response.Message)
33+
}
34+
35+
if len(response.Result) == 0 {
36+
fmt.Println("No GPG keys stored.")
37+
}
38+
39+
for keyIdx, key := range response.Result {
40+
fmt.Printf("[%d]\tFingerprint:\t%s\n", keyIdx, key.Fingerprint)
41+
42+
typeName := "unknown"
43+
44+
switch key.KeyType {
45+
case 1:
46+
typeName = "rsa"
47+
case 16:
48+
typeName = "elgamal"
49+
case 17:
50+
typeName = "dsa"
51+
case 18:
52+
typeName = "ecdh"
53+
case 19:
54+
typeName = "ecdsa"
55+
case 22:
56+
typeName = "eddsa"
57+
case 25:
58+
typeName = "x25519"
59+
}
60+
61+
fmt.Printf("\tKey type:\t%s%d\n", typeName, key.KeySize)
62+
63+
for nameIdx, name := range key.Names {
64+
fmt.Printf("\t[%d]\t%s\n", nameIdx, name)
65+
}
66+
fmt.Printf("\n")
67+
}
68+
69+
return nil
70+
}
71+
72+
func runGpgKeyList(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, _ []string) error {
73+
log.Debug().Msgf("Requesting GPG keys from the server...")
74+
client, err := api.Init(&flags.ConnectionDetails)
75+
if err == nil {
76+
err = client.Login()
77+
}
78+
if err != nil {
79+
return utils.Errorf(err, L("unable to login to the server"))
80+
}
81+
82+
return gpgKeyList(client)
83+
}

mgrctl/cmd/gpg/remove.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.Post[float64](client, "admin/gpg/removeGpgKey", map[string]interface{}{
20+
"fingerprint": fingerprint,
21+
})
22+
23+
if err != nil {
24+
return utils.Errorf(err, L("error removing GPG key"))
25+
}
26+
27+
if !response.Success {
28+
return fmt.Errorf(L("failed to remove GPG key: %s"), response.Message)
29+
}
30+
31+
if int(response.Result) == 1 {
32+
fmt.Println(L("GPG key successfully removed"))
33+
} else {
34+
fmt.Println(L("unable to remove GPG key, server returned an error"))
35+
}
36+
37+
return nil
38+
}
39+
40+
func runGpgKeyRemove(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
41+
fingerprint := args[0]
42+
43+
log.Debug().Msgf("Requesting GPG key deletion from the server...")
44+
45+
client, err := api.Init(&flags.ConnectionDetails)
46+
if err == nil {
47+
err = client.Login()
48+
}
49+
if err != nil {
50+
return utils.Errorf(err, L("unable to login to the server"))
51+
}
52+
53+
return gpgKeyRemove(client, fingerprint)
54+
}

mgrctl/cmd/gpg/upload.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.Post[float64](client, "admin/gpg/uploadGpgKey", map[string]interface{}{
25+
"gpgKey": key,
26+
})
27+
if err != nil {
28+
return utils.Errorf(err, L("error uploading GPG key"))
29+
}
30+
31+
if !response.Success {
32+
return fmt.Errorf(L("failed to upload GPG key: %s"), response.Message)
33+
}
34+
35+
if int(response.Result) == 1 {
36+
fmt.Println(L("GPG key successfully uploaded"))
37+
} else {
38+
fmt.Println(L("unable to upload GPG key, server returned an error"))
39+
}
40+
41+
return nil
42+
}
43+
44+
func readKey(source string) (string, error) {
45+
var data []byte
46+
var err error
47+
48+
if _, err = os.Stat(source); err == nil {
49+
log.Debug().Msgf("Reading GPG key from file %s", source)
50+
data, err = os.ReadFile(source)
51+
if err != nil {
52+
return "", utils.Errorf(err, L("failed to read key file %s"), source)
53+
}
54+
} else {
55+
log.Debug().Msgf("Downloading GPG key from %s", source)
56+
data, err = utils.GetURLBody(source)
57+
if err != nil {
58+
return "", utils.Errorf(err, L("failed to download key from %s"), source)
59+
}
60+
}
61+
62+
key := string(data)
63+
// Armored GPG keys start with this header.
64+
if !strings.Contains(key, armorHeader) {
65+
return "", errors.New(L("the provided key is not an armored GPG key"))
66+
}
67+
68+
return key, nil
69+
}
70+
71+
func runGpgKeyUpload(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
72+
source := args[0]
73+
74+
key, err := readKey(source)
75+
if err != nil {
76+
return err
77+
}
78+
79+
log.Debug().Msgf("Uploading GPG key...")
80+
client, err := api.Init(&flags.ConnectionDetails)
81+
if err == nil {
82+
err = client.Login()
83+
}
84+
if err != nil {
85+
return utils.Errorf(err, L("unable to login to the server"))
86+
}
87+
88+
return gpgKeyUpload(client, key)
89+
}

0 commit comments

Comments
 (0)