Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix bootstrap merge #2801

Merged
merged 7 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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 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 {
zirain marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("unable to unmarshal default bootstrap: %w", err)
}

Check warning on line 150 in api/v1alpha1/validation/envoyproxy_validate.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/validation/envoyproxy_validate.go#L149-L150

Added lines #L149 - L150 were not covered by tests
if err := defaultBootstrap.Validate(); err != nil {
return fmt.Errorf("default bootstrap validation failed: %w", err)
}

Check warning on line 153 in api/v1alpha1/validation/envoyproxy_validate.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/validation/envoyproxy_validate.go#L152-L153

Added lines #L152 - L153 were not covered by tests

// 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)

Check warning on line 162 in api/v1alpha1/validation/envoyproxy_validate.go

View check run for this annotation

Codecov / codecov/patch

api/v1alpha1/validation/envoyproxy_validate.go#L162

Added line #L162 was not covered by tests
}

// 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 @@
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