Skip to content

Commit

Permalink
add recert cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
tjungblu committed Mar 14, 2024
1 parent 479c2c7 commit a8c8457
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 11 deletions.
2 changes: 2 additions & 0 deletions cmd/cluster-etcd-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
goflag "flag"
"fmt"
"github.com/openshift/cluster-etcd-operator/pkg/cmd/recert"
"io/ioutil"
"math/rand"
"os"
Expand Down Expand Up @@ -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
}
175 changes: 175 additions & 0 deletions pkg/cmd/recert/recert.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion pkg/cmd/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
13 changes: 6 additions & 7 deletions pkg/cmd/render/certs.go → pkg/operator/etcdcertsigner/certs.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}}},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package render
package etcdcertsigner

import (
"bytes"
Expand All @@ -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))
Expand All @@ -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))
Expand Down

0 comments on commit a8c8457

Please sign in to comment.