Skip to content

Commit

Permalink
IP: create associated Service
Browse files Browse the repository at this point in the history
  • Loading branch information
fra98 committed Sep 21, 2023
1 parent 22bb158 commit 5564841
Show file tree
Hide file tree
Showing 11 changed files with 592 additions and 11 deletions.
14 changes: 14 additions & 0 deletions apis/ipam/v1alpha1/ip_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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.
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions apis/ipam/v1alpha1/network_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
25 changes: 23 additions & 2 deletions apis/ipam/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

371 changes: 369 additions & 2 deletions deployments/liqo/crds/ipam.liqo.io_ips.yaml

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions deployments/liqo/crds/ipam.liqo.io_networks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions deployments/liqo/files/liqo-controller-manager-ClusterRole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- services
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- discovery.k8s.io
resources:
Expand Down
3 changes: 3 additions & 0 deletions pkg/consts/replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
132 changes: 132 additions & 0 deletions pkg/liqo-controller-manager/ip-controller/exposition.go
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 14 additions & 1 deletion pkg/liqo-controller-manager/ip-controller/ip_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 21 additions & 3 deletions pkg/virtualKubelet/reflection/exposition/endpointslice.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type NamespacedEndpointSliceReflector struct {

ipamclient ipam.IpamClient
translations sync.Map
epsSkipUnmap sync.Map
}

// NewEndpointSliceReflector returns a new EndpointSliceReflector instance.
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand Down

0 comments on commit 5564841

Please sign in to comment.