diff --git a/.circleci/config.yml b/.circleci/config.yml index b5f3663235..3506aed19f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,8 +6,8 @@ executors: - image: docker.mirror.hashicorp.services/circleci/golang:1.14 environment: TEST_RESULTS: /tmp/test-results # path to where test results are saved - CONSUL_VERSION: 1.10.0-beta1 # Consul's OSS version to use in tests - CONSUL_ENT_VERSION: 1.10.0+ent-beta1 # Consul's enterprise version to use in tests + CONSUL_VERSION: 1.10.0-beta2 # Consul's OSS version to use in tests + CONSUL_ENT_VERSION: 1.10.0+ent-beta2 # Consul's enterprise version to use in tests jobs: go-fmt-and-vet: diff --git a/CHANGELOG.md b/CHANGELOG.md index a04bbf78e9..1548c2942b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ IMPROVEMENTS: using this CRD but via annotations. [[GH-502](https://github.com/hashicorp/consul-k8s/pull/502)], [[GH-485](https://github.com/hashicorp/consul-k8s/pull/485)] * CRDs: Update ProxyDefaults with Mode and TransparentProxy fields. Note: Mode and TransparentProxy should not be set using the CRD but via annotations. [[GH-505](https://github.com/hashicorp/consul-k8s/pull/505)], [[GH-485](https://github.com/hashicorp/consul-k8s/pull/485)] +* CRDs: Add CRD for MeshConfigEntry. Supported in Consul 1.10+ [[GH-513](https://github.com/hashicorp/consul-k8s/pull/513)] * Connect: No longer set multiple tagged addresses in Consul when k8s service has multiple ports and Transparent Proxy is enabled. [[GH-511](https://github.com/hashicorp/consul-k8s/pull/511)] * Connect: Allow exclusion of inbound ports, outbound ports and CIDRs, and additional user IDs when diff --git a/api/common/common.go b/api/common/common.go index 5c1e4a5379..3d0ae3f6e7 100644 --- a/api/common/common.go +++ b/api/common/common.go @@ -12,6 +12,7 @@ const ( TerminatingGateway string = "terminatinggateway" Global string = "global" + Mesh string = "mesh" DefaultConsulNamespace string = "default" WildcardNamespace string = "*" diff --git a/api/v1alpha1/mesh_types.go b/api/v1alpha1/mesh_types.go new file mode 100644 index 0000000000..8e29724906 --- /dev/null +++ b/api/v1alpha1/mesh_types.go @@ -0,0 +1,163 @@ +package v1alpha1 + +import ( + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/consul-k8s/api/common" + capi "github.com/hashicorp/consul/api" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + MeshKubeKind = "mesh" +) + +func init() { + SchemeBuilder.Register(&Mesh{}, &MeshList{}) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Mesh is the Schema for the mesh 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" +type Mesh struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MeshSpec `json:"spec,omitempty"` + Status `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// MeshList contains a list of Mesh +type MeshList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Mesh `json:"items"` +} + +// MeshSpec defines the desired state of Mesh +type MeshSpec struct { + TransparentProxy TransparentProxyMeshConfig `json:"transparentProxy,omitempty"` +} + +// TransparentProxyMeshConfig controls configuration specific to proxies in "transparent" mode. Added in v1.10.0. +type TransparentProxyMeshConfig struct { + // CatalogDestinationsOnly determines whether sidecar proxies operating in "transparent" mode can proxy traffic + // to IP addresses not registered in Consul's catalog. If enabled, traffic will only be proxied to upstreams + // with service registrations in the catalog. + CatalogDestinationsOnly bool `json:"catalogDestinationsOnly,omitempty"` +} + +func (in *TransparentProxyMeshConfig) toConsul() capi.TransparentProxyMeshConfig { + return capi.TransparentProxyMeshConfig{CatalogDestinationsOnly: in.CatalogDestinationsOnly} +} + +func (in *Mesh) GetObjectMeta() metav1.ObjectMeta { + return in.ObjectMeta +} + +func (in *Mesh) AddFinalizer(name string) { + in.ObjectMeta.Finalizers = append(in.Finalizers(), name) +} + +func (in *Mesh) RemoveFinalizer(name string) { + var newFinalizers []string + for _, oldF := range in.Finalizers() { + if oldF != name { + newFinalizers = append(newFinalizers, oldF) + } + } + in.ObjectMeta.Finalizers = newFinalizers + +} + +func (in *Mesh) Finalizers() []string { + return in.ObjectMeta.Finalizers +} + +func (in *Mesh) ConsulKind() string { + return capi.MeshConfig +} + +func (in *Mesh) ConsulMirroringNS() string { + return common.DefaultConsulNamespace +} + +func (in *Mesh) KubeKind() string { + return MeshKubeKind +} + +func (in *Mesh) 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 *Mesh) SyncedConditionStatus() corev1.ConditionStatus { + cond := in.Status.GetCondition(ConditionSynced) + if cond == nil { + return corev1.ConditionUnknown + } + return cond.Status +} + +func (in *Mesh) ConsulName() string { + return in.ObjectMeta.Name +} + +func (in *Mesh) ConsulGlobalResource() bool { + return true +} + +func (in *Mesh) KubernetesName() string { + return in.ObjectMeta.Name +} + +func (in *Mesh) SetSyncedCondition(status corev1.ConditionStatus, reason string, message string) { + in.Status.Conditions = Conditions{ + { + Type: ConditionSynced, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + }, + } +} + +func (in *Mesh) SetLastSyncedTime(time *metav1.Time) { + in.Status.LastSyncedTime = time +} + +func (in *Mesh) ToConsul(datacenter string) capi.ConfigEntry { + return &capi.MeshConfigEntry{ + TransparentProxy: in.Spec.TransparentProxy.toConsul(), + Meta: meta(datacenter), + } +} + +func (in *Mesh) MatchesConsul(candidate capi.ConfigEntry) bool { + configEntry, ok := candidate.(*capi.MeshConfigEntry) + if !ok { + return false + } + // No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality. + return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.MeshConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty()) +} + +func (in *Mesh) Validate(_ bool) error { + return nil +} + +// DefaultNamespaceFields has no behaviour here as meshes have no namespace specific fields. +func (in *Mesh) DefaultNamespaceFields(_ bool, _ string, _ bool, _ string) { + return +} diff --git a/api/v1alpha1/mesh_types_test.go b/api/v1alpha1/mesh_types_test.go new file mode 100644 index 0000000000..3b41c29a34 --- /dev/null +++ b/api/v1alpha1/mesh_types_test.go @@ -0,0 +1,241 @@ +package v1alpha1 + +import ( + "testing" + "time" + + "github.com/hashicorp/consul-k8s/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" +) + +// Test MatchesConsul for cases that should return true. +func TestMesh_MatchesConsul(t *testing.T) { + cases := map[string]struct { + Ours Mesh + Theirs capi.ConfigEntry + Matches bool + }{ + "empty fields matches": { + Ours: Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Mesh, + }, + Spec: MeshSpec{}, + }, + Theirs: &capi.MeshConfigEntry{ + Namespace: "default", + CreateIndex: 1, + ModifyIndex: 2, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + Matches: true, + }, + "all fields set matches": { + Ours: Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Mesh, + }, + Spec: MeshSpec{ + TransparentProxy: TransparentProxyMeshConfig{ + CatalogDestinationsOnly: true, + }, + }, + }, + Theirs: &capi.MeshConfigEntry{ + TransparentProxy: capi.TransparentProxyMeshConfig{ + CatalogDestinationsOnly: true, + }, + CreateIndex: 1, + ModifyIndex: 2, + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + Matches: true, + }, + "mismatched types does not match": { + Ours: Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Mesh, + }, + Spec: MeshSpec{}, + }, + Theirs: &capi.ServiceConfigEntry{ + Name: common.Mesh, + Kind: capi.MeshConfig, + }, + Matches: false, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + require.Equal(t, c.Matches, c.Ours.MatchesConsul(c.Theirs)) + }) + } +} + +func TestMesh_ToConsul(t *testing.T) { + cases := map[string]struct { + Ours Mesh + Exp *capi.MeshConfigEntry + }{ + "empty fields": { + Ours: Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: MeshSpec{}, + }, + Exp: &capi.MeshConfigEntry{ + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + }, + "every field set": { + Ours: Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + }, + Spec: MeshSpec{ + TransparentProxy: TransparentProxyMeshConfig{ + CatalogDestinationsOnly: true, + }, + }, + }, + Exp: &capi.MeshConfigEntry{ + TransparentProxy: capi.TransparentProxyMeshConfig{ + CatalogDestinationsOnly: true, + }, + Namespace: "", + Meta: map[string]string{ + common.SourceKey: common.SourceValue, + common.DatacenterKey: "datacenter", + }, + }, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + act := c.Ours.ToConsul("datacenter") + mesh, ok := act.(*capi.MeshConfigEntry) + require.True(t, ok, "could not cast") + require.Equal(t, c.Exp, mesh) + }) + } +} + +func TestMesh_AddFinalizer(t *testing.T) { + mesh := &Mesh{} + mesh.AddFinalizer("finalizer") + require.Equal(t, []string{"finalizer"}, mesh.ObjectMeta.Finalizers) +} + +func TestMesh_RemoveFinalizer(t *testing.T) { + mesh := &Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Finalizers: []string{"f1", "f2"}, + }, + } + mesh.RemoveFinalizer("f1") + require.Equal(t, []string{"f2"}, mesh.ObjectMeta.Finalizers) +} + +func TestMesh_SetSyncedCondition(t *testing.T) { + mesh := &Mesh{} + mesh.SetSyncedCondition(corev1.ConditionTrue, "reason", "message") + + require.Equal(t, corev1.ConditionTrue, mesh.Status.Conditions[0].Status) + require.Equal(t, "reason", mesh.Status.Conditions[0].Reason) + require.Equal(t, "message", mesh.Status.Conditions[0].Message) + now := metav1.Now() + require.True(t, mesh.Status.Conditions[0].LastTransitionTime.Before(&now)) +} + +func TestMesh_SetLastSyncedTime(t *testing.T) { + mesh := &Mesh{} + syncedTime := metav1.NewTime(time.Now()) + mesh.SetLastSyncedTime(&syncedTime) + + require.Equal(t, &syncedTime, mesh.Status.LastSyncedTime) +} + +func TestMesh_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) { + mesh := &Mesh{ + Status: Status{ + Conditions: []Condition{{ + Type: ConditionSynced, + Status: status, + }}, + }, + } + + require.Equal(t, status, mesh.SyncedConditionStatus()) + }) + } +} + +func TestMesh_GetConditionWhenStatusNil(t *testing.T) { + require.Nil(t, (&Mesh{}).GetCondition(ConditionSynced)) +} + +func TestMesh_SyncedConditionStatusWhenStatusNil(t *testing.T) { + require.Equal(t, corev1.ConditionUnknown, (&Mesh{}).SyncedConditionStatus()) +} + +func TestMesh_SyncedConditionWhenStatusNil(t *testing.T) { + status, reason, message := (&Mesh{}).SyncedCondition() + require.Equal(t, corev1.ConditionUnknown, status) + require.Equal(t, "", reason) + require.Equal(t, "", message) +} + +func TestMesh_ConsulKind(t *testing.T) { + require.Equal(t, capi.MeshConfig, (&Mesh{}).ConsulKind()) +} + +func TestMesh_KubeKind(t *testing.T) { + require.Equal(t, "mesh", (&Mesh{}).KubeKind()) +} + +func TestMesh_ConsulName(t *testing.T) { + require.Equal(t, "foo", (&Mesh{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}).ConsulName()) +} + +func TestMesh_KubernetesName(t *testing.T) { + require.Equal(t, "foo", (&Mesh{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}).KubernetesName()) +} + +func TestMesh_ConsulNamespace(t *testing.T) { + require.Equal(t, common.DefaultConsulNamespace, (&Mesh{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}}).ConsulMirroringNS()) +} + +func TestMesh_ConsulGlobalResource(t *testing.T) { + require.True(t, (&Mesh{}).ConsulGlobalResource()) +} + +func TestMesh_ObjectMeta(t *testing.T) { + meta := metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + } + mesh := &Mesh{ + ObjectMeta: meta, + } + require.Equal(t, meta, mesh.GetObjectMeta()) +} diff --git a/api/v1alpha1/mesh_webhook.go b/api/v1alpha1/mesh_webhook.go new file mode 100644 index 0000000000..e5d84a0750 --- /dev/null +++ b/api/v1alpha1/mesh_webhook.go @@ -0,0 +1,70 @@ +package v1alpha1 + +import ( + "context" + "fmt" + "net/http" + + "github.com/go-logr/logr" + "github.com/hashicorp/consul-k8s/api/common" + capi "github.com/hashicorp/consul/api" + admissionv1 "k8s.io/api/admission/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// +kubebuilder:object:generate=false + +type MeshWebhook struct { + client.Client + ConsulClient *capi.Client + Logger logr.Logger + decoder *admission.Decoder + EnableConsulNamespaces bool + EnableNSMirroring bool +} + +// 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-mesh,mutating=true,failurePolicy=fail,groups=consul.hashicorp.com,resources=mesh,versions=v1alpha1,name=mutate-mesh.consul.hashicorp.com,sideEffects=None,admissionReviewVersions=v1beta1;v1 + +func (v *MeshWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { + var mesh Mesh + var meshList MeshList + err := v.decoder.Decode(req, &mesh) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if req.Operation == admissionv1.Create { + v.Logger.Info("validate create", "name", mesh.KubernetesName()) + + if mesh.KubernetesName() != common.Mesh { + return admission.Errored(http.StatusBadRequest, + fmt.Errorf(`%s resource name must be "%s"`, + mesh.KubeKind(), common.Mesh)) + } + + if err := v.Client.List(ctx, &meshList); err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + + if len(meshList.Items) > 0 { + return admission.Errored(http.StatusBadRequest, + fmt.Errorf("%s resource already defined - only one mesh entry is supported", + mesh.KubeKind())) + } + } + + return admission.Allowed(fmt.Sprintf("valid %s request", mesh.KubeKind())) +} + +func (v *MeshWebhook) InjectDecoder(d *admission.Decoder) error { + v.decoder = d + return nil +} diff --git a/api/v1alpha1/mesh_webhook_test.go b/api/v1alpha1/mesh_webhook_test.go new file mode 100644 index 0000000000..d8f44cc7b2 --- /dev/null +++ b/api/v1alpha1/mesh_webhook_test.go @@ -0,0 +1,101 @@ +package v1alpha1 + +import ( + "context" + "encoding/json" + "testing" + + logrtest "github.com/go-logr/logr/testing" + "github.com/hashicorp/consul-k8s/api/common" + "github.com/stretchr/testify/require" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestValidateMesh(t *testing.T) { + otherNS := "other" + + cases := map[string]struct { + existingResources []runtime.Object + newResource *Mesh + expAllow bool + expErrMessage string + }{ + "no duplicates, valid": { + existingResources: nil, + newResource: &Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Mesh, + }, + Spec: MeshSpec{}, + }, + expAllow: true, + }, + "mesh exists": { + existingResources: []runtime.Object{&Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Mesh, + }, + }}, + newResource: &Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Mesh, + }, + Spec: MeshSpec{ + TransparentProxy: TransparentProxyMeshConfig{ + CatalogDestinationsOnly: true, + }, + }, + }, + expAllow: false, + expErrMessage: "mesh resource already defined - only one mesh entry is supported", + }, + "name not mesh": { + existingResources: []runtime.Object{}, + newResource: &Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: "local", + }, + }, + expAllow: false, + expErrMessage: "mesh resource name must be \"mesh\"", + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + marshalledRequestObject, err := json.Marshal(c.newResource) + require.NoError(t, err) + s := runtime.NewScheme() + s.AddKnownTypes(GroupVersion, &Mesh{}, &MeshList{}) + client := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(c.existingResources...).Build() + decoder, err := admission.NewDecoder(s) + require.NoError(t, err) + + validator := &MeshWebhook{ + Client: client, + ConsulClient: nil, + Logger: logrtest.TestLogger{T: t}, + decoder: decoder, + } + response := validator.Handle(ctx, admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Name: c.newResource.KubernetesName(), + Namespace: otherNS, + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: marshalledRequestObject, + }, + }, + }) + + require.Equal(t, c.expAllow, response.Allowed) + if c.expErrMessage != "" { + require.Equal(t, c.expErrMessage, response.AdmissionResponse.Result.Message) + } + }) + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 80dd04e02b..349c90786c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -436,6 +436,33 @@ func (in *LoadBalancer) DeepCopy() *LoadBalancer { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Mesh) DeepCopyInto(out *Mesh) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mesh. +func (in *Mesh) DeepCopy() *Mesh { + if in == nil { + return nil + } + out := new(Mesh) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Mesh) 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 *MeshGateway) DeepCopyInto(out *MeshGateway) { *out = *in @@ -451,6 +478,54 @@ func (in *MeshGateway) DeepCopy() *MeshGateway { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MeshList) DeepCopyInto(out *MeshList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Mesh, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MeshList. +func (in *MeshList) DeepCopy() *MeshList { + if in == nil { + return nil + } + out := new(MeshList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MeshList) 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 *MeshSpec) DeepCopyInto(out *MeshSpec) { + *out = *in + out.TransparentProxy = in.TransparentProxy +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MeshSpec. +func (in *MeshSpec) DeepCopy() *MeshSpec { + if in == nil { + return nil + } + out := new(MeshSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) { *out = *in @@ -1430,6 +1505,21 @@ func (in *TransparentProxy) DeepCopy() *TransparentProxy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TransparentProxyMeshConfig) DeepCopyInto(out *TransparentProxyMeshConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TransparentProxyMeshConfig. +func (in *TransparentProxyMeshConfig) DeepCopy() *TransparentProxyMeshConfig { + if in == nil { + return nil + } + out := new(TransparentProxyMeshConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Upstream) DeepCopyInto(out *Upstream) { *out = *in diff --git a/config/crd/bases/consul.hashicorp.com_meshes.yaml b/config/crd/bases/consul.hashicorp.com_meshes.yaml new file mode 100644 index 0000000000..ac0ce8e7e2 --- /dev/null +++ b/config/crd/bases/consul.hashicorp.com_meshes.yaml @@ -0,0 +1,99 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.5.0 + creationTimestamp: null + name: meshes.consul.hashicorp.com +spec: + group: consul.hashicorp.com + names: + kind: Mesh + listKind: MeshList + plural: meshes + singular: mesh + 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: Mesh is the Schema for the mesh 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: MeshSpec defines the desired state of Mesh + properties: + transparentProxy: + description: TransparentProxyMeshConfig controls configuration specific to proxies in "transparent" mode. Added in v1.10.0. + properties: + catalogDestinationsOnly: + description: CatalogDestinationsOnly determines whether sidecar proxies operating in "transparent" mode can proxy traffic to IP addresses not registered in Consul's catalog. If enabled, traffic will only be proxied to upstreams with service registrations in the catalog. + type: boolean + type: object + 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/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 81d3c41ddf..ca5496003f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -9,6 +9,7 @@ resources: - bases/consul.hashicorp.com_serviceintentions.yaml - bases/consul.hashicorp.com_ingressgateways.yaml - bases/consul.hashicorp.com_terminatinggateways.yaml +- bases/consul.hashicorp.com_meshes.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -21,6 +22,7 @@ patchesStrategicMerge: - patches/webhook_in_serviceintentions.yaml - patches/webhook_in_ingressgateways.yaml - patches/webhook_in_terminatinggateways.yaml +#- patches/webhook_in_meshes.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. @@ -32,6 +34,7 @@ patchesStrategicMerge: #- patches/cainjection_in_serviceintentions.yaml #- patches/cainjection_in_ingressgateways.yaml #- patches/cainjection_in_terminatinggateways.yaml +#- patches/cainjection_in_meshes.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 6cbe52f2db..734e80da0f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -26,6 +26,26 @@ rules: - get - patch - update +- apiGroups: + - consul.hashicorp.com + resources: + - mesh + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - consul.hashicorp.com + resources: + - mesh/status + verbs: + - get + - patch + - update - apiGroups: - consul.hashicorp.com resources: diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 96d63800b9..21d6cf6dec 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -27,6 +27,27 @@ webhooks: resources: - ingressgateways sideEffects: None +- admissionReviewVersions: + - v1beta1 + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-v1alpha1-mesh + failurePolicy: Fail + name: mutate-mesh.consul.hashicorp.com + rules: + - apiGroups: + - consul.hashicorp.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - mesh + sideEffects: None - admissionReviewVersions: - v1beta1 - v1 diff --git a/controller/configentry_controller_test.go b/controller/configentry_controller_test.go index b7322e4dd7..6a0afd60fa 100644 --- a/controller/configentry_controller_test.go +++ b/controller/configentry_controller_test.go @@ -130,6 +130,36 @@ func TestConfigEntryControllers_createsConfigEntry(t *testing.T) { require.Equal(t, capi.MeshGatewayModeRemote, proxyDefault.MeshGateway.Mode) }, }, + { + kubeKind: "Mesh", + consulKind: capi.MeshConfig, + configEntryResource: &v1alpha1.Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Mesh, + Namespace: kubeNS, + }, + Spec: v1alpha1.MeshSpec{ + TransparentProxy: v1alpha1.TransparentProxyMeshConfig{ + CatalogDestinationsOnly: true, + }, + }, + }, + reconciler: func(client client.Client, consulClient *capi.Client, logger logr.Logger) testReconciler { + return &MeshController{ + Client: client, + Log: logger, + ConfigEntryController: &ConfigEntryController{ + ConsulClient: consulClient, + DatacenterName: datacenterName, + }, + } + }, + compare: func(t *testing.T, consulEntry capi.ConfigEntry) { + mesh, ok := consulEntry.(*capi.MeshConfigEntry) + require.True(t, ok, "cast error") + require.True(t, mesh.TransparentProxy.CatalogDestinationsOnly) + }, + }, { kubeKind: "ServiceRouter", consulKind: capi.ServiceRouter, @@ -548,6 +578,40 @@ func TestConfigEntryControllers_updatesConfigEntry(t *testing.T) { require.Equal(t, capi.MeshGatewayModeLocal, proxyDefault.MeshGateway.Mode) }, }, + { + kubeKind: "Mesh", + consulKind: capi.MeshConfig, + configEntryResource: &v1alpha1.Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Mesh, + Namespace: kubeNS, + }, + Spec: v1alpha1.MeshSpec{ + TransparentProxy: v1alpha1.TransparentProxyMeshConfig{ + CatalogDestinationsOnly: true, + }, + }, + }, + reconciler: func(client client.Client, consulClient *capi.Client, logger logr.Logger) testReconciler { + return &MeshController{ + Client: client, + Log: logger, + ConfigEntryController: &ConfigEntryController{ + ConsulClient: consulClient, + DatacenterName: datacenterName, + }, + } + }, + updateF: func(resource common.ConfigEntryResource) { + mesh := resource.(*v1alpha1.Mesh) + mesh.Spec.TransparentProxy.CatalogDestinationsOnly = false + }, + compare: func(t *testing.T, consulEntry capi.ConfigEntry) { + meshConfigEntry, ok := consulEntry.(*capi.MeshConfigEntry) + require.True(t, ok, "cast error") + require.False(t, meshConfigEntry.TransparentProxy.CatalogDestinationsOnly) + }, + }, { kubeKind: "ServiceSplitter", consulKind: capi.ServiceSplitter, @@ -978,6 +1042,33 @@ func TestConfigEntryControllers_deletesConfigEntry(t *testing.T) { } }, }, + { + kubeKind: "Mesh", + consulKind: capi.MeshConfig, + configEntryResourceWithDeletion: &v1alpha1.Mesh{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.Global, + Namespace: kubeNS, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{FinalizerName}, + }, + Spec: v1alpha1.MeshSpec{ + TransparentProxy: v1alpha1.TransparentProxyMeshConfig{ + CatalogDestinationsOnly: true, + }, + }, + }, + reconciler: func(client client.Client, consulClient *capi.Client, logger logr.Logger) testReconciler { + return &MeshController{ + Client: client, + Log: logger, + ConfigEntryController: &ConfigEntryController{ + ConsulClient: consulClient, + DatacenterName: datacenterName, + }, + } + }, + }, { kubeKind: "ServiceRouter", consulKind: capi.ServiceRouter, diff --git a/controller/mesh_controller.go b/controller/mesh_controller.go new file mode 100644 index 0000000000..860c10a4d1 --- /dev/null +++ b/controller/mesh_controller.go @@ -0,0 +1,42 @@ +package controller + +import ( + "context" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + consulv1alpha1 "github.com/hashicorp/consul-k8s/api/v1alpha1" +) + +// MeshController reconciles a Mesh object +type MeshController struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + ConfigEntryController *ConfigEntryController +} + +// +kubebuilder:rbac:groups=consul.hashicorp.com,resources=mesh,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=consul.hashicorp.com,resources=mesh/status,verbs=get;update;patch + +func (r *MeshController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return r.ConfigEntryController.ReconcileEntry(ctx, r, req, &consulv1alpha1.Mesh{}) +} + +func (r *MeshController) Logger(name types.NamespacedName) logr.Logger { + return r.Log.WithValues("request", name) +} + +func (r *MeshController) UpdateStatus(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return r.Status().Update(ctx, obj, opts...) +} + +func (r *MeshController) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&consulv1alpha1.Mesh{}). + Complete(r) +} diff --git a/go.mod b/go.mod index fcbf1560af..208d14e071 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/go-cmp v0.5.2 github.com/google/go-querystring v1.0.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/hashicorp/consul/api v1.4.1-0.20210416003128-a11ea6254e61 + github.com/hashicorp/consul/api v1.4.1-0.20210504212756-347f3d212843 github.com/hashicorp/consul/sdk v0.7.0 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-discover v0.0.0-20200812215701-c4b85f6ed31f diff --git a/go.sum b/go.sum index 05bd9df41f..1d2b2161f5 100644 --- a/go.sum +++ b/go.sum @@ -316,8 +316,10 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.4.1-0.20210416003128-a11ea6254e61 h1:ph/jWkp4SOUzjq8HQXUuV5UXRKDFkV3l1RPupDG4QFY= -github.com/hashicorp/consul/api v1.4.1-0.20210416003128-a11ea6254e61/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk= +github.com/hashicorp/consul/api v1.4.1-0.20210504212756-347f3d212843 h1:KrwvodQtuOqcAscposKbpRBxNsjRvWb2EE28WBNcH7E= +github.com/hashicorp/consul/api v1.4.1-0.20210504212756-347f3d212843/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk= +github.com/hashicorp/consul/api v1.4.1-0.20210505201732-0a6d439dbb2c h1:CxKbZF3rL3HBU88k2IBZQe5wBWkPAxlI0zBPAWLOMfI= +github.com/hashicorp/consul/api v1.4.1-0.20210505201732-0a6d439dbb2c/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.7.0 h1:H6R9d008jDcHPQPAqPNuydAshJ4v5/8URdFnUvK/+sc= github.com/hashicorp/consul/sdk v0.7.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= diff --git a/subcommand/controller/command.go b/subcommand/controller/command.go index 5dc859e166..3135aa5c3c 100644 --- a/subcommand/controller/command.go +++ b/subcommand/controller/command.go @@ -171,6 +171,15 @@ func (c *Command) Run(args []string) int { setupLog.Error(err, "unable to create controller", "controller", common.ProxyDefaults) return 1 } + if err = (&controller.MeshController{ + ConfigEntryController: configEntryReconciler, + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controller").WithName(common.Mesh), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", common.Mesh) + return 1 + } if err = (&controller.ServiceRouterController{ ConfigEntryController: configEntryReconciler, Client: mgr.GetClient(), @@ -252,6 +261,14 @@ func (c *Command) Run(args []string) int { EnableConsulNamespaces: c.flagEnableNamespaces, EnableNSMirroring: c.flagEnableNSMirroring, }}) + mgr.GetWebhookServer().Register("/mutate-v1alpha1-mesh", + &webhook.Admission{Handler: &v1alpha1.MeshWebhook{ + Client: mgr.GetClient(), + ConsulClient: consulClient, + Logger: ctrl.Log.WithName("webhooks").WithName(common.Mesh), + EnableConsulNamespaces: c.flagEnableNamespaces, + EnableNSMirroring: c.flagEnableNSMirroring, + }}) mgr.GetWebhookServer().Register("/mutate-v1alpha1-servicerouter", &webhook.Admission{Handler: &v1alpha1.ServiceRouterWebhook{ Client: mgr.GetClient(),