Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit 9ab0b2e

Browse files
authored
feat: Add ability to hide certain annotations on secret resources (#577)
* Add option to hide annotations on secrets Signed-off-by: Siddhesh Ghadi <[email protected]> * Handle err Signed-off-by: Siddhesh Ghadi <[email protected]> * Move hide logic to a generic func Signed-off-by: Siddhesh Ghadi <[email protected]> * Remove test code Signed-off-by: Siddhesh Ghadi <[email protected]> * Address review comments Signed-off-by: Siddhesh Ghadi <[email protected]> * Handle lastAppliedConfig special case Signed-off-by: Siddhesh Ghadi <[email protected]> * Fix if logic and remove comments Signed-off-by: Siddhesh Ghadi <[email protected]> --------- Signed-off-by: Siddhesh Ghadi <[email protected]>
1 parent 09e5225 commit 9ab0b2e

File tree

3 files changed

+199
-32
lines changed

3 files changed

+199
-32
lines changed

pkg/diff/diff.go

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
const (
3636
couldNotMarshalErrMsg = "Could not unmarshal to object of type %s: %v"
3737
AnnotationLastAppliedConfig = "kubectl.kubernetes.io/last-applied-configuration"
38+
replacement = "++++++++"
3839
)
3940

4041
// Holds diffing result of two resources
@@ -969,21 +970,21 @@ func CreateTwoWayMergePatch(orig, new, dataStruct interface{}) ([]byte, bool, er
969970
return patch, string(patch) != "{}", nil
970971
}
971972

972-
// HideSecretData replaces secret data values in specified target, live secrets and in last applied configuration of live secret with stars. Also preserves differences between
973-
// target, live and last applied config values. E.g. if all three are equal the values would be replaced with same number of stars. If all the are different then number of stars
973+
// 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
974+
// 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(+)
974975
// in replacement should be different.
975-
func HideSecretData(target *unstructured.Unstructured, live *unstructured.Unstructured) (*unstructured.Unstructured, *unstructured.Unstructured, error) {
976-
var orig *unstructured.Unstructured
976+
func HideSecretData(target *unstructured.Unstructured, live *unstructured.Unstructured, hideAnnotations map[string]bool) (*unstructured.Unstructured, *unstructured.Unstructured, error) {
977+
var liveLastAppliedAnnotation *unstructured.Unstructured
977978
if live != nil {
978-
orig, _ = GetLastAppliedConfigAnnotation(live)
979+
liveLastAppliedAnnotation, _ = GetLastAppliedConfigAnnotation(live)
979980
live = live.DeepCopy()
980981
}
981982
if target != nil {
982983
target = target.DeepCopy()
983984
}
984985

985986
keys := map[string]bool{}
986-
for _, obj := range []*unstructured.Unstructured{target, live, orig} {
987+
for _, obj := range []*unstructured.Unstructured{target, live, liveLastAppliedAnnotation} {
987988
if obj == nil {
988989
continue
989990
}
@@ -995,25 +996,57 @@ func HideSecretData(target *unstructured.Unstructured, live *unstructured.Unstru
995996
}
996997
}
997998

999+
var err error
1000+
target, live, liveLastAppliedAnnotation, err = hide(target, live, liveLastAppliedAnnotation, keys, "data")
1001+
if err != nil {
1002+
return nil, nil, err
1003+
}
1004+
1005+
target, live, liveLastAppliedAnnotation, err = hide(target, live, liveLastAppliedAnnotation, hideAnnotations, "metadata", "annotations")
1006+
if err != nil {
1007+
return nil, nil, err
1008+
}
1009+
1010+
if live != nil && liveLastAppliedAnnotation != nil {
1011+
annotations := live.GetAnnotations()
1012+
if annotations == nil {
1013+
annotations = make(map[string]string)
1014+
}
1015+
// special case: hide "kubectl.kubernetes.io/last-applied-configuration" annotation
1016+
if _, ok := hideAnnotations[corev1.LastAppliedConfigAnnotation]; ok {
1017+
annotations[corev1.LastAppliedConfigAnnotation] = replacement
1018+
} else {
1019+
lastAppliedData, err := json.Marshal(liveLastAppliedAnnotation)
1020+
if err != nil {
1021+
return nil, nil, fmt.Errorf("error marshaling json: %s", err)
1022+
}
1023+
annotations[corev1.LastAppliedConfigAnnotation] = string(lastAppliedData)
1024+
}
1025+
live.SetAnnotations(annotations)
1026+
}
1027+
return target, live, nil
1028+
}
1029+
1030+
func hide(target, live, liveLastAppliedAnnotation *unstructured.Unstructured, keys map[string]bool, fields ...string) (*unstructured.Unstructured, *unstructured.Unstructured, *unstructured.Unstructured, error) {
9981031
for k := range keys {
9991032
// we use "+" rather than the more common "*"
1000-
nextReplacement := "++++++++"
1033+
nextReplacement := replacement
10011034
valToReplacement := make(map[string]string)
1002-
for _, obj := range []*unstructured.Unstructured{target, live, orig} {
1035+
for _, obj := range []*unstructured.Unstructured{target, live, liveLastAppliedAnnotation} {
10031036
var data map[string]interface{}
10041037
if obj != nil {
10051038
// handles an edge case when secret data has nil value
10061039
// https://github.com/argoproj/argo-cd/issues/5584
1007-
dataValue, ok := obj.Object["data"]
1040+
dataValue, ok, _ := unstructured.NestedFieldCopy(obj.Object, fields...)
10081041
if ok {
10091042
if dataValue == nil {
10101043
continue
10111044
}
10121045
}
10131046
var err error
1014-
data, _, err = unstructured.NestedMap(obj.Object, "data")
1047+
data, _, err = unstructured.NestedMap(obj.Object, fields...)
10151048
if err != nil {
1016-
return nil, nil, fmt.Errorf("unstructured.NestedMap error: %s", err)
1049+
return nil, nil, nil, fmt.Errorf("unstructured.NestedMap error: %s", err)
10171050
}
10181051
}
10191052
if data == nil {
@@ -1031,25 +1064,13 @@ func HideSecretData(target *unstructured.Unstructured, live *unstructured.Unstru
10311064
valToReplacement[val] = replacement
10321065
}
10331066
data[k] = replacement
1034-
err := unstructured.SetNestedField(obj.Object, data, "data")
1067+
err := unstructured.SetNestedField(obj.Object, data, fields...)
10351068
if err != nil {
1036-
return nil, nil, fmt.Errorf("unstructured.SetNestedField error: %s", err)
1069+
return nil, nil, nil, fmt.Errorf("unstructured.SetNestedField error: %s", err)
10371070
}
10381071
}
10391072
}
1040-
if live != nil && orig != nil {
1041-
annotations := live.GetAnnotations()
1042-
if annotations == nil {
1043-
annotations = make(map[string]string)
1044-
}
1045-
lastAppliedData, err := json.Marshal(orig)
1046-
if err != nil {
1047-
return nil, nil, fmt.Errorf("error marshaling json: %s", err)
1048-
}
1049-
annotations[corev1.LastAppliedConfigAnnotation] = string(lastAppliedData)
1050-
live.SetAnnotations(annotations)
1051-
}
1052-
return target, live, nil
1073+
return target, live, liveLastAppliedAnnotation, nil
10531074
}
10541075

10551076
func toString(val interface{}) string {

pkg/diff/diff_test.go

Lines changed: 151 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,9 @@ var (
986986
func TestHideSecretDataSameKeysDifferentValues(t *testing.T) {
987987
target, live, err := HideSecretData(
988988
createSecret(map[string]string{"key1": "test", "key2": "test"}),
989-
createSecret(map[string]string{"key1": "test-1", "key2": "test-1"}))
989+
createSecret(map[string]string{"key1": "test-1", "key2": "test-1"}),
990+
nil,
991+
)
990992
require.NoError(t, err)
991993

992994
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
@@ -996,7 +998,9 @@ func TestHideSecretDataSameKeysDifferentValues(t *testing.T) {
996998
func TestHideSecretDataSameKeysSameValues(t *testing.T) {
997999
target, live, err := HideSecretData(
9981000
createSecret(map[string]string{"key1": "test", "key2": "test"}),
999-
createSecret(map[string]string{"key1": "test", "key2": "test"}))
1001+
createSecret(map[string]string{"key1": "test", "key2": "test"}),
1002+
nil,
1003+
)
10001004
require.NoError(t, err)
10011005

10021006
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
@@ -1006,13 +1010,155 @@ func TestHideSecretDataSameKeysSameValues(t *testing.T) {
10061010
func TestHideSecretDataDifferentKeysDifferentValues(t *testing.T) {
10071011
target, live, err := HideSecretData(
10081012
createSecret(map[string]string{"key1": "test", "key2": "test"}),
1009-
createSecret(map[string]string{"key2": "test-1", "key3": "test-1"}))
1013+
createSecret(map[string]string{"key2": "test-1", "key3": "test-1"}),
1014+
nil,
1015+
)
10101016
require.NoError(t, err)
10111017

10121018
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
10131019
assert.Equal(t, map[string]interface{}{"key2": replacement2, "key3": replacement1}, secretData(live))
10141020
}
10151021

1022+
func TestHideSecretAnnotations(t *testing.T) {
1023+
tests := []struct {
1024+
name string
1025+
hideAnnots map[string]bool
1026+
annots map[string]interface{}
1027+
expectedAnnots map[string]interface{}
1028+
targetNil bool
1029+
}{
1030+
{
1031+
name: "no hidden annotations",
1032+
hideAnnots: nil,
1033+
annots: map[string]interface{}{"token/value": "secret", "key": "secret-key", "app": "test"},
1034+
expectedAnnots: map[string]interface{}{"token/value": "secret", "key": "secret-key", "app": "test"},
1035+
},
1036+
{
1037+
name: "hide annotations",
1038+
hideAnnots: map[string]bool{"token/value": true, "key": true},
1039+
annots: map[string]interface{}{"token/value": "secret", "key": "secret-key", "app": "test"},
1040+
expectedAnnots: map[string]interface{}{"token/value": replacement1, "key": replacement1, "app": "test"},
1041+
},
1042+
{
1043+
name: "hide annotations in last-applied-config",
1044+
hideAnnots: map[string]bool{"token/value": true, "key": true},
1045+
annots: map[string]interface{}{
1046+
"token/value": "secret",
1047+
"app": "test",
1048+
"kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{"app":"test","token/value":"secret","key":"secret-key"},"labels":{"app.kubernetes.io/instance":"test"},"name":"my-secret","namespace":"default"},"type":"Opaque"}`,
1049+
},
1050+
expectedAnnots: map[string]interface{}{
1051+
"token/value": replacement1,
1052+
"app": "test",
1053+
"kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{"app":"test","key":"++++++++","token/value":"++++++++"},"labels":{"app.kubernetes.io/instance":"test"},"name":"my-secret","namespace":"default"},"type":"Opaque"}`,
1054+
},
1055+
targetNil: true,
1056+
},
1057+
{
1058+
name: "special case: hide last-applied-config annotation",
1059+
hideAnnots: map[string]bool{"kubectl.kubernetes.io/last-applied-configuration": true},
1060+
annots: map[string]interface{}{
1061+
"token/value": replacement1,
1062+
"app": "test",
1063+
"kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{"app":"test","token/value":"secret","key":"secret-key"},"labels":{"app.kubernetes.io/instance":"test"},"name":"my-secret","namespace":"default"},"type":"Opaque"}`,
1064+
},
1065+
expectedAnnots: map[string]interface{}{
1066+
"app": "test",
1067+
"kubectl.kubernetes.io/last-applied-configuration": replacement1,
1068+
},
1069+
targetNil: true,
1070+
},
1071+
{
1072+
name: "hide annotations for malformed annotations",
1073+
hideAnnots: map[string]bool{"token/value": true, "key": true},
1074+
annots: map[string]interface{}{"token/value": 0, "key": "secret", "app": true},
1075+
expectedAnnots: map[string]interface{}{"token/value": replacement1, "key": replacement1, "app": true},
1076+
},
1077+
}
1078+
1079+
for _, tt := range tests {
1080+
t.Run(tt.name, func(t *testing.T) {
1081+
1082+
unSecret := &unstructured.Unstructured{
1083+
Object: map[string]interface{}{
1084+
"apiVersion": "v1",
1085+
"kind": "Secret",
1086+
"metadata": map[string]interface{}{
1087+
"name": "test-secret",
1088+
"annotations": tt.annots,
1089+
},
1090+
"type": "Opaque",
1091+
},
1092+
}
1093+
1094+
liveUn := remarshal(unSecret, applyOptions(diffOptionsForTest()))
1095+
targetUn := remarshal(unSecret, applyOptions(diffOptionsForTest()))
1096+
1097+
if tt.targetNil {
1098+
targetUn = nil
1099+
}
1100+
1101+
target, live, err := HideSecretData(targetUn, liveUn, tt.hideAnnots)
1102+
require.NoError(t, err)
1103+
1104+
// verify configured annotations are hidden
1105+
for _, obj := range []*unstructured.Unstructured{target, live} {
1106+
if obj != nil {
1107+
annots, _, _ := unstructured.NestedMap(obj.Object, "metadata", "annotations")
1108+
for ek, ev := range tt.expectedAnnots {
1109+
v, found := annots[ek]
1110+
assert.True(t, found)
1111+
assert.Equal(t, ev, v)
1112+
}
1113+
}
1114+
}
1115+
})
1116+
}
1117+
}
1118+
1119+
func TestHideSecretAnnotationsPreserveDifference(t *testing.T) {
1120+
hideAnnots := map[string]bool{"token/value": true}
1121+
1122+
liveUn := &unstructured.Unstructured{
1123+
Object: map[string]interface{}{
1124+
"apiVersion": "v1",
1125+
"kind": "Secret",
1126+
"metadata": map[string]interface{}{
1127+
"name": "test-secret",
1128+
"annotations": map[string]interface{}{"token/value": "secret", "app": "test"},
1129+
},
1130+
"type": "Opaque",
1131+
},
1132+
}
1133+
targetUn := &unstructured.Unstructured{
1134+
Object: map[string]interface{}{
1135+
"apiVersion": "v1",
1136+
"kind": "Secret",
1137+
"metadata": map[string]interface{}{
1138+
"name": "test-secret",
1139+
"annotations": map[string]interface{}{"token/value": "new-secret", "app": "test"},
1140+
},
1141+
"type": "Opaque",
1142+
},
1143+
}
1144+
1145+
liveUn = remarshal(liveUn, applyOptions(diffOptionsForTest()))
1146+
targetUn = remarshal(targetUn, applyOptions(diffOptionsForTest()))
1147+
1148+
target, live, err := HideSecretData(targetUn, liveUn, hideAnnots)
1149+
require.NoError(t, err)
1150+
1151+
liveAnnots := live.GetAnnotations()
1152+
v, found := liveAnnots["token/value"]
1153+
assert.True(t, found)
1154+
assert.Equal(t, replacement2, v)
1155+
1156+
targetAnnots := target.GetAnnotations()
1157+
v, found = targetAnnots["token/value"]
1158+
assert.True(t, found)
1159+
assert.Equal(t, replacement1, v)
1160+
}
1161+
10161162
func getTargetSecretJsonBytes() []byte {
10171163
return []byte(`
10181164
{
@@ -1078,7 +1224,7 @@ func TestHideSecretDataHandleEmptySecret(t *testing.T) {
10781224
liveSecret := bytesToUnstructured(t, getLiveSecretJsonBytes())
10791225

10801226
// when
1081-
target, live, err := HideSecretData(targetSecret, liveSecret)
1227+
target, live, err := HideSecretData(targetSecret, liveSecret, nil)
10821228

10831229
// then
10841230
assert.NoError(t, err)
@@ -1096,7 +1242,7 @@ func TestHideSecretDataLastAppliedConfig(t *testing.T) {
10961242
require.NoError(t, err)
10971243
liveSecret.SetAnnotations(map[string]string{corev1.LastAppliedConfigAnnotation: string(lastAppliedStr)})
10981244

1099-
target, live, err := HideSecretData(targetSecret, liveSecret)
1245+
target, live, err := HideSecretData(targetSecret, liveSecret, nil)
11001246
require.NoError(t, err)
11011247
err = json.Unmarshal([]byte(live.GetAnnotations()[corev1.LastAppliedConfigAnnotation]), &lastAppliedSecret)
11021248
require.NoError(t, err)

pkg/utils/kube/resource_ops.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (k *kubectlResourceOperations) runResourceCommand(ctx context.Context, obj
8080
if err != nil {
8181
return "", err
8282
}
83-
redacted, _, err := diff.HideSecretData(&obj, nil)
83+
redacted, _, err := diff.HideSecretData(&obj, nil, nil)
8484
if err != nil {
8585
return "", err
8686
}

0 commit comments

Comments
 (0)