Skip to content

Commit

Permalink
fix bootstrap merge (envoyproxy#2801)
Browse files Browse the repository at this point in the history
* fix bootstrap merge

Signed-off-by: zirain <[email protected]>

* refactor validateBootstrap

Signed-off-by: zirain <[email protected]>

* lint

Signed-off-by: zirain <[email protected]>

* update test

Signed-off-by: zirain <[email protected]>

---------

Signed-off-by: zirain <[email protected]>
  • Loading branch information
zirain authored and Xunzhuo committed Mar 13, 2024
1 parent e16e1da commit 96c0cda
Show file tree
Hide file tree
Showing 17 changed files with 627 additions and 157 deletions.
38 changes: 13 additions & 25 deletions api/v1alpha1/validation/envoyproxy_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ import (
"errors"
"fmt"
"net/netip"
"reflect"

bootstrapv3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/testing/protocmp"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"sigs.k8s.io/yaml"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/utils/proto"
"github.com/envoyproxy/gateway/internal/xds/bootstrap"
_ "github.com/envoyproxy/gateway/internal/xds/extensions" // register the generated types to support protojson unmarshalling
)
Expand Down Expand Up @@ -140,42 +138,33 @@ func validateService(spec *egv1a1.EnvoyProxySpec) []error {
}

func validateBootstrap(boostrapConfig *egv1a1.ProxyBootstrap) error {
// Validate user bootstrap config
defaultBootstrap := &bootstrapv3.Bootstrap{}
// TODO: need validate when enable prometheus?
defaultBootstrapStr, err := bootstrap.GetRenderedBootstrapConfig(nil)
if err != nil {
return err
}
if err := proto.FromYAML([]byte(defaultBootstrapStr), defaultBootstrap); err != nil {
return fmt.Errorf("unable to unmarshal default bootstrap: %w", err)
}
if err := defaultBootstrap.Validate(); err != nil {
return fmt.Errorf("default bootstrap validation failed: %w", err)
}

// Validate user bootstrap config
userBootstrapStr, err := bootstrap.ApplyBootstrapConfig(boostrapConfig, defaultBootstrapStr)
if err != nil {
return err
}

jsonData, err := yaml.YAMLToJSON([]byte(userBootstrapStr))
if err != nil {
return fmt.Errorf("unable to convert user bootstrap to json: %w", err)
}

userBootstrap := &bootstrapv3.Bootstrap{}
if err := protojson.Unmarshal(jsonData, userBootstrap); err != nil {
return fmt.Errorf("unable to unmarshal user bootstrap: %w", err)
if err := proto.FromYAML([]byte(userBootstrapStr), userBootstrap); err != nil {
return fmt.Errorf("failed to parse default bootstrap config: %w", err)
}

// Call Validate method
if err := userBootstrap.Validate(); err != nil {
return fmt.Errorf("validation failed for user bootstrap: %w", err)
}

jsonData, err = yaml.YAMLToJSON([]byte(defaultBootstrapStr))
if err != nil {
return fmt.Errorf("unable to convert default bootstrap to json: %w", err)
}

if err := protojson.Unmarshal(jsonData, defaultBootstrap); err != nil {
return fmt.Errorf("unable to unmarshal default bootstrap: %w", err)
}

// Ensure dynamic resources config is same
if userBootstrap.DynamicResources == nil ||
cmp.Diff(userBootstrap.DynamicResources, defaultBootstrap.DynamicResources, protocmp.Transform()) != "" {
Expand All @@ -196,9 +185,8 @@ func validateBootstrap(boostrapConfig *egv1a1.ProxyBootstrap) error {
break
}
}

// nolint // Circumvents this error "Error: copylocks: call of reflect.DeepEqual copies lock value:"
if userXdsCluster == nil || !reflect.DeepEqual(*userXdsCluster.LoadAssignment, *defaultXdsCluster.LoadAssignment) {
if userXdsCluster == nil ||
cmp.Diff(userXdsCluster.LoadAssignment, defaultXdsCluster.LoadAssignment, protocmp.Transform()) != "" {
return fmt.Errorf("xds_cluster's loadAssigntment cannot be modified")
}

Expand Down
128 changes: 128 additions & 0 deletions internal/utils/proto/google_proto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

// Copied from https://github.com/kumahq/kuma/tree/9ea78e31147a855ac54a7a2c92c724ee9a75de46/pkg/util/proto
// to avoid importing the entire kuma codebase breaking our go.mod file

package proto

import (
"fmt"

"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/durationpb"
)

type (
MergeFunction func(dst, src protoreflect.Message)
mergeOptions struct {
customMergeFn map[protoreflect.FullName]MergeFunction
}
)
type OptionFn func(options mergeOptions) mergeOptions

func MergeFunctionOptionFn(name protoreflect.FullName, function MergeFunction) OptionFn {
return func(options mergeOptions) mergeOptions {
options.customMergeFn[name] = function
return options
}
}

// ReplaceMergeFn instead of merging all subfields one by one, takes src and set it to dest
var ReplaceMergeFn MergeFunction = func(dst, src protoreflect.Message) {
dst.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
dst.Clear(fd)
return true
})
src.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
dst.Set(fd, v)
return true
})
}

func Merge(dst, src proto.Message) {
duration := &durationpb.Duration{}
merge(dst, src, MergeFunctionOptionFn(duration.ProtoReflect().Descriptor().FullName(), ReplaceMergeFn))
}

// Merge Code of proto.Merge with modifications to support custom types
func merge(dst, src proto.Message, opts ...OptionFn) {
mo := mergeOptions{customMergeFn: map[protoreflect.FullName]MergeFunction{}}
for _, opt := range opts {
mo = opt(mo)
}
mo.mergeMessage(dst.ProtoReflect(), src.ProtoReflect())
}

func (o mergeOptions) mergeMessage(dst, src protoreflect.Message) {
// The regular proto.mergeMessage would have a fast path method option here.
// As we want to have exceptions we always use the slow path.
if !dst.IsValid() {
panic(fmt.Sprintf("cannot merge into invalid %v message", dst.Descriptor().FullName()))
}

src.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
switch {
case fd.IsList():
o.mergeList(dst.Mutable(fd).List(), v.List(), fd)
case fd.IsMap():
o.mergeMap(dst.Mutable(fd).Map(), v.Map(), fd.MapValue())
case fd.Message() != nil:
mergeFn, exists := o.customMergeFn[fd.Message().FullName()]
if exists {
mergeFn(dst.Mutable(fd).Message(), v.Message())
} else {
o.mergeMessage(dst.Mutable(fd).Message(), v.Message())
}
case fd.Kind() == protoreflect.BytesKind:
dst.Set(fd, o.cloneBytes(v))
default:
dst.Set(fd, v)
}
return true
})

if len(src.GetUnknown()) > 0 {
dst.SetUnknown(append(dst.GetUnknown(), src.GetUnknown()...))
}
}

func (o mergeOptions) mergeList(dst, src protoreflect.List, fd protoreflect.FieldDescriptor) {
// Merge semantics appends to the end of the existing list.
for i, n := 0, src.Len(); i < n; i++ {
switch v := src.Get(i); {
case fd.Message() != nil:
dstv := dst.NewElement()
o.mergeMessage(dstv.Message(), v.Message())
dst.Append(dstv)
case fd.Kind() == protoreflect.BytesKind:
dst.Append(o.cloneBytes(v))
default:
dst.Append(v)
}
}
}

func (o mergeOptions) mergeMap(dst, src protoreflect.Map, fd protoreflect.FieldDescriptor) {
// Merge semantics replaces, rather than merges into existing entries.
src.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
switch {
case fd.Message() != nil:
dstv := dst.NewValue()
o.mergeMessage(dstv.Message(), v.Message())
dst.Set(k, dstv)
case fd.Kind() == protoreflect.BytesKind:
dst.Set(k, o.cloneBytes(v))
default:
dst.Set(k, v)
}
return true
})
}

func (o mergeOptions) cloneBytes(v protoreflect.Value) protoreflect.Value {
return protoreflect.ValueOfBytes(append([]byte{}, v.Bytes()...))
}
40 changes: 40 additions & 0 deletions internal/utils/proto/proto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

// Copied from https://github.com/kumahq/kuma/tree/9ea78e31147a855ac54a7a2c92c724ee9a75de46/pkg/util/proto
// to avoid importing the entire kuma codebase breaking our go.mod file

package proto

import (
"bytes"

"github.com/golang/protobuf/jsonpb"
protov1 "github.com/golang/protobuf/proto"
"google.golang.org/protobuf/proto"
"sigs.k8s.io/yaml"
)

func FromYAML(content []byte, pb proto.Message) error {
json, err := yaml.YAMLToJSON(content)
if err != nil {
return err
}
return FromJSON(json, pb)
}

func ToYAML(pb proto.Message) ([]byte, error) {
marshaler := &jsonpb.Marshaler{}
json, err := marshaler.MarshalToString(protov1.MessageV1(pb))
if err != nil {
return nil, err
}
return yaml.JSONToYAML([]byte(json))
}

func FromJSON(content []byte, out proto.Message) error {
unmarshaler := &jsonpb.Unmarshaler{AllowUnknownFields: true}
return unmarshaler.Unmarshal(bytes.NewReader(content), protov1.MessageV1(out))
}
62 changes: 0 additions & 62 deletions internal/utils/yaml/yaml.go

This file was deleted.

67 changes: 0 additions & 67 deletions internal/utils/yaml/yaml_test.go

This file was deleted.

Loading

0 comments on commit 96c0cda

Please sign in to comment.