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

feat(ec): allow rollbacks for embedded cluster #4972

Merged
merged 12 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
75 changes: 60 additions & 15 deletions pkg/store/kotsstore/downstream_store.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package kotsstore

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
Expand All @@ -13,8 +15,10 @@ import (
"github.com/replicatedhq/kots/pkg/kotsutil"
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/persistence"
"github.com/replicatedhq/kots/pkg/store"
"github.com/replicatedhq/kots/pkg/store/types"
"github.com/replicatedhq/kots/pkg/tasks"
"github.com/replicatedhq/kots/pkg/util"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
"github.com/rqlite/gorqlite"
)
Expand Down Expand Up @@ -423,8 +427,10 @@ func (s *KOTSStore) GetDownstreamVersions(appID string, clusterID string, downlo
if err := s.AddDownstreamVersionDetails(appID, clusterID, v, false); err != nil {
return nil, errors.Wrap(err, "failed to add details to latest downloaded version")
}
v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, result, license.Spec.IsSemverRequired)
break
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i cant understand the reason for this break here but it makes it so that the loop only iterates for the first non-pending_download version

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the answer is in the comment above:

// checking if a version is deployable requires getting all versions again.
// check if latest version is deployable separately to avoid cycle dependencies between functions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

populating if a version is deployable or not only happens in the version history handler because it's an expensive process.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other versions are populated below if checkIfDeployable is true

v.IsDeployable, v.NonDeployableCause, err = isAppVersionDeployable(s, appID, v, result, license.Spec.IsSemverRequired)
if err != nil {
return nil, errors.Wrapf(err, "failed to check if version %s is deployable", v.VersionLabel)
}
}

if currentVersion == nil {
Expand Down Expand Up @@ -676,7 +682,10 @@ func (s *KOTSStore) AddDownstreamVersionsDetails(appID string, clusterID string,
}

for _, v := range versions {
v.IsDeployable, v.NonDeployableCause = isAppVersionDeployable(v, allVersions, license.Spec.IsSemverRequired)
v.IsDeployable, v.NonDeployableCause, err = isAppVersionDeployable(s, appID, v, allVersions, license.Spec.IsSemverRequired)
if err != nil {
return errors.Wrapf(err, "failed to check if version %s is deployable", v.VersionLabel)
}
}
}

Expand Down Expand Up @@ -866,28 +875,28 @@ func isSameUpstreamRelease(v1 *downstreamtypes.DownstreamVersion, v2 *downstream
return v1.Semver.EQ(*v2.Semver)
}

func isAppVersionDeployable(version *downstreamtypes.DownstreamVersion, appVersions *downstreamtypes.DownstreamVersions, isSemverRequired bool) (bool, string) {
func isAppVersionDeployable(s store.Store, appID string, version *downstreamtypes.DownstreamVersion, appVersions *downstreamtypes.DownstreamVersions, isSemverRequired bool) (bool, string, error) {
if version.HasFailingStrictPreflights {
return false, "Deployment is disabled as a strict analyzer in this version's preflight checks has failed or has not been run."
return false, "Deployment is disabled as a strict analyzer in this version's preflight checks has failed or has not been run.", nil
}

if version.Status == types.VersionPendingDownload {
return false, "Version is pending download."
return false, "Version is pending download.", nil
}

if version.Status == types.VersionPendingConfig {
return false, "Version is pending configuration."
return false, "Version is pending configuration.", nil
}

if appVersions.CurrentVersion == nil {
// no version has been deployed yet, treat as an initial install where any version can be deployed at first.
return true, ""
return true, "", nil
}

if version.Sequence == appVersions.CurrentVersion.Sequence {
// version is currently deployed, so previous required versions should've already been deployed.
// also, we shouldn't block re-deploying if a previous release is edited later by the vendor to be required.
return true, ""
return true, "", nil
}

// rollback support is determined across all versions from all channels
Expand All @@ -906,6 +915,7 @@ func isAppVersionDeployable(version *downstreamtypes.DownstreamVersion, appVersi
break
}
}

if versionIndex > deployedVersionIndex {
// this is a past version
// rollback support is based off of the latest downloaded version
Expand All @@ -914,10 +924,23 @@ func isAppVersionDeployable(version *downstreamtypes.DownstreamVersion, appVersi
continue
}
if v.KOTSKinds == nil || !v.KOTSKinds.KotsApplication.Spec.AllowRollback {
return false, "Rollback is not supported."
return false, "Rollback is not supported.", nil
}
break
}

if util.IsEmbeddedCluster() {
// Compare the embedded cluster config of the version specified to the currently
// deployed version to check if it has changed. If it has, then we do not allow
// rollbacks.
changed, err := didECClusterConfigChange(s, appID, version, appVersions.CurrentVersion)
if err != nil {
return false, "", errors.Wrapf(err, "failed to check if embedded cluster config changed for version %d", version.Sequence)
}
if changed {
return false, "Rollback is not supported, cluster configuration has changed.", nil
}
}
}

// if semantic versioning is not enabled, only require versions from the same channel AND with a lower cursor/channel sequence
Expand Down Expand Up @@ -951,7 +974,7 @@ func isAppVersionDeployable(version *downstreamtypes.DownstreamVersion, appVersi

if deployedVersionIndex == -1 {
// the deployed version is from a different channel
return true, ""
return true, "", nil
}

// find required versions between the deployed version and the desired version
Expand All @@ -969,7 +992,7 @@ ALL_VERSIONS_LOOP:
// this is a past version
// >= because if the deployed version is required, rolling back isn't allowed
if i >= deployedVersionIndex && i < versionIndex {
return false, "One or more non-reversible versions have been deployed since this version."
return false, "One or more non-reversible versions have been deployed since this version.", nil
}
continue
}
Expand Down Expand Up @@ -997,12 +1020,34 @@ ALL_VERSIONS_LOOP:
}
versionLabelsStr := strings.Join(versionLabels, ", ")
if len(requiredVersions) == 1 {
return false, fmt.Sprintf("This version cannot be deployed because version %s is required and must be deployed first.", versionLabelsStr)
return false, fmt.Sprintf("This version cannot be deployed because version %s is required and must be deployed first.", versionLabelsStr), nil
}
return false, fmt.Sprintf("This version cannot be deployed because versions %s are required and must be deployed first.", versionLabelsStr)
return false, fmt.Sprintf("This version cannot be deployed because versions %s are required and must be deployed first.", versionLabelsStr), nil
}

return true, ""
return true, "", nil
}

// didECClusterConfigChange compares the embedded cluster config of the version specified to the
// currently deployed version to check if it has changed.
func didECClusterConfigChange(s store.Store, appID string, version *downstreamtypes.DownstreamVersion, currentVersion *downstreamtypes.DownstreamVersion) (bool, error) {
currentConf, err := s.GetEmbeddedClusterConfigForVersion(appID, currentVersion.Sequence)
if err != nil {
return false, errors.Wrapf(err, "failed to get embedded cluster config for current version %d", currentVersion.Sequence)
}
currentECConfigBytes, err := json.Marshal(currentConf)
if err != nil {
return false, errors.Wrapf(err, "failed to marshal embedded cluster config for current version %d", currentVersion.Sequence)
}
thisConf, err := s.GetEmbeddedClusterConfigForVersion(appID, version.Sequence)
if err != nil {
return false, errors.Wrap(err, "failed to get embedded cluster config")
}
ecConfigBytes, err := json.Marshal(thisConf)
if err != nil {
return false, errors.Wrap(err, "failed to marshal embedded cluster config")
}
return !bytes.Equal(ecConfigBytes, currentECConfigBytes), nil
}

func getReleaseNotes(appID string, parentSequence int64) (string, error) {
Expand Down
163 changes: 162 additions & 1 deletion pkg/store/kotsstore/downstream_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import (
"testing"

"github.com/blang/semver"
"github.com/golang/mock/gomock"
embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types"
"github.com/replicatedhq/kots/pkg/cursor"
"github.com/replicatedhq/kots/pkg/kotsutil"
mock_store "github.com/replicatedhq/kots/pkg/store/mock"
"github.com/replicatedhq/kots/pkg/store/types"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_isSameUpstreamRelease(t *testing.T) {
Expand Down Expand Up @@ -250,8 +254,10 @@ func Test_isAppVersionDeployable(t *testing.T) {
version *downstreamtypes.DownstreamVersion
appVersions *downstreamtypes.DownstreamVersions
isSemverRequired bool
setup func(t *testing.T, mockStore *mock_store.MockStore)
expectedIsDeployable bool
expectedCause string
wantErr bool
}{
{
name: "failing strict preflights",
Expand Down Expand Up @@ -3621,6 +3627,149 @@ func Test_isAppVersionDeployable(t *testing.T) {
},
/* ---- Semver rollback tests end here ---- */
/* ---- Semver tests end here ---- */
/* ---- Embedded cluster config tests start here ---- */
{
name: "embedded cluster config change should not allow rollbacks",
setup: func(t *testing.T, mockStore *mock_store.MockStore) {
t.Setenv("EMBEDDED_CLUSTER_ID", "1234")

mockStore.EXPECT().GetEmbeddedClusterConfigForVersion("APPID", int64(0)).Return(&embeddedclusterv1beta1.Config{
Spec: embeddedclusterv1beta1.ConfigSpec{
Version: "1.0.0-ec.0",
},
}, nil)
mockStore.EXPECT().GetEmbeddedClusterConfigForVersion("APPID", int64(1)).Return(&embeddedclusterv1beta1.Config{
Spec: embeddedclusterv1beta1.ConfigSpec{
Version: "1.0.0-ec.1",
},
}, nil)
},
version: &downstreamtypes.DownstreamVersion{
VersionLabel: "1.0.0",
Sequence: 0,
},
appVersions: &downstreamtypes.DownstreamVersions{
CurrentVersion: &downstreamtypes.DownstreamVersion{
VersionLabel: "2.0.0",
Sequence: 1,
},
AllVersions: []*downstreamtypes.DownstreamVersion{
{
VersionLabel: "3.0.0",
Sequence: 2,
KOTSKinds: &kotsutil.KotsKinds{
KotsApplication: kotsv1beta1.Application{
Spec: kotsv1beta1.ApplicationSpec{
AllowRollback: true,
},
},
},
},
{
VersionLabel: "2.0.0",
Sequence: 1,
},
{
VersionLabel: "1.0.0",
Sequence: 0,
},
},
},
expectedIsDeployable: false,
expectedCause: "Rollback is not supported, cluster configuration has changed.",
wantErr: false,
},
{
name: "embedded cluster config no change should allow rollbacks",
setup: func(t *testing.T, mockStore *mock_store.MockStore) {
t.Setenv("EMBEDDED_CLUSTER_ID", "1234")

mockStore.EXPECT().GetEmbeddedClusterConfigForVersion("APPID", int64(0)).Return(&embeddedclusterv1beta1.Config{
Spec: embeddedclusterv1beta1.ConfigSpec{
Version: "1.0.0-ec.0",
},
}, nil)
mockStore.EXPECT().GetEmbeddedClusterConfigForVersion("APPID", int64(1)).Return(&embeddedclusterv1beta1.Config{
Spec: embeddedclusterv1beta1.ConfigSpec{
Version: "1.0.0-ec.0",
},
}, nil)
},
version: &downstreamtypes.DownstreamVersion{
VersionLabel: "1.0.0",
Sequence: 0,
},
appVersions: &downstreamtypes.DownstreamVersions{
CurrentVersion: &downstreamtypes.DownstreamVersion{
VersionLabel: "2.0.0",
Sequence: 1,
},
AllVersions: []*downstreamtypes.DownstreamVersion{
{
VersionLabel: "3.0.0",
Sequence: 2,
KOTSKinds: &kotsutil.KotsKinds{
KotsApplication: kotsv1beta1.Application{
Spec: kotsv1beta1.ApplicationSpec{
AllowRollback: true,
},
},
},
},
{
VersionLabel: "2.0.0",
Sequence: 1,
},
{
VersionLabel: "1.0.0",
Sequence: 0,
},
},
},
expectedIsDeployable: true,
expectedCause: "",
wantErr: false,
},
{
name: "embedded cluster, allowRollback = false should not allow rollbacks",
setup: func(t *testing.T, mockStore *mock_store.MockStore) {
t.Setenv("EMBEDDED_CLUSTER_ID", "1234")
},
version: &downstreamtypes.DownstreamVersion{
VersionLabel: "1.0.0",
Sequence: 0,
},
appVersions: &downstreamtypes.DownstreamVersions{
CurrentVersion: &downstreamtypes.DownstreamVersion{
VersionLabel: "2.0.0",
Sequence: 1,
},
AllVersions: []*downstreamtypes.DownstreamVersion{
{
VersionLabel: "3.0.0",
Sequence: 2,
KOTSKinds: &kotsutil.KotsKinds{
KotsApplication: kotsv1beta1.Application{
Spec: kotsv1beta1.ApplicationSpec{
AllowRollback: false,
},
},
},
},
{
VersionLabel: "2.0.0",
Sequence: 1,
},
{
VersionLabel: "1.0.0",
Sequence: 0,
},
},
},
expectedIsDeployable: false,
expectedCause: "Rollback is not supported.",
wantErr: false,
},
}

for _, test := range tests {
Expand Down Expand Up @@ -3656,7 +3805,19 @@ func Test_isAppVersionDeployable(t *testing.T) {
}
}

isDeployable, cause := isAppVersionDeployable(test.version, test.appVersions, test.isSemverRequired)
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockStore := mock_store.NewMockStore(ctrl)
if test.setup != nil {
test.setup(t, mockStore)
}
isDeployable, cause, err := isAppVersionDeployable(mockStore, "APPID", test.version, test.appVersions, test.isSemverRequired)
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, test.expectedIsDeployable, isDeployable)
assert.Equal(t, test.expectedCause, cause)
})
Expand Down
3 changes: 3 additions & 0 deletions pkg/store/kotsstore/kots_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types"
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/persistence"
"github.com/replicatedhq/kots/pkg/store"
"github.com/replicatedhq/kots/pkg/util"
kotsscheme "github.com/replicatedhq/kotskinds/client/kotsclientset/scheme"
troubleshootscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
Expand All @@ -34,6 +35,8 @@ type KOTSStore struct {
}

func init() {
store.SetStore(StoreFromEnv())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, why the pattern change here? this makes it harder to add more implementations of the store interfaces in the future.


kotsscheme.AddToScheme(scheme.Scheme)
veleroscheme.AddToScheme(scheme.Scheme)
troubleshootscheme.AddToScheme(scheme.Scheme)
Expand Down
4 changes: 0 additions & 4 deletions pkg/store/kotsstore/version_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,6 @@ func (s *KOTSStore) IsRollbackSupportedForVersion(appID string, sequence int64)
return false, errors.Wrap(err, "failed to load kots app from contents")
}

if util.IsEmbeddedCluster() {
return false, nil
}

return kotsAppSpec.Spec.AllowRollback, nil
}

Expand Down
Loading
Loading