nd002test/on-premise/main/rancher_v2.9.1/source/rancher-2.9.1/pkg/rancher/migrations.go

663 lines
21 KiB
Go

package rancher
import (
"encoding/json"
"fmt"
"strings"
"github.com/mcuadros/go-version"
"github.com/rancher/norman/condition"
v32 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
rkev1 "github.com/rancher/rancher/pkg/apis/rke.cattle.io/v1"
"github.com/rancher/rancher/pkg/auth/tokens"
"github.com/rancher/rancher/pkg/capr"
"github.com/rancher/rancher/pkg/controllers/management/clusterprovisioner"
"github.com/rancher/rancher/pkg/features"
v3 "github.com/rancher/rancher/pkg/generated/controllers/management.cattle.io/v3"
rancherversion "github.com/rancher/rancher/pkg/version"
"github.com/rancher/rancher/pkg/wrangler"
"github.com/rancher/wrangler/v3/pkg/data"
"github.com/rancher/wrangler/v3/pkg/data/convert"
controllerv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
"github.com/rancher/wrangler/v3/pkg/summary"
"github.com/sirupsen/logrus"
"golang.org/x/mod/semver"
v1 "k8s.io/api/core/v1"
k8serror "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/util/retry"
capi "sigs.k8s.io/cluster-api/api/v1beta1"
)
const (
cattleNamespace = "cattle-system"
forceUpgradeLogoutConfig = "forceupgradelogout"
forceLocalSystemAndDefaultProjectCreation = "forcelocalprojectcreation"
forceSystemNamespacesAssignment = "forcesystemnamespaceassignment"
migrateFromMachineToPlanSecret = "migratefrommachinetoplanesecret"
migrateEncryptionKeyRotationLeaderToStatus = "migrateencryptionkeyrotationleadertostatus"
migrateDynamicSchemaToMachinePools = "migratedynamicschematomachinepools"
migrateRKEClusterState = "migraterkeclusterstate"
migrateSystemAgentVarDirToDataDirectory = "migratesystemagentvardirtodatadirectory"
rancherVersionKey = "rancherVersion"
projectsCreatedKey = "projectsCreated"
namespacesAssignedKey = "namespacesAssigned"
capiMigratedKey = "capiMigrated"
encryptionKeyRotationStatusMigratedKey = "encryptionKeyRotationStatusMigrated"
dynamicSchemaMachinePoolsMigratedKey = "dynamicSchemaMachinePoolsMigrated"
rkeClustersAnnotatedForMigrationKey = "rkeClustersAnnotatedForMigration"
systemAgentVarDirMigratedKey = "systemAgentVarDirMigrated"
)
func runMigrations(wranglerContext *wrangler.Context) error {
if err := forceUpgradeLogout(wranglerContext.Core.ConfigMap(), wranglerContext.Mgmt.Token(), "v2.6.0"); err != nil {
return err
}
if err := forceSystemAndDefaultProjectCreation(wranglerContext.Core.ConfigMap(), wranglerContext.Mgmt.Cluster()); err != nil {
return err
}
if features.MCM.Enabled() {
if err := forceSystemNamespaceAssignment(wranglerContext.Core.ConfigMap(), wranglerContext.Mgmt.Project()); err != nil {
return err
}
}
if features.RKE2.Enabled() {
// must migrate system agent data directory first, since update requests will be rejected by webhook if
// "CATTLE_AGENT_VAR_DIR" is set within AgentEnvVars.
if err := migrateSystemAgentDataDirectory(wranglerContext); err != nil {
return err
}
if err := migrateCAPIMachineLabelsAndAnnotationsToPlanSecret(wranglerContext); err != nil {
return err
}
if err := migrateEncryptionKeyRotationLeader(wranglerContext); err != nil {
return err
}
if err := migrateMachinePoolsDynamicSchemaLabel(wranglerContext); err != nil {
return err
}
}
return migrateRKEClusterStates(wranglerContext)
}
func getConfigMap(configMapController controllerv1.ConfigMapController, configMapName string) (*v1.ConfigMap, error) {
cm, err := configMapController.Cache().Get(cattleNamespace, configMapName)
if err != nil && !k8serror.IsNotFound(err) {
return nil, err
}
// if this is the first ever migration initialize the configmap
if cm == nil {
cm = &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: cattleNamespace,
},
Data: make(map[string]string, 1),
}
}
// we do not migrate in development environments
if rancherversion.Version == "dev" {
return nil, nil
}
return cm, nil
}
func createOrUpdateConfigMap(configMapClient controllerv1.ConfigMapClient, cm *v1.ConfigMap) error {
var err error
if cm.ObjectMeta.GetResourceVersion() != "" {
_, err = configMapClient.Update(cm)
} else {
_, err = configMapClient.Create(cm)
}
return err
}
// forceUpgradeLogout will delete all dashboard tokens forcing a logout. This is useful when there is a major frontend
// upgrade and we want all users to be sent to a central point. This function will check for the `forceUpgradeLogoutConfig`
// configuration map and only run if the last migrated version is lower than the given `migrationVersion`.
func forceUpgradeLogout(configMapController controllerv1.ConfigMapController, tokenController v3.TokenController, migrationVersion string) error {
cm, err := getConfigMap(configMapController, forceUpgradeLogoutConfig)
if err != nil || cm == nil {
return err
}
// if no last migration is found we always run force logout
if lastMigration, ok := cm.Data[rancherVersionKey]; ok {
// if a valid sem ver is found we only migrate if the version is less than the target version
if semver.IsValid(lastMigration) && semver.IsValid(rancherversion.Version) && version.Compare(migrationVersion, lastMigration, "<=") {
return nil
}
// if an unknown format is given we migrate any time the current version does not equal the last migration
if lastMigration == rancherversion.Version {
return nil
}
}
logrus.Infof("Detected %s upgrade, forcing logout for all web users", migrationVersion)
// list all tokens that were created for the dashboard
allTokens, err := tokenController.Cache().List(labels.SelectorFromSet(labels.Set{tokens.TokenKindLabel: "session"}))
if err != nil {
logrus.Error("Failed to list tokens for upgrade forced logout")
return err
}
// log out all the dashboard users forcing them to be redirected to the login page
for _, token := range allTokens {
err = tokenController.Delete(token.ObjectMeta.Name, &metav1.DeleteOptions{})
if err != nil && !k8serror.IsNotFound(err) {
logrus.Errorf("Failed to delete token [%s] for upgrade forced logout", token.Name)
}
}
cm.Data[rancherVersionKey] = rancherversion.Version
return createOrUpdateConfigMap(configMapController, cm)
}
// forceSystemAndDefaultProjectCreation will set the corresponding conditions on the local cluster object,
// if it exists, to Unknown. This will force the corresponding controller to check that the projects exist
// and create them, if necessary.
func forceSystemAndDefaultProjectCreation(configMapController controllerv1.ConfigMapController, clusterClient v3.ClusterClient) error {
cm, err := getConfigMap(configMapController, forceLocalSystemAndDefaultProjectCreation)
if err != nil || cm == nil {
return err
}
if cm.Data[projectsCreatedKey] == "true" {
return nil
}
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
localCluster, err := clusterClient.Get("local", metav1.GetOptions{})
if err != nil {
if k8serror.IsNotFound(err) {
return nil
}
return err
}
v32.ClusterConditionDefaultProjectCreated.Unknown(localCluster)
v32.ClusterConditionSystemProjectCreated.Unknown(localCluster)
_, err = clusterClient.Update(localCluster)
return err
}); err != nil {
return err
}
cm.Data[projectsCreatedKey] = "true"
return createOrUpdateConfigMap(configMapController, cm)
}
func forceSystemNamespaceAssignment(configMapController controllerv1.ConfigMapController, projectClient v3.ProjectClient) error {
cm, err := getConfigMap(configMapController, forceSystemNamespacesAssignment)
if err != nil || cm == nil {
return err
}
if cm.Data[namespacesAssignedKey] == rancherversion.Version {
return nil
}
err = applyProjectConditionForNamespaceAssignment("authz.management.cattle.io/system-project=true", v32.ProjectConditionSystemNamespacesAssigned, projectClient)
if err != nil {
return err
}
err = applyProjectConditionForNamespaceAssignment("authz.management.cattle.io/default-project=true", v32.ProjectConditionDefaultNamespacesAssigned, projectClient)
if err != nil {
return err
}
cm.Data[namespacesAssignedKey] = rancherversion.Version
return createOrUpdateConfigMap(configMapController, cm)
}
func applyProjectConditionForNamespaceAssignment(label string, condition condition.Cond, projectClient v3.ProjectClient) error {
projects, err := projectClient.List("", metav1.ListOptions{LabelSelector: label})
if err != nil {
return err
}
for i := range projects.Items {
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
p := &projects.Items[i]
p, err = projectClient.Get(p.Namespace, p.Name, metav1.GetOptions{})
if err != nil {
return err
}
condition.Unknown(p)
_, err = projectClient.Update(p)
return err
}); err != nil {
return err
}
}
return nil
}
// migrateRKEClusterStates sets the `RKEForceUpdate` annotation on all management cluster objects for
// RKE-provisioned clusters.
func migrateRKEClusterStates(w *wrangler.Context) error {
cm, err := getConfigMap(w.Core.ConfigMap(), migrateRKEClusterState)
if err != nil {
return fmt.Errorf("error getting configmap %s: %w", migrateRKEClusterState, err)
} else if cm == nil {
return nil
}
// Check if this migration has already run.
if cm.Data[rkeClustersAnnotatedForMigrationKey] == "true" {
return nil
}
// Collect all RKE clusters that need migration.
mgmtClusters, err := w.Mgmt.Cluster().List(metav1.ListOptions{})
if err != nil {
return fmt.Errorf("error listing management clusters: %w", err)
}
// Mark all RKE clusters for migration.
for _, cluster := range mgmtClusters.Items {
// Skip this cluster if it's not RKE-provisioned.
if cluster.Spec.RancherKubernetesEngineConfig == nil {
continue
}
// Retry the cluster object status update on conflict in case something else is updating it at the same time.
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
c, err := w.Mgmt.Cluster().Get(cluster.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("error getting cluster %s: %w", cluster.Name, err)
}
clusterCopy := c.DeepCopy()
clusterCopy.Annotations[clusterprovisioner.RKEForceUpdate] = "true"
if _, err = w.Mgmt.Cluster().Update(clusterCopy); err != nil {
return fmt.Errorf("error updating cluster %s: %w", cluster.Name, err)
}
return nil
}); err != nil {
logrus.Errorf("error updating annotation for cluster %s: %s", cluster.Name, err)
return err
}
}
cm.Data = map[string]string{
rkeClustersAnnotatedForMigrationKey: "true",
}
// Update the configmap that indicates that this migration is complete.
if err := retry.OnError(retry.DefaultBackoff, func(err error) bool {
// Retry all errors.
return true
}, func() error {
return createOrUpdateConfigMap(w.Core.ConfigMap(), cm)
}); err != nil {
return fmt.Errorf("error updating configmap %s: %w", cm.Name, err)
}
return nil
}
func migrateCAPIMachineLabelsAndAnnotationsToPlanSecret(w *wrangler.Context) error {
cm, err := getConfigMap(w.Core.ConfigMap(), migrateFromMachineToPlanSecret)
if err != nil || cm == nil {
return err
}
if cm.Data[capiMigratedKey] == "true" {
return nil
}
mgmtClusters, err := w.Mgmt.Cluster().List(metav1.ListOptions{})
if err != nil {
return err
}
bootstrapLabelExcludes := map[string]struct{}{
capr.InitNodeMachineIDLabel: {},
capr.InitNodeLabel: {},
}
boostrapAnnotationExcludes := map[string]struct{}{
capr.DrainAnnotation: {},
capr.DrainDoneAnnotation: {},
capr.JoinURLAnnotation: {},
capr.PostDrainAnnotation: {},
capr.PreDrainAnnotation: {},
capr.UnCordonAnnotation: {},
}
for _, mgmtCluster := range mgmtClusters.Items {
provClusters, err := w.Provisioning.Cluster().List(mgmtCluster.Spec.FleetWorkspaceName, metav1.ListOptions{})
if k8serror.IsNotFound(err) || len(provClusters.Items) == 0 {
continue
} else if err != nil {
return err
}
for _, provCluster := range provClusters.Items {
machines, err := w.CAPI.Machine().List(provCluster.Namespace, metav1.ListOptions{LabelSelector: labels.Set{capi.ClusterNameLabel: provCluster.Name}.String()})
if err != nil {
return err
}
otherMachines, err := w.CAPI.Machine().List(provCluster.Namespace, metav1.ListOptions{LabelSelector: labels.Set{capr.ClusterNameLabel: provCluster.Name}.String()})
if err != nil {
return err
}
allMachines := append(machines.Items, otherMachines.Items...)
for _, machine := range allMachines {
if machine.Spec.Bootstrap.ConfigRef == nil || machine.Spec.Bootstrap.ConfigRef.APIVersion != capr.RKEAPIVersion {
continue
}
planSecrets, err := w.Core.Secret().List(machine.Namespace, metav1.ListOptions{LabelSelector: labels.Set{capr.MachineNameLabel: machine.Name}.String()})
if err != nil {
return err
}
if len(planSecrets.Items) == 0 {
continue
}
for _, secret := range planSecrets.Items {
if err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
secret, err := w.Core.Secret().Get(secret.Namespace, secret.Name, metav1.GetOptions{})
if err != nil {
return err
}
secret = secret.DeepCopy()
capr.CopyMap(secret.Labels, machine.Labels)
capr.CopyMap(secret.Annotations, machine.Annotations)
_, err = w.Core.Secret().Update(secret)
return err
}); err != nil {
return err
}
}
if err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
bootstrap, err := w.RKE.RKEBootstrap().Get(machine.Spec.Bootstrap.ConfigRef.Namespace, machine.Spec.Bootstrap.ConfigRef.Name, metav1.GetOptions{})
if err != nil {
return err
}
bootstrap = bootstrap.DeepCopy()
capr.CopyMapWithExcludes(bootstrap.Labels, machine.Labels, bootstrapLabelExcludes)
capr.CopyMapWithExcludes(bootstrap.Annotations, machine.Annotations, boostrapAnnotationExcludes)
if bootstrap.Spec.ClusterName == "" {
// If the bootstrap spec cluster name is blank, we need to update the bootstrap spec to the correct value
// This is to handle old rkebootstrap objects for unmanaged clusters that did not have the spec properly set
if v, ok := bootstrap.Labels[capi.ClusterNameLabel]; ok && v != "" {
bootstrap.Spec.ClusterName = v
}
}
_, err = w.RKE.RKEBootstrap().Update(bootstrap)
return err
}); err != nil {
return err
}
if machine.Spec.InfrastructureRef.APIVersion == capr.RKEAPIVersion || machine.Spec.InfrastructureRef.APIVersion == capr.RKEMachineAPIVersion {
gv, err := schema.ParseGroupVersion(machine.Spec.InfrastructureRef.APIVersion)
if err != nil {
// This error should not occur because RKEAPIVersion and RKEMachineAPIVersion are valid
continue
}
gvk := schema.GroupVersionKind{
Group: gv.Group,
Version: gv.Version,
Kind: machine.Spec.InfrastructureRef.Kind,
}
if err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
infraMachine, err := w.Dynamic.Get(gvk, machine.Spec.InfrastructureRef.Namespace, machine.Spec.InfrastructureRef.Name)
if err != nil {
return err
}
d, err := data.Convert(infraMachine.DeepCopyObject())
if err != nil {
return err
}
if changed, err := insertOrUpdateCondition(d, summary.NewCondition("Ready", "True", "", "")); err != nil {
return err
} else if changed {
_, err = w.Dynamic.UpdateStatus(&unstructured.Unstructured{Object: d})
return err
}
return err
}); err != nil {
return err
}
}
}
}
}
cm.Data[capiMigratedKey] = "true"
return createOrUpdateConfigMap(w.Core.ConfigMap(), cm)
}
func migrateEncryptionKeyRotationLeader(w *wrangler.Context) error {
cm, err := getConfigMap(w.Core.ConfigMap(), migrateEncryptionKeyRotationLeaderToStatus)
if err != nil || cm == nil {
return err
}
if cm.Data[encryptionKeyRotationStatusMigratedKey] == "true" {
return nil
}
mgmtClusters, err := w.Mgmt.Cluster().List(metav1.ListOptions{})
if err != nil {
return err
}
for _, mgmtCluster := range mgmtClusters.Items {
if err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
cp, err := w.RKE.RKEControlPlane().Get(mgmtCluster.Spec.FleetWorkspaceName, mgmtCluster.Name, metav1.GetOptions{})
if k8serror.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
leader := cp.Annotations["rke.cattle.io/encrypt-key-rotation-leader"]
if leader == "" {
return nil
}
cp = cp.DeepCopy()
cp.Status.RotateEncryptionKeysLeader = leader
cp, err = w.RKE.RKEControlPlane().UpdateStatus(cp)
if err != nil {
return err
}
cp = cp.DeepCopy()
delete(cp.Annotations, "rke.cattle.io/encrypt-key-rotation-leader")
cp, err = w.RKE.RKEControlPlane().Update(cp)
if err != nil {
return err
}
return nil
}); err != nil {
return err
}
}
cm.Data[encryptionKeyRotationStatusMigratedKey] = "true"
return createOrUpdateConfigMap(w.Core.ConfigMap(), cm)
}
func migrateMachinePoolsDynamicSchemaLabel(w *wrangler.Context) error {
cm, err := getConfigMap(w.Core.ConfigMap(), migrateDynamicSchemaToMachinePools)
if err != nil || cm == nil {
return err
}
if cm.Data[dynamicSchemaMachinePoolsMigratedKey] == "true" {
return nil
}
mgmtClusters, err := w.Mgmt.Cluster().List(metav1.ListOptions{})
if err != nil {
return err
}
for _, mgmtCluster := range mgmtClusters.Items {
provClusters, err := w.Provisioning.Cluster().List(mgmtCluster.Spec.FleetWorkspaceName, metav1.ListOptions{})
if k8serror.IsNotFound(err) || len(provClusters.Items) == 0 {
continue
} else if err != nil {
return err
}
for _, provCluster := range provClusters.Items {
if provCluster.Spec.RKEConfig == nil {
continue
}
if err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
cluster, err := w.Provisioning.Cluster().Get(provCluster.Namespace, provCluster.Name, metav1.GetOptions{})
if k8serror.IsNotFound(err) {
return nil
} else if err != nil {
return err
}
cluster = cluster.DeepCopy()
// search for machine pools without the `dynamic-schema-spec` annotation and apply it
for i, machinePool := range cluster.Spec.RKEConfig.MachinePools {
var spec v32.DynamicSchemaSpec
if machinePool.DynamicSchemaSpec != "" && json.Unmarshal([]byte(machinePool.DynamicSchemaSpec), &spec) == nil {
continue
}
nodeConfig := machinePool.NodeConfig
if nodeConfig == nil {
return fmt.Errorf("machine pool node config must not be nil")
}
apiVersion := nodeConfig.APIVersion
if apiVersion != capr.DefaultMachineConfigAPIVersion && apiVersion != "" {
continue
}
ds, err := w.Mgmt.DynamicSchema().Get(strings.ToLower(nodeConfig.Kind), metav1.GetOptions{})
if err != nil {
return err
}
specJSON, err := json.Marshal(ds.Spec)
if err != nil {
return err
}
cluster.Spec.RKEConfig.MachinePools[i].DynamicSchemaSpec = string(specJSON)
}
_, err = w.Provisioning.Cluster().Update(cluster)
if err != nil {
return err
}
return nil
}); err != nil {
return err
}
}
}
cm.Data[dynamicSchemaMachinePoolsMigratedKey] = "true"
return createOrUpdateConfigMap(w.Core.ConfigMap(), cm)
}
func migrateSystemAgentDataDirectory(w *wrangler.Context) error {
cm, err := getConfigMap(w.Core.ConfigMap(), migrateSystemAgentVarDirToDataDirectory)
if err != nil || cm == nil {
return err
}
if cm.Data[systemAgentVarDirMigratedKey] == "true" {
return nil
}
provClusters, err := w.Provisioning.Cluster().List("", metav1.ListOptions{})
if err != nil {
return err
}
for _, cluster := range provClusters.Items {
systemAgentDataDir := ""
envVars := make([]rkev1.EnvVar, 0, len(cluster.Spec.AgentEnvVars))
for _, e := range cluster.Spec.AgentEnvVars {
if e.Name == capr.SystemAgentDataDirEnvVar {
// don't break, the webhook allows duplicate entries and the last one would have been the effective data dir
systemAgentDataDir = e.Value
} else {
envVars = append(envVars, e)
}
}
if systemAgentDataDir == "" {
continue
}
cluster = *cluster.DeepCopy()
cluster.Spec.AgentEnvVars = envVars
cluster.Spec.RKEConfig.DataDirectories.SystemAgent = systemAgentDataDir
_, err = w.Provisioning.Cluster().Update(&cluster)
if err != nil {
return err
}
}
cm.Data[systemAgentVarDirMigratedKey] = "true"
return createOrUpdateConfigMap(w.Core.ConfigMap(), cm)
}
func insertOrUpdateCondition(d data.Object, desiredCondition summary.Condition) (bool, error) {
for _, cond := range summary.GetUnstructuredConditions(d) {
if desiredCondition.Equals(cond) {
return false, nil
}
}
// The conditions must be converted to a map so that DeepCopyJSONValue will
// recognize it as a map instead of a data.Object.
newCond, err := convert.EncodeToMap(desiredCondition.Object)
if err != nil {
return false, err
}
dConditions := d.Slice("status", "conditions")
conditions := make([]interface{}, len(dConditions))
found := false
for i, cond := range dConditions {
if cond.String("type") == desiredCondition.Type() {
conditions[i] = newCond
found = true
} else {
conditions[i], err = convert.EncodeToMap(cond)
if err != nil {
return false, err
}
}
}
if !found {
conditions = append(conditions, newCond)
}
d.SetNested(conditions, "status", "conditions")
return true, nil
}