diff --git a/acceptance/go.mod b/acceptance/go.mod index 66fa6c4b44..503ca5333c 100644 --- a/acceptance/go.mod +++ b/acceptance/go.mod @@ -101,4 +101,4 @@ require ( k8s.io/utils v0.0.0-20220812165043-ad590609e2e5 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect sigs.k8s.io/yaml v1.2.0 // indirect -) +) \ No newline at end of file diff --git a/acceptance/go.sum b/acceptance/go.sum index 44969f22ae..3301c3a7da 100644 --- a/acceptance/go.sum +++ b/acceptance/go.sum @@ -1213,4 +1213,4 @@ sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3 sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= \ No newline at end of file diff --git a/acceptance/tests/fixtures/bases/crds-oss/serviceresolver.yaml b/acceptance/tests/fixtures/bases/crds-oss/serviceresolver.yaml index a71da92e35..fc236966d6 100644 --- a/acceptance/tests/fixtures/bases/crds-oss/serviceresolver.yaml +++ b/acceptance/tests/fixtures/bases/crds-oss/serviceresolver.yaml @@ -7,4 +7,4 @@ metadata: name: resolver spec: redirect: - service: bar + service: bar \ No newline at end of file diff --git a/charts/consul/templates/crd-serviceresolvers.yaml b/charts/consul/templates/crd-serviceresolvers.yaml index 04d6dd9754..2a6f7923b8 100644 --- a/charts/consul/templates/crd-serviceresolvers.yaml +++ b/charts/consul/templates/crd-serviceresolvers.yaml @@ -95,6 +95,10 @@ spec: type: string type: array type: object + samenessGroup: + description: SamenessGroup is the name of the sameness group + to try during failover. + type: string service: description: Service is the service to resolve instead of the default as the failover group of instances during failover. @@ -248,6 +252,10 @@ spec: description: Peer is the name of the cluster peer to resolve the service from instead of the current one. type: string + samenessGroup: + description: SamenessGroup is the name of the sameness group to + resolve the service from instead of the current one. + type: string service: description: Service is a service to resolve instead of the current service. diff --git a/control-plane/api/v1alpha1/serviceresolver_types.go b/control-plane/api/v1alpha1/serviceresolver_types.go index 48483550ba..75aa44f6b9 100644 --- a/control-plane/api/v1alpha1/serviceresolver_types.go +++ b/control-plane/api/v1alpha1/serviceresolver_types.go @@ -5,16 +5,19 @@ package v1alpha1 import ( "encoding/json" + "regexp" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/hashicorp/consul-k8s/control-plane/api/common" - capi "github.com/hashicorp/consul/api" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/hashicorp/consul-k8s/control-plane/api/common" + capi "github.com/hashicorp/consul/api" + "github.com/hashicorp/go-bexpr" ) const ServiceResolverKubeKind string = "serviceresolver" @@ -97,6 +100,8 @@ type ServiceResolverRedirect struct { // Peer is the name of the cluster peer to resolve the service from instead // of the current one. Peer string `json:"peer,omitempty"` + // SamenessGroup is the name of the sameness group to resolve the service from instead of the current one. + SamenessGroup string `json:"samenessGroup,omitempty"` } type ServiceResolverSubsetMap map[string]ServiceResolverSubset @@ -133,6 +138,8 @@ type ServiceResolverFailover struct { Targets []ServiceResolverFailoverTarget `json:"targets,omitempty"` // Policy specifies the exact mechanism used for failover. Policy *FailoverPolicy `json:"policy,omitempty"` + // SamenessGroup is the name of the sameness group to try during failover. + SamenessGroup string `json:"samenessGroup,omitempty"` } type ServiceResolverFailoverTarget struct { @@ -322,14 +329,17 @@ func (in *ServiceResolver) Validate(consulMeta common.ConsulMeta) error { var errs field.ErrorList path := field.NewPath("spec") - for k, v := range in.Spec.Failover { - if err := v.validate(path.Child("failover").Key(k)); err != nil { - errs = append(errs, err) - } + for subset, f := range in.Spec.Failover { + errs = append(errs, f.validate(path.Child("failover").Key(subset), consulMeta)...) + } + if len(in.Spec.Failover) > 0 && in.Spec.Redirect != nil { + asJSON, _ := json.Marshal(in) + errs = append(errs, field.Invalid(path, string(asJSON), "service resolver redirect and failover cannot both be set")) } + errs = append(errs, in.Spec.Redirect.validate(path.Child("redirect"), consulMeta)...) + errs = append(errs, in.Spec.Subsets.validate(path.Child("subsets"))...) errs = append(errs, in.Spec.LoadBalancer.validate(path.Child("loadBalancer"))...) - errs = append(errs, in.validateEnterprise(consulMeta)...) if len(errs) > 0 { @@ -356,6 +366,31 @@ func (in ServiceResolverSubsetMap) toConsul() map[string]capi.ServiceResolverSub return m } +func (in ServiceResolverSubsetMap) validate(path *field.Path) field.ErrorList { + var errs field.ErrorList + if len(in) == 0 { + return nil + } + validServiceSubset := regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`) + + for name, subset := range in { + indexPath := path.Key(name) + + if name == "" { + errs = append(errs, field.Invalid(indexPath, name, "subset defined with empty name")) + } + if !validServiceSubset.MatchString(name) { + errs = append(errs, field.Invalid(indexPath, name, "subset name must begin or end with lower case alphanumeric characters, and contain lower case alphanumeric characters or '-' in between")) + } + if subset.Filter != "" { + if _, err := bexpr.CreateEvaluator(subset.Filter, nil); err != nil { + errs = append(errs, field.Invalid(indexPath.Child("filter"), subset.Filter, "filter for subset is not a valid expression")) + } + } + } + return errs +} + func (in ServiceResolverSubset) toConsul() capi.ServiceResolverSubset { return capi.ServiceResolverSubset{ Filter: in.Filter, @@ -374,7 +409,74 @@ func (in *ServiceResolverRedirect) toConsul() *capi.ServiceResolverRedirect { Datacenter: in.Datacenter, Partition: in.Partition, Peer: in.Peer, + SamenessGroup: in.SamenessGroup, + } +} + +func (in *ServiceResolverRedirect) validate(path *field.Path, consulMeta common.ConsulMeta) field.ErrorList { + var errs field.ErrorList + if in == nil { + return nil + } + + asJSON, _ := json.Marshal(in) + if in.isEmpty() { + errs = append(errs, field.Invalid(path, "{}", + "service resolver redirect cannot be empty")) + } + + if consulMeta.Partition != "default" && in.Datacenter != "" { + errs = append(errs, field.Invalid(path.Child("datacenter"), in.Datacenter, + "cross-datacenter redirect is only supported in the default partition")) + } + if consulMeta.Partition != in.Partition && in.Datacenter != "" { + errs = append(errs, field.Invalid(path.Child("partition"), in.Partition, + "cross-datacenter and cross-partition redirect is not supported")) + } + + switch { + case in.SamenessGroup != "" && in.ServiceSubset != "": + errs = append(errs, field.Invalid(path, string(asJSON), + "samenessGroup cannot be set with serviceSubset")) + case in.SamenessGroup != "" && in.Partition != "": + errs = append(errs, field.Invalid(path, string(asJSON), + "partition cannot be set with samenessGroup")) + case in.SamenessGroup != "" && in.Datacenter != "": + errs = append(errs, field.Invalid(path, string(asJSON), + "samenessGroup cannot be set with datacenter")) + case in.Peer != "" && in.ServiceSubset != "": + errs = append(errs, field.Invalid(path, string(asJSON), + "peer cannot be set with serviceSubset")) + case in.Peer != "" && in.Partition != "": + errs = append(errs, field.Invalid(path, string(asJSON), + "partition cannot be set with peer")) + case in.Peer != "" && in.Datacenter != "": + errs = append(errs, field.Invalid(path, string(asJSON), + "peer cannot be set with datacenter")) + case in.Service == "": + if in.ServiceSubset != "" { + errs = append(errs, field.Invalid(path, string(asJSON), + "serviceSubset defined without service")) + } + if in.Namespace != "" { + errs = append(errs, field.Invalid(path, string(asJSON), + "namespace defined without service")) + } + if in.Partition != "" { + errs = append(errs, field.Invalid(path, string(asJSON), + "partition defined without service")) + } + if in.Peer != "" { + errs = append(errs, field.Invalid(path, string(asJSON), + "peer defined without service")) + } } + + return errs +} + +func (in *ServiceResolverRedirect) isEmpty() bool { + return in.Service == "" && in.ServiceSubset == "" && in.Namespace == "" && in.Partition == "" && in.Datacenter == "" && in.Peer == "" && in.SamenessGroup == "" } func (in ServiceResolverFailoverMap) toConsul() map[string]capi.ServiceResolverFailover { @@ -383,27 +485,38 @@ func (in ServiceResolverFailoverMap) toConsul() map[string]capi.ServiceResolverF } m := make(map[string]capi.ServiceResolverFailover) for k, v := range in { - m[k] = v.toConsul() + if f := v.toConsul(); f != nil { + m[k] = *f + } } return m } -func (in ServiceResolverFailover) toConsul() capi.ServiceResolverFailover { +func (in *ServiceResolverFailover) toConsul() *capi.ServiceResolverFailover { + if in == nil { + return nil + } var targets []capi.ServiceResolverFailoverTarget for _, target := range in.Targets { targets = append(targets, target.toConsul()) } - return capi.ServiceResolverFailover{ + var policy *capi.ServiceResolverFailoverPolicy + if in.Policy != nil { + policy = &capi.ServiceResolverFailoverPolicy{ + Mode: in.Policy.Mode, + Regions: in.Policy.Regions, + } + } + + return &capi.ServiceResolverFailover{ Service: in.Service, ServiceSubset: in.ServiceSubset, Namespace: in.Namespace, Datacenters: in.Datacenters, Targets: targets, - Policy: &capi.ServiceResolverFailoverPolicy{ - Mode: in.Policy.Mode, - Regions: in.Policy.Regions, - }, + Policy: policy, + SamenessGroup: in.SamenessGroup, } } @@ -513,17 +626,79 @@ func (in *ServiceResolver) validateEnterprise(consulMeta common.ConsulMeta) fiel } func (in *ServiceResolverFailover) isEmpty() bool { - return in.Service == "" && in.ServiceSubset == "" && in.Namespace == "" && len(in.Datacenters) == 0 && len(in.Targets) == 0 && in.Policy == nil + return in.Service == "" && in.ServiceSubset == "" && in.Namespace == "" && len(in.Datacenters) == 0 && len(in.Targets) == 0 && in.Policy == nil && in.SamenessGroup == "" } -func (in *ServiceResolverFailover) validate(path *field.Path) *field.Error { +func (in *ServiceResolverFailover) validate(path *field.Path, consulMeta common.ConsulMeta) field.ErrorList { + var errs field.ErrorList if in.isEmpty() { // NOTE: We're passing "{}" here as our value because we know that the // error is we have an empty object. - return field.Invalid(path, "{}", - "service, serviceSubset, namespace, datacenters, policy, and targets cannot all be empty at once") + errs = append(errs, field.Invalid(path, "{}", + "service, serviceSubset, namespace, datacenters, policy, and targets cannot all be empty at once")) } - return nil + + if consulMeta.Partition != "default" && len(in.Datacenters) != 0 { + errs = append(errs, field.Invalid(path.Child("datacenters"), in.Datacenters, + "cross-datacenter failover is only supported in the default partition")) + } + + errs = append(errs, in.Policy.validate(path.Child("policy"))...) + + asJSON, _ := json.Marshal(in) + if in.SamenessGroup != "" { + switch { + case len(in.Datacenters) > 0: + errs = append(errs, field.Invalid(path, string(asJSON), + "samenessGroup cannot be set with datacenters")) + case in.ServiceSubset != "": + errs = append(errs, field.Invalid(path, string(asJSON), + "samenessGroup cannot be set with serviceSubset")) + case len(in.Targets) > 0: + errs = append(errs, field.Invalid(path, string(asJSON), + "samenessGroup cannot be set with targets")) + } + } + + if len(in.Datacenters) != 0 && len(in.Targets) != 0 { + errs = append(errs, field.Invalid(path, string(asJSON), + "targets cannot be set with datacenters")) + } + + if in.ServiceSubset != "" && len(in.Targets) != 0 { + errs = append(errs, field.Invalid(path, string(asJSON), + "targets cannot be set with serviceSubset")) + } + + if in.Service != "" && len(in.Targets) != 0 { + errs = append(errs, field.Invalid(path, string(asJSON), + "targets cannot be set with service")) + } + + for i, target := range in.Targets { + asJSON, _ := json.Marshal(target) + switch { + case target.Peer != "" && target.ServiceSubset != "": + errs = append(errs, field.Invalid(path.Child("targets").Index(i), string(asJSON), + "target.peer cannot be set with target.serviceSubset")) + case target.Peer != "" && target.Partition != "": + errs = append(errs, field.Invalid(path.Child("targets").Index(i), string(asJSON), + "target.partition cannot be set with target.peer")) + case target.Peer != "" && target.Datacenter != "": + errs = append(errs, field.Invalid(path.Child("targets").Index(i), string(asJSON), + "target.peer cannot be set with target.datacenter")) + case target.Partition != "" && target.Datacenter != "": + errs = append(errs, field.Invalid(path.Child("targets").Index(i), string(asJSON), + "target.partition cannot be set with target.datacenter")) + } + } + + for i, dc := range in.Datacenters { + if dc == "" { + errs = append(errs, field.Invalid(path.Child("datacenters").Index(i), "", "found empty datacenter")) + } + } + return errs } func (in *LoadBalancer) validate(path *field.Path) field.ErrorList { diff --git a/control-plane/api/v1alpha1/serviceresolver_types_test.go b/control-plane/api/v1alpha1/serviceresolver_types_test.go index c82394784d..d09f0809c8 100644 --- a/control-plane/api/v1alpha1/serviceresolver_types_test.go +++ b/control-plane/api/v1alpha1/serviceresolver_types_test.go @@ -4,6 +4,7 @@ package v1alpha1 import ( + "strings" "testing" "time" @@ -12,6 +13,7 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" ) func TestServiceResolver_MatchesConsul(t *testing.T) { @@ -74,6 +76,7 @@ func TestServiceResolver_MatchesConsul(t *testing.T) { Mode: "sequential", Regions: []string{"us-west-2"}, }, + SamenessGroup: "sg2", }, "failover2": { Service: "failover2", @@ -84,6 +87,7 @@ func TestServiceResolver_MatchesConsul(t *testing.T) { Mode: "", Regions: []string{"us-west-1"}, }, + SamenessGroup: "sg3", }, "failover3": { Targets: []ServiceResolverFailoverTarget{ @@ -153,6 +157,7 @@ func TestServiceResolver_MatchesConsul(t *testing.T) { Mode: "sequential", Regions: []string{"us-west-2"}, }, + SamenessGroup: "sg2", }, "failover2": { Service: "failover2", @@ -163,6 +168,7 @@ func TestServiceResolver_MatchesConsul(t *testing.T) { Mode: "", Regions: []string{"us-west-1"}, }, + SamenessGroup: "sg3", }, "failover3": { Targets: []capi.ServiceResolverFailoverTarget{ @@ -281,6 +287,7 @@ func TestServiceResolver_ToConsul(t *testing.T) { Mode: "sequential", Regions: []string{"us-west-2"}, }, + SamenessGroup: "sg2", }, "failover2": { Service: "failover2", @@ -291,6 +298,7 @@ func TestServiceResolver_ToConsul(t *testing.T) { Mode: "", Regions: []string{"us-west-1"}, }, + SamenessGroup: "sg3", }, "failover3": { Targets: []ServiceResolverFailoverTarget{ @@ -360,6 +368,7 @@ func TestServiceResolver_ToConsul(t *testing.T) { Mode: "sequential", Regions: []string{"us-west-2"}, }, + SamenessGroup: "sg2", }, "failover2": { Service: "failover2", @@ -370,6 +379,7 @@ func TestServiceResolver_ToConsul(t *testing.T) { Mode: "", Regions: []string{"us-west-1"}, }, + SamenessGroup: "sg3", }, "failover3": { Targets: []capi.ServiceResolverFailoverTarget{ @@ -543,16 +553,15 @@ func TestServiceResolver_Validate(t *testing.T) { Name: "foo", }, Spec: ServiceResolverSpec{ - Redirect: &ServiceResolverRedirect{ - Service: "bar", - Namespace: "namespace-a", - }, Failover: map[string]ServiceResolverFailover{ - "failA": { + "v1": { Service: "baz", Namespace: "namespace-b", }, }, + Subsets: map[string]ServiceResolverSubset{ + "v1": {Filter: "Service.Meta.version == v1"}, + }, }, }, namespacesEnabled: true, @@ -568,10 +577,8 @@ func TestServiceResolver_Validate(t *testing.T) { Redirect: &ServiceResolverRedirect{ Service: "bar", }, - Failover: map[string]ServiceResolverFailover{ - "failA": { - Service: "baz", - }, + Subsets: map[string]ServiceResolverSubset{ + "v1": {Filter: "Service.Meta.version == v1"}, }, }, }, @@ -585,17 +592,15 @@ func TestServiceResolver_Validate(t *testing.T) { Name: "foo", }, Spec: ServiceResolverSpec{ - Redirect: &ServiceResolverRedirect{ - Service: "bar", - Namespace: "namespace-a", - Partition: "other", - }, Failover: map[string]ServiceResolverFailover{ - "failA": { + "v1": { Service: "baz", Namespace: "namespace-b", }, }, + Subsets: map[string]ServiceResolverSubset{ + "v1": {Filter: "Service.Meta.version == v1"}, + }, }, }, namespacesEnabled: true, @@ -611,11 +616,6 @@ func TestServiceResolver_Validate(t *testing.T) { Redirect: &ServiceResolverRedirect{ Service: "bar", }, - Failover: map[string]ServiceResolverFailover{ - "failA": { - Service: "baz", - }, - }, }, }, namespacesEnabled: false, @@ -629,13 +629,13 @@ func TestServiceResolver_Validate(t *testing.T) { }, Spec: ServiceResolverSpec{ Failover: map[string]ServiceResolverFailover{ - "failA": { + "v1": { Service: "", ServiceSubset: "", Namespace: "", Datacenters: nil, }, - "failB": { + "v2": { Service: "", ServiceSubset: "", Namespace: "", @@ -646,10 +646,32 @@ func TestServiceResolver_Validate(t *testing.T) { }, namespacesEnabled: false, expectedErrMsgs: []string{ - "spec.failover[failA]: Invalid value: \"{}\": service, serviceSubset, namespace, datacenters, policy, and targets cannot all be empty at once", - "spec.failover[failB]: Invalid value: \"{}\": service, serviceSubset, namespace, datacenters, policy, and targets cannot all be empty at once", + "spec.failover[v1]: Invalid value: \"{}\": service, serviceSubset, namespace, datacenters, policy, and targets cannot all be empty at once", + "spec.failover[v2]: Invalid value: \"{}\": service, serviceSubset, namespace, datacenters, policy, and targets cannot all be empty at once", }, }, + "service resolver redirect and failover cannot both be set": { + input: &ServiceResolver{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ServiceResolverSpec{ + Redirect: &ServiceResolverRedirect{ + Service: "bar", + Namespace: "namespace-a", + }, + Failover: map[string]ServiceResolverFailover{ + "failA": { + Service: "baz", + Namespace: "namespace-b", + }, + }, + }, + }, + namespacesEnabled: true, + partitionsEnabled: false, + expectedErrMsgs: []string{"service resolver redirect and failover cannot both be set"}, + }, "hashPolicy.field invalid": { input: &ServiceResolver{ ObjectMeta: metav1.ObjectMeta{ @@ -723,11 +745,19 @@ func TestServiceResolver_Validate(t *testing.T) { }, }, }, + Subsets: map[string]ServiceResolverSubset{ + "": { + Filter: "random string", + }, + }, }, }, namespacesEnabled: false, expectedErrMsgs: []string{ - `serviceresolver.consul.hashicorp.com "foo" is invalid: spec.loadBalancer.hashPolicies[0]: Invalid value: "{\"field\":\"header\",\"sourceIP\":true}": cannot set both field and sourceIP`, + `spec.loadBalancer.hashPolicies[0]: Invalid value: "{\"field\":\"header\",\"sourceIP\":true}": cannot set both field and sourceIP`, + `subset defined with empty name`, + `subset name must begin or end with lower case alphanumeric characters, and contain lower case alphanumeric characters or '-' in between`, + `filter for subset is not a valid expression`, }, }, "hashPolicy nothing set is valid": { @@ -778,6 +808,7 @@ func TestServiceResolver_Validate(t *testing.T) { }, Spec: ServiceResolverSpec{ Redirect: &ServiceResolverRedirect{ + Service: "bar", Namespace: "namespace-a", }, }, @@ -794,6 +825,7 @@ func TestServiceResolver_Validate(t *testing.T) { }, Spec: ServiceResolverSpec{ Redirect: &ServiceResolverRedirect{ + Service: "bar", Namespace: "namespace-a", Partition: "other", }, @@ -812,14 +844,19 @@ func TestServiceResolver_Validate(t *testing.T) { }, Spec: ServiceResolverSpec{ Failover: map[string]ServiceResolverFailover{ - "failA": { + "v1": { Namespace: "namespace-a", }, }, + Subsets: map[string]ServiceResolverSubset{ + "v1": { + Filter: "Service.Meta.version == v1", + }, + }, }, }, expectedErrMsgs: []string{ - "serviceresolver.consul.hashicorp.com \"foo\" is invalid: spec.failover[failA].namespace: Invalid value: \"namespace-a\": Consul Enterprise namespaces must be enabled to set failover.namespace", + "serviceresolver.consul.hashicorp.com \"foo\" is invalid: spec.failover[v1].namespace: Invalid value: \"namespace-a\": Consul Enterprise namespaces must be enabled to set failover.namespace", }, namespacesEnabled: false, }, @@ -860,3 +897,497 @@ func TestServiceResolver_Validate(t *testing.T) { }) } } + +func TestServiceResolverRedirect_ToConsul(t *testing.T) { + cases := map[string]struct { + Ours *ServiceResolverRedirect + Exp *capi.ServiceResolverRedirect + }{ + "nil": { + Ours: nil, + Exp: nil, + }, + "empty fields": { + Ours: &ServiceResolverRedirect{}, + Exp: &capi.ServiceResolverRedirect{}, + }, + "every field set": { + Ours: &ServiceResolverRedirect{ + Service: "foo", + ServiceSubset: "v1", + Namespace: "ns1", + Datacenter: "dc1", + Partition: "default", + Peer: "peer1", + SamenessGroup: "sg1", + }, + Exp: &capi.ServiceResolverRedirect{ + Service: "foo", + ServiceSubset: "v1", + Namespace: "ns1", + Datacenter: "dc1", + Partition: "default", + Peer: "peer1", + SamenessGroup: "sg1", + }, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + actual := c.Ours.toConsul() + require.Equal(t, c.Exp, actual) + }) + } +} + +func TestServiceResolverRedirect_Validate(t *testing.T) { + cases := map[string]struct { + input *ServiceResolverRedirect + consulMeta common.ConsulMeta + expectedErrMsgs []string + }{ + "empty redirect": { + input: &ServiceResolverRedirect{}, + consulMeta: common.ConsulMeta{}, + expectedErrMsgs: []string{ + "service resolver redirect cannot be empty", + }, + }, + "cross-datacenter redirect is only supported in the default partition": { + input: &ServiceResolverRedirect{ + Datacenter: "dc2", + Partition: "p2", + Service: "foo", + }, + consulMeta: common.ConsulMeta{ + Partition: "p2", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "cross-datacenter redirect is only supported in the default partition", + }, + }, + "cross-datacenter and cross-partition redirect is not supported": { + input: &ServiceResolverRedirect{ + Partition: "p1", + Datacenter: "dc2", + Service: "foo", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "cross-datacenter and cross-partition redirect is not supported", + }, + }, + "samenessGroup cannot be set with serviceSubset": { + input: &ServiceResolverRedirect{ + Service: "foo", + ServiceSubset: "v1", + SamenessGroup: "sg2", + }, + expectedErrMsgs: []string{ + "samenessGroup cannot be set with serviceSubset", + }, + }, + "samenessGroup cannot be set with partition": { + input: &ServiceResolverRedirect{ + Partition: "default", + Service: "foo", + SamenessGroup: "sg2", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "partition cannot be set with samenessGroup", + }, + }, + "samenessGroup cannot be set with datacenter": { + input: &ServiceResolverRedirect{ + Datacenter: "dc2", + Service: "foo", + SamenessGroup: "sg2", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "cross-datacenter and cross-partition redirect is not supported", + "samenessGroup cannot be set with datacenter", + }, + }, + "peer cannot be set with serviceSubset": { + input: &ServiceResolverRedirect{ + Peer: "p2", + Service: "foo", + ServiceSubset: "v1", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "peer cannot be set with serviceSubset", + }, + }, + "partition cannot be set with peer": { + input: &ServiceResolverRedirect{ + Partition: "default", + Peer: "p2", + Service: "foo", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "partition cannot be set with peer", + }, + }, + "peer cannot be set with datacenter": { + input: &ServiceResolverRedirect{ + Peer: "p2", + Service: "foo", + Datacenter: "dc2", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "peer cannot be set with datacenter", + "cross-datacenter and cross-partition redirect is not supported", + }, + }, + "serviceSubset defined without service": { + input: &ServiceResolverRedirect{ + ServiceSubset: "v1", + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "serviceSubset defined without service", + }, + }, + "namespace defined without service": { + input: &ServiceResolverRedirect{ + Namespace: "ns1", + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "namespace defined without service", + }, + }, + "partition defined without service": { + input: &ServiceResolverRedirect{ + Partition: "default", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "partition defined without service", + }, + }, + "peer defined without service": { + input: &ServiceResolverRedirect{ + Peer: "p2", + }, + consulMeta: common.ConsulMeta{ + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "peer defined without service", + }, + }, + } + + path := field.NewPath("spec.redirect") + for name, testCase := range cases { + t.Run(name, func(t *testing.T) { + errList := testCase.input.validate(path, testCase.consulMeta) + compareErrorLists(t, testCase.expectedErrMsgs, errList) + }) + } +} + +func compareErrorLists(t *testing.T, expectedErrMsgs []string, errList field.ErrorList) { + if len(expectedErrMsgs) != 0 { + require.Equal(t, len(expectedErrMsgs), len(errList)) + for _, m := range expectedErrMsgs { + found := false + for _, e := range errList { + errMsg := e.ErrorBody() + if strings.Contains(errMsg, m) { + found = true + break + } + } + require.Equal(t, true, found) + } + } else { + require.Equal(t, 0, len(errList)) + } +} + +func TestServiceResolverFailover_ToConsul(t *testing.T) { + cases := map[string]struct { + Ours *ServiceResolverFailover + Exp *capi.ServiceResolverFailover + }{ + "nil": { + Ours: nil, + Exp: nil, + }, + "empty fields": { + Ours: &ServiceResolverFailover{}, + Exp: &capi.ServiceResolverFailover{}, + }, + "every field set": { + Ours: &ServiceResolverFailover{ + Service: "foo", + ServiceSubset: "v1", + Namespace: "ns1", + Datacenters: []string{"dc1"}, + Targets: []ServiceResolverFailoverTarget{ + { + Peer: "p2", + }, + }, + Policy: &FailoverPolicy{ + Mode: "sequential", + Regions: []string{"us-west-2"}, + }, + SamenessGroup: "sg1", + }, + Exp: &capi.ServiceResolverFailover{ + Service: "foo", + ServiceSubset: "v1", + Namespace: "ns1", + Datacenters: []string{"dc1"}, + Targets: []capi.ServiceResolverFailoverTarget{ + { + Peer: "p2", + }, + }, + Policy: &capi.ServiceResolverFailoverPolicy{ + Mode: "sequential", + Regions: []string{"us-west-2"}, + }, + SamenessGroup: "sg1", + }, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + actual := c.Ours.toConsul() + require.Equal(t, c.Exp, actual) + }) + } +} + +func TestServiceResolverFailover_Validate(t *testing.T) { + cases := map[string]struct { + input *ServiceResolverFailover + consulMeta common.ConsulMeta + expectedErrMsgs []string + }{ + "empty failover": { + input: &ServiceResolverFailover{}, + consulMeta: common.ConsulMeta{}, + expectedErrMsgs: []string{ + "service, serviceSubset, namespace, datacenters, policy, and targets cannot all be empty at once", + }, + }, + "cross-datacenter failover is only supported in the default partition": { + input: &ServiceResolverFailover{ + Datacenters: []string{"dc2"}, + Service: "foo", + }, + consulMeta: common.ConsulMeta{ + Partition: "p2", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "cross-datacenter failover is only supported in the default partition", + }, + }, + "samenessGroup cannot be set with datacenters": { + input: &ServiceResolverFailover{ + Service: "foo", + Datacenters: []string{"dc2"}, + SamenessGroup: "sg2", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "samenessGroup cannot be set with datacenters", + }, + }, + "samenessGroup cannot be set with serviceSubset": { + input: &ServiceResolverFailover{ + ServiceSubset: "v1", + Service: "foo", + SamenessGroup: "sg2", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "samenessGroup cannot be set with serviceSubset", + }, + }, + "samenessGroup cannot be set with targets": { + input: &ServiceResolverFailover{ + Targets: []ServiceResolverFailoverTarget{ + { + Peer: "p2", + }, + }, + SamenessGroup: "sg2", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "samenessGroup cannot be set with targets", + }, + }, + "targets cannot be set with datacenters": { + input: &ServiceResolverFailover{ + Targets: []ServiceResolverFailoverTarget{ + { + Peer: "p2", + }, + }, + Datacenters: []string{"dc1"}, + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "targets cannot be set with datacenters", + }, + }, + "targets cannot be set with serviceSubset or service": { + input: &ServiceResolverFailover{ + Targets: []ServiceResolverFailoverTarget{ + { + Peer: "p2", + }, + }, + ServiceSubset: "v1", + Service: "foo", + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "targets cannot be set with serviceSubset", + "targets cannot be set with service", + }, + }, + "target.peer cannot be set with target.serviceSubset": { + input: &ServiceResolverFailover{ + Targets: []ServiceResolverFailoverTarget{ + { + Peer: "p2", + ServiceSubset: "v1", + }, + }, + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "target.peer cannot be set with target.serviceSubset", + }, + }, + "target.partition cannot be set with target.peer": { + input: &ServiceResolverFailover{ + Targets: []ServiceResolverFailoverTarget{ + { + Peer: "p2", + Partition: "partition2", + }, + }, + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "target.partition cannot be set with target.peer", + }, + }, + "target.peer cannot be set with target.datacenter": { + input: &ServiceResolverFailover{ + Targets: []ServiceResolverFailoverTarget{ + { + Peer: "p2", + Datacenter: "dc2", + }, + }, + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "target.peer cannot be set with target.datacenter", + }, + }, + "target.partition cannot be set with target.datacenter": { + input: &ServiceResolverFailover{ + Targets: []ServiceResolverFailoverTarget{ + { + Partition: "p2", + Datacenter: "dc2", + }, + }, + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "target.partition cannot be set with target.datacenter", + }, + }, + "found empty datacenter": { + input: &ServiceResolverFailover{ + Datacenters: []string{""}, + }, + consulMeta: common.ConsulMeta{ + Partition: "default", + PartitionsEnabled: true, + }, + expectedErrMsgs: []string{ + "found empty datacenter", + }, + }, + } + + path := field.NewPath("spec.redirect") + for name, testCase := range cases { + t.Run(name, func(t *testing.T) { + errList := testCase.input.validate(path, testCase.consulMeta) + compareErrorLists(t, testCase.expectedErrMsgs, errList) + }) + } +} diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml index bae83ac7b9..69084724a9 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml @@ -91,6 +91,10 @@ spec: type: string type: array type: object + samenessGroup: + description: SamenessGroup is the name of the sameness group + to try during failover. + type: string service: description: Service is the service to resolve instead of the default as the failover group of instances during failover. @@ -244,6 +248,10 @@ spec: description: Peer is the name of the cluster peer to resolve the service from instead of the current one. type: string + samenessGroup: + description: SamenessGroup is the name of the sameness group to + resolve the service from instead of the current one. + type: string service: description: Service is a service to resolve instead of the current service. diff --git a/control-plane/go.mod b/control-plane/go.mod index 5307056ce3..06ac31d7eb 100644 --- a/control-plane/go.mod +++ b/control-plane/go.mod @@ -6,12 +6,13 @@ require ( github.com/deckarep/golang-set v1.7.1 github.com/fsnotify/fsnotify v1.5.4 github.com/go-logr/logr v0.4.0 - github.com/google/go-cmp v0.5.7 + github.com/google/go-cmp v0.5.8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/consul-k8s/control-plane/cni v0.0.0-20220831174802-b8af65262de8 github.com/hashicorp/consul-server-connection-manager v0.1.0 - github.com/hashicorp/consul/api v1.10.1-0.20230418163148-eb9f671eafae + github.com/hashicorp/consul/api v1.10.1-0.20230427155444-391ed069c461 github.com/hashicorp/consul/sdk v0.13.1 + github.com/hashicorp/go-bexpr v0.1.11 github.com/hashicorp/go-discover v0.0.0-20200812215701-c4b85f6ed31f github.com/hashicorp/go-hclog v1.2.2 github.com/hashicorp/go-multierror v1.1.1 @@ -104,6 +105,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -130,12 +132,12 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/sys v0.1.0 // indirect golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/api v0.43.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect diff --git a/control-plane/go.sum b/control-plane/go.sum index c045aba7bd..2ad5716991 100644 --- a/control-plane/go.sum +++ b/control-plane/go.sum @@ -299,8 +299,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -353,8 +353,10 @@ github.com/hashicorp/consul-k8s/control-plane/cni v0.0.0-20220831174802-b8af6526 github.com/hashicorp/consul-server-connection-manager v0.1.0 h1:XCweGvMHzra88rYv2zxwwuUOjBUdcQmNKVrnQmt/muo= github.com/hashicorp/consul-server-connection-manager v0.1.0/go.mod h1:XVVlO+Yk7aiRpspiHZkrrFVn9BJIiOPnQIzqytPxGaU= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.10.1-0.20230418163148-eb9f671eafae h1:lYnO52QxlfATRZ1Vo8tQV+lFns7rZ4iAbbi3JN4ZAQw= -github.com/hashicorp/consul/api v1.10.1-0.20230418163148-eb9f671eafae/go.mod h1:f8zVJwBcLdr1IQnfdfszjUM0xzp31Zl3bpws3pL9uFM= +github.com/hashicorp/consul/api v1.10.1-0.20230424202255-e47f3216e51b h1:6JQAVFzqHzB51SP55BOLoDsw6sVD2WGkNSOoxI1hVZ4= +github.com/hashicorp/consul/api v1.10.1-0.20230424202255-e47f3216e51b/go.mod h1:tXfrC6o0yFTgAW46xd5Ic8STHc9oIBcRVBcwhX5KNCQ= +github.com/hashicorp/consul/api v1.10.1-0.20230427155444-391ed069c461 h1:cbsTR88ShbvcRMqLU8K0atm4GmRr8UH4x4jX4e12RYE= +github.com/hashicorp/consul/api v1.10.1-0.20230427155444-391ed069c461/go.mod h1:tXfrC6o0yFTgAW46xd5Ic8STHc9oIBcRVBcwhX5KNCQ= github.com/hashicorp/consul/proto-public v0.1.0 h1:O0LSmCqydZi363hsqc6n2v5sMz3usQMXZF6ziK3SzXU= github.com/hashicorp/consul/proto-public v0.1.0/go.mod h1:vs2KkuWwtjkIgA5ezp4YKPzQp4GitV+q/+PvksrA92k= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -363,6 +365,8 @@ github.com/hashicorp/consul/sdk v0.13.1/go.mod h1:SW/mM4LbKfqmMvcFu8v+eiQQ7oitXE github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.11 h1:6DqdA/KBjurGby9yTY0bmkathya0lfwF2SeuubCI7dY= +github.com/hashicorp/go-bexpr v0.1.11/go.mod h1:f03lAo0duBlDIUMGCuad8oLcgejw4m7U+N8T+6Kz1AE= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -530,6 +534,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw= +github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= @@ -765,6 +771,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -943,8 +951,9 @@ golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1027,11 +1036,10 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=