cli: fix upgrade apply for image-only upgrades (#1468)

This fixes a bug where `upgrade apply` fails if only the image is
upgraded, due to mishandling of an empty configmap.
Making stubStableClient more complex is needed since it is called
with multiple configMaps now.
This commit is contained in:
Otto Bittner 2023-03-22 11:53:47 +01:00 committed by GitHub
parent 02fc3dc635
commit 9f6e924066
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 83 additions and 62 deletions

View file

@ -115,13 +115,24 @@ func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config)
upgradeErrs := []error{} upgradeErrs := []error{}
upgradeErr := &compatibility.InvalidUpgradeError{} upgradeErr := &compatibility.InvalidUpgradeError{}
err = u.updateImage(&nodeVersion, imageReference, imageVersion.Version) err = u.updateImage(&nodeVersion, imageReference, imageVersion.Version)
if errors.As(err, &upgradeErr) { switch {
case errors.As(err, &upgradeErr):
upgradeErrs = append(upgradeErrs, fmt.Errorf("skipping image upgrades: %w", err)) upgradeErrs = append(upgradeErrs, fmt.Errorf("skipping image upgrades: %w", err))
case err != nil:
return fmt.Errorf("updating image version: %w", err)
} }
components, err := u.updateK8s(&nodeVersion, versionConfig.ClusterVersion, versionConfig.KubernetesComponents) components, err := u.updateK8s(&nodeVersion, versionConfig.ClusterVersion, versionConfig.KubernetesComponents)
if errors.As(err, &upgradeErr) { switch {
case err == nil:
err := u.applyComponentsCM(ctx, components)
if err != nil {
return fmt.Errorf("applying k8s components ConfigMap: %w", err)
}
case errors.As(err, &upgradeErr):
upgradeErrs = append(upgradeErrs, fmt.Errorf("skipping Kubernetes upgrades: %w", err)) upgradeErrs = append(upgradeErrs, fmt.Errorf("skipping Kubernetes upgrades: %w", err))
default:
return fmt.Errorf("updating Kubernetes version: %w", err)
} }
if len(upgradeErrs) == 2 { if len(upgradeErrs) == 2 {
@ -131,32 +142,41 @@ func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config)
if err := u.updateMeasurements(ctx, conf.GetMeasurements()); err != nil { if err := u.updateMeasurements(ctx, conf.GetMeasurements()); err != nil {
return fmt.Errorf("updating measurements: %w", err) return fmt.Errorf("updating measurements: %w", err)
} }
updatedNodeVersion, err := u.applyUpgrade(ctx, &components, nodeVersion)
updatedNodeVersion, err := u.applyNodeVersion(ctx, nodeVersion)
if err != nil { if err != nil {
return fmt.Errorf("applying upgrade: %w", err) return fmt.Errorf("applying upgrade: %w", err)
} }
if updatedNodeVersion.Spec.ImageReference != imageReference || switch {
updatedNodeVersion.Spec.ImageVersion != imageVersion.Version || case updatedNodeVersion.Spec.ImageReference != imageReference:
updatedNodeVersion.Spec.KubernetesComponentsReference != components.ObjectMeta.Name || return fmt.Errorf("expected NodeVersion to contain %s, got %s", imageReference, updatedNodeVersion.Spec.ImageReference)
updatedNodeVersion.Spec.KubernetesClusterVersion != versionConfig.ClusterVersion { case updatedNodeVersion.Spec.ImageVersion != imageVersion.Version:
return errors.New("unexpected value in updated nodeVersion object") return fmt.Errorf("expected NodeVersion to contain %s, got %s", imageVersion.Version, updatedNodeVersion.Spec.ImageVersion)
case updatedNodeVersion.Spec.KubernetesComponentsReference != nodeVersion.Spec.KubernetesComponentsReference:
return fmt.Errorf("expected NodeVersion to contain %s, got %s", nodeVersion.Spec.KubernetesComponentsReference, updatedNodeVersion.Spec.KubernetesComponentsReference)
case updatedNodeVersion.Spec.KubernetesClusterVersion != versionConfig.ClusterVersion:
return fmt.Errorf("expected NodeVersion to contain %s, got %s", versionConfig.ClusterVersion, updatedNodeVersion.Spec.KubernetesClusterVersion)
} }
return errors.Join(upgradeErrs...) return errors.Join(upgradeErrs...)
} }
func (u *Upgrader) applyUpgrade(ctx context.Context, components *corev1.ConfigMap, nodeVersion updatev1alpha1.NodeVersion) (updatev1alpha1.NodeVersion, error) { // applyComponentsCM applies the k8s components ConfigMap to the cluster.
func (u *Upgrader) applyComponentsCM(ctx context.Context, components *corev1.ConfigMap) error {
_, err := u.stableInterface.createConfigMap(ctx, components) _, err := u.stableInterface.createConfigMap(ctx, components)
// If the map already exists we can use that map and assume it has the same content as 'configMap'. // If the map already exists we can use that map and assume it has the same content as 'configMap'.
if err != nil && !k8serrors.IsAlreadyExists(err) { if err != nil && !k8serrors.IsAlreadyExists(err) {
return updatev1alpha1.NodeVersion{}, fmt.Errorf("creating k8s-components ConfigMap: %w. %T", err, err) return fmt.Errorf("creating k8s-components ConfigMap: %w. %T", err, err)
} }
return nil
}
func (u *Upgrader) applyNodeVersion(ctx context.Context, nodeVersion updatev1alpha1.NodeVersion) (updatev1alpha1.NodeVersion, error) {
raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nodeVersion) raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nodeVersion)
if err != nil { if err != nil {
return updatev1alpha1.NodeVersion{}, fmt.Errorf("converting nodeVersion to unstructured: %w", err) return updatev1alpha1.NodeVersion{}, fmt.Errorf("converting nodeVersion to unstructured: %w", err)
} }
u.log.Debugf("Triggering Kubernetes version upgrade now") u.log.Debugf("Triggering NodeVersion upgrade now")
// Send the updated NodeVersion resource // Send the updated NodeVersion resource
updated, err := u.dynamicInterface.update(ctx, &unstructured.Unstructured{Object: raw}) updated, err := u.dynamicInterface.update(ctx, &unstructured.Unstructured{Object: raw})
if err != nil { if err != nil {
@ -199,21 +219,21 @@ func (u *Upgrader) updateImage(nodeVersion *updatev1alpha1.NodeVersion, newImage
return nil return nil
} }
func (u *Upgrader) updateK8s(nodeVersion *updatev1alpha1.NodeVersion, newClusterVersion string, components components.Components) (corev1.ConfigMap, error) { func (u *Upgrader) updateK8s(nodeVersion *updatev1alpha1.NodeVersion, newClusterVersion string, components components.Components) (*corev1.ConfigMap, error) {
configMap, err := internalk8s.ConstructK8sComponentsCM(components, newClusterVersion) configMap, err := internalk8s.ConstructK8sComponentsCM(components, newClusterVersion)
if err != nil { if err != nil {
return corev1.ConfigMap{}, fmt.Errorf("constructing k8s-components ConfigMap: %w", err) return nil, fmt.Errorf("constructing k8s-components ConfigMap: %w", err)
} }
if err := compatibility.IsValidUpgrade(nodeVersion.Spec.KubernetesClusterVersion, newClusterVersion); err != nil { if err := compatibility.IsValidUpgrade(nodeVersion.Spec.KubernetesClusterVersion, newClusterVersion); err != nil {
return corev1.ConfigMap{}, err return nil, err
} }
u.log.Debugf("Updating local copy of nodeVersion Kubernetes version from %s to %s", nodeVersion.Spec.KubernetesClusterVersion, newClusterVersion) u.log.Debugf("Updating local copy of nodeVersion Kubernetes version from %s to %s", nodeVersion.Spec.KubernetesClusterVersion, newClusterVersion)
nodeVersion.Spec.KubernetesComponentsReference = configMap.ObjectMeta.Name nodeVersion.Spec.KubernetesComponentsReference = configMap.ObjectMeta.Name
nodeVersion.Spec.KubernetesClusterVersion = newClusterVersion nodeVersion.Spec.KubernetesClusterVersion = newClusterVersion
return configMap, nil return &configMap, nil
} }
// KubernetesVersion returns the version of Kubernetes the Constellation is currently running on. // KubernetesVersion returns the version of Kubernetes the Constellation is currently running on.

View file

@ -52,10 +52,8 @@ func TestUpgradeNodeVersion(t *testing.T) {
currentImageVersion: "v1.2.2", currentImageVersion: "v1.2.2",
currentClusterVersion: versions.SupportedK8sVersions()[0], currentClusterVersion: versions.SupportedK8sVersions()[0],
stable: &stubStableClient{ stable: &stubStableClient{
configMap: &corev1.ConfigMap{ configMaps: map[string]*corev1.ConfigMap{
Data: map[string]string{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
}, },
}, },
wantUpdate: true, wantUpdate: true,
@ -70,10 +68,8 @@ func TestUpgradeNodeVersion(t *testing.T) {
currentImageVersion: "v1.2.2", currentImageVersion: "v1.2.2",
currentClusterVersion: versions.SupportedK8sVersions()[0], currentClusterVersion: versions.SupportedK8sVersions()[0],
stable: &stubStableClient{ stable: &stubStableClient{
configMap: &corev1.ConfigMap{ configMaps: map[string]*corev1.ConfigMap{
Data: map[string]string{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
}, },
}, },
wantUpdate: true, wantUpdate: true,
@ -93,10 +89,8 @@ func TestUpgradeNodeVersion(t *testing.T) {
currentImageVersion: "v1.2.2", currentImageVersion: "v1.2.2",
currentClusterVersion: versions.SupportedK8sVersions()[0], currentClusterVersion: versions.SupportedK8sVersions()[0],
stable: &stubStableClient{ stable: &stubStableClient{
configMap: &corev1.ConfigMap{ configMaps: map[string]*corev1.ConfigMap{
Data: map[string]string{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
}, },
}, },
wantUpdate: true, wantUpdate: true,
@ -211,10 +205,8 @@ func TestUpdateMeasurements(t *testing.T) {
}{ }{
"success": { "success": {
updater: &stubStableClient{ updater: &stubStableClient{
configMap: &corev1.ConfigMap{ configMaps: map[string]*corev1.ConfigMap{
Data: map[string]string{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
}, },
}, },
newMeasurements: measurements.M{ newMeasurements: measurements.M{
@ -224,10 +216,8 @@ func TestUpdateMeasurements(t *testing.T) {
}, },
"measurements are the same": { "measurements are the same": {
updater: &stubStableClient{ updater: &stubStableClient{
configMap: &corev1.ConfigMap{ configMaps: map[string]*corev1.ConfigMap{
Data: map[string]string{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
}, },
}, },
newMeasurements: measurements.M{ newMeasurements: measurements.M{
@ -236,10 +226,8 @@ func TestUpdateMeasurements(t *testing.T) {
}, },
"trying to set warnOnly to true results in error": { "trying to set warnOnly to true results in error": {
updater: &stubStableClient{ updater: &stubStableClient{
configMap: &corev1.ConfigMap{ configMaps: map[string]*corev1.ConfigMap{
Data: map[string]string{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
}, },
}, },
newMeasurements: measurements.M{ newMeasurements: measurements.M{
@ -249,10 +237,8 @@ func TestUpdateMeasurements(t *testing.T) {
}, },
"setting warnOnly to false is allowed": { "setting warnOnly to false is allowed": {
updater: &stubStableClient{ updater: &stubStableClient{
configMap: &corev1.ConfigMap{ configMaps: map[string]*corev1.ConfigMap{
Data: map[string]string{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":true}}`),
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":true}}`,
},
}, },
}, },
newMeasurements: measurements.M{ newMeasurements: measurements.M{
@ -266,10 +252,8 @@ func TestUpdateMeasurements(t *testing.T) {
}, },
"update error": { "update error": {
updater: &stubStableClient{ updater: &stubStableClient{
configMap: &corev1.ConfigMap{ configMaps: map[string]*corev1.ConfigMap{
Data: map[string]string{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
}, },
updateErr: someErr, updateErr: someErr,
}, },
@ -297,9 +281,9 @@ func TestUpdateMeasurements(t *testing.T) {
if tc.wantUpdate { if tc.wantUpdate {
newMeasurementsJSON, err := json.Marshal(tc.newMeasurements) newMeasurementsJSON, err := json.Marshal(tc.newMeasurements)
require.NoError(t, err) require.NoError(t, err)
assert.JSONEq(string(newMeasurementsJSON), tc.updater.updatedConfigMap.Data[constants.MeasurementsFilename]) assert.JSONEq(string(newMeasurementsJSON), tc.updater.updatedConfigMaps[constants.JoinConfigMap].Data[constants.MeasurementsFilename])
} else { } else {
assert.Nil(tc.updater.updatedConfigMap) assert.Nil(tc.updater.updatedConfigMaps)
} }
}) })
} }
@ -424,6 +408,17 @@ func TestUpdateK8s(t *testing.T) {
} }
} }
func newJoinConfigMap(data string) *corev1.ConfigMap {
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: constants.JoinConfigMap,
},
Data: map[string]string{
constants.MeasurementsFilename: data,
},
}
}
type stubDynamicClient struct { type stubDynamicClient struct {
object *unstructured.Unstructured object *unstructured.Unstructured
updatedObject *unstructured.Unstructured updatedObject *unstructured.Unstructured
@ -441,8 +436,8 @@ func (u *stubDynamicClient) update(_ context.Context, updatedObject *unstructure
} }
type stubStableClient struct { type stubStableClient struct {
configMap *corev1.ConfigMap configMaps map[string]*corev1.ConfigMap
updatedConfigMap *corev1.ConfigMap updatedConfigMaps map[string]*corev1.ConfigMap
k8sVersion string k8sVersion string
getErr error getErr error
updateErr error updateErr error
@ -450,18 +445,24 @@ type stubStableClient struct {
k8sErr error k8sErr error
} }
func (s *stubStableClient) getCurrentConfigMap(_ context.Context, _ string) (*corev1.ConfigMap, error) { func (s *stubStableClient) getCurrentConfigMap(_ context.Context, name string) (*corev1.ConfigMap, error) {
return s.configMap, s.getErr return s.configMaps[name], s.getErr
} }
func (s *stubStableClient) updateConfigMap(_ context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) { func (s *stubStableClient) updateConfigMap(_ context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) {
s.updatedConfigMap = configMap if s.updatedConfigMaps == nil {
return s.updatedConfigMap, s.updateErr s.updatedConfigMaps = map[string]*corev1.ConfigMap{}
}
s.updatedConfigMaps[configMap.ObjectMeta.Name] = configMap
return s.updatedConfigMaps[configMap.ObjectMeta.Name], s.updateErr
} }
func (s *stubStableClient) createConfigMap(_ context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) { func (s *stubStableClient) createConfigMap(_ context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) {
s.configMap = configMap if s.configMaps == nil {
return s.configMap, s.createErr s.configMaps = map[string]*corev1.ConfigMap{}
}
s.configMaps[configMap.ObjectMeta.Name] = configMap
return s.configMaps[configMap.ObjectMeta.Name], s.createErr
} }
func (s *stubStableClient) kubernetesVersion() (string, error) { func (s *stubStableClient) kubernetesVersion() (string, error) {