From 5423e18b56aa86fad475dbf202b938ccb1edabe8 Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Mon, 21 Jun 2021 20:58:51 -0700 Subject: [PATCH 1/2] Add 'kops promote keypair' command --- cmd/kops/BUILD.bazel | 2 + cmd/kops/promote.go | 52 +++++++++++ cmd/kops/promote_keypair.go | 156 +++++++++++++++++++++++++++++++ cmd/kops/root.go | 1 + docs/cli/kops.md | 1 + docs/cli/kops_promote.md | 50 ++++++++++ docs/cli/kops_promote_keypair.md | 58 ++++++++++++ 7 files changed, 320 insertions(+) create mode 100644 cmd/kops/promote.go create mode 100644 cmd/kops/promote_keypair.go create mode 100644 docs/cli/kops_promote.md create mode 100644 docs/cli/kops_promote_keypair.md diff --git a/cmd/kops/BUILD.bazel b/cmd/kops/BUILD.bazel index f730cdc3a5..4a6fb998a8 100644 --- a/cmd/kops/BUILD.bazel +++ b/cmd/kops/BUILD.bazel @@ -37,6 +37,8 @@ go_library( "get_keypairs.go", "get_secrets.go", "main.go", + "promote.go", + "promote_keypair.go", "replace.go", "rollingupdate.go", "rollingupdatecluster.go", diff --git a/cmd/kops/promote.go b/cmd/kops/promote.go new file mode 100644 index 0000000000..eae5494ef1 --- /dev/null +++ b/cmd/kops/promote.go @@ -0,0 +1,52 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + + "github.com/spf13/cobra" + "k8s.io/kops/cmd/kops/util" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + promoteLong = templates.LongDesc(i18n.T(` + Promote a resource.`)) + + promoteExample = templates.Examples(i18n.T(` + # Promote the newest ca keypair to be the primary. + kops promote keypair ca +`)) + + promoteShort = i18n.T(`Promote a resource.`) +) + +func NewCmdPromote(f *util.Factory, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "promote", + Short: promoteShort, + Long: promoteLong, + Example: promoteExample, + } + + // create subcommands + cmd.AddCommand(NewCmdPromoteKeypair(f, out)) + + return cmd +} diff --git a/cmd/kops/promote_keypair.go b/cmd/kops/promote_keypair.go new file mode 100644 index 0000000000..2cbe4a714e --- /dev/null +++ b/cmd/kops/promote_keypair.go @@ -0,0 +1,156 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "io" + "math/big" + + "github.com/spf13/cobra" + "k8s.io/klog/v2" + "k8s.io/kops/cmd/kops/util" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + promoteKeypairLong = templates.LongDesc(i18n.T(` + Promote a keypair to be the primary, used for signing. + `)) + + promoteKeypairExample = templates.Examples(i18n.T(` + # Promote the newest ca keypair to be the primary. + kops promote keypair ca \ + --name k8s-cluster.example.com --state s3://my-state-store + + # Promote a specific service-account keypair to be the primary. + kops promote keypair service-account 5938372002934847 \ + --name k8s-cluster.example.com --state s3://my-state-store + `)) + + promoteKeypairShort = i18n.T(`Promote a keypair to be the primary, used for signing.`) +) + +type PromoteKeypairOptions struct { + ClusterName string + Keyset string + KeypairID string +} + +// NewCmdPromoteKeypair returns a promote keypair command. +func NewCmdPromoteKeypair(f *util.Factory, out io.Writer) *cobra.Command { + options := &PromoteKeypairOptions{} + + cmd := &cobra.Command{ + Use: "keypair KEYSET [ID]", + Short: promoteKeypairShort, + Long: promoteKeypairLong, + Example: promoteKeypairExample, + Run: func(cmd *cobra.Command, args []string) { + ctx := context.TODO() + + options.ClusterName = rootCommand.ClusterName() + + if options.ClusterName == "" { + exitWithError(fmt.Errorf("--name is required")) + return + } + + if len(args) == 0 { + exitWithError(fmt.Errorf("must specify name of keyset promote keypair in")) + } + if len(args) > 2 { + exitWithError(fmt.Errorf("can only promote to one keyset at a time")) + } + options.Keyset = args[0] + if len(args) > 1 { + options.KeypairID = args[1] + } + + err := RunPromoteKeypair(ctx, f, out, options) + if err != nil { + exitWithError(err) + } + }, + } + + return cmd +} + +// RunPromoteKeypair promotes a keypair. +func RunPromoteKeypair(ctx context.Context, f *util.Factory, out io.Writer, options *PromoteKeypairOptions) error { + if keysetCommonNames[options.Keyset] == "" { + return fmt.Errorf("promoting keypairs for %q is not supported", options.Keyset) + } + + cluster, err := GetCluster(ctx, f, options.ClusterName) + if err != nil { + return fmt.Errorf("error getting cluster: %q: %v", options.ClusterName, err) + } + + clientSet, err := f.Clientset() + if err != nil { + return fmt.Errorf("error getting clientset: %v", err) + } + + keyStore, err := clientSet.KeyStore(cluster) + if err != nil { + return fmt.Errorf("error getting keystore: %v", err) + } + + keyset, err := keyStore.FindKeyset(options.Keyset) + if err != nil { + return fmt.Errorf("reading keyset: %v", err) + } else if keyset == nil { + return fmt.Errorf("keyset not found") + } + + keypairID := options.KeypairID + if keypairID == "" { + highestId := big.NewInt(0) + for id, item := range keyset.Items { + if item.PrivateKey != nil { + itemId, ok := big.NewInt(0).SetString(id, 10) + if ok && highestId.Cmp(itemId) < 0 { + highestId = itemId + } + } + } + + keypairID = highestId.String() + if keypairID == keyset.Primary.Id { + return fmt.Errorf("no keypair newer than current primary %s", keypairID) + } + } else if item := keyset.Items[keypairID]; item != nil { + if item.PrivateKey == nil { + return fmt.Errorf("keypair has no private key") + } + } else { + return fmt.Errorf("keypair not found") + } + + keyset.Primary = keyset.Items[keypairID] + err = keyStore.StoreKeyset(options.Keyset, keyset) + if err != nil { + return fmt.Errorf("error writing keyset: %v", err) + } + + klog.Infof("promoted keypair %s", keypairID) + return nil +} diff --git a/cmd/kops/root.go b/cmd/kops/root.go index 8f7e50d2de..535391063c 100644 --- a/cmd/kops/root.go +++ b/cmd/kops/root.go @@ -145,6 +145,7 @@ func NewCmdRoot(f *util.Factory, out io.Writer) *cobra.Command { cmd.AddCommand(NewCmdExport(f, out)) cmd.AddCommand(NewCmdGet(f, out)) cmd.AddCommand(commands.NewCmdHelpers(f, out)) + cmd.AddCommand(NewCmdPromote(f, out)) cmd.AddCommand(NewCmdUpdate(f, out)) cmd.AddCommand(NewCmdReplace(f, out)) cmd.AddCommand(NewCmdRollingUpdate(f, out)) diff --git a/docs/cli/kops.md b/docs/cli/kops.md index 787a51bb8e..59d58408b2 100644 --- a/docs/cli/kops.md +++ b/docs/cli/kops.md @@ -44,6 +44,7 @@ kOps is Kubernetes Operations. * [kops edit](kops_edit.md) - Edit clusters and other resources. * [kops export](kops_export.md) - Export configuration. * [kops get](kops_get.md) - Get one or many resources. +* [kops promote](kops_promote.md) - Promote a resource. * [kops replace](kops_replace.md) - Replace cluster resources. * [kops rolling-update](kops_rolling-update.md) - Rolling update a cluster. * [kops set](kops_set.md) - Set fields on clusters and other resources. diff --git a/docs/cli/kops_promote.md b/docs/cli/kops_promote.md new file mode 100644 index 0000000000..f99c8e15b5 --- /dev/null +++ b/docs/cli/kops_promote.md @@ -0,0 +1,50 @@ + + + +## kops promote + +Promote a resource. + +### Synopsis + +Promote a resource. + +### Examples + +``` + # Promote the newest ca keypair to be the primary. + kops promote keypair ca +``` + +### Options + +``` + -h, --help help for promote +``` + +### Options inherited from parent commands + +``` + --add_dir_header If true, adds the file directory to the header of the log messages + --alsologtostderr log to standard error as well as files + --config string yaml config file (default is $HOME/.kops.yaml) + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory + --log_file string If non-empty, use this log file + --log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) + --logtostderr log to standard error instead of files (default true) + --name string Name of cluster. Overrides KOPS_CLUSTER_NAME environment variable + --one_output If true, only write logs to their native severity level (vs also writing to each lower severity level) + --skip_headers If true, avoid header prefixes in the log messages + --skip_log_headers If true, avoid headers when opening log files + --state string Location of state storage (kops 'config' file). Overrides KOPS_STATE_STORE environment variable + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + -v, --v Level number for the log level verbosity + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO + +* [kops](kops.md) - kOps is Kubernetes Operations. +* [kops promote keypair](kops_promote_keypair.md) - Promote a keypair to be the primary, used for signing. + diff --git a/docs/cli/kops_promote_keypair.md b/docs/cli/kops_promote_keypair.md new file mode 100644 index 0000000000..b5a3adf73f --- /dev/null +++ b/docs/cli/kops_promote_keypair.md @@ -0,0 +1,58 @@ + + + +## kops promote keypair + +Promote a keypair to be the primary, used for signing. + +### Synopsis + +Promote a keypair to be the primary, used for signing. + +``` +kops promote keypair KEYSET [ID] [flags] +``` + +### Examples + +``` + # Promote the newest ca keypair to be the primary. + kops promote keypair ca \ + --name k8s-cluster.example.com --state s3://my-state-store + + # Promote a specific service-account keypair to be the primary. + kops promote keypair service-account 5938372002934847 \ + --name k8s-cluster.example.com --state s3://my-state-store +``` + +### Options + +``` + -h, --help help for keypair +``` + +### Options inherited from parent commands + +``` + --add_dir_header If true, adds the file directory to the header of the log messages + --alsologtostderr log to standard error as well as files + --config string yaml config file (default is $HOME/.kops.yaml) + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory + --log_file string If non-empty, use this log file + --log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) + --logtostderr log to standard error instead of files (default true) + --name string Name of cluster. Overrides KOPS_CLUSTER_NAME environment variable + --one_output If true, only write logs to their native severity level (vs also writing to each lower severity level) + --skip_headers If true, avoid header prefixes in the log messages + --skip_log_headers If true, avoid headers when opening log files + --state string Location of state storage (kops 'config' file). Overrides KOPS_STATE_STORE environment variable + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + -v, --v Level number for the log level verbosity + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO + +* [kops promote](kops_promote.md) - Promote a resource. + From 366210d18973a1f9a726cb80339668086d231cf3 Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Mon, 21 Jun 2021 21:37:21 -0700 Subject: [PATCH 2/2] Remove dead code --- nodeup/pkg/model/fakes_test.go | 4 -- pkg/configserver/BUILD.bazel | 1 - pkg/configserver/keystore.go | 6 -- upup/pkg/fi/ca.go | 3 - upup/pkg/fi/clientset_castore.go | 53 ---------------- upup/pkg/fi/vfs_castore.go | 106 ------------------------------- 6 files changed, 173 deletions(-) diff --git a/nodeup/pkg/model/fakes_test.go b/nodeup/pkg/model/fakes_test.go index cb4461db2c..402e532a92 100644 --- a/nodeup/pkg/model/fakes_test.go +++ b/nodeup/pkg/model/fakes_test.go @@ -112,7 +112,3 @@ func (k fakeCAStore) FindCert(name string) (*pki.Certificate, error) { func (k fakeCAStore) ListKeysets() (map[string]*fi.Keyset, error) { panic("fakeCAStore does not implement ListKeysets") } - -func (k fakeCAStore) DeleteKeysetItem(item *kops.Keyset, id string) error { - panic("fakeCAStore does not implement DeleteKeysetItem") -} diff --git a/pkg/configserver/BUILD.bazel b/pkg/configserver/BUILD.bazel index 2433c7b3f6..af913f5845 100644 --- a/pkg/configserver/BUILD.bazel +++ b/pkg/configserver/BUILD.bazel @@ -9,7 +9,6 @@ go_library( importpath = "k8s.io/kops/pkg/configserver", visibility = ["//visibility:public"], deps = [ - "//pkg/apis/kops:go_default_library", "//pkg/apis/nodeup:go_default_library", "//pkg/pki:go_default_library", "//upup/pkg/fi:go_default_library", diff --git a/pkg/configserver/keystore.go b/pkg/configserver/keystore.go index 5dde635c97..d548388a13 100644 --- a/pkg/configserver/keystore.go +++ b/pkg/configserver/keystore.go @@ -20,7 +20,6 @@ import ( "crypto/x509" "fmt" - "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/apis/nodeup" "k8s.io/kops/pkg/pki" "k8s.io/kops/upup/pkg/fi" @@ -88,8 +87,3 @@ func (s *configserverKeyStore) FindCert(name string) (*pki.Certificate, error) { func (s *configserverKeyStore) ListKeysets() (map[string]*fi.Keyset, error) { return nil, fmt.Errorf("ListKeysets not supported by configserverKeyStore") } - -// DeleteKeysetItem implements fi.CAStore -func (s *configserverKeyStore) DeleteKeysetItem(item *kops.Keyset, id string) error { - return fmt.Errorf("DeleteKeysetItem not supported by configserverKeyStore") -} diff --git a/upup/pkg/fi/ca.go b/upup/pkg/fi/ca.go index e7e410c2a7..abd3dfd6e7 100644 --- a/upup/pkg/fi/ca.go +++ b/upup/pkg/fi/ca.go @@ -93,9 +93,6 @@ type CAStore interface { // ListKeysets will return all the KeySets. ListKeysets() (map[string]*Keyset, error) - - // DeleteKeysetItem will delete the specified item from the Keyset - DeleteKeysetItem(item *kops.Keyset, id string) error } // SSHCredentialStore holds SSHCredential objects diff --git a/upup/pkg/fi/clientset_castore.go b/upup/pkg/fi/clientset_castore.go index d1a0683c6f..2bce8f8479 100644 --- a/upup/pkg/fi/clientset_castore.go +++ b/upup/pkg/fi/clientset_castore.go @@ -307,47 +307,6 @@ func (c *ClientsetCAStore) storeKeyset(ctx context.Context, name string, keyset return nil } -// deleteKeysetItem deletes the specified key from the registry; deleting the whole Keyset if it was the last one. -func deleteKeysetItem(client kopsinternalversion.KeysetInterface, name string, keysetType kops.KeysetType, id string) error { - ctx := context.TODO() - - keyset, err := client.Get(ctx, name, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return nil - } - return fmt.Errorf("error reading Keyset %q: %v", name, err) - } - - if keyset.Spec.Type != keysetType { - return fmt.Errorf("mismatch on Keyset type on %q", name) - } - - var newKeys []kops.KeysetItem - found := false - for _, ki := range keyset.Spec.Keys { - if ki.Id == id { - found = true - } else { - newKeys = append(newKeys, ki) - } - } - if !found { - return fmt.Errorf("KeysetItem %q not found in Keyset %q", id, name) - } - if len(newKeys) == 0 { - if err := client.Delete(ctx, name, metav1.DeleteOptions{}); err != nil { - return fmt.Errorf("error deleting Keyset %q: %v", name, err) - } - } else { - keyset.Spec.Keys = newKeys - if _, err := client.Update(ctx, keyset, metav1.UpdateOptions{}); err != nil { - return fmt.Errorf("error updating Keyset %q: %v", name, err) - } - } - return nil -} - // addSSHCredential saves the specified SSH Credential to the registry, doing an update or insert func (c *ClientsetCAStore) addSSHCredential(ctx context.Context, name string, publicKey string) error { create := false @@ -425,18 +384,6 @@ func (c *ClientsetCAStore) FindSSHPublicKeys(name string) ([]*kops.SSHCredential return items, nil } -// DeleteKeysetItem implements CAStore::DeleteKeysetItem -func (c *ClientsetCAStore) DeleteKeysetItem(item *kops.Keyset, id string) error { - switch item.Spec.Type { - case kops.SecretTypeKeypair: - client := c.clientset.Keysets(c.namespace) - return deleteKeysetItem(client, item.Name, kops.SecretTypeKeypair, id) - default: - // Primarily because we need to make sure users can recreate them! - return fmt.Errorf("deletion of keystore items of type %v not (yet) supported", item.Spec.Type) - } -} - // DeleteSSHCredential implements SSHCredentialStore::DeleteSSHCredential func (c *ClientsetCAStore) DeleteSSHCredential(item *kops.SSHCredential) error { ctx := context.TODO() diff --git a/upup/pkg/fi/vfs_castore.go b/upup/pkg/fi/vfs_castore.go index bd853f3d7e..e5e5a3e24a 100644 --- a/upup/pkg/fi/vfs_castore.go +++ b/upup/pkg/fi/vfs_castore.go @@ -19,7 +19,6 @@ package fi import ( "bytes" "fmt" - "math/big" "os" "sort" "strings" @@ -75,18 +74,10 @@ func (c *VFSCAStore) buildCertificatePoolPath(name string) vfs.Path { return c.basedir.Join("issued", name) } -func (c *VFSCAStore) buildCertificatePath(name string, id string) vfs.Path { - return c.basedir.Join("issued", name, id+".crt") -} - func (c *VFSCAStore) buildPrivateKeyPoolPath(name string) vfs.Path { return c.basedir.Join("private", name) } -func (c *VFSCAStore) buildPrivateKeyPath(name string, id string) vfs.Path { - return c.basedir.Join("private", name, id+".key") -} - func (c *VFSCAStore) parseKeysetYaml(data []byte) (*kops.Keyset, bool, error) { defaultReadVersion := v1alpha2.SchemeGroupVersion.WithKind("Keyset") @@ -495,73 +486,6 @@ func (c *VFSCAStore) FindPrivateKey(id string) (*pki.PrivateKey, error) { return key, nil } -func (c *VFSCAStore) deletePrivateKey(name string, id string) (bool, error) { - // Delete the file itself - { - - p := c.buildPrivateKeyPath(name, id) - if err := p.Remove(); err != nil && !os.IsNotExist(err) { - return false, err - } - } - - // Update the bundle - { - p := c.buildPrivateKeyPoolPath(name) - ks, err := c.loadKeyset(p) - if err != nil { - return false, err - } - - if ks == nil || ks.Items[id] == nil { - return false, nil - } - delete(ks.Items, id) - if ks.Primary != nil && ks.Primary.Id == id { - ks.Primary = nil - } - - if err := writeKeysetBundle(c.cluster, p, name, ks, true); err != nil { - return false, fmt.Errorf("error writing bundle: %v", err) - } - } - - return true, nil -} - -func (c *VFSCAStore) deleteCertificate(name string, id string) (bool, error) { - // Delete the file itself - { - p := c.buildCertificatePath(name, id) - if err := p.Remove(); err != nil && !os.IsNotExist(err) { - return false, err - } - } - - // Update the bundle - { - p := c.buildCertificatePoolPath(name) - ks, err := c.loadKeyset(p) - if err != nil { - return false, err - } - - if ks == nil || ks.Items[id] == nil { - return false, nil - } - delete(ks.Items, id) - if ks.Primary != nil && ks.Primary.Id == id { - ks.Primary = nil - } - - if err := writeKeysetBundle(c.cluster, p, name, ks, false); err != nil { - return false, fmt.Errorf("error writing bundle: %v", err) - } - } - - return true, nil -} - // AddSSHPublicKey stores an SSH public key func (c *VFSCAStore) AddSSHPublicKey(name string, pubkey []byte) error { id, err := sshcredentials.Fingerprint(string(pubkey)) @@ -617,36 +541,6 @@ func (c *VFSCAStore) FindSSHPublicKeys(name string) ([]*kops.SSHCredential, erro return items, nil } -// DeleteKeysetItem implements CAStore::DeleteKeysetItem -func (c *VFSCAStore) DeleteKeysetItem(item *kops.Keyset, id string) error { - switch item.Spec.Type { - case kops.SecretTypeKeypair: - _, ok := big.NewInt(0).SetString(id, 10) - if !ok { - return fmt.Errorf("keypair had non-integer version: %q", id) - } - removed, err := c.deleteCertificate(item.Name, id) - if err != nil { - return fmt.Errorf("error deleting certificate: %v", err) - } - if !removed { - klog.Warningf("certificate %s:%s was not found", item.Name, id) - } - removed, err = c.deletePrivateKey(item.Name, id) - if err != nil { - return fmt.Errorf("error deleting private key: %v", err) - } - if !removed { - klog.Warningf("private key %s:%s was not found", item.Name, id) - } - return nil - - default: - // Primarily because we need to make sure users can recreate them! - return fmt.Errorf("deletion of keystore items of type %v not (yet) supported", item.Spec.Type) - } -} - func (c *VFSCAStore) DeleteSSHCredential(item *kops.SSHCredential) error { if item.Spec.PublicKey == "" { return fmt.Errorf("must specific public key to delete SSHCredential")