Skip to content
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

ETCD-573: add recert cmd #1227

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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