Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ctrl): implements MeshFederation reconciliation #170

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

bartoszmajsak
Copy link
Collaborator

@bartoszmajsak bartoszmajsak commented Jan 27, 2025

This commit introduces the initial implementation of the MeshFederation
controller. The controller is responsible for:

  • Managing the MeshFederation server lifecycle (Start serving FDS #152)
  • Configuring MeshFederation resources, including:
    • Ingress Gateway
    • PeerAuthentication
    • EnvoyFilter (for OpenShift Router)
    • Routes (for OpenShift Router)
  • Watching Kubernetes Services to:

Support both label selectors and expressions (#52 #143) as export rules.

Basic EnvTest tests are included to verify the setup (happy paths for both
istio and openshift-router ingress types).

Introducing MeshFederation.Status.ExportedServices to keep track of
exported services in each reconcile.

Current limitations / random thoughts

There are loose ends I identified while implementing it. Most are in TODOs,
but I thought keeping them in the PR description helps with getting initial context.

  • not tested for multiple MeshFederation instances
    • we currently create one FDS server per federation, but we have a single port for
      all which is hardcoded (:15080), some ideas:
      • share server across federations
        • ❓ is that a bottleneck?
        • we are lacking a way to identify to which federation a remote belongs to,
          which makes it impossible to e.g. push SotW to only relevant peers on a given
          MeshFederation reconcile. Perhaps expanding FDS protocol can solve it.
      • alternatively we can make the port customizable, but that would mean configuring
        all remotes in a single federation instead of relying on the defaults
    • defaulting root ns (istio-system) can cause issues when we create mesh-wide resources)
  • not ensuring one-per-ns MeshFederation
    • should be done through admission webhook if that becomes a requirement

Fixes #152
Fixes #52
Fixes #143
Fixes #153

@bartoszmajsak
Copy link
Collaborator Author

bartoszmajsak commented Jan 27, 2025

This PR contains two other, previously stacked PR:

@bartoszmajsak bartoszmajsak force-pushed the mesh-federation-controller branch from e0d1ba7 to 326005f Compare January 27, 2025 06:35
@bartoszmajsak bartoszmajsak changed the title feat(ctrl): implement MeshFederation reconciliation feat(ctrl): implements MeshFederation reconciliation Jan 27, 2025
@bartoszmajsak bartoszmajsak force-pushed the mesh-federation-controller branch 7 times, most recently from dd49df9 to 204092f Compare January 27, 2025 08:20
This commit introduces the initial implementation of the MeshFederation
controller. The controller is responsible for:

- Managing the MeshFederation server lifecycle (openshift-service-mesh#152)
  - removes a need for a channel to trigger push - pushing directly
    instead
- Configuring MeshFederation resources, including:
  - IngressGateway
  - PeerAuthentication
  - EnvoyFilter (for OpenShift Router)
  - Routes (for OpenShift Router)
- Watching Kubernetes services to:
  - Push SotW updates to all connected peers (openshift-service-mesh#153)
  - Update MeshFederation cluster configuration
  - Support both label selectors and expressions (openshift-service-mesh#52 openshift-service-mesh#143)

Basic EnvTest tests are included to verify the setup.

Fixes openshift-service-mesh#152 openshift-service-mesh#52 openshift-service-mesh#143 openshift-service-mesh#153
@bartoszmajsak bartoszmajsak force-pushed the mesh-federation-controller branch from 204092f to 3fba4e1 Compare January 27, 2025 14:08
Copy link
Member

@eoinfennessy eoinfennessy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial pass with a few comments/suggestions. I'll continue the review later.

@bartoszmajsak
Copy link
Collaborator Author

bartoszmajsak commented Jan 27, 2025

Lets not ok-to-merge yet, ideally we should combine it with #168, as there were API changes among other things.

Owns(&routev1.Route{}).
Watches(
// TODO(design): initial reconcile will trigger a lot of requests - one for each service. This can become expensive.
&corev1.Service{},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FOLLOW-UP

We don't need to watch the corev1.Service object, we can limit it only to what we are interested in at this point - labels. For this purpose watch on PartialObjectMetadata is sufficient. This will also reduce the payload send from k8s api-server.

@eoinfennessy I think you can also think about it for the zones as you rely on namespace only IIRC. Thing I don't know yet is if that's the same cache as regular objects.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I saw that the other day and woke up with PartialObjectMetadata thing in my head so I just dumped it here before first coffee :)

Copy link
Member

@eoinfennessy eoinfennessy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I just added a few small nits/suggestions.

return accResult, errors.Join(errs...)
}

func (r *Reconciler) handleServiceToExport(ctx context.Context, object client.Object) []reconcile.Request {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think the name could be a little clearer. Something like this:

Suggested change
func (r *Reconciler) handleServiceToExport(ctx context.Context, object client.Object) []reconcile.Request {
func (r *Reconciler) mapServiceToReconcileRequests(ctx context.Context, object client.Object) []reconcile.Request {

meshFederations := &v1alpha1.MeshFederationList{}
// TODO paginate? options?
if errList := r.Client.List(ctx, meshFederations); errList != nil {
logger.Error(errList, "failed mapping Service to MeshFederations", "service", object.GetName()+"/"+object.GetNamespace())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.Error(errList, "failed mapping Service to MeshFederations", "service", object.GetName()+"/"+object.GetNamespace())
logger.Error(errList, "failed to list MeshFederations when mapping Service to reconcile requests", "service", object.GetName()+"/"+object.GetNamespace())

Comment on lines +265 to +282
createGateway := func() objReconciler[*v1alpha3.Gateway, istionetv1alpha3.Gateway] {
result := &v1alpha3.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "federation-ingress-gateway",
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{"federation.openshift-service-mesh.io/peer": "todo"},
},
}

result.Spec = desiredSpec()

return objReconciler[*v1alpha3.Gateway, istionetv1alpha3.Gateway]{
Obj: result,
DesiredSpec: func() istionetv1alpha3.Gateway {
return desiredSpec()
},
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit

Suggested change
createGateway := func() objReconciler[*v1alpha3.Gateway, istionetv1alpha3.Gateway] {
result := &v1alpha3.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "federation-ingress-gateway",
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{"federation.openshift-service-mesh.io/peer": "todo"},
},
}
result.Spec = desiredSpec()
return objReconciler[*v1alpha3.Gateway, istionetv1alpha3.Gateway]{
Obj: result,
DesiredSpec: func() istionetv1alpha3.Gateway {
return desiredSpec()
},
}
}
createGateway := func() objReconciler[*v1alpha3.Gateway, istionetv1alpha3.Gateway] {
result := &v1alpha3.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: "federation-ingress-gateway",
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{"federation.openshift-service-mesh.io/peer": "todo"},
},
Spec: desiredSpec(),
}
return objReconciler[*v1alpha3.Gateway, istionetv1alpha3.Gateway]{
Obj: result,
DesiredSpec: func() istionetv1alpha3.Gateway {
return desiredSpec()
},
}
}

Comment on lines +127 to +146
envoyFilter := func(svcName, svcNamespace string, port int32) objReconciler[*v1alpha3.EnvoyFilter, istionetv1alpha3.EnvoyFilter] {
result := &v1alpha3.EnvoyFilter{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("sni-%s-%s-%d", svcName, svcNamespace, port),
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{
"federation.openshift-service-mesh.io/peer": "todo",
},
},
}

result.Spec = desiredSpec(svcName, svcNamespace, port)

return objReconciler[*v1alpha3.EnvoyFilter, istionetv1alpha3.EnvoyFilter]{
Obj: result,
DesiredSpec: func() istionetv1alpha3.EnvoyFilter {
return desiredSpec(svcName, svcNamespace, port)
},
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit

Suggested change
envoyFilter := func(svcName, svcNamespace string, port int32) objReconciler[*v1alpha3.EnvoyFilter, istionetv1alpha3.EnvoyFilter] {
result := &v1alpha3.EnvoyFilter{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("sni-%s-%s-%d", svcName, svcNamespace, port),
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{
"federation.openshift-service-mesh.io/peer": "todo",
},
},
}
result.Spec = desiredSpec(svcName, svcNamespace, port)
return objReconciler[*v1alpha3.EnvoyFilter, istionetv1alpha3.EnvoyFilter]{
Obj: result,
DesiredSpec: func() istionetv1alpha3.EnvoyFilter {
return desiredSpec(svcName, svcNamespace, port)
},
}
}
envoyFilter := func(svcName, svcNamespace string, port int32) objReconciler[*v1alpha3.EnvoyFilter, istionetv1alpha3.EnvoyFilter] {
result := &v1alpha3.EnvoyFilter{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("sni-%s-%s-%d", svcName, svcNamespace, port),
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{
"federation.openshift-service-mesh.io/peer": "todo",
},
},
Spec: desiredSpec(svcName, svcNamespace, port),
}
return objReconciler[*v1alpha3.EnvoyFilter, istionetv1alpha3.EnvoyFilter]{
Obj: result,
DesiredSpec: func() istionetv1alpha3.EnvoyFilter {
return desiredSpec(svcName, svcNamespace, port)
},
}
}

Comment on lines +193 to +210
createRoute := func(svcName, svcNamespace string, port int32) objReconciler[*routev1.Route, routev1.RouteSpec] {
result := &routev1.Route{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s-%d-to-federation-ingress-gateway", svcName, svcNamespace, port),
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{"federation.openshift-service-mesh.io/peer": "todo"},
},
}

result.Spec = desiredSpec(svcName, svcNamespace, port)

return objReconciler[*routev1.Route, routev1.RouteSpec]{
Obj: result,
DesiredSpec: func() routev1.RouteSpec {
return desiredSpec(svcName, svcNamespace, port)
},
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit

Suggested change
createRoute := func(svcName, svcNamespace string, port int32) objReconciler[*routev1.Route, routev1.RouteSpec] {
result := &routev1.Route{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s-%d-to-federation-ingress-gateway", svcName, svcNamespace, port),
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{"federation.openshift-service-mesh.io/peer": "todo"},
},
}
result.Spec = desiredSpec(svcName, svcNamespace, port)
return objReconciler[*routev1.Route, routev1.RouteSpec]{
Obj: result,
DesiredSpec: func() routev1.RouteSpec {
return desiredSpec(svcName, svcNamespace, port)
},
}
}
createRoute := func(svcName, svcNamespace string, port int32) objReconciler[*routev1.Route, routev1.RouteSpec] {
result := &routev1.Route{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s-%d-to-federation-ingress-gateway", svcName, svcNamespace, port),
Namespace: meshFederation.Spec.ControlPlaneNamespace,
Labels: map[string]string{"federation.openshift-service-mesh.io/peer": "todo"},
},
Spec: desiredSpec(svcName, svcNamespace, port),
}
return objReconciler[*routev1.Route, routev1.RouteSpec]{
Obj: result,
DesiredSpec: func() routev1.RouteSpec {
return desiredSpec(svcName, svcNamespace, port)
},
}
}

Comment on lines +161 to +168
resources := pushRequest.Resources
if resources == nil {
var err error
resources, err = adss.generateResources(pushRequest.TypeUrl)
if err != nil {
return err
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability suggestion avoiding variable reassignment:

Suggested change
resources := pushRequest.Resources
if resources == nil {
var err error
resources, err = adss.generateResources(pushRequest.TypeUrl)
if err != nil {
return err
}
}
var resources []*anypb.Any
if pushRequest.Resources == nil {
var err error
resources, err = adss.generateResources(pushRequest.TypeUrl)
if err != nil {
return err
}
} else {
resources = pushRequest.Resources
}

Comment on lines +154 to +155
// TODO(design): do we want to keep all the services? seems convenient, but shouldn't we be concerned about the size?
saved.Status.ExportedServices = meshFederation.Status.ExportedServices
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FOLLOW-UP

@jewertow @bartoszmajsak

It may be possible to minimise the number of full SotW pushes by comparing the old list of services to the new list of services and only pushing if the list has changed. This could be achieved by storing a hash of the services in the MeshFederation's status (status.lastPushHash: 1234abcd) and only pushing if this hash changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
3 participants