-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
virtctl: add commands to dynamically set SSH keys and passwords on a VM #9818
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_library") | ||
|
||
go_library( | ||
name = "go_default_library", | ||
srcs = ["credentials.go"], | ||
importpath = "kubevirt.io/kubevirt/pkg/virtctl/credentials", | ||
visibility = ["//visibility:public"], | ||
deps = [ | ||
"//pkg/virtctl/credentials/add-key:go_default_library", | ||
"//pkg/virtctl/credentials/remove-key:go_default_library", | ||
"//pkg/virtctl/credentials/set-password:go_default_library", | ||
"//pkg/virtctl/templates:go_default_library", | ||
"//vendor/github.com/spf13/cobra:go_default_library", | ||
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") | ||
|
||
go_library( | ||
name = "go_default_library", | ||
srcs = ["add-key.go"], | ||
importpath = "kubevirt.io/kubevirt/pkg/virtctl/credentials/add-key", | ||
visibility = ["//visibility:public"], | ||
deps = [ | ||
"//pkg/apimachinery/patch:go_default_library", | ||
"//pkg/virtctl/credentials/common:go_default_library", | ||
"//pkg/virtctl/templates:go_default_library", | ||
"//staging/src/kubevirt.io/api/core/v1:go_default_library", | ||
"//staging/src/kubevirt.io/client-go/kubecli:go_default_library", | ||
"//vendor/github.com/spf13/cobra:go_default_library", | ||
"//vendor/k8s.io/api/core/v1:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library", | ||
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library", | ||
"//vendor/k8s.io/utils/pointer:go_default_library", | ||
], | ||
) | ||
|
||
go_test( | ||
name = "go_default_test", | ||
srcs = ["add-key_test.go"], | ||
deps = [ | ||
"//staging/src/kubevirt.io/api/core:go_default_library", | ||
"//staging/src/kubevirt.io/api/core/v1:go_default_library", | ||
"//staging/src/kubevirt.io/client-go/api:go_default_library", | ||
"//staging/src/kubevirt.io/client-go/kubecli:go_default_library", | ||
"//staging/src/kubevirt.io/client-go/testutils:go_default_library", | ||
"//tests/clientcmd:go_default_library", | ||
"//vendor/github.com/evanphx/json-patch:go_default_library", | ||
"//vendor/github.com/golang/mock/gomock:go_default_library", | ||
"//vendor/github.com/onsi/ginkgo/v2:go_default_library", | ||
"//vendor/github.com/onsi/gomega:go_default_library", | ||
"//vendor/k8s.io/api/core/v1:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library", | ||
"//vendor/k8s.io/apimachinery/pkg/util/rand:go_default_library", | ||
"//vendor/k8s.io/client-go/kubernetes:go_default_library", | ||
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library", | ||
"//vendor/k8s.io/client-go/testing:go_default_library", | ||
"//vendor/k8s.io/utils/pointer:go_default_library", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
package add_key | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
core "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/types" | ||
"k8s.io/client-go/tools/clientcmd" | ||
"k8s.io/utils/pointer" | ||
v1 "kubevirt.io/api/core/v1" | ||
"kubevirt.io/client-go/kubecli" | ||
|
||
"kubevirt.io/kubevirt/pkg/apimachinery/patch" | ||
"kubevirt.io/kubevirt/pkg/virtctl/credentials/common" | ||
"kubevirt.io/kubevirt/pkg/virtctl/templates" | ||
) | ||
|
||
func NewCommand(clientConfig clientcmd.ClientConfig) *cobra.Command { | ||
cmdFlags := &addSshKeyFlags{} | ||
cmd := &cobra.Command{ | ||
Use: "add-ssh-key", | ||
Short: "Add credentials to a virtual machine.", | ||
Args: templates.ExactArgs("add-ssh-key", 1), | ||
Example: exampleUsage, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
return runAddKeyCommand(clientConfig, cmdFlags, cmd, args) | ||
}, | ||
} | ||
cmdFlags.AddToCommand(cmd) | ||
|
||
cmd.SetUsageTemplate(templates.UsageTemplate()) | ||
return cmd | ||
} | ||
|
||
const exampleUsage = ` # Add an SSH key for a running virtual machine. | ||
{{ProgramName}} credentials add-ssh-key --user <username> --file <path-to-ssh-public-key> <vm-name> | ||
|
||
# Add an SSH key for a running virtual machine. Key is provided as literal parameter. | ||
{{ProgramName}} credentials add-ssh-key --user <username> --value <literal-ssh-public-key> <vm-name> | ||
|
||
# Add an SSH key to a secret that is not owned by the virtual machine. | ||
{{ProgramName}} credentials add-ssh-key --user <username> --file <path-to-ssh-public-key> --force <vm-name> | ||
|
||
# Create a new secret with the SSH key, and assign it to the specified virtual machine. | ||
{{ProgramName}} credentials add-ssh-key --create-secret --user <username> --file <path-to-ssh-public-key> <vm-name> | ||
|
||
# Create a new secret with the SSH key, and assign it to a running VM. It will take effect after restart. | ||
{{ProgramName}} credentials add-ssh-key --create-secret --user <username> --file <path-to-ssh-public-key> --force <vm-name> | ||
` | ||
|
||
type addSshKeyFlags struct { | ||
common.SshCommandFlags | ||
|
||
CreateSecret bool | ||
UpdateSecret bool | ||
|
||
Force bool | ||
} | ||
|
||
func (a *addSshKeyFlags) AddToCommand(cmd *cobra.Command) { | ||
a.SshCommandFlags.AddToCommand(cmd) | ||
|
||
const ( | ||
createSecretFlag = "create-secret" | ||
updateSecretFlag = "update-secret" | ||
) | ||
|
||
cmd.Flags().BoolVar(&a.CreateSecret, createSecretFlag, false, "Create a new secret for the SSH key. The new key will not be added to a running VM. Use --force to add a new secret even if the VM is running.") | ||
cmd.Flags().BoolVar(&a.UpdateSecret, updateSecretFlag, false, "Add the SSH key to an existing secret. Use --force option, if the secret does not have owner reference pointing to the VM.") | ||
cmd.MarkFlagsMutuallyExclusive(createSecretFlag, updateSecretFlag) | ||
|
||
cmd.Flags().BoolVar(&a.Force, "force", false, "Force update of secret, even if it's not owned by the VM.") | ||
} | ||
|
||
func runAddKeyCommand(clientConfig clientcmd.ClientConfig, cmdFlags *addSshKeyFlags, cmd *cobra.Command, args []string) error { | ||
vmName := args[0] | ||
vmNamespace, _, err := clientConfig.Namespace() | ||
if err != nil { | ||
return fmt.Errorf("error getting namespace: %w", err) | ||
} | ||
|
||
// Reading the key before accessing cluster | ||
sshKey, err := common.GetSshKey(&cmdFlags.SshCommandFlags) | ||
if err != nil { | ||
return fmt.Errorf("error getting ssh key: %w", err) | ||
} | ||
|
||
cli, err := kubecli.GetKubevirtClientFromClientConfig(clientConfig) | ||
if err != nil { | ||
return fmt.Errorf("error getting kubevirt client: %w", err) | ||
} | ||
|
||
vm, err := cli.VirtualMachine(vmNamespace).Get(cmd.Context(), vmName, &metav1.GetOptions{}) | ||
if err != nil { | ||
return fmt.Errorf("error getting virtual machine: %w", err) | ||
} | ||
|
||
if shouldCreateNewSecret(cmdFlags, vm) { | ||
return addSecretWithSshKey(cmd, cli, cmdFlags, vm, sshKey) | ||
} | ||
return updateSecretWithSshKey(cmd, cli, cmdFlags, vm, sshKey) | ||
} | ||
|
||
func addSecretWithSshKey(cmd *cobra.Command, cli kubecli.KubevirtClient, cmdFlags *addSshKeyFlags, vm *v1.VirtualMachine, sshKey string) (err error) { | ||
if !cmdFlags.Force { | ||
// Only create a secret if VM is not running. | ||
_, err := cli.VirtualMachineInstance(vm.Namespace).Get(cmd.Context(), vm.Name, &metav1.GetOptions{}) | ||
if err == nil { | ||
return fmt.Errorf("virtual machine %s is running. Use --force flag to update a running VM, it will take effect after restart", vm.Name) | ||
} | ||
if !errors.IsNotFound(err) { | ||
return fmt.Errorf("error when getting virtual machine instance: %w", err) | ||
} | ||
} | ||
|
||
secret := newSecretWithKey(vm, sshKey) | ||
secret, err = cli.CoreV1().Secrets(vm.Namespace).Create(cmd.Context(), secret, metav1.CreateOptions{}) | ||
if err != nil { | ||
return fmt.Errorf("error creating secret: %w", err) | ||
} | ||
|
||
accessCredential := newAccessCredential(secret.Name, cmdFlags.User) | ||
accessCredentialPatch := patchToAddAccessCredential(accessCredential) | ||
|
||
// First, Try to add the new access credential to the existing array. | ||
_, err = cli.VirtualMachine(vm.Namespace).Patch(cmd.Context(), vm.Name, types.JSONPatchType, common.MustMarshalPatch(accessCredentialPatch), &metav1.PatchOptions{}) | ||
if err != nil { | ||
// If it fails, it probably means that the array is nil. Try to add the array. | ||
fullPatch := common.MustMarshalPatch(append(patchToAddAccessCredentialsArray(), accessCredentialPatch)...) | ||
_, err = cli.VirtualMachine(vm.Namespace).Patch(cmd.Context(), vm.Name, types.JSONPatchType, fullPatch, &metav1.PatchOptions{}) | ||
if err != nil { | ||
return fmt.Errorf("error patching virtual machine: %w", err) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func updateSecretWithSshKey(cmd *cobra.Command, cli kubecli.KubevirtClient, cmdFlags *addSshKeyFlags, vm *v1.VirtualMachine, sshKey string) error { | ||
secrets := common.GetSshSecretsForUser(vm.Spec.Template.Spec.AccessCredentials, cmdFlags.User) | ||
if len(secrets) == 0 { | ||
return fmt.Errorf("no secrets specified for user: %s", cmdFlags.User) | ||
} | ||
|
||
secretName, err := common.FindSecretOrGetFirst(cmdFlags.Secret, secrets) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
secret, err := cli.CoreV1().Secrets(vm.Namespace).Get(cmd.Context(), secretName, metav1.GetOptions{}) | ||
if err != nil { | ||
return fmt.Errorf("error getting secret \"%s\": %w", secretName, err) | ||
} | ||
|
||
if secretContainsKey(secret.Data, sshKey) { | ||
cmd.Printf("Secret \"%s\" already contains this SSH key.", secretName) | ||
return nil | ||
} | ||
|
||
if !cmdFlags.Force { | ||
// Check if secret is owned by the VM. This is useful to not accidentally update a secret that is used by multiple VMs. | ||
if !common.IsOwnedByVm(secret, vm) { | ||
return fmt.Errorf("secret %s does not have an owner reference pointing to VM %s", secretName, vm.Name) | ||
} | ||
} | ||
|
||
addKeyPatch := common.AddKeyToSecretPatchOp(common.RandomWithPrefix("ssh-key-"), []byte(sshKey)) | ||
|
||
// First, try patch to add a new key | ||
_, err = cli.CoreV1().Secrets(vm.Namespace).Patch(cmd.Context(), secretName, types.JSONPatchType, common.MustMarshalPatch(addKeyPatch), metav1.PatchOptions{}) | ||
if err != nil { | ||
// If it fails, the /data may be nil. Try a patch that adds the /data field | ||
fullPatch := common.MustMarshalPatch(append(common.AddDataFieldToSecretPatchOp(), addKeyPatch)...) | ||
_, err = cli.CoreV1().Secrets(vm.Namespace).Patch(cmd.Context(), secretName, types.JSONPatchType, fullPatch, metav1.PatchOptions{}) | ||
if err != nil { | ||
return fmt.Errorf("error patching secret \"%s\": %w", secretName, err) | ||
} | ||
} | ||
|
||
cmd.Printf("Successfully added the key to secret \"%s\"", secretName) | ||
return nil | ||
} | ||
|
||
func shouldCreateNewSecret(flags *addSshKeyFlags, vm *v1.VirtualMachine) bool { | ||
if flags.CreateSecret { | ||
return true | ||
} | ||
if flags.UpdateSecret { | ||
return false | ||
} | ||
|
||
// Default behavior: Create a new secret, if no secret is defined for a user | ||
secrets := common.GetSshSecretsForUser(vm.Spec.Template.Spec.AccessCredentials, flags.User) | ||
return len(secrets) == 0 | ||
} | ||
|
||
func newSecretWithKey(vm *v1.VirtualMachine, sshKey string) *core.Secret { | ||
return &core.Secret{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
GenerateName: vm.Name + "-ssh-key-", | ||
Namespace: vm.Namespace, | ||
OwnerReferences: []metav1.OwnerReference{{ | ||
APIVersion: v1.VirtualMachineGroupVersionKind.GroupVersion().String(), | ||
Kind: v1.VirtualMachineGroupVersionKind.Kind, | ||
Name: vm.Name, | ||
UID: vm.UID, | ||
Controller: pointer.Bool(true), | ||
}}, | ||
}, | ||
Data: map[string][]byte{ | ||
common.RandomWithPrefix("ssh-key-"): []byte(sshKey), | ||
}, | ||
} | ||
} | ||
|
||
func secretContainsKey(secretData map[string][]byte, key string) bool { | ||
for _, data := range secretData { | ||
lines := strings.Split(string(data), "\n") | ||
for _, line := range lines { | ||
if common.LineContainsKey(line, key) { | ||
return true | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func newAccessCredential(secretName string, user string) *v1.AccessCredential { | ||
return &v1.AccessCredential{ | ||
SSHPublicKey: &v1.SSHPublicKeyAccessCredential{ | ||
Source: v1.SSHPublicKeyAccessCredentialSource{ | ||
Secret: &v1.AccessCredentialSecretSource{ | ||
SecretName: secretName, | ||
}, | ||
}, | ||
PropagationMethod: v1.SSHPublicKeyAccessCredentialPropagationMethod{ | ||
QemuGuestAgent: &v1.QemuGuestAgentSSHPublicKeyAccessCredentialPropagation{ | ||
Users: []string{user}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func patchToAddAccessCredentialsArray() []patch.PatchOperation { | ||
return []patch.PatchOperation{{ | ||
Op: patch.PatchTestOp, | ||
Path: "/spec/template/spec/accessCredentials", | ||
Value: nil, | ||
}, { | ||
Op: patch.PatchAddOp, | ||
Path: "/spec/template/spec/accessCredentials", | ||
Value: []v1.AccessCredential{}, | ||
}} | ||
} | ||
|
||
func patchToAddAccessCredential(credential *v1.AccessCredential) patch.PatchOperation { | ||
return patch.PatchOperation{ | ||
Op: patch.PatchAddOp, | ||
Path: "/spec/template/spec/accessCredentials/-", | ||
Value: credential, | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:any chances to check the actual error? Maybe with Contains?