Skip to content

Commit fa865d3

Browse files
authored
fix: controller would drop fields when updating DestinationRules (argoproj#1253)
Signed-off-by: Hui Kang <[email protected]>
1 parent 02e10d8 commit fa865d3

File tree

4 files changed

+185
-5
lines changed

4 files changed

+185
-5
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/hashicorp/golang-lru v0.5.4 // indirect
1919
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a
2020
github.com/lunixbochs/vtclean v1.0.0 // indirect
21+
github.com/mitchellh/mapstructure v1.3.3
2122
github.com/newrelic/newrelic-client-go v0.49.0
2223
github.com/pkg/errors v0.9.1
2324
github.com/prometheus/client_golang v1.10.0

rollout/trafficrouting/istio/istio.go

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
jsonpatch "github.com/evanphx/json-patch/v5"
10+
"github.com/mitchellh/mapstructure"
1011
log "github.com/sirupsen/logrus"
1112
k8serrors "k8s.io/apimachinery/pkg/api/errors"
1213
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -224,15 +225,49 @@ func (r *Reconciler) UpdateHash(canaryHash, stableHash string) error {
224225
return nil
225226
}
226227

228+
// destinationRuleReplaceExtraMarshal relace the key of "Extra" with the actual content
229+
// e.g., "trafficpolicy" and return the bytes of the new object
230+
func destinationRuleReplaceExtraMarshal(dRule *DestinationRule) []byte {
231+
dRuleNew := map[string]interface{}{}
232+
dRuleNew["metadata"] = dRule.ObjectMeta.DeepCopy()
233+
234+
subsets := []map[string]interface{}{}
235+
for _, subset := range dRule.Spec.Subsets {
236+
newsubset := map[string]interface{}{}
237+
newsubset["name"] = subset.Name
238+
newsubset["labels"] = subset.Labels
239+
240+
if subset.Extra == nil {
241+
subsets = append(subsets, newsubset)
242+
continue
243+
}
244+
245+
extra := map[string]interface{}{}
246+
inputbyte, _ := json.Marshal(subset.Extra)
247+
json.Unmarshal(inputbyte, &extra)
248+
249+
subset.Extra = nil
250+
for k, v := range extra {
251+
newsubset[k] = v
252+
}
253+
subsets = append(subsets, newsubset)
254+
}
255+
dRuleNew["spec"] = map[string]interface{}{
256+
"subsets": subsets,
257+
}
258+
259+
dRuleNewBytes, _ := json.Marshal(dRuleNew)
260+
return dRuleNewBytes
261+
}
262+
227263
func updateDestinationRule(ctx context.Context, client dynamic.ResourceInterface, orig []byte, dRule, dRuleNew *DestinationRule) (bool, error) {
228264
dRuleBytes, err := json.Marshal(dRule)
229265
if err != nil {
230266
return false, err
231267
}
232-
dRuleNewBytes, err := json.Marshal(dRuleNew)
233-
if err != nil {
234-
return false, err
235-
}
268+
dRuleNewBytes := destinationRuleReplaceExtraMarshal(dRuleNew)
269+
log.Debugf("dRuleNewBytes: %s", string(dRuleNewBytes))
270+
236271
patch, err := jsonpatch.CreateMergePatch(dRuleBytes, dRuleNewBytes)
237272
if err != nil {
238273
return false, err
@@ -253,7 +288,7 @@ func updateDestinationRule(ctx context.Context, client dynamic.ResourceInterface
253288
if err != nil {
254289
return false, err
255290
}
256-
log.Infof("updated destinationrule: %s", string(patch))
291+
log.Infof("updating destinationrule: %s", string(patch))
257292
return true, nil
258293
}
259294

@@ -275,12 +310,97 @@ func unstructuredToDestinationRules(un *unstructured.Unstructured) ([]byte, *Des
275310
return dRuleBytes, dRule1, dRule2, nil
276311
}
277312

313+
func unMarshalSubsets(dRule *DestinationRule, dRuleBytes []byte) error {
314+
var err error
315+
316+
unstructured := map[string]interface{}{}
317+
var extractFieldBytes func([]byte, string) ([]byte, error)
318+
extractFieldBytes = func(input []byte, name string) ([]byte, error) {
319+
err = json.Unmarshal(input, &unstructured)
320+
if err != nil {
321+
return nil, err
322+
}
323+
fieldBytes, err := json.Marshal(unstructured[name])
324+
if err != nil {
325+
return nil, err
326+
}
327+
return fieldBytes, nil
328+
}
329+
330+
specBytes, err := extractFieldBytes(dRuleBytes, "spec")
331+
if err != nil {
332+
return err
333+
}
334+
335+
subsetsBytes, err := extractFieldBytes(specBytes, "subsets")
336+
if err != nil {
337+
return err
338+
}
339+
340+
subsetsMap := []map[string]interface{}{}
341+
err = json.Unmarshal(subsetsBytes, &subsetsMap)
342+
if err != nil {
343+
return err
344+
}
345+
346+
dRule.Spec.Subsets = []Subset{}
347+
for _, si := range subsetsMap {
348+
var subset Subset
349+
350+
jsonInput, _ := json.Marshal(si)
351+
extra, err := UnmarshalJson(jsonInput, &subset)
352+
if err != nil {
353+
return err
354+
}
355+
356+
subset.Extra = extra
357+
if len(subset.Extra) == 0 {
358+
subset.Extra = nil
359+
}
360+
dRule.Spec.Subsets = append(dRule.Spec.Subsets, subset)
361+
}
362+
return nil
363+
}
364+
365+
func UnmarshalJson(input []byte, result interface{}) (map[string]interface{}, error) {
366+
// unmarshal json to a map
367+
foomap := make(map[string]interface{})
368+
json.Unmarshal(input, &foomap)
369+
370+
// create a mapstructure decoder
371+
var md mapstructure.Metadata
372+
decoder, err := mapstructure.NewDecoder(
373+
&mapstructure.DecoderConfig{
374+
Metadata: &md,
375+
Result: result,
376+
})
377+
if err != nil {
378+
return nil, err
379+
}
380+
381+
// decode the unmarshalled map into the given struct
382+
if err := decoder.Decode(foomap); err != nil {
383+
return nil, err
384+
}
385+
386+
// copy and return unused fields
387+
unused := map[string]interface{}{}
388+
for _, k := range md.Unused {
389+
unused[k] = foomap[k]
390+
}
391+
return unused, nil
392+
}
393+
278394
func jsonBytesToDestinationRule(dRuleBytes []byte) (*DestinationRule, error) {
279395
var dRule DestinationRule
280396
err := json.Unmarshal(dRuleBytes, &dRule)
281397
if err != nil {
282398
return nil, err
283399
}
400+
err = unMarshalSubsets(&dRule, dRuleBytes)
401+
if err != nil {
402+
return nil, err
403+
}
284404
return &dRule, nil
285405
}
286406

rollout/trafficrouting/istio/istio_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package istio
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"testing"
78

@@ -391,6 +392,62 @@ func rolloutWithDestinationRule() *v1alpha1.Rollout {
391392
}
392393
}
393394

395+
// TestUpdateHashWithListers verifies behavior of UpdateHash when using informers/listers
396+
func TestUpdateHashAdditionalFieldsWithListers(t *testing.T) {
397+
ro := rolloutWithDestinationRule()
398+
obj := unstructuredutil.StrToUnstructuredUnsafe(`
399+
apiVersion: networking.istio.io/v1alpha3
400+
kind: DestinationRule
401+
metadata:
402+
name: istio-destrule
403+
namespace: default
404+
spec:
405+
host: ratings.prod.svc.cluster.local
406+
trafficPolicy:
407+
loadBalancer:
408+
simple: LEAST_CONN
409+
subsets:
410+
- name: stable
411+
labels:
412+
version: v3
413+
- name: canary
414+
trafficPolicy:
415+
loadBalancer:
416+
simple: ROUND_ROBIN
417+
`)
418+
client := testutil.NewFakeDynamicClient(obj)
419+
vsvcLister, druleLister := getIstioListers(client)
420+
r := NewReconciler(ro, client, record.NewFakeEventRecorder(), vsvcLister, druleLister)
421+
client.ClearActions()
422+
423+
err := r.UpdateHash("abc123", "def456")
424+
assert.NoError(t, err)
425+
actions := client.Actions()
426+
assert.Len(t, actions, 1)
427+
assert.Equal(t, "update", actions[0].GetVerb())
428+
429+
dRuleUn, err := client.Resource(istioutil.GetIstioDestinationRuleGVR()).Namespace(r.rollout.Namespace).Get(context.TODO(), "istio-destrule", metav1.GetOptions{})
430+
assert.NoError(t, err)
431+
432+
dRuleUnBytes, err := json.Marshal(dRuleUn)
433+
assert.NoError(t, err)
434+
assert.Equal(t, `{"apiVersion":"networking.istio.io/v1alpha3","kind":"DestinationRule","metadata":{"annotations":{"argo-rollouts.argoproj.io/managed-by-rollouts":"rollout"},"name":"istio-destrule","namespace":"default"},"spec":{"host":"ratings.prod.svc.cluster.local","subsets":[{"labels":{"rollouts-pod-template-hash":"def456","version":"v3"},"name":"stable"},{"labels":{"rollouts-pod-template-hash":"abc123"},"name":"canary","trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}],"trafficPolicy":{"loadBalancer":{"simple":"LEAST_CONN"}}}}`,
435+
string(dRuleUnBytes))
436+
437+
_, dRule, _, err := unstructuredToDestinationRules(dRuleUn)
438+
assert.NoError(t, err)
439+
assert.Equal(t, dRule.Annotations[v1alpha1.ManagedByRolloutsKey], "rollout")
440+
assert.Equal(t, dRule.Spec.Subsets[0].Labels[v1alpha1.DefaultRolloutUniqueLabelKey], "def456")
441+
assert.Equal(t, dRule.Spec.Subsets[1].Labels[v1alpha1.DefaultRolloutUniqueLabelKey], "abc123")
442+
assert.Nil(t, dRule.Spec.Subsets[0].Extra)
443+
assert.NotNil(t, dRule.Spec.Subsets[1].Extra)
444+
445+
jsonBytes, err := json.Marshal(dRule)
446+
assert.NoError(t, err)
447+
assert.Equal(t, `{"metadata":{"name":"istio-destrule","namespace":"default","creationTimestamp":null,"annotations":{"argo-rollouts.argoproj.io/managed-by-rollouts":"rollout"}},"spec":{"subsets":[{"name":"stable","labels":{"rollouts-pod-template-hash":"def456","version":"v3"}},{"name":"canary","labels":{"rollouts-pod-template-hash":"abc123"},"Extra":{"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}}]}}`,
448+
string(jsonBytes))
449+
}
450+
394451
// TestUpdateHashWithListers verifies behavior of UpdateHash when using informers/listers
395452
func TestUpdateHashWithListers(t *testing.T) {
396453
ro := rolloutWithDestinationRule()

rollout/trafficrouting/istio/istio_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,6 @@ type DestinationRuleSpec struct {
4747
type Subset struct {
4848
Name string `json:"name,omitempty"`
4949
Labels map[string]string `json:"labels,omitempty"`
50+
// TrafficPolicy *json.RawMessage `json:"trafficPolicy,omitempty"`
51+
Extra map[string]interface{} `json:",omitempty"`
5052
}

0 commit comments

Comments
 (0)