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

Implement backup and restore for Cluster Role Bindings and OIDC #591

Merged
merged 20 commits into from
Jan 9, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions hack/runtime-migrator/cmd/backup/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,35 @@ import (
"github.com/kyma-project/infrastructure-manager/pkg/gardener/kubeconfig"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"log/slog"
"sigs.k8s.io/controller-runtime/pkg/client"
"time"
)

const (
timeoutK8sOperation = 20 * time.Second
expirationTime = 60 * time.Minute
)

type Backup struct {
shootClient gardener_types.ShootInterface
kubeconfigProvider kubeconfig.Provider
kcpClient client.Client
outputWriter backup.OutputWriter
results backup.Results
cfg initialisation.Config
}

func NewBackup(cfg initialisation.Config, kubeconfigProvider kubeconfig.Provider, shootClient gardener_types.ShootInterface) (Backup, error) {
func NewBackup(cfg initialisation.Config, kcpClient client.Client, shootClient gardener_types.ShootInterface) (Backup, error) {
outputWriter, err := backup.NewOutputWriter(cfg.OutputPath)
if err != nil {
return Backup{}, err
}

return Backup{
shootClient: shootClient,
kubeconfigProvider: kubeconfigProvider,
outputWriter: outputWriter,
results: backup.NewBackupResults(outputWriter.NewResultsDir),
cfg: cfg,
shootClient: shootClient,
kcpClient: kcpClient,
outputWriter: outputWriter,
results: backup.NewBackupResults(outputWriter.NewResultsDir),
cfg: cfg,
}, nil
}

Expand All @@ -50,7 +51,7 @@ func (b Backup) Do(ctx context.Context, runtimeIDs []string) error {
return err
}

backuper := backup.NewBackuper(b.cfg.IsDryRun, b.kubeconfigProvider)
backuper := backup.NewBackuper(b.cfg.IsDryRun, b.kcpClient, timeoutK8sOperation)

for _, runtimeID := range runtimeIDs {
shootToBackup, err := shoot.Fetch(ctx, shootList, b.shootClient, runtimeID)
Expand All @@ -69,7 +70,7 @@ func (b Backup) Do(ctx context.Context, runtimeIDs []string) error {
continue
}

runtimeBackup, err := backuper.Do(ctx, *shootToBackup)
runtimeBackup, err := backuper.Do(ctx, *shootToBackup, runtimeID)
if err != nil {
errMsg := fmt.Sprintf("Failed to backup runtime: %v", err)
b.results.ErrorOccurred(runtimeID, shootToBackup.Name, errMsg)
Expand All @@ -80,6 +81,7 @@ func (b Backup) Do(ctx context.Context, runtimeIDs []string) error {

if b.cfg.IsDryRun {
slog.Info("Runtime processed successfully (dry-run)", "runtimeID", runtimeID)
b.results.OperationSucceeded(runtimeID, shootToBackup.Name)

continue
}
Expand All @@ -92,7 +94,7 @@ func (b Backup) Do(ctx context.Context, runtimeIDs []string) error {
continue
}

slog.Info("Runtime backup created successfully successfully", "runtimeID", runtimeID)
slog.Info("Runtime backup created successfully", "runtimeID", runtimeID)
b.results.OperationSucceeded(runtimeID, shootToBackup.Name)
}

Expand Down
10 changes: 5 additions & 5 deletions hack/runtime-migrator/cmd/backup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ func main() {

gardenerNamespace := fmt.Sprintf("garden-%s", cfg.GardenerProjectName)

kubeconfigProvider, err := initialisation.SetupKubernetesKubeconfigProvider(cfg.GardenerKubeconfigPath, gardenerNamespace, expirationTime)
shootClient, _, err := initialisation.SetupGardenerShootClients(cfg.GardenerKubeconfigPath, gardenerNamespace)
if err != nil {
slog.Error(fmt.Sprintf("Failed to create kubeconfig provider: %v", err))
slog.Error("Failed to setup Gardener shoot client", slog.Any("error", err))
os.Exit(1)
}

shootClient, _, err := initialisation.SetupGardenerShootClients(cfg.GardenerKubeconfigPath, gardenerNamespace)
kcpClient, err := initialisation.CreateKcpClient(&cfg)
if err != nil {
slog.Error("Failed to setup Gardener shoot client", slog.Any("error", err))
slog.Error("Failed to create kcp client", slog.Any("error", err))
os.Exit(1)
}

backup, err := NewBackup(cfg, kubeconfigProvider, shootClient)
backup, err := NewBackup(cfg, kcpClient, shootClient)
if err != nil {
slog.Error("Failed to initialize backup", slog.Any("error", err))
os.Exit(1)
Expand Down
6 changes: 3 additions & 3 deletions hack/runtime-migrator/cmd/restore/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ func main() {

gardenerNamespace := fmt.Sprintf("garden-%s", cfg.GardenerProjectName)

kubeconfigProvider, err := initialisation.SetupKubernetesKubeconfigProvider(cfg.GardenerKubeconfigPath, gardenerNamespace, expirationTime)
_, err := initialisation.SetupKubernetesKubeconfigProvider(cfg.GardenerKubeconfigPath, gardenerNamespace, expirationTime)
if err != nil {
slog.Error(fmt.Sprintf("Failed to create kubeconfig provider: %v", err))
os.Exit(1)
}

_, err = initialisation.CreateKcpClient(&cfg.Config)
kcpClient, err := initialisation.CreateKcpClient(&cfg.Config)
if err != nil {
slog.Error("Failed to create kcp client", slog.Any("error", err))
os.Exit(1)
Expand All @@ -42,7 +42,7 @@ func main() {
os.Exit(1)
}

restore, err := NewRestore(cfg, kubeconfigProvider, shootClient, dynamicGardenerClient)
restore, err := NewRestore(cfg, kcpClient, shootClient, dynamicGardenerClient)
if err != nil {
slog.Error("Failed to setup Gardener shoot client", slog.Any("error", err))
os.Exit(1)
Expand Down
146 changes: 132 additions & 14 deletions hack/runtime-migrator/cmd/restore/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import (
"fmt"
"github.com/gardener/gardener/pkg/apis/core/v1beta1"
gardener_types "github.com/gardener/gardener/pkg/client/core/clientset/versioned/typed/core/v1beta1"
authenticationv1alpha1 "github.com/gardener/oidc-webhook-authenticator/apis/authentication/v1alpha1"
"github.com/kyma-project/infrastructure-manager/hack/runtime-migrator-app/internal/backup"
"github.com/kyma-project/infrastructure-manager/hack/runtime-migrator-app/internal/initialisation"
"github.com/kyma-project/infrastructure-manager/hack/runtime-migrator-app/internal/restore"
"github.com/kyma-project/infrastructure-manager/hack/runtime-migrator-app/internal/shoot"
"github.com/kyma-project/infrastructure-manager/pkg/gardener/kubeconfig"
v12 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"log/slog"
Expand All @@ -24,15 +27,15 @@ const (
type Restore struct {
shootClient gardener_types.ShootInterface
dynamicGardenerClient client.Client
kubeconfigProvider kubeconfig.Provider
kcpClient client.Client
outputWriter restore.OutputWriter
results restore.Results
cfg initialisation.RestoreConfig
}

const fieldManagerName = "kim"
const fieldManagerName = "kim-restore"

func NewRestore(cfg initialisation.RestoreConfig, kubeconfigProvider kubeconfig.Provider, shootClient gardener_types.ShootInterface, dynamicGardenerClient client.Client) (Restore, error) {
func NewRestore(cfg initialisation.RestoreConfig, kcpClient client.Client, shootClient gardener_types.ShootInterface, dynamicGardenerClient client.Client) (Restore, error) {
outputWriter, err := restore.NewOutputWriter(cfg.OutputPath)
if err != nil {
return Restore{}, err
Expand All @@ -41,7 +44,7 @@ func NewRestore(cfg initialisation.RestoreConfig, kubeconfigProvider kubeconfig.
return Restore{
shootClient: shootClient,
dynamicGardenerClient: dynamicGardenerClient,
kubeconfigProvider: kubeconfigProvider,
kcpClient: kcpClient,
outputWriter: outputWriter,
results: restore.NewRestoreResults(outputWriter.NewResultsDir),
cfg: cfg,
Expand All @@ -57,7 +60,7 @@ func (r Restore) Do(ctx context.Context, runtimeIDs []string) error {
return err
}

restorer := restore.NewRestorer(r.cfg.BackupDir)
restorer := restore.NewBackupReader(r.cfg.BackupDir, r.cfg.RestoreCRB, r.cfg.RestoreOIDC)

for _, runtimeID := range runtimeIDs {
currentShoot, err := shoot.Fetch(ctx, shootList, r.shootClient, runtimeID)
Expand All @@ -77,33 +80,40 @@ func (r Restore) Do(ctx context.Context, runtimeIDs []string) error {
continue
}

shootToRestore, err := restorer.Do(runtimeID, currentShoot.Name)
objectsToRestore, err := restorer.Do(runtimeID, currentShoot.Name)
if err != nil {
errMsg := fmt.Sprintf("Failed to restore runtime: %v", err)
errMsg := fmt.Sprintf("Failed to read runtime from backup directory: %v", err)
r.results.ErrorOccurred(runtimeID, currentShoot.Name, errMsg)
slog.Error(errMsg, "runtimeID", runtimeID)

continue
}

if currentShoot.Generation > objectsToRestore.OriginalShoot.Generation+1 {
slog.Warn("Verify the current state of the system. Restore should be performed manually, as the backup may overwrite user's changes.", "runtimeID", runtimeID)
r.results.AutomaticRestoreImpossible(runtimeID, currentShoot.Name)

continue
}

if r.cfg.IsDryRun {
slog.Info("Runtime processed successfully (dry-run)", "runtimeID", runtimeID)
r.results.OperationSucceeded(runtimeID, currentShoot.Name)
r.results.OperationSucceeded(runtimeID, currentShoot.Name, nil, nil)

continue
}

err = r.applyResources(ctx, shootToRestore)
appliedCRBs, appliedOIDC, err := r.applyResources(ctx, objectsToRestore, runtimeID)
if err != nil {
errMsg := fmt.Sprintf("Failed to restore runtime: %v", err)
errMsg := fmt.Sprintf("Failed to apply resources: %v", err)
r.results.ErrorOccurred(runtimeID, currentShoot.Name, errMsg)
slog.Error(errMsg, "runtimeID", runtimeID)

continue
}

slog.Info("Runtime restore performed successfully", "runtimeID", runtimeID)
r.results.OperationSucceeded(runtimeID, currentShoot.Name)
r.results.OperationSucceeded(runtimeID, currentShoot.Name, appliedCRBs, appliedOIDC)
}

resultsFile, err := r.outputWriter.SaveRestoreResults(r.results)
Expand All @@ -117,12 +127,120 @@ func (r Restore) Do(ctx context.Context, runtimeIDs []string) error {
return nil
}

func (r Restore) applyResources(ctx context.Context, shootToRestore v1beta1.Shoot) error {
func (r Restore) applyResources(ctx context.Context, objectsToRestore backup.RuntimeBackup, runtimeID string) ([]v12.ClusterRoleBinding, []authenticationv1alpha1.OpenIDConnect, error) {
err := r.applyShoot(ctx, objectsToRestore.ShootForPatch)
if err != nil {
return nil, nil, err
}

clusterClient, err := initialisation.GetRuntimeClient(ctx, r.kcpClient, runtimeID)
if err != nil {
return nil, nil, err
}

appliedCRBs, err := r.applyCRBs(ctx, clusterClient, objectsToRestore.ClusterRoleBindings)
if err != nil {
return nil, nil, err
}

appliedOIDC, err := r.applyOIDC(ctx, clusterClient, objectsToRestore.OIDCConfig)
if err != nil {
return nil, nil, err
}

return appliedCRBs, appliedOIDC, nil
}

func (r Restore) applyShoot(ctx context.Context, shoot v1beta1.Shoot) error {
patchCtx, cancel := context.WithTimeout(ctx, timeoutK8sOperation)
defer cancel()

return r.dynamicGardenerClient.Patch(patchCtx, &shootToRestore, client.Apply, &client.PatchOptions{
return r.dynamicGardenerClient.Patch(patchCtx, &shoot, client.Apply, &client.PatchOptions{
FieldManager: fieldManagerName,
Force: ptr.To(true),
})
}

func (r Restore) applyCRBs(ctx context.Context, clusterClient client.Client, crbs []v12.ClusterRoleBinding) ([]v12.ClusterRoleBinding, error) {
appliedCRBs := make([]v12.ClusterRoleBinding, 0)

for _, crb := range crbs {
key := client.ObjectKey{
Name: crb.Name,
Namespace: crb.Namespace,
}
applied, err := applyCRBIfDoesntExist(ctx, key, &crb, clusterClient)
if err != nil {
return nil, err
}

if applied {
appliedCRBs = append(appliedCRBs, crb)
}
}

return appliedCRBs, nil
}

func (r Restore) applyOIDC(ctx context.Context, clusterClient client.Client, oidcConfigs []authenticationv1alpha1.OpenIDConnect) ([]authenticationv1alpha1.OpenIDConnect, error) {
appliedOIDCs := make([]authenticationv1alpha1.OpenIDConnect, 0)

for _, oidc := range oidcConfigs {
key := client.ObjectKey{
Name: oidc.Name,
Namespace: oidc.Namespace,
}
applied, err := applyOIDCIfDoesntExist(ctx, key, &oidc, clusterClient)
if err != nil {
return nil, err
}

if applied {
appliedOIDCs = append(appliedOIDCs, oidc)
}
}

return appliedOIDCs, nil
}

func applyCRBIfDoesntExist(ctx context.Context, key client.ObjectKey, object *v12.ClusterRoleBinding, clusterClient client.Client) (bool, error) {
getCtx, cancelGet := context.WithTimeout(ctx, timeoutK8sOperation)
defer cancelGet()

var existingObject v12.ClusterRoleBinding

err := clusterClient.Get(getCtx, key, &existingObject, &client.GetOptions{})
if err == nil {
return false, nil
}

if err != nil && !errors.IsNotFound(err) {
return false, err
}

createCtx, cancelCreate := context.WithTimeout(ctx, timeoutK8sOperation)
defer cancelCreate()

return true, clusterClient.Create(createCtx, object, &client.CreateOptions{})
}

func applyOIDCIfDoesntExist(ctx context.Context, key client.ObjectKey, object *authenticationv1alpha1.OpenIDConnect, clusterClient client.Client) (bool, error) {
getCtx, cancelGet := context.WithTimeout(ctx, timeoutK8sOperation)
defer cancelGet()

var existingObject authenticationv1alpha1.OpenIDConnect

err := clusterClient.Get(getCtx, key, &existingObject, &client.GetOptions{})
if err == nil {
return false, nil
}
slog.Error(err.Error())
if err != nil && !errors.IsNotFound(err) {
return false, err
}

createCtx, cancelCreate := context.WithTimeout(ctx, timeoutK8sOperation)
defer cancelCreate()

return true, clusterClient.Create(createCtx, object, &client.CreateOptions{})
}
Loading
Loading