From fd0b756acc7b9ad1d7ba26af674f4ef526b269fa Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Wed, 4 Dec 2024 00:04:08 +0000 Subject: [PATCH 1/8] Replace use of unstructured API with typed bindings for Link Signed-off-by: Alex Leong --- 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 +++ multicluster/cmd/check.go | 118 ++--- multicluster/cmd/link.go | 81 +++- multicluster/cmd/service-mirror/main.go | 210 +++++---- multicluster/cmd/uninstall.go | 10 +- multicluster/cmd/unlink.go | 5 +- .../service-mirror/cluster_watcher.go | 365 ++++++++------- .../cluster_watcher_headless.go | 20 +- .../cluster_watcher_mirroring_test.go | 72 +-- .../cluster_watcher_test_util.go | 440 ++++++++++-------- multicluster/service-mirror/probe_worker.go | 40 +- pkg/multicluster/link.go | 418 ----------------- 34 files changed, 1833 insertions(+), 1024 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/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..3617d6948d00c --- /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 string `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,omitempty"` + // 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/cmd/check.go b/multicluster/cmd/check.go index 9f112c143c82d..89f52d71439cf 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 { errors = append(errors, 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 { errors = append(errors, 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 { errors = append(errors, 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 { errors = append(errors, 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 { errors = append(errors, err.Error()) } - 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(strings.Join(errors, "\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..0019e59ed6b26 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,19 @@ 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{ + ObjectMeta: metav1.ObjectMeta{ + Name: opts.clusterName, + Namespace: opts.namespace, + }, + 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 +279,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 +290,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 +307,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 +579,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..881e16ebf1db3 100644 --- a/multicluster/cmd/service-mirror/main.go +++ b/multicluster/cmd/service-mirror/main.go @@ -9,17 +9,18 @@ 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/tools/cache" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" @@ -83,7 +84,7 @@ 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 + // informer is used as a dynamic client for unstructured access to Link custom // resources. // // controllerK8sAPI is used by the cluster watcher to manage @@ -101,6 +102,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 +119,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 +135,98 @@ 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) + informer := l5dcrdinformer.NewSharedInformerFactory(l5dClient, controllerK8s.ResyncTime) + + _, err := informer.Link().V1alpha2().Links().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(link, *namespace, controllerK8sAPI) + 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 restart the link watch to give it another try + log.Error(err) + time.Sleep(linkWatchRestartAfter) } + } else { + log.Infof("Link %s deleted", linkName) + cleanupWorkers() } } } @@ -291,21 +323,22 @@ func cleanupWorkers() { } } -func loadCredentials(ctx context.Context, link multicluster.Link, namespace string, k8sAPI *k8s.KubernetesAPI) ([]byte, error) { +func loadCredentials(link *v1alpha2.Link, namespace string, k8sAPI *controllerK8s.API) ([]byte, error) { // Load the credentials secret - secret, err := k8sAPI.Interface.CoreV1().Secrets(namespace).Get(ctx, link.ClusterCredentialsSecret, metav1.GetOptions{}) + secret, err := k8sAPI.Secret().Lister().Secrets(namespace).Get(link.Spec.ClusterCredentialsSecret) 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 +348,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 +356,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 +367,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 +400,7 @@ func startLocalClusterWatcher( ctx context.Context, namespace string, controllerK8sAPI *controllerK8s.API, + linkClient l5dcrdclient.Interface, requeueLimit int, repairPeriod time.Duration, enableHeadlessSvc bool, @@ -377,19 +412,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/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..814acbb7437ae 100644 --- a/multicluster/service-mirror/cluster_watcher.go +++ b/multicluster/service-mirror/cluster_watcher.go @@ -3,23 +3,24 @@ package servicemirror import ( "context" "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 +47,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 +192,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 +203,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 +212,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 +221,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 +240,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 +248,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 +278,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 +320,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 +330,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 +352,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 +374,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.ParseUint(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 +444,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 +567,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 +651,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 +707,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 +1001,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 +1035,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 +1092,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 +1148,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 +1349,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 +1412,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 +1440,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 +1488,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 +1522,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 +1538,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.ParseUint(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 +1560,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 +1632,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 +1684,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 +1702,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 +1721,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 +1736,155 @@ 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 get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) + } + link.Status.MirrorServices = updateServiceStatus(remoteName, namespace, condition, link.Status.MirrorServices) + rcsw.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) + _, err = rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Patch( + context.Background(), + link.Name, + types.MergePatchType, + []byte(fmt.Sprintf(`{"status": %s}`, link.Status)), + metav1.PatchOptions{}, + "status", + ) if err != nil { - rcsw.log.Errorf("Failed to update link status: %s", err) + rcsw.log.Errorf("Failed to patch link status %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) } } -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.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) + _, err = rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Patch( + context.Background(), + link.Name, + types.MergePatchType, + []byte(fmt.Sprintf(`{"status": %s}`, link.Status)), + 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 (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("got link status: %s", link.Object) - statuses, found, err := unstructured.NestedSlice(link.Object, "status", statusSection) + 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.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) + _, err = rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Patch( + context.Background(), + link.Name, + types.MergePatchType, + []byte(fmt.Sprintf(`{"status": %s}`, link.Status)), + metav1.PatchOptions{}, + "status", + ) if err != nil { - return err + rcsw.log.Errorf("Failed to patch link status %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) } - if !found { - statuses = make([]interface{}, 0) +} + +func (rcsw *RemoteClusterServiceWatcher) deleteLinkFederatedStatus(remoteName, namespace string) { + 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 get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) + } + link.Status.FederatedServices = deleteServiceStatus(remoteName, namespace, link.Status.FederatedServices) + rcsw.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) + _, err = rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Patch( + context.Background(), + link.Name, + types.MergePatchType, + []byte(fmt.Sprintf(`{"status": %s}`, link.Status)), + 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{} { +func mirrorStatusCondition(success bool, reason string, message string, localRef *corev1.Service) v1alpha2.LinkCondition { status := "True" if !success { status = "False" } - 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..fc8f9a037ff97 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,10 +393,11 @@ 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) } + k8s.NewFakeAPIWithL5dClient() remoteAPI.Sync(nil) localAPI.Sync(nil) @@ -404,18 +405,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 +486,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 +497,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 +637,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 +648,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..63d05a68676eb 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 = "60s" ) 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: "120s", + }, + 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/probe_worker.go b/multicluster/service-mirror/probe_worker.go index 33bc947f0be22..09f10940a6ccc 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,25 @@ 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 + } + probeTickerPeriod, err := time.ParseDuration(pw.probeSpec.Period) + if err != nil { + pw.log.Errorf("could not parse probe period: %s", err) + return + } + 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 +96,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 +129,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 -} From df34c379ea509674d7c74b91cfc4a55802bd61ec Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Wed, 4 Dec 2024 00:08:55 +0000 Subject: [PATCH 2/8] update comment Signed-off-by: Alex Leong --- multicluster/cmd/service-mirror/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/multicluster/cmd/service-mirror/main.go b/multicluster/cmd/service-mirror/main.go index 881e16ebf1db3..e4690543bd33b 100644 --- a/multicluster/cmd/service-mirror/main.go +++ b/multicluster/cmd/service-mirror/main.go @@ -84,8 +84,8 @@ func Main(args []string) { }() // We create two different kubernetes API clients for the local cluster: - // informer 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. From bc8e95abb702f729e777b66b6cdba8b3f045b56d Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Wed, 4 Dec 2024 01:05:17 +0000 Subject: [PATCH 3/8] add type meta to link command Signed-off-by: Alex Leong --- multicluster/cmd/link.go | 1 + multicluster/service-mirror/cluster_watcher.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/multicluster/cmd/link.go b/multicluster/cmd/link.go index 0019e59ed6b26..b6899bc047aaa 100644 --- a/multicluster/cmd/link.go +++ b/multicluster/cmd/link.go @@ -244,6 +244,7 @@ A full list of configurable values can be found at https://github.com/linkerd/li } link := v1alpha2.Link{ + TypeMeta: metav1.TypeMeta{Kind: "Link", APIVersion: "multicluster.linkerd.io/v1alpha2"}, ObjectMeta: metav1.ObjectMeta{ Name: opts.clusterName, Namespace: opts.namespace, diff --git a/multicluster/service-mirror/cluster_watcher.go b/multicluster/service-mirror/cluster_watcher.go index 814acbb7437ae..83743cf028e7b 100644 --- a/multicluster/service-mirror/cluster_watcher.go +++ b/multicluster/service-mirror/cluster_watcher.go @@ -375,7 +375,7 @@ func (rcsw *RemoteClusterServiceWatcher) mirrorNamespaceIfNecessary(ctx context. // (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, error) { - gatewayPort, err := strconv.ParseUint(rcsw.link.Spec.GatewayPort, 10, 32) + gatewayPort, err := strconv.ParseInt(rcsw.link.Spec.GatewayPort, 10, 32) if err != nil { return nil, err } @@ -1539,7 +1539,7 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) er // 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.Spec.TargetClusterName) - probePort, err := strconv.ParseUint(rcsw.link.Spec.ProbeSpec.Port, 10, 32) + probePort, err := strconv.ParseInt(rcsw.link.Spec.ProbeSpec.Port, 10, 32) if err != nil { return fmt.Errorf("failed to parse probe port: %w", err) } From 44de64ad7645ba3937d78d361c61dfa3072734d9 Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Thu, 5 Dec 2024 03:49:23 +0000 Subject: [PATCH 4/8] fixes Signed-off-by: Alex Leong --- controller/gen/apis/link/v1alpha2/types.go | 2 +- .../templates/service-mirror.yaml | 2 +- multicluster/cmd/service-mirror/main.go | 23 +++++--- .../service-mirror/cluster_watcher.go | 52 +++++-------------- .../service-mirror/events_formatting.go | 6 +++ 5 files changed, 38 insertions(+), 47 deletions(-) diff --git a/controller/gen/apis/link/v1alpha2/types.go b/controller/gen/apis/link/v1alpha2/types.go index 3617d6948d00c..2f868320f0bc2 100644 --- a/controller/gen/apis/link/v1alpha2/types.go +++ b/controller/gen/apis/link/v1alpha2/types.go @@ -87,7 +87,7 @@ type LinkCondition struct { Reason string `json:"reason,omitempty"` // Human readable message that describes details about last transition. // +optional - Message string `json:"message,omitempty"` + Message string `json:"message"` // LocalRef is a reference to the local mirror or federated service. LocalRef ObjectRef `json:"localRef,omitempty"` } 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/service-mirror/main.go b/multicluster/cmd/service-mirror/main.go index e4690543bd33b..39a094d2d37c0 100644 --- a/multicluster/cmd/service-mirror/main.go +++ b/multicluster/cmd/service-mirror/main.go @@ -20,6 +20,7 @@ import ( sm "github.com/linkerd/linkerd2/pkg/servicemirror" log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/leaderelection" @@ -139,9 +140,16 @@ func Main(args []string) { // Use a small buffered channel for Link updates to avoid dropping // updates if there is an update burst. results := make(chan *v1alpha2.Link, 100) - informer := l5dcrdinformer.NewSharedInformerFactory(l5dClient, controllerK8s.ResyncTime) - - _, err := informer.Link().V1alpha2().Links().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + 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 { @@ -213,16 +221,17 @@ func Main(args []string) { case link := <-results: if link != nil { log.Infof("Got updated link %s: %+v", linkName, link) - creds, err := loadCredentials(link, *namespace, controllerK8sAPI) + 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 restart the link watch to give it another try + // and requeue the link to give it another try log.Error(err) time.Sleep(linkWatchRestartAfter) + results <- link } } else { log.Infof("Link %s deleted", linkName) @@ -323,9 +332,9 @@ func cleanupWorkers() { } } -func loadCredentials(link *v1alpha2.Link, namespace string, k8sAPI *controllerK8s.API) ([]byte, error) { +func loadCredentials(ctx context.Context, link *v1alpha2.Link, namespace string, k8sAPI kubernetes.Interface) ([]byte, error) { // Load the credentials secret - secret, err := k8sAPI.Secret().Lister().Secrets(namespace).Get(link.Spec.ClusterCredentialsSecret) + 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.Spec.ClusterCredentialsSecret, err) } diff --git a/multicluster/service-mirror/cluster_watcher.go b/multicluster/service-mirror/cluster_watcher.go index 83743cf028e7b..056fd74d84aca 100644 --- a/multicluster/service-mirror/cluster_watcher.go +++ b/multicluster/service-mirror/cluster_watcher.go @@ -2,6 +2,7 @@ package servicemirror import ( "context" + "encoding/json" "errors" "fmt" "net" @@ -1746,18 +1747,7 @@ func (rcsw *RemoteClusterServiceWatcher) updateLinkMirrorStatus(remoteName, name 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.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) - _, err = rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Patch( - context.Background(), - link.Name, - types.MergePatchType, - []byte(fmt.Sprintf(`{"status": %s}`, link.Status)), - metav1.PatchOptions{}, - "status", - ) - if err != nil { - rcsw.log.Errorf("Failed to patch link status %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) - } + rcsw.patchLinkStatus(link.Status) } func (rcsw *RemoteClusterServiceWatcher) updateLinkFederatedStatus(remoteName, namespace string, condition v1alpha2.LinkCondition) { @@ -1770,18 +1760,7 @@ func (rcsw *RemoteClusterServiceWatcher) updateLinkFederatedStatus(remoteName, n 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.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) - _, err = rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Patch( - context.Background(), - link.Name, - types.MergePatchType, - []byte(fmt.Sprintf(`{"status": %s}`, link.Status)), - metav1.PatchOptions{}, - "status", - ) - if err != nil { - rcsw.log.Errorf("Failed to patch link status %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) - } + rcsw.patchLinkStatus(link.Status) } func (rcsw *RemoteClusterServiceWatcher) deleteLinkMirrorStatus(remoteName, namespace string) { @@ -1794,18 +1773,7 @@ func (rcsw *RemoteClusterServiceWatcher) deleteLinkMirrorStatus(remoteName, name 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.log.Infof("patching link status %s/%s", rcsw.link.Namespace, rcsw.link.Name) - _, err = rcsw.linkClient.LinkV1alpha2().Links(rcsw.link.GetNamespace()).Patch( - context.Background(), - link.Name, - types.MergePatchType, - []byte(fmt.Sprintf(`{"status": %s}`, link.Status)), - metav1.PatchOptions{}, - "status", - ) - if err != nil { - rcsw.log.Errorf("Failed to patch link status %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) - } + rcsw.patchLinkStatus(link.Status) } func (rcsw *RemoteClusterServiceWatcher) deleteLinkFederatedStatus(remoteName, namespace string) { @@ -1818,12 +1786,20 @@ func (rcsw *RemoteClusterServiceWatcher) deleteLinkFederatedStatus(remoteName, n rcsw.log.Errorf("Failed to get link %s/%s: %s", rcsw.link.Namespace, rcsw.link.Name, err) } 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(), - link.Name, + rcsw.link.Name, types.MergePatchType, - []byte(fmt.Sprintf(`{"status": %s}`, link.Status)), + []byte(fmt.Sprintf(`{"status": %s}`, string(statusBytes))), metav1.PatchOptions{}, "status", ) 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 { From 073450f8384e416cd2c7ff9816d4a6c428105755 Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Thu, 5 Dec 2024 18:05:35 +0000 Subject: [PATCH 5/8] update golden files Signed-off-by: Alex Leong --- multicluster/cmd/testdata/service_mirror_default.golden | 2 +- multicluster/cmd/testdata/service_mirror_ha.golden | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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"] From f7c55896239751af1701246dd77897eefe7df583 Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Fri, 6 Dec 2024 01:08:10 +0000 Subject: [PATCH 6/8] probe period annotation doesn't have units Signed-off-by: Alex Leong --- multicluster/service-mirror/cluster_watcher_test_util.go | 4 ++-- multicluster/service-mirror/probe_worker.go | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/multicluster/service-mirror/cluster_watcher_test_util.go b/multicluster/service-mirror/cluster_watcher_test_util.go index 63d05a68676eb..339e67d4833c7 100644 --- a/multicluster/service-mirror/cluster_watcher_test_util.go +++ b/multicluster/service-mirror/cluster_watcher_test_util.go @@ -22,7 +22,7 @@ const ( clusterDomain = "cluster.local" defaultProbePath = "/probe" defaultProbePort = 12345 - defaultProbePeriod = "60s" + defaultProbePeriod = "60" ) var ( @@ -500,7 +500,7 @@ var createExportedHeadlessService = &testEnvironment{ ProbeSpec: v1alpha2.ProbeSpec{ Port: "123456", Path: "/probe1", - Period: "120s", + Period: "120", }, Selector: defaultSelector, RemoteDiscoverySelector: defaultRemoteDiscoverySelector, diff --git a/multicluster/service-mirror/probe_worker.go b/multicluster/service-mirror/probe_worker.go index 09f10940a6ccc..42e9989f19342 100644 --- a/multicluster/service-mirror/probe_worker.go +++ b/multicluster/service-mirror/probe_worker.go @@ -70,11 +70,12 @@ func (pw *ProbeWorker) run() { pw.log.Error("Probe spec is nil") return } - probeTickerPeriod, err := time.ParseDuration(pw.probeSpec.Period) + 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() From 80f89defd9e5cf9cdc0fcb1a2c2feef479602ce4 Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Fri, 6 Dec 2024 01:59:56 +0000 Subject: [PATCH 7/8] add created by annotation to Links Signed-off-by: Alex Leong --- multicluster/cmd/link.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/multicluster/cmd/link.go b/multicluster/cmd/link.go index b6899bc047aaa..045d2d5070578 100644 --- a/multicluster/cmd/link.go +++ b/multicluster/cmd/link.go @@ -248,6 +248,9 @@ A full list of configurable values can be found at https://github.com/linkerd/li ObjectMeta: metav1.ObjectMeta{ Name: opts.clusterName, Namespace: opts.namespace, + Annotations: map[string]string{ + k8s.CreatedByAnnotation: k8s.CreatedByAnnotationValue(), + }, }, Spec: v1alpha2.LinkSpec{ TargetClusterName: opts.clusterName, From 4e90e95b514205ef4189e5ea9950e525813b72ad Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Sat, 7 Dec 2024 00:12:29 +0000 Subject: [PATCH 8/8] feedback Signed-off-by: Alex Leong --- bin/update-codegen.sh | 5 ++--- controller/gen/apis/link/v1alpha2/types.go | 2 +- multicluster/service-mirror/cluster_watcher.go | 4 ++-- .../service-mirror/cluster_watcher_mirroring_test.go | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) 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/v1alpha2/types.go b/controller/gen/apis/link/v1alpha2/types.go index 2f868320f0bc2..2ea3a6fb7b029 100644 --- a/controller/gen/apis/link/v1alpha2/types.go +++ b/controller/gen/apis/link/v1alpha2/types.go @@ -74,7 +74,7 @@ type LinkCondition struct { Type string `json:"type"` // Status of the condition. // Can be True, False, Unknown - Status string `json:"status"` + Status metav1.ConditionStatus `json:"status"` // Last time an ExternalWorkload was probed for a condition. // +optional LastProbeTime metav1.Time `json:"lastProbeTime,omitempty"` diff --git a/multicluster/service-mirror/cluster_watcher.go b/multicluster/service-mirror/cluster_watcher.go index 056fd74d84aca..29a6d4d235044 100644 --- a/multicluster/service-mirror/cluster_watcher.go +++ b/multicluster/service-mirror/cluster_watcher.go @@ -1844,9 +1844,9 @@ func deleteServiceStatus(remoteName, namespace string, statuses []v1alpha2.Servi } func mirrorStatusCondition(success bool, reason string, message string, localRef *corev1.Service) v1alpha2.LinkCondition { - status := "True" + status := metav1.ConditionTrue if !success { - status = "False" + status = metav1.ConditionFalse } condition := v1alpha2.LinkCondition{ LastTransitionTime: metav1.Now(), diff --git a/multicluster/service-mirror/cluster_watcher_mirroring_test.go b/multicluster/service-mirror/cluster_watcher_mirroring_test.go index fc8f9a037ff97..8d2af3074bce6 100644 --- a/multicluster/service-mirror/cluster_watcher_mirroring_test.go +++ b/multicluster/service-mirror/cluster_watcher_mirroring_test.go @@ -397,7 +397,6 @@ func TestLocalNamespaceCreatedAfterServiceExport(t *testing.T) { if err != nil { t.Fatal(err) } - k8s.NewFakeAPIWithL5dClient() remoteAPI.Sync(nil) localAPI.Sync(nil)