Skip to content

Commit

Permalink
Merge pull request #16937 from justinsb/sqs_policies_normalize
Browse files Browse the repository at this point in the history
Fix: Normalize the SQS policies before comparing them
  • Loading branch information
k8s-ci-robot authored Nov 9, 2024
2 parents 5cc8ce0 + dcd4529 commit 6bb024d
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 0 deletions.
165 changes: 165 additions & 0 deletions pkg/jsonutils/transform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package jsonutils

import (
"encoding/json"
"fmt"
"sort"
)

// Transformer is used to transform JSON values
type Transformer struct {
stringTransforms []func(path string, value string) (string, error)
objectTransforms []func(path string, value map[string]any) error
sliceTransforms []func(path string, value []any) ([]any, error)
}

// NewTransformer is the constructor for a Transformer
func NewTransformer() *Transformer {
return &Transformer{}
}

// AddStringTransform adds a function that will be called for each string value in the JSON tree
func (t *Transformer) AddStringTransform(fn func(path string, value string) (string, error)) {
t.stringTransforms = append(t.stringTransforms, fn)
}

// AddObjectTransform adds a function that will be called for each object in the JSON tree
func (t *Transformer) AddObjectTransform(fn func(path string, value map[string]any) error) {
t.objectTransforms = append(t.objectTransforms, fn)
}

// AddSliceTransform adds a function that will be called for each slice in the JSON tree
func (t *Transformer) AddSliceTransform(fn func(path string, value []any) ([]any, error)) {
t.sliceTransforms = append(t.sliceTransforms, fn)
}

// Transform applies the transformations to the JSON tree
func (o *Transformer) Transform(v map[string]any) error {
_, err := o.visitAny(v, "")
return err
}

// visitAny is a helper function that visits any value in the JSON tree
func (o *Transformer) visitAny(v any, path string) (any, error) {
if v == nil {
return v, nil
}
switch v := v.(type) {
case map[string]any:
if err := o.visitMap(v, path); err != nil {
return nil, err
}
return v, nil
case []any:
return o.visitSlice(v, path)
case int64, float64, bool:
return o.visitPrimitive(v, path)
case string:
return o.visitString(v, path)
default:
return nil, fmt.Errorf("unhandled type at path %q: %T", path, v)
}
}

func (o *Transformer) visitMap(m map[string]any, path string) error {
for _, fn := range o.objectTransforms {
if err := fn(path, m); err != nil {
return err
}
}

for k, v := range m {
childPath := path + "." + k

v2, err := o.visitAny(v, childPath)
if err != nil {
return err
}
m[k] = v2
}

return nil
}

// visitSlice is a helper function that visits a slice in the JSON tree
func (o *Transformer) visitSlice(s []any, path string) (any, error) {
for _, fn := range o.sliceTransforms {
var err error
s, err = fn(path+"[]", s)
if err != nil {
return nil, err
}
}

for i, v := range s {
v2, err := o.visitAny(v, path+"[]")
if err != nil {
return nil, err
}
s[i] = v2
}

return s, nil
}

// SortSlice sorts a slice of any values, ordered by their JSON representations.
// This is not very efficient, but is convenient for small slice where we don't know their types.
func SortSlice(s []any) ([]any, error) {
type entry struct {
o any
sortKey string
}

var entries []entry
for i := range s {
j, err := json.Marshal(s[i])
if err != nil {
return nil, fmt.Errorf("error converting to json: %w", err)
}
entries = append(entries, entry{o: s[i], sortKey: string(j)})
}

sort.Slice(entries, func(i, j int) bool {
return entries[i].sortKey < entries[j].sortKey
})

out := make([]any, 0, len(s))
for i := range s {
out = append(out, entries[i].o)
}

return out, nil
}

// visitPrimitive is a helper function that visits a primitive value in the JSON tree
func (o *Transformer) visitPrimitive(v any, _ string) (any, error) {
return v, nil
}

// visitString is a helper function that visits a string value in the JSON tree
func (o *Transformer) visitString(v string, path string) (string, error) {
for _, fn := range o.stringTransforms {
var err error
v, err = fn(path, v)
if err != nil {
return "", err
}
}
return v, nil
}
49 changes: 49 additions & 0 deletions upup/pkg/fi/cloudup/awstasks/sqs.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog/v2"
"k8s.io/kops/pkg/jsonutils"
"k8s.io/kops/upup/pkg/fi/cloudup/terraformWriter"

"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -115,6 +116,13 @@ func (q *SQS) Find(c *fi.CloudupContext) (*SQS, error) {
return nil, fmt.Errorf("error parsing actual Policy for SQS %q: %v", aws.ToString(q.Name), err)
}

if err := normalizePolicy(expectedJson); err != nil {
return nil, err
}
if err := normalizePolicy(actualJson); err != nil {
return nil, err
}

if reflect.DeepEqual(actualJson, expectedJson) {
klog.V(2).Infof("actual Policy was json-equal to expected; returning expected value")
actualPolicy = expectedPolicy
Expand All @@ -139,6 +147,47 @@ func (q *SQS) Find(c *fi.CloudupContext) (*SQS, error) {
return actual, nil
}

type JSONObject map[string]any

func (j *JSONObject) Slice(key string) ([]any, bool, error) {
v, found := (*j)[key]
if !found {
return nil, false, nil
}
s, ok := v.([]any)
if !ok {
return nil, false, fmt.Errorf("expected slice at %q, got %T", key, v)
}
return s, true, nil
}

func (j *JSONObject) Object(key string) (JSONObject, bool, error) {
v, found := (*j)[key]
if !found {
return nil, false, nil
}
m, ok := v.(JSONObject)
if !ok {
return nil, false, fmt.Errorf("expected object at %q, got %T", key, v)
}
return m, true, nil
}

// normalizePolicy sorts the Service principals in the policy, so that we can compare policies more easily.
func normalizePolicy(policy map[string]interface{}) error {
xform := jsonutils.NewTransformer()
xform.AddSliceTransform(func(path string, value []any) ([]any, error) {
if path != ".Statement[].Principal.Service[]" {
return value, nil
}
return jsonutils.SortSlice(value)
})
if err := xform.Transform(policy); err != nil {
return err
}
return nil
}

func (q *SQS) Run(c *fi.CloudupContext) error {
return fi.CloudupDefaultDeltaRunMethod(q, c)
}
Expand Down

0 comments on commit 6bb024d

Please sign in to comment.