@@ -1166,6 +1166,126 @@ func TestServerSideDiff(t *testing.T) {
11661166 assert .Empty (t , predictedDeploy .Annotations [AnnotationLastAppliedConfig ])
11671167 assert .Empty (t , liveDeploy .Annotations [AnnotationLastAppliedConfig ])
11681168 })
1169+
1170+ t .Run ("will mask Secret data symmetrically so identical values do not produce a spurious diff" , func (t * testing.T ) {
1171+ t .Parallel ()
1172+
1173+ desired := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "vault:secret/foo" }, nil )
1174+ live := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "injected-by-webhook" }, nil )
1175+ predictedLiveJSON := mustMarshalJSON (t , buildSecret ("test-secret" , "default" , map [string ]string {"password" : "injected-by-webhook" }, nil ))
1176+
1177+ opts := append (buildOpts (predictedLiveJSON ), WithIgnoreMutationWebhook (false ))
1178+ result , err := serverSideDiff (desired , live , opts ... )
1179+ require .NoError (t , err )
1180+ require .NotNil (t , result )
1181+
1182+ assert .False (t , result .Modified , "identical secret values on both sides must not be flagged as modified after masking" )
1183+
1184+ predictedData := mustGetSecretData (t , result .PredictedLive )
1185+ liveData := mustGetSecretData (t , result .NormalizedLive )
1186+ assert .Equal (t , "++++++++" , predictedData ["password" ], "predicted data must be masked, not raw" )
1187+ assert .Equal (t , "++++++++" , liveData ["password" ], "live data must be masked, not raw" )
1188+ })
1189+
1190+ t .Run ("will keep Secret data masked but still detect genuine value differences" , func (t * testing.T ) {
1191+ t .Parallel ()
1192+
1193+ desired := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "vault:secret/foo" }, nil )
1194+ live := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "old-value" }, nil )
1195+ predictedLiveJSON := mustMarshalJSON (t , buildSecret ("test-secret" , "default" , map [string ]string {"password" : "new-value" }, nil ))
1196+
1197+ opts := append (buildOpts (predictedLiveJSON ), WithIgnoreMutationWebhook (false ))
1198+ result , err := serverSideDiff (desired , live , opts ... )
1199+ require .NoError (t , err )
1200+ require .NotNil (t , result )
1201+
1202+ assert .True (t , result .Modified , "different secret values must still be flagged as modified" )
1203+
1204+ predictedData := mustGetSecretData (t , result .PredictedLive )
1205+ liveData := mustGetSecretData (t , result .NormalizedLive )
1206+ // HideSecretData yields different placeholder lengths for different values, so the
1207+ // data field is masked on both sides and the two placeholders differ.
1208+ assert .NotEqual (t , "new-value" , predictedData ["password" ], "raw new value must not leak into PredictedLive" )
1209+ assert .NotEqual (t , "old-value" , liveData ["password" ], "raw old value must not leak into NormalizedLive" )
1210+ assert .NotEqual (t , predictedData ["password" ], liveData ["password" ], "differing values must yield differing placeholders" )
1211+ })
1212+
1213+ t .Run ("will detect Secret key additions and removals" , func (t * testing.T ) {
1214+ t .Parallel ()
1215+
1216+ desired := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "x" , "token" : "y" }, nil )
1217+ live := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "x" }, nil )
1218+ predictedLiveJSON := mustMarshalJSON (t , buildSecret ("test-secret" , "default" , map [string ]string {"password" : "x" , "token" : "y" }, nil ))
1219+
1220+ opts := append (buildOpts (predictedLiveJSON ), WithIgnoreMutationWebhook (false ))
1221+ result , err := serverSideDiff (desired , live , opts ... )
1222+ require .NoError (t , err )
1223+ require .NotNil (t , result )
1224+
1225+ assert .True (t , result .Modified , "added Secret keys must still be flagged as modified after masking" )
1226+ })
1227+
1228+ t .Run ("will not mask non-core Secret resources" , func (t * testing.T ) {
1229+ // Resources whose Kind is "Secret" but whose Group is non-empty (e.g. CRDs)
1230+ // must not be touched by the core/v1 Secret masking path.
1231+ t .Parallel ()
1232+
1233+ desired := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "raw-value" }, nil )
1234+ desired .SetAPIVersion ("custom.io/v1" )
1235+ live := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "raw-value" }, nil )
1236+ live .SetAPIVersion ("custom.io/v1" )
1237+ predictedLiveJSON := mustMarshalJSON (t , desired )
1238+
1239+ opts := append (buildOpts (predictedLiveJSON ), WithIgnoreMutationWebhook (false ))
1240+ result , err := serverSideDiff (desired , live , opts ... )
1241+ require .NoError (t , err )
1242+ require .NotNil (t , result )
1243+
1244+ predictedData := mustGetSecretData (t , result .PredictedLive )
1245+ assert .Equal (t , "raw-value" , predictedData ["password" ], "non-core Secret data must be left untouched" )
1246+ })
1247+ }
1248+
1249+ // buildSecret returns a core/v1 Secret as an *unstructured.Unstructured.
1250+ func buildSecret (name , namespace string , data map [string ]string , annotations map [string ]string ) * unstructured.Unstructured {
1251+ dataField := make (map [string ]any , len (data ))
1252+ for k , v := range data {
1253+ dataField [k ] = v
1254+ }
1255+ metadata := map [string ]any {
1256+ "name" : name ,
1257+ "namespace" : namespace ,
1258+ }
1259+ if len (annotations ) > 0 {
1260+ annField := make (map [string ]any , len (annotations ))
1261+ for k , v := range annotations {
1262+ annField [k ] = v
1263+ }
1264+ metadata ["annotations" ] = annField
1265+ }
1266+ return & unstructured.Unstructured {Object : map [string ]any {
1267+ "apiVersion" : "v1" ,
1268+ "kind" : "Secret" ,
1269+ "metadata" : metadata ,
1270+ "type" : "Opaque" ,
1271+ "data" : dataField ,
1272+ }}
1273+ }
1274+
1275+ func mustMarshalJSON (t * testing.T , obj * unstructured.Unstructured ) string {
1276+ t .Helper ()
1277+ bytes , err := json .Marshal (obj )
1278+ require .NoError (t , err )
1279+ return string (bytes )
1280+ }
1281+
1282+ func mustGetSecretData (t * testing.T , secretBytes []byte ) map [string ]any {
1283+ t .Helper ()
1284+ var obj map [string ]any
1285+ require .NoError (t , json .Unmarshal (secretBytes , & obj ))
1286+ data , ok := obj ["data" ].(map [string ]any )
1287+ require .True (t , ok , "expected data field to be a map" )
1288+ return data
11691289}
11701290
11711291// testIgnoreDifferencesNormalizer implements a simple normalizer that removes specified fields
0 commit comments