mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-02-23 16:30:11 -05:00
upgrade: retry when node image update fails due to conflict error (#2123)
* retry when node image update fails due to conflict error * improve test
This commit is contained in:
parent
a87b7894db
commit
92abb890ef
@ -38,6 +38,7 @@ go_library(
|
|||||||
"@io_k8s_client_go//dynamic",
|
"@io_k8s_client_go//dynamic",
|
||||||
"@io_k8s_client_go//kubernetes",
|
"@io_k8s_client_go//kubernetes",
|
||||||
"@io_k8s_client_go//tools/clientcmd",
|
"@io_k8s_client_go//tools/clientcmd",
|
||||||
|
"@io_k8s_client_go//util/retry",
|
||||||
"@io_k8s_kubernetes//cmd/kubeadm/app/apis/kubeadm/v1beta3",
|
"@io_k8s_kubernetes//cmd/kubeadm/app/apis/kubeadm/v1beta3",
|
||||||
"@io_k8s_sigs_yaml//:yaml",
|
"@io_k8s_sigs_yaml//:yaml",
|
||||||
],
|
],
|
||||||
@ -59,10 +60,13 @@ go_test(
|
|||||||
"//internal/versions/components",
|
"//internal/versions/components",
|
||||||
"//operators/constellation-node-operator/api/v1alpha1",
|
"//operators/constellation-node-operator/api/v1alpha1",
|
||||||
"@com_github_stretchr_testify//assert",
|
"@com_github_stretchr_testify//assert",
|
||||||
|
"@com_github_stretchr_testify//mock",
|
||||||
"@com_github_stretchr_testify//require",
|
"@com_github_stretchr_testify//require",
|
||||||
"@io_k8s_api//core/v1:core",
|
"@io_k8s_api//core/v1:core",
|
||||||
|
"@io_k8s_apimachinery//pkg/api/errors",
|
||||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||||
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured",
|
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured",
|
||||||
"@io_k8s_apimachinery//pkg/runtime",
|
"@io_k8s_apimachinery//pkg/runtime",
|
||||||
|
"@io_k8s_apimachinery//pkg/runtime/schema",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -44,6 +44,7 @@ import (
|
|||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
"k8s.io/client-go/util/retry"
|
||||||
kubeadmv1beta3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
kubeadmv1beta3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
)
|
)
|
||||||
@ -426,23 +427,31 @@ func (u *Upgrader) applyComponentsCM(ctx context.Context, components *corev1.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *Upgrader) applyNodeVersion(ctx context.Context, nodeVersion updatev1alpha1.NodeVersion) (updatev1alpha1.NodeVersion, error) {
|
func (u *Upgrader) applyNodeVersion(ctx context.Context, nodeVersion updatev1alpha1.NodeVersion) (updatev1alpha1.NodeVersion, error) {
|
||||||
raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nodeVersion)
|
|
||||||
if err != nil {
|
|
||||||
return updatev1alpha1.NodeVersion{}, fmt.Errorf("converting nodeVersion to unstructured: %w", err)
|
|
||||||
}
|
|
||||||
u.log.Debugf("Triggering NodeVersion upgrade now")
|
u.log.Debugf("Triggering NodeVersion upgrade now")
|
||||||
// Send the updated NodeVersion resource
|
var updatedNodeVersion updatev1alpha1.NodeVersion
|
||||||
|
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||||
|
newNode, err := u.getClusterStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("retrieving current NodeVersion: %w", err)
|
||||||
|
}
|
||||||
|
cmd := newUpgradeVersionCmd(nodeVersion)
|
||||||
|
cmd.SetUpdatedVersions(&newNode)
|
||||||
|
raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&newNode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("converting nodeVersion to unstructured: %w", err)
|
||||||
|
}
|
||||||
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 {
|
||||||
return updatev1alpha1.NodeVersion{}, fmt.Errorf("updating NodeVersion: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedNodeVersion updatev1alpha1.NodeVersion
|
|
||||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(updated.UnstructuredContent(), &updatedNodeVersion); err != nil {
|
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(updated.UnstructuredContent(), &updatedNodeVersion); err != nil {
|
||||||
return updatev1alpha1.NodeVersion{}, fmt.Errorf("converting unstructured to NodeVersion: %w", err)
|
return fmt.Errorf("converting unstructured to NodeVersion: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
return updatedNodeVersion, nil
|
return updatedNodeVersion, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Upgrader) getClusterStatus(ctx context.Context) (updatev1alpha1.NodeVersion, error) {
|
func (u *Upgrader) getClusterStatus(ctx context.Context) (updatev1alpha1.NodeVersion, error) {
|
||||||
@ -599,3 +608,34 @@ type imageFetcher interface {
|
|||||||
image, region string,
|
image, region string,
|
||||||
) (string, error)
|
) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type upgradeVersionCmd struct {
|
||||||
|
imageVersion string
|
||||||
|
imageRef string
|
||||||
|
k8sComponentsRef string
|
||||||
|
k8sVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpgradeVersionCmd(newNodeVersion updatev1alpha1.NodeVersion) upgradeVersionCmd {
|
||||||
|
return upgradeVersionCmd{
|
||||||
|
imageVersion: newNodeVersion.Spec.ImageVersion,
|
||||||
|
imageRef: newNodeVersion.Spec.ImageReference,
|
||||||
|
k8sComponentsRef: newNodeVersion.Spec.KubernetesComponentsReference,
|
||||||
|
k8sVersion: newNodeVersion.Spec.KubernetesClusterVersion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u upgradeVersionCmd) SetUpdatedVersions(node *updatev1alpha1.NodeVersion) {
|
||||||
|
if u.imageVersion != "" {
|
||||||
|
node.Spec.ImageVersion = u.imageVersion
|
||||||
|
}
|
||||||
|
if u.imageRef != "" {
|
||||||
|
node.Spec.ImageReference = u.imageRef
|
||||||
|
}
|
||||||
|
if u.k8sComponentsRef != "" {
|
||||||
|
node.Spec.KubernetesComponentsReference = u.k8sComponentsRef
|
||||||
|
}
|
||||||
|
if u.k8sVersion != "" {
|
||||||
|
node.Spec.KubernetesClusterVersion = u.k8sVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
@ -24,11 +26,13 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
||||||
updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1"
|
updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUpgradeNodeVersion(t *testing.T) {
|
func TestUpgradeNodeVersion(t *testing.T) {
|
||||||
@ -46,6 +50,7 @@ func TestUpgradeNodeVersion(t *testing.T) {
|
|||||||
wantErr bool
|
wantErr bool
|
||||||
wantUpdate bool
|
wantUpdate bool
|
||||||
assertCorrectError func(t *testing.T, err error) bool
|
assertCorrectError func(t *testing.T, err error) bool
|
||||||
|
customClientFn func(nodeVersion updatev1alpha1.NodeVersion) DynamicInterface
|
||||||
}{
|
}{
|
||||||
"success": {
|
"success": {
|
||||||
conf: func() *config.Config {
|
conf: func() *config.Config {
|
||||||
@ -252,6 +257,29 @@ func TestUpgradeNodeVersion(t *testing.T) {
|
|||||||
return assert.ErrorAs(t, err, &upgradeErr)
|
return assert.ErrorAs(t, err, &upgradeErr)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"succeed after update retry when the updated node object is outdated": {
|
||||||
|
conf: func() *config.Config {
|
||||||
|
conf := config.Default()
|
||||||
|
conf.Image = "v1.2.3"
|
||||||
|
conf.KubernetesVersion = versions.SupportedK8sVersions()[1]
|
||||||
|
return conf
|
||||||
|
}(),
|
||||||
|
currentImageVersion: "v1.2.2",
|
||||||
|
currentClusterVersion: versions.SupportedK8sVersions()[0],
|
||||||
|
stable: &stubStableClient{
|
||||||
|
configMaps: map[string]*corev1.ConfigMap{
|
||||||
|
constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantUpdate: false, // because customClient is used
|
||||||
|
customClientFn: func(nodeVersion updatev1alpha1.NodeVersion) DynamicInterface {
|
||||||
|
fakeClient := &fakeDynamicClient{}
|
||||||
|
fakeClient.On("GetCurrent", mock.Anything, mock.Anything).Return(unstructedObjectWithGeneration(nodeVersion, 1), nil)
|
||||||
|
fakeClient.On("Update", mock.Anything, mock.Anything).Return(nil, kerrors.NewConflict(schema.GroupResource{Resource: nodeVersion.Name}, nodeVersion.Name, nil)).Once()
|
||||||
|
fakeClient.On("Update", mock.Anything, mock.Anything).Return(unstructedObjectWithGeneration(nodeVersion, 2), nil).Once()
|
||||||
|
return fakeClient
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range testCases {
|
for name, tc := range testCases {
|
||||||
@ -269,9 +297,6 @@ func TestUpgradeNodeVersion(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
unstrNodeVersion, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nodeVersion)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
var badUpdatedObject *unstructured.Unstructured
|
var badUpdatedObject *unstructured.Unstructured
|
||||||
if tc.badImageVersion != "" {
|
if tc.badImageVersion != "" {
|
||||||
nodeVersion.Spec.ImageVersion = tc.badImageVersion
|
nodeVersion.Spec.ImageVersion = tc.badImageVersion
|
||||||
@ -280,6 +305,8 @@ func TestUpgradeNodeVersion(t *testing.T) {
|
|||||||
badUpdatedObject = &unstructured.Unstructured{Object: unstrBadNodeVersion}
|
badUpdatedObject = &unstructured.Unstructured{Object: unstrBadNodeVersion}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unstrNodeVersion, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nodeVersion)
|
||||||
|
require.NoError(err)
|
||||||
dynamicClient := &stubDynamicClient{object: &unstructured.Unstructured{Object: unstrNodeVersion}, badUpdatedObject: badUpdatedObject, getErr: tc.getErr}
|
dynamicClient := &stubDynamicClient{object: &unstructured.Unstructured{Object: unstrNodeVersion}, badUpdatedObject: badUpdatedObject, getErr: tc.getErr}
|
||||||
upgrader := Upgrader{
|
upgrader := Upgrader{
|
||||||
stableInterface: tc.stable,
|
stableInterface: tc.stable,
|
||||||
@ -288,9 +315,11 @@ func TestUpgradeNodeVersion(t *testing.T) {
|
|||||||
log: logger.NewTest(t),
|
log: logger.NewTest(t),
|
||||||
outWriter: io.Discard,
|
outWriter: io.Discard,
|
||||||
}
|
}
|
||||||
|
if tc.customClientFn != nil {
|
||||||
|
upgrader.dynamicInterface = tc.customClientFn(nodeVersion)
|
||||||
|
}
|
||||||
|
|
||||||
err = upgrader.UpgradeNodeVersion(context.Background(), tc.conf, tc.force)
|
err = upgrader.UpgradeNodeVersion(context.Background(), tc.conf, tc.force)
|
||||||
|
|
||||||
// Check upgrades first because if we checked err first, UpgradeImage may error due to other reasons and still trigger an upgrade.
|
// Check upgrades first because if we checked err first, UpgradeImage may error due to other reasons and still trigger an upgrade.
|
||||||
if tc.wantUpdate {
|
if tc.wantUpdate {
|
||||||
assert.NotNil(dynamicClient.updatedObject)
|
assert.NotNil(dynamicClient.updatedObject)
|
||||||
@ -550,6 +579,23 @@ func newJoinConfigMap(data string) *corev1.ConfigMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeDynamicClient struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *fakeDynamicClient) GetCurrent(ctx context.Context, str string) (*unstructured.Unstructured, error) {
|
||||||
|
args := u.Called(ctx, str)
|
||||||
|
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *fakeDynamicClient) Update(ctx context.Context, updatedObject *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||||
|
args := u.Called(ctx, updatedObject)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return updatedObject, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
type stubDynamicClient struct {
|
type stubDynamicClient struct {
|
||||||
object *unstructured.Unstructured
|
object *unstructured.Unstructured
|
||||||
updatedObject *unstructured.Unstructured
|
updatedObject *unstructured.Unstructured
|
||||||
@ -615,3 +661,10 @@ func (f *stubImageFetcher) FetchReference(_ context.Context,
|
|||||||
) (string, error) {
|
) (string, error) {
|
||||||
return f.reference, f.fetchReferenceErr
|
return f.reference, f.fetchReferenceErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unstructedObjectWithGeneration(nodeVersion updatev1alpha1.NodeVersion, generation int64) *unstructured.Unstructured {
|
||||||
|
unstrNodeVersion, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(&nodeVersion)
|
||||||
|
object := &unstructured.Unstructured{Object: unstrNodeVersion}
|
||||||
|
object.SetGeneration(generation)
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
1
go.mod
1
go.mod
@ -133,6 +133,7 @@ require (
|
|||||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||||
github.com/google/pprof v0.0.0-20221103000818-d260c55eee4c // indirect
|
github.com/google/pprof v0.0.0-20221103000818-d260c55eee4c // indirect
|
||||||
github.com/google/s2a-go v0.1.4 // indirect
|
github.com/google/s2a-go v0.1.4 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.14.0 // indirect
|
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||||
|
Loading…
x
Reference in New Issue
Block a user