constellation/cli/internal/cloudcmd/upgrade_test.go
Otto Bittner 50646b2a10 cli: refactor upgrade apply cmd to match name
* `upgrade apply` will try to make the locally configured and
actual version in the cluster match by appling necessary
upgrades.
* Skip image or kubernetes upgrades if one is already
in progress.
* Skip downgrades/equal-as-running versions
* Move NodeVersionResourceName constant from operators
to internal as its needed in the CLI.
2023-02-15 16:44:47 +01:00

441 lines
12 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cloudcmd
import (
"context"
"encoding/json"
"errors"
"io"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/versions/components"
updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
func TestUpgradeK8s(t *testing.T) {
someErr := errors.New("some error")
testCases := map[string]struct {
stable stubStableClient
conditions []metav1.Condition
activeClusterVersionUpgrade bool
newClusterVersion string
currentClusterVersion string
components components.Components
getErr error
assertCorrectError func(t *testing.T, err error) bool
wantErr bool
}{
"success": {
currentClusterVersion: "v1.2.2",
newClusterVersion: "v1.2.3",
},
"not an upgrade": {
currentClusterVersion: "v1.2.3",
newClusterVersion: "v1.2.3",
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
target := &InvalidUpgradeError{}
return assert.ErrorAs(t, err, &target)
},
},
"downgrade": {
currentClusterVersion: "v1.2.3",
newClusterVersion: "v1.2.2",
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
target := &InvalidUpgradeError{}
return assert.ErrorAs(t, err, &target)
},
},
"no constellation-version object": {
getErr: someErr,
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
return assert.ErrorIs(t, err, someErr)
},
},
"upgrade in progress": {
currentClusterVersion: "v1.2.2",
newClusterVersion: "v1.2.3",
conditions: []metav1.Condition{{
Type: updatev1alpha1.ConditionOutdated,
Status: metav1.ConditionTrue,
}},
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
return assert.ErrorIs(t, err, ErrInProgress)
},
},
"configmap create fails": {
currentClusterVersion: "v1.2.2",
newClusterVersion: "v1.2.3",
stable: stubStableClient{
createErr: someErr,
},
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
return assert.ErrorIs(t, err, someErr)
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
nodeVersion := updatev1alpha1.NodeVersion{
Spec: updatev1alpha1.NodeVersionSpec{
KubernetesClusterVersion: tc.currentClusterVersion,
},
Status: updatev1alpha1.NodeVersionStatus{
Conditions: tc.conditions,
ActiveClusterVersionUpgrade: tc.activeClusterVersionUpgrade,
},
}
unstrNodeVersion, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nodeVersion)
require.NoError(err)
upgrader := Upgrader{
stableInterface: &tc.stable,
dynamicInterface: &stubDynamicClient{object: &unstructured.Unstructured{Object: unstrNodeVersion}, getErr: tc.getErr},
log: logger.NewTest(t),
outWriter: io.Discard,
}
err = upgrader.UpgradeK8s(context.Background(), tc.newClusterVersion, tc.components)
if tc.wantErr {
tc.assertCorrectError(t, err)
return
}
assert.NoError(err)
})
}
}
func TestUpgradeImage(t *testing.T) {
someErr := errors.New("some error")
testCases := map[string]struct {
stable *stubStableClient
conditions []metav1.Condition
currentImageVersion string
newImageVersion string
getErr error
wantErr bool
wantUpdate bool
assertCorrectError func(t *testing.T, err error) bool
}{
"success": {
currentImageVersion: "v1.2.2",
newImageVersion: "v1.2.3",
stable: &stubStableClient{
configMap: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
},
wantUpdate: true,
},
"not an upgrade": {
currentImageVersion: "v1.2.2",
newImageVersion: "v1.2.2",
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
target := &InvalidUpgradeError{}
return assert.ErrorAs(t, err, &target)
},
},
"downgrade": {
currentImageVersion: "v1.2.2",
newImageVersion: "v1.2.1",
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
target := &InvalidUpgradeError{}
return assert.ErrorAs(t, err, &target)
},
},
"upgrade in progress": {
currentImageVersion: "v1.2.2",
newImageVersion: "v1.2.3",
conditions: []metav1.Condition{{
Type: updatev1alpha1.ConditionOutdated,
Status: metav1.ConditionTrue,
}},
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
return assert.ErrorIs(t, err, ErrInProgress)
},
},
"get error": {
getErr: someErr,
wantErr: true,
assertCorrectError: func(t *testing.T, err error) bool {
return assert.ErrorIs(t, err, someErr)
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
nodeVersion := updatev1alpha1.NodeVersion{
Spec: updatev1alpha1.NodeVersionSpec{
ImageVersion: tc.currentImageVersion,
},
Status: updatev1alpha1.NodeVersionStatus{
Conditions: tc.conditions,
},
}
unstrNodeVersion, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nodeVersion)
require.NoError(err)
dynamicClient := &stubDynamicClient{object: &unstructured.Unstructured{Object: unstrNodeVersion}, getErr: tc.getErr}
upgrader := Upgrader{
stableInterface: tc.stable,
dynamicInterface: dynamicClient,
log: logger.NewTest(t),
outWriter: io.Discard,
}
err = upgrader.UpgradeImage(context.Background(), "", tc.newImageVersion, nil)
// Check upgrades first because if we checked err first, UpgradeImage may error due to other reasons and still trigger an upgrade.
if tc.wantUpdate {
assert.NotNil(dynamicClient.updatedObject)
} else {
assert.Nil(dynamicClient.updatedObject)
}
if tc.wantErr {
assert.Error(err)
tc.assertCorrectError(t, err)
return
}
assert.NoError(err)
})
}
}
func TestUpdateMeasurements(t *testing.T) {
someErr := errors.New("error")
testCases := map[string]struct {
updater *stubStableClient
newMeasurements measurements.M
wantUpdate bool
wantErr bool
}{
"success": {
updater: &stubStableClient{
configMap: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
},
newMeasurements: measurements.M{
0: measurements.WithAllBytes(0xBB, false),
},
wantUpdate: true,
},
"measurements are the same": {
updater: &stubStableClient{
configMap: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
},
newMeasurements: measurements.M{
0: measurements.WithAllBytes(0xAA, false),
},
},
"trying to set warnOnly to true results in error": {
updater: &stubStableClient{
configMap: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
},
newMeasurements: measurements.M{
0: measurements.WithAllBytes(0xAA, true),
},
wantErr: true,
},
"setting warnOnly to false is allowed": {
updater: &stubStableClient{
configMap: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":true}}`,
},
},
},
newMeasurements: measurements.M{
0: measurements.WithAllBytes(0xAA, false),
},
wantUpdate: true,
},
"getCurrent error": {
updater: &stubStableClient{getErr: someErr},
wantErr: true,
},
"update error": {
updater: &stubStableClient{
configMap: &corev1.ConfigMap{
Data: map[string]string{
constants.MeasurementsFilename: `{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`,
},
},
updateErr: someErr,
},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
upgrader := &Upgrader{
stableInterface: tc.updater,
outWriter: io.Discard,
log: logger.NewTest(t),
}
err := upgrader.updateMeasurements(context.Background(), tc.newMeasurements)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
if tc.wantUpdate {
newMeasurementsJSON, err := json.Marshal(tc.newMeasurements)
require.NoError(t, err)
assert.JSONEq(string(newMeasurementsJSON), tc.updater.updatedConfigMap.Data[constants.MeasurementsFilename])
} else {
assert.Nil(tc.updater.updatedConfigMap)
}
})
}
}
func TestUpdateImage(t *testing.T) {
someErr := errors.New("error")
testCases := map[string]struct {
nodeVersion updatev1alpha1.NodeVersion
newImageReference string
newImageVersion string
oldImageVersion string
updateErr error
wantUpdate bool
wantErr bool
}{
"success": {
nodeVersion: updatev1alpha1.NodeVersion{
Spec: updatev1alpha1.NodeVersionSpec{
ImageReference: "old-image-ref",
ImageVersion: "old-image-ver",
},
},
newImageReference: "new-image-ref",
newImageVersion: "new-image-ver",
wantUpdate: true,
},
"update error": {
updateErr: someErr,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
upgradeClient := &stubDynamicClient{updateErr: tc.updateErr}
upgrader := &Upgrader{
dynamicInterface: upgradeClient,
outWriter: io.Discard,
log: logger.NewTest(t),
}
err := upgrader.updateImage(context.Background(), tc.nodeVersion, tc.newImageReference, tc.newImageVersion)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
if tc.wantUpdate {
assert.Equal(tc.newImageReference, upgradeClient.updatedObject.Object["spec"].(map[string]any)["image"])
assert.Equal(tc.newImageVersion, upgradeClient.updatedObject.Object["spec"].(map[string]any)["imageVersion"])
} else {
assert.Nil(upgradeClient.updatedObject)
}
})
}
}
type stubDynamicClient struct {
object *unstructured.Unstructured
updatedObject *unstructured.Unstructured
getErr error
updateErr error
}
func (u *stubDynamicClient) getCurrent(ctx context.Context, name string) (*unstructured.Unstructured, error) {
return u.object, u.getErr
}
func (u *stubDynamicClient) update(_ context.Context, updatedObject *unstructured.Unstructured) (*unstructured.Unstructured, error) {
u.updatedObject = updatedObject
return u.updatedObject, u.updateErr
}
type stubStableClient struct {
configMap *corev1.ConfigMap
updatedConfigMap *corev1.ConfigMap
k8sVersion string
getErr error
updateErr error
createErr error
k8sErr error
}
func (s *stubStableClient) getCurrentConfigMap(ctx context.Context, name string) (*corev1.ConfigMap, error) {
return s.configMap, s.getErr
}
func (s *stubStableClient) updateConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) {
s.updatedConfigMap = configMap
return nil, s.updateErr
}
func (s *stubStableClient) createConfigMap(ctx context.Context, configMap *corev1.ConfigMap) (*corev1.ConfigMap, error) {
s.configMap = configMap
return s.configMap, s.createErr
}
func (s *stubStableClient) kubernetesVersion() (string, error) {
return s.k8sVersion, s.k8sErr
}