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

kubeadm alpha certs generate-csr #92183

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/kubeadm/app/cmd/alpha/BUILD
Expand Up @@ -21,6 +21,7 @@ go_library(
"//cmd/kubeadm/app/cmd/util:go_default_library",
wallrj marked this conversation as resolved.
Show resolved Hide resolved
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/features:go_default_library",
"//cmd/kubeadm/app/phases/certs:go_default_library",
"//cmd/kubeadm/app/phases/certs/renewal:go_default_library",
"//cmd/kubeadm/app/phases/copycerts:go_default_library",
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
Expand All @@ -34,6 +35,7 @@ go_library(
"//vendor/github.com/lithammer/dedent:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
],
)

Expand All @@ -59,6 +61,8 @@ go_test(
],
embed = [":go_default_library"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library",
"//cmd/kubeadm/app/constants:go_default_library",
"//cmd/kubeadm/app/phases/certs:go_default_library",
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
Expand All @@ -68,5 +72,8 @@ go_test(
"//cmd/kubeadm/test/kubeconfig:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
],
)
93 changes: 93 additions & 0 deletions cmd/kubeadm/app/cmd/alpha/certs.go
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/lithammer/dedent"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"k8s.io/apimachinery/pkg/util/duration"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
Expand All @@ -33,8 +34,10 @@ import (
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/copycerts"
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
)
Expand Down Expand Up @@ -68,6 +71,22 @@ var (

You can also use "kubeadm init --upload-certs" without specifying a certificate key and it will
generate and print one for you.
`)
generateCSRLongDesc = cmdutil.LongDesc(`
Generates keys and certificate signing requests (CSRs) for all the certificates required to run the control plane.
This command also generates partial kubeconfig files with private key data in the "users > user > client-key-data" field,
and for each kubeconfig file an accompanying ".csr" file is created.

This command is designed for use in [Kubeadm External CA Mode](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-certs/#external-ca-mode).
It generates CSRs which you can then submit to your external certificate authority for signing.

The PEM encoded signed certificates should then be saved alongside the key files, using ".crt" as the file extension,
or in the case of kubeconfig files, the PEM encoded signed certificate should be base64 encoded
and added to the kubeconfig file in the "users > user > client-certificate-data" field.
`)
generateCSRExample = cmdutil.Examples(`
# The following command will generate keys and CSRs for all control-plane certificates and kubeconfig files:
kubeadm alpha certs generate-csr --kubeconfig-dir /tmp/etc-k8s --cert-dir /tmp/etc-k8s/pki
`)
)

Expand All @@ -82,9 +101,83 @@ func newCmdCertsUtility(out io.Writer) *cobra.Command {
cmd.AddCommand(newCmdCertsRenewal(out))
cmd.AddCommand(newCmdCertsExpiration(out, constants.KubernetesDir))
cmd.AddCommand(NewCmdCertificateKey())
cmd.AddCommand(newCmdGenCSR())
return cmd
}

// genCSRConfig is the configuration required by the gencsr command
type genCSRConfig struct {
wallrj marked this conversation as resolved.
Show resolved Hide resolved
kubeadmConfigPath string
certDir string
kubeConfigDir string
kubeadmConfig *kubeadmapi.InitConfiguration
}

func newGenCSRConfig() *genCSRConfig {
return &genCSRConfig{
kubeConfigDir: kubeadmconstants.KubernetesDir,
}
}

func (o *genCSRConfig) addFlagSet(flagSet *pflag.FlagSet) {
options.AddConfigFlag(flagSet, &o.kubeadmConfigPath)
options.AddCertificateDirFlag(flagSet, &o.certDir)
options.AddKubeConfigDirFlag(flagSet, &o.kubeConfigDir)
}

// load merges command line flag values into kubeadm's config.
// Reads Kubeadm config from a file (if present)
// else use dynamically generated default config.
// This configuration contains the DNS names and IP addresses which
// are encoded in the control-plane CSRs.
func (o *genCSRConfig) load() (err error) {
o.kubeadmConfig, err = configutil.LoadOrDefaultInitConfiguration(
wallrj marked this conversation as resolved.
Show resolved Hide resolved
o.kubeadmConfigPath,
&kubeadmapiv1beta2.InitConfiguration{},
&kubeadmapiv1beta2.ClusterConfiguration{},
)
if err != nil {
return err
}
// --cert-dir takes priority over kubeadm config if set.
if o.certDir != "" {
o.kubeadmConfig.CertificatesDir = o.certDir
}
return nil
}

// newCmdGenCSR returns cobra.Command for generating keys and CSRs
func newCmdGenCSR() *cobra.Command {
config := newGenCSRConfig()

cmd := &cobra.Command{
Use: "generate-csr",
Short: "Generate keys and certificate signing requests",
Long: generateCSRLongDesc,
Example: generateCSRExample,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if err := config.load(); err != nil {
return err
}
wallrj marked this conversation as resolved.
Show resolved Hide resolved
return runGenCSR(config)
},
}
config.addFlagSet(cmd.Flags())
return cmd
}

// runGenCSR contains the logic of the generate-csr sub-command.
func runGenCSR(config *genCSRConfig) error {
if err := certsphase.CreateDefaultKeysAndCSRFiles(config.kubeadmConfig); err != nil {
return err
}
if err := kubeconfigphase.CreateDefaultKubeConfigsAndCSRFiles(config.kubeConfigDir, config.kubeadmConfig); err != nil {
return err
}
return nil
}

// NewCmdCertificateKey returns cobra.Command for certificate key generate
func NewCmdCertificateKey() *cobra.Command {
return &cobra.Command{
Expand Down
189 changes: 189 additions & 0 deletions cmd/kubeadm/app/cmd/alpha/certs_test.go
Expand Up @@ -27,6 +27,13 @@ import (
"time"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
wallrj marked this conversation as resolved.
Show resolved Hide resolved

"k8s.io/client-go/tools/clientcmd"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
Expand Down Expand Up @@ -285,3 +292,185 @@ func TestRenewUsingCSR(t *testing.T) {
t.Fatalf("couldn't load certificate %q: %v", cert.Name, err)
}
}

func TestRunGenCSR(t *testing.T) {
tmpDir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpDir)

kubeConfigDir := filepath.Join(tmpDir, "kubernetes")
certDir := kubeConfigDir + "/pki"

expectedCertificates := []string{
"apiserver",
"apiserver-etcd-client",
"apiserver-kubelet-client",
"front-proxy-client",
"etcd/healthcheck-client",
"etcd/peer",
"etcd/server",
}

expectedKubeConfigs := []string{
"admin",
"kubelet",
wallrj marked this conversation as resolved.
Show resolved Hide resolved
"controller-manager",
"scheduler",
}

config := genCSRConfig{
kubeConfigDir: kubeConfigDir,
kubeadmConfig: &kubeadmapi.InitConfiguration{
LocalAPIEndpoint: kubeadmapi.APIEndpoint{
AdvertiseAddress: "192.0.2.1",
BindPort: 443,
},
ClusterConfiguration: kubeadmapi.ClusterConfiguration{
Networking: kubeadmapi.Networking{
ServiceSubnet: "192.0.2.0/24",
},
CertificatesDir: certDir,
KubernetesVersion: "v1.19.0",
},
},
}

err := runGenCSR(&config)
require.NoError(t, err, "expected runGenCSR to not fail")

t.Log("The command generates key and CSR files in the configured --cert-dir")
for _, name := range expectedCertificates {
_, err = pkiutil.TryLoadKeyFromDisk(certDir, name)
assert.NoErrorf(t, err, "failed to load key file: %s", name)

_, err = pkiutil.TryLoadCSRFromDisk(certDir, name)
assert.NoError(t, err, "failed to load CSR file: %s", name)
}

t.Log("The command generates kubeconfig files in the configured --kubeconfig-dir")
for _, name := range expectedKubeConfigs {
_, err = clientcmd.LoadFromFile(kubeConfigDir + "/" + name + ".conf")
assert.NoErrorf(t, err, "failed to load kubeconfig file: %s", name)

_, err = pkiutil.TryLoadCSRFromDisk(kubeConfigDir, name+".conf")
assert.NoError(t, err, "failed to load kubeconfig CSR file: %s", name)
}
}
wallrj marked this conversation as resolved.
Show resolved Hide resolved

func TestGenCSRConfig(t *testing.T) {
type assertion func(*testing.T, *genCSRConfig)

hasCertDir := func(expected string) assertion {
return func(t *testing.T, config *genCSRConfig) {
assert.Equal(t, expected, config.kubeadmConfig.CertificatesDir)
}
}
hasKubeConfigDir := func(expected string) assertion {
return func(t *testing.T, config *genCSRConfig) {
assert.Equal(t, expected, config.kubeConfigDir)
}
}
hasAdvertiseAddress := func(expected string) assertion {
return func(t *testing.T, config *genCSRConfig) {
assert.Equal(t, expected, config.kubeadmConfig.LocalAPIEndpoint.AdvertiseAddress)
}
}

// A minimal kubeadm config with just enough values to avoid triggering
// auto-detection of config values at runtime.
const kubeadmConfig = `
apiVersion: kubeadm.k8s.io/v1beta2
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 192.0.2.1
nodeRegistration:
criSocket: /path/to/dockershim.sock
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
certificatesDir: /custom/config/certificates-dir
kubernetesVersion: v1.19.0
`

tmpDir := testutil.SetupTempDir(t)
defer os.RemoveAll(tmpDir)

customConfigPath := tmpDir + "/kubeadm.conf"

f, err := os.Create(customConfigPath)
require.NoError(t, err)
_, err = f.Write([]byte(kubeadmConfig))
require.NoError(t, err)

tests := []struct {
name string
flags []string
assertions []assertion
expectErr bool
}{
{
name: "default",
assertions: []assertion{
hasCertDir(kubeadmapiv1beta2.DefaultCertificatesDir),
hasKubeConfigDir(kubeadmconstants.KubernetesDir),
},
},
{
name: "--cert-dir overrides default",
flags: []string{"--cert-dir", "/foo/bar/pki"},
assertions: []assertion{
hasCertDir("/foo/bar/pki"),
},
},
{
name: "--config is loaded",
flags: []string{"--config", customConfigPath},
assertions: []assertion{
hasCertDir("/custom/config/certificates-dir"),
hasAdvertiseAddress("192.0.2.1"),
},
},
{
name: "--config not found",
flags: []string{"--config", "/does/not/exist"},
expectErr: true,
},
{
name: "--cert-dir overrides --config certificatesDir",
flags: []string{
"--config", customConfigPath,
"--cert-dir", "/foo/bar/pki",
},
assertions: []assertion{
hasCertDir("/foo/bar/pki"),
hasAdvertiseAddress("192.0.2.1"),
},
},
{
name: "--kubeconfig-dir overrides default",
flags: []string{
"--kubeconfig-dir", "/foo/bar/kubernetes",
},
assertions: []assertion{
hasKubeConfigDir("/foo/bar/kubernetes"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
flagset := pflag.NewFlagSet("flags-for-gencsr", pflag.ContinueOnError)
config := newGenCSRConfig()
config.addFlagSet(flagset)
require.NoError(t, flagset.Parse(test.flags))

err := config.load()
if test.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
for _, assertFunc := range test.assertions {
assertFunc(t, config)
}
})
}
}
2 changes: 2 additions & 0 deletions cmd/kubeadm/app/cmd/phases/init/certs.go
Expand Up @@ -66,7 +66,9 @@ func NewCertsPhase() workflow.Phase {
func localFlags() *pflag.FlagSet {
set := pflag.NewFlagSet("csr", pflag.ExitOnError)
options.AddCSRFlag(set, &csrOnly)
set.MarkDeprecated(options.CSROnly, "This flag will be removed in a future version. Please use kubeadm alpha certs generate-csr instead.")
options.AddCSRDirFlag(set, &csrDir)
set.MarkDeprecated(options.CSRDir, "This flag will be removed in a future version. Please use kubeadm alpha certs generate-csr instead.")
return set
wallrj marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down