From 396af7c9460b1b3b1d22f686603c2d883e02b6b5 Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Tue, 10 Dec 2024 11:44:19 -0800 Subject: [PATCH] refactor(multicluster): Replace use of unstructured API with typed bindings for Link CR (#13420) The linkerd-multicluster extension uses client-go's `unstructured` API to access Link custom resources. This API allowed us to develop quickly without the work of generating typed bindings. However, using the unstrucutred API is error prone since fields must be accessed by their string name. It is also inconsistent with the rest of the project which uses typed bindings. We replace the use of the unstructured API for Link resources with generated typed bindings. The client-go APIs are slightly different and client-go does not provide a way to update subresources for typed bindings. Therefore, when updating a Link's status subresource, we use a patch instead of an update. Signed-off-by: Alex Leong --- bin/update-codegen.sh | 5 +- controller/gen/apis/link/v1alpha1/types.go | 9 +- .../link/v1alpha1/zz_generated.deepcopy.go | 1 + controller/gen/apis/link/v1alpha2/doc.go | 3 + controller/gen/apis/link/v1alpha2/register.go | 48 ++ controller/gen/apis/link/v1alpha2/types.go | 110 +++++ .../link/v1alpha2/zz_generated.deepcopy.go | 225 +++++++++ .../client/clientset/versioned/clientset.go | 13 + .../versioned/fake/clientset_generated.go | 7 + .../clientset/versioned/fake/register.go | 2 + .../clientset/versioned/scheme/register.go | 2 + .../versioned/typed/link/v1alpha2/doc.go | 20 + .../versioned/typed/link/v1alpha2/fake/doc.go | 20 + .../typed/link/v1alpha2/fake/fake_link.go | 134 ++++++ .../link/v1alpha2/fake/fake_link_client.go | 40 ++ .../link/v1alpha2/generated_expansion.go | 21 + .../versioned/typed/link/v1alpha2/link.go | 67 +++ .../typed/link/v1alpha2/link_client.go | 107 +++++ .../informers/externalversions/generic.go | 9 +- .../externalversions/link/interface.go | 8 + .../link/v1alpha2/interface.go | 45 ++ .../externalversions/link/v1alpha2/link.go | 90 ++++ .../link/v1alpha2/expansion_generated.go | 27 ++ .../gen/client/listers/link/v1alpha2/link.go | 70 +++ .../templates/service-mirror.yaml | 2 +- multicluster/cmd/check.go | 118 ++--- multicluster/cmd/link.go | 85 +++- multicluster/cmd/service-mirror/main.go | 221 +++++---- .../testdata/service_mirror_default.golden | 2 +- .../cmd/testdata/service_mirror_ha.golden | 2 +- multicluster/cmd/uninstall.go | 10 +- multicluster/cmd/unlink.go | 5 +- .../service-mirror/cluster_watcher.go | 343 +++++++------- .../cluster_watcher_headless.go | 20 +- .../cluster_watcher_mirroring_test.go | 71 +-- .../cluster_watcher_test_util.go | 440 ++++++++++-------- .../service-mirror/events_formatting.go | 6 + multicluster/service-mirror/probe_worker.go | 41 +- pkg/multicluster/link.go | 418 ----------------- 39 files changed, 1835 insertions(+), 1032 deletions(-) create mode 100644 controller/gen/apis/link/v1alpha2/doc.go create mode 100644 controller/gen/apis/link/v1alpha2/register.go create mode 100644 controller/gen/apis/link/v1alpha2/types.go create mode 100644 controller/gen/apis/link/v1alpha2/zz_generated.deepcopy.go create mode 100644 controller/gen/client/clientset/versioned/typed/link/v1alpha2/doc.go create mode 100644 controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/doc.go create mode 100644 controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/fake_link.go create mode 100644 controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/fake_link_client.go create mode 100644 controller/gen/client/clientset/versioned/typed/link/v1alpha2/generated_expansion.go create mode 100644 controller/gen/client/clientset/versioned/typed/link/v1alpha2/link.go create mode 100644 controller/gen/client/clientset/versioned/typed/link/v1alpha2/link_client.go create mode 100644 controller/gen/client/informers/externalversions/link/v1alpha2/interface.go create mode 100644 controller/gen/client/informers/externalversions/link/v1alpha2/link.go create mode 100644 controller/gen/client/listers/link/v1alpha2/expansion_generated.go create mode 100644 controller/gen/client/listers/link/v1alpha2/link.go delete mode 100644 pkg/multicluster/link.go diff --git a/bin/update-codegen.sh b/bin/update-codegen.sh index 1f9cb33f94ae0..9aebdefaa2e6f 100755 --- a/bin/update-codegen.sh +++ b/bin/update-codegen.sh @@ -20,11 +20,10 @@ git clone --depth 1 --branch "$GEN_VER" https://github.com/kubernetes/code-gener rm -rf "${SCRIPT_ROOT}/controller/gen/client/clientset/*" rm -rf "${SCRIPT_ROOT}/controller/gen/client/listeners/*" rm -rf "${SCRIPT_ROOT}/controller/gen/client/informers/*" -crds=(serviceprofile:v1alpha2 server:v1beta1 serverauthorization:v1beta1 link:v1alpha1 policy:v1alpha1 policy:v1beta3 externalworkload:v1beta1) +crds=(serviceprofile server serverauthorization link policy policy externalworkload) for crd in "${crds[@]}" do - crd_path=$(tr : / <<< "$crd") - rm -f "${SCRIPT_ROOT}/controller/gen/apis/${crd_path}/zz_generated.deepcopy.go" + rm -f "${SCRIPT_ROOT}"/controller/gen/apis/"${crd}"/*/zz_generated.deepcopy.go done # shellcheck disable=SC1091 diff --git a/controller/gen/apis/link/v1alpha1/types.go b/controller/gen/apis/link/v1alpha1/types.go index c18d3aed7f896..ff74983a07368 100644 --- a/controller/gen/apis/link/v1alpha1/types.go +++ b/controller/gen/apis/link/v1alpha1/types.go @@ -37,13 +37,16 @@ type LinkSpec struct { GatewayIdentity string `json:"gatewayIdentity,omitempty"` ProbeSpec ProbeSpec `json:"probeSpec,omitempty"` Selector metav1.LabelSelector `json:"selector,omitempty"` + RemoteDiscoverySelector metav1.LabelSelector `json:"remoteDiscoverySelector,omitempty"` } // ProbeSpec for gateway health probe type ProbeSpec struct { - Path string `json:"path,omitempty"` - Port string `json:"port,omitempty"` - Period string `json:"period,omitempty"` + Path string `json:"path,omitempty"` + Port string `json:"port,omitempty"` + Period string `json:"period,omitempty"` + Timeout string `json:"timeout,omitempty"` + FailureThreshold string `json:"failureThreshold,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/controller/gen/apis/link/v1alpha1/zz_generated.deepcopy.go b/controller/gen/apis/link/v1alpha1/zz_generated.deepcopy.go index aee1096fcd93f..55e0b6e3bf87a 100644 --- a/controller/gen/apis/link/v1alpha1/zz_generated.deepcopy.go +++ b/controller/gen/apis/link/v1alpha1/zz_generated.deepcopy.go @@ -90,6 +90,7 @@ func (in *LinkSpec) DeepCopyInto(out *LinkSpec) { *out = *in out.ProbeSpec = in.ProbeSpec in.Selector.DeepCopyInto(&out.Selector) + in.RemoteDiscoverySelector.DeepCopyInto(&out.RemoteDiscoverySelector) return } diff --git a/controller/gen/apis/link/v1alpha2/doc.go b/controller/gen/apis/link/v1alpha2/doc.go new file mode 100644 index 0000000000000..03ed54aa807d7 --- /dev/null +++ b/controller/gen/apis/link/v1alpha2/doc.go @@ -0,0 +1,3 @@ +// +k8s:deepcopy-gen=package + +package v1alpha2 diff --git a/controller/gen/apis/link/v1alpha2/register.go b/controller/gen/apis/link/v1alpha2/register.go new file mode 100644 index 0000000000000..7890e47eedb1c --- /dev/null +++ b/controller/gen/apis/link/v1alpha2/register.go @@ -0,0 +1,48 @@ +package v1alpha2 + +import ( + "github.com/linkerd/linkerd2/controller/gen/apis/link" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // SchemeGroupVersion is the identifier for the API which includes the name + // of the group and the version of the API. + SchemeGroupVersion = schema.GroupVersion{ + Group: link.GroupName, + Version: "v1alpha2", + } + + // SchemeBuilder collects functions that add things to a scheme. It's to + // allow code to compile without explicitly referencing generated types. + // You should declare one in each package that will have generated deep + // copy or conversion functions. + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme applies all the stored functions to the scheme. A non-nil error + // indicates that one function failed and the attempt was abandoned. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified +// GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &Link{}, + &LinkList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/controller/gen/apis/link/v1alpha2/types.go b/controller/gen/apis/link/v1alpha2/types.go new file mode 100644 index 0000000000000..2ea3a6fb7b029 --- /dev/null +++ b/controller/gen/apis/link/v1alpha2/types.go @@ -0,0 +1,110 @@ +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +genclient:noStatus +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +groupName=multicluster.linkerd.io + +type Link struct { + // TypeMeta is the metadata for the resource, like kind and apiversion + metav1.TypeMeta `json:",inline"` + + // ObjectMeta contains the metadata for the particular object, including + // things like... + // - name + // - namespace + // - self link + // - labels + // - ... etc ... + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec is the custom resource spec + Spec LinkSpec `json:"spec"` + + // Status defines the current state of a Link + Status LinkStatus `json:"status,omitempty"` +} + +// LinkSpec specifies a LinkSpec resource. +type LinkSpec struct { + TargetClusterName string `json:"targetClusterName,omitempty"` + TargetClusterDomain string `json:"targetClusterDomain,omitempty"` + TargetClusterLinkerdNamespace string `json:"targetClusterLinkerdNamespace,omitempty"` + ClusterCredentialsSecret string `json:"clusterCredentialsSecret,omitempty"` + GatewayAddress string `json:"gatewayAddress,omitempty"` + GatewayPort string `json:"gatewayPort,omitempty"` + GatewayIdentity string `json:"gatewayIdentity,omitempty"` + ProbeSpec ProbeSpec `json:"probeSpec,omitempty"` + Selector *metav1.LabelSelector `json:"selector,omitempty"` + RemoteDiscoverySelector *metav1.LabelSelector `json:"remoteDiscoverySelector,omitempty"` + FederatedServiceSelector *metav1.LabelSelector `json:"federatedServiceSelector,omitempty"` +} + +// ProbeSpec for gateway health probe +type ProbeSpec struct { + Path string `json:"path,omitempty"` + Port string `json:"port,omitempty"` + Period string `json:"period,omitempty"` + Timeout string `json:"timeout,omitempty"` + FailureThreshold string `json:"failureThreshold,omitempty"` +} + +// LinkStatus holds information about the status services mirrored with this +// Link. +type LinkStatus struct { + // +optional + MirrorServices []ServiceStatus `json:"mirrorServices,omitempty"` + // +optional + FederatedServices []ServiceStatus `json:"federatedServices,omitempty"` +} + +type ServiceStatus struct { + Conditions []LinkCondition `json:"conditions,omitempty"` + ControllerName string `json:"controllerName,omitempty"` + RemoteRef ObjectRef `json:"remoteRef,omitempty"` +} + +// LinkCondition represents the service state of an ExternalWorkload +type LinkCondition struct { + // Type of the condition + Type string `json:"type"` + // Status of the condition. + // Can be True, False, Unknown + Status metav1.ConditionStatus `json:"status"` + // Last time an ExternalWorkload was probed for a condition. + // +optional + LastProbeTime metav1.Time `json:"lastProbeTime,omitempty"` + // Last time a condition transitioned from one status to another. + // +optional + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + // Unique one word reason in CamelCase that describes the reason for a + // transition. + // +optional + Reason string `json:"reason,omitempty"` + // Human readable message that describes details about last transition. + // +optional + Message string `json:"message"` + // LocalRef is a reference to the local mirror or federated service. + LocalRef ObjectRef `json:"localRef,omitempty"` +} + +type ObjectRef struct { + Group string `json:"group,omitempty"` + Kind string `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// LinkList is a list of LinkList resources. +type LinkList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []Link `json:"items"` +} diff --git a/controller/gen/apis/link/v1alpha2/zz_generated.deepcopy.go b/controller/gen/apis/link/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 0000000000000..7dc43c1f2bf20 --- /dev/null +++ b/controller/gen/apis/link/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,225 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Link) DeepCopyInto(out *Link) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Link. +func (in *Link) DeepCopy() *Link { + if in == nil { + return nil + } + out := new(Link) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Link) 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 *LinkCondition) DeepCopyInto(out *LinkCondition) { + *out = *in + in.LastProbeTime.DeepCopyInto(&out.LastProbeTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + out.LocalRef = in.LocalRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LinkCondition. +func (in *LinkCondition) DeepCopy() *LinkCondition { + if in == nil { + return nil + } + out := new(LinkCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LinkList) DeepCopyInto(out *LinkList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Link, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LinkList. +func (in *LinkList) DeepCopy() *LinkList { + if in == nil { + return nil + } + out := new(LinkList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LinkList) 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 *LinkSpec) DeepCopyInto(out *LinkSpec) { + *out = *in + out.ProbeSpec = in.ProbeSpec + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.RemoteDiscoverySelector != nil { + in, out := &in.RemoteDiscoverySelector, &out.RemoteDiscoverySelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.FederatedServiceSelector != nil { + in, out := &in.FederatedServiceSelector, &out.FederatedServiceSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LinkSpec. +func (in *LinkSpec) DeepCopy() *LinkSpec { + if in == nil { + return nil + } + out := new(LinkSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LinkStatus) DeepCopyInto(out *LinkStatus) { + *out = *in + if in.MirrorServices != nil { + in, out := &in.MirrorServices, &out.MirrorServices + *out = make([]ServiceStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.FederatedServices != nil { + in, out := &in.FederatedServices, &out.FederatedServices + *out = make([]ServiceStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LinkStatus. +func (in *LinkStatus) DeepCopy() *LinkStatus { + if in == nil { + return nil + } + out := new(LinkStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectRef) DeepCopyInto(out *ObjectRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectRef. +func (in *ObjectRef) DeepCopy() *ObjectRef { + if in == nil { + return nil + } + out := new(ObjectRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProbeSpec) DeepCopyInto(out *ProbeSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProbeSpec. +func (in *ProbeSpec) DeepCopy() *ProbeSpec { + if in == nil { + return nil + } + out := new(ProbeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceStatus) DeepCopyInto(out *ServiceStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]LinkCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.RemoteRef = in.RemoteRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceStatus. +func (in *ServiceStatus) DeepCopy() *ServiceStatus { + if in == nil { + return nil + } + out := new(ServiceStatus) + in.DeepCopyInto(out) + return out +} diff --git a/controller/gen/client/clientset/versioned/clientset.go b/controller/gen/client/clientset/versioned/clientset.go index 3705884643e53..ace3be65abf9d 100644 --- a/controller/gen/client/clientset/versioned/clientset.go +++ b/controller/gen/client/clientset/versioned/clientset.go @@ -24,6 +24,7 @@ import ( externalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1" linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha1" + linkv1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha2" policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1alpha1" policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1beta3" serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/server/v1beta1" @@ -40,6 +41,7 @@ type Interface interface { Discovery() discovery.DiscoveryInterface ExternalworkloadV1beta1() externalworkloadv1beta1.ExternalworkloadV1beta1Interface LinkV1alpha1() linkv1alpha1.LinkV1alpha1Interface + LinkV1alpha2() linkv1alpha2.LinkV1alpha2Interface PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface PolicyV1beta3() policyv1beta3.PolicyV1beta3Interface ServerV1beta1() serverv1beta1.ServerV1beta1Interface @@ -54,6 +56,7 @@ type Clientset struct { *discovery.DiscoveryClient externalworkloadV1beta1 *externalworkloadv1beta1.ExternalworkloadV1beta1Client linkV1alpha1 *linkv1alpha1.LinkV1alpha1Client + linkV1alpha2 *linkv1alpha2.LinkV1alpha2Client policyV1alpha1 *policyv1alpha1.PolicyV1alpha1Client policyV1beta3 *policyv1beta3.PolicyV1beta3Client serverV1beta1 *serverv1beta1.ServerV1beta1Client @@ -73,6 +76,11 @@ func (c *Clientset) LinkV1alpha1() linkv1alpha1.LinkV1alpha1Interface { return c.linkV1alpha1 } +// LinkV1alpha2 retrieves the LinkV1alpha2Client +func (c *Clientset) LinkV1alpha2() linkv1alpha2.LinkV1alpha2Interface { + return c.linkV1alpha2 +} + // PolicyV1alpha1 retrieves the PolicyV1alpha1Client func (c *Clientset) PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface { return c.policyV1alpha1 @@ -160,6 +168,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, if err != nil { return nil, err } + cs.linkV1alpha2, err = linkv1alpha2.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } cs.policyV1alpha1, err = policyv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err @@ -211,6 +223,7 @@ func New(c rest.Interface) *Clientset { var cs Clientset cs.externalworkloadV1beta1 = externalworkloadv1beta1.New(c) cs.linkV1alpha1 = linkv1alpha1.New(c) + cs.linkV1alpha2 = linkv1alpha2.New(c) cs.policyV1alpha1 = policyv1alpha1.New(c) cs.policyV1beta3 = policyv1beta3.New(c) cs.serverV1beta1 = serverv1beta1.New(c) diff --git a/controller/gen/client/clientset/versioned/fake/clientset_generated.go b/controller/gen/client/clientset/versioned/fake/clientset_generated.go index 39db6f01e40fe..c39e89131f868 100644 --- a/controller/gen/client/clientset/versioned/fake/clientset_generated.go +++ b/controller/gen/client/clientset/versioned/fake/clientset_generated.go @@ -24,6 +24,8 @@ import ( fakeexternalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/externalworkload/v1beta1/fake" linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha1" fakelinkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha1/fake" + linkv1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha2" + fakelinkv1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake" policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1alpha1" fakepolicyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1alpha1/fake" policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/policy/v1beta3" @@ -109,6 +111,11 @@ func (c *Clientset) LinkV1alpha1() linkv1alpha1.LinkV1alpha1Interface { return &fakelinkv1alpha1.FakeLinkV1alpha1{Fake: &c.Fake} } +// LinkV1alpha2 retrieves the LinkV1alpha2Client +func (c *Clientset) LinkV1alpha2() linkv1alpha2.LinkV1alpha2Interface { + return &fakelinkv1alpha2.FakeLinkV1alpha2{Fake: &c.Fake} +} + // PolicyV1alpha1 retrieves the PolicyV1alpha1Client func (c *Clientset) PolicyV1alpha1() policyv1alpha1.PolicyV1alpha1Interface { return &fakepolicyv1alpha1.FakePolicyV1alpha1{Fake: &c.Fake} diff --git a/controller/gen/client/clientset/versioned/fake/register.go b/controller/gen/client/clientset/versioned/fake/register.go index 13a59e01c238f..2cf18a8461aaf 100644 --- a/controller/gen/client/clientset/versioned/fake/register.go +++ b/controller/gen/client/clientset/versioned/fake/register.go @@ -21,6 +21,7 @@ package fake import ( externalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1" linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1" + linkv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1" policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3" serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1" @@ -41,6 +42,7 @@ var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ externalworkloadv1beta1.AddToScheme, linkv1alpha1.AddToScheme, + linkv1alpha2.AddToScheme, policyv1alpha1.AddToScheme, policyv1beta3.AddToScheme, serverv1beta1.AddToScheme, diff --git a/controller/gen/client/clientset/versioned/scheme/register.go b/controller/gen/client/clientset/versioned/scheme/register.go index 28c8441d16c5b..f8a522712a6ba 100644 --- a/controller/gen/client/clientset/versioned/scheme/register.go +++ b/controller/gen/client/clientset/versioned/scheme/register.go @@ -21,6 +21,7 @@ package scheme import ( externalworkloadv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1" linkv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1" + linkv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1" policyv1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3" serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1" @@ -41,6 +42,7 @@ var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ externalworkloadv1beta1.AddToScheme, linkv1alpha1.AddToScheme, + linkv1alpha2.AddToScheme, policyv1alpha1.AddToScheme, policyv1beta3.AddToScheme, serverv1beta1.AddToScheme, diff --git a/controller/gen/client/clientset/versioned/typed/link/v1alpha2/doc.go b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/doc.go new file mode 100644 index 0000000000000..baaf2d9853708 --- /dev/null +++ b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha2 diff --git a/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/doc.go b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/doc.go new file mode 100644 index 0000000000000..16f44399065ed --- /dev/null +++ b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/fake_link.go b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/fake_link.go new file mode 100644 index 0000000000000..1b00e4aee998a --- /dev/null +++ b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/fake_link.go @@ -0,0 +1,134 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeLinks implements LinkInterface +type FakeLinks struct { + Fake *FakeLinkV1alpha2 + ns string +} + +var linksResource = v1alpha2.SchemeGroupVersion.WithResource("links") + +var linksKind = v1alpha2.SchemeGroupVersion.WithKind("Link") + +// Get takes name of the link, and returns the corresponding link object, and an error if there is any. +func (c *FakeLinks) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.Link, err error) { + emptyResult := &v1alpha2.Link{} + obj, err := c.Fake. + Invokes(testing.NewGetActionWithOptions(linksResource, c.ns, name, options), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha2.Link), err +} + +// List takes label and field selectors, and returns the list of Links that match those selectors. +func (c *FakeLinks) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.LinkList, err error) { + emptyResult := &v1alpha2.LinkList{} + obj, err := c.Fake. + Invokes(testing.NewListActionWithOptions(linksResource, linksKind, c.ns, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.LinkList{ListMeta: obj.(*v1alpha2.LinkList).ListMeta} + for _, item := range obj.(*v1alpha2.LinkList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested links. +func (c *FakeLinks) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchActionWithOptions(linksResource, c.ns, opts)) + +} + +// Create takes the representation of a link and creates it. Returns the server's representation of the link, and an error, if there is any. +func (c *FakeLinks) Create(ctx context.Context, link *v1alpha2.Link, opts v1.CreateOptions) (result *v1alpha2.Link, err error) { + emptyResult := &v1alpha2.Link{} + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(linksResource, c.ns, link, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha2.Link), err +} + +// Update takes the representation of a link and updates it. Returns the server's representation of the link, and an error, if there is any. +func (c *FakeLinks) Update(ctx context.Context, link *v1alpha2.Link, opts v1.UpdateOptions) (result *v1alpha2.Link, err error) { + emptyResult := &v1alpha2.Link{} + obj, err := c.Fake. + Invokes(testing.NewUpdateActionWithOptions(linksResource, c.ns, link, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha2.Link), err +} + +// Delete takes name of the link and deletes it. Returns an error if one occurs. +func (c *FakeLinks) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(linksResource, c.ns, name, opts), &v1alpha2.Link{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeLinks) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionActionWithOptions(linksResource, c.ns, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha2.LinkList{}) + return err +} + +// Patch applies the patch and returns the patched link. +func (c *FakeLinks) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.Link, err error) { + emptyResult := &v1alpha2.Link{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(linksResource, c.ns, name, pt, data, opts, subresources...), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha2.Link), err +} diff --git a/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/fake_link_client.go b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/fake_link_client.go new file mode 100644 index 0000000000000..57376f52f5340 --- /dev/null +++ b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/fake/fake_link_client.go @@ -0,0 +1,40 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/typed/link/v1alpha2" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeLinkV1alpha2 struct { + *testing.Fake +} + +func (c *FakeLinkV1alpha2) Links(namespace string) v1alpha2.LinkInterface { + return &FakeLinks{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeLinkV1alpha2) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/controller/gen/client/clientset/versioned/typed/link/v1alpha2/generated_expansion.go b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/generated_expansion.go new file mode 100644 index 0000000000000..6e8cd598f9ee7 --- /dev/null +++ b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +type LinkExpansion interface{} diff --git a/controller/gen/client/clientset/versioned/typed/link/v1alpha2/link.go b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/link.go new file mode 100644 index 0000000000000..34ff796678301 --- /dev/null +++ b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/link.go @@ -0,0 +1,67 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + + v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" + scheme "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// LinksGetter has a method to return a LinkInterface. +// A group's client should implement this interface. +type LinksGetter interface { + Links(namespace string) LinkInterface +} + +// LinkInterface has methods to work with Link resources. +type LinkInterface interface { + Create(ctx context.Context, link *v1alpha2.Link, opts v1.CreateOptions) (*v1alpha2.Link, error) + Update(ctx context.Context, link *v1alpha2.Link, opts v1.UpdateOptions) (*v1alpha2.Link, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha2.Link, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha2.LinkList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.Link, err error) + LinkExpansion +} + +// links implements LinkInterface +type links struct { + *gentype.ClientWithList[*v1alpha2.Link, *v1alpha2.LinkList] +} + +// newLinks returns a Links +func newLinks(c *LinkV1alpha2Client, namespace string) *links { + return &links{ + gentype.NewClientWithList[*v1alpha2.Link, *v1alpha2.LinkList]( + "links", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *v1alpha2.Link { return &v1alpha2.Link{} }, + func() *v1alpha2.LinkList { return &v1alpha2.LinkList{} }), + } +} diff --git a/controller/gen/client/clientset/versioned/typed/link/v1alpha2/link_client.go b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/link_client.go new file mode 100644 index 0000000000000..6bbfade305278 --- /dev/null +++ b/controller/gen/client/clientset/versioned/typed/link/v1alpha2/link_client.go @@ -0,0 +1,107 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "net/http" + + v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" + "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type LinkV1alpha2Interface interface { + RESTClient() rest.Interface + LinksGetter +} + +// LinkV1alpha2Client is used to interact with features provided by the link group. +type LinkV1alpha2Client struct { + restClient rest.Interface +} + +func (c *LinkV1alpha2Client) Links(namespace string) LinkInterface { + return newLinks(c, namespace) +} + +// NewForConfig creates a new LinkV1alpha2Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*LinkV1alpha2Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new LinkV1alpha2Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*LinkV1alpha2Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &LinkV1alpha2Client{client}, nil +} + +// NewForConfigOrDie creates a new LinkV1alpha2Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *LinkV1alpha2Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new LinkV1alpha2Client for the given RESTClient. +func New(c rest.Interface) *LinkV1alpha2Client { + return &LinkV1alpha2Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha2.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *LinkV1alpha2Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/controller/gen/client/informers/externalversions/generic.go b/controller/gen/client/informers/externalversions/generic.go index d9a60362ba1f3..264f7cea42210 100644 --- a/controller/gen/client/informers/externalversions/generic.go +++ b/controller/gen/client/informers/externalversions/generic.go @@ -23,13 +23,14 @@ import ( v1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/externalworkload/v1beta1" v1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha1" + v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" policyv1alpha1 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1alpha1" v1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/policy/v1beta3" serverv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta1" v1beta2 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta2" serverv1beta3 "github.com/linkerd/linkerd2/controller/gen/apis/server/v1beta3" serverauthorizationv1beta1 "github.com/linkerd/linkerd2/controller/gen/apis/serverauthorization/v1beta1" - v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2" + serviceprofilev1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) @@ -68,8 +69,12 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case v1alpha1.SchemeGroupVersion.WithResource("links"): return &genericInformer{resource: resource.GroupResource(), informer: f.Link().V1alpha1().Links().Informer()}, nil + // Group=link, Version=v1alpha2 + case v1alpha2.SchemeGroupVersion.WithResource("links"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Link().V1alpha2().Links().Informer()}, nil + // Group=linkerd.io, Version=v1alpha2 - case v1alpha2.SchemeGroupVersion.WithResource("serviceprofiles"): + case serviceprofilev1alpha2.SchemeGroupVersion.WithResource("serviceprofiles"): return &genericInformer{resource: resource.GroupResource(), informer: f.Linkerd().V1alpha2().ServiceProfiles().Informer()}, nil // Group=policy, Version=v1alpha1 diff --git a/controller/gen/client/informers/externalversions/link/interface.go b/controller/gen/client/informers/externalversions/link/interface.go index dcc0c6e05e2f8..834dca7303e80 100644 --- a/controller/gen/client/informers/externalversions/link/interface.go +++ b/controller/gen/client/informers/externalversions/link/interface.go @@ -21,12 +21,15 @@ package link import ( internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces" v1alpha1 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/link/v1alpha1" + v1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/link/v1alpha2" ) // Interface provides access to each of this group's versions. type Interface interface { // V1alpha1 provides access to shared informers for resources in V1alpha1. V1alpha1() v1alpha1.Interface + // V1alpha2 provides access to shared informers for resources in V1alpha2. + V1alpha2() v1alpha2.Interface } type group struct { @@ -44,3 +47,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList func (g *group) V1alpha1() v1alpha1.Interface { return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) } + +// V1alpha2 returns a new v1alpha2.Interface. +func (g *group) V1alpha2() v1alpha2.Interface { + return v1alpha2.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/controller/gen/client/informers/externalversions/link/v1alpha2/interface.go b/controller/gen/client/informers/externalversions/link/v1alpha2/interface.go new file mode 100644 index 0000000000000..4bb7a5d4644b6 --- /dev/null +++ b/controller/gen/client/informers/externalversions/link/v1alpha2/interface.go @@ -0,0 +1,45 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // Links returns a LinkInformer. + Links() LinkInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// Links returns a LinkInformer. +func (v *version) Links() LinkInformer { + return &linkInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/controller/gen/client/informers/externalversions/link/v1alpha2/link.go b/controller/gen/client/informers/externalversions/link/v1alpha2/link.go new file mode 100644 index 0000000000000..e3c51384e6247 --- /dev/null +++ b/controller/gen/client/informers/externalversions/link/v1alpha2/link.go @@ -0,0 +1,90 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + time "time" + + linkv1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" + versioned "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned" + internalinterfaces "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions/internalinterfaces" + v1alpha2 "github.com/linkerd/linkerd2/controller/gen/client/listers/link/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// LinkInformer provides access to a shared informer and lister for +// Links. +type LinkInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.LinkLister +} + +type linkInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewLinkInformer constructs a new informer for Link type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewLinkInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredLinkInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredLinkInformer constructs a new informer for Link type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredLinkInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.LinkV1alpha2().Links(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.LinkV1alpha2().Links(namespace).Watch(context.TODO(), options) + }, + }, + &linkv1alpha2.Link{}, + resyncPeriod, + indexers, + ) +} + +func (f *linkInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredLinkInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *linkInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&linkv1alpha2.Link{}, f.defaultInformer) +} + +func (f *linkInformer) Lister() v1alpha2.LinkLister { + return v1alpha2.NewLinkLister(f.Informer().GetIndexer()) +} diff --git a/controller/gen/client/listers/link/v1alpha2/expansion_generated.go b/controller/gen/client/listers/link/v1alpha2/expansion_generated.go new file mode 100644 index 0000000000000..cc932de5896d9 --- /dev/null +++ b/controller/gen/client/listers/link/v1alpha2/expansion_generated.go @@ -0,0 +1,27 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +// LinkListerExpansion allows custom methods to be added to +// LinkLister. +type LinkListerExpansion interface{} + +// LinkNamespaceListerExpansion allows custom methods to be added to +// LinkNamespaceLister. +type LinkNamespaceListerExpansion interface{} diff --git a/controller/gen/client/listers/link/v1alpha2/link.go b/controller/gen/client/listers/link/v1alpha2/link.go new file mode 100644 index 0000000000000..2f53905425164 --- /dev/null +++ b/controller/gen/client/listers/link/v1alpha2/link.go @@ -0,0 +1,70 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/listers" + "k8s.io/client-go/tools/cache" +) + +// LinkLister helps list Links. +// All objects returned here must be treated as read-only. +type LinkLister interface { + // List lists all Links in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.Link, err error) + // Links returns an object that can list and get Links. + Links(namespace string) LinkNamespaceLister + LinkListerExpansion +} + +// linkLister implements the LinkLister interface. +type linkLister struct { + listers.ResourceIndexer[*v1alpha2.Link] +} + +// NewLinkLister returns a new LinkLister. +func NewLinkLister(indexer cache.Indexer) LinkLister { + return &linkLister{listers.New[*v1alpha2.Link](indexer, v1alpha2.Resource("link"))} +} + +// Links returns an object that can list and get Links. +func (s *linkLister) Links(namespace string) LinkNamespaceLister { + return linkNamespaceLister{listers.NewNamespaced[*v1alpha2.Link](s.ResourceIndexer, namespace)} +} + +// LinkNamespaceLister helps list and get Links. +// All objects returned here must be treated as read-only. +type LinkNamespaceLister interface { + // List lists all Links in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.Link, err error) + // Get retrieves the Link from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha2.Link, error) + LinkNamespaceListerExpansion +} + +// linkNamespaceLister implements the LinkNamespaceLister +// interface. +type linkNamespaceLister struct { + listers.ResourceIndexer[*v1alpha2.Link] +} diff --git a/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml b/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml index fe6b301b7d4c0..5e3125b1fa51a 100644 --- a/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml +++ b/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml @@ -58,7 +58,7 @@ rules: verbs: ["list", "get", "watch"] - apiGroups: ["multicluster.linkerd.io"] resources: ["links/status"] - verbs: ["update"] + verbs: ["update", "patch"] - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["create", "get", "update", "patch"] diff --git a/multicluster/cmd/check.go b/multicluster/cmd/check.go index fc5ef8a519c4a..2912b873f8fbb 100644 --- a/multicluster/cmd/check.go +++ b/multicluster/cmd/check.go @@ -11,10 +11,10 @@ import ( "strings" "time" + "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" pkgcmd "github.com/linkerd/linkerd2/pkg/cmd" "github.com/linkerd/linkerd2/pkg/healthcheck" "github.com/linkerd/linkerd2/pkg/k8s" - "github.com/linkerd/linkerd2/pkg/multicluster" "github.com/linkerd/linkerd2/pkg/servicemirror" "github.com/linkerd/linkerd2/pkg/tls" "github.com/linkerd/linkerd2/pkg/version" @@ -66,13 +66,13 @@ func (options *checkOptions) validate() error { type healthChecker struct { *healthcheck.HealthChecker - links []multicluster.Link + links []v1alpha2.Link } func newHealthChecker(linkerdHC *healthcheck.HealthChecker) *healthChecker { return &healthChecker{ linkerdHC, - []multicluster.Link{}, + []v1alpha2.Link{}, } } @@ -322,18 +322,18 @@ func (hc *healthChecker) linkAccess(ctx context.Context) error { } func (hc *healthChecker) checkLinks(ctx context.Context) error { - links, err := multicluster.GetLinks(ctx, hc.KubeAPIClient().DynamicClient) + links, err := hc.KubeAPIClient().L5dCrdClient.LinkV1alpha2().Links("").List(ctx, metav1.ListOptions{}) if err != nil { return err } - if len(links) == 0 { + if len(links.Items) == 0 { return healthcheck.SkipError{Reason: "no links detected"} } linkNames := []string{} - for _, l := range links { - linkNames = append(linkNames, fmt.Sprintf("\t* %s", l.TargetClusterName)) + for _, l := range links.Items { + linkNames = append(linkNames, fmt.Sprintf("\t* %s", l.Spec.TargetClusterName)) } - hc.links = links + hc.links = links.Items return healthcheck.VerboseSuccess{Message: strings.Join(linkNames, "\n")} } @@ -341,15 +341,15 @@ func (hc *healthChecker) checkLinkVersions() error { errors := []error{} links := []string{} for _, link := range hc.links { - parts := strings.Split(link.CreatedBy, " ") + parts := strings.Split(link.Annotations[k8s.CreatedByAnnotation], " ") if len(parts) == 2 && parts[0] == "linkerd/cli" { if parts[1] == version.Version { - links = append(links, fmt.Sprintf("\t* %s", link.TargetClusterName)) + links = append(links, fmt.Sprintf("\t* %s", link.Spec.TargetClusterName)) } else { - errors = append(errors, fmt.Errorf("* %s: CLI version is %s but Link version is %s", link.TargetClusterName, version.Version, parts[1])) + errors = append(errors, fmt.Errorf("* %s: CLI version is %s but Link version is %s", link.Spec.TargetClusterName, version.Version, parts[1])) } } else { - errors = append(errors, fmt.Errorf("* %s: unable to determine version", link.TargetClusterName)) + errors = append(errors, fmt.Errorf("* %s: unable to determine version", link.Spec.TargetClusterName)) } } if len(errors) > 0 { @@ -366,9 +366,9 @@ func (hc *healthChecker) checkRemoteClusterConnectivity(ctx context.Context) err links := []string{} for _, link := range hc.links { // Load the credentials secret - secret, err := hc.KubeAPIClient().Interface.CoreV1().Secrets(link.Namespace).Get(ctx, link.ClusterCredentialsSecret, metav1.GetOptions{}) + secret, err := hc.KubeAPIClient().Interface.CoreV1().Secrets(link.Namespace).Get(ctx, link.Spec.ClusterCredentialsSecret, metav1.GetOptions{}) if err != nil { - errors = append(errors, fmt.Errorf("* secret: [%s/%s]: %w", link.Namespace, link.ClusterCredentialsSecret, err)) + errors = append(errors, fmt.Errorf("* secret: [%s/%s]: %w", link.Namespace, link.Spec.ClusterCredentialsSecret, err)) continue } config, err := servicemirror.ParseRemoteClusterSecret(secret) @@ -378,27 +378,27 @@ func (hc *healthChecker) checkRemoteClusterConnectivity(ctx context.Context) err } clientConfig, err := clientcmd.RESTConfigFromKubeConfig(config) if err != nil { - errors = append(errors, fmt.Errorf("* secret: [%s/%s] cluster: [%s]: unable to parse api config: %w", secret.Namespace, secret.Name, link.TargetClusterName, err)) + errors = append(errors, fmt.Errorf("* secret: [%s/%s] cluster: [%s]: unable to parse api config: %w", secret.Namespace, secret.Name, link.Spec.TargetClusterName, err)) continue } remoteAPI, err := k8s.NewAPIForConfig(clientConfig, "", []string{}, healthcheck.RequestTimeout, 0, 0) if err != nil { - errors = append(errors, fmt.Errorf("* secret: [%s/%s] cluster: [%s]: could not instantiate api for target cluster: %w", secret.Namespace, secret.Name, link.TargetClusterName, err)) + errors = append(errors, fmt.Errorf("* secret: [%s/%s] cluster: [%s]: could not instantiate api for target cluster: %w", secret.Namespace, secret.Name, link.Spec.TargetClusterName, err)) continue } // We use this call just to check connectivity. _, err = remoteAPI.Discovery().ServerVersion() if err != nil { - errors = append(errors, fmt.Errorf("* failed to connect to API for cluster: [%s]: %w", link.TargetClusterName, err)) + errors = append(errors, fmt.Errorf("* failed to connect to API for cluster: [%s]: %w", link.Spec.TargetClusterName, err)) continue } verbs := []string{"get", "list", "watch"} for _, verb := range verbs { if err := healthcheck.CheckCanPerformAction(ctx, remoteAPI, verb, corev1.NamespaceAll, "", "v1", "services"); err != nil { - errors = append(errors, fmt.Errorf("* missing service permission [%s] for cluster [%s]: %w", verb, link.TargetClusterName, err)) + errors = append(errors, fmt.Errorf("* missing service permission [%s] for cluster [%s]: %w", verb, link.Spec.TargetClusterName, err)) } } - links = append(links, fmt.Sprintf("\t* %s", link.TargetClusterName)) + links = append(links, fmt.Sprintf("\t* %s", link.Spec.TargetClusterName)) } if len(errors) > 0 { return joinErrors(errors, 2) @@ -414,9 +414,9 @@ func (hc *healthChecker) checkRemoteClusterAnchors(ctx context.Context, localAnc links := []string{} for _, link := range hc.links { // Load the credentials secret - secret, err := hc.KubeAPIClient().Interface.CoreV1().Secrets(link.Namespace).Get(ctx, link.ClusterCredentialsSecret, metav1.GetOptions{}) + secret, err := hc.KubeAPIClient().Interface.CoreV1().Secrets(link.Namespace).Get(ctx, link.Spec.ClusterCredentialsSecret, metav1.GetOptions{}) if err != nil { - errors = append(errors, fmt.Sprintf("* secret: [%s/%s]: %s", link.Namespace, link.ClusterCredentialsSecret, err)) + errors = append(errors, fmt.Sprintf("* secret: [%s/%s]: %s", link.Namespace, link.Spec.ClusterCredentialsSecret, err)) continue } config, err := servicemirror.ParseRemoteClusterSecret(secret) @@ -426,29 +426,29 @@ func (hc *healthChecker) checkRemoteClusterAnchors(ctx context.Context, localAnc } clientConfig, err := clientcmd.RESTConfigFromKubeConfig(config) if err != nil { - errors = append(errors, fmt.Sprintf("* secret: [%s/%s] cluster: [%s]: unable to parse api config: %s", secret.Namespace, secret.Name, link.TargetClusterName, err)) + errors = append(errors, fmt.Sprintf("* secret: [%s/%s] cluster: [%s]: unable to parse api config: %s", secret.Namespace, secret.Name, link.Spec.TargetClusterName, err)) continue } remoteAPI, err := k8s.NewAPIForConfig(clientConfig, "", []string{}, healthcheck.RequestTimeout, 0, 0) if err != nil { - errors = append(errors, fmt.Sprintf("* secret: [%s/%s] cluster: [%s]: could not instantiate api for target cluster: %s", secret.Namespace, secret.Name, link.TargetClusterName, err)) + errors = append(errors, fmt.Sprintf("* secret: [%s/%s] cluster: [%s]: could not instantiate api for target cluster: %s", secret.Namespace, secret.Name, link.Spec.TargetClusterName, err)) continue } - _, values, err := healthcheck.FetchCurrentConfiguration(ctx, remoteAPI, link.TargetClusterLinkerdNamespace) + _, values, err := healthcheck.FetchCurrentConfiguration(ctx, remoteAPI, link.Spec.TargetClusterLinkerdNamespace) if err != nil { - errors = append(errors, fmt.Sprintf("* %s: unable to fetch anchors: %s", link.TargetClusterName, err)) + errors = append(errors, fmt.Sprintf("* %s: unable to fetch anchors: %s", link.Spec.TargetClusterName, err)) continue } remoteAnchors, err := tls.DecodePEMCertificates(values.IdentityTrustAnchorsPEM) if err != nil { - errors = append(errors, fmt.Sprintf("* %s: cannot parse trust anchors", link.TargetClusterName)) + errors = append(errors, fmt.Sprintf("* %s: cannot parse trust anchors", link.Spec.TargetClusterName)) continue } // we fail early if the lens are not the same. If they are the // same, we can only compare certs one way and be sure we have // identical anchors if len(remoteAnchors) != len(localAnchors) { - errors = append(errors, fmt.Sprintf("* %s", link.TargetClusterName)) + errors = append(errors, fmt.Sprintf("* %s", link.Spec.TargetClusterName)) continue } localAnchorsMap := make(map[string]*x509.Certificate) @@ -458,11 +458,11 @@ func (hc *healthChecker) checkRemoteClusterAnchors(ctx context.Context, localAnc for _, remote := range remoteAnchors { local, ok := localAnchorsMap[string(remote.Signature)] if !ok || !local.Equal(remote) { - errors = append(errors, fmt.Sprintf("* %s", link.TargetClusterName)) + errors = append(errors, fmt.Sprintf("* %s", link.Spec.TargetClusterName)) break } } - links = append(links, fmt.Sprintf("\t* %s", link.TargetClusterName)) + links = append(links, fmt.Sprintf("\t* %s", link.Spec.TargetClusterName)) } if len(errors) > 0 { return fmt.Errorf("Problematic clusters:\n %s", strings.Join(errors, "\n ")) @@ -480,9 +480,9 @@ func (hc *healthChecker) checkServiceMirrorLocalRBAC(ctx context.Context) error err := healthcheck.CheckServiceAccounts( ctx, hc.KubeAPIClient(), - []string{fmt.Sprintf(linkerdServiceMirrorServiceAccountName, link.TargetClusterName)}, + []string{fmt.Sprintf(linkerdServiceMirrorServiceAccountName, link.Spec.TargetClusterName)}, link.Namespace, - serviceMirrorComponentsSelector(link.TargetClusterName), + serviceMirrorComponentsSelector(link.Spec.TargetClusterName), ) if err != nil { messages = append(messages, err.Error()) @@ -491,8 +491,8 @@ func (hc *healthChecker) checkServiceMirrorLocalRBAC(ctx context.Context) error ctx, hc.KubeAPIClient(), true, - []string{fmt.Sprintf(linkerdServiceMirrorClusterRoleName, link.TargetClusterName)}, - serviceMirrorComponentsSelector(link.TargetClusterName), + []string{fmt.Sprintf(linkerdServiceMirrorClusterRoleName, link.Spec.TargetClusterName)}, + serviceMirrorComponentsSelector(link.Spec.TargetClusterName), ) if err != nil { messages = append(messages, err.Error()) @@ -501,8 +501,8 @@ func (hc *healthChecker) checkServiceMirrorLocalRBAC(ctx context.Context) error ctx, hc.KubeAPIClient(), true, - []string{fmt.Sprintf(linkerdServiceMirrorClusterRoleName, link.TargetClusterName)}, - serviceMirrorComponentsSelector(link.TargetClusterName), + []string{fmt.Sprintf(linkerdServiceMirrorClusterRoleName, link.Spec.TargetClusterName)}, + serviceMirrorComponentsSelector(link.Spec.TargetClusterName), ) if err != nil { messages = append(messages, err.Error()) @@ -512,8 +512,8 @@ func (hc *healthChecker) checkServiceMirrorLocalRBAC(ctx context.Context) error hc.KubeAPIClient(), true, link.Namespace, - []string{fmt.Sprintf(linkerdServiceMirrorRoleName, link.TargetClusterName)}, - serviceMirrorComponentsSelector(link.TargetClusterName), + []string{fmt.Sprintf(linkerdServiceMirrorRoleName, link.Spec.TargetClusterName)}, + serviceMirrorComponentsSelector(link.Spec.TargetClusterName), ) if err != nil { messages = append(messages, err.Error()) @@ -523,13 +523,13 @@ func (hc *healthChecker) checkServiceMirrorLocalRBAC(ctx context.Context) error hc.KubeAPIClient(), true, link.Namespace, - []string{fmt.Sprintf(linkerdServiceMirrorRoleName, link.TargetClusterName)}, - serviceMirrorComponentsSelector(link.TargetClusterName), + []string{fmt.Sprintf(linkerdServiceMirrorRoleName, link.Spec.TargetClusterName)}, + serviceMirrorComponentsSelector(link.Spec.TargetClusterName), ) if err != nil { messages = append(messages, err.Error()) } - links = append(links, fmt.Sprintf("\t* %s", link.TargetClusterName)) + links = append(links, fmt.Sprintf("\t* %s", link.Spec.TargetClusterName)) } if len(messages) > 0 { return errors.New(strings.Join(messages, "\n")) @@ -545,18 +545,18 @@ func (hc *healthChecker) checkServiceMirrorController(ctx context.Context) error clusterNames := []string{} for _, link := range hc.links { options := metav1.ListOptions{ - LabelSelector: serviceMirrorComponentsSelector(link.TargetClusterName), + LabelSelector: serviceMirrorComponentsSelector(link.Spec.TargetClusterName), } result, err := hc.KubeAPIClient().AppsV1().Deployments(corev1.NamespaceAll).List(ctx, options) if err != nil { return err } if len(result.Items) > 1 { - errors = append(errors, fmt.Errorf("* too many service mirror controller deployments for Link %s", link.TargetClusterName)) + errors = append(errors, fmt.Errorf("* too many service mirror controller deployments for Link %s", link.Spec.TargetClusterName)) continue } if len(result.Items) == 0 { - errors = append(errors, fmt.Errorf("* no service mirror controller deployment for Link %s", link.TargetClusterName)) + errors = append(errors, fmt.Errorf("* no service mirror controller deployment for Link %s", link.Spec.TargetClusterName)) continue } controller := result.Items[0] @@ -564,7 +564,7 @@ func (hc *healthChecker) checkServiceMirrorController(ctx context.Context) error errors = append(errors, fmt.Errorf("* service mirror controller is not available: %s/%s", controller.Namespace, controller.Name)) continue } - clusterNames = append(clusterNames, fmt.Sprintf("\t* %s", link.TargetClusterName)) + clusterNames = append(clusterNames, fmt.Sprintf("\t* %s", link.Spec.TargetClusterName)) } if len(errors) > 0 { return joinErrors(errors, 2) @@ -587,19 +587,19 @@ func (hc *healthChecker) checkIfGatewayMirrorsHaveEndpoints(ctx context.Context, // When linked against a cluster without a gateway, there will be no // gateway address and no probe spec initialised. In such cases, skip // the check - if link.GatewayAddress == "" || link.ProbeSpec.Path == "" { + if link.Spec.GatewayAddress == "" || link.Spec.ProbeSpec.Path == "" { continue } // Check that each gateway probe service has endpoints. - selector := metav1.ListOptions{LabelSelector: fmt.Sprintf("%s,%s=%s", k8s.MirroredGatewayLabel, k8s.RemoteClusterNameLabel, link.TargetClusterName)} + selector := metav1.ListOptions{LabelSelector: fmt.Sprintf("%s,%s=%s", k8s.MirroredGatewayLabel, k8s.RemoteClusterNameLabel, link.Spec.TargetClusterName)} gatewayMirrors, err := hc.KubeAPIClient().CoreV1().Services(metav1.NamespaceAll).List(ctx, selector) if err != nil { errors = append(errors, err) continue } if len(gatewayMirrors.Items) != 1 { - errors = append(errors, fmt.Errorf("wrong number (%d) of probe gateways for target cluster %s", len(gatewayMirrors.Items), link.TargetClusterName)) + errors = append(errors, fmt.Errorf("wrong number (%d) of probe gateways for target cluster %s", len(gatewayMirrors.Items), link.Spec.TargetClusterName)) continue } svc := gatewayMirrors.Items[0] @@ -611,16 +611,16 @@ func (hc *healthChecker) checkIfGatewayMirrorsHaveEndpoints(ctx context.Context, // Get the service mirror component in the linkerd-multicluster // namespace which corresponds to the current link. - selector = metav1.ListOptions{LabelSelector: fmt.Sprintf("component=linkerd-service-mirror,mirror.linkerd.io/cluster-name=%s", link.TargetClusterName)} + selector = metav1.ListOptions{LabelSelector: fmt.Sprintf("component=linkerd-service-mirror,mirror.linkerd.io/cluster-name=%s", link.Spec.TargetClusterName)} pods, err := hc.KubeAPIClient().CoreV1().Pods(multiclusterNs.Name).List(ctx, selector) if err != nil { - errors = append(errors, fmt.Errorf("failed to get the service-mirror component for target cluster %s: %w", link.TargetClusterName, err)) + errors = append(errors, fmt.Errorf("failed to get the service-mirror component for target cluster %s: %w", link.Spec.TargetClusterName, err)) continue } - lease, err := hc.KubeAPIClient().CoordinationV1().Leases(multiclusterNs.Name).Get(ctx, fmt.Sprintf("service-mirror-write-%s", link.TargetClusterName), metav1.GetOptions{}) + lease, err := hc.KubeAPIClient().CoordinationV1().Leases(multiclusterNs.Name).Get(ctx, fmt.Sprintf("service-mirror-write-%s", link.Spec.TargetClusterName), metav1.GetOptions{}) if err != nil { - errors = append(errors, fmt.Errorf("failed to get the service-mirror component Lease for target cluster %s: %w", link.TargetClusterName, err)) + errors = append(errors, fmt.Errorf("failed to get the service-mirror component Lease for target cluster %s: %w", link.Spec.TargetClusterName, err)) continue } @@ -634,23 +634,23 @@ func (hc *healthChecker) checkIfGatewayMirrorsHaveEndpoints(ctx context.Context, // information. gatewayMetrics := getGatewayMetrics(hc.KubeAPIClient(), pods.Items, leaders, wait) if len(gatewayMetrics) != 1 { - errors = append(errors, fmt.Errorf("expected exactly one gateway metric for target cluster %s; got %d", link.TargetClusterName, len(gatewayMetrics))) + errors = append(errors, fmt.Errorf("expected exactly one gateway metric for target cluster %s; got %d", link.Spec.TargetClusterName, len(gatewayMetrics))) continue } var metricsParser expfmt.TextParser parsedMetrics, err := metricsParser.TextToMetricFamilies(bytes.NewReader(gatewayMetrics[0].metrics)) if err != nil { - errors = append(errors, fmt.Errorf("failed to parse gateway metrics for target cluster %s: %w", link.TargetClusterName, err)) + errors = append(errors, fmt.Errorf("failed to parse gateway metrics for target cluster %s: %w", link.Spec.TargetClusterName, err)) continue } // Ensure the gateway for the current link is alive. for _, metrics := range parsedMetrics["gateway_alive"].GetMetric() { - if !isTargetClusterMetric(metrics, link.TargetClusterName) { + if !isTargetClusterMetric(metrics, link.Spec.TargetClusterName) { continue } if metrics.GetGauge().GetValue() != 1 { - err = fmt.Errorf("liveness checks failed for %s", link.TargetClusterName) + err = fmt.Errorf("liveness checks failed for %s", link.Spec.TargetClusterName) } break } @@ -658,7 +658,7 @@ func (hc *healthChecker) checkIfGatewayMirrorsHaveEndpoints(ctx context.Context, errors = append(errors, err) continue } - links = append(links, fmt.Sprintf("\t* %s", link.TargetClusterName)) + links = append(links, fmt.Sprintf("\t* %s", link.Spec.TargetClusterName)) } if len(errors) > 0 { return joinErrors(errors, 1) @@ -706,15 +706,15 @@ func (hc *healthChecker) checkForOrphanedServices(ctx context.Context) error { if err != nil { return err } - links, err := multicluster.GetLinks(ctx, hc.KubeAPIClient().DynamicClient) + links, err := hc.KubeAPIClient().L5dCrdClient.LinkV1alpha2().Links("").List(ctx, metav1.ListOptions{}) if err != nil { return err } for _, svc := range mirrorServices.Items { targetCluster := svc.Labels[k8s.RemoteClusterNameLabel] hasLink := false - for _, link := range links { - if link.TargetClusterName == targetCluster { + for _, link := range links.Items { + if link.Spec.TargetClusterName == targetCluster { hasLink = true break } diff --git a/multicluster/cmd/link.go b/multicluster/cmd/link.go index ae9e79f357866..045d2d5070578 100644 --- a/multicluster/cmd/link.go +++ b/multicluster/cmd/link.go @@ -9,6 +9,7 @@ import ( "path" "strings" + "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" "github.com/linkerd/linkerd2/multicluster/static" multicluster "github.com/linkerd/linkerd2/multicluster/values" "github.com/linkerd/linkerd2/pkg/charts" @@ -16,7 +17,6 @@ import ( pkgcmd "github.com/linkerd/linkerd2/pkg/cmd" "github.com/linkerd/linkerd2/pkg/flags" "github.com/linkerd/linkerd2/pkg/k8s" - mc "github.com/linkerd/linkerd2/pkg/multicluster" "github.com/linkerd/linkerd2/pkg/version" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -243,15 +243,23 @@ A full list of configurable values can be found at https://github.com/linkerd/li return err } - link := mc.Link{ - Name: opts.clusterName, - Namespace: opts.namespace, - TargetClusterName: opts.clusterName, - TargetClusterDomain: configMap.ClusterDomain, - TargetClusterLinkerdNamespace: controlPlaneNamespace, - ClusterCredentialsSecret: fmt.Sprintf("cluster-credentials-%s", opts.clusterName), - RemoteDiscoverySelector: remoteDiscoverySelector, - FederatedServiceSelector: federatedServiceSelector, + link := v1alpha2.Link{ + TypeMeta: metav1.TypeMeta{Kind: "Link", APIVersion: "multicluster.linkerd.io/v1alpha2"}, + ObjectMeta: metav1.ObjectMeta{ + Name: opts.clusterName, + Namespace: opts.namespace, + Annotations: map[string]string{ + k8s.CreatedByAnnotation: k8s.CreatedByAnnotationValue(), + }, + }, + Spec: v1alpha2.LinkSpec{ + TargetClusterName: opts.clusterName, + TargetClusterDomain: configMap.ClusterDomain, + TargetClusterLinkerdNamespace: controlPlaneNamespace, + ClusterCredentialsSecret: fmt.Sprintf("cluster-credentials-%s", opts.clusterName), + RemoteDiscoverySelector: remoteDiscoverySelector, + FederatedServiceSelector: federatedServiceSelector, + }, } // If there is a gateway in the exporting cluster, populate Link @@ -275,9 +283,9 @@ A full list of configurable values can be found at https://github.com/linkerd/li } if opts.gatewayAddresses != "" { - link.GatewayAddress = opts.gatewayAddresses + link.Spec.GatewayAddress = opts.gatewayAddresses } else if len(gwAddresses) > 0 { - link.GatewayAddress = strings.Join(gwAddresses, ",") + link.Spec.GatewayAddress = strings.Join(gwAddresses, ",") } else { return fmt.Errorf("Gateway %s.%s has no ingress addresses", gateway.Name, gateway.Namespace) } @@ -286,13 +294,13 @@ A full list of configurable values can be found at https://github.com/linkerd/li if !ok || gatewayIdentity == "" { return fmt.Errorf("Gateway %s.%s has no %s annotation", gateway.Name, gateway.Namespace, k8s.GatewayIdentity) } - link.GatewayIdentity = gatewayIdentity + link.Spec.GatewayIdentity = gatewayIdentity - probeSpec, err := mc.ExtractProbeSpec(gateway) + probeSpec, err := extractProbeSpec(gateway) if err != nil { return err } - link.ProbeSpec = probeSpec + link.Spec.ProbeSpec = probeSpec gatewayPort, err := extractGatewayPort(gateway) if err != nil { @@ -303,27 +311,22 @@ A full list of configurable values can be found at https://github.com/linkerd/li if opts.gatewayPort != 0 { gatewayPort = opts.gatewayPort } - link.GatewayPort = gatewayPort + link.Spec.GatewayPort = fmt.Sprintf("%d", gatewayPort) - link.Selector, err = metav1.ParseToLabelSelector(opts.selector) + link.Spec.Selector, err = metav1.ParseToLabelSelector(opts.selector) if err != nil { return err } } - obj, err := link.ToUnstructured() - if err != nil { - return err - } - var linkOut []byte if opts.output == "yaml" { - linkOut, err = yaml.Marshal(obj.Object) + linkOut, err = yaml.Marshal(link) if err != nil { return err } } else if opts.output == "json" { - linkOut, err = json.Marshal(obj.Object) + linkOut, err = json.Marshal(link) if err != nil { return err } @@ -580,3 +583,37 @@ func extractSAToken(secrets []corev1.Secret, saName string) (string, error) { return "", fmt.Errorf("could not find service account token secret for %s", saName) } + +// ExtractProbeSpec parses the ProbSpec from a gateway service's annotations. +// For now we're not including the failureThreshold and timeout fields which +// are new since edge-24.9.3, to avoid errors when attempting to apply them in +// clusters with an older Link CRD. +func extractProbeSpec(gateway *corev1.Service) (v1alpha2.ProbeSpec, error) { + path := gateway.Annotations[k8s.GatewayProbePath] + if path == "" { + return v1alpha2.ProbeSpec{}, errors.New("probe path is empty") + } + + port, err := extractPort(gateway.Spec, k8s.ProbePortName) + if err != nil { + return v1alpha2.ProbeSpec{}, err + } + + return v1alpha2.ProbeSpec{ + Path: path, + Port: fmt.Sprintf("%d", port), + Period: gateway.Annotations[k8s.GatewayProbePeriod], + }, nil +} + +func extractPort(spec corev1.ServiceSpec, portName string) (uint32, error) { + for _, p := range spec.Ports { + if p.Name == portName { + if spec.Type == "NodePort" { + return uint32(p.NodePort), nil + } + return uint32(p.Port), nil + } + } + return 0, fmt.Errorf("could not find port with name %s", portName) +} diff --git a/multicluster/cmd/service-mirror/main.go b/multicluster/cmd/service-mirror/main.go index a20558885bb1e..39a094d2d37c0 100644 --- a/multicluster/cmd/service-mirror/main.go +++ b/multicluster/cmd/service-mirror/main.go @@ -9,17 +9,19 @@ import ( "syscall" "time" + "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" + l5dcrdclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned" + l5dcrdinformer "github.com/linkerd/linkerd2/controller/gen/client/informers/externalversions" controllerK8s "github.com/linkerd/linkerd2/controller/k8s" servicemirror "github.com/linkerd/linkerd2/multicluster/service-mirror" "github.com/linkerd/linkerd2/pkg/admin" "github.com/linkerd/linkerd2/pkg/flags" "github.com/linkerd/linkerd2/pkg/k8s" - "github.com/linkerd/linkerd2/pkg/multicluster" sm "github.com/linkerd/linkerd2/pkg/servicemirror" log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - dynamic "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" @@ -83,8 +85,8 @@ func Main(args []string) { }() // We create two different kubernetes API clients for the local cluster: - // k8sAPI is used as a dynamic client for unstructured access to Link custom - // resources. + // l5dClient is used for watching Link resources and updating their + // statuses. // // controllerK8sAPI is used by the cluster watcher to manage // mirror resources such as services, namespaces, and endpoints. @@ -101,6 +103,14 @@ func Main(args []string) { if err != nil { log.Fatalf("Failed to initialize K8s API: %s", err) } + config, err := k8s.GetConfig(*kubeConfigPath, "") + if err != nil { + log.Fatalf("error configuring Kubernetes API client: %s", err) + } + l5dClient, err := controllerK8s.NewL5DCRDClient(config) + if err != nil { + log.Fatalf("Failed to initialize K8s API: %s", err) + } metrics := servicemirror.NewProbeMetricVecs() controllerK8sAPI.Sync(nil) @@ -110,7 +120,7 @@ func Main(args []string) { if *localMirror { run = func(ctx context.Context) { - err = startLocalClusterWatcher(ctx, *namespace, controllerK8sAPI, *requeueLimit, *repairPeriod, *enableHeadlessSvc, *enableNamespaceCreation, *federatedServiceSelector) + err = startLocalClusterWatcher(ctx, *namespace, controllerK8sAPI, l5dClient, *requeueLimit, *repairPeriod, *enableHeadlessSvc, *enableNamespaceCreation, *federatedServiceSelector) if err != nil { log.Fatalf("Failed to start local cluster watcher: %s", err) } @@ -126,75 +136,106 @@ func Main(args []string) { cleanupWorkers() } } else { - k8sAPI, err := k8s.NewAPI(*kubeConfigPath, "", "", []string{}, 0) - //TODO: Use can-i to check for required permissions - if err != nil { - log.Fatalf("Failed to initialize K8s API: %s", err) - } - linkClient := k8sAPI.DynamicClient.Resource(multicluster.LinkGVR).Namespace(*namespace) - run = func(ctx context.Context) { - main: - for { - // Start link watch - linkWatch, err := linkClient.Watch(ctx, metav1.ListOptions{}) - if err != nil { - log.Fatalf("Failed to watch Link %s: %s", linkName, err) - } - results := linkWatch.ResultChan() - - // Each time the link resource is updated, reload the config and restart the - // cluster watcher. - for { - select { - // ctx.Done() is a one-shot channel that will be closed once - // the context has been cancelled. Receiving from a closed - // channel yields the value immediately. - case <-ctx.Done(): - // The channel will be closed by the leader elector when a - // lease is lost, or by a background task handling SIGTERM. - // Before terminating the loop, stop the workers and set - // them to nil to release memory. - cleanupWorkers() + // Use a small buffered channel for Link updates to avoid dropping + // updates if there is an update burst. + results := make(chan *v1alpha2.Link, 100) + informerFactory := l5dcrdinformer.NewSharedInformerFactoryWithOptions( + l5dClient, + controllerK8s.ResyncTime, + l5dcrdinformer.WithNamespace(*namespace), + ) + informer := informerFactory.Link().V1alpha2().Links().Informer() + log.Infof("Starting Link informer") + informerFactory.Start(ctx.Done()) + + _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + link, ok := obj.(*v1alpha2.Link) + if !ok { + log.Errorf("object is not a Link: %+v", obj) return - case event, ok := <-results: + } + if link.GetName() == linkName { + select { + case results <- link: + default: + log.Errorf("Link update dropped (queue full): %s", link.GetName()) + } + } + }, + UpdateFunc: func(_, obj interface{}) { + link, ok := obj.(*v1alpha2.Link) + if !ok { + log.Errorf("object is not a Link: %+v", obj) + return + } + if link.GetName() == linkName { + select { + case results <- link: + default: + log.Errorf("Link update dropped (queue full): %s", link.GetName()) + } + } + }, + DeleteFunc: func(obj interface{}) { + link, ok := obj.(*v1alpha2.Link) + if !ok { + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + log.Errorf("couldn't get object from DeletedFinalStateUnknown %#v", obj) + return + } + link, ok = tombstone.Obj.(*v1alpha2.Link) if !ok { - log.Info("Link watch terminated; restarting watch") - continue main + log.Errorf("DeletedFinalStateUnknown contained object that is not a Link %#v", obj) + return } - switch obj := event.Object.(type) { - case *dynamic.Unstructured: - if obj.GetName() == linkName { - switch event.Type { - case watch.Added, watch.Modified: - link, err := multicluster.NewLink(*obj) - if err != nil { - log.Errorf("Failed to parse link %s: %s", linkName, err) - continue - } - log.Infof("Got updated link %s: %+v", linkName, link) - creds, err := loadCredentials(ctx, link, *namespace, k8sAPI) - if err != nil { - log.Errorf("Failed to load remote cluster credentials: %s", err) - } - err = restartClusterWatcher(ctx, link, *namespace, creds, controllerK8sAPI, *requeueLimit, *repairPeriod, metrics, *enableHeadlessSvc, *enableNamespaceCreation) - if err != nil { - // failed to restart cluster watcher; give a bit of slack - // and restart the link watch to give it another try - log.Error(err) - time.Sleep(linkWatchRestartAfter) - linkWatch.Stop() - } - case watch.Deleted: - log.Infof("Link %s deleted", linkName) - cleanupWorkers() - default: - log.Infof("Ignoring event type %s", event.Type) - } - } + } + if link.GetName() == linkName { + select { + case results <- nil: // nil indicates the link was deleted default: - log.Errorf("Unknown object type detected: %+v", obj) + log.Errorf("Link delete dropped (queue full): %s", link.GetName()) + } + } + }, + }) + if err != nil { + log.Fatalf("Failed to add event handler to Link informer: %s", err) + } + + // Each time the link resource is updated, reload the config and restart the + // cluster watcher. + for { + select { + // ctx.Done() is a one-shot channel that will be closed once + // the context has been cancelled. Receiving from a closed + // channel yields the value immediately. + case <-ctx.Done(): + // The channel will be closed by the leader elector when a + // lease is lost, or by a background task handling SIGTERM. + // Before terminating the loop, stop the workers and set + // them to nil to release memory. + cleanupWorkers() + case link := <-results: + if link != nil { + log.Infof("Got updated link %s: %+v", linkName, link) + creds, err := loadCredentials(ctx, link, *namespace, controllerK8sAPI.Client) + if err != nil { + log.Errorf("Failed to load remote cluster credentials: %s", err) + } + err = restartClusterWatcher(ctx, link, *namespace, creds, controllerK8sAPI, l5dClient, *requeueLimit, *repairPeriod, metrics, *enableHeadlessSvc, *enableNamespaceCreation) + if err != nil { + // failed to restart cluster watcher; give a bit of slack + // and requeue the link to give it another try + log.Error(err) + time.Sleep(linkWatchRestartAfter) + results <- link } + } else { + log.Infof("Link %s deleted", linkName) + cleanupWorkers() } } } @@ -291,21 +332,22 @@ func cleanupWorkers() { } } -func loadCredentials(ctx context.Context, link multicluster.Link, namespace string, k8sAPI *k8s.KubernetesAPI) ([]byte, error) { +func loadCredentials(ctx context.Context, link *v1alpha2.Link, namespace string, k8sAPI kubernetes.Interface) ([]byte, error) { // Load the credentials secret - secret, err := k8sAPI.Interface.CoreV1().Secrets(namespace).Get(ctx, link.ClusterCredentialsSecret, metav1.GetOptions{}) + secret, err := k8sAPI.CoreV1().Secrets(namespace).Get(ctx, link.Spec.ClusterCredentialsSecret, metav1.GetOptions{}) if err != nil { - return nil, fmt.Errorf("failed to load credentials secret %s: %w", link.ClusterCredentialsSecret, err) + return nil, fmt.Errorf("failed to load credentials secret %s: %w", link.Spec.ClusterCredentialsSecret, err) } return sm.ParseRemoteClusterSecret(secret) } func restartClusterWatcher( ctx context.Context, - link multicluster.Link, + link *v1alpha2.Link, namespace string, creds []byte, controllerK8sAPI *controllerK8s.API, + linkClient l5dcrdclient.Interface, requeueLimit int, repairPeriod time.Duration, metrics servicemirror.ProbeMetricVecs, @@ -315,7 +357,7 @@ func restartClusterWatcher( cleanupWorkers() - workerMetrics, err := metrics.NewWorkerMetrics(link.TargetClusterName) + workerMetrics, err := metrics.NewWorkerMetrics(link.Spec.TargetClusterName) if err != nil { return fmt.Errorf("failed to create metrics for cluster watcher: %w", err) } @@ -323,8 +365,8 @@ func restartClusterWatcher( // If linked against a cluster that has a gateway, start a probe and // initialise the liveness channel var ch chan bool - if link.ProbeSpec.Path != "" { - probeWorker = servicemirror.NewProbeWorker(fmt.Sprintf("probe-gateway-%s", link.TargetClusterName), &link.ProbeSpec, workerMetrics, link.TargetClusterName) + if link.Spec.ProbeSpec.Path != "" { + probeWorker = servicemirror.NewProbeWorker(fmt.Sprintf("probe-gateway-%s", link.Spec.TargetClusterName), &link.Spec.ProbeSpec, workerMetrics, link.Spec.TargetClusterName) probeWorker.Start() ch = probeWorker.Liveness } @@ -334,16 +376,17 @@ func restartClusterWatcher( if err != nil { return fmt.Errorf("unable to parse kube config: %w", err) } - remoteAPI, err := controllerK8s.InitializeAPIForConfig(ctx, cfg, false, link.TargetClusterName, controllerK8s.Svc, controllerK8s.Endpoint) + remoteAPI, err := controllerK8s.InitializeAPIForConfig(ctx, cfg, false, link.Spec.TargetClusterName, controllerK8s.Svc, controllerK8s.Endpoint) if err != nil { - return fmt.Errorf("cannot initialize api for target cluster %s: %w", link.TargetClusterName, err) + return fmt.Errorf("cannot initialize api for target cluster %s: %w", link.Spec.TargetClusterName, err) } cw, err := servicemirror.NewRemoteClusterServiceWatcher( ctx, namespace, controllerK8sAPI, remoteAPI, - &link, + linkClient, + link, requeueLimit, repairPeriod, ch, @@ -366,6 +409,7 @@ func startLocalClusterWatcher( ctx context.Context, namespace string, controllerK8sAPI *controllerK8s.API, + linkClient l5dcrdclient.Interface, requeueLimit int, repairPeriod time.Duration, enableHeadlessSvc bool, @@ -377,19 +421,24 @@ func startLocalClusterWatcher( return fmt.Errorf("failed to parse federated service selector: %w", err) } - link := multicluster.Link{ - Name: "local", - Namespace: namespace, - TargetClusterName: "", - Selector: nil, - RemoteDiscoverySelector: nil, - FederatedServiceSelector: federatedLabelSelector, + link := v1alpha2.Link{ + ObjectMeta: metav1.ObjectMeta{ + Name: "local", + Namespace: namespace, + }, + Spec: v1alpha2.LinkSpec{ + TargetClusterName: "", + Selector: nil, + RemoteDiscoverySelector: nil, + FederatedServiceSelector: federatedLabelSelector, + }, } cw, err := servicemirror.NewRemoteClusterServiceWatcher( ctx, namespace, controllerK8sAPI, controllerK8sAPI, + linkClient, &link, requeueLimit, repairPeriod, diff --git a/multicluster/cmd/testdata/service_mirror_default.golden b/multicluster/cmd/testdata/service_mirror_default.golden index 7fe4138740971..0cf54fa75656e 100644 --- a/multicluster/cmd/testdata/service_mirror_default.golden +++ b/multicluster/cmd/testdata/service_mirror_default.golden @@ -50,7 +50,7 @@ rules: verbs: ["list", "get", "watch"] - apiGroups: ["multicluster.linkerd.io"] resources: ["links/status"] - verbs: ["update"] + verbs: ["update", "patch"] - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["create", "get", "update", "patch"] diff --git a/multicluster/cmd/testdata/service_mirror_ha.golden b/multicluster/cmd/testdata/service_mirror_ha.golden index 09d26266a3cdd..60ed1d015add7 100644 --- a/multicluster/cmd/testdata/service_mirror_ha.golden +++ b/multicluster/cmd/testdata/service_mirror_ha.golden @@ -50,7 +50,7 @@ rules: verbs: ["list", "get", "watch"] - apiGroups: ["multicluster.linkerd.io"] resources: ["links/status"] - verbs: ["update"] + verbs: ["update", "patch"] - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["create", "get", "update", "patch"] diff --git a/multicluster/cmd/uninstall.go b/multicluster/cmd/uninstall.go index a7e447c2b7965..822e6375ea33c 100644 --- a/multicluster/cmd/uninstall.go +++ b/multicluster/cmd/uninstall.go @@ -8,10 +8,10 @@ import ( pkgCmd "github.com/linkerd/linkerd2/pkg/cmd" "github.com/linkerd/linkerd2/pkg/k8s" - mc "github.com/linkerd/linkerd2/pkg/multicluster" "github.com/spf13/cobra" kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd" ) @@ -41,15 +41,15 @@ func newMulticlusterUninstallCommand() *cobra.Command { return err } - links, err := mc.GetLinks(cmd.Context(), k8sAPI.DynamicClient) + links, err := k8sAPI.L5dCrdClient.LinkV1alpha2().Links("").List(cmd.Context(), metav1.ListOptions{}) if err != nil && !kerrors.IsNotFound(err) { return err } - if len(links) > 0 { + if len(links.Items) > 0 { err := []string{"Please unlink the following clusters before uninstalling multicluster:"} - for _, link := range links { - err = append(err, fmt.Sprintf(" * %s", link.TargetClusterName)) + for _, link := range links.Items { + err = append(err, fmt.Sprintf(" * %s", link.Spec.TargetClusterName)) } return errors.New(strings.Join(err, "\n")) } diff --git a/multicluster/cmd/unlink.go b/multicluster/cmd/unlink.go index c1f1ee8d88f2a..fa60dfa7e4f8d 100644 --- a/multicluster/cmd/unlink.go +++ b/multicluster/cmd/unlink.go @@ -9,7 +9,6 @@ import ( pkgcmd "github.com/linkerd/linkerd2/pkg/cmd" "github.com/linkerd/linkerd2/pkg/k8s" "github.com/linkerd/linkerd2/pkg/k8s/resource" - mc "github.com/linkerd/linkerd2/pkg/multicluster" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" appsv1 "k8s.io/api/apps/v1" @@ -54,7 +53,7 @@ func newUnlinkCommand() *cobra.Command { return err } - l, err := mc.GetLink(cmd.Context(), k.DynamicClient, opts.namespace, opts.clusterName) + l, err := k.L5dCrdClient.LinkV1alpha2().Links(opts.namespace).Get(cmd.Context(), opts.clusterName, metav1.GetOptions{}) if err != nil { return err } @@ -74,7 +73,7 @@ func newUnlinkCommand() *cobra.Command { role, roleBinding, serviceAccount, serviceMirror, lease, } - if l.ProbeSpec.Path != "" { + if l.Spec.ProbeSpec.Path != "" { gatewayMirror := resource.NewNamespaced(corev1.SchemeGroupVersion.String(), "Service", fmt.Sprintf("probe-gateway-%s", opts.clusterName), opts.namespace) resources = append(resources, gatewayMirror) } diff --git a/multicluster/service-mirror/cluster_watcher.go b/multicluster/service-mirror/cluster_watcher.go index ec8179b4812ae..29a6d4d235044 100644 --- a/multicluster/service-mirror/cluster_watcher.go +++ b/multicluster/service-mirror/cluster_watcher.go @@ -2,24 +2,26 @@ package servicemirror import ( "context" + "encoding/json" "errors" - "flag" "fmt" "net" "sort" + "strconv" "strings" "time" + "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" + l5dcrdclient "github.com/linkerd/linkerd2/controller/gen/client/clientset/versioned" "github.com/linkerd/linkerd2/controller/k8s" consts "github.com/linkerd/linkerd2/pkg/k8s" - "github.com/linkerd/linkerd2/pkg/multicluster" "github.com/prometheus/client_golang/prometheus" logging "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" @@ -46,9 +48,10 @@ type ( // problems or general glitch in the Matrix. RemoteClusterServiceWatcher struct { serviceMirrorNamespace string - link *multicluster.Link + link *v1alpha2.Link remoteAPIClient *k8s.API localAPIClient *k8s.API + linkClient l5dcrdclient.Interface stopper chan struct{} eventBroadcaster record.EventBroadcaster recorder record.EventRecorder @@ -190,7 +193,8 @@ func NewRemoteClusterServiceWatcher( serviceMirrorNamespace string, localAPI *k8s.API, remoteAPI *k8s.API, - link *multicluster.Link, + linkClient l5dcrdclient.Interface, + link *v1alpha2.Link, requeueLimit int, repairPeriod time.Duration, liveness chan bool, @@ -200,7 +204,7 @@ func NewRemoteClusterServiceWatcher( _, err := remoteAPI.Client.Discovery().ServerVersion() if err != nil { remoteAPI.UnregisterGauges() - return nil, fmt.Errorf("cannot connect to api for target cluster %s: %w", link.TargetClusterName, err) + return nil, fmt.Errorf("cannot connect to api for target cluster %s: %w", link.Spec.TargetClusterName, err) } // Create k8s event recorder @@ -209,7 +213,7 @@ func NewRemoteClusterServiceWatcher( Interface: remoteAPI.Client.CoreV1().Events(""), }) recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{ - Component: fmt.Sprintf("linkerd-service-mirror-%s", link.TargetClusterName), + Component: fmt.Sprintf("linkerd-service-mirror-%s", link.Spec.TargetClusterName), }) stopper := make(chan struct{}) @@ -218,11 +222,12 @@ func NewRemoteClusterServiceWatcher( link: link, remoteAPIClient: remoteAPI, localAPIClient: localAPI, + linkClient: linkClient, stopper: stopper, eventBroadcaster: eventBroadcaster, recorder: recorder, log: logging.WithFields(logging.Fields{ - "cluster": link.TargetClusterName, + "cluster": link.Spec.TargetClusterName, }), eventsQueue: workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]()), requeueLimit: requeueLimit, @@ -236,7 +241,7 @@ func NewRemoteClusterServiceWatcher( } func (rcsw *RemoteClusterServiceWatcher) mirrorServiceName(remoteName string) string { - return fmt.Sprintf("%s-%s", remoteName, rcsw.link.TargetClusterName) + return fmt.Sprintf("%s-%s", remoteName, rcsw.link.Spec.TargetClusterName) } func (rcsw *RemoteClusterServiceWatcher) federatedServiceName(remoteName string) string { @@ -244,11 +249,11 @@ func (rcsw *RemoteClusterServiceWatcher) federatedServiceName(remoteName string) } func (rcsw *RemoteClusterServiceWatcher) targetResourceName(mirrorName string) string { - return strings.TrimSuffix(mirrorName, "-"+rcsw.link.TargetClusterName) + return strings.TrimSuffix(mirrorName, "-"+rcsw.link.Spec.TargetClusterName) } func (rcsw *RemoteClusterServiceWatcher) originalResourceName(mirroredName string) string { - return strings.TrimSuffix(mirroredName, fmt.Sprintf("-%s", rcsw.link.TargetClusterName)) + return strings.TrimSuffix(mirroredName, fmt.Sprintf("-%s", rcsw.link.Spec.TargetClusterName)) } // Provides labels for mirrored or federatedservice. @@ -274,10 +279,10 @@ func (rcsw *RemoteClusterServiceWatcher) getCommonServiceLabels(remoteService *c // with the "SvcMirrorPrefix"). func (rcsw *RemoteClusterServiceWatcher) getMirrorServiceLabels(remoteService *corev1.Service) map[string]string { labels := rcsw.getCommonServiceLabels(remoteService) - labels[consts.RemoteClusterNameLabel] = rcsw.link.TargetClusterName + labels[consts.RemoteClusterNameLabel] = rcsw.link.Spec.TargetClusterName if rcsw.isRemoteDiscovery(remoteService.Labels) { - labels[consts.RemoteDiscoveryLabel] = rcsw.link.TargetClusterName + labels[consts.RemoteDiscoveryLabel] = rcsw.link.Spec.TargetClusterName labels[consts.RemoteServiceLabel] = remoteService.GetName() } @@ -316,7 +321,7 @@ func (rcsw *RemoteClusterServiceWatcher) getCommonServiceAnnotations(remoteServi func (rcsw *RemoteClusterServiceWatcher) getMirrorServiceAnnotations(remoteService *corev1.Service) map[string]string { annotations := rcsw.getCommonServiceAnnotations(remoteService) - annotations[consts.RemoteServiceFqName] = fmt.Sprintf("%s.%s.svc.%s", remoteService.Name, remoteService.Namespace, rcsw.link.TargetClusterDomain) + annotations[consts.RemoteServiceFqName] = fmt.Sprintf("%s.%s.svc.%s", remoteService.Name, remoteService.Namespace, rcsw.link.Spec.TargetClusterDomain) annotations[consts.RemoteResourceVersionAnnotation] = remoteService.ResourceVersion // needed to detect real changes return annotations @@ -326,12 +331,12 @@ func (rcsw *RemoteClusterServiceWatcher) getMirrorServiceAnnotations(remoteServi func (rcsw *RemoteClusterServiceWatcher) getFederatedServiceAnnotations(remoteService *corev1.Service) map[string]string { annotations := rcsw.getCommonServiceAnnotations(remoteService) - if rcsw.link.TargetClusterName == "" { + if rcsw.link.Spec.TargetClusterName == "" { // Local discovery annotations[consts.LocalDiscoveryAnnotation] = remoteService.Name } else { // Remote discovery - annotations[consts.RemoteDiscoveryAnnotation] = fmt.Sprintf("%s@%s", remoteService.Name, rcsw.link.TargetClusterName) + annotations[consts.RemoteDiscoveryAnnotation] = fmt.Sprintf("%s@%s", remoteService.Name, rcsw.link.Spec.TargetClusterName) } return annotations @@ -348,7 +353,7 @@ func (rcsw *RemoteClusterServiceWatcher) mirrorNamespaceIfNecessary(ctx context. ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ consts.MirroredResourceLabel: "true", - consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName, + consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName, }, Name: namespace, }, @@ -370,22 +375,26 @@ func (rcsw *RemoteClusterServiceWatcher) mirrorNamespaceIfNecessary(ctx context. // that we should send traffic to and create endpoint ports that bind to the mirrored service ports // (same name, etc) but send traffic to the gateway port. This way we do not need to do any remapping // on the service side of things. It all happens in the endpoints. -func (rcsw *RemoteClusterServiceWatcher) getEndpointsPorts(service *corev1.Service) []corev1.EndpointPort { +func (rcsw *RemoteClusterServiceWatcher) getEndpointsPorts(service *corev1.Service) ([]corev1.EndpointPort, error) { + gatewayPort, err := strconv.ParseInt(rcsw.link.Spec.GatewayPort, 10, 32) + if err != nil { + return nil, err + } var endpointsPorts []corev1.EndpointPort for _, remotePort := range service.Spec.Ports { endpointsPorts = append(endpointsPorts, corev1.EndpointPort{ Name: remotePort.Name, Protocol: remotePort.Protocol, - Port: int32(rcsw.link.GatewayPort), + Port: int32(gatewayPort), }) } - return endpointsPorts + return endpointsPorts, nil } func (rcsw *RemoteClusterServiceWatcher) cleanupOrphanedServices(ctx context.Context) error { matchLabels := map[string]string{ consts.MirroredResourceLabel: "true", - consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName, + consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName, } servicesOnLocalCluster, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector()) @@ -436,7 +445,7 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupOrphanedServices(ctx context.Con func (rcsw *RemoteClusterServiceWatcher) cleanupMirroredResources(ctx context.Context) error { matchLabels := map[string]string{ consts.MirroredResourceLabel: "true", - consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName, + consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName, } services, err := rcsw.localAPIClient.Svc().Lister().List(labels.Set(matchLabels).AsSelector()) @@ -559,11 +568,11 @@ func (rcsw *RemoteClusterServiceWatcher) handleFederatedServiceLeave(ctx context return RetryableError{[]error{fmt.Errorf("could not fetch service %s/%s: %w", ev.Namespace, localServiceName, err)}} } - if rcsw.link.TargetClusterName == "" { + if rcsw.link.Spec.TargetClusterName == "" { // Local discovery delete(localService.Annotations, consts.LocalDiscoveryAnnotation) } else { - remoteTarget := fmt.Sprintf("%s@%s", ev.Name, rcsw.link.TargetClusterName) + remoteTarget := fmt.Sprintf("%s@%s", ev.Name, rcsw.link.Spec.TargetClusterName) if !remoteDiscoveryContains(localService.Annotations[consts.RemoteDiscoveryAnnotation], remoteTarget) { return nil } @@ -643,17 +652,21 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteExportedServiceUpdated(ctx } copiedEndpoints := ev.localEndpoints.DeepCopy() + ports, err := rcsw.getEndpointsPorts(ev.remoteUpdate) + if err != nil { + return err + } copiedEndpoints.Subsets = []corev1.EndpointSubset{ { Addresses: gatewayAddresses, - Ports: rcsw.getEndpointsPorts(ev.remoteUpdate), + Ports: ports, }, } if copiedEndpoints.Annotations == nil { copiedEndpoints.Annotations = make(map[string]string) } - copiedEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.GatewayIdentity + copiedEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.Spec.GatewayIdentity err = rcsw.updateMirrorEndpoints(ctx, copiedEndpoints) if err != nil { @@ -695,12 +708,12 @@ func (rcsw *RemoteClusterServiceWatcher) handleFederatedServiceJoin(ctx context. return fmt.Errorf("headless service %s/%s cannot join federated service", ev.remoteUpdate.GetNamespace(), ev.remoteUpdate.GetName()) } - if rcsw.link.TargetClusterName == "" { + if rcsw.link.Spec.TargetClusterName == "" { // Local discovery ev.localService.Annotations[consts.LocalDiscoveryAnnotation] = ev.remoteUpdate.Name } else { // Remote discovery - remoteTarget := fmt.Sprintf("%s@%s", ev.remoteUpdate.Name, rcsw.link.TargetClusterName) + remoteTarget := fmt.Sprintf("%s@%s", ev.remoteUpdate.Name, rcsw.link.Spec.TargetClusterName) if remoteDiscoveryContains(ev.localService.Annotations[consts.RemoteDiscoveryAnnotation], remoteTarget) { return nil } @@ -989,28 +1002,33 @@ func (rcsw *RemoteClusterServiceWatcher) createGatewayEndpoints(ctx context.Cont Namespace: exportedService.Namespace, Labels: map[string]string{ consts.MirroredResourceLabel: "true", - consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName, + consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName, }, Annotations: map[string]string{ - consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", exportedService.Name, exportedService.Namespace, rcsw.link.TargetClusterDomain), + consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", exportedService.Name, exportedService.Namespace, rcsw.link.Spec.TargetClusterDomain), }, }, } - rcsw.log.Infof("Resolved gateway [%v:%d] for %s", gatewayAddresses, rcsw.link.GatewayPort, serviceInfo) + rcsw.log.Infof("Resolved gateway [%v:%s] for %s", gatewayAddresses, rcsw.link.Spec.GatewayPort, serviceInfo) + ports, err := rcsw.getEndpointsPorts(exportedService) + if err != nil { + return err + } if !empty && len(gatewayAddresses) > 0 { + endpointsToCreate.Subsets = []corev1.EndpointSubset{ { Addresses: gatewayAddresses, - Ports: rcsw.getEndpointsPorts(exportedService), + Ports: ports, }, } } else if !empty { endpointsToCreate.Subsets = []corev1.EndpointSubset{ { NotReadyAddresses: gatewayAddresses, - Ports: rcsw.getEndpointsPorts(exportedService), + Ports: ports, }, } rcsw.log.Warnf("could not resolve gateway addresses for %s; setting endpoint subsets to not ready", serviceInfo) @@ -1018,8 +1036,8 @@ func (rcsw *RemoteClusterServiceWatcher) createGatewayEndpoints(ctx context.Cont rcsw.log.Warnf("exported service %s is empty", serviceInfo) } - if rcsw.link.GatewayIdentity != "" { - endpointsToCreate.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.GatewayIdentity + if rcsw.link.Spec.GatewayIdentity != "" { + endpointsToCreate.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.Spec.GatewayIdentity } rcsw.log.Infof("Creating a new endpoints for %s", serviceInfo) @@ -1075,7 +1093,7 @@ func (rcsw *RemoteClusterServiceWatcher) createOrUpdateService(service *corev1.S if localSvc.Labels != nil { _, isMirroredRes := localSvc.Labels[consts.MirroredResourceLabel] clusterName := localSvc.Labels[consts.RemoteClusterNameLabel] - if isMirroredRes && (clusterName == rcsw.link.TargetClusterName) { + if isMirroredRes && (clusterName == rcsw.link.Spec.TargetClusterName) { rcsw.eventsQueue.Add(&RemoteServiceUnexported{ Name: service.Name, Namespace: service.Namespace, @@ -1131,7 +1149,7 @@ func (rcsw *RemoteClusterServiceWatcher) createOrUpdateService(service *corev1.S func (rcsw *RemoteClusterServiceWatcher) getMirrorServices() (*corev1.ServiceList, error) { matchLabels := map[string]string{ consts.MirroredResourceLabel: "true", - consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName, + consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName, } services, err := rcsw.localAPIClient.Client.CoreV1().Services("").List(context.Background(), metav1.ListOptions{LabelSelector: labels.SelectorFromSet(matchLabels).String()}) if err != nil { @@ -1332,7 +1350,7 @@ func (rcsw *RemoteClusterServiceWatcher) Start(ctx context.Context) error { go rcsw.processEvents(ctx) // If no gateway address is present, do not repair endpoints - if rcsw.link.GatewayAddress == "" { + if rcsw.link.Spec.GatewayAddress == "" { return nil } @@ -1395,7 +1413,7 @@ func (rcsw *RemoteClusterServiceWatcher) Stop(cleanupState bool) { func (rcsw *RemoteClusterServiceWatcher) resolveGatewayAddress() ([]corev1.EndpointAddress, error) { var gatewayEndpoints []corev1.EndpointAddress var errors []error - for _, addr := range strings.Split(rcsw.link.GatewayAddress, ",") { + for _, addr := range strings.Split(rcsw.link.Spec.GatewayAddress, ",") { ipAddrs, err := net.LookupIP(addr) if err != nil { err = fmt.Errorf("Error resolving '%s': %w", addr, err) @@ -1423,7 +1441,7 @@ func (rcsw *RemoteClusterServiceWatcher) resolveGatewayAddress() ([]corev1.Endpo func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) error { endpointRepairCounter.With(prometheus.Labels{ - gatewayClusterName: rcsw.link.TargetClusterName, + gatewayClusterName: rcsw.link.Spec.TargetClusterName, }).Inc() // Create or update the gateway mirror endpoints responsible for driving @@ -1471,10 +1489,15 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) er } } updatedEndpoints := endpoints.DeepCopy() + ports, err := rcsw.getEndpointsPorts(&svc) + if err != nil { + rcsw.log.Errorf("Failed to get endpoints ports: %s", err) + continue + } updatedEndpoints.Subsets = []corev1.EndpointSubset{ { Addresses: gatewayAddresses, - Ports: rcsw.getEndpointsPorts(&svc), + Ports: ports, }, } @@ -1500,7 +1523,7 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) er if updatedEndpoints.Annotations == nil { updatedEndpoints.Annotations = make(map[string]string) } - updatedEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.GatewayIdentity + updatedEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.Spec.GatewayIdentity err = rcsw.updateMirrorEndpoints(ctx, updatedEndpoints) if err != nil { @@ -1516,16 +1539,20 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) er // worker responsible for probing gateway liveness, so these endpoints are // never in a not ready state. func (rcsw *RemoteClusterServiceWatcher) createOrUpdateGatewayEndpoints(ctx context.Context, addressses []corev1.EndpointAddress) error { - gatewayMirrorName := fmt.Sprintf("probe-gateway-%s", rcsw.link.TargetClusterName) + gatewayMirrorName := fmt.Sprintf("probe-gateway-%s", rcsw.link.Spec.TargetClusterName) + probePort, err := strconv.ParseInt(rcsw.link.Spec.ProbeSpec.Port, 10, 32) + if err != nil { + return fmt.Errorf("failed to parse probe port: %w", err) + } endpoints := &corev1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: gatewayMirrorName, Namespace: rcsw.serviceMirrorNamespace, Labels: map[string]string{ - consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName, + consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName, }, Annotations: map[string]string{ - consts.RemoteGatewayIdentity: rcsw.link.GatewayIdentity, + consts.RemoteGatewayIdentity: rcsw.link.Spec.GatewayIdentity, }, }, Subsets: []corev1.EndpointSubset{ @@ -1534,14 +1561,14 @@ func (rcsw *RemoteClusterServiceWatcher) createOrUpdateGatewayEndpoints(ctx cont Ports: []corev1.EndpointPort{ { Name: "mc-probe", - Port: int32(rcsw.link.ProbeSpec.Port), + Port: int32(probePort), Protocol: "TCP", }, }, }, }, } - _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(endpoints.Namespace).Get(ctx, endpoints.Name, metav1.GetOptions{}) + _, err = rcsw.localAPIClient.Client.CoreV1().Endpoints(endpoints.Namespace).Get(ctx, endpoints.Name, metav1.GetOptions{}) if err != nil { if !kerrors.IsNotFound(err) { return err @@ -1606,10 +1633,14 @@ func (rcsw *RemoteClusterServiceWatcher) handleCreateOrUpdateEndpoints( if err != nil { return err } + ports, err := rcsw.getEndpointsPorts(exportedService) + if err != nil { + return err + } ep.Subsets = []corev1.EndpointSubset{ { Addresses: gatewayAddresses, - Ports: rcsw.getEndpointsPorts(exportedService), + Ports: ports, }, } } @@ -1654,13 +1685,13 @@ func (rcsw *RemoteClusterServiceWatcher) updateReadiness(endpoints *corev1.Endpo func (rcsw *RemoteClusterServiceWatcher) isExported(l map[string]string) bool { // Treat an empty selector as "Nothing" instead of "Everything" so that // when the selector field is unset, we don't export all Services. - if rcsw.link.Selector == nil { + if rcsw.link.Spec.Selector == nil { return false } - if len(rcsw.link.Selector.MatchExpressions)+len(rcsw.link.Selector.MatchLabels) == 0 { + if len(rcsw.link.Spec.Selector.MatchExpressions)+len(rcsw.link.Spec.Selector.MatchLabels) == 0 { return false } - selector, err := metav1.LabelSelectorAsSelector(rcsw.link.Selector) + selector, err := metav1.LabelSelectorAsSelector(rcsw.link.Spec.Selector) if err != nil { rcsw.log.Errorf("Invalid selector: %s", err) return false @@ -1672,13 +1703,13 @@ func (rcsw *RemoteClusterServiceWatcher) isRemoteDiscovery(l map[string]string) // Treat an empty remoteDiscoverySelector as "Nothing" instead of // "Everything" so that when the remoteDiscoverySelector field is unset, we // don't export all Services. - if rcsw.link.RemoteDiscoverySelector == nil { + if rcsw.link.Spec.RemoteDiscoverySelector == nil { return false } - if len(rcsw.link.RemoteDiscoverySelector.MatchExpressions)+len(rcsw.link.RemoteDiscoverySelector.MatchLabels) == 0 { + if len(rcsw.link.Spec.RemoteDiscoverySelector.MatchExpressions)+len(rcsw.link.Spec.RemoteDiscoverySelector.MatchLabels) == 0 { return false } - remoteDiscoverySelector, err := metav1.LabelSelectorAsSelector(rcsw.link.RemoteDiscoverySelector) + remoteDiscoverySelector, err := metav1.LabelSelectorAsSelector(rcsw.link.Spec.RemoteDiscoverySelector) if err != nil { rcsw.log.Errorf("Invalid selector: %s", err) return false @@ -1691,13 +1722,13 @@ func (rcsw *RemoteClusterServiceWatcher) isFederatedServiceMember(l map[string]s // Treat an empty federatedServiceSelector as "Nothing" instead of // "Everything" so that when the federatedServiceSelector field is unset, we // don't export all Services. - if rcsw.link.FederatedServiceSelector == nil { + if rcsw.link.Spec.FederatedServiceSelector == nil { return false } - if len(rcsw.link.FederatedServiceSelector.MatchExpressions)+len(rcsw.link.FederatedServiceSelector.MatchLabels) == 0 { + if len(rcsw.link.Spec.FederatedServiceSelector.MatchExpressions)+len(rcsw.link.Spec.FederatedServiceSelector.MatchLabels) == 0 { return false } - federatedServiceSelector, err := metav1.LabelSelectorAsSelector(rcsw.link.FederatedServiceSelector) + federatedServiceSelector, err := metav1.LabelSelectorAsSelector(rcsw.link.Spec.FederatedServiceSelector) if err != nil { rcsw.log.Errorf("Invalid selector: %s", err) return false @@ -1706,156 +1737,130 @@ func (rcsw *RemoteClusterServiceWatcher) isFederatedServiceMember(l map[string]s return federatedServiceSelector.Matches(labels.Set(l)) } -func (rcsw *RemoteClusterServiceWatcher) updateLinkMirrorStatus(remoteName, namespace string, condition map[string]interface{}) { - err := rcsw.updateLinkStatus("mirrorServices", remoteName, namespace, condition) +func (rcsw *RemoteClusterServiceWatcher) updateLinkMirrorStatus(remoteName, namespace string, condition v1alpha2.LinkCondition) { + if rcsw.link.Spec.TargetClusterName == "" { + // The local cluster has no Link resource. + return + } + link, err := rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{}) if err != nil { - rcsw.log.Errorf("Failed to update link status: %s", err) + rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) } + link.Status.MirrorServices = updateServiceStatus(remoteName, namespace, condition, link.Status.MirrorServices) + rcsw.patchLinkStatus(link.Status) } -func (rcsw *RemoteClusterServiceWatcher) updateLinkFederatedStatus(remoteName, namespace string, condition map[string]interface{}) { - err := rcsw.updateLinkStatus("federatedServices", remoteName, namespace, condition) +func (rcsw *RemoteClusterServiceWatcher) updateLinkFederatedStatus(remoteName, namespace string, condition v1alpha2.LinkCondition) { + if rcsw.link.Spec.TargetClusterName == "" { + // The local cluster has no Link resource. + return + } + link, err := rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{}) if err != nil { - rcsw.log.Errorf("Failed to update link status: %s", err) + rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) } + link.Status.FederatedServices = updateServiceStatus(remoteName, namespace, condition, link.Status.FederatedServices) + rcsw.patchLinkStatus(link.Status) } -func (rcsw *RemoteClusterServiceWatcher) updateLinkStatus(statusSection, remoteName, namespace string, condition map[string]interface{}) error { - if rcsw.link.TargetClusterName == "" { +func (rcsw *RemoteClusterServiceWatcher) deleteLinkMirrorStatus(remoteName, namespace string) { + if rcsw.link.Spec.TargetClusterName == "" { // The local cluster has no Link resource. - return nil + return } - rcsw.log.Errorf("fetching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) - link, err := rcsw.localAPIClient.DynamicClient.Resource(multicluster.LinkGVR).Namespace(rcsw.link.Namespace).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{}) + link, err := rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{}) if err != nil { - return err + rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) + } + link.Status.MirrorServices = deleteServiceStatus(remoteName, namespace, link.Status.MirrorServices) + rcsw.patchLinkStatus(link.Status) +} + +func (rcsw *RemoteClusterServiceWatcher) deleteLinkFederatedStatus(remoteName, namespace string) { + if rcsw.link.Spec.TargetClusterName == "" { + // The local cluster has no Link resource. + return } - rcsw.log.Errorf("got link status: %s", link.Object) - statuses, found, err := unstructured.NestedSlice(link.Object, "status", statusSection) + link, err := rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{}) if err != nil { - return err + rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) } - if !found { - statuses = make([]interface{}, 0) + link.Status.FederatedServices = deleteServiceStatus(remoteName, namespace, link.Status.FederatedServices) + rcsw.patchLinkStatus(link.Status) +} + +func (rcsw *RemoteClusterServiceWatcher) patchLinkStatus(status v1alpha2.LinkStatus) { + rcsw.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) + statusBytes, err := json.Marshal(status) + if err != nil { + rcsw.log.Errorf("Failed to marshal link status: %s", err) + } + _, err = rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Patch( + context.Background(), + rcsw.link.Name, + types.MergePatchType, + []byte(fmt.Sprintf(`{"status": %s}`, string(statusBytes))), + metav1.PatchOptions{}, + "status", + ) + if err != nil { + rcsw.log.Errorf("Failed to patch link status %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) } +} + +func updateServiceStatus(remoteName, namespace string, condition v1alpha2.LinkCondition, statuses []v1alpha2.ServiceStatus) []v1alpha2.ServiceStatus { foundStatus := false for i, status := range statuses { - status, ok := status.(map[string]interface{}) - if !ok { - return fmt.Errorf("mirrorServices status must be an object") - } - statusRemoteName, _, err := unstructured.NestedString(status, "remoteRef", "name") - if err != nil { - return err - } - statusRemoteNamespace, _, err := unstructured.NestedString(status, "remoteRef", "namespace") - if err != nil { - return err - } - if statusRemoteName == remoteName && statusRemoteNamespace == namespace { + if status.RemoteRef.Name == remoteName && status.RemoteRef.Namespace == namespace { foundStatus = true - status["conditions"] = []interface{}{condition} + status.Conditions = []v1alpha2.LinkCondition{condition} statuses[i] = status } } if !foundStatus { - statuses = append(statuses, map[string]interface{}{ - "controllerName": "linkerd.io/service-mirror", - "remoteRef": map[string]interface{}{ - "name": remoteName, - "namespace": namespace, - "kind": "Service", - "group": corev1.GroupName, + statuses = append(statuses, v1alpha2.ServiceStatus{ + ControllerName: "linkerd.io/service-mirror", + RemoteRef: v1alpha2.ObjectRef{ + Name: remoteName, + Namespace: namespace, + Kind: "Service", + Group: corev1.GroupName, }, - "conditions": []interface{}{condition}, + Conditions: []v1alpha2.LinkCondition{condition}, }) } - err = unstructured.SetNestedSlice(link.Object, statuses, "status", statusSection) - if err != nil { - return err - } - rcsw.log.Errorf("new link status: %s", link.Object) - flag.Set("v", "10") - _, err = rcsw.localAPIClient.DynamicClient.Resource(multicluster.LinkGVR).Namespace(rcsw.link.Namespace).UpdateStatus(context.Background(), link, metav1.UpdateOptions{}) - flag.Set("v", "2") - return err -} - -func (rcsw *RemoteClusterServiceWatcher) deleteLinkMirrorStatus(remoteName, namespace string) { - err := rcsw.deleteLinkStatus("mirrorServices", remoteName, namespace) - if err != nil { - rcsw.log.Errorf("Failed to update link status: %s", err) - } -} - -func (rcsw *RemoteClusterServiceWatcher) deleteLinkFederatedStatus(remoteName, namespace string) { - err := rcsw.deleteLinkStatus("federatedServices", remoteName, namespace) - if err != nil { - rcsw.log.Errorf("Failed to update link status: %s", err) - } + return statuses } -func (rcsw *RemoteClusterServiceWatcher) deleteLinkStatus(statusSection, remoteName, namespace string) error { - if rcsw.link.TargetClusterName == "" { - // The local cluster has no Link resource. - return nil - } - link, err := rcsw.localAPIClient.DynamicClient.Resource(multicluster.LinkGVR).Namespace(rcsw.link.Namespace).Get(context.Background(), rcsw.link.Name, metav1.GetOptions{}) - if err != nil { - return err - } - statuses, found, err := unstructured.NestedSlice(link.Object, "status", statusSection) - if err != nil { - return err - } - if !found { - statuses = make([]interface{}, 0) - } - newStatuses := make([]interface{}, 0) +func deleteServiceStatus(remoteName, namespace string, statuses []v1alpha2.ServiceStatus) []v1alpha2.ServiceStatus { + newStatuses := make([]v1alpha2.ServiceStatus, 0) for _, status := range statuses { - status, ok := status.(map[string]interface{}) - if !ok { - return fmt.Errorf("mirrorServices status must be an object") - } - statusRemoteName, _, err := unstructured.NestedString(status, "remoteRef", "name") - if err != nil { - return err - } - statusRemoteNamespace, _, err := unstructured.NestedString(status, "remoteRef", "namespace") - if err != nil { - return err - } - if statusRemoteName == remoteName && statusRemoteNamespace == namespace { + if status.RemoteRef.Name == remoteName && status.RemoteRef.Namespace == namespace { continue } newStatuses = append(newStatuses, status) } - err = unstructured.SetNestedSlice(link.Object, newStatuses, "status", statusSection) - if err != nil { - return err - } - _, err = rcsw.localAPIClient.DynamicClient.Resource(multicluster.LinkGVR).Namespace(rcsw.link.Namespace).UpdateStatus(context.Background(), link, metav1.UpdateOptions{}) - return err + return newStatuses } -func mirrorStatusCondition(success bool, reason string, message string, localRef *corev1.Service) map[string]interface{} { - status := "True" +func mirrorStatusCondition(success bool, reason string, message string, localRef *corev1.Service) v1alpha2.LinkCondition { + status := metav1.ConditionTrue if !success { - status = "False" + status = metav1.ConditionFalse } - condition := map[string]interface{}{ - "lastTransitionTime": time.Now().Format(time.RFC3339), - "message": message, - "reason": reason, - "status": status, - "type": "Mirrored", + condition := v1alpha2.LinkCondition{ + LastTransitionTime: metav1.Now(), + Message: message, + Reason: reason, + Status: status, + Type: "Mirrored", } if localRef != nil { - condition["localRef"] = map[string]interface{}{ - "name": localRef.Name, - "namespace": localRef.Namespace, - "kind": "Service", - "group": corev1.GroupName, + condition.LocalRef = v1alpha2.ObjectRef{ + Name: localRef.Name, + Namespace: localRef.Namespace, + Kind: "Service", + Group: corev1.GroupName, } } return condition diff --git a/multicluster/service-mirror/cluster_watcher_headless.go b/multicluster/service-mirror/cluster_watcher_headless.go index 0c952090ba77a..cfbc80cbc76df 100644 --- a/multicluster/service-mirror/cluster_watcher_headless.go +++ b/multicluster/service-mirror/cluster_watcher_headless.go @@ -292,17 +292,17 @@ func (rcsw *RemoteClusterServiceWatcher) createHeadlessMirrorEndpoints(ctx conte Namespace: exportedService.Namespace, Labels: map[string]string{ consts.MirroredResourceLabel: "true", - consts.RemoteClusterNameLabel: rcsw.link.TargetClusterName, + consts.RemoteClusterNameLabel: rcsw.link.Spec.TargetClusterName, }, Annotations: map[string]string{ - consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", exportedService.Name, exportedService.Namespace, rcsw.link.TargetClusterDomain), + consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.svc.%s", exportedService.Name, exportedService.Namespace, rcsw.link.Spec.TargetClusterDomain), }, }, Subsets: subsetsToCreate, } - if rcsw.link.GatewayIdentity != "" { - headlessMirrorEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.GatewayIdentity + if rcsw.link.Spec.GatewayIdentity != "" { + headlessMirrorEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.Spec.GatewayIdentity } rcsw.log.Infof("Creating a new headless mirror endpoints object for headless mirror %s/%s", headlessMirrorServiceName, exportedService.Namespace) @@ -334,7 +334,7 @@ func (rcsw *RemoteClusterServiceWatcher) createEndpointMirrorService(ctx context endpointMirrorAnnotations := map[string]string{ consts.RemoteResourceVersionAnnotation: resourceVersion, // needed to detect real changes - consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.%s.svc.%s", endpointHostname, exportedService.Name, exportedService.Namespace, rcsw.link.TargetClusterDomain), + consts.RemoteServiceFqName: fmt.Sprintf("%s.%s.%s.svc.%s", endpointHostname, exportedService.Name, exportedService.Namespace, rcsw.link.Spec.TargetClusterDomain), } endpointMirrorLabels := rcsw.getMirrorServiceLabels(exportedService) @@ -353,6 +353,10 @@ func (rcsw *RemoteClusterServiceWatcher) createEndpointMirrorService(ctx context Ports: remapRemoteServicePorts(exportedService.Spec.Ports), }, } + ports, err := rcsw.getEndpointsPorts(exportedService) + if err != nil { + return nil, err + } endpointMirrorEndpoints := &corev1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: endpointMirrorService.Name, @@ -365,13 +369,13 @@ func (rcsw *RemoteClusterServiceWatcher) createEndpointMirrorService(ctx context Subsets: []corev1.EndpointSubset{ { Addresses: gatewayAddresses, - Ports: rcsw.getEndpointsPorts(exportedService), + Ports: ports, }, }, } - if rcsw.link.GatewayIdentity != "" { - endpointMirrorEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.GatewayIdentity + if rcsw.link.Spec.GatewayIdentity != "" { + endpointMirrorEndpoints.Annotations[consts.RemoteGatewayIdentity] = rcsw.link.Spec.GatewayIdentity } exportedServiceInfo := fmt.Sprintf("%s/%s", exportedService.Namespace, exportedService.Name) diff --git a/multicluster/service-mirror/cluster_watcher_mirroring_test.go b/multicluster/service-mirror/cluster_watcher_mirroring_test.go index 973e1ae285e16..8d2af3074bce6 100644 --- a/multicluster/service-mirror/cluster_watcher_mirroring_test.go +++ b/multicluster/service-mirror/cluster_watcher_mirroring_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/go-test/deep" + "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" "github.com/linkerd/linkerd2/controller/k8s" consts "github.com/linkerd/linkerd2/pkg/k8s" - "github.com/linkerd/linkerd2/pkg/multicluster" logging "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -393,7 +393,7 @@ func TestLocalNamespaceCreatedAfterServiceExport(t *testing.T) { if err != nil { t.Fatal(err) } - localAPI, err := k8s.NewFakeAPI() + localAPI, l5dAPI, err := k8s.NewFakeAPIWithL5dClient() if err != nil { t.Fatal(err) } @@ -404,18 +404,21 @@ func TestLocalNamespaceCreatedAfterServiceExport(t *testing.T) { eventRecorder := record.NewFakeRecorder(100) watcher := RemoteClusterServiceWatcher{ - link: &multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: &v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, remoteAPIClient: remoteAPI, localAPIClient: localAPI, + linkClient: l5dAPI, stopper: nil, recorder: eventRecorder, log: logging.WithFields(logging.Fields{"cluster": clusterName}), @@ -482,7 +485,7 @@ func TestServiceCreatedGatewayAlive(t *testing.T) { if err != nil { t.Fatal(err) } - localAPI, err := k8s.NewFakeAPI( + localAPI, l5dAPI, err := k8s.NewFakeAPIWithL5dClient( asYaml(namespace("ns")), ) if err != nil { @@ -493,18 +496,21 @@ func TestServiceCreatedGatewayAlive(t *testing.T) { events := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]()) watcher := RemoteClusterServiceWatcher{ - link: &multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.0.1", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: &v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.0.1", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, remoteAPIClient: remoteAPI, localAPIClient: localAPI, + linkClient: l5dAPI, log: logging.WithFields(logging.Fields{"cluster": clusterName}), eventsQueue: events, requeueLimit: 0, @@ -630,7 +636,7 @@ func TestServiceCreatedGatewayDown(t *testing.T) { if err != nil { t.Fatal(err) } - localAPI, err := k8s.NewFakeAPI( + localAPI, l5dAPI, err := k8s.NewFakeAPIWithL5dClient( asYaml(namespace("ns")), ) if err != nil { @@ -641,18 +647,21 @@ func TestServiceCreatedGatewayDown(t *testing.T) { events := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]()) watcher := RemoteClusterServiceWatcher{ - link: &multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.0.1", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: &v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.0.1", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, remoteAPIClient: remoteAPI, localAPIClient: localAPI, + linkClient: l5dAPI, log: logging.WithFields(logging.Fields{"cluster": clusterName}), eventsQueue: events, requeueLimit: 0, diff --git a/multicluster/service-mirror/cluster_watcher_test_util.go b/multicluster/service-mirror/cluster_watcher_test_util.go index d1c2c7444e767..339e67d4833c7 100644 --- a/multicluster/service-mirror/cluster_watcher_test_util.go +++ b/multicluster/service-mirror/cluster_watcher_test_util.go @@ -5,12 +5,11 @@ import ( "fmt" "log" "strings" - "time" "github.com/go-test/deep" + "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" "github.com/linkerd/linkerd2/controller/k8s" consts "github.com/linkerd/linkerd2/pkg/k8s" - "github.com/linkerd/linkerd2/pkg/multicluster" logging "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -23,14 +22,14 @@ const ( clusterDomain = "cluster.local" defaultProbePath = "/probe" defaultProbePort = 12345 - defaultProbePeriod = 60 + defaultProbePeriod = "60" ) var ( - defaultProbeSpec = multicluster.ProbeSpec{ + defaultProbeSpec = v1alpha2.ProbeSpec{ Path: defaultProbePath, - Port: defaultProbePort, - Period: time.Duration(defaultProbePeriod) * time.Second, + Port: fmt.Sprintf("%d", defaultProbePort), + Period: defaultProbePeriod, } defaultSelector, _ = metav1.ParseToLabelSelector(consts.DefaultExportedServiceSelector + "=true") defaultRemoteDiscoverySelector, _ = metav1.ParseToLabelSelector(consts.DefaultExportedServiceSelector + "=remote-discovery") @@ -40,7 +39,7 @@ type testEnvironment struct { events []interface{} remoteResources []string localResources []string - link multicluster.Link + link v1alpha2.Link } func (te *testEnvironment) runEnvironment(watcherQueue workqueue.TypedRateLimitingInterface[any]) (*k8s.API, error) { @@ -48,7 +47,7 @@ func (te *testEnvironment) runEnvironment(watcherQueue workqueue.TypedRateLimiti if err != nil { return nil, err } - localAPI, err := k8s.NewFakeAPI(te.localResources...) + localAPI, l5dAPI, err := k8s.NewFakeAPIWithL5dClient(te.localResources...) if err != nil { return nil, err } @@ -59,6 +58,7 @@ func (te *testEnvironment) runEnvironment(watcherQueue workqueue.TypedRateLimiti link: &te.link, remoteAPIClient: remoteAPI, localAPIClient: localAPI, + linkClient: l5dAPI, stopper: nil, log: logging.WithFields(logging.Fields{"cluster": clusterName}), eventsQueue: watcherQueue, @@ -107,15 +107,17 @@ var createExportedService = &testEnvironment{ localResources: []string{ asYaml(namespace("ns1")), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -144,15 +146,17 @@ var createRemoteDiscoveryService = &testEnvironment{ localResources: []string{ asYaml(namespace("ns1")), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -181,15 +185,17 @@ var createFederatedService = &testEnvironment{ localResources: []string{ asYaml(namespace("ns1")), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -233,15 +239,17 @@ func joinFederatedService() *testEnvironment { asYaml(namespace("ns1")), asYaml(fedSvc), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } } @@ -271,15 +279,17 @@ var leftFederatedService = &testEnvironment{ }, }, "", fmt.Sprintf("service-one@other,service-one@%s", clusterName))), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -308,15 +318,17 @@ var createLocalFederatedService = &testEnvironment{ localResources: []string{ asYaml(namespace("ns1")), }, - link: multicluster.Link{ - TargetClusterName: "", // local cluster - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: "", // local cluster + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -360,15 +372,17 @@ func joinLocalFederatedService() *testEnvironment { asYaml(namespace("ns1")), asYaml(fedSvc), }, - link: multicluster.Link{ - TargetClusterName: "", // local cluster - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: "", // local cluster + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } } @@ -398,15 +412,17 @@ var leftLocalFederatedService = &testEnvironment{ }, }, "service-one", "service-one@other")), }, - link: multicluster.Link{ - TargetClusterName: "", // local cluster - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: "", // local cluster + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -444,7 +460,7 @@ var createExportedHeadlessService = &testEnvironment{ }, }, remoteResources: []string{ - asYaml(gateway("existing-gateway", "existing-namespace", "222", "192.0.2.129", "gateway", 889, "gateway-identity", 123456, "/probe1", 120)), + asYaml(gateway("existing-gateway", "existing-namespace", "222", "192.0.2.129", "gateway", 889, "gateway-identity", 123456, "/probe1", "120s")), asYaml(remoteHeadlessService("service-one", "ns2", "111", nil, []corev1.ServicePort{ { @@ -474,19 +490,21 @@ var createExportedHeadlessService = &testEnvironment{ localResources: []string{ asYaml(namespace("ns2")), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.129", - GatewayPort: 889, - ProbeSpec: multicluster.ProbeSpec{ - Port: 123456, - Path: "/probe1", - Period: 120, - }, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.129", + GatewayPort: "889", + ProbeSpec: v1alpha2.ProbeSpec{ + Port: "123456", + Path: "/probe1", + Period: "120", + }, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -501,15 +519,17 @@ var deleteMirrorService = &testEnvironment{ asYaml(mirrorService("test-service-remote-to-delete-remote", "test-namespace-to-delete", "", nil)), asYaml(endpoints("test-service-remote-to-delete-remote", "test-namespace-to-delete", "", "gateway-identity", nil)), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -595,15 +615,17 @@ var updateServiceWithChangedPorts = &testEnvironment{ }, })), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -702,15 +724,17 @@ var updateEndpointsWithChangedHosts = &testEnvironment{ }, })), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } var clusterUnregistered = &testEnvironment{ @@ -723,8 +747,10 @@ var clusterUnregistered = &testEnvironment{ asYaml(mirrorService("test-service-2-remote", "test-namespace", "", nil)), asYaml(endpoints("test-service-2-remote", "test-namespace", "", "", nil)), }, - link: multicluster.Link{ - TargetClusterName: clusterName, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + }, }, } @@ -746,8 +772,10 @@ var gcTriggered = &testEnvironment{ asYaml(remoteService("test-service-1", "test-namespace", "", map[string]string{consts.DefaultExportedServiceSelector: "true"}, nil)), asYaml(remoteHeadlessService("test-headless-service", "test-namespace", "", nil, nil)), }, - link: multicluster.Link{ - TargetClusterName: clusterName, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + }, }, } @@ -793,19 +821,13 @@ var noGatewayLink = &testEnvironment{ asYaml(endpoints("service-one", "ns1", "192.0.2.127", "gateway-identity", []corev1.EndpointPort{})), asYaml(endpoints("service-two", "ns1", "192.0.2.128", "gateway-identity", []corev1.EndpointPort{})), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "", - GatewayAddress: "", - GatewayPort: 0, - ProbeSpec: multicluster.ProbeSpec{ - Path: "", - Port: 0, - Period: time.Duration(0) * time.Second, - }, - Selector: &metav1.LabelSelector{}, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + Selector: &metav1.LabelSelector{}, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -816,15 +838,17 @@ func onAddOrUpdateExportedSvc(isAdd bool) *testEnvironment { consts.DefaultExportedServiceSelector: "true", }, nil)), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -841,15 +865,17 @@ func onAddOrUpdateRemoteServiceUpdated(isAdd bool) *testEnvironment { asYaml(mirrorService("test-service-remote", "test-namespace", "pastResourceVersion", nil)), asYaml(endpoints("test-service-remote", "test-namespace", "0.0.0.0", "", nil)), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } } @@ -865,15 +891,17 @@ func onAddOrUpdateSameResVersion(isAdd bool) *testEnvironment { asYaml(mirrorService("test-service-remote", "test-namespace", "currentResVersion", nil)), asYaml(endpoints("test-service-remote", "test-namespace", "0.0.0.0", "", nil)), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } } @@ -887,15 +915,17 @@ func serviceNotExportedAnymore(isAdd bool) *testEnvironment { asYaml(mirrorService("test-service-remote", "test-namespace", "currentResVersion", nil)), asYaml(endpoints("test-service-remote", "test-namespace", "0.0.0.0", "", nil)), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } } @@ -908,15 +938,17 @@ var onDeleteExportedService = &testEnvironment{ }, nil), }, }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -926,15 +958,17 @@ var onDeleteNonExportedService = &testEnvironment{ svc: remoteService("gateway", "test-namespace", "currentResVersion", map[string]string{}, nil), }, }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "gateway-identity", - GatewayAddress: "192.0.2.127", - GatewayPort: 888, - ProbeSpec: defaultProbeSpec, - Selector: defaultSelector, - RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + GatewayIdentity: "gateway-identity", + GatewayAddress: "192.0.2.127", + GatewayPort: "888", + ProbeSpec: defaultProbeSpec, + Selector: defaultSelector, + RemoteDiscoverySelector: defaultRemoteDiscoverySelector, + }, }, } @@ -1225,7 +1259,7 @@ func asYaml(obj interface{}) string { return string(bytes) } -func gateway(name, namespace, resourceVersion, ip, portName string, port int32, identity string, probePort int32, probePath string, probePeriod int) *corev1.Service { +func gateway(name, namespace, resourceVersion, ip, portName string, port int32, identity string, probePort int32, probePath string, probePeriod string) *corev1.Service { svc := corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", @@ -1238,7 +1272,7 @@ func gateway(name, namespace, resourceVersion, ip, portName string, port int32, Annotations: map[string]string{ consts.GatewayIdentity: identity, consts.GatewayProbePath: probePath, - consts.GatewayProbePeriod: fmt.Sprint(probePeriod), + consts.GatewayProbePeriod: probePeriod, }, }, Spec: corev1.ServiceSpec{ @@ -1451,19 +1485,13 @@ func createEnvWithSelector(defaultSelector, remoteSelector *metav1.LabelSelector asYaml(endpoints("service-one", "ns1", "192.0.2.127", "gateway-identity", []corev1.EndpointPort{})), asYaml(endpoints("service-two", "ns1", "192.0.3.127", "gateway-identity", []corev1.EndpointPort{})), }, - link: multicluster.Link{ - TargetClusterName: clusterName, - TargetClusterDomain: clusterDomain, - GatewayIdentity: "", - GatewayAddress: "", - GatewayPort: 0, - ProbeSpec: multicluster.ProbeSpec{ - Path: "", - Port: 0, - Period: time.Duration(0) * time.Second, + link: v1alpha2.Link{ + Spec: v1alpha2.LinkSpec{ + TargetClusterName: clusterName, + TargetClusterDomain: clusterDomain, + Selector: defaultSelector, + RemoteDiscoverySelector: remoteSelector, }, - Selector: defaultSelector, - RemoteDiscoverySelector: remoteSelector, }, } } diff --git a/multicluster/service-mirror/events_formatting.go b/multicluster/service-mirror/events_formatting.go index bba411b9c4bc7..e0a3eb3da1a42 100644 --- a/multicluster/service-mirror/events_formatting.go +++ b/multicluster/service-mirror/events_formatting.go @@ -38,10 +38,16 @@ func formatPorts(ports []corev1.EndpointPort) string { } func formatService(svc *corev1.Service) string { + if svc == nil { + return "Service: nil" + } return fmt.Sprintf("Service: {name: %s, namespace: %s, annotations: [%s], labels [%s]}", svc.Name, svc.Namespace, formatMetadata(svc.Annotations), formatMetadata(svc.Labels)) } func formatEndpoints(endpoints *corev1.Endpoints) string { + if endpoints == nil { + return "Endpoints: nil" + } var subsets []string for _, ss := range endpoints.Subsets { diff --git a/multicluster/service-mirror/probe_worker.go b/multicluster/service-mirror/probe_worker.go index 33bc947f0be22..42e9989f19342 100644 --- a/multicluster/service-mirror/probe_worker.go +++ b/multicluster/service-mirror/probe_worker.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/linkerd/linkerd2/pkg/multicluster" + "github.com/linkerd/linkerd2/controller/gen/apis/link/v1alpha2" "github.com/prometheus/client_golang/prometheus" logging "github.com/sirupsen/logrus" ) @@ -19,14 +19,14 @@ type ProbeWorker struct { alive bool Liveness chan bool *sync.RWMutex - probeSpec *multicluster.ProbeSpec + probeSpec *v1alpha2.ProbeSpec stopCh chan struct{} metrics *ProbeMetrics log *logging.Entry } // NewProbeWorker creates a new probe worker associated with a particular gateway -func NewProbeWorker(localGatewayName string, spec *multicluster.ProbeSpec, metrics *ProbeMetrics, probekey string) *ProbeWorker { +func NewProbeWorker(localGatewayName string, spec *v1alpha2.ProbeSpec, metrics *ProbeMetrics, probekey string) *ProbeWorker { metrics.gatewayEnabled.Set(1) return &ProbeWorker{ localGatewayName: localGatewayName, @@ -42,7 +42,7 @@ func NewProbeWorker(localGatewayName string, spec *multicluster.ProbeSpec, metri } // UpdateProbeSpec is used to update the probe specification when something about the gateway changes -func (pw *ProbeWorker) UpdateProbeSpec(spec *multicluster.ProbeSpec) { +func (pw *ProbeWorker) UpdateProbeSpec(spec *v1alpha2.ProbeSpec) { pw.Lock() pw.probeSpec = spec pw.Unlock() @@ -66,12 +66,26 @@ func (pw *ProbeWorker) run() { successLabel := prometheus.Labels{probeSuccessfulLabel: "true"} notSuccessLabel := prometheus.Labels{probeSuccessfulLabel: "false"} - probeTickerPeriod := pw.probeSpec.Period - maxJitter := pw.probeSpec.Period / 10 // max jitter is 10% of period + if pw.probeSpec == nil { + pw.log.Error("Probe spec is nil") + return + } + probeTickerPeriodSeconds, err := strconv.ParseInt(pw.probeSpec.Period, 10, 64) + if err != nil { + pw.log.Errorf("could not parse probe period: %s", err) + return + } + probeTickerPeriod := time.Duration(probeTickerPeriodSeconds) * time.Second + maxJitter := probeTickerPeriod / 10 // max jitter is 10% of period probeTicker := NewTicker(probeTickerPeriod, maxJitter) defer probeTicker.Stop() - var failures uint32 = 0 + failureThreshold, err := strconv.ParseUint(pw.probeSpec.FailureThreshold, 10, 32) + if err != nil { + pw.log.Errorf("could not parse failure threshold: %s", err) + return + } + var failures uint64 = 0 probeLoop: for { @@ -83,11 +97,11 @@ probeLoop: if err := pw.doProbe(); err != nil { pw.log.Warn(err) failures++ - if failures < pw.probeSpec.FailureThreshold { + if failures < failureThreshold { continue probeLoop } - pw.log.Warnf("Failure threshold (%d) reached - Marking as unhealthy", pw.probeSpec.FailureThreshold) + pw.log.Warnf("Failure threshold (%s) reached - Marking as unhealthy", pw.probeSpec.FailureThreshold) pw.metrics.alive.Set(0) pw.metrics.probes.With(notSuccessLabel).Inc() if pw.alive { @@ -116,12 +130,15 @@ func (pw *ProbeWorker) doProbe() error { pw.RLock() defer pw.RUnlock() + timeout, err := time.ParseDuration(pw.probeSpec.Timeout) + if err != nil { + return fmt.Errorf("could not parse timeout: %w", err) + } client := http.Client{ - Timeout: pw.probeSpec.Timeout, + Timeout: timeout, } - strPort := strconv.Itoa(int(pw.probeSpec.Port)) - urlAddress := net.JoinHostPort(pw.localGatewayName, strPort) + urlAddress := net.JoinHostPort(pw.localGatewayName, pw.probeSpec.Port) req, err := http.NewRequest("GET", fmt.Sprintf("http://%s%s", urlAddress, pw.probeSpec.Path), nil) if err != nil { return fmt.Errorf("could not create a GET request to gateway: %w", err) diff --git a/pkg/multicluster/link.go b/pkg/multicluster/link.go deleted file mode 100644 index 5fbe9c47fc111..0000000000000 --- a/pkg/multicluster/link.go +++ /dev/null @@ -1,418 +0,0 @@ -package multicluster - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strconv" - "strings" - "time" - - "github.com/linkerd/linkerd2/pkg/k8s" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" -) - -const DefaultFailureThreshold = 3 -const DefaultProbeTimeout = "30s" - -type ( - // ProbeSpec defines how a gateway should be queried for health. Once per - // period, the probe workers will send an HTTP request to the remote gateway - // on the given port with the given path and expect a HTTP 200 response. - ProbeSpec struct { - FailureThreshold uint32 - Path string - Port uint32 - Period time.Duration - Timeout time.Duration - } - - // Link is an internal representation of the link.multicluster.linkerd.io - // custom resource. It defines a multicluster link to a gateway in a - // target cluster and is configures the behavior of a service mirror - // controller. - Link struct { - Name string - Namespace string - CreatedBy string - TargetClusterName string - TargetClusterDomain string - TargetClusterLinkerdNamespace string - ClusterCredentialsSecret string - GatewayAddress string - GatewayPort uint32 - GatewayIdentity string - ProbeSpec ProbeSpec - Selector *metav1.LabelSelector - RemoteDiscoverySelector *metav1.LabelSelector - FederatedServiceSelector *metav1.LabelSelector - } - - ErrFieldMissing struct { - Field string - } -) - -// LinkGVR is the Group Version and Resource of the Link custom resource. -var LinkGVR = schema.GroupVersionResource{ - Group: k8s.LinkAPIGroup, - Version: k8s.LinkAPIVersion, - Resource: "links", -} - -func (ps ProbeSpec) String() string { - return fmt.Sprintf("ProbeSpec: {path: %s, port: %d, period: %s}", ps.Path, ps.Port, ps.Period) -} - -func (e *ErrFieldMissing) Error() string { - return fmt.Sprintf("Field '%s' is missing", e.Field) -} - -// NewLink parses an unstructured link.multicluster.linkerd.io resource and -// converts it to a structured internal representation. -func NewLink(u unstructured.Unstructured) (Link, error) { - - spec, ok := u.Object["spec"] - if !ok { - return Link{}, errors.New("Field 'spec' is missing") - } - specObj, ok := spec.(map[string]interface{}) - if !ok { - return Link{}, errors.New("Field 'spec' is not an object") - } - - ps, ok := specObj["probeSpec"] - if !ok { - return Link{}, errors.New("Field 'probeSpec' is missing") - } - psObj, ok := ps.(map[string]interface{}) - if !ok { - return Link{}, errors.New("Field 'probeSpec' it not an object") - } - - probeSpec, err := newProbeSpec(psObj) - if err != nil { - return Link{}, err - } - - targetClusterName, err := stringField(specObj, "targetClusterName") - if err != nil { - return Link{}, err - } - - targetClusterDomain, err := stringField(specObj, "targetClusterDomain") - if err != nil { - return Link{}, err - } - - targetClusterLinkerdNamespace, err := stringField(specObj, "targetClusterLinkerdNamespace") - if err != nil { - return Link{}, err - } - - clusterCredentialsSecret, err := stringField(specObj, "clusterCredentialsSecret") - if err != nil { - return Link{}, err - } - - gatewayAddress, err := stringField(specObj, "gatewayAddress") - if err != nil { - return Link{}, err - } - - portStr, err := stringField(specObj, "gatewayPort") - if err != nil { - return Link{}, err - } - gatewayPort, err := strconv.ParseUint(portStr, 10, 32) - if err != nil { - return Link{}, err - } - - gatewayIdentity, err := stringField(specObj, "gatewayIdentity") - if err != nil { - return Link{}, err - } - - selector := metav1.LabelSelector{} - if selectorObj, ok := specObj["selector"]; ok { - bytes, err := json.Marshal(selectorObj) - if err != nil { - return Link{}, err - } - err = json.Unmarshal(bytes, &selector) - if err != nil { - return Link{}, err - } - } - - remoteDiscoverySelector := metav1.LabelSelector{} - if selectorObj, ok := specObj["remoteDiscoverySelector"]; ok { - bytes, err := json.Marshal(selectorObj) - if err != nil { - return Link{}, err - } - err = json.Unmarshal(bytes, &remoteDiscoverySelector) - if err != nil { - return Link{}, err - } - } - - federatedServiceSelector := metav1.LabelSelector{} - if selectorObj, ok := specObj["federatedServiceSelector"]; ok { - bytes, err := json.Marshal(selectorObj) - if err != nil { - return Link{}, err - } - err = json.Unmarshal(bytes, &federatedServiceSelector) - if err != nil { - return Link{}, err - } - } - - return Link{ - Name: u.GetName(), - Namespace: u.GetNamespace(), - CreatedBy: u.GetAnnotations()[k8s.CreatedByAnnotation], - TargetClusterName: targetClusterName, - TargetClusterDomain: targetClusterDomain, - TargetClusterLinkerdNamespace: targetClusterLinkerdNamespace, - ClusterCredentialsSecret: clusterCredentialsSecret, - GatewayAddress: gatewayAddress, - GatewayPort: uint32(gatewayPort), - GatewayIdentity: gatewayIdentity, - ProbeSpec: probeSpec, - Selector: &selector, - RemoteDiscoverySelector: &remoteDiscoverySelector, - FederatedServiceSelector: &federatedServiceSelector, - }, nil -} - -// ToUnstructured converts a Link struct into an unstructured resource that can -// be used by a kubernetes dynamic client. -func (l Link) ToUnstructured() (unstructured.Unstructured, error) { - // only specify failureThreshold and timeout if they're not empty, to - // remain compatible with older Link CRDs - probeSpec := map[string]interface{}{ - "path": l.ProbeSpec.Path, - "port": fmt.Sprintf("%d", l.ProbeSpec.Port), - "period": l.ProbeSpec.Period.String(), - } - - if l.ProbeSpec.FailureThreshold > 0 { - probeSpec["failureThreshold"] = fmt.Sprintf("%d", l.ProbeSpec.FailureThreshold) - } - if l.ProbeSpec.Timeout > 0 { - probeSpec["timeout"] = l.ProbeSpec.Timeout.String() - } - - spec := map[string]interface{}{ - "targetClusterName": l.TargetClusterName, - "targetClusterDomain": l.TargetClusterDomain, - "targetClusterLinkerdNamespace": l.TargetClusterLinkerdNamespace, - "clusterCredentialsSecret": l.ClusterCredentialsSecret, - "gatewayAddress": l.GatewayAddress, - "gatewayPort": fmt.Sprintf("%d", l.GatewayPort), - "gatewayIdentity": l.GatewayIdentity, - "probeSpec": probeSpec, - } - - data, err := json.Marshal(l.Selector) - if err != nil { - return unstructured.Unstructured{}, err - } - selector := make(map[string]interface{}) - err = json.Unmarshal(data, &selector) - if err != nil { - return unstructured.Unstructured{}, err - } - spec["selector"] = selector - - data, err = json.Marshal(l.RemoteDiscoverySelector) - if err != nil { - return unstructured.Unstructured{}, err - } - remoteDiscoverySelector := make(map[string]interface{}) - err = json.Unmarshal(data, &remoteDiscoverySelector) - if err != nil { - return unstructured.Unstructured{}, err - } - spec["remoteDiscoverySelector"] = remoteDiscoverySelector - - data, err = json.Marshal(l.FederatedServiceSelector) - if err != nil { - return unstructured.Unstructured{}, err - } - federatedServiceSelector := make(map[string]interface{}) - err = json.Unmarshal(data, &federatedServiceSelector) - if err != nil { - return unstructured.Unstructured{}, err - } - spec["federatedServiceSelector"] = federatedServiceSelector - - return unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": k8s.LinkAPIGroupVersion, - "kind": k8s.LinkKind, - "metadata": map[string]interface{}{ - "name": l.Name, - "namespace": l.Namespace, - "annotations": map[string]string{ - k8s.CreatedByAnnotation: k8s.CreatedByAnnotationValue(), - }, - }, - "spec": spec, - "status": map[string]interface{}{}, - }, - }, nil -} - -// ExtractProbeSpec parses the ProbSpec from a gateway service's annotations. -// For now we're not including the failureThreshold and timeout fields which -// are new since edge-24.9.3, to avoid errors when attempting to apply them in -// clusters with an older Link CRD. -func ExtractProbeSpec(gateway *corev1.Service) (ProbeSpec, error) { - path := gateway.Annotations[k8s.GatewayProbePath] - if path == "" { - return ProbeSpec{}, errors.New("probe path is empty") - } - - port, err := extractPort(gateway.Spec, k8s.ProbePortName) - if err != nil { - return ProbeSpec{}, err - } - - period, err := strconv.ParseUint(gateway.Annotations[k8s.GatewayProbePeriod], 10, 32) - if err != nil { - return ProbeSpec{}, err - } - - return ProbeSpec{ - Path: path, - Port: port, - Period: time.Duration(period) * time.Second, - }, nil -} - -// GetLinks fetches a list of all Link objects in the cluster. -func GetLinks(ctx context.Context, client dynamic.Interface) ([]Link, error) { - list, err := client.Resource(LinkGVR).List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } - links := []Link{} - errs := []string{} - for _, u := range list.Items { - link, err := NewLink(u) - if err != nil { - errs = append(errs, fmt.Sprintf("failed to parse Link %s: %s", u.GetName(), err)) - } else { - links = append(links, link) - } - } - if len(errs) > 0 { - return nil, errors.New(strings.Join(errs, "\n")) - } - return links, nil -} - -// GetLink fetches a Link object from Kubernetes by name/namespace. -func GetLink(ctx context.Context, client dynamic.Interface, namespace, name string) (Link, error) { - unstructured, err := client.Resource(LinkGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return Link{}, err - } - return NewLink(*unstructured) -} - -func extractPort(spec corev1.ServiceSpec, portName string) (uint32, error) { - for _, p := range spec.Ports { - if p.Name == portName { - if spec.Type == "NodePort" { - return uint32(p.NodePort), nil - } - return uint32(p.Port), nil - } - } - return 0, fmt.Errorf("could not find port with name %s", portName) -} - -func newProbeSpec(obj map[string]interface{}) (ProbeSpec, error) { - periodStr, err := stringField(obj, "period") - if err != nil { - return ProbeSpec{}, err - } - period, err := time.ParseDuration(periodStr) - if err != nil { - return ProbeSpec{}, err - } - - failureThresholdStr, err := stringField(obj, "failureThreshold") - if err != nil { - var efm *ErrFieldMissing - if errors.As(err, &efm) { - // older Links might not have this field - failureThresholdStr = fmt.Sprint(DefaultFailureThreshold) - } else { - return ProbeSpec{}, err - } - } - failureThreshold, err := strconv.ParseUint(failureThresholdStr, 10, 32) - if err != nil { - return ProbeSpec{}, err - } - - timeoutStr, err := stringField(obj, "timeout") - if err != nil { - var efm *ErrFieldMissing - if errors.As(err, &efm) { - // older Links might not have this field - timeoutStr = DefaultProbeTimeout - } else { - return ProbeSpec{}, err - } - } - timeout, err := time.ParseDuration(timeoutStr) - if err != nil { - return ProbeSpec{}, err - } - - path, err := stringField(obj, "path") - if err != nil { - return ProbeSpec{}, err - } - - portStr, err := stringField(obj, "port") - if err != nil { - return ProbeSpec{}, err - } - port, err := strconv.ParseUint(portStr, 10, 32) - if err != nil { - return ProbeSpec{}, err - } - - return ProbeSpec{ - FailureThreshold: uint32(failureThreshold), - Path: path, - Port: uint32(port), - Period: period, - Timeout: timeout, - }, nil -} - -func stringField(obj map[string]interface{}, key string) (string, error) { - value, ok := obj[key] - if !ok { - return "", &ErrFieldMissing{Field: key} - } - str, ok := value.(string) - if !ok { - return "", fmt.Errorf("Field '%s' is not a string", key) - } - return str, nil -}