Skip to content
Draft
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
3 changes: 2 additions & 1 deletion mgradm/cmd/gpg/add/gpg.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 SUSE LLC
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

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

gpgAddKeyCmd.Flags().BoolP("force", "f", false, L("Import without asking confirmation"))
Expand Down
11 changes: 6 additions & 5 deletions mgradm/cmd/gpg/gpg.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 SUSE LLC
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

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

gpgKeyCmd.AddCommand(gpgadd.NewCommand(globalFlags))
Expand Down
3 changes: 2 additions & 1 deletion mgradm/cmd/gpg/list/gpg.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 SUSE LLC
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

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

gpgListKeyCmd.Flags().BoolP("system", "s", false, L("List keys from system keyring"))
Expand Down
2 changes: 2 additions & 0 deletions mgrctl/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/api"
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/cp"
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/gpg"
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/proxy"
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/ssh"
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/term"
Expand Down Expand Up @@ -57,6 +58,7 @@ func NewUyunictlCommand() *cobra.Command {
rootCmd.AddCommand(completion.NewCommand(globalFlags))
rootCmd.AddCommand(proxy.NewCommand(globalFlags))
rootCmd.AddCommand(ssh.NewCommand(globalFlags))
rootCmd.AddCommand(gpg.NewCommand(globalFlags))

rootCmd.AddCommand(utils.GetConfigHelpCommand())

Expand Down
62 changes: 62 additions & 0 deletions mgrctl/cmd/gpg/gpg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package gpg

import (
"github.com/spf13/cobra"
"github.com/uyuni-project/uyuni-tools/shared/api"
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
"github.com/uyuni-project/uyuni-tools/shared/types"
"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type apiFlags struct {
api.ConnectionDetails `mapstructure:"api"`
}

func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
var flags apiFlags

gpgCmd := &cobra.Command{
Use: "gpg",
Short: L("GPG key management commands"),
}

gpgUploadKeyCmd := &cobra.Command{
Use: "upload [path or URL]",
Short: L("Upload a GPG key to the server"),
Long: L(`Uploads an armored GPG key to the server from a local file or a remote URL.`),
RunE: func(cmd *cobra.Command, args []string) error {
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runGpgKeyUpload)
},
Args: cobra.ExactArgs(1),
}

gpgListKeysCmd := &cobra.Command{
Use: "list",
Short: L("List all GPG keys on the server"),
Long: L(`Retrieves a list of registered and/or previously uploaded GPG keys.`),
RunE: func(cmd *cobra.Command, args []string) error {
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runGpgKeyList)
},
}

gpgRemoveKeyCmd := &cobra.Command{
Use: "remove [fingerprint]",
Short: L("Remove a GPG key from the server"),
Long: L(`Removes a key identified by its fingerprint from the server's GPG keyring.`),
RunE: func(cmd *cobra.Command, args []string) error {
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runGpgKeyRemove)
},
Args: cobra.ExactArgs(1),
}

gpgCmd.AddCommand(gpgUploadKeyCmd)
gpgCmd.AddCommand(gpgListKeysCmd)
gpgCmd.AddCommand(gpgRemoveKeyCmd)
api.AddAPIFlags(gpgCmd)

return gpgCmd
}
88 changes: 88 additions & 0 deletions mgrctl/cmd/gpg/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package gpg

import (
"fmt"

"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/uyuni-project/uyuni-tools/shared/api"
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
"github.com/uyuni-project/uyuni-tools/shared/types"
"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type ListGpgKeysResponse struct {
KeyType int64 `mapstructure:"keyType"`
KeySize int64 `mapstructure:"keySize"`
Fingerprint string `mapstructure:"fingerprint"`
Names []string `mapstructure:"names"`
}

func gpgKeyList(client *api.APIClient) error {
response, err := api.GetChecked[[]ListGpgKeysResponse](
client,
"admin/gpg/listGpgKeys",
"admin.gpg.list_gpg_keys",
)

if err != nil {
return utils.Errorf(err, L("error listing GPG keys"))
}

if !response.Success {
return fmt.Errorf(L("failed to list GPG keys: %s"), response.Message)
}

if len(response.Result) == 0 {
fmt.Println("No GPG keys stored.")
}

for keyIdx, key := range response.Result {
fmt.Printf("[%d]\tFingerprint:\t%s\n", keyIdx, key.Fingerprint)

typeName := "unknown"

switch key.KeyType {
case 1:
typeName = "rsa"
case 16:
typeName = "elgamal"
case 17:
typeName = "dsa"
case 18:
typeName = "ecdh"
case 19:
typeName = "ecdsa"
case 22:
typeName = "eddsa"
case 25:
typeName = "x25519"
}

fmt.Printf("\tKey type:\t%s%d\n", typeName, key.KeySize)

for nameIdx, name := range key.Names {
fmt.Printf("\t[%d]\t%s\n", nameIdx, name)
}
fmt.Printf("\n")
}

return nil
}

func runGpgKeyList(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, _ []string) error {
log.Debug().Msgf("Requesting GPG keys from the server...")
client, err := api.Init(&flags.ConnectionDetails)
if err == nil {
err = client.Login()
}
if err != nil {
return utils.Errorf(err, L("unable to login to the server"))
}

return gpgKeyList(client)
}
59 changes: 59 additions & 0 deletions mgrctl/cmd/gpg/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package gpg

import (
"fmt"

"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/uyuni-project/uyuni-tools/shared/api"
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
"github.com/uyuni-project/uyuni-tools/shared/types"
"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func gpgKeyRemove(client *api.APIClient, fingerprint string) error {
response, err := api.PostChecked[float64](
client,
"admin/gpg/removeGpgKey",
"admin.gpg.remove_gpg_key",
map[string]interface{}{
"fingerprint": fingerprint,
},
)

if err != nil {
return utils.Errorf(err, L("error removing GPG key"))
}

if !response.Success {
return fmt.Errorf(L("failed to remove GPG key: %s"), response.Message)
}

if int(response.Result) == 1 {
fmt.Println(L("GPG key successfully removed"))
} else {
fmt.Println(L("unable to remove GPG key, server returned an error"))
}

return nil
}

func runGpgKeyRemove(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
fingerprint := args[0]

log.Debug().Msgf("Requesting GPG key deletion from the server...")

client, err := api.Init(&flags.ConnectionDetails)
if err == nil {
err = client.Login()
}
if err != nil {
return utils.Errorf(err, L("unable to login to the server"))
}

return gpgKeyRemove(client, fingerprint)
}
95 changes: 95 additions & 0 deletions mgrctl/cmd/gpg/upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2026 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package gpg

import (
"errors"
"fmt"
"os"
"strings"

"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/uyuni-project/uyuni-tools/shared/api"
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
"github.com/uyuni-project/uyuni-tools/shared/types"
"github.com/uyuni-project/uyuni-tools/shared/utils"
)

const armorHeader = "-----BEGIN PGP PUBLIC KEY BLOCK-----"

func gpgKeyUpload(client *api.APIClient, key string) error {
response, err := api.PostChecked[float64](
client,
"admin/gpg/uploadGpgKey",
"admin.gpg.upload_gpg_key",
map[string]interface{}{
"gpgKey": key,
},
)

if err != nil {
return utils.Errorf(err, L("error uploading GPG key"))
}

if !response.Success {
return fmt.Errorf(L("failed to upload GPG key: %s"), response.Message)
}

if int(response.Result) == 1 {
fmt.Println(L("GPG key successfully uploaded"))
} else {
fmt.Println(L("unable to upload GPG key, server returned an error"))
}

return nil
}

func readKey(source string) (string, error) {
var data []byte
var err error

if _, err = os.Stat(source); err == nil {
log.Debug().Msgf("Reading GPG key from file %s", source)
data, err = os.ReadFile(source)
if err != nil {
return "", utils.Errorf(err, L("failed to read key file %s"), source)
}
} else {
log.Debug().Msgf("Downloading GPG key from %s", source)
data, err = utils.GetURLBody(source)
if err != nil {
return "", utils.Errorf(err, L("failed to download key from %s"), source)
}
}

key := string(data)
// Armored GPG keys start with this header.
if !strings.Contains(key, armorHeader) {
return "", errors.New(L("the provided key is not an armored GPG key"))
}

return key, nil
}

func runGpgKeyUpload(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
source := args[0]

key, err := readKey(source)
if err != nil {
return err
}

log.Debug().Msgf("Uploading GPG key...")
client, err := api.Init(&flags.ConnectionDetails)
if err == nil {
err = client.Login()
}
if err != nil {
return utils.Errorf(err, L("unable to login to the server"))
}

return gpgKeyUpload(client, key)
}
Loading
Loading