Skip to content
This repository was archived by the owner on Jul 30, 2021. It is now read-only.

Commit 172994e

Browse files
authored
Add apiserver recovery backend. (#500)
Also fix some path munging that I somehow broke in the process of writing tests :-/
1 parent 04d3b92 commit 172994e

File tree

5 files changed

+103
-20
lines changed

5 files changed

+103
-20
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,26 @@ bootkube start --asset-dir=my-cluster
5858

5959
### Recover a downed cluster
6060

61-
In the case of a partial or total control plane outage (i.e. due to lost master nodes) an experimental `recover` command can extract and write manifests from a backup location. These manifests can then be used by the `start` command to reboot the cluster. Currently recovery from an external running etcd cluster is the only supported method.
61+
In the case of a partial or total control plane outage (i.e. due to lost master nodes) an experimental `recover` command can extract and write manifests from a backup location. These manifests can then be used by the `start` command to reboot the cluster. Currently recovery from a running apiserver or external running etcd cluster are the only supported methods.
6262

6363
To see available options, run:
6464

6565
```
6666
bootkube recover --help
6767
```
6868

69-
Example:
69+
Recover from an external running etcd cluster:
7070

7171
```
7272
bootkube recover --asset-dir=recovered --etcd-servers=http://127.0.0.1:2379 --kubeconfig=/etc/kubernetes/kubeconfig
7373
```
7474

75+
Recover from a running apiserver (i.e. if the scheduler pods are all down):
76+
77+
```
78+
bootkube recover --asset-dir=recovered --kubeconfig=/etc/kubernetes/kubeconfig
79+
```
80+
7581
For a complete recovery example please see the [hack/multi-node/bootkube-test-recovery](hack/multi-node/bootkube-test-recovery) script.
7682

7783
## Building

cmd/bootkube/recover.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"time"
1313

14+
"github.com/kubernetes-incubator/bootkube/pkg/bootkube"
1415
"github.com/kubernetes-incubator/bootkube/pkg/recovery"
1516

1617
"github.com/coreos/etcd/clientv3"
@@ -20,8 +21,8 @@ import (
2021
var (
2122
cmdRecover = &cobra.Command{
2223
Use: "recover",
23-
Short: "Recover a control plane from state stored in etcd.",
24-
Long: "",
24+
Short: "Recover a self-hosted control plane",
25+
Long: "This command reads control plane manifests from a running apiserver or etcd and writes them to asset-dir. Users can then use `bootkube start` pointed at this asset-dir to re-the a self-hosted cluster. Please see the project README for more details and examples.",
2526
PreRunE: validateRecoverOpts,
2627
RunE: runCmdRecover,
2728
SilenceUsage: true,
@@ -44,7 +45,7 @@ func init() {
4445
cmdRecover.Flags().StringVar(&recoverOpts.etcdCAPath, "etcd-ca-path", "", "Path to an existing PEM encoded CA that will be used for TLS-enabled communication between the apiserver and etcd. Must be used in conjunction with --etcd-certificate-path and --etcd-private-key-path, and must have etcd configured to use TLS with matching secrets.")
4546
cmdRecover.Flags().StringVar(&recoverOpts.etcdCertificatePath, "etcd-certificate-path", "", "Path to an existing certificate that will be used for TLS-enabled communication between the apiserver and etcd. Must be used in conjunction with --etcd-ca-path and --etcd-private-key-path, and must have etcd configured to use TLS with matching secrets.")
4647
cmdRecover.Flags().StringVar(&recoverOpts.etcdPrivateKeyPath, "etcd-private-key-path", "", "Path to an existing private key that will be used for TLS-enabled communication between the apiserver and etcd. Must be used in conjunction with --etcd-ca-path and --etcd-certificate-path, and must have etcd configured to use TLS with matching secrets.")
47-
cmdRecover.Flags().StringVar(&recoverOpts.etcdServers, "etcd-servers", "", "List of etcd servers URLs including host:port, comma separated.")
48+
cmdRecover.Flags().StringVar(&recoverOpts.etcdServers, "etcd-servers", "", "List of etcd server URLs including host:port, comma separated.")
4849
cmdRecover.Flags().StringVar(&recoverOpts.etcdPrefix, "etcd-prefix", "/registry", "Path prefix to Kubernetes cluster data in etcd.")
4950
cmdRecover.Flags().StringVar(&recoverOpts.kubeConfigPath, "kubeconfig", "", "Path to kubeconfig for communicating with the cluster.")
5051
}
@@ -55,11 +56,25 @@ func runCmdRecover(cmd *cobra.Command, args []string) error {
5556
if err != nil {
5657
return err
5758
}
58-
etcdClient, err := createEtcdClient()
59-
if err != nil {
60-
return err
59+
60+
var backend recovery.Backend
61+
switch {
62+
case recoverOpts.etcdServers != "":
63+
bootkube.UserOutput("Attempting recovery using etcd cluster at %q...\n", recoverOpts.etcdServers)
64+
etcdClient, err := createEtcdClient()
65+
if err != nil {
66+
return err
67+
}
68+
backend = recovery.NewEtcdBackend(etcdClient, recoverOpts.etcdPrefix)
69+
default:
70+
bootkube.UserOutput("Attempting recovery using apiserver at %q...\n", recoverOpts.kubeConfigPath)
71+
backend, err = recovery.NewAPIServerBackend(recoverOpts.kubeConfigPath)
72+
if err != nil {
73+
return err
74+
}
6175
}
62-
as, err := recovery.Recover(context.Background(), recovery.NewEtcdBackend(etcdClient, recoverOpts.etcdPrefix), recoverOpts.kubeConfigPath)
76+
77+
as, err := recovery.Recover(context.Background(), backend, recoverOpts.kubeConfigPath)
6378
if err != nil {
6479
return err
6580
}

pkg/recovery/apiserver.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package recovery
2+
3+
import (
4+
"context"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
"k8s.io/client-go/kubernetes"
8+
"k8s.io/client-go/pkg/api"
9+
"k8s.io/client-go/tools/clientcmd"
10+
)
11+
12+
type apiServerBackend struct {
13+
client *kubernetes.Clientset
14+
}
15+
16+
// NewAPIServerBackend constructs a new backend to talk to the API server using the given
17+
// kubeConfig.
18+
//
19+
// TODO(diegs): support using a service account instead of a kubeconfig.
20+
func NewAPIServerBackend(kubeConfigPath string) (Backend, error) {
21+
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
22+
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeConfigPath},
23+
&clientcmd.ConfigOverrides{})
24+
config, err := kubeConfig.ClientConfig()
25+
if err != nil {
26+
return nil, err
27+
}
28+
client, err := kubernetes.NewForConfig(config)
29+
if err != nil {
30+
return nil, err
31+
}
32+
return &apiServerBackend{
33+
client: client,
34+
}, nil
35+
}
36+
37+
// read implements Backend.read().
38+
func (b *apiServerBackend) read(context.Context) (*controlPlane, error) {
39+
cp := &controlPlane{}
40+
configMaps, err := b.client.CoreV1().ConfigMaps(api.NamespaceSystem).List(metav1.ListOptions{})
41+
if err != nil {
42+
return nil, err
43+
}
44+
cp.configMaps = *configMaps
45+
deployments, err := b.client.ExtensionsV1beta1().Deployments(api.NamespaceSystem).List(metav1.ListOptions{})
46+
if err != nil {
47+
return nil, err
48+
}
49+
cp.deployments = *deployments
50+
daemonSets, err := b.client.ExtensionsV1beta1().DaemonSets(api.NamespaceSystem).List(metav1.ListOptions{})
51+
if err != nil {
52+
return nil, err
53+
}
54+
cp.daemonSets = *daemonSets
55+
secrets, err := b.client.CoreV1().Secrets(api.NamespaceSystem).List(metav1.ListOptions{})
56+
if err != nil {
57+
return nil, err
58+
}
59+
cp.secrets = *secrets
60+
return cp, nil
61+
}

pkg/recovery/recover.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"fmt"
1414
"io/ioutil"
1515
"path"
16+
"path/filepath"
1617
"reflect"
1718

1819
"github.com/ghodss/yaml"
@@ -29,7 +30,7 @@ import (
2930
const (
3031
k8sAppLabel = "k8s-app" // The label used in versions > v0.4.2
3132
componentAppLabel = "component" // The label used in versions <= v0.4.2
32-
kubeletKubeConfigPath = "/etc/kubernetes/kubeconfig"
33+
kubeletKubeConfigPath = "/etc/kubernetes"
3334
)
3435

3536
var (
@@ -190,14 +191,14 @@ func fixUpBootstrapPods(pods []v1.Pod) (requiredConfigMaps, requiredSecrets map[
190191
for i := range pod.Spec.Volumes {
191192
vol := &pod.Spec.Volumes[i]
192193
if vol.Secret != nil {
193-
pathPrefix := path.Join(asset.BootstrapSecretsDir, "secrets", vol.Secret.SecretName)
194-
requiredSecrets[vol.Secret.SecretName] = pathPrefix
195-
vol.HostPath = &v1.HostPathVolumeSource{Path: pathPrefix}
194+
pathSuffix := filepath.Join("secrets", vol.Secret.SecretName)
195+
requiredSecrets[vol.Secret.SecretName] = filepath.Join(asset.AssetPathSecrets, pathSuffix)
196+
vol.HostPath = &v1.HostPathVolumeSource{Path: filepath.Join(asset.BootstrapSecretsDir, pathSuffix)}
196197
vol.Secret = nil
197198
} else if vol.ConfigMap != nil {
198-
pathPrefix := path.Join(asset.BootstrapSecretsDir, "config-maps", vol.ConfigMap.Name)
199-
requiredConfigMaps[vol.ConfigMap.Name] = pathPrefix
200-
vol.HostPath = &v1.HostPathVolumeSource{Path: pathPrefix}
199+
pathSuffix := filepath.Join("config-maps", vol.ConfigMap.Name)
200+
requiredConfigMaps[vol.ConfigMap.Name] = filepath.Join(asset.AssetPathSecrets, pathSuffix)
201+
vol.HostPath = &v1.HostPathVolumeSource{Path: path.Join(asset.BootstrapSecretsDir, pathSuffix)}
201202
vol.ConfigMap = nil
202203
}
203204
}

pkg/recovery/recover_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ func TestFixUpBootstrapPods(t *testing.T) {
253253
VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/etc/kubernetes/bootstrap-secrets/secrets/kube-apiserver"}},
254254
}, {
255255
Name: "kubeconfig",
256-
VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/etc/kubernetes/kubeconfig"}},
256+
VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/etc/kubernetes"}},
257257
}},
258258
},
259259
}, {
@@ -278,12 +278,12 @@ func TestFixUpBootstrapPods(t *testing.T) {
278278
}},
279279
Volumes: []v1.Volume{{
280280
Name: "kubeconfig",
281-
VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/etc/kubernetes/kubeconfig"}},
281+
VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/etc/kubernetes"}},
282282
}},
283283
},
284284
}}
285-
wantConfigMaps := map[string]string{"kube-apiserver": "/etc/kubernetes/bootstrap-secrets/config-maps/kube-apiserver"}
286-
wantSecrets := map[string]string{"kube-apiserver": "/etc/kubernetes/bootstrap-secrets/secrets/kube-apiserver"}
285+
wantConfigMaps := map[string]string{"kube-apiserver": "tls/config-maps/kube-apiserver"}
286+
wantSecrets := map[string]string{"kube-apiserver": "tls/secrets/kube-apiserver"}
287287
gotConfigMaps, gotSecrets := fixUpBootstrapPods(pods)
288288
if !reflect.DeepEqual(gotSecrets, wantSecrets) || !reflect.DeepEqual(gotConfigMaps, wantConfigMaps) {
289289
t.Errorf("fixUpBootstrapPods(%v) = %v, %v, want: %v, %v", pods, gotConfigMaps, gotSecrets, wantConfigMaps, wantSecrets)

0 commit comments

Comments
 (0)