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(application-controller): Add support for rollback multi-source applications #14124

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
38 changes: 38 additions & 0 deletions assets/swagger.json
Expand Up @@ -1633,6 +1633,20 @@
"type": "string",
"name": "project",
"in": "query"
},
{
"type": "integer",
"format": "int32",
"description": "source index (for multi source apps).",
"name": "sourceIndex",
"in": "query"
},
{
"type": "integer",
"format": "int32",
"description": "versionId from historical data (for multi source apps).",
"name": "versionId",
"in": "query"
}
],
"responses": {
Expand Down Expand Up @@ -1683,6 +1697,20 @@
"type": "string",
"name": "project",
"in": "query"
},
{
"type": "integer",
"format": "int32",
"description": "source index (for multi source apps).",
"name": "sourceIndex",
"in": "query"
},
{
"type": "integer",
"format": "int32",
"description": "versionId from historical data (for multi source apps).",
"name": "versionId",
"in": "query"
}
],
"responses": {
Expand Down Expand Up @@ -5050,6 +5078,16 @@
},
"source": {
"$ref": "#/definitions/v1alpha1ApplicationSource"
},
"sourceIndex": {
"type": "integer",
"format": "int32",
"title": "source index (for multi source apps)"
},
"versionId": {
"type": "integer",
"format": "int32",
"title": "versionId from historical data (for multi source apps)"
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/argocd/commands/admin/app.go
Expand Up @@ -431,7 +431,7 @@ func reconcileApplications(
sources = append(sources, app.Spec.GetSource())
revisions = append(revisions, app.Spec.GetSource().TargetRevision)

res, err := appStateManager.CompareAppState(&app, proj, revisions, sources, false, false, nil, false)
res, err := appStateManager.CompareAppState(&app, proj, revisions, sources, false, false, nil, false, false)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/argocd/commands/app.go
Expand Up @@ -640,7 +640,7 @@ func printAppSourceDetails(appSrc *argoappv1.ApplicationSource) {
if appSrc.Path != "" {
fmt.Printf(printOpFmtStr, " Path:", appSrc.Path)
}
if appSrc.Ref != "" {
if appSrc.IsRef() {
fmt.Printf(printOpFmtStr, " Ref:", appSrc.Ref)
}
if appSrc.Helm != nil && len(appSrc.Helm.ValueFiles) > 0 {
Expand Down Expand Up @@ -920,7 +920,7 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C

func unset(source *argoappv1.ApplicationSource, opts unsetOpts) (updated bool, nothingToUnset bool) {
needToUnsetRef := false
if opts.ref && source.Ref != "" {
if opts.ref && source.IsRef() {
source.Ref = ""
updated = true
needToUnsetRef = true
Expand Down
36 changes: 36 additions & 0 deletions cmd/argocd/commands/app_test.go
Expand Up @@ -212,6 +212,42 @@ func TestPrintTreeViewDetailedAppGet(t *testing.T) {
assert.Contains(t, output, "numalogic-rollout-demo-5dcd5457d5-6trpt")
assert.Contains(t, output, "Degraded")
assert.Contains(t, output, "Readiness Gate failed")
}

func TestFindRevisionHistoryWithoutPassedIdWithMultipleSources(t *testing.T) {

histories := v1alpha1.RevisionHistories{}

histories = append(histories, v1alpha1.RevisionHistory{ID: 1})
histories = append(histories, v1alpha1.RevisionHistory{ID: 2})
histories = append(histories, v1alpha1.RevisionHistory{ID: 3})

status := v1alpha1.ApplicationStatus{
Resources: nil,
Sync: v1alpha1.SyncStatus{},
Health: v1alpha1.HealthStatus{},
History: histories,
Conditions: nil,
ReconciledAt: nil,
OperationState: nil,
ObservedAt: nil,
SourceType: "",
Summary: v1alpha1.ApplicationSummary{},
}

application := v1alpha1.Application{
Status: status,
}

history, err := findRevisionHistory(&application, -1)

if err != nil {
t.Fatal("Find revision history should fail without errors")
}

if history == nil {
t.Fatal("History should be found")
}

}

Expand Down
2 changes: 1 addition & 1 deletion controller/appcontroller.go
Expand Up @@ -1581,7 +1581,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo

compareResult, err := ctrl.appStateManager.CompareAppState(app, project, revisions, sources,
refreshType == appv1.RefreshTypeHard,
comparisonLevel == CompareWithLatestForceResolve, localManifests, hasMultipleSources)
comparisonLevel == CompareWithLatestForceResolve, localManifests, hasMultipleSources, false)

if goerrors.Is(err, CompareStateRepoError) {
logCtx.Warnf("Ignoring temporary failed attempt to compare app state against repo: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion controller/hook.go
Expand Up @@ -51,7 +51,7 @@ func (ctrl *ApplicationController) executePostDeleteHooks(app *v1alpha1.Applicat
revisions = append(revisions, src.TargetRevision)
}

targets, _, err := ctrl.appStateManager.GetRepoObjs(app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj)
targets, _, err := ctrl.appStateManager.GetRepoObjs(app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj, false)
if err != nil {
return false, err
}
Expand Down
20 changes: 13 additions & 7 deletions controller/state.go
Expand Up @@ -70,9 +70,9 @@ type managedResource struct {

// AppStateManager defines methods which allow to compare application spec and actual application state.
type AppStateManager interface {
CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool) (*comparisonResult, error)
CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool, rollback bool) (*comparisonResult, error)
SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error)
GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, rollback bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error)
}

// comparisonResult holds the state of an application after the reconciliation
Expand Down Expand Up @@ -124,7 +124,7 @@ type appStateManager struct {
// task to the repo-server. It returns the list of generated manifests as unstructured
// objects. It also returns the full response from all calls to the repo server as the
// second argument.
func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error) {
func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, rollback bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error) {
ts := stats.NewTimingStats()
helmRepos, err := m.db.ListHelmRepositories(context.Background())
if err != nil {
Expand Down Expand Up @@ -176,7 +176,14 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
targetObjs := make([]*unstructured.Unstructured, 0)

// Store the map of all sources having ref field into a map for applications with sources field
refSources, err := argo.GetRefSources(context.Background(), app.Spec, m.db)
// If it's for a rollback process, the refSources[*].targetRevision fields are the desired
// revisions for the rollback
refSources, err := argo.GetRefSources(context.Background(), argo.GetRefSourcesOptions{
Sources: sources,
Db: m.db,
Revisions: revisions,
IsRollback: rollback,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to get ref sources: %v", err)
}
Expand Down Expand Up @@ -262,7 +269,6 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
return nil, nil, fmt.Errorf("failed to unmarshal manifests for source %d of %d: %w", i+1, len(sources), err)
}
targetObjs = append(targetObjs, targetObj...)

manifestInfos = append(manifestInfos, manifestInfo)
}

Expand Down Expand Up @@ -393,7 +399,7 @@ func isManagedNamespace(ns *unstructured.Unstructured, app *v1alpha1.Application
// CompareAppState compares application git state to the live app state, using the specified
// revision and supplied source. If revision or overrides are empty, then compares against
// revision and overrides in the app spec.
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string, hasMultipleSources bool) (*comparisonResult, error) {
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string, hasMultipleSources bool, rollback bool) (*comparisonResult, error) {
ts := stats.NewTimingStats()
appLabelKey, resourceOverrides, resFilter, err := m.getComparisonSettings()

Expand Down Expand Up @@ -451,7 +457,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
}
}

targetObjs, manifestInfos, err = m.GetRepoObjs(app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project)
targetObjs, manifestInfos, err = m.GetRepoObjs(app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project, rollback)
if err != nil {
targetObjs = make([]*unstructured.Unstructured, 0)
msg := fmt.Sprintf("Failed to load target state: %s", err.Error())
Expand Down