Skip to content

Commit 19f0a54

Browse files
authored
Merge pull request kubernetes#92183 from wallrj/2163-csr-only-external-ca-mode-2
kubeadm alpha certs generate-csr
2 parents 1bcf42b + 81554ff commit 19f0a54

File tree

7 files changed

+508
-56
lines changed

7 files changed

+508
-56
lines changed

cmd/kubeadm/app/cmd/alpha/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ go_library(
2121
"//cmd/kubeadm/app/cmd/util:go_default_library",
2222
"//cmd/kubeadm/app/constants:go_default_library",
2323
"//cmd/kubeadm/app/features:go_default_library",
24+
"//cmd/kubeadm/app/phases/certs:go_default_library",
2425
"//cmd/kubeadm/app/phases/certs/renewal:go_default_library",
2526
"//cmd/kubeadm/app/phases/copycerts:go_default_library",
2627
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
@@ -34,6 +35,7 @@ go_library(
3435
"//vendor/github.com/lithammer/dedent:go_default_library",
3536
"//vendor/github.com/pkg/errors:go_default_library",
3637
"//vendor/github.com/spf13/cobra:go_default_library",
38+
"//vendor/github.com/spf13/pflag:go_default_library",
3739
],
3840
)
3941

@@ -59,6 +61,8 @@ go_test(
5961
],
6062
embed = [":go_default_library"],
6163
deps = [
64+
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
65+
"//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library",
6266
"//cmd/kubeadm/app/constants:go_default_library",
6367
"//cmd/kubeadm/app/phases/certs:go_default_library",
6468
"//cmd/kubeadm/app/phases/kubeconfig:go_default_library",
@@ -68,5 +72,8 @@ go_test(
6872
"//cmd/kubeadm/test/kubeconfig:go_default_library",
6973
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
7074
"//vendor/github.com/spf13/cobra:go_default_library",
75+
"//vendor/github.com/spf13/pflag:go_default_library",
76+
"//vendor/github.com/stretchr/testify/assert:go_default_library",
77+
"//vendor/github.com/stretchr/testify/require:go_default_library",
7178
],
7279
)

cmd/kubeadm/app/cmd/alpha/certs.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/lithammer/dedent"
2525
"github.com/pkg/errors"
2626
"github.com/spf13/cobra"
27+
"github.com/spf13/pflag"
2728

2829
"k8s.io/apimachinery/pkg/util/duration"
2930
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
@@ -33,8 +34,10 @@ import (
3334
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
3435
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
3536
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
37+
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
3638
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal"
3739
"k8s.io/kubernetes/cmd/kubeadm/app/phases/copycerts"
40+
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
3841
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
3942
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
4043
)
@@ -68,6 +71,22 @@ var (
6871
6972
You can also use "kubeadm init --upload-certs" without specifying a certificate key and it will
7073
generate and print one for you.
74+
`)
75+
generateCSRLongDesc = cmdutil.LongDesc(`
76+
Generates keys and certificate signing requests (CSRs) for all the certificates required to run the control plane.
77+
This command also generates partial kubeconfig files with private key data in the "users > user > client-key-data" field,
78+
and for each kubeconfig file an accompanying ".csr" file is created.
79+
80+
This command is designed for use in [Kubeadm External CA Mode](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-certs/#external-ca-mode).
81+
It generates CSRs which you can then submit to your external certificate authority for signing.
82+
83+
The PEM encoded signed certificates should then be saved alongside the key files, using ".crt" as the file extension,
84+
or in the case of kubeconfig files, the PEM encoded signed certificate should be base64 encoded
85+
and added to the kubeconfig file in the "users > user > client-certificate-data" field.
86+
`)
87+
generateCSRExample = cmdutil.Examples(`
88+
# The following command will generate keys and CSRs for all control-plane certificates and kubeconfig files:
89+
kubeadm alpha certs generate-csr --kubeconfig-dir /tmp/etc-k8s --cert-dir /tmp/etc-k8s/pki
7190
`)
7291
)
7392

@@ -82,9 +101,83 @@ func newCmdCertsUtility(out io.Writer) *cobra.Command {
82101
cmd.AddCommand(newCmdCertsRenewal(out))
83102
cmd.AddCommand(newCmdCertsExpiration(out, constants.KubernetesDir))
84103
cmd.AddCommand(NewCmdCertificateKey())
104+
cmd.AddCommand(newCmdGenCSR())
85105
return cmd
86106
}
87107

108+
// genCSRConfig is the configuration required by the gencsr command
109+
type genCSRConfig struct {
110+
kubeadmConfigPath string
111+
certDir string
112+
kubeConfigDir string
113+
kubeadmConfig *kubeadmapi.InitConfiguration
114+
}
115+
116+
func newGenCSRConfig() *genCSRConfig {
117+
return &genCSRConfig{
118+
kubeConfigDir: kubeadmconstants.KubernetesDir,
119+
}
120+
}
121+
122+
func (o *genCSRConfig) addFlagSet(flagSet *pflag.FlagSet) {
123+
options.AddConfigFlag(flagSet, &o.kubeadmConfigPath)
124+
options.AddCertificateDirFlag(flagSet, &o.certDir)
125+
options.AddKubeConfigDirFlag(flagSet, &o.kubeConfigDir)
126+
}
127+
128+
// load merges command line flag values into kubeadm's config.
129+
// Reads Kubeadm config from a file (if present)
130+
// else use dynamically generated default config.
131+
// This configuration contains the DNS names and IP addresses which
132+
// are encoded in the control-plane CSRs.
133+
func (o *genCSRConfig) load() (err error) {
134+
o.kubeadmConfig, err = configutil.LoadOrDefaultInitConfiguration(
135+
o.kubeadmConfigPath,
136+
&kubeadmapiv1beta2.InitConfiguration{},
137+
&kubeadmapiv1beta2.ClusterConfiguration{},
138+
)
139+
if err != nil {
140+
return err
141+
}
142+
// --cert-dir takes priority over kubeadm config if set.
143+
if o.certDir != "" {
144+
o.kubeadmConfig.CertificatesDir = o.certDir
145+
}
146+
return nil
147+
}
148+
149+
// newCmdGenCSR returns cobra.Command for generating keys and CSRs
150+
func newCmdGenCSR() *cobra.Command {
151+
config := newGenCSRConfig()
152+
153+
cmd := &cobra.Command{
154+
Use: "generate-csr",
155+
Short: "Generate keys and certificate signing requests",
156+
Long: generateCSRLongDesc,
157+
Example: generateCSRExample,
158+
Args: cobra.NoArgs,
159+
RunE: func(cmd *cobra.Command, args []string) error {
160+
if err := config.load(); err != nil {
161+
return err
162+
}
163+
return runGenCSR(config)
164+
},
165+
}
166+
config.addFlagSet(cmd.Flags())
167+
return cmd
168+
}
169+
170+
// runGenCSR contains the logic of the generate-csr sub-command.
171+
func runGenCSR(config *genCSRConfig) error {
172+
if err := certsphase.CreateDefaultKeysAndCSRFiles(config.kubeadmConfig); err != nil {
173+
return err
174+
}
175+
if err := kubeconfigphase.CreateDefaultKubeConfigsAndCSRFiles(config.kubeConfigDir, config.kubeadmConfig); err != nil {
176+
return err
177+
}
178+
return nil
179+
}
180+
88181
// NewCmdCertificateKey returns cobra.Command for certificate key generate
89182
func NewCmdCertificateKey() *cobra.Command {
90183
return &cobra.Command{

cmd/kubeadm/app/cmd/alpha/certs_test.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ import (
2727
"time"
2828

2929
"github.com/spf13/cobra"
30+
"github.com/spf13/pflag"
31+
"github.com/stretchr/testify/assert"
32+
"github.com/stretchr/testify/require"
33+
34+
"k8s.io/client-go/tools/clientcmd"
35+
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
36+
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
3037
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
3138
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
3239
kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
@@ -285,3 +292,185 @@ func TestRenewUsingCSR(t *testing.T) {
285292
t.Fatalf("couldn't load certificate %q: %v", cert.Name, err)
286293
}
287294
}
295+
296+
func TestRunGenCSR(t *testing.T) {
297+
tmpDir := testutil.SetupTempDir(t)
298+
defer os.RemoveAll(tmpDir)
299+
300+
kubeConfigDir := filepath.Join(tmpDir, "kubernetes")
301+
certDir := kubeConfigDir + "/pki"
302+
303+
expectedCertificates := []string{
304+
"apiserver",
305+
"apiserver-etcd-client",
306+
"apiserver-kubelet-client",
307+
"front-proxy-client",
308+
"etcd/healthcheck-client",
309+
"etcd/peer",
310+
"etcd/server",
311+
}
312+
313+
expectedKubeConfigs := []string{
314+
"admin",
315+
"kubelet",
316+
"controller-manager",
317+
"scheduler",
318+
}
319+
320+
config := genCSRConfig{
321+
kubeConfigDir: kubeConfigDir,
322+
kubeadmConfig: &kubeadmapi.InitConfiguration{
323+
LocalAPIEndpoint: kubeadmapi.APIEndpoint{
324+
AdvertiseAddress: "192.0.2.1",
325+
BindPort: 443,
326+
},
327+
ClusterConfiguration: kubeadmapi.ClusterConfiguration{
328+
Networking: kubeadmapi.Networking{
329+
ServiceSubnet: "192.0.2.0/24",
330+
},
331+
CertificatesDir: certDir,
332+
KubernetesVersion: "v1.19.0",
333+
},
334+
},
335+
}
336+
337+
err := runGenCSR(&config)
338+
require.NoError(t, err, "expected runGenCSR to not fail")
339+
340+
t.Log("The command generates key and CSR files in the configured --cert-dir")
341+
for _, name := range expectedCertificates {
342+
_, err = pkiutil.TryLoadKeyFromDisk(certDir, name)
343+
assert.NoErrorf(t, err, "failed to load key file: %s", name)
344+
345+
_, err = pkiutil.TryLoadCSRFromDisk(certDir, name)
346+
assert.NoError(t, err, "failed to load CSR file: %s", name)
347+
}
348+
349+
t.Log("The command generates kubeconfig files in the configured --kubeconfig-dir")
350+
for _, name := range expectedKubeConfigs {
351+
_, err = clientcmd.LoadFromFile(kubeConfigDir + "/" + name + ".conf")
352+
assert.NoErrorf(t, err, "failed to load kubeconfig file: %s", name)
353+
354+
_, err = pkiutil.TryLoadCSRFromDisk(kubeConfigDir, name+".conf")
355+
assert.NoError(t, err, "failed to load kubeconfig CSR file: %s", name)
356+
}
357+
}
358+
359+
func TestGenCSRConfig(t *testing.T) {
360+
type assertion func(*testing.T, *genCSRConfig)
361+
362+
hasCertDir := func(expected string) assertion {
363+
return func(t *testing.T, config *genCSRConfig) {
364+
assert.Equal(t, expected, config.kubeadmConfig.CertificatesDir)
365+
}
366+
}
367+
hasKubeConfigDir := func(expected string) assertion {
368+
return func(t *testing.T, config *genCSRConfig) {
369+
assert.Equal(t, expected, config.kubeConfigDir)
370+
}
371+
}
372+
hasAdvertiseAddress := func(expected string) assertion {
373+
return func(t *testing.T, config *genCSRConfig) {
374+
assert.Equal(t, expected, config.kubeadmConfig.LocalAPIEndpoint.AdvertiseAddress)
375+
}
376+
}
377+
378+
// A minimal kubeadm config with just enough values to avoid triggering
379+
// auto-detection of config values at runtime.
380+
const kubeadmConfig = `
381+
apiVersion: kubeadm.k8s.io/v1beta2
382+
kind: InitConfiguration
383+
localAPIEndpoint:
384+
advertiseAddress: 192.0.2.1
385+
nodeRegistration:
386+
criSocket: /path/to/dockershim.sock
387+
---
388+
apiVersion: kubeadm.k8s.io/v1beta2
389+
kind: ClusterConfiguration
390+
certificatesDir: /custom/config/certificates-dir
391+
kubernetesVersion: v1.19.0
392+
`
393+
394+
tmpDir := testutil.SetupTempDir(t)
395+
defer os.RemoveAll(tmpDir)
396+
397+
customConfigPath := tmpDir + "/kubeadm.conf"
398+
399+
f, err := os.Create(customConfigPath)
400+
require.NoError(t, err)
401+
_, err = f.Write([]byte(kubeadmConfig))
402+
require.NoError(t, err)
403+
404+
tests := []struct {
405+
name string
406+
flags []string
407+
assertions []assertion
408+
expectErr bool
409+
}{
410+
{
411+
name: "default",
412+
assertions: []assertion{
413+
hasCertDir(kubeadmapiv1beta2.DefaultCertificatesDir),
414+
hasKubeConfigDir(kubeadmconstants.KubernetesDir),
415+
},
416+
},
417+
{
418+
name: "--cert-dir overrides default",
419+
flags: []string{"--cert-dir", "/foo/bar/pki"},
420+
assertions: []assertion{
421+
hasCertDir("/foo/bar/pki"),
422+
},
423+
},
424+
{
425+
name: "--config is loaded",
426+
flags: []string{"--config", customConfigPath},
427+
assertions: []assertion{
428+
hasCertDir("/custom/config/certificates-dir"),
429+
hasAdvertiseAddress("192.0.2.1"),
430+
},
431+
},
432+
{
433+
name: "--config not found",
434+
flags: []string{"--config", "/does/not/exist"},
435+
expectErr: true,
436+
},
437+
{
438+
name: "--cert-dir overrides --config certificatesDir",
439+
flags: []string{
440+
"--config", customConfigPath,
441+
"--cert-dir", "/foo/bar/pki",
442+
},
443+
assertions: []assertion{
444+
hasCertDir("/foo/bar/pki"),
445+
hasAdvertiseAddress("192.0.2.1"),
446+
},
447+
},
448+
{
449+
name: "--kubeconfig-dir overrides default",
450+
flags: []string{
451+
"--kubeconfig-dir", "/foo/bar/kubernetes",
452+
},
453+
assertions: []assertion{
454+
hasKubeConfigDir("/foo/bar/kubernetes"),
455+
},
456+
},
457+
}
458+
for _, test := range tests {
459+
t.Run(test.name, func(t *testing.T) {
460+
flagset := pflag.NewFlagSet("flags-for-gencsr", pflag.ContinueOnError)
461+
config := newGenCSRConfig()
462+
config.addFlagSet(flagset)
463+
require.NoError(t, flagset.Parse(test.flags))
464+
465+
err := config.load()
466+
if test.expectErr {
467+
assert.Error(t, err)
468+
} else {
469+
assert.NoError(t, err)
470+
}
471+
for _, assertFunc := range test.assertions {
472+
assertFunc(t, config)
473+
}
474+
})
475+
}
476+
}

cmd/kubeadm/app/cmd/phases/init/certs.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ func NewCertsPhase() workflow.Phase {
6666
func localFlags() *pflag.FlagSet {
6767
set := pflag.NewFlagSet("csr", pflag.ExitOnError)
6868
options.AddCSRFlag(set, &csrOnly)
69+
set.MarkDeprecated(options.CSROnly, "This flag will be removed in a future version. Please use kubeadm alpha certs generate-csr instead.")
6970
options.AddCSRDirFlag(set, &csrDir)
71+
set.MarkDeprecated(options.CSRDir, "This flag will be removed in a future version. Please use kubeadm alpha certs generate-csr instead.")
7072
return set
7173
}
7274

0 commit comments

Comments
 (0)