From 5a49f0db2aaa3574b072d461ce1e8e06e41867bd Mon Sep 17 00:00:00 2001 From: malizz Date: Tue, 18 Apr 2023 16:12:58 -0700 Subject: [PATCH] add sameness group CRD (#2048) * draft of adding sameness group CRD * move sameness group tests to ent test file * update tests * fix lint issues * generate yaml and update helm charts * update field descriptions and validation and its test * remove unwanted files, add license comments back * rename samenessgroups to samenessgroup * fix resource names * update failing unit test --- CONTRIBUTING.md | 10 +- .../templates/connect-inject-clusterrole.yaml | 2 + ...t-inject-mutatingwebhookconfiguration.yaml | 21 + .../consul/templates/crd-proxydefaults.yaml | 14 +- .../consul/templates/crd-samenessgroups.yaml | 126 ++++++ .../templates/crd-serviceresolvers.yaml | 9 +- control-plane/PROJECT | 13 + control-plane/api/common/common.go | 1 + .../api/v1alpha1/samenessgroup_types.go | 262 ++++++++++++ .../api/v1alpha1/samenessgroup_types_test.go | 390 ++++++++++++++++++ .../api/v1alpha1/samenessgroup_webhook.go | 61 +++ .../v1alpha1/servicedefaults_types_test.go | 2 +- control-plane/api/v1alpha1/shared_types.go | 8 +- .../api/v1alpha1/zz_generated.deepcopy.go | 124 ++++++ .../consul.hashicorp.com_proxydefaults.yaml | 12 +- .../consul.hashicorp.com_samenessgroups.yaml | 121 ++++++ ...consul.hashicorp.com_serviceresolvers.yaml | 9 +- control-plane/config/rbac/role.yaml | 23 +- control-plane/config/webhook/manifests.yaml | 24 +- .../configentry_controller.go | 2 +- .../configentry_controller_ent_test.go | 382 +++++++++++++++-- .../configentry_controller_test.go | 2 +- .../exportedservices_controller.go | 2 +- .../exportedservices_controller_ent_test.go | 20 +- .../ingressgateway_controller.go | 2 +- .../mesh_controller.go | 2 +- .../proxydefaults_controller.go | 2 +- .../controllers/samenessgroups_controller.go | 41 ++ .../servicedefaults_controller.go | 2 +- .../serviceintentions_controller.go | 2 +- .../serviceresolver_controller.go | 2 +- .../servicerouter_controller.go | 2 +- .../servicesplitter_controller.go | 2 +- .../terminatinggateway_controller.go | 2 +- control-plane/go.mod | 4 + control-plane/main.go | 3 +- .../subcommand/inject-connect/command.go | 39 +- 37 files changed, 1640 insertions(+), 105 deletions(-) create mode 100644 charts/consul/templates/crd-samenessgroups.yaml create mode 100644 control-plane/api/v1alpha1/samenessgroup_types.go create mode 100644 control-plane/api/v1alpha1/samenessgroup_types_test.go create mode 100644 control-plane/api/v1alpha1/samenessgroup_webhook.go create mode 100644 control-plane/config/crd/bases/consul.hashicorp.com_samenessgroups.yaml rename control-plane/{controller => controllers}/configentry_controller.go (99%) rename control-plane/{controller => controllers}/configentry_controller_ent_test.go (64%) rename control-plane/{controller => controllers}/configentry_controller_test.go (99%) rename control-plane/{controller => controllers}/exportedservices_controller.go (98%) rename control-plane/{controller => controllers}/exportedservices_controller_ent_test.go (95%) rename control-plane/{controller => controllers}/ingressgateway_controller.go (98%) rename control-plane/{controller => controllers}/mesh_controller.go (98%) rename control-plane/{controller => controllers}/proxydefaults_controller.go (98%) create mode 100644 control-plane/controllers/samenessgroups_controller.go rename control-plane/{controller => controllers}/servicedefaults_controller.go (98%) rename control-plane/{controller => controllers}/serviceintentions_controller.go (98%) rename control-plane/{controller => controllers}/serviceresolver_controller.go (98%) rename control-plane/{controller => controllers}/servicerouter_controller.go (98%) rename control-plane/{controller => controllers}/servicesplitter_controller.go (98%) rename control-plane/{controller => controllers}/terminatinggateway_controller.go (98%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e634e323e..3745c80bac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -167,7 +167,7 @@ rebase the branch on main, fixing any conflicts along the way before the code ca ```bash operator-sdk create api --group consul --version v1alpha1 --kind IngressGateway --controller --namespaced=true --make=false --resource=true ``` -1. Re-order the file so it looks like: +1. Re-order the generated ingressgateway_types.go file, so it looks like: ```go func init() { SchemeBuilder.Register(&IngressGateway{}, &IngressGatewayList{}) @@ -320,8 +320,6 @@ rebase the branch on main, fixing any conflicts along the way before the code ca ### Controller 1. Delete the file `control-plane/controllers/suite_test.go`. We don't write suite tests, just unit tests. -1. Move `control-plane/controllers/ingressgateway_controller.go` to `control-plane/controller` directory. -1. Delete the `control-plane/controllers` directory. 1. Rename `Reconciler` to `Controller`, e.g. `IngressGatewayReconciler` => `IngressGatewayController` 1. Use the existing controller files as a guide and make this file match. 1. Add your controller as a case in the tests in `configentry_controller_test.go`: @@ -395,13 +393,13 @@ rebase the branch on main, fixing any conflicts along the way before the code ca ``` ### Updating Helm chart -1. Update `charts/consul/templates/controller-mutatingwebhookconfiguration` with the webhook for this resource +1. Update `charts/consul/templates/connect-inject-mutatingwebhookconfiguration` with the webhook for this resource using the updated `control-plane/config/webhook/manifests.v1beta1.yaml` and replacing `clientConfig.service.name/namespace` with the templated strings shown below to match the other webhooks.: ```yaml - clientConfig: service: - name: {{ template "consul.fullname" . }}-controller-webhook + name: {{ template "consul.fullname" . }}-connect-injector namespace: {{ .Release.Namespace }} path: /mutate-v1alpha1-ingressgateway failurePolicy: Fail @@ -421,7 +419,7 @@ rebase the branch on main, fixing any conflicts along the way before the code ca - ingressgateways sideEffects: None ``` -1. Update `charts/consul/templates/controller-clusterrole.yaml` to allow the controller to +1. Update `charts/consul/templates/connect-inject-clusterrole.yaml` to allow the controller to manage your resource type. ### Testing A New CRD diff --git a/charts/consul/templates/connect-inject-clusterrole.yaml b/charts/consul/templates/connect-inject-clusterrole.yaml index f2e12f0ad9..e383e5ce28 100644 --- a/charts/consul/templates/connect-inject-clusterrole.yaml +++ b/charts/consul/templates/connect-inject-clusterrole.yaml @@ -24,6 +24,7 @@ rules: - serviceintentions - ingressgateways - terminatinggateways + - samenessgroups {{- if .Values.global.peering.enabled }} - peeringacceptors - peeringdialers @@ -49,6 +50,7 @@ rules: - serviceintentions/status - ingressgateways/status - terminatinggateways/status + - samenessgroups/status {{- if .Values.global.peering.enabled }} - peeringacceptors/status - peeringdialers/status diff --git a/charts/consul/templates/connect-inject-mutatingwebhookconfiguration.yaml b/charts/consul/templates/connect-inject-mutatingwebhookconfiguration.yaml index afcfd3800f..b68efdb9f8 100644 --- a/charts/consul/templates/connect-inject-mutatingwebhookconfiguration.yaml +++ b/charts/consul/templates/connect-inject-mutatingwebhookconfiguration.yaml @@ -291,5 +291,26 @@ webhooks: admissionReviewVersions: - "v1beta1" - "v1" +- admissionReviewVersions: + - v1beta1 + - v1 + clientConfig: + service: + name: {{ template "consul.fullname" . }}-connect-injector + namespace: {{ .Release.Namespace }} + path: /mutate-v1alpha1-samenessgroup + failurePolicy: Fail + name: mutate-samenessgroup.consul.hashicorp.com + rules: + - apiGroups: + - consul.hashicorp.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - samenessgroups + sideEffects: None {{- end }} {{- end }} diff --git a/charts/consul/templates/crd-proxydefaults.yaml b/charts/consul/templates/crd-proxydefaults.yaml index aaaa3228a1..6bb1234066 100644 --- a/charts/consul/templates/crd-proxydefaults.yaml +++ b/charts/consul/templates/crd-proxydefaults.yaml @@ -143,16 +143,18 @@ spec: type: object type: array type: object - failoverPolicy: - description: FailoverPolicy specifies the exact mechanism used for failover. + failoverPolicy: + description: FailoverPolicy specifies the exact mechanism used for + failover. properties: mode: - description: Mode specifies the type of failover that will be performed. - Valid values are "sequential", "" (equivalent to "sequential") and "order-by-locality". + description: Mode specifies the type of failover that will be + performed. Valid values are "sequential", "" (equivalent to + "sequential") and "order-by-locality". type: string - regions: + regions: description: The ordered list of the regions of the failover targets. - Valid values can be "us-west-1", "us-west-2", and so on. + Valid values can be "us-west-1", "us-west-2", and so on. items: type: string type: array diff --git a/charts/consul/templates/crd-samenessgroups.yaml b/charts/consul/templates/crd-samenessgroups.yaml new file mode 100644 index 0000000000..e541539481 --- /dev/null +++ b/charts/consul/templates/crd-samenessgroups.yaml @@ -0,0 +1,126 @@ +{{- if .Values.connectInject.enabled }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: samenessgroups.consul.hashicorp.com + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: crd +spec: + group: consul.hashicorp.com + names: + kind: SamenessGroup + listKind: SamenessGroupList + plural: samenessgroups + shortNames: + - sameness-group + singular: samenessgroup + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The sync status of the resource with Consul + jsonPath: .status.conditions[?(@.type=="Synced")].status + name: Synced + type: string + - description: The last successful synced time of the resource with Consul + jsonPath: .status.lastSyncedTime + name: Last Synced + type: date + - description: The age of the resource + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: SamenessGroup is the Schema for the samenessgroups API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SamenessGroupSpec defines the desired state of SamenessGroup. + properties: + defaultForFailover: + description: 'DefaultForFailover indicates that upstream requests to members of the given sameness group will implicitly failover between members of this sameness group.' + type: boolean + includeLocal: + description: 'IncludeLocal is used to include the local partition as the first member of the sameness group.' + type: boolean + members: + description: 'Members are the partitions and peers that are part of the sameness group.' + items: + properties: + partition: + type: string + peer: + type: string + type: object + type: array + type: object + status: + properties: + conditions: + description: Conditions indicate the latest available observations + of a resource's current state. + items: + description: 'Conditions define a readiness condition for a Consul + resource. See: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties' + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition. + type: string + required: + - status + - type + type: object + type: array + lastSyncedTime: + description: LastSyncedTime is the last time the resource successfully + synced with Consul. + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +{{- end }} diff --git a/charts/consul/templates/crd-serviceresolvers.yaml b/charts/consul/templates/crd-serviceresolvers.yaml index 842506d642..1942b79b8f 100644 --- a/charts/consul/templates/crd-serviceresolvers.yaml +++ b/charts/consul/templates/crd-serviceresolvers.yaml @@ -80,13 +80,14 @@ spec: the current namespace is used. type: string policy: - description: FailoverPolicy specifies the exact mechanism used for failover. + description: Policy specifies the exact mechanism used for failover. properties: mode: - description: Mode specifies the type of failover that will be performed. - Valid values are "sequential", "" (equivalent to "sequential") and "order-by-locality". + description: Mode specifies the type of failover that will + be performed. Valid values are "sequential", "" (equivalent + to "sequential") and "order-by-locality". type: string - regions: + regions: description: The ordered list of the regions of the failover targets. Valid values can be "us-west-1", "us-west-2", and so on. items: diff --git a/control-plane/PROJECT b/control-plane/PROJECT index c11e857849..eb653ad34a 100644 --- a/control-plane/PROJECT +++ b/control-plane/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: hashicorp.com layout: - go.kubebuilder.io/v2 @@ -77,4 +81,13 @@ resources: kind: PeeringDialer path: github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1beta1 + namespaced: true + controller: true + domain: hashicorp.com + group: consul + kind: SamenessGroup + path: github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/control-plane/api/common/common.go b/control-plane/api/common/common.go index 2c579ba715..4faeccada1 100644 --- a/control-plane/api/common/common.go +++ b/control-plane/api/common/common.go @@ -14,6 +14,7 @@ const ( ExportedServices string = "exportedservices" IngressGateway string = "ingressgateway" TerminatingGateway string = "terminatinggateway" + SamenessGroup string = "samenessgroup" Global string = "global" Mesh string = "mesh" diff --git a/control-plane/api/v1alpha1/samenessgroup_types.go b/control-plane/api/v1alpha1/samenessgroup_types.go new file mode 100644 index 0000000000..7f5f194150 --- /dev/null +++ b/control-plane/api/v1alpha1/samenessgroup_types.go @@ -0,0 +1,262 @@ +package v1alpha1 + +import ( + "encoding/json" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/consul-k8s/control-plane/api/common" + "github.com/hashicorp/consul/api" + 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" +) + +const ( + SamenessGroupKubeKind string = "samenessgroup" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +func init() { + SchemeBuilder.Register(&SamenessGroup{}, &SamenessGroupList{}) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// SamenessGroup is the Schema for the samenessgroups API +// +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul" +// +kubebuilder:printcolumn:name="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource" +// +kubebuilder:resource:shortName="sameness-group" +type SamenessGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec SamenessGroupSpec `json:"spec,omitempty"` + Status `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// SamenessGroupList contains a list of SamenessGroup. +type SamenessGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SamenessGroup `json:"items"` +} + +// SamenessGroupSpec defines the desired state of SamenessGroup. +type SamenessGroupSpec struct { + // DefaultForFailover indicates that upstream requests to members of the given sameness group will implicitly failover between members of this sameness group. + // When DefaultForFailover is true, the local partition must be a member of the sameness group or IncludeLocal must be set to true. + DefaultForFailover bool `json:"defaultForFailover,omitempty"` + // IncludeLocal is used to include the local partition as the first member of the sameness group. + // The local partition can only be a member of a single sameness group. + IncludeLocal bool `json:"includeLocal,omitempty"` + // Members are the partitions and peers that are part of the sameness group. + // If a member of a sameness group does not exist, it will be ignored. + Members []SamenessGroupMember `json:"members,omitempty"` +} + +type SamenessGroupMember struct { + // The partitions and peers that are part of the sameness group. + // A sameness group member cannot define both peer and partition at the same time. + Partition string `json:"partition,omitempty"` + Peer string `json:"peer,omitempty"` +} + +func (in *SamenessGroup) GetObjectMeta() metav1.ObjectMeta { + return in.ObjectMeta +} + +func (in *SamenessGroup) AddFinalizer(name string) { + in.ObjectMeta.Finalizers = append(in.Finalizers(), name) +} + +func (in *SamenessGroup) RemoveFinalizer(name string) { + var newFinalizers []string + for _, oldF := range in.Finalizers() { + if oldF != name { + newFinalizers = append(newFinalizers, oldF) + } + } + in.ObjectMeta.Finalizers = newFinalizers +} + +func (in *SamenessGroup) Finalizers() []string { + return in.ObjectMeta.Finalizers +} + +func (in *SamenessGroup) ConsulKind() string { + return capi.SamenessGroup +} + +func (in *SamenessGroup) ConsulGlobalResource() bool { + return false +} + +func (in *SamenessGroup) ConsulMirroringNS() string { + return common.DefaultConsulNamespace +} + +func (in *SamenessGroup) KubeKind() string { + return SamenessGroupKubeKind +} + +func (in *SamenessGroup) ConsulName() string { + return in.ObjectMeta.Name +} + +func (in *SamenessGroup) KubernetesName() string { + return in.ObjectMeta.Name +} + +func (in *SamenessGroup) SetSyncedCondition(status corev1.ConditionStatus, reason, message string) { + in.Status.Conditions = Conditions{ + { + Type: ConditionSynced, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + }, + } +} + +func (in *SamenessGroup) SetLastSyncedTime(time *metav1.Time) { + in.Status.LastSyncedTime = time +} + +func (in *SamenessGroup) SyncedCondition() (status corev1.ConditionStatus, reason, message string) { + cond := in.Status.GetCondition(ConditionSynced) + if cond == nil { + return corev1.ConditionUnknown, "", "" + } + return cond.Status, cond.Reason, cond.Message +} + +func (in *SamenessGroup) SyncedConditionStatus() corev1.ConditionStatus { + cond := in.Status.GetCondition(ConditionSynced) + if cond == nil { + return corev1.ConditionUnknown + } + return cond.Status +} + +func (in *SamenessGroup) ToConsul(datacenter string) api.ConfigEntry { + return &capi.SamenessGroupConfigEntry{ + Kind: in.ConsulKind(), + Name: in.ConsulName(), + DefaultForFailover: in.Spec.DefaultForFailover, + IncludeLocal: in.Spec.IncludeLocal, + Members: SamenessGroupMembers(in.Spec.Members).toConsul(), + Meta: meta(datacenter), + } +} + +func (in *SamenessGroup) MatchesConsul(candidate api.ConfigEntry) bool { + configEntry, ok := candidate.(*capi.SamenessGroupConfigEntry) + if !ok { + return false + } + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.SamenessGroupConfigEntry{}, "Partition", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty(), + cmp.Comparer(transparentProxyConfigComparer)) +} + +func (in *SamenessGroup) Validate(consulMeta common.ConsulMeta) error { + var allErrs field.ErrorList + path := field.NewPath("spec") + + if in == nil { + return nil + } + if in.Name == "" { + allErrs = append(allErrs, field.Invalid(path.Child("name"), in.Name, "sameness groups must have a name defined")) + } + + partition := consulMeta.Partition + includesLocal := in.Spec.IncludeLocal + + if in.ObjectMeta.Namespace != "default" && in.ObjectMeta.Namespace != "" { + allErrs = append(allErrs, field.Invalid(path.Child("name"), consulMeta.DestinationNamespace, "sameness groups must reside in the default namespace")) + } + + if len(in.Spec.Members) == 0 { + asJSON, _ := json.Marshal(in.Spec.Members) + allErrs = append(allErrs, field.Invalid(path.Child("members"), string(asJSON), "sameness groups must have at least one member")) + } + + seenMembers := make(map[SamenessGroupMember]struct{}) + for i, m := range in.Spec.Members { + if partition == m.Partition { + includesLocal = true + } + if err := m.validate(path.Child("members").Index(i)); err != nil { + allErrs = append(allErrs, err) + } + if _, ok := seenMembers[m]; ok { + asJSON, _ := json.Marshal(m) + allErrs = append(allErrs, field.Invalid(path.Child("members").Index(i), string(asJSON), "sameness group members must be unique")) + } + seenMembers[m] = struct{}{} + + } + + if !includesLocal { + allErrs = append(allErrs, field.Invalid(path.Child("members"), in.Spec.IncludeLocal, "the local partition must be a member of sameness groups")) + } + + if len(allErrs) > 0 { + return apierrors.NewInvalid( + schema.GroupKind{Group: ConsulHashicorpGroup, Kind: SamenessGroupKubeKind}, + in.KubernetesName(), allErrs) + } + + return nil +} + +// DefaultNamespaceFields has no behaviour here as sameness-groups have no namespace specific fields. +func (in *SamenessGroup) DefaultNamespaceFields(_ common.ConsulMeta) { +} + +type SamenessGroupMembers []SamenessGroupMember + +func (in SamenessGroupMembers) toConsul() []capi.SamenessGroupMember { + if in == nil { + return nil + } + + outMembers := make([]capi.SamenessGroupMember, 0, len(in)) + for _, e := range in { + consulMember := capi.SamenessGroupMember{ + Peer: e.Peer, + Partition: e.Partition, + } + outMembers = append(outMembers, consulMember) + } + return outMembers +} + +func (in *SamenessGroupMember) validate(path *field.Path) *field.Error { + asJSON, _ := json.Marshal(in) + + if in == nil { + return field.Invalid(path, string(asJSON), "sameness group member is nil") + } + if in.isEmpty() { + return field.Invalid(path, string(asJSON), "sameness group members must specify either partition or peer") + } + // We do not allow referencing peer connections in other partitions. + if in.Peer != "" && in.Partition != "" { + return field.Invalid(path, string(asJSON), "sameness group members cannot specify both partition and peer in the same entry") + } + return nil +} + +func (in *SamenessGroupMember) isEmpty() bool { + return in.Peer == "" && in.Partition == "" +} diff --git a/control-plane/api/v1alpha1/samenessgroup_types_test.go b/control-plane/api/v1alpha1/samenessgroup_types_test.go new file mode 100644 index 0000000000..0f461701cc --- /dev/null +++ b/control-plane/api/v1alpha1/samenessgroup_types_test.go @@ -0,0 +1,390 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package v1alpha1 + +import ( + "github.com/hashicorp/consul-k8s/control-plane/api/common" + capi "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" + "time" +) + +func TestSamenessGroups_ToConsul(t *testing.T) { + cases := map[string]struct { + input *SamenessGroup + expected *capi.SamenessGroupConfigEntry + }{ + "empty fields": { + &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: SamenessGroupSpec{}, + }, + &capi.SamenessGroupConfigEntry{ + Name: "foo", + Kind: capi.SamenessGroup, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + }, + "every field set": { + &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []SamenessGroupMember{ + { + Peer: "peer2", + }, + { + Partition: "p2", + }, + }, + }, + }, + &capi.SamenessGroupConfigEntry{ + Name: "foo", + Kind: capi.SamenessGroup, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + DefaultForFailover: true, + IncludeLocal: true, + Members: []capi.SamenessGroupMember{ + { + Peer: "peer2", + }, + { + Partition: "p2", + }, + }, + }, + }, + } + for name, testCase := range cases { + t.Run(name, func(t *testing.T) { + output := testCase.input.ToConsul("datacenter") + require.Equal(t, testCase.expected, output) + }) + } +} + +func TestSamenessGroups_MatchesConsul(t *testing.T) { + cases := map[string]struct { + internal *SamenessGroup + consul capi.ConfigEntry + matches bool + }{ + "empty fields matches": { + &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-test-sameness-group", + }, + Spec: SamenessGroupSpec{}, + }, + &capi.SamenessGroupConfigEntry{ + Kind: capi.SamenessGroup, + Name: "my-test-sameness-group", + CreateIndex: 1, + ModifyIndex: 2, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + true, + }, + "all fields populated matches": { + &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-test-sameness-group", + }, + Spec: SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []SamenessGroupMember{ + { + Peer: "peer2", + }, + { + Partition: "p2", + }, + }, + }, + }, + &capi.SamenessGroupConfigEntry{ + Kind: capi.SamenessGroup, + Name: "my-test-sameness-group", + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + DefaultForFailover: true, + IncludeLocal: true, + Members: []capi.SamenessGroupMember{ + { + Peer: "peer2", + }, + { + Partition: "p2", + }, + }, + }, + true, + }, + } + + for name, testCase := range cases { + t.Run(name, func(t *testing.T) { + require.Equal(t, testCase.matches, testCase.internal.MatchesConsul(testCase.consul)) + }) + } +} + +func TestSamenessGroups_Validate(t *testing.T) { + cases := map[string]struct { + input *SamenessGroup + partitionsEnabled bool + expectedErrMsg string + }{ + "valid": { + input: &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-sameness-group", + }, + Spec: SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []SamenessGroupMember{ + { + Peer: "peer2", + Partition: "", + }, + { + Peer: "", + Partition: "p2", + }, + }, + }, + }, + partitionsEnabled: true, + expectedErrMsg: "", + }, + "invalid - with peer and partition both": { + input: &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-sameness-group", + }, + Spec: SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []SamenessGroupMember{ + { + Peer: "peer2", + Partition: "p2", + }, + }, + }, + }, + partitionsEnabled: true, + expectedErrMsg: "sameness group members cannot specify both partition and peer in the same entry", + }, + "invalid - no name": { + input: &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{}, + Spec: SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []SamenessGroupMember{ + { + Peer: "peer2", + }, + { + Partition: "p2", + }, + }, + }, + }, + partitionsEnabled: true, + expectedErrMsg: "sameness groups must have a name defined", + }, + "invalid - empty members": { + input: &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-sameness-group", + }, + Spec: SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []SamenessGroupMember{}, + }, + }, + partitionsEnabled: true, + expectedErrMsg: "sameness groups must have at least one member", + }, + "invalid - not unique members": { + input: &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-sameness-group", + }, + Spec: SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []SamenessGroupMember{ + { + Peer: "peer2", + }, + { + Peer: "peer2", + }, + }, + }, + }, + partitionsEnabled: true, + expectedErrMsg: "sameness group members must be unique", + }, + "invalid - not in default namespace": { + input: &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-sameness-group", + Namespace: "non-default", + }, + Spec: SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []SamenessGroupMember{ + { + Peer: "peer2", + }, + }, + }, + }, + partitionsEnabled: true, + expectedErrMsg: "sameness groups must reside in the default namespace", + }, + } + + for name, testCase := range cases { + t.Run(name, func(t *testing.T) { + err := testCase.input.Validate(common.ConsulMeta{}) + if testCase.expectedErrMsg != "" { + require.ErrorContains(t, err, testCase.expectedErrMsg) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSamenessGroups_GetObjectMeta(t *testing.T) { + meta := metav1.ObjectMeta{ + Name: "name", + } + samenessGroups := &SamenessGroup{ + ObjectMeta: meta, + } + require.Equal(t, meta, samenessGroups.GetObjectMeta()) +} + +func TestSamenessGroups_AddFinalizer(t *testing.T) { + samenessGroups := &SamenessGroup{} + samenessGroups.AddFinalizer("finalizer") + require.Equal(t, []string{"finalizer"}, samenessGroups.ObjectMeta.Finalizers) +} + +func TestSamenessGroups_RemoveFinalizer(t *testing.T) { + samenessGroups := &SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{"f1", "f2"}, + }, + } + samenessGroups.RemoveFinalizer("f1") + require.Equal(t, []string{"f2"}, samenessGroups.ObjectMeta.Finalizers) +} + +func TestSamenessGroups_ConsulKind(t *testing.T) { + require.Equal(t, capi.SamenessGroup, (&SamenessGroup{}).ConsulKind()) +} + +func TestSamenessGroups_ConsulGlobalResource(t *testing.T) { + require.False(t, (&SamenessGroup{}).ConsulGlobalResource()) +} + +func TestSamenessGroups_ConsulMirroringNS(t *testing.T) { + +} + +func TestSamenessGroups_KubeKind(t *testing.T) { + require.Equal(t, "samenessgroup", (&SamenessGroup{}).KubeKind()) +} + +func TestSamenessGroups_ConsulName(t *testing.T) { + require.Equal(t, "foo", (&SamenessGroup{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}).ConsulName()) +} + +func TestSamenessGroups_KubernetesName(t *testing.T) { + require.Equal(t, "foo", (&SamenessGroup{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}).KubernetesName()) +} + +func TestSamenessGroups_SetSyncedCondition(t *testing.T) { + samenessGroups := &SamenessGroup{} + samenessGroups.SetSyncedCondition(corev1.ConditionTrue, "reason", "message") + + require.Equal(t, corev1.ConditionTrue, samenessGroups.Status.Conditions[0].Status) + require.Equal(t, "reason", samenessGroups.Status.Conditions[0].Reason) + require.Equal(t, "message", samenessGroups.Status.Conditions[0].Message) + now := metav1.Now() + require.True(t, samenessGroups.Status.Conditions[0].LastTransitionTime.Before(&now)) +} + +func TestSamenessGroups_SetLastSyncedTime(t *testing.T) { + samenessGroups := &SamenessGroup{} + syncedTime := metav1.NewTime(time.Now()) + samenessGroups.SetLastSyncedTime(&syncedTime) + + require.Equal(t, &syncedTime, samenessGroups.Status.LastSyncedTime) +} + +func TestSamenessGroups_GetSyncedConditionStatus(t *testing.T) { + cases := []corev1.ConditionStatus{ + corev1.ConditionUnknown, + corev1.ConditionFalse, + corev1.ConditionTrue, + } + for _, status := range cases { + t.Run(string(status), func(t *testing.T) { + samenessGroups := &SamenessGroup{ + Status: Status{ + Conditions: []Condition{{ + Type: ConditionSynced, + Status: status, + }}, + }, + } + + require.Equal(t, status, samenessGroups.SyncedConditionStatus()) + }) + } +} + +func TestSamenessGroups_SyncedConditionStatusWhenStatusNil(t *testing.T) { + require.Equal(t, corev1.ConditionUnknown, (&SamenessGroup{}).SyncedConditionStatus()) +} + +func TestSamenessGroups_SyncedConditionWhenStatusNil(t *testing.T) { + status, reason, message := (&SamenessGroup{}).SyncedCondition() + require.Equal(t, corev1.ConditionUnknown, status) + require.Equal(t, "", reason) + require.Equal(t, "", message) +} diff --git a/control-plane/api/v1alpha1/samenessgroup_webhook.go b/control-plane/api/v1alpha1/samenessgroup_webhook.go new file mode 100644 index 0000000000..6c1da5cba2 --- /dev/null +++ b/control-plane/api/v1alpha1/samenessgroup_webhook.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package v1alpha1 + +import ( + "context" + "net/http" + + "github.com/go-logr/logr" + "github.com/hashicorp/consul-k8s/control-plane/api/common" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// +kubebuilder:object:generate=false + +type SamenessGroupWebhook struct { + Logger logr.Logger + + // ConsulMeta contains metadata specific to the Consul installation. + ConsulMeta common.ConsulMeta + + decoder *admission.Decoder + client.Client +} + +// NOTE: The path value in the below line is the path to the webhook. +// If it is updated, run code-gen, update subcommand/controller/command.go +// and the consul-helm value for the path to the webhook. +// +// NOTE: The below line cannot be combined with any other comment. If it is it will break the code generation. +// +// +kubebuilder:webhook:verbs=create;update,path=/mutate-v1alpha1-samenessgroups,mutating=true,failurePolicy=fail,groups=consul.hashicorp.com,resources=samenessgroups,versions=v1alpha1,name=mutate-samenessgroup.consul.hashicorp.com,sideEffects=None,admissionReviewVersions=v1beta1;v1 + +func (v *SamenessGroupWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { + var resource SamenessGroup + err := v.decoder.Decode(req, &resource) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + return common.ValidateConfigEntry(ctx, req, v.Logger, v, &resource, v.ConsulMeta) +} + +func (v *SamenessGroupWebhook) List(ctx context.Context) ([]common.ConfigEntryResource, error) { + var resourceList SamenessGroupList + if err := v.Client.List(ctx, &resourceList); err != nil { + return nil, err + } + var entries []common.ConfigEntryResource + for _, item := range resourceList.Items { + entries = append(entries, common.ConfigEntryResource(&item)) + } + return entries, nil +} + +func (v *SamenessGroupWebhook) InjectDecoder(d *admission.Decoder) error { + v.decoder = d + return nil +} diff --git a/control-plane/api/v1alpha1/servicedefaults_types_test.go b/control-plane/api/v1alpha1/servicedefaults_types_test.go index 9f9e7ebbda..69b749decd 100644 --- a/control-plane/api/v1alpha1/servicedefaults_types_test.go +++ b/control-plane/api/v1alpha1/servicedefaults_types_test.go @@ -1296,7 +1296,7 @@ func TestServiceDefaults_ConsulName(t *testing.T) { } func TestServiceDefaults_KubernetesName(t *testing.T) { - require.Equal(t, "foo", (&ServiceDefaults{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}).ConsulName()) + require.Equal(t, "foo", (&ServiceDefaults{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}).KubernetesName()) } func TestServiceDefaults_ConsulNamespace(t *testing.T) { diff --git a/control-plane/api/v1alpha1/shared_types.go b/control-plane/api/v1alpha1/shared_types.go index fdac9a3fea..98eb3f1b20 100644 --- a/control-plane/api/v1alpha1/shared_types.go +++ b/control-plane/api/v1alpha1/shared_types.go @@ -248,9 +248,11 @@ func (in EnvoyExtension) validate(path *field.Path) *field.Error { // FailoverPolicy specifies the exact mechanism used for failover. type FailoverPolicy struct { // Mode specifies the type of failover that will be performed. Valid values are - // "default", "" (equivalent to "default") and "order-by-locality". - Mode string `json:",omitempty"` - Regions []string `json:",omitempty"` + // "sequential", "" (equivalent to "sequential") and "order-by-locality". + Mode string `json:"mode,omitempty"` + // Regions is the ordered list of the regions of the failover targets. + // Valid values can be "us-west-1", "us-west-2", and so on. + Regions []string `json:"regions,omitempty"` } func (in *FailoverPolicy) toConsul() *capi.ServiceResolverFailoverPolicy { diff --git a/control-plane/api/v1alpha1/zz_generated.deepcopy.go b/control-plane/api/v1alpha1/zz_generated.deepcopy.go index d12db29d14..f485b5c5f1 100644 --- a/control-plane/api/v1alpha1/zz_generated.deepcopy.go +++ b/control-plane/api/v1alpha1/zz_generated.deepcopy.go @@ -255,6 +255,26 @@ func (in *ExposePath) DeepCopy() *ExposePath { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FailoverPolicy) DeepCopyInto(out *FailoverPolicy) { + *out = *in + if in.Regions != nil { + in, out := &in.Regions, &out.Regions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FailoverPolicy. +func (in *FailoverPolicy) DeepCopy() *FailoverPolicy { + if in == nil { + return nil + } + out := new(FailoverPolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GatewayServiceTLSConfig) DeepCopyInto(out *GatewayServiceTLSConfig) { *out = *in @@ -1272,6 +1292,11 @@ func (in *ProxyDefaultsSpec) DeepCopyInto(out *ProxyDefaultsSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.FailoverPolicy != nil { + in, out := &in.FailoverPolicy, &out.FailoverPolicy + *out = new(FailoverPolicy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyDefaultsSpec. @@ -1299,6 +1324,100 @@ func (in *RingHashConfig) DeepCopy() *RingHashConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SamenessGroupMember) DeepCopyInto(out *SamenessGroupMember) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SamenessGroupMember. +func (in *SamenessGroupMember) DeepCopy() *SamenessGroupMember { + if in == nil { + return nil + } + out := new(SamenessGroupMember) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SamenessGroup) DeepCopyInto(out *SamenessGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SamenessGroups. +func (in *SamenessGroup) DeepCopy() *SamenessGroup { + if in == nil { + return nil + } + out := new(SamenessGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SamenessGroup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SamenessGroupList) DeepCopyInto(out *SamenessGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SamenessGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SamenessGroupList. +func (in *SamenessGroupList) DeepCopy() *SamenessGroupList { + if in == nil { + return nil + } + out := new(SamenessGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SamenessGroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SamenessGroupSpec) DeepCopyInto(out *SamenessGroupSpec) { + *out = *in + if in.Members != nil { + in, out := &in.Members, &out.Members + *out = make([]SamenessGroupMember, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SamenessGroupSpec. +func (in *SamenessGroupSpec) DeepCopy() *SamenessGroupSpec { + if in == nil { + return nil + } + out := new(SamenessGroupSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Secret) DeepCopyInto(out *Secret) { *out = *in @@ -1594,6 +1713,11 @@ func (in *ServiceResolverFailover) DeepCopyInto(out *ServiceResolverFailover) { *out = make([]ServiceResolverFailoverTarget, len(*in)) copy(*out, *in) } + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(FailoverPolicy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceResolverFailover. diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_proxydefaults.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_proxydefaults.yaml index 0f90e95e16..c66b5fdd0f 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_proxydefaults.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_proxydefaults.yaml @@ -139,14 +139,16 @@ spec: type: object type: array type: object - failoverPolicy: - description: FailoverPolicy specifies the exact mechanism used for failover. + failoverPolicy: + description: FailoverPolicy specifies the exact mechanism used for + failover. properties: mode: - description: Mode specifies the type of failover that will be performed. - Valid values are "sequential", "" (equivalent to "sequential") and "order-by-locality". + description: Mode specifies the type of failover that will be + performed. Valid values are "sequential", "" (equivalent to + "sequential") and "order-by-locality". type: string - regions: + regions: description: The ordered list of the regions of the failover targets. Valid values can be "us-west-1", "us-west-2", and so on. items: diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_samenessgroups.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_samenessgroups.yaml new file mode 100644 index 0000000000..5efda1ffa2 --- /dev/null +++ b/control-plane/config/crd/bases/consul.hashicorp.com_samenessgroups.yaml @@ -0,0 +1,121 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: samenessgroups.consul.hashicorp.com +spec: + group: consul.hashicorp.com + names: + kind: SamenessGroup + listKind: SamenessGroupList + plural: samenessgroups + shortNames: + - sameness-group + singular: samenessgroup + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The sync status of the resource with Consul + jsonPath: .status.conditions[?(@.type=="Synced")].status + name: Synced + type: string + - description: The last successful synced time of the resource with Consul + jsonPath: .status.lastSyncedTime + name: Last Synced + type: date + - description: The age of the resource + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: SamenessGroup is the Schema for the samenessgroups API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SamenessGroupSpec defines the desired state of SamenessGroup. + properties: + defaultForFailover: + description: 'DefaultForFailover indicates that upstream requests to members of the given sameness group will implicitly failover between members of this sameness group.' + type: boolean + includeLocal: + description: 'IncludeLocal is used to include the local partition as the first member of the sameness group.' + type: boolean + members: + description: 'Members are the partitions and peers that are part of the sameness group.' + items: + properties: + partition: + type: string + peer: + type: string + type: object + type: array + type: object + status: + properties: + conditions: + description: Conditions indicate the latest available observations + of a resource's current state. + items: + description: 'Conditions define a readiness condition for a Consul + resource. See: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties' + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition. + type: string + required: + - status + - type + type: object + type: array + lastSyncedTime: + description: LastSyncedTime is the last time the resource successfully + synced with Consul. + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] 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 a36784cc77..56b5e72014 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_serviceresolvers.yaml @@ -76,13 +76,14 @@ spec: the current namespace is used. type: string policy: - description: FailoverPolicy specifies the exact mechanism used for failover. + description: Policy specifies the exact mechanism used for failover. properties: mode: - description: Mode specifies the type of failover that will be performed. - Valid values are "sequential", "" (equivalent to "sequential") and "order-by-locality". + description: Mode specifies the type of failover that will + be performed. Valid values are "sequential", "" (equivalent + to "sequential") and "order-by-locality". type: string - regions: + regions: description: The ordered list of the regions of the failover targets. Valid values can be "us-west-1", "us-west-2", and so on. items: diff --git a/control-plane/config/rbac/role.yaml b/control-plane/config/rbac/role.yaml index 245f09568f..562eb5f9f9 100644 --- a/control-plane/config/rbac/role.yaml +++ b/control-plane/config/rbac/role.yaml @@ -1,6 +1,3 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -146,6 +143,26 @@ rules: - get - patch - update +- apiGroups: + - consul.hashicorp.com + resources: + - samenessgroups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - consul.hashicorp.com + resources: + - samenessgroups/status + verbs: + - get + - patch + - update - apiGroups: - consul.hashicorp.com resources: diff --git a/control-plane/config/webhook/manifests.yaml b/control-plane/config/webhook/manifests.yaml index d064b50acb..f96d669544 100644 --- a/control-plane/config/webhook/manifests.yaml +++ b/control-plane/config/webhook/manifests.yaml @@ -1,6 +1,3 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - --- apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration @@ -134,6 +131,27 @@ webhooks: resources: - proxydefaults sideEffects: None +- admissionReviewVersions: + - v1beta1 + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-v1alpha1-samenessgroup + failurePolicy: Fail + name: mutate-samenessgroup.consul.hashicorp.com + rules: + - apiGroups: + - consul.hashicorp.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - samenessgroups + sideEffects: None - admissionReviewVersions: - v1beta1 - v1 diff --git a/control-plane/controller/configentry_controller.go b/control-plane/controllers/configentry_controller.go similarity index 99% rename from control-plane/controller/configentry_controller.go rename to control-plane/controllers/configentry_controller.go index 593fb1514f..c2c6da9071 100644 --- a/control-plane/controller/configentry_controller.go +++ b/control-plane/controllers/configentry_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/configentry_controller_ent_test.go b/control-plane/controllers/configentry_controller_ent_test.go similarity index 64% rename from control-plane/controller/configentry_controller_ent_test.go rename to control-plane/controllers/configentry_controller_ent_test.go index cfe9985e56..ef2aa6b7a4 100644 --- a/control-plane/controller/configentry_controller_ent_test.go +++ b/control-plane/controllers/configentry_controller_ent_test.go @@ -3,7 +3,7 @@ //go:build enterprise -package controller_test +package controllers import ( "context" @@ -15,7 +15,7 @@ import ( logrtest "github.com/go-logr/logr/testing" "github.com/hashicorp/consul-k8s/control-plane/api/common" "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" - "github.com/hashicorp/consul-k8s/control-plane/controller" + "github.com/hashicorp/consul-k8s/control-plane/consul" "github.com/hashicorp/consul-k8s/control-plane/helper/test" capi "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" @@ -29,11 +29,323 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -// NOTE: We're not testing each controller type here because that's done in +// NOTE: We're not testing each controller type here because that's mostly done in // the OSS tests and it would result in too many permutations. Instead -// we're only testing with the ServiceDefaults and ProxyDefaults controller which will exercise -// all the namespaces code for config entries that are namespaced and those that +// we're only testing with the ServiceDefaults and ProxyDefaults controllers which +// will exercise all the namespaces code for config entries that are namespaced and those that // exist in the global namespace. +// We also test Enterprise only features like SamenessGroups. + +func TestConfigEntryController_createsEntConfigEntry(t *testing.T) { + t.Parallel() + kubeNS := "default" + + cases := []struct { + kubeKind string + consulKind string + consulPrereqs []capi.ConfigEntry + configEntryResource common.ConfigEntryResource + reconciler func(client.Client, *consul.Config, consul.ServerConnectionManager, logr.Logger) testReconciler + compare func(t *testing.T, consul capi.ConfigEntry) + }{ + { + kubeKind: "SamenessGroup", + consulKind: capi.SamenessGroup, + configEntryResource: &v1alpha1.SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: kubeNS, + }, + Spec: v1alpha1.SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []v1alpha1.SamenessGroupMember{ + { + Peer: "dc1", + Partition: "", + }, + }, + }, + }, + reconciler: func(client client.Client, cfg *consul.Config, watcher consul.ServerConnectionManager, logger logr.Logger) testReconciler { + return &SamenessGroupController{ + Client: client, + Log: logger, + ConfigEntryController: &ConfigEntryController{ + ConsulClientConfig: cfg, + ConsulServerConnMgr: watcher, + DatacenterName: datacenterName, + }, + } + }, + compare: func(t *testing.T, consulEntry capi.ConfigEntry) { + resource, ok := consulEntry.(*capi.SamenessGroupConfigEntry) + require.True(t, ok, "cast error") + require.Equal(t, true, resource.DefaultForFailover) + require.Equal(t, true, resource.IncludeLocal) + require.Equal(t, "dc1", resource.Members[0].Peer) + require.Equal(t, "", resource.Members[0].Partition) + }, + }, + } + + for _, c := range cases { + t.Run(c.kubeKind, func(t *testing.T) { + req := require.New(t) + ctx := context.Background() + + s := runtime.NewScheme() + s.AddKnownTypes(v1alpha1.GroupVersion, c.configEntryResource) + fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(c.configEntryResource).Build() + + testClient := test.TestServerWithMockConnMgrWatcher(t, nil) + testClient.TestServer.WaitForServiceIntentions(t) + consulClient := testClient.APIClient + + for _, configEntry := range c.consulPrereqs { + written, _, err := consulClient.ConfigEntries().Set(configEntry, nil) + req.NoError(err) + req.True(written) + } + + r := c.reconciler(fakeClient, testClient.Cfg, testClient.Watcher, logrtest.TestLogger{T: t}) + namespacedName := types.NamespacedName{ + Namespace: kubeNS, + Name: c.configEntryResource.KubernetesName(), + } + resp, err := r.Reconcile(ctx, ctrl.Request{ + NamespacedName: namespacedName, + }) + req.NoError(err) + req.False(resp.Requeue) + + cfg, _, err := consulClient.ConfigEntries().Get(c.consulKind, c.configEntryResource.ConsulName(), nil) + req.NoError(err) + req.Equal(c.configEntryResource.ConsulName(), cfg.GetName()) + c.compare(t, cfg) + + // Check that the status is "synced". + err = fakeClient.Get(ctx, namespacedName, c.configEntryResource) + req.NoError(err) + req.Equal(corev1.ConditionTrue, c.configEntryResource.SyncedConditionStatus()) + + // Check that the finalizer is added. + req.Contains(c.configEntryResource.Finalizers(), FinalizerName) + }) + } +} + +func TestConfigEntryController_updatesEntConfigEntry(t *testing.T) { + t.Parallel() + kubeNS := "default" + + cases := []struct { + kubeKind string + consulKind string + consulPrereqs []capi.ConfigEntry + configEntryResource common.ConfigEntryResource + reconciler func(client.Client, *consul.Config, consul.ServerConnectionManager, logr.Logger) testReconciler + updateF func(common.ConfigEntryResource) + compare func(t *testing.T, consul capi.ConfigEntry) + }{ + { + kubeKind: "SamenessGroup", + consulKind: capi.SamenessGroup, + configEntryResource: &v1alpha1.SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: kubeNS, + }, + Spec: v1alpha1.SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []v1alpha1.SamenessGroupMember{ + { + Peer: "dc1", + Partition: "", + }, + }, + }, + }, + reconciler: func(client client.Client, cfg *consul.Config, watcher consul.ServerConnectionManager, logger logr.Logger) testReconciler { + return &SamenessGroupController{ + Client: client, + Log: logger, + ConfigEntryController: &ConfigEntryController{ + ConsulClientConfig: cfg, + ConsulServerConnMgr: watcher, + DatacenterName: datacenterName, + }, + } + }, + updateF: func(resource common.ConfigEntryResource) { + sg := resource.(*v1alpha1.SamenessGroup) + sg.Spec.IncludeLocal = false + }, + compare: func(t *testing.T, consulEntry capi.ConfigEntry) { + resource, ok := consulEntry.(*capi.SamenessGroupConfigEntry) + require.True(t, ok, "cast error") + require.Equal(t, true, resource.DefaultForFailover) + require.Equal(t, false, resource.IncludeLocal) + require.Equal(t, "dc1", resource.Members[0].Peer) + require.Equal(t, "", resource.Members[0].Partition) + }, + }, + } + + for _, c := range cases { + t.Run(c.kubeKind, func(t *testing.T) { + req := require.New(t) + ctx := context.Background() + + s := runtime.NewScheme() + s.AddKnownTypes(v1alpha1.GroupVersion, c.configEntryResource) + fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(c.configEntryResource).Build() + + testClient := test.TestServerWithMockConnMgrWatcher(t, nil) + testClient.TestServer.WaitForServiceIntentions(t) + consulClient := testClient.APIClient + + // Create any prereqs. + for _, configEntry := range c.consulPrereqs { + written, _, err := consulClient.ConfigEntries().Set(configEntry, nil) + req.NoError(err) + req.True(written) + } + + // We haven't run reconcile yet so we must create the config entry + // in Consul ourselves. + { + written, _, err := consulClient.ConfigEntries().Set(c.configEntryResource.ToConsul(datacenterName), nil) + req.NoError(err) + req.True(written) + } + + // Now run reconcile which should update the entry in Consul. + { + namespacedName := types.NamespacedName{ + Namespace: kubeNS, + Name: c.configEntryResource.KubernetesName(), + } + // First get it so we have the latest revision number. + err := fakeClient.Get(ctx, namespacedName, c.configEntryResource) + req.NoError(err) + + // Update the entry in Kube and run reconcile. + c.updateF(c.configEntryResource) + err = fakeClient.Update(ctx, c.configEntryResource) + req.NoError(err) + r := c.reconciler(fakeClient, testClient.Cfg, testClient.Watcher, logrtest.TestLogger{T: t}) + resp, err := r.Reconcile(ctx, ctrl.Request{ + NamespacedName: namespacedName, + }) + req.NoError(err) + req.False(resp.Requeue) + + // Now check that the object in Consul is as expected. + cfg, _, err := consulClient.ConfigEntries().Get(c.consulKind, c.configEntryResource.ConsulName(), nil) + req.NoError(err) + req.Equal(c.configEntryResource.ConsulName(), cfg.GetName()) + c.compare(t, cfg) + } + }) + } +} + +func TestConfigEntryController_deletesEntConfigEntry(t *testing.T) { + t.Parallel() + kubeNS := "default" + + cases := []struct { + kubeKind string + consulKind string + consulPrereq []capi.ConfigEntry + configEntryResourceWithDeletion common.ConfigEntryResource + reconciler func(client.Client, *consul.Config, consul.ServerConnectionManager, logr.Logger) testReconciler + }{ + { + kubeKind: "SamenessGroup", + consulKind: capi.SamenessGroup, + configEntryResourceWithDeletion: &v1alpha1.SamenessGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: kubeNS, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{FinalizerName}, + }, + Spec: v1alpha1.SamenessGroupSpec{ + DefaultForFailover: true, + IncludeLocal: true, + Members: []v1alpha1.SamenessGroupMember{ + { + Peer: "dc1", + Partition: "", + }, + }, + }, + }, + reconciler: func(client client.Client, cfg *consul.Config, watcher consul.ServerConnectionManager, logger logr.Logger) testReconciler { + return &SamenessGroupController{ + Client: client, + Log: logger, + ConfigEntryController: &ConfigEntryController{ + ConsulClientConfig: cfg, + ConsulServerConnMgr: watcher, + DatacenterName: datacenterName, + }, + } + }, + }, + } + + for _, c := range cases { + t.Run(c.kubeKind, func(t *testing.T) { + req := require.New(t) + + s := runtime.NewScheme() + s.AddKnownTypes(v1alpha1.GroupVersion, c.configEntryResourceWithDeletion) + fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(c.configEntryResourceWithDeletion).Build() + + testClient := test.TestServerWithMockConnMgrWatcher(t, nil) + testClient.TestServer.WaitForServiceIntentions(t) + consulClient := testClient.APIClient + + // Create any prereqs. + for _, configEntry := range c.consulPrereq { + written, _, err := consulClient.ConfigEntries().Set(configEntry, nil) + req.NoError(err) + req.True(written) + } + + // We haven't run reconcile yet so we must create the config entry + // in Consul ourselves. + { + written, _, err := consulClient.ConfigEntries().Set(c.configEntryResourceWithDeletion.ToConsul(datacenterName), nil) + req.NoError(err) + req.True(written) + } + + // Now run reconcile. It's marked for deletion so this should delete it. + { + namespacedName := types.NamespacedName{ + Namespace: kubeNS, + Name: c.configEntryResourceWithDeletion.KubernetesName(), + } + r := c.reconciler(fakeClient, testClient.Cfg, testClient.Watcher, logrtest.TestLogger{T: t}) + resp, err := r.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: namespacedName, + }) + req.NoError(err) + req.False(resp.Requeue) + + _, _, err = consulClient.ConfigEntries().Get(c.consulKind, c.configEntryResourceWithDeletion.ConsulName(), nil) + req.EqualError(err, + fmt.Sprintf("Unexpected response code: 404 (Config entry not found for %q / %q)", + c.consulKind, c.configEntryResourceWithDeletion.ConsulName())) + } + }) + } +} func TestConfigEntryController_createsConfigEntry_consulNamespaces(tt *testing.T) { tt.Parallel() @@ -88,7 +400,7 @@ func TestConfigEntryController_createsConfigEntry_consulNamespaces(tt *testing.T ConsulKind string ConsulNamespace string KubeResource common.ConfigEntryResource - GetController func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler + GetController func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler AssertValidConfig func(entry capi.ConfigEntry) bool }{ "namespaced": { @@ -102,8 +414,8 @@ func TestConfigEntryController_createsConfigEntry_consulNamespaces(tt *testing.T Protocol: "http", }, }, - GetController: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ServiceDefaultsController{ + GetController: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ServiceDefaultsController{ Client: client, Log: logger, Scheme: scheme, @@ -132,8 +444,8 @@ func TestConfigEntryController_createsConfigEntry_consulNamespaces(tt *testing.T }, }, }, - GetController: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ProxyDefaultsController{ + GetController: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ProxyDefaultsController{ Client: client, Log: logger, Scheme: scheme, @@ -170,8 +482,8 @@ func TestConfigEntryController_createsConfigEntry_consulNamespaces(tt *testing.T }, }, }, - GetController: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ServiceIntentionsController{ + GetController: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ServiceIntentionsController{ Client: client, Log: logger, Scheme: scheme, @@ -206,7 +518,7 @@ func TestConfigEntryController_createsConfigEntry_consulNamespaces(tt *testing.T fakeClient, logrtest.TestLogger{T: t}, s, - &controller.ConfigEntryController{ + &ConfigEntryController{ ConsulClientConfig: testClient.Cfg, ConsulServerConnMgr: testClient.Watcher, EnableConsulNamespaces: true, @@ -299,7 +611,7 @@ func TestConfigEntryController_updatesConfigEntry_consulNamespaces(tt *testing.T ConsulKind string ConsulNamespace string KubeResource common.ConfigEntryResource - GetControllerFunc func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler + GetControllerFunc func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler AssertValidConfigFunc func(entry capi.ConfigEntry) bool WriteConfigEntryFunc func(consulClient *capi.Client, namespace string) error UpdateResourceFunc func(client client.Client, ctx context.Context, in common.ConfigEntryResource) error @@ -310,15 +622,15 @@ func TestConfigEntryController_updatesConfigEntry_consulNamespaces(tt *testing.T ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: c.SourceKubeNS, - Finalizers: []string{controller.FinalizerName}, + Finalizers: []string{FinalizerName}, }, Spec: v1alpha1.ServiceDefaultsSpec{ Protocol: "http", }, }, ConsulNamespace: c.ExpConsulNS, - GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ServiceDefaultsController{ + GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ServiceDefaultsController{ Client: client, Log: logger, Scheme: scheme, @@ -352,7 +664,7 @@ func TestConfigEntryController_updatesConfigEntry_consulNamespaces(tt *testing.T ObjectMeta: metav1.ObjectMeta{ Name: common.Global, Namespace: c.SourceKubeNS, - Finalizers: []string{controller.FinalizerName}, + Finalizers: []string{FinalizerName}, }, Spec: v1alpha1.ProxyDefaultsSpec{ MeshGateway: v1alpha1.MeshGateway{ @@ -361,8 +673,8 @@ func TestConfigEntryController_updatesConfigEntry_consulNamespaces(tt *testing.T }, }, ConsulNamespace: common.DefaultConsulNamespace, - GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ProxyDefaultsController{ + GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ProxyDefaultsController{ Client: client, Log: logger, Scheme: scheme, @@ -398,7 +710,7 @@ func TestConfigEntryController_updatesConfigEntry_consulNamespaces(tt *testing.T ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: c.SourceKubeNS, - Finalizers: []string{controller.FinalizerName}, + Finalizers: []string{FinalizerName}, }, Spec: v1alpha1.ServiceIntentionsSpec{ Destination: v1alpha1.IntentionDestination{ @@ -415,8 +727,8 @@ func TestConfigEntryController_updatesConfigEntry_consulNamespaces(tt *testing.T }, }, ConsulNamespace: c.ExpConsulNS, - GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ServiceIntentionsController{ + GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ServiceIntentionsController{ Client: client, Log: logger, Scheme: scheme, @@ -468,7 +780,7 @@ func TestConfigEntryController_updatesConfigEntry_consulNamespaces(tt *testing.T fakeClient, logrtest.TestLogger{T: t}, s, - &controller.ConfigEntryController{ + &ConfigEntryController{ ConsulClientConfig: testClient.Cfg, ConsulServerConnMgr: testClient.Watcher, EnableConsulNamespaces: true, @@ -577,7 +889,7 @@ func TestConfigEntryController_deletesConfigEntry_consulNamespaces(tt *testing.T ConsulKind string ConsulNamespace string KubeResource common.ConfigEntryResource - GetControllerFunc func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler + GetControllerFunc func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler WriteConfigEntryFunc func(consulClient *capi.Client, namespace string) error }{ "namespaced": { @@ -588,7 +900,7 @@ func TestConfigEntryController_deletesConfigEntry_consulNamespaces(tt *testing.T ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: c.SourceKubeNS, - Finalizers: []string{controller.FinalizerName}, + Finalizers: []string{FinalizerName}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, }, Spec: v1alpha1.ServiceDefaultsSpec{ @@ -596,8 +908,8 @@ func TestConfigEntryController_deletesConfigEntry_consulNamespaces(tt *testing.T }, }, ConsulNamespace: c.ExpConsulNS, - GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ServiceDefaultsController{ + GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ServiceDefaultsController{ Client: client, Log: logger, Scheme: scheme, @@ -621,7 +933,7 @@ func TestConfigEntryController_deletesConfigEntry_consulNamespaces(tt *testing.T ObjectMeta: metav1.ObjectMeta{ Name: common.Global, Namespace: c.SourceKubeNS, - Finalizers: []string{controller.FinalizerName}, + Finalizers: []string{FinalizerName}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, }, Spec: v1alpha1.ProxyDefaultsSpec{ @@ -631,8 +943,8 @@ func TestConfigEntryController_deletesConfigEntry_consulNamespaces(tt *testing.T }, }, ConsulNamespace: common.DefaultConsulNamespace, - GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ProxyDefaultsController{ + GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ProxyDefaultsController{ Client: client, Log: logger, Scheme: scheme, @@ -658,7 +970,7 @@ func TestConfigEntryController_deletesConfigEntry_consulNamespaces(tt *testing.T ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: c.SourceKubeNS, - Finalizers: []string{controller.FinalizerName}, + Finalizers: []string{FinalizerName}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, }, Spec: v1alpha1.ServiceIntentionsSpec{ @@ -676,8 +988,8 @@ func TestConfigEntryController_deletesConfigEntry_consulNamespaces(tt *testing.T }, }, ConsulNamespace: c.ExpConsulNS, - GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *controller.ConfigEntryController) reconcile.Reconciler { - return &controller.ServiceIntentionsController{ + GetControllerFunc: func(client client.Client, logger logr.Logger, scheme *runtime.Scheme, cont *ConfigEntryController) reconcile.Reconciler { + return &ServiceIntentionsController{ Client: client, Log: logger, Scheme: scheme, @@ -717,7 +1029,7 @@ func TestConfigEntryController_deletesConfigEntry_consulNamespaces(tt *testing.T fakeClient, logrtest.TestLogger{T: t}, s, - &controller.ConfigEntryController{ + &ConfigEntryController{ ConsulClientConfig: testClient.Cfg, ConsulServerConnMgr: testClient.Watcher, EnableConsulNamespaces: true, diff --git a/control-plane/controller/configentry_controller_test.go b/control-plane/controllers/configentry_controller_test.go similarity index 99% rename from control-plane/controller/configentry_controller_test.go rename to control-plane/controllers/configentry_controller_test.go index 47f28f02d1..494715cf4f 100644 --- a/control-plane/controller/configentry_controller_test.go +++ b/control-plane/controllers/configentry_controller_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/exportedservices_controller.go b/control-plane/controllers/exportedservices_controller.go similarity index 98% rename from control-plane/controller/exportedservices_controller.go rename to control-plane/controllers/exportedservices_controller.go index 90d7261d9a..e72b743a1f 100644 --- a/control-plane/controller/exportedservices_controller.go +++ b/control-plane/controllers/exportedservices_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/exportedservices_controller_ent_test.go b/control-plane/controllers/exportedservices_controller_ent_test.go similarity index 95% rename from control-plane/controller/exportedservices_controller_ent_test.go rename to control-plane/controllers/exportedservices_controller_ent_test.go index aba193bdd9..40ab5d1e1e 100644 --- a/control-plane/controller/exportedservices_controller_ent_test.go +++ b/control-plane/controllers/exportedservices_controller_ent_test.go @@ -3,7 +3,7 @@ //go:build enterprise -package controller_test +package controllers_test import ( "context" @@ -14,7 +14,7 @@ import ( logrtest "github.com/go-logr/logr/testing" "github.com/hashicorp/consul-k8s/control-plane/api/common" "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" - "github.com/hashicorp/consul-k8s/control-plane/controller" + "github.com/hashicorp/consul-k8s/control-plane/controllers" "github.com/hashicorp/consul-k8s/control-plane/helper/test" capi "github.com/hashicorp/consul/api" "github.com/stretchr/testify/require" @@ -103,11 +103,11 @@ func TestExportedServicesController_createsExportedServices(tt *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(exportedServices).Build() - controller := &controller.ExportedServicesController{ + controller := &controllers.ExportedServicesController{ Client: fakeClient, Log: logrtest.TestLogger{T: t}, Scheme: s, - ConfigEntryController: &controller.ConfigEntryController{ + ConfigEntryController: &controllers.ConfigEntryController{ ConsulClientConfig: testClient.Cfg, ConsulServerConnMgr: testClient.Watcher, EnableConsulNamespaces: true, @@ -195,7 +195,7 @@ func TestExportedServicesController_updatesExportedServices(tt *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "default", Namespace: c.SourceKubeNS, - Finalizers: []string{controller.FinalizerName}, + Finalizers: []string{controllers.FinalizerName}, }, Spec: v1alpha1.ExportedServicesSpec{ Services: []v1alpha1.ExportedService{ @@ -218,11 +218,11 @@ func TestExportedServicesController_updatesExportedServices(tt *testing.T) { consulClient := testClient.APIClient fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(exportedServices).Build() - controller := &controller.ExportedServicesController{ + controller := &controllers.ExportedServicesController{ Client: fakeClient, Log: logrtest.TestLogger{T: t}, Scheme: s, - ConfigEntryController: &controller.ConfigEntryController{ + ConfigEntryController: &controllers.ConfigEntryController{ ConsulClientConfig: testClient.Cfg, ConsulServerConnMgr: testClient.Watcher, EnableConsulNamespaces: true, @@ -332,7 +332,7 @@ func TestExportedServicesController_deletesExportedServices(tt *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "default", Namespace: c.SourceKubeNS, - Finalizers: []string{controller.FinalizerName}, + Finalizers: []string{controllers.FinalizerName}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, }, Spec: v1alpha1.ExportedServicesSpec{ @@ -356,11 +356,11 @@ func TestExportedServicesController_deletesExportedServices(tt *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(exportedServices).Build() - controller := &controller.ExportedServicesController{ + controller := &controllers.ExportedServicesController{ Client: fakeClient, Log: logrtest.TestLogger{T: t}, Scheme: s, - ConfigEntryController: &controller.ConfigEntryController{ + ConfigEntryController: &controllers.ConfigEntryController{ ConsulClientConfig: testClient.Cfg, ConsulServerConnMgr: testClient.Watcher, EnableConsulNamespaces: true, diff --git a/control-plane/controller/ingressgateway_controller.go b/control-plane/controllers/ingressgateway_controller.go similarity index 98% rename from control-plane/controller/ingressgateway_controller.go rename to control-plane/controllers/ingressgateway_controller.go index fffc3c5a06..faa728dd4f 100644 --- a/control-plane/controller/ingressgateway_controller.go +++ b/control-plane/controllers/ingressgateway_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/mesh_controller.go b/control-plane/controllers/mesh_controller.go similarity index 98% rename from control-plane/controller/mesh_controller.go rename to control-plane/controllers/mesh_controller.go index 9f7d8cd7c8..92839d0104 100644 --- a/control-plane/controller/mesh_controller.go +++ b/control-plane/controllers/mesh_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/proxydefaults_controller.go b/control-plane/controllers/proxydefaults_controller.go similarity index 98% rename from control-plane/controller/proxydefaults_controller.go rename to control-plane/controllers/proxydefaults_controller.go index 7499928ea4..1415da8688 100644 --- a/control-plane/controller/proxydefaults_controller.go +++ b/control-plane/controllers/proxydefaults_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controllers/samenessgroups_controller.go b/control-plane/controllers/samenessgroups_controller.go new file mode 100644 index 0000000000..afc243f267 --- /dev/null +++ b/control-plane/controllers/samenessgroups_controller.go @@ -0,0 +1,41 @@ +package controllers + +import ( + "context" + "k8s.io/apimachinery/pkg/types" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + consulv1alpha1 "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" +) + +// SamenessGroupController reconciles a SamenessGroups object. +type SamenessGroupController struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + ConfigEntryController *ConfigEntryController +} + +//+kubebuilder:rbac:groups=consul.hashicorp.com,resources=samenessgroups,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=consul.hashicorp.com,resources=samenessgroups/status,verbs=get;update;patch + +func (r *SamenessGroupController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return r.ConfigEntryController.ReconcileEntry(ctx, r, req, &consulv1alpha1.SamenessGroup{}) +} + +func (r *SamenessGroupController) Logger(name types.NamespacedName) logr.Logger { + return r.Log.WithValues("request", name) +} + +func (r *SamenessGroupController) UpdateStatus(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return r.Status().Update(ctx, obj, opts...) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *SamenessGroupController) SetupWithManager(mgr ctrl.Manager) error { + return setupWithManager(mgr, &consulv1alpha1.SamenessGroup{}, r) +} diff --git a/control-plane/controller/servicedefaults_controller.go b/control-plane/controllers/servicedefaults_controller.go similarity index 98% rename from control-plane/controller/servicedefaults_controller.go rename to control-plane/controllers/servicedefaults_controller.go index b96ff7e566..9c2dbe683d 100644 --- a/control-plane/controller/servicedefaults_controller.go +++ b/control-plane/controllers/servicedefaults_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/serviceintentions_controller.go b/control-plane/controllers/serviceintentions_controller.go similarity index 98% rename from control-plane/controller/serviceintentions_controller.go rename to control-plane/controllers/serviceintentions_controller.go index 43298a23f7..30dcb63f81 100644 --- a/control-plane/controller/serviceintentions_controller.go +++ b/control-plane/controllers/serviceintentions_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/serviceresolver_controller.go b/control-plane/controllers/serviceresolver_controller.go similarity index 98% rename from control-plane/controller/serviceresolver_controller.go rename to control-plane/controllers/serviceresolver_controller.go index cca014ab50..f82c4d42a2 100644 --- a/control-plane/controller/serviceresolver_controller.go +++ b/control-plane/controllers/serviceresolver_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/servicerouter_controller.go b/control-plane/controllers/servicerouter_controller.go similarity index 98% rename from control-plane/controller/servicerouter_controller.go rename to control-plane/controllers/servicerouter_controller.go index 6ed1e52fad..831179eee9 100644 --- a/control-plane/controller/servicerouter_controller.go +++ b/control-plane/controllers/servicerouter_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/servicesplitter_controller.go b/control-plane/controllers/servicesplitter_controller.go similarity index 98% rename from control-plane/controller/servicesplitter_controller.go rename to control-plane/controllers/servicesplitter_controller.go index dc5e2a8dd3..1dd89dc278 100644 --- a/control-plane/controller/servicesplitter_controller.go +++ b/control-plane/controllers/servicesplitter_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/controller/terminatinggateway_controller.go b/control-plane/controllers/terminatinggateway_controller.go similarity index 98% rename from control-plane/controller/terminatinggateway_controller.go rename to control-plane/controllers/terminatinggateway_controller.go index 10af041c39..9550a2d04f 100644 --- a/control-plane/controller/terminatinggateway_controller.go +++ b/control-plane/controllers/terminatinggateway_controller.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package controller +package controllers import ( "context" diff --git a/control-plane/go.mod b/control-plane/go.mod index b139e1cd3f..23900deb46 100644 --- a/control-plane/go.mod +++ b/control-plane/go.mod @@ -25,6 +25,8 @@ require ( github.com/mitchellh/cli v1.1.0 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 + github.com/onsi/ginkgo v1.16.4 + github.com/onsi/gomega v1.17.0 github.com/stretchr/testify v1.7.2 go.uber.org/zap v1.19.0 golang.org/x/text v0.3.8 @@ -108,6 +110,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 // indirect + github.com/nxadm/tail v1.4.8 // indirect github.com/oklog/run v1.0.0 // indirect github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect @@ -144,6 +147,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/resty.v1 v1.12.0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.22.2 // indirect diff --git a/control-plane/main.go b/control-plane/main.go index a4ccc9630c..64ccd5d43a 100644 --- a/control-plane/main.go +++ b/control-plane/main.go @@ -7,8 +7,9 @@ import ( "log" "os" - "github.com/hashicorp/consul-k8s/control-plane/version" "github.com/mitchellh/cli" + + "github.com/hashicorp/consul-k8s/control-plane/version" ) func main() { diff --git a/control-plane/subcommand/inject-connect/command.go b/control-plane/subcommand/inject-connect/command.go index 7e44cc8ddf..d5783f4446 100644 --- a/control-plane/subcommand/inject-connect/command.go +++ b/control-plane/subcommand/inject-connect/command.go @@ -21,7 +21,7 @@ import ( "github.com/hashicorp/consul-k8s/control-plane/connect-inject/controllers/peering" "github.com/hashicorp/consul-k8s/control-plane/connect-inject/metrics" "github.com/hashicorp/consul-k8s/control-plane/connect-inject/webhook" - "github.com/hashicorp/consul-k8s/control-plane/controller" + "github.com/hashicorp/consul-k8s/control-plane/controllers" mutatingwebhookconfiguration "github.com/hashicorp/consul-k8s/control-plane/helper/mutating-webhook-configuration" "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" "github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" @@ -462,7 +462,7 @@ func (c *Command) Run(args []string) int { Prefix: c.flagK8SNSMirroringPrefix, } - configEntryReconciler := &controller.ConfigEntryController{ + configEntryReconciler := &controllers.ConfigEntryController{ ConsulClientConfig: c.consul.ConsulClientConfig(), ConsulServerConnMgr: watcher, DatacenterName: c.consul.Datacenter, @@ -472,7 +472,7 @@ func (c *Command) Run(args []string) int { NSMirroringPrefix: c.flagK8SNSMirroringPrefix, CrossNSACLPolicy: c.flagCrossNamespaceACLPolicy, } - if err = (&controller.ServiceDefaultsController{ + if err = (&controllers.ServiceDefaultsController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.ServiceDefaults), @@ -481,7 +481,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.ServiceDefaults) return 1 } - if err = (&controller.ServiceResolverController{ + if err = (&controllers.ServiceResolverController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.ServiceResolver), @@ -490,7 +490,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.ServiceResolver) return 1 } - if err = (&controller.ProxyDefaultsController{ + if err = (&controllers.ProxyDefaultsController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.ProxyDefaults), @@ -499,7 +499,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.ProxyDefaults) return 1 } - if err = (&controller.MeshController{ + if err = (&controllers.MeshController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.Mesh), @@ -508,7 +508,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.Mesh) return 1 } - if err = (&controller.ExportedServicesController{ + if err = (&controllers.ExportedServicesController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.ExportedServices), @@ -517,7 +517,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.ExportedServices) return 1 } - if err = (&controller.ServiceRouterController{ + if err = (&controllers.ServiceRouterController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.ServiceRouter), @@ -526,7 +526,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.ServiceRouter) return 1 } - if err = (&controller.ServiceSplitterController{ + if err = (&controllers.ServiceSplitterController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.ServiceSplitter), @@ -535,7 +535,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.ServiceSplitter) return 1 } - if err = (&controller.ServiceIntentionsController{ + if err = (&controllers.ServiceIntentionsController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.ServiceIntentions), @@ -544,7 +544,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.ServiceIntentions) return 1 } - if err = (&controller.IngressGatewayController{ + if err = (&controllers.IngressGatewayController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.IngressGateway), @@ -553,7 +553,7 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.IngressGateway) return 1 } - if err = (&controller.TerminatingGatewayController{ + if err = (&controllers.TerminatingGatewayController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), Log: ctrl.Log.WithName("controller").WithName(apicommon.TerminatingGateway), @@ -562,6 +562,15 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", apicommon.TerminatingGateway) return 1 } + if err = (&controllers.SamenessGroupController{ + ConfigEntryController: configEntryReconciler, + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controller").WithName(apicommon.SamenessGroup), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", apicommon.SamenessGroup) + return 1 + } if err = mgr.AddReadyzCheck("ready", webhook.ReadinessCheck{CertDir: c.flagCertDir}.Ready); err != nil { setupLog.Error(err, "unable to create readiness check", "controller", endpoints.Controller{}) @@ -714,6 +723,12 @@ func (c *Command) Run(args []string) int { Logger: ctrl.Log.WithName("webhooks").WithName(apicommon.TerminatingGateway), ConsulMeta: consulMeta, }}) + mgr.GetWebhookServer().Register("/mutate-v1alpha1-samenessgroup", + &ctrlRuntimeWebhook.Admission{Handler: &v1alpha1.SamenessGroupWebhook{ + Client: mgr.GetClient(), + Logger: ctrl.Log.WithName("webhooks").WithName(apicommon.SamenessGroup), + ConsulMeta: consulMeta, + }}) if c.flagEnableWebhookCAUpdate { err = c.updateWebhookCABundle(ctx)