Skip to content

Commit

Permalink
Merge branch 'master' into fix-git-urls
Browse files Browse the repository at this point in the history
  • Loading branch information
BKirov authored Apr 4, 2024
2 parents 2a046c2 + 44da206 commit bc87931
Show file tree
Hide file tree
Showing 21 changed files with 1,248 additions and 342 deletions.
1 change: 1 addition & 0 deletions USERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ Currently, the following organizations are **officially** using Argo CD:
1. [QuintoAndar](https://quintoandar.com.br)
1. [Quipper](https://www.quipper.com/)
1. [RapidAPI](https://www.rapidapi.com/)
1. [rebuy](https://www.rebuy.de/)
1. [Recreation.gov](https://www.recreation.gov/)
1. [Red Hat](https://www.redhat.com/)
1. [Redpill Linpro](https://www.redpill-linpro.com/)
Expand Down
90 changes: 82 additions & 8 deletions cmd/argocd/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,8 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
serverSideGenerate bool
localIncludes []string
appNamespace string
revisions []string
sourceIndexes []int64
)
shortDesc := "Perform a diff against the target and live state."
var command = &cobra.Command{
Expand All @@ -1138,6 +1140,11 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
c.HelpFunc()(c, args)
os.Exit(2)
}

if len(revisions) != len(sourceIndexes) {
errors.CheckError(fmt.Errorf("While using revisions and source-indexes, length of values for both flags should be same."))
}

clientset := headless.NewClientOrDie(clientOpts, c)
conn, appIf := clientset.NewApplicationClientOrDie()
defer argoio.Close(conn)
Expand All @@ -1156,7 +1163,27 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
diffOption := &DifferenceOption{}
if revision != "" {
if app.Spec.HasMultipleSources() && len(revisions) > 0 && len(sourceIndexes) > 0 {

revisionSourceMappings := make(map[int64]string, 0)
for i, index := range sourceIndexes {
if index <= 0 {
errors.CheckError(fmt.Errorf("source-index cannot be less than or equal to 0. Index starts at 1."))
}
revisionSourceMappings[index] = revisions[i]
}

q := application.ApplicationManifestQuery{
Name: &appName,
AppNamespace: &appNs,
RevisionSourceMappings: revisionSourceMappings,
}
res, err := appIf.GetManifests(ctx, &q)
errors.CheckError(err)

diffOption.res = res
diffOption.revisionSourceMappings = &revisionSourceMappings
} else if revision != "" {
q := application.ApplicationManifestQuery{
Name: &appName,
Revision: &revision,
Expand Down Expand Up @@ -1206,17 +1233,20 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing")
command.Flags().StringArrayVar(&localIncludes, "local-include", []string{"*.yaml", "*.yml", "*.json"}, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path.")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only render the difference in namespace")
command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for the index of sources in source-indexes")
command.Flags().Int64SliceVar(&sourceIndexes, "source-indexes", []int64{}, "List of source indexes. Default is empty array. Indexes start at 1.")
return command
}

// DifferenceOption struct to store diff options
type DifferenceOption struct {
local string
localRepoRoot string
revision string
cluster *argoappv1.Cluster
res *repoapiclient.ManifestResponse
serversideRes *repoapiclient.ManifestResponse
local string
localRepoRoot string
revision string
cluster *argoappv1.Cluster
res *repoapiclient.ManifestResponse
serversideRes *repoapiclient.ManifestResponse
revisionSourceMappings *map[int64]string
}

// findandPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
Expand All @@ -1228,7 +1258,7 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
if diffOptions.local != "" {
localObjs := groupObjsByKey(getLocalObjects(ctx, app, proj, diffOptions.local, diffOptions.localRepoRoot, argoSettings.AppLabelKey, diffOptions.cluster.Info.ServerVersion, diffOptions.cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod), liveObjs, app.Spec.Destination.Namespace)
items = groupObjsForDiff(resources, localObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
} else if diffOptions.revision != "" {
} else if diffOptions.revision != "" || (diffOptions.revisionSourceMappings != nil) {
var unstructureds []*unstructured.Unstructured
for _, mfst := range diffOptions.res.Manifests {
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
Expand Down Expand Up @@ -2708,23 +2738,41 @@ func NewApplicationManifestsCommand(clientOpts *argocdclient.ClientOptions) *cob
var (
source string
revision string
revisions []string
sourceIndexes []int64
local string
localRepoRoot string
)
var command = &cobra.Command{
Use: "manifests APPNAME",
Short: "Print manifests of an application",
Example: templates.Examples(`
# Get manifests for an application
argocd app manifests my-app
# Get manifests for an application at a specific revision
argocd app manifests my-app --revision 0.0.1
# Get manifests for a multi-source application at specific revisions for specific sources
argocd app manifests my-app --revisions 0.0.1 --source-indexes 1 --revisions 0.0.2 --source-indexes 2
`),
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()

if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}

if len(revisions) != len(sourceIndexes) {
errors.CheckError(fmt.Errorf("While using revisions and source-indexes, length of values for both flags should be same."))
}

appName, appNs := argo.ParseFromQualifiedName(args[0], "")
clientset := headless.NewClientOrDie(clientOpts, c)
conn, appIf := clientset.NewApplicationClientOrDie()
defer argoio.Close(conn)

resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{
ApplicationName: &appName,
AppNamespace: &appNs,
Expand All @@ -2750,6 +2798,30 @@ func NewApplicationManifestsCommand(clientOpts *argocdclient.ClientOptions) *cob

proj := getProject(c, clientOpts, ctx, app.Spec.Project)
unstructureds = getLocalObjects(context.Background(), app, proj.Project, local, localRepoRoot, argoSettings.AppLabelKey, cluster.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod)
} else if len(revisions) > 0 && len(sourceIndexes) > 0 {

revisionSourceMappings := make(map[int64]string, 0)
for i, index := range sourceIndexes {
if index <= 0 {
errors.CheckError(fmt.Errorf("source-index cannot be less than or equal to 0, Index starts at 1"))
}
revisionSourceMappings[index] = revisions[i]
}

q := application.ApplicationManifestQuery{
Name: &appName,
AppNamespace: &appNs,
Revision: pointer.String(revision),
RevisionSourceMappings: revisionSourceMappings,
}
res, err := appIf.GetManifests(ctx, &q)
errors.CheckError(err)

for _, mfst := range res.Manifests {
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
errors.CheckError(err)
unstructureds = append(unstructureds, obj)
}
} else if revision != "" {
q := application.ApplicationManifestQuery{
Name: &appName,
Expand Down Expand Up @@ -2787,6 +2859,8 @@ func NewApplicationManifestsCommand(clientOpts *argocdclient.ClientOptions) *cob
}
command.Flags().StringVar(&source, "source", "git", "Source of manifests. One of: live|git")
command.Flags().StringVar(&revision, "revision", "", "Show manifests at a specific revision")
command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for the index of sources in source-indexes")
command.Flags().Int64SliceVar(&sourceIndexes, "source-indexes", []int64{}, "List of source indexes. Default is empty array. Indexes start at 1.")
command.Flags().StringVar(&local, "local", "", "If set, show locally-generated manifests. Value is the absolute path to app manifests within the manifest repo. Example: '/home/username/apps/env/app-1'.")
command.Flags().StringVar(&localRepoRoot, "local-repo-root", ".", "Path to the local repository root. Used together with --local allows setting the repository root. Example: '/home/username/apps'.")
return command
Expand Down
117 changes: 34 additions & 83 deletions controller/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package controller

import (
"context"
"encoding/json"
goerrors "errors"
"fmt"
"os"
Expand All @@ -11,6 +10,7 @@ import (
"time"

cdcommon "github.com/argoproj/argo-cd/v2/common"
"k8s.io/apimachinery/pkg/util/strategicpatch"

"github.com/argoproj/gitops-engine/pkg/sync"
"github.com/argoproj/gitops-engine/pkg/sync/common"
Expand All @@ -21,6 +21,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/kubectl/pkg/util/openapi"

"github.com/argoproj/argo-cd/v2/controller/metrics"
Expand Down Expand Up @@ -405,11 +406,10 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
}
}

// normalizeTargetResources will apply the diff normalization in all live and target resources.
// Then it calculates the merge patch between the normalized live and the current live resources.
// Finally it applies the merge patch in the normalized target resources. This is done to ensure
// that target resources have the same ignored diff fields values from live ones to avoid them to
// be applied in the cluster. Returns the list of normalized target resources.
// normalizeTargetResources modifies target resources to ensure ignored fields are not touched during synchronization:
// - applies normalization to the target resources based on the live resources
// - copies ignored fields from the matching live resources: apply normalizer to the live resource,
// calculates the patch performed by normalizer and applies the patch to the target resource
func normalizeTargetResources(cr *comparisonResult) ([]*unstructured.Unstructured, error) {
// normalize live and target resources
normalized, err := diff.Normalize(cr.reconciliationResult.Live, cr.reconciliationResult.Target, cr.diffConfig)
Expand All @@ -428,94 +428,35 @@ func normalizeTargetResources(cr *comparisonResult) ([]*unstructured.Unstructure
patchedTargets = append(patchedTargets, originalTarget)
continue
}
// calculate targetPatch between normalized and target resource
targetPatch, err := getMergePatch(normalizedTarget, originalTarget)
if err != nil {
return nil, err
}

// check if there is a patch to apply. An empty patch is identified by a '{}' string.
if len(targetPatch) > 2 {
livePatch, err := getMergePatch(normalized.Lives[idx], live)
if err != nil {
return nil, err
}
// generate a minimal patch that uses the fields from targetPatch (template)
// with livePatch values
patch, err := compilePatch(targetPatch, livePatch)
var lookupPatchMeta *strategicpatch.PatchMetaFromStruct
versionedObject, err := scheme.Scheme.New(normalizedTarget.GroupVersionKind())
if err == nil {
meta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
if err != nil {
return nil, err
}
normalizedTarget, err = applyMergePatch(normalizedTarget, patch)
if err != nil {
return nil, err
}
} else {
// if there is no patch just use the original target
normalizedTarget = originalTarget
lookupPatchMeta = &meta
}
patchedTargets = append(patchedTargets, normalizedTarget)
}
return patchedTargets, nil
}

// compilePatch will generate a patch using the fields from templatePatch with
// the values from valuePatch.
func compilePatch(templatePatch, valuePatch []byte) ([]byte, error) {
templateMap := make(map[string]interface{})
err := json.Unmarshal(templatePatch, &templateMap)
if err != nil {
return nil, err
}
valueMap := make(map[string]interface{})
err = json.Unmarshal(valuePatch, &valueMap)
if err != nil {
return nil, err
}
resultMap := intersectMap(templateMap, valueMap)
return json.Marshal(resultMap)
}
livePatch, err := getMergePatch(normalized.Lives[idx], live, lookupPatchMeta)
if err != nil {
return nil, err
}

// intersectMap will return map with the fields intersection from the 2 provided
// maps populated with the valueMap values.
func intersectMap(templateMap, valueMap map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range templateMap {
if innerTMap, ok := v.(map[string]interface{}); ok {
if innerVMap, ok := valueMap[k].(map[string]interface{}); ok {
result[k] = intersectMap(innerTMap, innerVMap)
}
} else if innerTSlice, ok := v.([]interface{}); ok {
if innerVSlice, ok := valueMap[k].([]interface{}); ok {
items := []interface{}{}
for idx, innerTSliceValue := range innerTSlice {
if idx < len(innerVSlice) {
if tSliceValueMap, ok := innerTSliceValue.(map[string]interface{}); ok {
if vSliceValueMap, ok := innerVSlice[idx].(map[string]interface{}); ok {
item := intersectMap(tSliceValueMap, vSliceValueMap)
items = append(items, item)
}
} else {
items = append(items, innerVSlice[idx])
}
}
}
if len(items) > 0 {
result[k] = items
}
}
} else {
if _, ok := valueMap[k]; ok {
result[k] = valueMap[k]
}
normalizedTarget, err = applyMergePatch(normalizedTarget, livePatch, versionedObject)
if err != nil {
return nil, err
}

patchedTargets = append(patchedTargets, normalizedTarget)
}
return result
return patchedTargets, nil
}

// getMergePatch calculates and returns the patch between the original and the
// modified unstructures.
func getMergePatch(original, modified *unstructured.Unstructured) ([]byte, error) {
func getMergePatch(original, modified *unstructured.Unstructured, lookupPatchMeta *strategicpatch.PatchMetaFromStruct) ([]byte, error) {
originalJSON, err := original.MarshalJSON()
if err != nil {
return nil, err
Expand All @@ -524,20 +465,30 @@ func getMergePatch(original, modified *unstructured.Unstructured) ([]byte, error
if err != nil {
return nil, err
}
if lookupPatchMeta != nil {
return strategicpatch.CreateThreeWayMergePatch(modifiedJSON, modifiedJSON, originalJSON, lookupPatchMeta, true)
}

return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
}

// applyMergePatch will apply the given patch in the obj and return the patched
// unstructure.
func applyMergePatch(obj *unstructured.Unstructured, patch []byte) (*unstructured.Unstructured, error) {
func applyMergePatch(obj *unstructured.Unstructured, patch []byte, versionedObject interface{}) (*unstructured.Unstructured, error) {
originalJSON, err := obj.MarshalJSON()
if err != nil {
return nil, err
}
patchedJSON, err := jsonpatch.MergePatch(originalJSON, patch)
var patchedJSON []byte
if versionedObject == nil {
patchedJSON, err = jsonpatch.MergePatch(originalJSON, patch)
} else {
patchedJSON, err = strategicpatch.StrategicMergePatch(originalJSON, patch, versionedObject)
}
if err != nil {
return nil, err
}

patchedObj := &unstructured.Unstructured{}
_, _, err = unstructured.UnstructuredJSONScheme.Decode(patchedJSON, nil, patchedObj)
if err != nil {
Expand Down
Loading

0 comments on commit bc87931

Please sign in to comment.