-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from datum-cloud/feature/bootstrap-controllers
Bootstrapped controller boilerplate, implemented very basic logic for network bindings, subnet claims, and subnets.
- Loading branch information
Showing
15 changed files
with
1,276 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.