From a8c8457f457d45b7703f8345e1e34c5831aaf496 Mon Sep 17 00:00:00 2001 From: Thomas Jungblut Date: Thu, 14 Mar 2024 16:18:20 +0100 Subject: [PATCH] add recert cmd --- cmd/cluster-etcd-operator/main.go | 2 + pkg/cmd/recert/recert.go | 175 ++++++++++++++++++ pkg/cmd/render/render.go | 3 +- .../etcdcertsigner}/certs.go | 13 +- .../etcdcertsigner}/certs_test.go | 6 +- 5 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 pkg/cmd/recert/recert.go rename pkg/{cmd/render => operator/etcdcertsigner}/certs.go (94%) rename pkg/{cmd/render => operator/etcdcertsigner}/certs_test.go (97%) diff --git a/cmd/cluster-etcd-operator/main.go b/cmd/cluster-etcd-operator/main.go index d4a3763d5..eb3cf858c 100644 --- a/cmd/cluster-etcd-operator/main.go +++ b/cmd/cluster-etcd-operator/main.go @@ -4,6 +4,7 @@ import ( "context" goflag "flag" "fmt" + "github.com/openshift/cluster-etcd-operator/pkg/cmd/recert" "io/ioutil" "math/rand" "os" @@ -75,6 +76,7 @@ func NewSSCSCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(readyz.NewReadyzCommand()) cmd.AddCommand(prune_backups.NewPruneCommand()) cmd.AddCommand(requestbackup.NewRequestBackupCommand(ctx)) + cmd.AddCommand(recert.NewRecertCommand(os.Stderr)) return cmd } diff --git a/pkg/cmd/recert/recert.go b/pkg/cmd/recert/recert.go new file mode 100644 index 000000000..313d78ce1 --- /dev/null +++ b/pkg/cmd/recert/recert.go @@ -0,0 +1,175 @@ +package recert + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/ghodss/yaml" + "github.com/openshift/cluster-etcd-operator/pkg/operator/etcdcertsigner" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "io" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/klog/v2" + "k8s.io/utils/path" + "net" + "os" + "path/filepath" +) + +// recertOpts holds values to drive the recert command. +type recertOpts struct { + // Path to output certificates + outputDir string + errOut io.Writer + + // hostname -> ip (v4/v6) as string + hostIPs map[string]string + force bool + // allowed values are json,yaml + format string +} + +// NewRecertCommand creates a recert command. +func NewRecertCommand(errOut io.Writer) *cobra.Command { + renderOpts := recertOpts{ + errOut: errOut, + } + cmd := &cobra.Command{ + Use: "recert", + Short: "Recreates all etcd related certificates for the given IPs and hostnames.", + Run: func(cmd *cobra.Command, args []string) { + must := func(fn func() error) { + if err := fn(); err != nil { + if cmd.HasParent() { + klog.Fatal(err) + } + fmt.Fprint(renderOpts.errOut, err.Error()) + } + } + + must(renderOpts.Validate) + must(renderOpts.Run) + }, + } + + renderOpts.AddFlags(cmd.Flags()) + + return cmd +} + +func (r *recertOpts) AddFlags(fs *pflag.FlagSet) { + fs.StringVarP(&r.outputDir, "output", "o", "", + "output path for certificates, must be a non-existent directory that will be automatically created") + fs.StringToStringVar(&r.hostIPs, "hips", map[string]string{}, + "a list of host=ip pairs, e.g. --hips \"master-1=192.168.2.1,master-2=192.168.2.2,master-3=192.168.2.3\"") + fs.BoolVarP(&r.force, "force", "f", false, "skips hostname/IP validation") + fs.StringVarP(&r.format, "format", "w", "yaml", + "options yaml and json output secrets and configmaps. Example: -w json") +} + +// Validate verifies the inputs. +func (r *recertOpts) Validate() error { + if len(r.outputDir) == 0 { + return errors.New("missing required flag: --output") + } + if len(r.hostIPs) == 0 { + return errors.New("need at least one hostname/IP pair in: --hips") + } + + if r.format != "yaml" && r.format != "json" { + return fmt.Errorf("only supported formats are \"json\" and \"yaml\", supplied %s", r.format) + } + + exists, err := path.Exists(path.CheckFollowSymlink, r.outputDir) + if err != nil { + return fmt.Errorf("error while checking whether output dir already exists: %w", err) + } + + if exists { + return fmt.Errorf("output dir %s already exists", r.outputDir) + } + + if r.force { + return nil + } + + for hostName, ipAddress := range r.hostIPs { + if ip := net.ParseIP(ipAddress); ip == nil { + return fmt.Errorf("could not parse IP address: %s", ip) + } + if validationErrs := validation.IsDNS1123Label(hostName); validationErrs != nil { + return fmt.Errorf("could not parse hostname as DNS1123 label: %s - %v", hostName, validationErrs) + } + } + + return nil +} + +func (r *recertOpts) hostIPsAsNodes() []*corev1.Node { + var nodes []*corev1.Node + for hostName, ipAddress := range r.hostIPs { + n := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: hostName, Labels: map[string]string{"node-role.kubernetes.io/master": ""}}, + Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Type: corev1.NodeInternalIP, Address: ipAddress}}}, + } + nodes = append(nodes, n) + } + + return nodes +} + +func (r *recertOpts) Run() error { + err := os.MkdirAll(r.outputDir, 0755) + if err != nil { + return fmt.Errorf("could not mkdir %s: %w", r.outputDir, err) + } + + nodes := r.hostIPsAsNodes() + certs, bundles, err := etcdcertsigner.CreateCertSecrets(nodes) + if err != nil { + return fmt.Errorf("could not create cert secrets, error was: %w", err) + } + + for _, cert := range certs { + destinationPath := filepath.Join(r.outputDir, fmt.Sprintf("%s-%s.%s", cert.Namespace, cert.Name, r.format)) + bytes, err := json.Marshal(cert) + if err != nil { + return fmt.Errorf("error while marshalling secret %s: %w", cert.Name, err) + } + if r.format == "yaml" { + bytes, err = yaml.JSONToYAML(bytes) + if err != nil { + return fmt.Errorf("error while converting secret from json to yaml %s: %w", cert.Name, err) + } + } + + err = os.WriteFile(destinationPath, bytes, 0644) + if err != nil { + return fmt.Errorf("error while writing secret %s: %w", cert.Name, err) + } + } + + for _, bundle := range bundles { + destinationPath := filepath.Join(r.outputDir, fmt.Sprintf("%s-%s.%s", bundle.Namespace, bundle.Name, r.format)) + bytes, err := json.Marshal(bundle) + if err != nil { + return fmt.Errorf("error while marshalling configmap %s: %w", bundle.Name, err) + } + if r.format == "yaml" { + bytes, err = yaml.JSONToYAML(bytes) + if err != nil { + return fmt.Errorf("error while converting configmap from json to yaml %s: %w", bundle.Name, err) + } + } + + err = os.WriteFile(destinationPath, bytes, 0644) + if err != nil { + return fmt.Errorf("error while writing configmap %s: %w", bundle.Name, err) + } + } + + return nil +} diff --git a/pkg/cmd/render/render.go b/pkg/cmd/render/render.go index f6904648f..778953d48 100644 --- a/pkg/cmd/render/render.go +++ b/pkg/cmd/render/render.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/openshift/cluster-etcd-operator/pkg/operator/etcdcertsigner" "io" corev1 "k8s.io/api/core/v1" "net" @@ -268,7 +269,7 @@ func newTemplateData(opts *renderOpts) (*TemplateData, error) { base64.StdEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte(templateData.BootstrapIP)), templateData.BootstrapIP) } - certs, bundles, err := createBootstrapCertSecrets(templateData.Hostname, templateData.BootstrapIP) + certs, bundles, err := etcdcertsigner.CreateBootstrapCertSecrets(templateData.Hostname, templateData.BootstrapIP) if err != nil { return nil, err } diff --git a/pkg/cmd/render/certs.go b/pkg/operator/etcdcertsigner/certs.go similarity index 94% rename from pkg/cmd/render/certs.go rename to pkg/operator/etcdcertsigner/certs.go index 8f2f42daf..02ddd8533 100644 --- a/pkg/cmd/render/certs.go +++ b/pkg/operator/etcdcertsigner/certs.go @@ -1,11 +1,10 @@ -package render +package etcdcertsigner import ( "context" "fmt" operatorv1 "github.com/openshift/api/operator/v1" "github.com/openshift/cluster-etcd-operator/pkg/operator/ceohelpers" - "github.com/openshift/cluster-etcd-operator/pkg/operator/etcdcertsigner" "github.com/openshift/cluster-etcd-operator/pkg/operator/health" "github.com/openshift/cluster-etcd-operator/pkg/operator/operatorclient" "github.com/openshift/cluster-etcd-operator/pkg/operator/resourcesynccontroller" @@ -20,9 +19,9 @@ import ( "k8s.io/client-go/kubernetes/fake" ) -// createCertSecrets will run the etcdcertsigner.EtcdCertSignerController once and collect all respective certs created. +// CreateCertSecrets will run the etcdcertsigner.EtcdCertSignerController once and collect all respective certs created. // The secrets will contain all signers, peer, serving and client certs. The configmaps contain all bundles. -func createCertSecrets(nodes []*corev1.Node) ([]corev1.Secret, []corev1.ConfigMap, error) { +func CreateCertSecrets(nodes []*corev1.Node) ([]corev1.Secret, []corev1.ConfigMap, error) { var fakeObjs []runtime.Object for _, node := range nodes { fakeObjs = append(fakeObjs, node) @@ -63,7 +62,7 @@ func createCertSecrets(nodes []*corev1.Node) ([]corev1.Secret, []corev1.ConfigMa return nil, nil, fmt.Errorf("could not parse master node labels: %w", err) } - controller := etcdcertsigner.NewEtcdCertSignerController( + controller := NewEtcdCertSignerController( health.NewMultiAlivenessChecker(), fakeKubeClient, fakeOperatorClient, @@ -137,8 +136,8 @@ func createCertSecrets(nodes []*corev1.Node) ([]corev1.Secret, []corev1.ConfigMa return secrets, bundles, nil } -func createBootstrapCertSecrets(hostName string, ipAddress string) ([]corev1.Secret, []corev1.ConfigMap, error) { - return createCertSecrets([]*corev1.Node{ +func CreateBootstrapCertSecrets(hostName string, ipAddress string) ([]corev1.Secret, []corev1.ConfigMap, error) { + return CreateCertSecrets([]*corev1.Node{ { ObjectMeta: metav1.ObjectMeta{Name: hostName, Labels: map[string]string{"node-role.kubernetes.io/master": ""}}, Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Type: corev1.NodeInternalIP, Address: ipAddress}}}, diff --git a/pkg/cmd/render/certs_test.go b/pkg/operator/etcdcertsigner/certs_test.go similarity index 97% rename from pkg/cmd/render/certs_test.go rename to pkg/operator/etcdcertsigner/certs_test.go index 039f377c4..d634ed255 100644 --- a/pkg/cmd/render/certs_test.go +++ b/pkg/operator/etcdcertsigner/certs_test.go @@ -1,4 +1,4 @@ -package render +package etcdcertsigner import ( "bytes" @@ -19,7 +19,7 @@ import ( func TestCertSingleNode(t *testing.T) { node := u.FakeNode("cp-1", u.WithMasterLabel(), u.WithNodeInternalIP("192.168.2.1")) - secrets, bundles, err := createCertSecrets([]*corev1.Node{node}) + secrets, bundles, err := CreateCertSecrets([]*corev1.Node{node}) require.NoError(t, err) require.Equal(t, 11, len(secrets)) @@ -36,7 +36,7 @@ func TestCertsMultiNode(t *testing.T) { u.FakeNode("cp-2", u.WithMasterLabel(), u.WithNodeInternalIP("192.168.2.2")), u.FakeNode("cp-3", u.WithMasterLabel(), u.WithNodeInternalIP("192.168.2.3")), } - secrets, bundles, err := createCertSecrets(nodes) + secrets, bundles, err := CreateCertSecrets(nodes) require.NoError(t, err) require.Equal(t, 17, len(secrets))