diff --git a/apis/ipam/v1alpha1/ip_types.go b/apis/ipam/v1alpha1/ip_types.go index b95d403828..475e9ee3c3 100644 --- a/apis/ipam/v1alpha1/ip_types.go +++ b/apis/ipam/v1alpha1/ip_types.go @@ -15,6 +15,7 @@ package v1alpha1 import ( + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -33,10 +34,22 @@ var ( IPGroupResource = schema.GroupResource{Group: GroupVersion.Group, Resource: IPResource} ) +// ServiceTemplate contains the template to create the associated service (and endpointslice) for the IP endopoint. +type ServiceTemplate struct { + // Metadata of the Service. + Metadata metav1.ObjectMeta `json:"metadata,omitempty"` + // Template Spec of the Service. + Spec v1.ServiceSpec `json:"spec,omitempty"` +} + // IPSpec defines a local IP. type IPSpec struct { // IP is the local IP. IP string `json:"ip"` + // ServiceTemplate contains the template to create the associated service (and endpointslice) for the IP endopoint. + // If empty the creation of the service is disabled (default). + // +kubebuilder:validation:Optional + ServiceTemplate *ServiceTemplate `json:"serviceTemplate,omitempty"` } // IPStatus defines remapped IPs. @@ -46,6 +59,7 @@ type IPStatus struct { } // +kubebuilder:object:root=true +// +kubebuilder:resource:categories=liqo // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Local IP",type=string,JSONPath=`.spec.ip` // +kubebuilder:printcolumn:name="Remapped IPs",type=string,JSONPath=`.status.ipMappings`,priority=1 diff --git a/apis/ipam/v1alpha1/network_types.go b/apis/ipam/v1alpha1/network_types.go index e2bbd0e026..07d16ce748 100644 --- a/apis/ipam/v1alpha1/network_types.go +++ b/apis/ipam/v1alpha1/network_types.go @@ -46,6 +46,7 @@ type NetworkStatus struct { } // +kubebuilder:object:root=true +// +kubebuilder:resource:categories=liqo // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Desired CIDR",type=string,JSONPath=`.spec.cidr` // +kubebuilder:printcolumn:name="Remapped CIDR",type=string,JSONPath=`.status.cidr` diff --git a/apis/ipam/v1alpha1/zz_generated.deepcopy.go b/apis/ipam/v1alpha1/zz_generated.deepcopy.go index 3e6627aca2..a5836d717c 100644 --- a/apis/ipam/v1alpha1/zz_generated.deepcopy.go +++ b/apis/ipam/v1alpha1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated // Copyright 2019-2023 The Liqo Authors // @@ -28,7 +27,7 @@ func (in *IP) DeepCopyInto(out *IP) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -85,6 +84,11 @@ func (in *IPList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPSpec) DeepCopyInto(out *IPSpec) { *out = *in + if in.ServiceTemplate != nil { + in, out := &in.ServiceTemplate, &out.ServiceTemplate + *out = new(ServiceTemplate) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPSpec. @@ -207,3 +211,20 @@ func (in *NetworkStatus) DeepCopy() *NetworkStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceTemplate) DeepCopyInto(out *ServiceTemplate) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTemplate. +func (in *ServiceTemplate) DeepCopy() *ServiceTemplate { + if in == nil { + return nil + } + out := new(ServiceTemplate) + in.DeepCopyInto(out) + return out +} diff --git a/deployments/liqo/crds/ipam.liqo.io_ips.yaml b/deployments/liqo/crds/ipam.liqo.io_ips.yaml index 00ddee8198..8590171c39 100644 --- a/deployments/liqo/crds/ipam.liqo.io_ips.yaml +++ b/deployments/liqo/crds/ipam.liqo.io_ips.yaml @@ -3,12 +3,13 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.9.2 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.13.0 name: ips.ipam.liqo.io spec: group: ipam.liqo.io names: + categories: + - liqo kind: IP listKind: IPList plural: ips @@ -49,6 +50,372 @@ spec: ip: description: IP is the local IP. type: string + serviceTemplate: + description: ServiceTemplate contains the template to create the associated + service (and endpointslice) for the IP endopoint. If empty the creation + of the service is disabled (default). + properties: + metadata: + description: Metadata of the Service. + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + description: Template Spec of the Service. + properties: + allocateLoadBalancerNodePorts: + description: allocateLoadBalancerNodePorts defines if NodePorts + will be automatically allocated for services with type LoadBalancer. Default + is "true". It may be set to "false" if the cluster load-balancer + does not rely on NodePorts. If the caller requests specific + NodePorts (by specifying a value), those requests will be + respected, regardless of this field. This field may only + be set for services with type LoadBalancer and will be cleared + if the type is changed to any other type. + type: boolean + clusterIP: + description: 'clusterIP is the IP address of the service and + is usually assigned randomly. If an address is specified + manually, is in-range (as per system configuration), and + is not in use, it will be allocated to the service; otherwise + creation of the service will fail. This field may not be + changed through updates unless the type field is also being + changed to ExternalName (which requires this field to be + blank) or the type field is being changed from ExternalName + (in which case this field may optionally be specified, as + describe above). Valid values are "None", empty string + (""), or a valid IP address. Setting this to "None" makes + a "headless service" (no virtual IP), which is useful when + direct endpoint connections are preferred and proxying is + not required. Only applies to types ClusterIP, NodePort, + and LoadBalancer. If this field is specified when creating + a Service of type ExternalName, creation will fail. This + field will be wiped when updating a Service to type ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + clusterIPs: + description: "ClusterIPs is a list of IP addresses assigned + to this service, and are usually assigned randomly. If + an address is specified manually, is in-range (as per system + configuration), and is not in use, it will be allocated + to the service; otherwise creation of the service will fail. + This field may not be changed through updates unless the + type field is also being changed to ExternalName (which + requires this field to be empty) or the type field is being + changed from ExternalName (in which case this field may + optionally be specified, as describe above). Valid values + are \"None\", empty string (\"\"), or a valid IP address. + \ Setting this to \"None\" makes a \"headless service\" + (no virtual IP), which is useful when direct endpoint connections + are preferred and proxying is not required. Only applies + to types ClusterIP, NodePort, and LoadBalancer. If this + field is specified when creating a Service of type ExternalName, + creation will fail. This field will be wiped when updating + a Service to type ExternalName. If this field is not specified, + it will be initialized from the clusterIP field. If this + field is specified, clients must ensure that clusterIPs[0] + and clusterIP have the same value. \n This field may hold + a maximum of two entries (dual-stack IPs, in either order). + These IPs must correspond to the values of the ipFamilies + field. Both clusterIPs and ipFamilies are governed by the + ipFamilyPolicy field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies" + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + description: externalIPs is a list of IP addresses for which + nodes in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that discovery + mechanisms will return as an alias for this service (e.g. + a DNS CNAME record). No proxying will be involved. Must + be a lowercase RFC-1123 hostname (https://tools.ietf.org/html/rfc1123) + and requires `type` to be "ExternalName". + type: string + externalTrafficPolicy: + description: externalTrafficPolicy describes how nodes distribute + service traffic they receive on one of the Service's "externally-facing" + addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). + If set to "Local", the proxy will configure the service + in a way that assumes that external load balancers will + take care of balancing the service traffic between nodes, + and so each node will deliver traffic only to the node-local + endpoints of the service, without masquerading the client + source IP. (Traffic mistakenly sent to a node with no endpoints + will be dropped.) The default value, "Cluster", uses the + standard behavior of routing to all endpoints evenly (possibly + modified by topology and other features). Note that traffic + sent to an External IP or LoadBalancer IP from within the + cluster will always get "Cluster" semantics, but clients + sending to a NodePort from within the cluster may need to + take traffic policy into account when picking a node. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck + nodePort for the service. This only applies when type is + set to LoadBalancer and externalTrafficPolicy is set to + Local. If a value is specified, is in-range, and is not + in use, it will be used. If not specified, a value will + be automatically allocated. External systems (e.g. load-balancers) + can use this port to determine if a given node holds endpoints + for this service or not. If this field is specified when + creating a Service which does not need it, creation will + fail. This field will be wiped when updating a Service to + no longer need it (e.g. changing type). This field cannot + be updated once set. + format: int32 + type: integer + internalTrafficPolicy: + description: InternalTrafficPolicy describes how nodes distribute + service traffic they receive on the ClusterIP. If set to + "Local", the proxy will assume that pods only want to talk + to endpoints of the service on the same node as the pod, + dropping the traffic if there are no local endpoints. The + default value, "Cluster", uses the standard behavior of + routing to all endpoints evenly (possibly modified by topology + and other features). + type: string + ipFamilies: + description: "IPFamilies is a list of IP families (e.g. IPv4, + IPv6) assigned to this service. This field is usually assigned + automatically based on cluster configuration and the ipFamilyPolicy + field. If this field is specified manually, the requested + family is available in the cluster, and ipFamilyPolicy allows + it, it will be used; otherwise creation of the service will + fail. This field is conditionally mutable: it allows for + adding or removing a secondary IP family, but it does not + allow changing the primary IP family of the Service. Valid + values are \"IPv4\" and \"IPv6\". This field only applies + to Services of types ClusterIP, NodePort, and LoadBalancer, + and does apply to \"headless\" services. This field will + be wiped when updating a Service to type ExternalName. \n + This field may hold a maximum of two entries (dual-stack + families, in either order). These families must correspond + to the values of the clusterIPs field, if specified. Both + clusterIPs and ipFamilies are governed by the ipFamilyPolicy + field." + items: + description: IPFamily represents the IP Family (IPv4 or + IPv6). This type is used to express the family of an IP + expressed by a type (e.g. service.spec.ipFamilies). + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + description: IPFamilyPolicy represents the dual-stack-ness + requested or required by this Service. If there is no value + provided, then this field will be set to SingleStack. Services + can be "SingleStack" (a single IP family), "PreferDualStack" + (two IP families on dual-stack configured clusters or a + single IP family on single-stack clusters), or "RequireDualStack" + (two IP families on dual-stack configured clusters, otherwise + fail). The ipFamilies and clusterIPs fields depend on the + value of this field. This field will be wiped when updating + a service to type ExternalName. + type: string + loadBalancerClass: + description: loadBalancerClass is the class of the load balancer + implementation this Service belongs to. If specified, the + value of this field must be a label-style identifier, with + an optional prefix, e.g. "internal-vip" or "example.com/internal-vip". + Unprefixed names are reserved for end-users. This field + can only be set when the Service type is 'LoadBalancer'. + If not set, the default load balancer implementation is + used, today this is typically done through the cloud provider + integration, but should apply for any default implementation. + If set, it is assumed that a load balancer implementation + is watching for Services with a matching class. Any default + load balancer implementation (e.g. cloud providers) should + ignore Services that set this field. This field can only + be set when creating or updating a Service to type 'LoadBalancer'. + Once set, it can not be changed. This field will be wiped + when a service is updated to a non 'LoadBalancer' type. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer. + This feature depends on whether the underlying cloud-provider + supports specifying the loadBalancerIP when a load balancer + is created. This field will be ignored if the cloud-provider + does not support the feature. Deprecated: This field was + under-specified and its meaning varies across implementations. + Using it is non-portable and it may not support dual-stack. + Users are encouraged to use implementation-specific annotations + when available.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, + this will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field + will be ignored if the cloud-provider does not support the + feature." More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's + port. + properties: + appProtocol: + description: "The application protocol for this port. + This is used as a hint for implementations to offer + richer behavior for protocols that they understand. + This field follows standard Kubernetes label syntax. + Valid values are either: \n * Un-prefixed protocol + names - reserved for IANA standard service names (as + per RFC-6335 and https://www.iana.org/assignments/service-names). + \n * Kubernetes-defined prefixed names: * 'kubernetes.io/h2c' + - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 + * 'kubernetes.io/ws' - WebSocket over cleartext as + described in https://www.rfc-editor.org/rfc/rfc6455 + * 'kubernetes.io/wss' - WebSocket over TLS as described + in https://www.rfc-editor.org/rfc/rfc6455 \n * Other + protocols should use implementation-defined prefixed + names such as mycompany.com/my-custom-protocol." + type: string + name: + description: The name of this port within the service. + This must be a DNS_LABEL. All ports within a ServiceSpec + must have unique names. When considering the endpoints + for a Service, this must match the 'name' field in + the EndpointPort. Optional if only one ServicePort + is defined on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type is NodePort or LoadBalancer. Usually + assigned by the system. If a value is specified, in-range, + and not in use it will be used, otherwise the operation + will fail. If not specified, a port will be allocated + if this Service requires one. If this field is specified + when creating a Service which does not need it, creation + will fail. This field will be wiped when updating + a Service to no longer need it (e.g. changing type + from NodePort to ClusterIP). More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP + description: The IP protocol for this port. Supports + "TCP", "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on + the pods targeted by the service. Number must be in + the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named + port in the target Pod''s container ports. If this + is not specified, the value of the ''port'' field + is used (an identity map). This field is ignored for + services with clusterIP=None, and should be omitted + or set equal to the ''port'' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + description: publishNotReadyAddresses indicates that any agent + which deals with endpoints for this Service should disregard + any indications of ready/not-ready. The primary use case + for setting this field is for a StatefulSet's Headless Service + to propagate SRV DNS records for its Pods for the purpose + of peer discovery. The Kubernetes controllers that generate + Endpoints and EndpointSlice resources for Services interpret + this to mean that all endpoints are considered "ready" even + if the Pods themselves are not. Agents which consume only + Kubernetes generated endpoints through the Endpoints or + EndpointSlice resources can safely assume this behavior. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys + and values matching this selector. If empty or not present, + the service is assumed to have an external process managing + its endpoints, which Kubernetes will not modify. Only applies + to types ClusterIP, NodePort, and LoadBalancer. Ignored + if type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. + Must be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds + of ClientIP type session sticky time. The value + must be >0 && <=86400(for 1 day) if ServiceAffinity + == "ClientIP". Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. + Defaults to ClusterIP. Valid options are ExternalName, ClusterIP, + NodePort, and LoadBalancer. "ClusterIP" allocates a cluster-internal + IP address for load-balancing to endpoints. Endpoints are + determined by the selector or if that is not specified, + by manual construction of an Endpoints object or EndpointSlice + objects. If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints rather + than a virtual IP. "NodePort" builds on ClusterIP and allocates + a port on every node which routes to the same endpoints + as the clusterIP. "LoadBalancer" builds on NodePort and + creates an external load-balancer (if supported in the current + cloud) which routes to the same endpoints as the clusterIP. + "ExternalName" aliases this service to the specified externalName. + Several other fields do not apply to ExternalName services. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + type: object required: - ip type: object diff --git a/deployments/liqo/crds/ipam.liqo.io_networks.yaml b/deployments/liqo/crds/ipam.liqo.io_networks.yaml index 8e4882c4be..d9b5dcdc8b 100644 --- a/deployments/liqo/crds/ipam.liqo.io_networks.yaml +++ b/deployments/liqo/crds/ipam.liqo.io_networks.yaml @@ -3,12 +3,13 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.9.2 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.13.0 name: networks.ipam.liqo.io spec: group: ipam.liqo.io names: + categories: + - liqo kind: Network listKind: NetworkList plural: networks diff --git a/deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml b/deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml index 4d01a1b812..6e88c61213 100644 --- a/deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml +++ b/deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml @@ -163,6 +163,18 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - discovery.k8s.io resources: diff --git a/pkg/consts/replication.go b/pkg/consts/replication.go index 4dcf44de41..7f81360a5c 100644 --- a/pkg/consts/replication.go +++ b/pkg/consts/replication.go @@ -77,6 +77,9 @@ const ( // PodAntiAffinityPresetKey is the annotation key used to express an anti-affinity preset to apply to offloaded pods. PodAntiAffinityPresetKey = "liqo.io/anti-affinity-preset" + // VKSkipUnmapIPAnnotationKey is the annotation key used to tell the VK to skip the unmapping of the IP as already managed by another entity. + VKSkipUnmapIPAnnotationKey = "liqo.io/vk-skip-unmap-ip" + // PodAntiAffinityPresetValueSoft is the annotation value corresponding to the "soft" anti-affinity preset (i.e., preferred). PodAntiAffinityPresetValueSoft = "soft" diff --git a/pkg/liqo-controller-manager/ip-controller/exposition.go b/pkg/liqo-controller-manager/ip-controller/exposition.go new file mode 100644 index 0000000000..003b842371 --- /dev/null +++ b/pkg/liqo-controller-manager/ip-controller/exposition.go @@ -0,0 +1,132 @@ +// Copyright 2019-2023 The Liqo 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. + +package ipctrl + +import ( + "context" + + v1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + ipamv1alpha1 "github.com/liqotech/liqo/apis/ipam/v1alpha1" + "github.com/liqotech/liqo/pkg/consts" +) + +// handleAssociatedService creates, updates or deletes the service associated to the IP. +func (r *IPReconciler) handleAssociatedService(ctx context.Context, ip *ipamv1alpha1.IP) error { + // Service associated to the IP + svc := v1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: ip.Name, + Namespace: ip.Namespace, + }} + svcMutateFn := func() error { + svc.SetLabels(labels.Merge(svc.GetLabels(), ip.Spec.ServiceTemplate.Metadata.GetLabels())) + svc.SetAnnotations(labels.Merge(svc.GetAnnotations(), ip.Spec.ServiceTemplate.Metadata.GetAnnotations())) + svc.Spec = ip.Spec.ServiceTemplate.Spec + return controllerutil.SetControllerReference(ip, &svc, r.Scheme) + } + + // EndpointSlice associated to the Service + eps := discoveryv1.EndpointSlice{ObjectMeta: metav1.ObjectMeta{ + Name: ip.Name, + Namespace: ip.Namespace, + }} + epsMutateFn := func() error { + eps.SetLabels(labels.Merge(eps.GetLabels(), labels.Set{discoveryv1.LabelServiceName: svc.Name})) + eps.SetAnnotations(labels.Merge(eps.GetAnnotations(), labels.Set{consts.VKSkipUnmapIPAnnotationKey: "true"})) + eps.AddressType = discoveryv1.AddressTypeIPv4 + eps.Endpoints = []discoveryv1.Endpoint{ + { + Addresses: []string{ip.Spec.IP}, + Conditions: discoveryv1.EndpointConditions{ + Ready: pointer.Bool(true), + }, + }, + } + var ports []discoveryv1.EndpointPort + for i := range ip.Spec.ServiceTemplate.Spec.Ports { + ports = append(ports, discoveryv1.EndpointPort{ + Name: &ip.Spec.ServiceTemplate.Spec.Ports[i].Name, + Protocol: &ip.Spec.ServiceTemplate.Spec.Ports[i].Protocol, + Port: &ip.Spec.ServiceTemplate.Spec.Ports[i].Port, + AppProtocol: ip.Spec.ServiceTemplate.Spec.Ports[i].AppProtocol, + }) + } + eps.Ports = ports + + return controllerutil.SetControllerReference(ip, &eps, r.Scheme) + } + + // Create service and endpointslice if the template is defined + if ip.Spec.ServiceTemplate != nil { + if err := enforceResource(ctx, r.Client, &svc, svcMutateFn, "service"); err != nil { + return err + } + if err := enforceResource(ctx, r.Client, &eps, epsMutateFn, "endpointslice"); err != nil { + return err + } + } else { + // Service spec is not defined, delete the associated service and endpointslices if previously created + if err := ensureResourceAbsence(ctx, r.Client, &svc, "service"); err != nil { + return err + } + if err := ensureResourceAbsence(ctx, r.Client, &eps, "endpointslice"); err != nil { + return err + } + } + + return nil +} + +// enforceResource ensures that the given resource exists. +// It either creates or update the resource. +func enforceResource(ctx context.Context, r client.Client, obj client.Object, mutateFn controllerutil.MutateFn, resourceKind string) error { + op, err := controllerutil.CreateOrUpdate(ctx, r, obj, mutateFn) + if err != nil { + klog.Errorf("error while creating/updating %s %q (operation: %s): %v", resourceKind, obj.GetName(), op, err) + return err + } + klog.Infof("%s %q correctly enforced (operation: %s)", resourceKind, obj.GetName(), op) + return nil +} + +// ensureResourceAbsence ensures that the given resource does not exist. +// If the resource does not exist, it does nothing. +func ensureResourceAbsence(ctx context.Context, r client.Client, obj client.Object, resourceKind string) error { + err := r.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, obj) + switch { + case err != nil && !apierrors.IsNotFound(err): + klog.Errorf("error while getting %s %q: %v", resourceKind, obj.GetName(), err) + return err + case apierrors.IsNotFound(err): + // The resource does not exist, do nothing. + klog.V(6).Infof("%s %q does not exist. Nothing to do", resourceKind, obj.GetName()) + default: + if err := r.Delete(ctx, obj); err != nil { + klog.Errorf("error while deleting %s %q: %v", resourceKind, obj.GetName(), err) + return err + } + klog.Infof("%s %q correctly deleted", resourceKind, obj.GetName()) + } + return nil +} diff --git a/pkg/liqo-controller-manager/ip-controller/ip_controller.go b/pkg/liqo-controller-manager/ip-controller/ip_controller.go index 9dae9dc1ec..e930792a53 100644 --- a/pkg/liqo-controller-manager/ip-controller/ip_controller.go +++ b/pkg/liqo-controller-manager/ip-controller/ip_controller.go @@ -18,6 +18,8 @@ import ( "context" "slices" + v1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -51,10 +53,11 @@ type IPReconciler struct { // +kubebuilder:rbac:groups=ipam.liqo.io,resources=ips/status,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=ipam.liqo.io,resources=ips/finalizers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=virtualkubelet.liqo.io,resources=virtualnodes,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=discovery.k8s.io,resources=endpointslices,verbs=get;list;watch;create;update;patch;delete // Reconcile Ip objects. func (r *IPReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - klog.Infof("Reconcilg IP %q", req.NamespacedName) // TODO:: delete var ip ipamv1alpha1.IP var desiredIP string @@ -107,6 +110,11 @@ func (r *IPReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Re } klog.Infof("updated IP %q status", req.NamespacedName) } + + // Create service and associated endpointslice if the template is defined + if err := r.handleAssociatedService(ctx, &ip); err != nil { + return ctrl.Result{}, err + } } else if controllerutil.ContainsFinalizer(&ip, ipamIPFinalizer) { // the resource is being deleted, but the finalizer is present: // - unmap the remapped IPs @@ -121,6 +129,9 @@ func (r *IPReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Re return ctrl.Result{}, err } klog.Infof("finalizer %q correctly removed from IP %q", ipamIPFinalizer, req.NamespacedName) + + // We do not have to delete possible service and endpointslice associated, as already deleted by + // the Kubernetes garbage collector (since they are owned by the IP resource). } return ctrl.Result{}, nil @@ -145,6 +156,8 @@ func (r *IPReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, w return ctrl.NewControllerManagedBy(mgr). For(&ipamv1alpha1.IP{}). + Owns(&v1.Service{}). + Owns(&discoveryv1.EndpointSlice{}). Watches(&v1alpha1.VirtualNode{}, handler.EnqueueRequestsFromMapFunc(enqueuer)). WithOptions(controller.Options{MaxConcurrentReconciles: workers}). Complete(r) diff --git a/pkg/liqo-controller-manager/network-controller/network_controller.go b/pkg/liqo-controller-manager/network-controller/network_controller.go index 18ccd9ab7d..c04febfed8 100644 --- a/pkg/liqo-controller-manager/network-controller/network_controller.go +++ b/pkg/liqo-controller-manager/network-controller/network_controller.go @@ -51,7 +51,6 @@ type NetworkReconciler struct { // Reconcile Network objects. func (r *NetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - klog.Infof("Reconcilg Network %q", req.NamespacedName) // TODO:: delete var nw ipamv1alpha1.Network var desiredCIDR, remappedCIDR string diff --git a/pkg/virtualKubelet/reflection/exposition/endpointslice.go b/pkg/virtualKubelet/reflection/exposition/endpointslice.go index d8c8255dd1..523ae6b554 100644 --- a/pkg/virtualKubelet/reflection/exposition/endpointslice.go +++ b/pkg/virtualKubelet/reflection/exposition/endpointslice.go @@ -65,6 +65,7 @@ type NamespacedEndpointSliceReflector struct { ipamclient ipam.IpamClient translations sync.Map + epsSkipUnmap sync.Map } // NewEndpointSliceReflector returns a new EndpointSliceReflector instance. @@ -165,9 +166,12 @@ func (ner *NamespacedEndpointSliceReflector) Handle(ctx context.Context, name st // The local endpointslice does no longer exist. Ensure it is also absent from the remote cluster. if !localExists { - // Release the address translations - if err := ner.UnmapEndpointIPs(ctx, name); err != nil { - return err + _, skipUnmap := ner.epsSkipUnmap.Load(name) + if !skipUnmap { + // Release the address translations + if err := ner.UnmapEndpointIPs(ctx, name); err != nil { + return err + } } defer tracer.Step("Ensured the absence of the remote object") @@ -180,6 +184,20 @@ func (ner *NamespacedEndpointSliceReflector) Handle(ctx context.Context, name st return nil } + // If the local endpointslice has the "skip unmap ip" annotation, then we do not have to unmap the addresses as already + // performed by other entities. We store in a cache if the endpointslice has the annotation or not, so that we have that + // information even when the local endpointslice is deleted (and therefore we can't check the existence of the annotation). + hasSkipUnmapAnnot := false + if local.GetAnnotations() != nil { + _, hasSkipUnmapAnnot = local.GetAnnotations()[consts.VKSkipUnmapIPAnnotationKey] + } + if hasSkipUnmapAnnot { + // the map contains only the keys of the endpoinstslices with the annotation, the values are not used. + ner.epsSkipUnmap.Store(name, nil) + } else { + ner.epsSkipUnmap.Delete(name) + } + // Wrap the address translation logic, so that we do not have to handle errors in the forge logic. var terr error translator := func(originals []string) []string {