Skip to content

Commit

Permalink
Decouple gslb from the kubernetes Ingress resource
Browse files Browse the repository at this point in the history
This change makes the GSLB resource independent of a Kubernetes Ingress. This is a first step to allow integrations with other ingress Resources (e.g. istio virtual services for Istio Gateway, HTTP routes for Gateway API) k8gb-io#552
The change is fully backwards compatible, embedding an Ingress resources will still behave the same way it worked before.

In addition to the code refacting a new ResourceRef field is introduced. This field allows referencing resources that are not embedded in the GSLB definition and opens the gates to reference any resource types. As an example, the configuration bellow allows a GSLB resource to load balance the application defined in the Ingress resource on the same namespace with labels `app: demo`
```
spec:
  resourceRef:
    ingress:
      matchLabels:
        app: demo
```

---

Implementation details

A couple of functions crucial for business logic, namely `GslbIngressExposedIPs` and `getServiceHealthStatus`, need to read configuration present in the Ingress resource. Since the code would become too complicated once new ways to configure ingress are integrated, the format of the data they depend on was generalized from the Kubernetes Ingress resource to an ingress agnostic format. The processing of the data looks as follows:
A new `GslbReferenceResolver` interface was created. An implementation of this interface is capable of understanding a type of ingress configuration (e.g.: kubernetes' ingress, istio's virtual service, gateway API's http route) and implements two functions: `GetServers` and `GetGslbExposedIPs`. These functions extract the backends of the applications and the IP addresses they are exposed on, respectively.
Once a reconciliation operation is triggered a new `GslbReferenceResolver` is instatiated. Then, the list of servers and the exposed IPs are read and stored in the status of the GSLB resource.
Finally, the rest of the logic remains the same, with the difference that functions implementing business logic read the configuration from the status instead of looking up the Kubernetes Ingress resource.

---

Points for discussion:
* Should the list of servers and exposed IPs be stored in the status of GSLB resource? An internal data structure would also work, however we would need to pass it as an argument to numerous functions.
* There is already a `depresolver` interface. Even though the names look similar `depresolver` resolves startup configuration, while `refresolver` resolves runtime configuration. In addition, logging is useful to communicate with the users but a logger cannot be instantiated in the `depresolver` package because it would lead to circular dependencies. For these reasons, a new package was created instead of adding this logic to the `depresolver` package. Naming of the package can also be discussed, the proposal `refresolver` comes from the fact that it resolves references to other resources.

Signed-off-by: abaguas <[email protected]>
  • Loading branch information
abaguas authored and ytsarev committed Jun 29, 2024
1 parent 34bfed6 commit 1ebd452
Show file tree
Hide file tree
Showing 31 changed files with 1,031 additions and 143 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ mocks:
mockgen -package=mocks -destination=controllers/mocks/manager_mock.go sigs.k8s.io/controller-runtime/pkg/manager Manager
mockgen -package=mocks -destination=controllers/mocks/client_mock.go sigs.k8s.io/controller-runtime/pkg/client Client
mockgen -package=mocks -destination=controllers/mocks/resolver_mock.go -source=controllers/depresolver/resolver.go GslbResolver
mockgen -package=mocks -destination=controllers/mocks/refresolver_mock.go -source=controllers/refresolver/refresolver.go GslbRefResolver
mockgen -package=mocks -destination=controllers/mocks/provider_mock.go -source=controllers/providers/dns/dns.go Provider
$(call golic)

Expand Down
42 changes: 40 additions & 2 deletions api/v1beta1/gslb_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,47 @@ type Strategy struct {
SplitBrainThresholdSeconds int `json:"splitBrainThresholdSeconds,omitempty"`
}

// ResourceRef selects a resource defining the GSLB's load balancer and server
// +k8s:openapi-gen=true
type ResourceRef struct {
// Ingress selects a kubernetes.networking.k8s.io/v1.Ingress resource
Ingress metav1.LabelSelector `json:"ingress,omitempty"`
}

// GslbSpec defines the desired state of Gslb
// +k8s:openapi-gen=true
type GslbSpec struct {
// Gslb-enabled Ingress Spec
Ingress IngressSpec `json:"ingress"`
Ingress IngressSpec `json:"ingress,omitempty"`
// Gslb Strategy spec
Strategy Strategy `json:"strategy"`
// ResourceRef spec
ResourceRef ResourceRef `json:"resourceRef,omitempty"`
}

// LoadBalancer holds the GSLB's load balancer configuration
// +k8s:openapi-gen=true
type LoadBalancer struct {
// ExposedIPs on the local Load Balancer. This information is extracted automatically from the 'ingress' or 'resourceRef' configuration (optional)
ExposedIPs []string `json:"exposedIps,omitempty"`
}

// Servers holds the GSLB's servers' configuration
// +k8s:openapi-gen=true
type Server struct {
// Hostname exposed by the GSLB. This information is extracted automatically from the 'ingress' or 'resourceRef' configuration (optional)
Host string `json:"host,omitempty"`
// Kubernetes Services backing the load balanced application under the hostname. This information is extracted automatically from the 'ingress' or 'resourceRef' configuration (optional)
Services []*NamespacedName `json:"services,omitempty"`
}

// NamespacedName holds a reference to a k8s resource
// +k8s:openapi-gen=true
type NamespacedName struct {
// Namespace where the resource can be found
Namespace string `json:"namespace"`
// Name of the resource
Name string `json:"name"`
}

// GslbStatus defines the observed state of Gslb
Expand All @@ -57,8 +91,12 @@ type GslbStatus struct {
HealthyRecords map[string][]string `json:"healthyRecords"`
// Cluster Geo Tag
GeoTag string `json:"geoTag"`
// Comma-separated list of hosts. Duplicating the value from range .spec.ingress.rules[*].host for printer column
// Comma-separated list of hosts
Hosts string `json:"hosts,omitempty"`
// LoadBalancer configuration
LoadBalancer LoadBalancer `json:"loadBalancer"`
// Servers configuration
Servers []*Server `json:"servers"`
}

// +kubebuilder:object:root=true
Expand Down
90 changes: 90 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

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

96 changes: 93 additions & 3 deletions chart/k8gb/crd/k8gb.absa.oss_gslbs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,55 @@ spec:
type: object
type: array
type: object
resourceRef:
description: ResourceRef spec
properties:
ingress:
description: Ingress selects a kubernetes.networking.k8s.io/v1.Ingress
resource
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector
that contains values, a key, and an operator that relates
the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: operator represents a key's relationship
to a set of values. Valid operators are In, NotIn,
Exists and DoesNotExist.
type: string
values:
description: values is an array of string values. If
the operator is In or NotIn, the values array must
be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced
during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs. A
single {key,value} in the matchLabels map is equivalent
to an element of matchExpressions, whose key field is "key",
the operator is "In", and the values array contains only
"value". The requirements are ANDed.
type: object
type: object
type: object
strategy:
description: Gslb Strategy spec
properties:
Expand All @@ -337,7 +386,6 @@ spec:
- type
type: object
required:
- ingress
- strategy
type: object
status:
Expand All @@ -354,9 +402,49 @@ spec:
description: Current Healthy DNS record structure
type: object
hosts:
description: Comma-separated list of hosts. Duplicating the value
from range .spec.ingress.rules[*].host for printer column
description: Comma-separated list of hosts
type: string
loadBalancer:
description: LoadBalancer configuration
properties:
exposedIps:
description: ExposedIPs on the local Load Balancer. This information
is extracted automatically from the 'ingress' or 'resourceRef'
configuration (optional)
items:
type: string
type: array
type: object
servers:
description: Servers configuration
items:
description: Servers holds the GSLB's servers' configuration
properties:
host:
description: Hostname exposed by the GSLB. This information
is extracted automatically from the 'ingress' or 'resourceRef'
configuration (optional)
type: string
services:
description: Kubernetes Services backing the load balanced application
under the hostname. This information is extracted automatically
from the 'ingress' or 'resourceRef' configuration (optional)
items:
description: NamespacedName holds a reference to a k8s resource
properties:
name:
description: Name of the resource
type: string
namespace:
description: Namespace where the resource can be found
type: string
required:
- name
- namespace
type: object
type: array
type: object
type: array
serviceHealth:
additionalProperties:
type: string
Expand All @@ -365,6 +453,8 @@ spec:
required:
- geoTag
- healthyRecords
- loadBalancer
- servers
- serviceHealth
type: object
type: object
Expand Down
5 changes: 1 addition & 4 deletions controllers/dnsupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ func (r *GslbReconciler) gslbDNSEndpoint(gslb *k8gbv1beta1.Gslb) (*externaldns.D
return nil, err
}

localTargets, err := r.DNSProvider.GslbIngressExposedIPs(gslb)
if err != nil {
return nil, err
}
localTargets := gslb.Status.LoadBalancer.ExposedIPs

for host, health := range serviceHealth {
var finalTargets = assistant.NewTargets()
Expand Down
40 changes: 35 additions & 5 deletions controllers/gslb_controller_reconciliation.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ Generated by GoLic, for more details see: https://github.com/AbsaOSS/golic
import (
"context"
"fmt"
"reflect"

"github.com/k8gb-io/k8gb/controllers/utils"

"github.com/k8gb-io/k8gb/controllers/providers/metrics"
"github.com/k8gb-io/k8gb/controllers/refresolver"

k8gbv1beta1 "github.com/k8gb-io/k8gb/api/v1beta1"
"github.com/k8gb-io/k8gb/controllers/depresolver"
Expand All @@ -33,6 +35,7 @@ import (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -63,7 +66,7 @@ var m = metrics.Metrics()
// +kubebuilder:rbac:groups=k8gb.absa.oss,resources=gslbs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=k8gb.absa.oss,resources=gslbs/status,verbs=get;update;patch

// Reconcile runs main reconiliation loop
// Reconcile runs main reconciliation loop
func (r *GslbReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
ctx, span := r.Tracer.Start(ctx, "Reconcile")
defer span.End()
Expand Down Expand Up @@ -138,17 +141,44 @@ func (r *GslbReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
}

// == Ingress ==========
ingress, err := r.gslbIngress(gslb)
if reflect.DeepEqual(gslb.Spec.ResourceRef.Ingress, metav1.LabelSelector{}) {
ingress, err := r.gslbIngress(gslb)
if err != nil {
m.IncrementError(gslb)
return result.RequeueError(err)
}

err = r.saveIngress(gslb, ingress)
if err != nil {
m.IncrementError(gslb)
return result.RequeueError(err)
}
}

// == Reference resolution ==
refResolver, err := refresolver.New(gslb, r.Client)
if err != nil {
m.IncrementError(gslb)
return result.RequeueError(err)
return result.RequeueError(fmt.Errorf("error resolving references to the refresolver object (%s)", err))
}
servers, err := refResolver.GetServers()
if err != nil {
m.IncrementError(gslb)
return result.RequeueError(fmt.Errorf("getting GSLB servers (%s)", err))
}
gslb.Status.Servers = servers
fmt.Printf("got servers: %v\n", servers)

err = r.saveIngress(gslb, ingress)
loadBalancerExposedIPs, err := refResolver.GetGslbExposedIPs(gslb, r.Client, r.Config.EdgeDNSServers)
if err != nil {
m.IncrementError(gslb)
return result.RequeueError(err)
return result.RequeueError(fmt.Errorf("getting load balancer exposed IPs (%s)", err))
}
gslb.Status.LoadBalancer.ExposedIPs = loadBalancerExposedIPs

log.Debug().
Str("gslb", gslb.Name).
Msg("Resolved LoadBalancer and Server configuration referenced by Ingress")

// == external-dns dnsendpoints CRs ==
dnsEndpoint, err := r.gslbDNSEndpoint(gslb)
Expand Down
Loading

0 comments on commit 1ebd452

Please sign in to comment.