From 259bff73ac9b721e4af4e333a106451350db4ed9 Mon Sep 17 00:00:00 2001
From: Francesco Torta <62566275+fra98@users.noreply.github.com>
Date: Wed, 6 Sep 2023 09:28:44 +0000
Subject: [PATCH] IP: create associated Service

---
 apis/ipam/v1alpha1/ip_types.go                |  14 +
 apis/ipam/v1alpha1/network_types.go           |   1 +
 apis/ipam/v1alpha1/zz_generated.deepcopy.go   |  25 +-
 deployments/liqo/crds/ipam.liqo.io_ips.yaml   | 371 +++++++++++++++++-
 .../liqo/crds/ipam.liqo.io_networks.yaml      |   5 +-
 .../liqo-controller-manager-ClusterRole.yaml  |  12 +
 pkg/consts/replication.go                     |   3 +
 .../ip-controller/exposition.go               | 132 +++++++
 .../ip-controller/ip_controller.go            |  15 +-
 .../network-controller/network_controller.go  |   1 -
 .../reflection/exposition/endpointslice.go    |  24 +-
 11 files changed, 592 insertions(+), 11 deletions(-)
 create mode 100644 pkg/liqo-controller-manager/ip-controller/exposition.go

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 {