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 11 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
5 changes: 5 additions & 0 deletions cmd/kotsadm/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"os"
"strings"

"github.com/replicatedhq/kots/pkg/store"
"github.com/replicatedhq/kots/pkg/store/kotsstore"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -15,6 +17,9 @@ func RootCmd() *cobra.Command {
Short: "kotsadm is the Admin Console for KOTS",
Long: ``,
Args: cobra.MinimumNArgs(1),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
store.SetStore(kotsstore.StoreFromEnv())
},
PreRun: func(cmd *cobra.Command, args []string) {
viper.BindPFlags(cmd.Flags())
},
Expand Down
6 changes: 1 addition & 5 deletions pkg/apparchive/helm-v1beta1.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ import (
)

var (
goTemplateRegex *regexp.Regexp
)

func init() {
goTemplateRegex = regexp.MustCompile(`({{)|(}})`)
}
)

func GetRenderedV1Beta1ChartsArchive(versionArchive string, downstreamName, kustomizeBinPath string) ([]byte, map[string][]byte, error) {
renderedChartsDir := filepath.Join(versionArchive, "rendered", downstreamName, "charts")
Expand Down
81 changes: 65 additions & 16 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,7 +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)
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)
}
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

}

Expand Down Expand Up @@ -676,7 +683,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 +876,32 @@ 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(
versionStore store.VersionStore,
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,18 +920,41 @@ func isAppVersionDeployable(version *downstreamtypes.DownstreamVersion, appVersi
break
}
}

// This is a past version
if versionIndex > deployedVersionIndex {
// this is a past version
// rollback support is based off of the latest downloaded version
// Rollback support is based off of the latest downloaded version so that a vendor can
// toggle on support without requiring the end user to deploy a new version.
for _, v := range appVersions.AllVersions {
// Find the first version that is not pending download. This will be the latest
// version.
if v.Status == types.VersionPendingDownload {
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() && appVersions.CurrentVersion != nil {
currentECConfig, err := getRawEmbeddedClusterConfigForVersion(versionStore, appID, appVersions.CurrentVersion.Sequence)
if err != nil {
return false, "", errors.Wrapf(err, "failed to get embedded cluster config for current version %d", appVersions.CurrentVersion.Sequence)
}
newECConfig, err := getRawEmbeddedClusterConfigForVersion(versionStore, appID, version.Sequence)
if err != nil {
return false, "", errors.Wrapf(err, "failed to get embedded cluster config for version %d", version.Sequence)
}
if util.IsEmbeddedCluster() && currentECConfig != nil {
// 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.
if !bytes.Equal(currentECConfig, newECConfig) {
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 +988,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 +1006,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 +1034,24 @@ 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
}

func getRawEmbeddedClusterConfigForVersion(versionStore store.VersionStore, appID string, sequence int64) ([]byte, error) {
currentConf, err := versionStore.GetEmbeddedClusterConfigForVersion(appID, sequence)
if err != nil {
return nil, errors.Wrap(err, "failed to get embedded cluster config")
}
b, err := json.Marshal(currentConf)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal embedded cluster config")
}
return b, nil
}

func getReleaseNotes(appID string, parentSequence int64) (string, error) {
Expand Down
Loading
Loading