2020-06-01 15:54:33 +00:00
/ *
The package provide functions that allows to compare set of Kubernetes resources using the logic equivalent to
` kubectl diff ` .
* /
2019-11-12 17:51:26 +00:00
package diff
import (
2020-10-27 00:14:56 +00:00
"bytes"
2023-12-18 19:45:13 +00:00
"context"
2025-01-29 15:51:13 +00:00
"encoding/base64"
2019-11-12 17:51:26 +00:00
"encoding/json"
"errors"
"fmt"
"reflect"
2020-05-15 20:01:24 +00:00
jsonpatch "github.com/evanphx/json-patch"
2019-11-12 17:51:26 +00:00
corev1 "k8s.io/api/core/v1"
2022-10-04 13:23:20 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2019-11-12 17:51:26 +00:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
2020-05-15 20:01:24 +00:00
"k8s.io/apimachinery/pkg/util/jsonmergepatch"
2022-08-02 18:48:09 +00:00
"k8s.io/apimachinery/pkg/util/managedfields"
2019-11-12 17:51:26 +00:00
"k8s.io/apimachinery/pkg/util/strategicpatch"
2020-05-15 20:01:24 +00:00
"k8s.io/client-go/kubernetes/scheme"
2022-08-02 18:48:09 +00:00
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
2022-10-04 13:23:20 +00:00
"sigs.k8s.io/structured-merge-diff/v4/merge"
2022-08-02 18:48:09 +00:00
"sigs.k8s.io/structured-merge-diff/v4/typed"
2019-11-12 17:51:26 +00:00
2021-04-20 17:54:02 +00:00
"github.com/argoproj/gitops-engine/internal/kubernetes_vendor/pkg/api/v1/endpoints"
2022-10-04 13:23:20 +00:00
"github.com/argoproj/gitops-engine/pkg/diff/internal/fieldmanager"
2022-08-02 18:48:09 +00:00
"github.com/argoproj/gitops-engine/pkg/sync/resource"
2020-05-15 20:01:24 +00:00
jsonutil "github.com/argoproj/gitops-engine/pkg/utils/json"
2022-08-02 18:48:09 +00:00
gescheme "github.com/argoproj/gitops-engine/pkg/utils/kube/scheme"
2019-11-12 17:51:26 +00:00
)
2022-09-16 14:22:00 +00:00
const (
couldNotMarshalErrMsg = "Could not unmarshal to object of type %s: %v"
AnnotationLastAppliedConfig = "kubectl.kubernetes.io/last-applied-configuration"
2024-10-29 10:29:52 +00:00
replacement = "++++++++"
2022-09-16 14:22:00 +00:00
)
2020-10-13 23:53:40 +00:00
2020-06-01 15:54:33 +00:00
// Holds diffing result of two resources
2019-11-12 17:51:26 +00:00
type DiffResult struct {
2020-06-01 15:54:33 +00:00
// Modified is set to true if resources are not matching
Modified bool
// Contains YAML representation of a live resource with applied normalizations
2020-05-15 20:01:24 +00:00
NormalizedLive [ ] byte
2020-06-01 15:54:33 +00:00
// Contains "expected" YAML representation of a live resource
PredictedLive [ ] byte
2019-11-12 17:51:26 +00:00
}
2020-06-01 15:54:33 +00:00
// Holds result of two resources sets comparison
2019-11-12 17:51:26 +00:00
type DiffResultList struct {
Diffs [ ] DiffResult
Modified bool
}
2025-02-06 22:07:19 +00:00
type noopNormalizer struct { }
2020-05-27 16:53:21 +00:00
2025-02-07 14:55:39 +00:00
func ( n * noopNormalizer ) Normalize ( _ * unstructured . Unstructured ) error {
2020-05-27 16:53:21 +00:00
return nil
}
// Normalizer updates resource before comparing it
2019-11-12 17:51:26 +00:00
type Normalizer interface {
Normalize ( un * unstructured . Unstructured ) error
}
2020-05-27 16:53:21 +00:00
// GetNoopNormalizer returns normalizer that does not apply any resource modifications
func GetNoopNormalizer ( ) Normalizer {
return & noopNormalizer { }
}
2019-11-12 17:51:26 +00:00
// Diff performs a diff on two unstructured objects. If the live object happens to have a
// "kubectl.kubernetes.io/last-applied-configuration", then perform a three way diff.
2020-10-27 00:14:56 +00:00
func Diff ( config , live * unstructured . Unstructured , opts ... Option ) ( * DiffResult , error ) {
o := applyOptions ( opts )
2019-11-12 17:51:26 +00:00
if config != nil {
2020-10-27 00:14:56 +00:00
config = remarshal ( config , o )
Normalize ( config , opts ... )
2019-11-12 17:51:26 +00:00
}
if live != nil {
2020-10-27 00:14:56 +00:00
live = remarshal ( live , o )
Normalize ( live , opts ... )
2019-11-12 17:51:26 +00:00
}
2022-08-02 18:48:09 +00:00
2023-12-18 19:45:13 +00:00
if o . serverSideDiff {
r , err := ServerSideDiff ( config , live , opts ... )
if err != nil {
return nil , fmt . Errorf ( "error calculating server side diff: %w" , err )
}
return r , nil
}
2022-08-02 18:48:09 +00:00
// TODO The two variables bellow are necessary because there is a cyclic
// dependency with the kube package that blocks the usage of constants
// from common package. common package needs to be refactored and exclude
// dependency from kube.
syncOptAnnotation := "argocd.argoproj.io/sync-options"
ssaAnnotation := "ServerSideApply=true"
// structuredMergeDiff is mainly used as a feature flag to enable
// calculating diffs using the structured-merge-diff library
// used in k8s while performing server-side applies. It checks the
// given diff Option or if the desired state resource has the
// Server-Side apply sync option annotation enabled.
structuredMergeDiff := o . structuredMergeDiff ||
( config != nil && resource . HasAnnotationOption ( config , syncOptAnnotation , ssaAnnotation ) )
if structuredMergeDiff {
r , err := StructuredMergeDiff ( config , live , o . gvkParser , o . manager )
if err != nil {
return nil , fmt . Errorf ( "error calculating structured merge diff: %w" , err )
}
return r , nil
}
2020-10-27 00:14:56 +00:00
orig , err := GetLastAppliedConfigAnnotation ( live )
if err != nil {
o . log . V ( 1 ) . Info ( fmt . Sprintf ( "Failed to get last applied configuration: %v" , err ) )
} else {
if orig != nil && config != nil {
Normalize ( orig , opts ... )
dr , err := ThreeWayDiff ( orig , config , live )
if err == nil {
return dr , nil
}
2020-10-30 19:46:27 +00:00
o . log . V ( 1 ) . Info ( fmt . Sprintf ( "three-way diff calculation failed: %v. Falling back to two-way diff" , err ) )
2019-11-12 17:51:26 +00:00
}
}
return TwoWayDiff ( config , live )
}
2023-12-18 19:45:13 +00:00
// ServerSideDiff will execute a k8s server-side apply in dry-run mode with the
// given config. The result will be compared with given live resource to determine
// diff. If config or live are nil it means resource creation or deletion. In this
// no call will be made to kube-api and a simple diff will be returned.
func ServerSideDiff ( config , live * unstructured . Unstructured , opts ... Option ) ( * DiffResult , error ) {
if live != nil && config != nil {
result , err := serverSideDiff ( config , live , opts ... )
if err != nil {
return nil , fmt . Errorf ( "serverSideDiff error: %w" , err )
}
return result , nil
}
// Currently, during resource creation a shallow diff (non ServerSide apply
// based) will be returned. The reasons are:
// - Saves 1 additional call to KubeAPI
// - Much lighter/faster diff
// - This is the existing behaviour users are already used to
// - No direct benefit to the user
result , err := handleResourceCreateOrDeleteDiff ( config , live )
if err != nil {
return nil , fmt . Errorf ( "error handling resource creation or deletion: %w" , err )
}
return result , nil
}
// ServerSideDiff will execute a k8s server-side apply in dry-run mode with the
// given config. The result will be compared with given live resource to determine
// diff. Modifications done by mutation webhooks are removed from the diff by default.
// This behaviour can be customized with Option.WithIgnoreMutationWebhook.
func serverSideDiff ( config , live * unstructured . Unstructured , opts ... Option ) ( * DiffResult , error ) {
o := applyOptions ( opts )
if o . serverSideDryRunner == nil {
return nil , fmt . Errorf ( "serverSideDryRunner is null" )
}
predictedLiveStr , err := o . serverSideDryRunner . Run ( context . Background ( ) , config , o . manager )
if err != nil {
2024-01-22 14:58:03 +00:00
return nil , fmt . Errorf ( "error running server side apply in dryrun mode for resource %s/%s: %w" , config . GetKind ( ) , config . GetName ( ) , err )
2023-12-18 19:45:13 +00:00
}
predictedLive , err := jsonStrToUnstructured ( predictedLiveStr )
if err != nil {
2024-01-22 14:58:03 +00:00
return nil , fmt . Errorf ( "error converting json string to unstructured for resource %s/%s: %w" , config . GetKind ( ) , config . GetName ( ) , err )
2023-12-18 19:45:13 +00:00
}
if o . ignoreMutationWebhook {
predictedLive , err = removeWebhookMutation ( predictedLive , live , o . gvkParser , o . manager )
if err != nil {
2024-01-22 14:58:03 +00:00
return nil , fmt . Errorf ( "error removing non config mutations for resource %s/%s: %w" , config . GetKind ( ) , config . GetName ( ) , err )
2023-12-18 19:45:13 +00:00
}
}
Normalize ( predictedLive , opts ... )
unstructured . RemoveNestedField ( predictedLive . Object , "metadata" , "managedFields" )
predictedLiveBytes , err := json . Marshal ( predictedLive )
if err != nil {
2024-01-22 14:58:03 +00:00
return nil , fmt . Errorf ( "error marshaling predicted live for resource %s/%s: %w" , config . GetKind ( ) , config . GetName ( ) , err )
2023-12-18 19:45:13 +00:00
}
unstructured . RemoveNestedField ( live . Object , "metadata" , "managedFields" )
liveBytes , err := json . Marshal ( live )
if err != nil {
2024-01-22 14:58:03 +00:00
return nil , fmt . Errorf ( "error marshaling live resource %s/%s: %w" , config . GetKind ( ) , config . GetName ( ) , err )
2023-12-18 19:45:13 +00:00
}
return buildDiffResult ( predictedLiveBytes , liveBytes ) , nil
}
// removeWebhookMutation will compare the predictedLive with live to identify
// changes done by mutation webhooks. Webhook mutations are identified by finding
// changes in predictedLive fields not associated with any manager in the
// managedFields. All fields under this condition will be reverted with their state
// from live. If the given predictedLive does not have the managedFields, an error
// will be returned.
2025-02-07 14:55:39 +00:00
func removeWebhookMutation ( predictedLive , live * unstructured . Unstructured , gvkParser * managedfields . GvkParser , _ string ) ( * unstructured . Unstructured , error ) {
2023-12-18 19:45:13 +00:00
plManagedFields := predictedLive . GetManagedFields ( )
if len ( plManagedFields ) == 0 {
return nil , fmt . Errorf ( "predictedLive for resource %s/%s must have the managedFields" , predictedLive . GetKind ( ) , predictedLive . GetName ( ) )
}
gvk := predictedLive . GetObjectKind ( ) . GroupVersionKind ( )
pt := gvkParser . Type ( gvk )
2024-05-09 17:07:15 +00:00
if pt == nil {
return nil , fmt . Errorf ( "unable to resolve parseableType for GroupVersionKind: %s" , gvk )
}
2023-12-18 19:45:13 +00:00
typedPredictedLive , err := pt . FromUnstructured ( predictedLive . Object )
if err != nil {
return nil , fmt . Errorf ( "error converting predicted live state from unstructured to %s: %w" , gvk , err )
}
typedLive , err := pt . FromUnstructured ( live . Object )
if err != nil {
return nil , fmt . Errorf ( "error converting live state from unstructured to %s: %w" , gvk , err )
}
// Compare the predicted live with the live resource
comparison , err := typedLive . Compare ( typedPredictedLive )
if err != nil {
return nil , fmt . Errorf ( "error comparing predicted resource to live resource: %w" , err )
}
// Loop over all existing managers in predicted live resource to identify
// fields mutated (in predicted live) not owned by any manager.
for _ , mfEntry := range plManagedFields {
mfs := & fieldpath . Set { }
err := mfs . FromJSON ( bytes . NewReader ( mfEntry . FieldsV1 . Raw ) )
if err != nil {
return nil , fmt . Errorf ( "error building managedFields set: %s" , err )
}
if comparison . Added != nil && ! comparison . Added . Empty ( ) {
// exclude the added fields owned by this manager from the comparison
comparison . Added = comparison . Added . Difference ( mfs )
}
if comparison . Modified != nil && ! comparison . Modified . Empty ( ) {
// exclude the modified fields owned by this manager from the comparison
comparison . Modified = comparison . Modified . Difference ( mfs )
}
if comparison . Removed != nil && ! comparison . Removed . Empty ( ) {
// exclude the removed fields owned by this manager from the comparison
comparison . Removed = comparison . Removed . Difference ( mfs )
}
}
// At this point, comparison holds all mutations that aren't owned by any
// of the existing managers.
if comparison . Added != nil && ! comparison . Added . Empty ( ) {
// remove added fields that aren't owned by any manager
typedPredictedLive = typedPredictedLive . RemoveItems ( comparison . Added )
}
if comparison . Modified != nil && ! comparison . Modified . Empty ( ) {
2024-12-11 20:28:47 +00:00
liveModValues := typedLive . ExtractItems ( comparison . Modified , typed . WithAppendKeyFields ( ) )
2023-12-18 19:45:13 +00:00
// revert modified fields not owned by any manager
typedPredictedLive , err = typedPredictedLive . Merge ( liveModValues )
if err != nil {
return nil , fmt . Errorf ( "error reverting webhook modified fields in predicted live resource: %s" , err )
}
}
if comparison . Removed != nil && ! comparison . Removed . Empty ( ) {
2024-12-11 20:28:47 +00:00
liveRmValues := typedLive . ExtractItems ( comparison . Removed , typed . WithAppendKeyFields ( ) )
2023-12-18 19:45:13 +00:00
// revert removed fields not owned by any manager
typedPredictedLive , err = typedPredictedLive . Merge ( liveRmValues )
if err != nil {
return nil , fmt . Errorf ( "error reverting webhook removed fields in predicted live resource: %s" , err )
}
}
plu := typedPredictedLive . AsValue ( ) . Unstructured ( )
2025-02-06 23:20:29 +00:00
pl , ok := plu . ( map [ string ] any )
2023-12-18 19:45:13 +00:00
if ! ok {
return nil , fmt . Errorf ( "error converting live typedValue: expected map got %T" , plu )
}
return & unstructured . Unstructured { Object : pl } , nil
}
func jsonStrToUnstructured ( jsonString string ) ( * unstructured . Unstructured , error ) {
2025-02-06 23:20:29 +00:00
res := make ( map [ string ] any )
2023-12-18 19:45:13 +00:00
err := json . Unmarshal ( [ ] byte ( jsonString ) , & res )
if err != nil {
return nil , fmt . Errorf ( "unmarshal error: %s" , err )
}
return & unstructured . Unstructured { Object : res } , nil
}
2022-08-02 18:48:09 +00:00
// StructuredMergeDiff will calculate the diff using the structured-merge-diff
// k8s library (https://github.com/kubernetes-sigs/structured-merge-diff).
func StructuredMergeDiff ( config , live * unstructured . Unstructured , gvkParser * managedfields . GvkParser , manager string ) ( * DiffResult , error ) {
if live != nil && config != nil {
2022-10-04 13:23:20 +00:00
params := & SMDParams {
config : config ,
live : live ,
gvkParser : gvkParser ,
manager : manager ,
}
return structuredMergeDiff ( params )
2022-08-02 18:48:09 +00:00
}
return handleResourceCreateOrDeleteDiff ( config , live )
}
2022-10-04 13:23:20 +00:00
// SMDParams defines the parameters required by the structuredMergeDiff
// function
type SMDParams struct {
config * unstructured . Unstructured
live * unstructured . Unstructured
gvkParser * managedfields . GvkParser
manager string
}
func structuredMergeDiff ( p * SMDParams ) ( * DiffResult , error ) {
gvk := p . config . GetObjectKind ( ) . GroupVersionKind ( )
pt := gescheme . ResolveParseableType ( gvk , p . gvkParser )
2024-05-09 17:07:15 +00:00
if pt == nil {
return nil , fmt . Errorf ( "unable to resolve parseableType for GroupVersionKind: %s" , gvk )
}
2022-10-04 13:23:20 +00:00
// Build typed value from live and config unstructures
tvLive , err := pt . FromUnstructured ( p . live . Object )
2022-08-02 18:48:09 +00:00
if err != nil {
return nil , fmt . Errorf ( "error building typed value from live resource: %w" , err )
}
2022-10-04 13:23:20 +00:00
tvConfig , err := pt . FromUnstructured ( p . config . Object )
2022-08-02 18:48:09 +00:00
if err != nil {
return nil , fmt . Errorf ( "error building typed value from config resource: %w" , err )
}
2022-10-04 13:23:20 +00:00
// Invoke the apply function to calculate the diff using
// the structured-merge-diff library
mergedLive , err := apply ( tvConfig , tvLive , p )
if err != nil {
return nil , fmt . Errorf ( "error calculating diff: %w" , err )
2022-08-02 18:48:09 +00:00
}
2022-10-04 13:23:20 +00:00
// When mergedLive is nil it means that there is no change
if mergedLive == nil {
liveBytes , err := json . Marshal ( p . live )
2022-08-02 18:48:09 +00:00
if err != nil {
2022-10-04 13:23:20 +00:00
return nil , fmt . Errorf ( "error marshaling live resource: %w" , err )
2022-08-02 18:48:09 +00:00
}
2022-10-04 13:23:20 +00:00
// In this case diff result will have live state for both,
// predicted and live.
return buildDiffResult ( liveBytes , liveBytes ) , nil
2022-08-02 18:48:09 +00:00
}
2022-10-04 13:23:20 +00:00
// Normalize merged live
predictedLive , err := normalizeTypedValue ( mergedLive )
2022-08-02 18:48:09 +00:00
if err != nil {
2022-08-03 14:57:58 +00:00
return nil , fmt . Errorf ( "error applying default values in predicted live: %w" , err )
2022-08-02 18:48:09 +00:00
}
2022-10-04 13:23:20 +00:00
// Normalize live
2022-09-16 14:22:00 +00:00
taintedLive , err := normalizeTypedValue ( tvLive )
2022-08-03 14:57:58 +00:00
if err != nil {
return nil , fmt . Errorf ( "error applying default values in live: %w" , err )
}
return buildDiffResult ( predictedLive , taintedLive ) , nil
2022-08-02 18:48:09 +00:00
}
2022-10-04 13:23:20 +00:00
// apply will build all the dependency required to invoke the smd.merge.updater.Apply
// to correctly calculate the diff with the same logic used in k8s with server-side
// apply.
func apply ( tvConfig , tvLive * typed . TypedValue , p * SMDParams ) ( * typed . TypedValue , error ) {
// Build the structured-merge-diff Updater
updater := merge . Updater {
Converter : fieldmanager . NewVersionConverter ( p . gvkParser , scheme . Scheme , p . config . GroupVersionKind ( ) . GroupVersion ( ) ) ,
}
// Build a list of managers and which API version they own
managed , err := fieldmanager . DecodeManagedFields ( p . live . GetManagedFields ( ) )
if err != nil {
return nil , fmt . Errorf ( "error decoding managed fields: %w" , err )
}
// Use the desired manifest to extract the target resource version
version := fieldpath . APIVersion ( p . config . GetAPIVersion ( ) )
// The manager string needs to be converted to the internal manager
// key used inside structured-merge-diff apply logic
managerKey , err := buildManagerInfoForApply ( p . manager )
if err != nil {
return nil , fmt . Errorf ( "error building manager info: %w" , err )
}
// Finally invoke Apply to execute the same function used in k8s
// server-side applies
mergedLive , _ , err := updater . Apply ( tvLive , tvConfig , version , managed . Fields ( ) , managerKey , true )
if err != nil {
return nil , fmt . Errorf ( "error while running updater.Apply: %w" , err )
}
return mergedLive , err
}
func buildManagerInfoForApply ( manager string ) ( string , error ) {
managerInfo := metav1 . ManagedFieldsEntry {
Manager : manager ,
Operation : metav1 . ManagedFieldsOperationApply ,
}
return fieldmanager . BuildManagerIdentifier ( & managerInfo )
}
2022-09-16 14:22:00 +00:00
// normalizeTypedValue will prepare the given tv so it can be used in diffs by:
// - removing last-applied-configuration annotation
// - applying default values
func normalizeTypedValue ( tv * typed . TypedValue ) ( [ ] byte , error ) {
ru := tv . AsValue ( ) . Unstructured ( )
2025-02-06 23:20:29 +00:00
r , ok := ru . ( map [ string ] any )
2022-08-02 18:48:09 +00:00
if ! ok {
return nil , fmt . Errorf ( "error converting result typedValue: expected map got %T" , ru )
}
2022-09-16 14:22:00 +00:00
resultUn := & unstructured . Unstructured { Object : r }
unstructured . RemoveNestedField ( resultUn . Object , "metadata" , "annotations" , AnnotationLastAppliedConfig )
resultBytes , err := json . Marshal ( resultUn )
2022-08-02 18:48:09 +00:00
if err != nil {
return nil , fmt . Errorf ( "error while marshaling merged unstructured: %w" , err )
}
2022-09-16 14:22:00 +00:00
obj , err := scheme . Scheme . New ( resultUn . GroupVersionKind ( ) )
2022-08-02 18:48:09 +00:00
if err == nil {
2022-09-16 14:22:00 +00:00
err := json . Unmarshal ( resultBytes , & obj )
2022-08-02 18:48:09 +00:00
if err != nil {
return nil , fmt . Errorf ( "error unmarshaling merged bytes into object: %w" , err )
}
2022-09-16 14:22:00 +00:00
resultBytes , err = patchDefaultValues ( resultBytes , obj )
2022-08-02 18:48:09 +00:00
if err != nil {
return nil , fmt . Errorf ( "error applying defaults: %w" , err )
}
}
2022-09-16 14:22:00 +00:00
return resultBytes , nil
2022-08-02 18:48:09 +00:00
}
2022-08-03 14:57:58 +00:00
func buildDiffResult ( predictedBytes [ ] byte , liveBytes [ ] byte ) * DiffResult {
2022-08-02 18:48:09 +00:00
return & DiffResult {
2022-08-03 14:57:58 +00:00
Modified : string ( liveBytes ) != string ( predictedBytes ) ,
2022-08-02 18:48:09 +00:00
NormalizedLive : liveBytes ,
2022-08-03 14:57:58 +00:00
PredictedLive : predictedBytes ,
}
2022-08-02 18:48:09 +00:00
}
2020-05-15 20:01:24 +00:00
// TwoWayDiff performs a three-way diff and uses specified config as a recently applied config
func TwoWayDiff ( config , live * unstructured . Unstructured ) ( * DiffResult , error ) {
if live != nil && config != nil {
return ThreeWayDiff ( config , config . DeepCopy ( ) , live )
2022-08-02 18:48:09 +00:00
}
return handleResourceCreateOrDeleteDiff ( config , live )
}
// handleResourceCreateOrDeleteDiff will calculate the diff in case of resource creation or
// deletion. Expects that config or live is nil which means that the resource is being
// created or being deleted. Will return error if both are nil or if none are nil.
func handleResourceCreateOrDeleteDiff ( config , live * unstructured . Unstructured ) ( * DiffResult , error ) {
if live != nil && config != nil {
return nil , errors . New ( "unnexpected state: expected live or config to be null: not create or delete operation" )
}
if live != nil {
2020-05-15 20:01:24 +00:00
liveData , err := json . Marshal ( live )
if err != nil {
return nil , err
}
2020-06-08 03:22:37 +00:00
return & DiffResult { Modified : false , NormalizedLive : liveData , PredictedLive : [ ] byte ( "null" ) } , nil
2020-05-15 20:01:24 +00:00
} else if config != nil {
predictedLiveData , err := json . Marshal ( config . Object )
if err != nil {
return nil , err
}
2020-06-08 03:22:37 +00:00
return & DiffResult { Modified : true , NormalizedLive : [ ] byte ( "null" ) , PredictedLive : predictedLiveData } , nil
2020-05-15 20:01:24 +00:00
} else {
return nil , errors . New ( "both live and config are null objects" )
2019-11-12 17:51:26 +00:00
}
}
2020-10-14 18:34:19 +00:00
// generateSchemeDefaultPatch runs the scheme default functions on the given parameter, and
// return a patch representing the delta vs the origin parameter object.
func generateSchemeDefaultPatch ( kubeObj runtime . Object ) ( [ ] byte , error ) {
// 1) Call scheme defaulter functions on a clone of our k8s resource object
patched := kubeObj . DeepCopyObject ( )
2024-06-25 18:54:38 +00:00
gescheme . Scheme . Default ( patched )
2020-10-14 18:34:19 +00:00
// 2) Compare the original object (pre-defaulter funcs) with patched object (post-default funcs),
// and generate a patch that can be applied against the original
patch , success , err := CreateTwoWayMergePatch ( kubeObj , patched , kubeObj . DeepCopyObject ( ) )
// Ignore empty patch: this only means that kubescheme.Scheme.Default(...) made no changes.
if string ( patch ) == "{}" && err == nil {
success = true
}
if err != nil || ! success {
if err == nil && ! success {
err = errors . New ( "empty result" )
}
return nil , err
}
return patch , err
}
2020-07-09 20:44:23 +00:00
// applyPatch executes kubernetes server side patch:
// uses corresponding data structure, applies appropriate defaults and executes strategic merge patch
func applyPatch ( liveBytes [ ] byte , patchBytes [ ] byte , newVersionedObject func ( ) ( runtime . Object , error ) ) ( [ ] byte , [ ] byte , error ) {
2020-10-14 18:34:19 +00:00
// Construct an empty instance of the object we are applying a patch against
2020-07-09 20:44:23 +00:00
predictedLive , err := newVersionedObject ( )
if err != nil {
return nil , nil , err
}
2020-10-14 18:34:19 +00:00
// Apply the patchBytes patch against liveBytes, using predictedLive to indicate the k8s data type
2020-07-09 20:44:23 +00:00
predictedLiveBytes , err := strategicpatch . StrategicMergePatch ( liveBytes , patchBytes , predictedLive )
if err != nil {
return nil , nil , err
2020-06-30 21:33:07 +00:00
}
2020-10-14 18:34:19 +00:00
// Unmarshal predictedLiveBytes into predictedLive; note that this will discard JSON fields in predictedLiveBytes
// which are not in the predictedLive struct. predictedLive is thus "tainted" and we should not use it directly.
2020-07-13 17:21:25 +00:00
if err = json . Unmarshal ( predictedLiveBytes , & predictedLive ) ; err == nil {
2020-10-14 18:34:19 +00:00
// 1) Calls 'kubescheme.Scheme.Default(predictedLive)' and generates a patch containing the delta of that
// call, which can then be applied to predictedLiveBytes.
//
// Why do we do this? Since predictedLive is "tainted" (missing extra fields), we cannot use it to populate
// predictedLiveBytes, BUT we still need predictedLive itself in order to call the default scheme functions.
// So, we call the default scheme functions on the "tainted" struct, to generate a patch, and then
// apply that patch to the untainted JSON.
patch , err := generateSchemeDefaultPatch ( predictedLive )
if err != nil {
return nil , nil , err
}
// 2) Apply the default-funcs patch against the original "untainted" JSON
// This allows us to apply the scheme default values generated above, against JSON that does not fully conform
// to its k8s resource type (eg the JSON may contain those invalid fields that we do not wish to discard).
predictedLiveBytes , err = strategicpatch . StrategicMergePatch ( predictedLiveBytes , patch , predictedLive . DeepCopyObject ( ) )
if err != nil {
return nil , nil , err
}
2025-02-06 23:20:29 +00:00
// 3) Unmarshall into a map[string]any, then back into byte[], to ensure the fields
2020-10-14 18:34:19 +00:00
// are sorted in a consistent order (we do the same below, so that they can be
// lexicographically compared with one another)
2025-02-06 23:20:29 +00:00
var result map [ string ] any
2020-10-14 18:34:19 +00:00
err = json . Unmarshal ( [ ] byte ( predictedLiveBytes ) , & result )
if err != nil {
return nil , nil , err
}
predictedLiveBytes , err = json . Marshal ( result )
2020-07-13 17:21:25 +00:00
if err != nil {
return nil , nil , err
}
2020-06-30 21:33:07 +00:00
}
2020-07-09 20:44:23 +00:00
live , err := newVersionedObject ( )
if err != nil {
return nil , nil , err
}
2020-07-13 17:21:25 +00:00
2020-10-14 18:34:19 +00:00
// As above, unknown JSON fields in liveBytes will be discarded in the unmarshalling to 'live'.
// However, this is much less likely since liveBytes is coming from a live k8s instance which
// has already accepted those resources. Regardless, we still treat 'live' as tainted.
2020-07-13 17:21:25 +00:00
if err = json . Unmarshal ( liveBytes , live ) ; err == nil {
2020-10-14 18:34:19 +00:00
// As above, indirectly apply the schema defaults against liveBytes
patch , err := generateSchemeDefaultPatch ( live )
if err != nil {
return nil , nil , err
}
liveBytes , err = strategicpatch . StrategicMergePatch ( liveBytes , patch , live . DeepCopyObject ( ) )
2020-07-13 17:21:25 +00:00
if err != nil {
return nil , nil , err
}
2020-10-14 18:34:19 +00:00
// Ensure the fields are sorted in a consistent order (as above)
2025-02-06 23:20:29 +00:00
var result map [ string ] any
2020-10-14 18:34:19 +00:00
err = json . Unmarshal ( [ ] byte ( liveBytes ) , & result )
if err != nil {
return nil , nil , err
}
liveBytes , err = json . Marshal ( result )
if err != nil {
return nil , nil , err
}
2020-07-09 20:44:23 +00:00
}
2020-07-13 17:21:25 +00:00
2020-07-09 20:44:23 +00:00
return liveBytes , predictedLiveBytes , nil
2020-06-30 21:33:07 +00:00
}
2022-08-02 18:48:09 +00:00
// patchDefaultValues will calculate the default values patch based on the
// given obj. It will apply the patch using the given objBytes and return
// the new patched object.
func patchDefaultValues ( objBytes [ ] byte , obj runtime . Object ) ( [ ] byte , error ) {
// 1) Call 'kubescheme.Scheme.Default(obj)' to generate a patch containing
// the default values for the given scheme.
patch , err := generateSchemeDefaultPatch ( obj )
if err != nil {
return nil , fmt . Errorf ( "error generating patch for default values: %w" , err )
}
// 2) Apply the patch with default values in objBytes.
patchedBytes , err := strategicpatch . StrategicMergePatch ( objBytes , patch , obj )
if err != nil {
return nil , fmt . Errorf ( "error applying patch for default values: %w" , err )
}
2025-02-06 23:20:29 +00:00
// 3) Unmarshall into a map[string]any, then back into byte[], to
2022-08-29 12:50:54 +00:00
// ensure the fields are sorted in a consistent order (we do the same below,
// so that they can be lexicographically compared with one another).
2025-02-06 23:20:29 +00:00
var result map [ string ] any
2022-08-29 12:50:54 +00:00
err = json . Unmarshal ( [ ] byte ( patchedBytes ) , & result )
if err != nil {
return nil , fmt . Errorf ( "error unmarshaling patched bytes: %w" , err )
}
patchedBytes , err = json . Marshal ( result )
if err != nil {
return nil , fmt . Errorf ( "error marshaling patched bytes: %w" , err )
}
2022-08-02 18:48:09 +00:00
return patchedBytes , nil
}
2019-11-12 17:51:26 +00:00
// ThreeWayDiff performs a diff with the understanding of how to incorporate the
// last-applied-configuration annotation in the diff.
// Inputs are assumed to be stripped of type information
func ThreeWayDiff ( orig , config , live * unstructured . Unstructured ) ( * DiffResult , error ) {
orig = removeNamespaceAnnotation ( orig )
config = removeNamespaceAnnotation ( config )
2020-05-15 20:01:24 +00:00
2019-11-12 17:51:26 +00:00
// 1. calculate a 3-way merge patch
2020-07-09 20:44:23 +00:00
patchBytes , newVersionedObject , err := threeWayMergePatch ( orig , config , live )
2019-11-12 17:51:26 +00:00
if err != nil {
return nil , err
}
2020-05-15 20:01:24 +00:00
// 2. get expected live object by applying the patch against the live object
2019-11-12 17:51:26 +00:00
liveBytes , err := json . Marshal ( live )
if err != nil {
return nil , err
}
2020-05-15 20:01:24 +00:00
var predictedLiveBytes [ ] byte
2020-10-14 18:34:19 +00:00
// If orig/config/live represents a registered scheme...
2020-07-09 20:44:23 +00:00
if newVersionedObject != nil {
2020-10-14 18:34:19 +00:00
// Apply patch while applying scheme defaults
2020-07-09 20:44:23 +00:00
liveBytes , predictedLiveBytes , err = applyPatch ( liveBytes , patchBytes , newVersionedObject )
2020-05-15 20:01:24 +00:00
if err != nil {
return nil , err
}
} else {
2020-10-14 18:34:19 +00:00
// Otherwise, merge patch directly as JSON
2020-05-15 20:01:24 +00:00
predictedLiveBytes , err = jsonpatch . MergePatch ( liveBytes , patchBytes )
if err != nil {
return nil , err
}
2019-11-12 17:51:26 +00:00
}
2020-05-15 20:01:24 +00:00
2022-08-03 14:57:58 +00:00
return buildDiffResult ( predictedLiveBytes , liveBytes ) , nil
2019-11-12 17:51:26 +00:00
}
// removeNamespaceAnnotation remove the namespace and an empty annotation map from the metadata.
// The namespace field is present in live (namespaced) objects, but not necessarily present in
// config or last-applied. This results in a diff which we don't care about. We delete the two so
// that the diff is more relevant.
func removeNamespaceAnnotation ( orig * unstructured . Unstructured ) * unstructured . Unstructured {
orig = orig . DeepCopy ( )
if metadataIf , ok := orig . Object [ "metadata" ] ; ok {
2025-02-06 23:20:29 +00:00
metadata := metadataIf . ( map [ string ] any )
2019-11-12 17:51:26 +00:00
delete ( metadata , "namespace" )
if annotationsIf , ok := metadata [ "annotations" ] ; ok {
shouldDelete := false
if annotationsIf == nil {
shouldDelete = true
} else {
2025-02-06 23:20:29 +00:00
annotation , ok := annotationsIf . ( map [ string ] any )
2021-12-22 19:16:05 +00:00
if ok && len ( annotation ) == 0 {
2019-11-12 17:51:26 +00:00
shouldDelete = true
}
}
if shouldDelete {
delete ( metadata , "annotations" )
}
}
}
return orig
}
2020-08-24 23:26:41 +00:00
// StatefulSet requires special handling since it embeds PersistentVolumeClaim resource.
// K8S API server applies additional default field which we cannot reproduce on client side.
// So workaround is to remove all "defaulted" fields from 'volumeClaimTemplates' of live resource.
func statefulSetWorkaround ( orig , live * unstructured . Unstructured ) * unstructured . Unstructured {
origTemplate , ok , err := unstructured . NestedSlice ( orig . Object , "spec" , "volumeClaimTemplates" )
if ! ok || err != nil {
return live
}
liveTemplate , ok , err := unstructured . NestedSlice ( live . Object , "spec" , "volumeClaimTemplates" )
if ! ok || err != nil {
return live
}
live = live . DeepCopy ( )
_ = unstructured . SetNestedField ( live . Object , jsonutil . RemoveListFields ( origTemplate , liveTemplate ) , "spec" , "volumeClaimTemplates" )
return live
}
2020-07-09 20:44:23 +00:00
func threeWayMergePatch ( orig , config , live * unstructured . Unstructured ) ( [ ] byte , func ( ) ( runtime . Object , error ) , error ) {
2019-11-12 17:51:26 +00:00
origBytes , err := json . Marshal ( orig . Object )
if err != nil {
2020-05-15 20:01:24 +00:00
return nil , nil , err
2019-11-12 17:51:26 +00:00
}
configBytes , err := json . Marshal ( config . Object )
if err != nil {
2020-05-15 20:01:24 +00:00
return nil , nil , err
2019-11-12 17:51:26 +00:00
}
2020-06-30 21:33:07 +00:00
2020-05-15 20:01:24 +00:00
if versionedObject , err := scheme . Scheme . New ( orig . GroupVersionKind ( ) ) ; err == nil {
2020-08-24 23:26:41 +00:00
gk := orig . GroupVersionKind ( ) . GroupKind ( )
if ( gk . Group == "apps" || gk . Group == "extensions" ) && gk . Kind == "StatefulSet" {
live = statefulSetWorkaround ( orig , live )
}
2020-06-30 21:33:07 +00:00
liveBytes , err := json . Marshal ( live . Object )
if err != nil {
return nil , nil , err
}
2020-05-15 20:01:24 +00:00
lookupPatchMeta , err := strategicpatch . NewPatchMetaFromStruct ( versionedObject )
if err != nil {
return nil , nil , err
}
patch , err := strategicpatch . CreateThreeWayMergePatch ( origBytes , configBytes , liveBytes , lookupPatchMeta , true )
if err != nil {
return nil , nil , err
}
2020-07-09 20:44:23 +00:00
newVersionedObject := func ( ) ( runtime . Object , error ) {
return scheme . Scheme . New ( orig . GroupVersionKind ( ) )
}
return patch , newVersionedObject , nil
2020-05-15 20:01:24 +00:00
} else {
2020-06-30 21:33:07 +00:00
// Remove defaulted fields from the live object.
// This subtracts any extra fields in the live object which are not present in last-applied-configuration.
live = & unstructured . Unstructured { Object : jsonutil . RemoveMapFields ( orig . Object , live . Object ) }
liveBytes , err := json . Marshal ( live . Object )
if err != nil {
return nil , nil , err
}
2020-05-15 20:01:24 +00:00
patch , err := jsonmergepatch . CreateThreeWayJSONMergePatch ( origBytes , configBytes , liveBytes )
if err != nil {
return nil , nil , err
}
return patch , nil , nil
2019-11-12 17:51:26 +00:00
}
}
2020-10-27 00:14:56 +00:00
func GetLastAppliedConfigAnnotation ( live * unstructured . Unstructured ) ( * unstructured . Unstructured , error ) {
2019-11-12 17:51:26 +00:00
if live == nil {
2020-10-27 00:14:56 +00:00
return nil , nil
2019-11-12 17:51:26 +00:00
}
2022-04-12 17:35:40 +00:00
annotations := live . GetAnnotations ( )
lastAppliedStr , ok := annotations [ corev1 . LastAppliedConfigAnnotation ]
2019-11-12 17:51:26 +00:00
if ! ok {
2020-10-27 00:14:56 +00:00
return nil , nil
2019-11-12 17:51:26 +00:00
}
var obj unstructured . Unstructured
err := json . Unmarshal ( [ ] byte ( lastAppliedStr ) , & obj )
if err != nil {
2020-10-27 00:14:56 +00:00
return nil , fmt . Errorf ( "failed to unmarshal %s in %s: %v" , corev1 . LastAppliedConfigAnnotation , live . GetName ( ) , err )
2019-11-12 17:51:26 +00:00
}
2020-10-27 00:14:56 +00:00
return & obj , nil
2019-11-12 17:51:26 +00:00
}
// DiffArray performs a diff on a list of unstructured objects. Objects are expected to match
// environments
2020-10-27 00:14:56 +00:00
func DiffArray ( configArray , liveArray [ ] * unstructured . Unstructured , opts ... Option ) ( * DiffResultList , error ) {
2019-11-12 17:51:26 +00:00
numItems := len ( configArray )
if len ( liveArray ) != numItems {
2020-10-27 00:14:56 +00:00
return nil , errors . New ( "left and right arrays have mismatched lengths" )
2019-11-12 17:51:26 +00:00
}
diffResultList := DiffResultList {
Diffs : make ( [ ] DiffResult , numItems ) ,
}
for i := 0 ; i < numItems ; i ++ {
config := configArray [ i ]
live := liveArray [ i ]
2020-10-27 00:14:56 +00:00
diffRes , err := Diff ( config , live , opts ... )
2020-05-15 20:01:24 +00:00
if err != nil {
return nil , err
}
2019-11-12 17:51:26 +00:00
diffResultList . Diffs [ i ] = * diffRes
if diffRes . Modified {
diffResultList . Modified = true
}
}
return & diffResultList , nil
}
2020-10-27 00:14:56 +00:00
func Normalize ( un * unstructured . Unstructured , opts ... Option ) {
2019-11-12 17:51:26 +00:00
if un == nil {
return
}
2020-10-27 00:14:56 +00:00
o := applyOptions ( opts )
2019-11-12 17:51:26 +00:00
2020-08-26 06:29:57 +00:00
// creationTimestamp is sometimes set to null in the config when exported (e.g. SealedSecrets)
// Removing the field allows a cleaner diff.
unstructured . RemoveNestedField ( un . Object , "metadata" , "creationTimestamp" )
2019-11-12 17:51:26 +00:00
gvk := un . GroupVersionKind ( )
if gvk . Group == "" && gvk . Kind == "Secret" {
2020-10-27 00:14:56 +00:00
NormalizeSecret ( un , opts ... )
2019-11-12 17:51:26 +00:00
} else if gvk . Group == "rbac.authorization.k8s.io" && ( gvk . Kind == "ClusterRole" || gvk . Kind == "Role" ) {
2020-10-27 00:14:56 +00:00
normalizeRole ( un , o )
2020-10-13 23:53:40 +00:00
} else if gvk . Group == "" && gvk . Kind == "Endpoints" {
2020-10-27 00:14:56 +00:00
normalizeEndpoint ( un , o )
2019-11-12 17:51:26 +00:00
}
2020-10-27 00:14:56 +00:00
err := o . normalizer . Normalize ( un )
if err != nil {
o . log . Error ( err , fmt . Sprintf ( "Failed to normalize %s/%s/%s" , un . GroupVersionKind ( ) , un . GetNamespace ( ) , un . GetName ( ) ) )
2019-11-12 17:51:26 +00:00
}
}
// NormalizeSecret mutates the supplied object and encodes stringData to data, and converts nils to
// empty strings. If the object is not a secret, or is an invalid secret, then returns the same object.
2020-10-27 00:14:56 +00:00
func NormalizeSecret ( un * unstructured . Unstructured , opts ... Option ) {
2019-11-12 17:51:26 +00:00
if un == nil {
return
}
gvk := un . GroupVersionKind ( )
if gvk . Group != "" || gvk . Kind != "Secret" {
return
}
2025-01-29 15:51:13 +00:00
// move stringData to data section
if stringData , found , err := unstructured . NestedMap ( un . Object , "stringData" ) ; found && err == nil {
2025-02-06 23:20:29 +00:00
var data map [ string ] any
2025-01-29 15:51:13 +00:00
data , found , _ = unstructured . NestedMap ( un . Object , "data" )
if ! found {
2025-02-06 23:20:29 +00:00
data = make ( map [ string ] any )
2025-01-29 15:51:13 +00:00
}
// base64 encode string values and add non-string values as is.
// This ensures that the apply fails if the secret is invalid.
for k , v := range stringData {
strVal , ok := v . ( string )
if ok {
data [ k ] = base64 . StdEncoding . EncodeToString ( [ ] byte ( strVal ) )
} else {
data [ k ] = v
}
}
err := unstructured . SetNestedField ( un . Object , data , "data" )
if err == nil {
delete ( un . Object , "stringData" )
}
}
2020-10-27 00:14:56 +00:00
o := applyOptions ( opts )
2019-11-12 17:51:26 +00:00
var secret corev1 . Secret
err := runtime . DefaultUnstructuredConverter . FromUnstructured ( un . Object , & secret )
if err != nil {
2020-10-27 00:14:56 +00:00
o . log . Error ( err , "Failed to convert from unstructured into Secret" )
2019-11-12 17:51:26 +00:00
return
}
// We normalize nils to empty string to handle: https://github.com/argoproj/argo-cd/issues/943
for k , v := range secret . Data {
if len ( v ) == 0 {
secret . Data [ k ] = [ ] byte ( "" )
}
}
newObj , err := runtime . DefaultUnstructuredConverter . ToUnstructured ( & secret )
if err != nil {
2020-10-27 00:14:56 +00:00
o . log . Error ( err , "object unable to convert from secret" )
2019-11-12 17:51:26 +00:00
return
}
if secret . Data != nil {
err = unstructured . SetNestedField ( un . Object , newObj [ "data" ] , "data" )
if err != nil {
2020-10-27 00:14:56 +00:00
o . log . Error ( err , "failed to set secret.data" )
2019-11-12 17:51:26 +00:00
return
}
}
}
2020-10-13 23:53:40 +00:00
// normalizeEndpoint normalizes endpoint meaning that EndpointSubsets are sorted lexicographically
2020-10-27 00:14:56 +00:00
func normalizeEndpoint ( un * unstructured . Unstructured , o options ) {
2020-10-13 23:53:40 +00:00
if un == nil {
return
}
gvk := un . GroupVersionKind ( )
if gvk . Group != "" || gvk . Kind != "Endpoints" {
return
}
var ep corev1 . Endpoints
err := runtime . DefaultUnstructuredConverter . FromUnstructured ( un . Object , & ep )
if err != nil {
2020-10-27 00:14:56 +00:00
o . log . Error ( err , "Failed to convert from unstructured into Endpoints" )
2020-10-13 23:53:40 +00:00
return
}
2022-01-26 18:45:17 +00:00
// add default protocol to subsets ports if it is empty
for s := range ep . Subsets {
subset := & ep . Subsets [ s ]
for p := range subset . Ports {
port := & subset . Ports [ p ]
if port . Protocol == "" {
port . Protocol = corev1 . ProtocolTCP
}
}
}
2021-01-06 03:35:02 +00:00
endpoints . SortSubsets ( ep . Subsets )
2020-10-13 23:53:40 +00:00
2020-10-27 00:14:56 +00:00
newObj , err := runtime . DefaultUnstructuredConverter . ToUnstructured ( & ep )
2020-10-13 23:53:40 +00:00
if err != nil {
2020-10-27 00:14:56 +00:00
o . log . Info ( fmt . Sprintf ( couldNotMarshalErrMsg , gvk , err ) )
2020-10-13 23:53:40 +00:00
return
}
2020-10-27 00:14:56 +00:00
un . Object = newObj
2020-10-13 23:53:40 +00:00
}
2020-05-15 20:01:24 +00:00
// normalizeRole mutates the supplied Role/ClusterRole and sets rules to null if it is an empty list or an aggregated role
2020-10-27 00:14:56 +00:00
func normalizeRole ( un * unstructured . Unstructured , o options ) {
2019-11-12 17:51:26 +00:00
if un == nil {
return
}
gvk := un . GroupVersionKind ( )
if gvk . Group != "rbac.authorization.k8s.io" || ( gvk . Kind != "Role" && gvk . Kind != "ClusterRole" ) {
return
}
2020-05-15 20:01:24 +00:00
// Check whether the role we're checking is an aggregation role. If it is, we ignore any differences in rules.
2020-10-27 00:14:56 +00:00
if o . ignoreAggregatedRoles {
2020-05-15 20:01:24 +00:00
aggrIf , ok := un . Object [ "aggregationRule" ]
if ok {
2025-02-06 23:20:29 +00:00
_ , ok = aggrIf . ( map [ string ] any )
2020-05-15 20:01:24 +00:00
if ! ok {
2022-04-12 17:35:40 +00:00
o . log . Info ( fmt . Sprintf ( "Malformed aggregationRule in resource '%s', won't modify." , un . GetName ( ) ) )
2020-05-15 20:01:24 +00:00
} else {
un . Object [ "rules" ] = nil
}
}
}
2019-11-12 17:51:26 +00:00
rulesIf , ok := un . Object [ "rules" ]
if ! ok {
return
}
2025-02-06 23:20:29 +00:00
rules , ok := rulesIf . ( [ ] any )
2019-11-12 17:51:26 +00:00
if ! ok {
return
}
if rules != nil && len ( rules ) == 0 {
un . Object [ "rules" ] = nil
}
}
// CreateTwoWayMergePatch is a helper to construct a two-way merge patch from objects (instead of bytes)
2025-02-06 23:20:29 +00:00
func CreateTwoWayMergePatch ( orig , new , dataStruct any ) ( [ ] byte , bool , error ) {
2019-11-12 17:51:26 +00:00
origBytes , err := json . Marshal ( orig )
if err != nil {
return nil , false , err
}
newBytes , err := json . Marshal ( new )
if err != nil {
return nil , false , err
}
patch , err := strategicpatch . CreateTwoWayMergePatch ( origBytes , newBytes , dataStruct )
if err != nil {
return nil , false , err
}
return patch , string ( patch ) != "{}" , nil
}
2024-10-29 10:29:52 +00:00
// HideSecretData replaces secret data & optional annotations values in specified target, live secrets and in last applied configuration of live secret with plus(+). Also preserves differences between
// target, live and last applied config values. E.g. if all three are equal the values would be replaced with same number of plus(+). If all are different then number of plus(+)
2019-11-12 17:51:26 +00:00
// in replacement should be different.
2024-10-29 10:29:52 +00:00
func HideSecretData ( target * unstructured . Unstructured , live * unstructured . Unstructured , hideAnnotations map [ string ] bool ) ( * unstructured . Unstructured , * unstructured . Unstructured , error ) {
var liveLastAppliedAnnotation * unstructured . Unstructured
2019-11-12 17:51:26 +00:00
if live != nil {
2024-10-29 10:29:52 +00:00
liveLastAppliedAnnotation , _ = GetLastAppliedConfigAnnotation ( live )
2019-11-12 17:51:26 +00:00
live = live . DeepCopy ( )
}
if target != nil {
target = target . DeepCopy ( )
}
keys := map [ string ] bool { }
2024-10-29 10:29:52 +00:00
for _ , obj := range [ ] * unstructured . Unstructured { target , live , liveLastAppliedAnnotation } {
2019-11-12 17:51:26 +00:00
if obj == nil {
continue
}
NormalizeSecret ( obj )
if data , found , err := unstructured . NestedMap ( obj . Object , "data" ) ; found && err == nil {
for k := range data {
keys [ k ] = true
}
}
}
2024-10-29 10:29:52 +00:00
var err error
target , live , liveLastAppliedAnnotation , err = hide ( target , live , liveLastAppliedAnnotation , keys , "data" )
if err != nil {
return nil , nil , err
}
target , live , liveLastAppliedAnnotation , err = hide ( target , live , liveLastAppliedAnnotation , hideAnnotations , "metadata" , "annotations" )
if err != nil {
return nil , nil , err
}
if live != nil && liveLastAppliedAnnotation != nil {
annotations := live . GetAnnotations ( )
if annotations == nil {
annotations = make ( map [ string ] string )
}
// special case: hide "kubectl.kubernetes.io/last-applied-configuration" annotation
if _ , ok := hideAnnotations [ corev1 . LastAppliedConfigAnnotation ] ; ok {
annotations [ corev1 . LastAppliedConfigAnnotation ] = replacement
} else {
lastAppliedData , err := json . Marshal ( liveLastAppliedAnnotation )
if err != nil {
return nil , nil , fmt . Errorf ( "error marshaling json: %s" , err )
}
annotations [ corev1 . LastAppliedConfigAnnotation ] = string ( lastAppliedData )
}
live . SetAnnotations ( annotations )
}
return target , live , nil
}
func hide ( target , live , liveLastAppliedAnnotation * unstructured . Unstructured , keys map [ string ] bool , fields ... string ) ( * unstructured . Unstructured , * unstructured . Unstructured , * unstructured . Unstructured , error ) {
2019-11-12 17:51:26 +00:00
for k := range keys {
// we use "+" rather than the more common "*"
2024-10-29 10:29:52 +00:00
nextReplacement := replacement
2019-11-12 17:51:26 +00:00
valToReplacement := make ( map [ string ] string )
2024-10-29 10:29:52 +00:00
for _ , obj := range [ ] * unstructured . Unstructured { target , live , liveLastAppliedAnnotation } {
2025-02-06 23:20:29 +00:00
var data map [ string ] any
2019-11-12 17:51:26 +00:00
if obj != nil {
2021-11-02 19:00:06 +00:00
// handles an edge case when secret data has nil value
// https://github.com/argoproj/argo-cd/issues/5584
2024-10-29 10:29:52 +00:00
dataValue , ok , _ := unstructured . NestedFieldCopy ( obj . Object , fields ... )
2021-11-02 19:00:06 +00:00
if ok {
if dataValue == nil {
continue
}
}
2019-11-12 17:51:26 +00:00
var err error
2024-10-29 10:29:52 +00:00
data , _ , err = unstructured . NestedMap ( obj . Object , fields ... )
2019-11-12 17:51:26 +00:00
if err != nil {
2024-10-29 10:29:52 +00:00
return nil , nil , nil , fmt . Errorf ( "unstructured.NestedMap error: %s" , err )
2019-11-12 17:51:26 +00:00
}
}
if data == nil {
2025-02-06 23:20:29 +00:00
data = make ( map [ string ] any )
2019-11-12 17:51:26 +00:00
}
valData , ok := data [ k ]
if ! ok {
continue
}
val := toString ( valData )
replacement , ok := valToReplacement [ val ]
if ! ok {
replacement = nextReplacement
nextReplacement = nextReplacement + "++++"
valToReplacement [ val ] = replacement
}
data [ k ] = replacement
2024-10-29 10:29:52 +00:00
err := unstructured . SetNestedField ( obj . Object , data , fields ... )
2019-11-12 17:51:26 +00:00
if err != nil {
2024-10-29 10:29:52 +00:00
return nil , nil , nil , fmt . Errorf ( "unstructured.SetNestedField error: %s" , err )
2019-11-12 17:51:26 +00:00
}
}
}
2024-10-29 10:29:52 +00:00
return target , live , liveLastAppliedAnnotation , nil
2019-11-12 17:51:26 +00:00
}
2025-02-06 23:20:29 +00:00
func toString ( val any ) string {
2019-11-12 17:51:26 +00:00
if val == nil {
return ""
}
return fmt . Sprintf ( "%s" , val )
}
// remarshal checks resource kind and version and re-marshal using corresponding struct custom marshaller.
// This ensures that expected resource state is formatter same as actual resource state in kubernetes
// and allows to find differences between actual and target states more accurately.
// Remarshalling also strips any type information (e.g. float64 vs. int) from the unstructured
// object. This is important for diffing since it will cause godiff to report a false difference.
2020-10-27 00:14:56 +00:00
func remarshal ( obj * unstructured . Unstructured , o options ) * unstructured . Unstructured {
2019-11-12 17:51:26 +00:00
data , err := json . Marshal ( obj )
if err != nil {
panic ( err )
}
2024-09-17 17:19:20 +00:00
// Unmarshal again to strip type information (e.g. float64 vs. int) from the unstructured
// object. This is important for diffing since it will cause godiff to report a false difference.
var newUn unstructured . Unstructured
err = json . Unmarshal ( data , & newUn )
if err != nil {
panic ( err )
}
obj = & newUn
2019-11-12 17:51:26 +00:00
gvk := obj . GroupVersionKind ( )
item , err := scheme . Scheme . New ( obj . GroupVersionKind ( ) )
if err != nil {
2020-10-14 18:34:19 +00:00
// This is common. the scheme is not registered
2020-10-27 00:14:56 +00:00
o . log . V ( 1 ) . Info ( fmt . Sprintf ( "Could not create new object of type %s: %v" , gvk , err ) )
2019-11-12 17:51:26 +00:00
return obj
}
// This will drop any omitempty fields, perform resource conversion etc...
unmarshalledObj := reflect . New ( reflect . TypeOf ( item ) . Elem ( ) ) . Interface ( )
2020-10-14 18:34:19 +00:00
// Unmarshal data into unmarshalledObj, but detect if there are any unknown fields that are not
// found in the target GVK object.
2020-10-27 00:14:56 +00:00
decoder := json . NewDecoder ( bytes . NewReader ( data ) )
2020-10-14 18:34:19 +00:00
decoder . DisallowUnknownFields ( )
if err := decoder . Decode ( & unmarshalledObj ) ; err != nil {
// Likely a field present in obj that is not present in the GVK type, or user
// may have specified an invalid spec in git, so return original object
2020-10-27 00:14:56 +00:00
o . log . V ( 1 ) . Info ( fmt . Sprintf ( couldNotMarshalErrMsg , gvk , err ) )
2019-11-12 17:51:26 +00:00
return obj
}
unstrBody , err := runtime . DefaultUnstructuredConverter . ToUnstructured ( unmarshalledObj )
if err != nil {
2020-10-27 00:14:56 +00:00
o . log . V ( 1 ) . Info ( fmt . Sprintf ( couldNotMarshalErrMsg , gvk , err ) )
2019-11-12 17:51:26 +00:00
return obj
}
2020-10-14 18:34:19 +00:00
// Remove all default values specified by custom formatter (e.g. creationTimestamp)
2019-11-12 17:51:26 +00:00
unstrBody = jsonutil . RemoveMapFields ( obj . Object , unstrBody )
return & unstructured . Unstructured { Object : unstrBody }
}