@@ -1254,6 +1254,125 @@ func TestServerSideDiff(t *testing.T) {
12541254 assert .Contains (t , liveData , "key3" , "key3 should still be in live state" )
12551255 })
12561256
1257+ t .Run ("will mask Secret data symmetrically so identical values do not produce a spurious diff" , func (t * testing.T ) {
1258+ t .Parallel ()
1259+
1260+ desired := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "vault:secret/foo" }, nil )
1261+ live := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "injected-by-webhook" }, nil )
1262+ predictedLiveJSON := mustMarshalJSON (t , buildSecret ("test-secret" , "default" , map [string ]string {"password" : "injected-by-webhook" }, nil ))
1263+
1264+ opts := append (buildOpts (predictedLiveJSON ), WithIgnoreMutationWebhook (false ))
1265+ result , err := serverSideDiff (desired , live , opts ... )
1266+ require .NoError (t , err )
1267+ require .NotNil (t , result )
1268+
1269+ assert .False (t , result .Modified , "identical secret values on both sides must not be flagged as modified after masking" )
1270+
1271+ predictedData := mustGetSecretData (t , result .PredictedLive )
1272+ liveData := mustGetSecretData (t , result .NormalizedLive )
1273+ assert .Equal (t , "++++++++" , predictedData ["password" ], "predicted data must be masked, not raw" )
1274+ assert .Equal (t , "++++++++" , liveData ["password" ], "live data must be masked, not raw" )
1275+ })
1276+
1277+ t .Run ("will keep Secret data masked but still detect genuine value differences" , func (t * testing.T ) {
1278+ t .Parallel ()
1279+
1280+ desired := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "vault:secret/foo" }, nil )
1281+ live := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "old-value" }, nil )
1282+ predictedLiveJSON := mustMarshalJSON (t , buildSecret ("test-secret" , "default" , map [string ]string {"password" : "new-value" }, nil ))
1283+
1284+ opts := append (buildOpts (predictedLiveJSON ), WithIgnoreMutationWebhook (false ))
1285+ result , err := serverSideDiff (desired , live , opts ... )
1286+ require .NoError (t , err )
1287+ require .NotNil (t , result )
1288+
1289+ assert .True (t , result .Modified , "different secret values must still be flagged as modified" )
1290+
1291+ predictedData := mustGetSecretData (t , result .PredictedLive )
1292+ liveData := mustGetSecretData (t , result .NormalizedLive )
1293+ // HideSecretData yields different placeholder lengths for different values, so the
1294+ // data field is masked on both sides and the two placeholders differ.
1295+ assert .NotEqual (t , "new-value" , predictedData ["password" ], "raw new value must not leak into PredictedLive" )
1296+ assert .NotEqual (t , "old-value" , liveData ["password" ], "raw old value must not leak into NormalizedLive" )
1297+ assert .NotEqual (t , predictedData ["password" ], liveData ["password" ], "differing values must yield differing placeholders" )
1298+ })
1299+
1300+ t .Run ("will detect Secret key additions and removals" , func (t * testing.T ) {
1301+ t .Parallel ()
1302+
1303+ desired := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "x" , "token" : "y" }, nil )
1304+ live := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "x" }, nil )
1305+ predictedLiveJSON := mustMarshalJSON (t , buildSecret ("test-secret" , "default" , map [string ]string {"password" : "x" , "token" : "y" }, nil ))
1306+
1307+ opts := append (buildOpts (predictedLiveJSON ), WithIgnoreMutationWebhook (false ))
1308+ result , err := serverSideDiff (desired , live , opts ... )
1309+ require .NoError (t , err )
1310+ require .NotNil (t , result )
1311+
1312+ assert .True (t , result .Modified , "added Secret keys must still be flagged as modified after masking" )
1313+ })
1314+
1315+ t .Run ("will not mask non-core Secret resources" , func (t * testing.T ) {
1316+ // Resources whose Kind is "Secret" but whose Group is non-empty (e.g. CRDs)
1317+ // must not be touched by the core/v1 Secret masking path.
1318+ t .Parallel ()
1319+
1320+ desired := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "raw-value" }, nil )
1321+ desired .SetAPIVersion ("custom.io/v1" )
1322+ live := buildSecret ("test-secret" , "default" , map [string ]string {"password" : "raw-value" }, nil )
1323+ live .SetAPIVersion ("custom.io/v1" )
1324+ predictedLiveJSON := mustMarshalJSON (t , desired )
1325+
1326+ opts := append (buildOpts (predictedLiveJSON ), WithIgnoreMutationWebhook (false ))
1327+ result , err := serverSideDiff (desired , live , opts ... )
1328+ require .NoError (t , err )
1329+ require .NotNil (t , result )
1330+
1331+ predictedData := mustGetSecretData (t , result .PredictedLive )
1332+ assert .Equal (t , "raw-value" , predictedData ["password" ], "non-core Secret data must be left untouched" )
1333+ })
1334+ }
1335+
1336+ // buildSecret returns a core/v1 Secret as an *unstructured.Unstructured.
1337+ func buildSecret (name , namespace string , data map [string ]string , annotations map [string ]string ) * unstructured.Unstructured {
1338+ dataField := make (map [string ]any , len (data ))
1339+ for k , v := range data {
1340+ dataField [k ] = v
1341+ }
1342+ metadata := map [string ]any {
1343+ "name" : name ,
1344+ "namespace" : namespace ,
1345+ }
1346+ if len (annotations ) > 0 {
1347+ annField := make (map [string ]any , len (annotations ))
1348+ for k , v := range annotations {
1349+ annField [k ] = v
1350+ }
1351+ metadata ["annotations" ] = annField
1352+ }
1353+ return & unstructured.Unstructured {Object : map [string ]any {
1354+ "apiVersion" : "v1" ,
1355+ "kind" : "Secret" ,
1356+ "metadata" : metadata ,
1357+ "type" : "Opaque" ,
1358+ "data" : dataField ,
1359+ }}
1360+ }
1361+
1362+ func mustMarshalJSON (t * testing.T , obj * unstructured.Unstructured ) string {
1363+ t .Helper ()
1364+ bytes , err := json .Marshal (obj )
1365+ require .NoError (t , err )
1366+ return string (bytes )
1367+ }
1368+
1369+ func mustGetSecretData (t * testing.T , secretBytes []byte ) map [string ]any {
1370+ t .Helper ()
1371+ var obj map [string ]any
1372+ require .NoError (t , json .Unmarshal (secretBytes , & obj ))
1373+ data , ok := obj ["data" ].(map [string ]any )
1374+ require .True (t , ok , "expected data field to be a map" )
1375+ return data
12571376}
12581377
12591378// testIgnoreDifferencesNormalizer implements a simple normalizer that removes specified fields
0 commit comments