Skip to content

Commit

Permalink
Merge pull request #3 from datum-cloud/feature/bootstrap-controllers
Browse files Browse the repository at this point in the history
Bootstrapped controller boilerplate, implemented very basic logic for network bindings, subnet claims, and subnets.
  • Loading branch information
joshlreese authored Nov 22, 2024
2 parents 49844ef + a447d06 commit 6f377db
Show file tree
Hide file tree
Showing 15 changed files with 1,276 additions and 0 deletions.
46 changes: 46 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"

networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
"go.datum.net/network-services-operator/internal/controller"
// +kubebuilder:scaffold:imports
)

Expand All @@ -29,6 +32,7 @@ var (
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))

utilruntime.Must(networkingv1alpha.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}

Expand Down Expand Up @@ -122,6 +126,48 @@ func main() {
os.Exit(1)
}

if err = (&controller.NetworkReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Network")
os.Exit(1)
}
if err = (&controller.NetworkBindingReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NetworkBinding")
os.Exit(1)
}
if err = (&controller.NetworkContextReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NetworkContext")
os.Exit(1)
}
if err = (&controller.NetworkPolicyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NetworkPolicy")
os.Exit(1)
}
if err = (&controller.SubnetReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Subnet")
os.Exit(1)
}
if err = (&controller.SubnetClaimReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "SubnetClaim")
os.Exit(1)
}
// +kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Expand Down
Empty file removed internal/.gitkeep
Empty file.
49 changes: 49 additions & 0 deletions internal/controller/network_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: AGPL-3.0-only

package controller

import (
"context"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
)

// NetworkReconciler reconciles a Network object
type NetworkReconciler struct {
client.Client
Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=networking.datumapis.com,resources=networks,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=networks/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=networks/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Network object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *NetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)

// TODO(user): your logic here

return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *NetworkReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&networkingv1alpha.Network{}).
Named("network").
Complete(r)
}
70 changes: 70 additions & 0 deletions internal/controller/network_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: AGPL-3.0-only

package controller

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
)

var _ = Describe("Network Controller", Pending, func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"

ctx := context.Background()

typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
}
network := &networkingv1alpha.Network{}

BeforeEach(func() {
By("creating the custom resource for the Kind Network")
err := k8sClient.Get(ctx, typeNamespacedName, network)
if err != nil && errors.IsNotFound(err) {
resource := &networkingv1alpha.Network{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})

AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &networkingv1alpha.Network{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())

By("Cleanup the specific resource instance Network")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &NetworkReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
}

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
})
})
183 changes: 183 additions & 0 deletions internal/controller/networkbinding_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: AGPL-3.0-only

package controller

import (
"context"
"encoding/json"
"fmt"
"hash/fnv"

apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/rand"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"

networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
)

// NetworkBindingReconciler reconciles a NetworkBinding object
type NetworkBindingReconciler struct {
client.Client
Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=networking.datumapis.com,resources=networkbindings,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=networkbindings/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=networking.datumapis.com,resources=networkbindings/finalizers,verbs=update

func (r *NetworkBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, err error) {
logger := log.FromContext(ctx)

// Each valid network binding should result in a NetworkAttachment being
// created for each unique `topology` that's found.

var binding networkingv1alpha.NetworkBinding
if err := r.Client.Get(ctx, req.NamespacedName, &binding); err != nil {
if apierrors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}

if !binding.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil
}

logger.Info("reconciling network binding")
defer logger.Info("reconcile complete")

readyCondition := metav1.Condition{
Type: networkingv1alpha.NetworkBindingReady,
Status: metav1.ConditionFalse,
Reason: "Unknown",
ObservedGeneration: binding.Generation,
Message: "Unknown state",
}

defer func() {
if err != nil {
// Don't update the status if errors are encountered
return
}
statusChanged := apimeta.SetStatusCondition(&binding.Status.Conditions, readyCondition)

if statusChanged {
err = r.Client.Status().Update(ctx, &binding)
}
}()

networkNamespace := binding.Spec.Network.Namespace

if len(networkNamespace) == 0 {
// Fall back to binding's namespace if NetworkRef does not specify one.
networkNamespace = binding.Namespace
}

var network networkingv1alpha.Network
networkObjectKey := client.ObjectKey{
Namespace: networkNamespace,
Name: binding.Spec.Network.Name,
}
if err := r.Client.Get(ctx, networkObjectKey, &network); err != nil {
readyCondition.Reason = "NetworkNotFound"
readyCondition.Message = "The network referenced in the binding was not found."
return ctrl.Result{}, fmt.Errorf("failed fetching network for binding: %w", err)
}

networkContextName, err := networkContextNameForBinding(&binding)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to determine network context name: %w", err)
}

var networkContext networkingv1alpha.NetworkContext
networkContextObjectKey := client.ObjectKey{
Namespace: networkNamespace,
Name: networkContextName,
}
if err := r.Client.Get(ctx, networkContextObjectKey, &networkContext); client.IgnoreNotFound(err) != nil {
return ctrl.Result{}, fmt.Errorf("failed fetching network context: %w", err)
}

if networkContext.CreationTimestamp.IsZero() {
networkContext = networkingv1alpha.NetworkContext{
ObjectMeta: metav1.ObjectMeta{
Namespace: networkNamespace,
Name: networkContextName,
},
Spec: networkingv1alpha.NetworkContextSpec{
Network: networkingv1alpha.LocalNetworkRef{
Name: binding.Spec.Network.Name,
},
Topology: binding.Spec.Topology,
},
}

if err := controllerutil.SetControllerReference(&network, &networkContext, r.Scheme); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set controller on network context: %w", err)
}

if err := r.Client.Create(ctx, &networkContext); err != nil {
return ctrl.Result{}, fmt.Errorf("failed creating network context: %w", err)
}
}

if !apimeta.IsStatusConditionTrue(networkContext.Status.Conditions, networkingv1alpha.NetworkContextReady) {
logger.Info("network context is not ready")
readyCondition.Reason = "NetworkContextNotReady"
readyCondition.Message = "Network context is not ready."

// Choosing to requeue here instead of establishing a watch on contexts, as
// once the context is created an ready, future bindings will immediately
// become ready.
return ctrl.Result{Requeue: true}, nil
}

binding.Status.NetworkContextRef = &networkingv1alpha.NetworkContextRef{
Namespace: networkContext.Namespace,
Name: networkContext.Name,
}

readyCondition.Status = metav1.ConditionTrue
readyCondition.Reason = "NetworkContextReady"
readyCondition.Message = "Network context is ready."

// Update is handled in the defer function above.

return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *NetworkBindingReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&networkingv1alpha.NetworkBinding{}, builder.WithPredicates(
predicate.NewPredicateFuncs(func(object client.Object) bool {
o := object.(*networkingv1alpha.NetworkBinding)
return o.Status.NetworkContextRef == nil
}),
)).
Complete(r)
}

func networkContextNameForBinding(binding *networkingv1alpha.NetworkBinding) (string, error) {
if binding.CreationTimestamp.IsZero() {
return "", fmt.Errorf("binding has not been created")
}
topologyBytes, err := json.Marshal(binding.Spec.Topology)
if err != nil {
return "", fmt.Errorf("failed marshaling topology to json: %w", err)
}

f := fnv.New32a()
f.Write(topologyBytes)
topologyHash := rand.SafeEncodeString(fmt.Sprint(f.Sum32()))

return fmt.Sprintf("%s-%s", binding.Spec.Network.Name, topologyHash), nil
}
Loading

0 comments on commit 6f377db

Please sign in to comment.