Skip to content

Commit c1c0c34

Browse files
committed
feature: cluster configuration backup
Signed-off-by: Maxim Vasilenko <[email protected]>
1 parent 7bad692 commit c1c0c34

File tree

10 files changed

+410
-32
lines changed

10 files changed

+410
-32
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package blacklist
2+
3+
import (
4+
_ "embed"
5+
6+
"github.com/samber/lo"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
"sigs.k8s.io/yaml"
10+
)
11+
12+
type Item struct {
13+
metav1.TypeMeta
14+
metav1.ObjectMeta // Only Name and Namespace are taken into account
15+
}
16+
17+
//go:embed blacklist.yml
18+
var rawBlacklist []byte
19+
var list []Item
20+
21+
func init() {
22+
if err := yaml.Unmarshal(rawBlacklist, &list); err != nil {
23+
panic(err)
24+
}
25+
}
26+
27+
func List() []Item {
28+
return list
29+
}
30+
31+
func Matches(obj unstructured.Unstructured) bool {
32+
_, foundInBlacklist := lo.Find(list, func(item Item) bool {
33+
return obj.GetName() == item.Name &&
34+
obj.GetKind() == item.Kind &&
35+
obj.GetAPIVersion() == item.APIVersion &&
36+
obj.GetNamespace() == item.Namespace
37+
})
38+
return foundInBlacklist
39+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
- apiVersion: deckhouse.io/v1
3+
kind: NodeGroup
4+
name: master
5+
namespace: namespaceName
6+
- apiVersion: deckhouse.io/v1
7+
kind: StaticInstance
8+
name: master-0
9+
namespace: test
10+
- apiVersion: deckhouse.io/v1
11+
kind: StaticInstance
12+
name: master-1
13+
namespace: namespaceName
14+
- apiVersion: deckhouse.io/v1
15+
kind: StaticInstance
16+
name: master-2
17+
namespace: namespaceName
18+
19+
---
20+
namespaceName:
21+
deckhouse.io/v1:
22+
StaticInstance:
23+
- master-0
24+
- master-1
25+
- master-2
26+
NodeGroup:
27+
- master
28+

internal/backup/cmd/backup.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/spf13/cobra"
2121
"k8s.io/kubectl/pkg/util/templates"
2222

23+
"github.com/deckhouse/deckhouse-cli/internal/backup/cmd/cluster-config"
2324
"github.com/deckhouse/deckhouse-cli/internal/backup/cmd/etcd"
2425
)
2526

@@ -35,8 +36,11 @@ func NewCommand() *cobra.Command {
3536
Long: backupLong,
3637
}
3738

39+
addPersistentFlags(backupCmd.PersistentFlags())
40+
3841
backupCmd.AddCommand(
3942
etcd.NewCommand(),
43+
cluster_config.NewCommand(),
4044
)
4145

4246
return backupCmd
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package cluster_config
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"os"
8+
"reflect"
9+
"runtime"
10+
"strings"
11+
12+
"github.com/samber/lo"
13+
"github.com/samber/lo/parallel"
14+
"github.com/spf13/cobra"
15+
corev1 "k8s.io/api/core/v1"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18+
"k8s.io/apimachinery/pkg/runtime/schema"
19+
"k8s.io/client-go/dynamic"
20+
"k8s.io/client-go/kubernetes"
21+
"k8s.io/kubectl/pkg/util/templates"
22+
23+
"github.com/deckhouse/deckhouse-cli/internal/backup/k8s"
24+
"github.com/deckhouse/deckhouse-cli/internal/backup/tarball"
25+
)
26+
27+
// TODO texts
28+
var clusterConfigLong = templates.LongDesc(`
29+
Take a snapshot of cluster configuration.
30+
31+
This command creates a snapshot various resources .
32+
33+
© Flant JSC 2024`)
34+
35+
func NewCommand() *cobra.Command {
36+
etcdCmd := &cobra.Command{
37+
Use: "cluster-config <backup-tarball-path>",
38+
Short: "Take a snapshot of cluster configuration",
39+
Long: clusterConfigLong,
40+
ValidArgs: []string{"backup-tarball-path"},
41+
SilenceErrors: true,
42+
SilenceUsage: true,
43+
// PreRunE: func(cmd *cobra.Command, args []string) error {
44+
// return validateFlags()
45+
// },
46+
RunE: backupConfig,
47+
}
48+
49+
addFlags(etcdCmd.Flags())
50+
return etcdCmd
51+
}
52+
53+
type BackupFunc func(
54+
kubeCl kubernetes.Interface,
55+
dynamicCl dynamic.Interface,
56+
namespaces []string,
57+
) ([]unstructured.Unstructured, error)
58+
59+
func backupConfig(cmd *cobra.Command, args []string) error {
60+
if len(args) != 1 {
61+
return fmt.Errorf("This command requires exactly 1 argument")
62+
}
63+
64+
// TODO move this to real file when done
65+
tarFile, err := os.CreateTemp(".", ".*.d8bkp")
66+
if err != nil {
67+
return fmt.Errorf("failed to create temp file: %v", err)
68+
}
69+
defer func(fileName string) {
70+
_ = os.Remove(fileName)
71+
}(tarFile.Name())
72+
73+
backup := tarball.NewBackup(tarFile)
74+
kubeCl, dynamicCl, err := setupK8sClients(cmd)
75+
if err != nil {
76+
return fmt.Errorf("setup k8s clients: %w", err)
77+
}
78+
79+
namespaceList, err := kubeCl.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
80+
if err != nil {
81+
return fmt.Errorf("Failed to list namespaces: %w", err)
82+
}
83+
namespaces := lo.Map(namespaceList.Items, func(ns corev1.Namespace, _ int) string {
84+
return ns.Name
85+
})
86+
87+
// TODO move this to separate packages
88+
backups := []BackupFunc{
89+
backupSecrets,
90+
backupConfigMaps,
91+
}
92+
93+
parallel.ForEach(backups, func(bf BackupFunc, _ int) {
94+
thisFuncName := runtime.FuncForPC(reflect.ValueOf(bf).Pointer()).Name()
95+
resources, err := bf(kubeCl, dynamicCl, namespaces)
96+
if err != nil {
97+
log.Fatalf("%s failed: %v", thisFuncName, err)
98+
}
99+
100+
if err = backup.PutResources(resources); err != nil {
101+
log.Fatalf("%s failed: %v", thisFuncName, err)
102+
}
103+
})
104+
105+
if err = backup.Close(); err != nil {
106+
return fmt.Errorf("close tarball failed: %w", err)
107+
}
108+
if err = tarFile.Sync(); err != nil {
109+
return fmt.Errorf("tarball flush failed: %w", err)
110+
}
111+
if err = tarFile.Close(); err != nil {
112+
return fmt.Errorf("tarball close failed: %w", err)
113+
}
114+
115+
if err = os.Rename(tarFile.Name(), args[0]); err != nil {
116+
return fmt.Errorf("write tarball failed: %w", err)
117+
}
118+
119+
return nil
120+
}
121+
122+
func setupK8sClients(cmd *cobra.Command) (*kubernetes.Clientset, *dynamic.DynamicClient, error) {
123+
kubeconfigPath, err := cmd.Flags().GetString("kubeconfig")
124+
if err != nil {
125+
return nil, nil, fmt.Errorf("Failed to setup Kubernetes client: %w", err)
126+
}
127+
128+
_, kubeCl, err := k8s.SetupK8sClientSet(kubeconfigPath)
129+
if err != nil {
130+
return nil, nil, fmt.Errorf("Failed to setup Kubernetes client: %w", err)
131+
}
132+
133+
dynamicCl := k8s.SetupDynamicClientFromK8sClientset(kubeCl.RESTClient())
134+
return kubeCl, dynamicCl, nil
135+
}
136+
137+
func backupSecrets(
138+
_ kubernetes.Interface,
139+
dynamicCl dynamic.Interface,
140+
namespaces []string,
141+
) ([]unstructured.Unstructured, error) {
142+
namespaces = lo.Filter(namespaces, func(item string, _ int) bool {
143+
return strings.HasPrefix(item, "d8-") || strings.HasPrefix(item, "kube-")
144+
})
145+
146+
secrets := parallel.Map(namespaces, func(namespace string, index int) []unstructured.Unstructured {
147+
gvr := schema.GroupVersionResource{
148+
Group: corev1.SchemeGroupVersion.Group,
149+
Version: corev1.SchemeGroupVersion.Version,
150+
Resource: "secrets",
151+
}
152+
153+
list, err := dynamicCl.Resource(gvr).Namespace(namespace).List(context.TODO(), metav1.ListOptions{})
154+
if err != nil {
155+
log.Fatalf("Failed to list secrets from : %v", err)
156+
}
157+
158+
return list.Items
159+
})
160+
161+
return lo.Flatten(secrets), nil
162+
}
163+
164+
func backupConfigMaps(
165+
_ kubernetes.Interface,
166+
dynamicCl dynamic.Interface,
167+
namespaces []string,
168+
) ([]unstructured.Unstructured, error) {
169+
namespaces = lo.Filter(namespaces, func(item string, _ int) bool {
170+
return strings.HasPrefix(item, "d8-") || strings.HasPrefix(item, "kube-")
171+
})
172+
173+
configmaps := parallel.Map(namespaces, func(namespace string, _ int) []unstructured.Unstructured {
174+
gvr := schema.GroupVersionResource{
175+
Group: corev1.SchemeGroupVersion.Group,
176+
Version: corev1.SchemeGroupVersion.Version,
177+
Resource: "configmaps",
178+
}
179+
180+
list, err := dynamicCl.Resource(gvr).Namespace(namespace).List(context.TODO(), metav1.ListOptions{})
181+
if err != nil {
182+
log.Fatalf("Failed to list configmaps from : %v", err)
183+
}
184+
185+
return list.Items
186+
})
187+
188+
return lo.Flatten(configmaps), nil
189+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package cluster_config
2+
3+
import "github.com/spf13/pflag"
4+
5+
func addFlags(flagSet *pflag.FlagSet) {
6+
7+
}

internal/backup/cmd/etcd/etcd.go

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ import (
3434
"k8s.io/client-go/kubernetes"
3535
_ "k8s.io/client-go/plugin/pkg/client/auth"
3636
"k8s.io/client-go/rest"
37-
"k8s.io/client-go/tools/clientcmd"
3837
"k8s.io/client-go/tools/remotecommand"
3938
"k8s.io/kubectl/pkg/util/templates"
39+
40+
"github.com/deckhouse/deckhouse-cli/internal/backup/k8s"
4041
)
4142

4243
var etcdLong = templates.LongDesc(`
@@ -55,7 +56,7 @@ func NewCommand() *cobra.Command {
5556
SilenceErrors: true,
5657
SilenceUsage: true,
5758
PreRunE: func(cmd *cobra.Command, args []string) error {
58-
return validateFlags()
59+
return validateFlags(cmd)
5960
},
6061
RunE: etcd,
6162
}
@@ -72,19 +73,23 @@ const (
7273
)
7374

7475
var (
75-
kubeconfigPath string
7676
requestedEtcdPodName string
7777

7878
verboseLog bool
7979
)
8080

81-
func etcd(_ *cobra.Command, args []string) error {
81+
func etcd(cmd *cobra.Command, args []string) error {
8282
log.SetFlags(log.LstdFlags)
8383
if len(args) != 1 {
8484
return fmt.Errorf("This command requires exactly 1 argument")
8585
}
8686

87-
config, kubeCl, err := setupK8sClientset(kubeconfigPath)
87+
kubeconfigPath, err := cmd.Flags().GetString("kubeconfig")
88+
if err != nil {
89+
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
90+
}
91+
92+
config, kubeCl, err := k8s.SetupK8sClientSet(kubeconfigPath)
8893
if err != nil {
8994
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
9095
}
@@ -234,21 +239,6 @@ func streamCommand(
234239
return nil
235240
}
236241

237-
func setupK8sClientset(kubeconfigPath string) (*rest.Config, *kubernetes.Clientset, error) {
238-
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
239-
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, nil).ClientConfig()
240-
if err != nil {
241-
return nil, nil, fmt.Errorf("Reading kubeconfig file: %w", err)
242-
}
243-
244-
kubeCl, err := kubernetes.NewForConfig(config)
245-
if err != nil {
246-
return nil, nil, fmt.Errorf("Constructing Kubernetes clientset: %w", err)
247-
}
248-
249-
return config, kubeCl, nil
250-
}
251-
252242
func findETCDPods(kubeCl kubernetes.Interface) ([]string, error) {
253243
if requestedEtcdPodName != "" {
254244
if err := checkEtcdPodExistsAndReady(kubeCl, requestedEtcdPodName); err != nil {

internal/backup/cmd/etcd/flags.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,11 @@ import (
2020
"fmt"
2121
"os"
2222

23+
"github.com/spf13/cobra"
2324
"github.com/spf13/pflag"
2425
)
2526

2627
func addFlags(flagSet *pflag.FlagSet) {
27-
defaultKubeconfigPath := os.ExpandEnv("$HOME/.kube/config")
28-
if p := os.Getenv("KUBECONFIG"); p != "" {
29-
defaultKubeconfigPath = p
30-
}
31-
32-
flagSet.StringVarP(
33-
&kubeconfigPath,
34-
"kubeconfig", "k",
35-
defaultKubeconfigPath,
36-
"KubeConfig of the cluster. (default is $KUBECONFIG when it is set, $HOME/.kube/config otherwise)",
37-
)
3828
flagSet.StringVarP(
3929
&requestedEtcdPodName,
4030
"etcd-pod", "p",
@@ -49,7 +39,12 @@ func addFlags(flagSet *pflag.FlagSet) {
4939
)
5040
}
5141

52-
func validateFlags() error {
42+
func validateFlags(cmd *cobra.Command) error {
43+
kubeconfigPath, err := cmd.Flags().GetString("kubeconfig")
44+
if err != nil {
45+
return fmt.Errorf("Failed to setup Kubernetes client: %w", err)
46+
}
47+
5348
stats, err := os.Stat(kubeconfigPath)
5449
if err != nil {
5550
return fmt.Errorf("Invalid --kubeconfig: %w", err)

0 commit comments

Comments
 (0)