Skip to content

Commit 985bd76

Browse files
authored
feat: Support cross-namespace package references with PackageRef field (#203)
### Motivation The current Function CRD uses a simple `Package` field to reference packages, which limits the ability to reference packages from different namespaces. This change introduces a new `PackageRef` structure that supports cross-namespace package references while maintaining backward compatibility and proper label management. The main problems this solves: 1. **Cross-namespace package references**: Functions can now reference packages from different namespaces 2. **Improved package label management**: Package labels now include namespace information to avoid conflicts 3. **Better validation**: Enhanced webhook validation for cross-namespace references 4. **Consistent labeling**: Automatic generation of package labels with namespace prefix ### Modifications 1. **Updated Function CRD structure**: - Replaced `Package` field with `PackageRef` structure containing `Name` and `Namespace` fields - Updated all test files to use the new `PackageRef` structure 2. **Enhanced webhook validation**: - Modified `function_webhook.go` to validate cross-namespace package references - Updated package label generation to include namespace prefix (format: `namespace.package-name`) - Added comprehensive tests for cross-namespace scenarios 3. **Improved controller logic**: - Updated `function_controller.go` to handle cross-namespace package lookups - Modified `mapPackageToFunctions` to work with namespace-aware package labels - Removed manual package label management from controller (now handled by webhook) 4. **Enhanced test coverage**: - Added extensive tests for cross-namespace package references - Updated existing tests to use `PackageRef` instead of `Package` - Added tests for automatic package label generation - Added integration tests for multiple functions with auto-generated labels 5. **Updated package webhook**: - Modified `packages_webhook.go` to use namespace-aware package labels when finding referencing functions The changes maintain backward compatibility while providing a more robust and flexible package reference system that supports cross-namespace operations.
1 parent 2ce7841 commit 985bd76

11 files changed

+523
-141
lines changed

operator/api/v1alpha1/function_types.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ import (
2121
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2222
)
2323

24+
// PackageRef defines a reference to a Package resource
25+
// +kubebuilder:object:generate=true
26+
// +kubebuilder:validation:Optional
27+
type PackageRef struct {
28+
// Name of the Package resource
29+
// +kubebuilder:validation:Required
30+
Name string `json:"name"`
31+
// Namespace of the Package resource
32+
// +kubebuilder:validation:Optional
33+
Namespace string `json:"namespace,omitempty"`
34+
}
35+
2436
// FunctionSpec defines the desired state of Function
2537
// +kubebuilder:object:generate=true
2638
// +kubebuilder:validation:Optional
@@ -31,9 +43,9 @@ type FunctionSpec struct {
3143
// Description of the function
3244
// +kubebuilder:validation:Optional
3345
Description string `json:"description,omitempty"`
34-
// Package name
46+
// Package reference
3547
// +kubebuilder:validation:Required
36-
Package string `json:"package"`
48+
PackageRef PackageRef `json:"packageRef"`
3749
// Module name
3850
// +kubebuilder:validation:Required
3951
Module string `json:"module"`

operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

operator/config/crd/bases/fs.functionstream.github.io_functions.yaml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,18 @@ spec:
5353
module:
5454
description: Module name
5555
type: string
56-
package:
57-
description: Package name
58-
type: string
56+
packageRef:
57+
description: Package reference
58+
properties:
59+
name:
60+
description: Name of the Package resource
61+
type: string
62+
namespace:
63+
description: Namespace of the Package resource
64+
type: string
65+
required:
66+
- name
67+
type: object
5968
replicas:
6069
default: 1
6170
description: Number of replicas for the function deployment
@@ -107,7 +116,7 @@ spec:
107116
type: string
108117
required:
109118
- module
110-
- package
119+
- packageRef
111120
type: object
112121
status:
113122
description: FunctionStatus defines the observed state of Function

operator/config/samples/fs_v1alpha1_function.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ metadata:
88
spec:
99
displayName: "Sample Function"
1010
description: "A sample function for demonstration purposes."
11-
package: "sample-package"
11+
packageRef:
12+
name: "sample-package"
13+
# namespace: "default" # Optional: defaults to the same namespace as the Function
1214
module: "sample-module"
1315
# TODO(user): Add fields here

operator/internal/controller/function_controller.go

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -84,28 +84,24 @@ func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
8484
return ctrl.Result{}, err
8585
}
8686

87-
// 2. Ensure Function has package label
88-
if fn.Labels == nil {
89-
fn.Labels = make(map[string]string)
90-
}
91-
labelUpdated := false
92-
if fn.Labels["package"] != fn.Spec.Package {
93-
fn.Labels["package"] = fn.Spec.Package
94-
labelUpdated = true
95-
}
87+
// 2. Get package label for later use
88+
packageLabel := generatePackageLabel(&fn)
9689

9790
// 3. Get Package
9891
var pkg fsv1alpha1.Package
99-
if err := r.Get(ctx, types.NamespacedName{Name: fn.Spec.Package, Namespace: req.Namespace}, &pkg); err != nil {
100-
log.Error(err, "Failed to get Package", "package", fn.Spec.Package)
92+
packageNamespace := fn.Spec.PackageRef.Namespace
93+
if packageNamespace == "" {
94+
packageNamespace = req.Namespace
95+
}
96+
if err := r.Get(ctx, types.NamespacedName{Name: fn.Spec.PackageRef.Name, Namespace: packageNamespace}, &pkg); err != nil {
10197
return ctrl.Result{}, err
10298
}
10399
image := ""
104100
if pkg.Spec.FunctionType.Cloud != nil {
105101
image = pkg.Spec.FunctionType.Cloud.Image
106102
}
107103
if image == "" {
108-
return ctrl.Result{}, fmt.Errorf("package %s has no image", fn.Spec.Package)
104+
return ctrl.Result{}, fmt.Errorf("package %s has no image", packageLabel)
109105
}
110106

111107
// 4. Build config yaml content
@@ -213,24 +209,6 @@ EOF
213209
}
214210
}
215211

216-
// 8. Update Function labels if needed
217-
if labelUpdated {
218-
// Re-fetch the Function to ensure we have the latest version
219-
var latestFn fsv1alpha1.Function
220-
if err := r.Get(ctx, req.NamespacedName, &latestFn); err != nil {
221-
log.Error(err, "Failed to get latest Function for label update")
222-
return ctrl.Result{}, err
223-
}
224-
// Apply our label changes to the latest version
225-
if latestFn.Labels == nil {
226-
latestFn.Labels = make(map[string]string)
227-
}
228-
latestFn.Labels["package"] = fn.Spec.Package
229-
if err := r.Update(ctx, &latestFn); err != nil {
230-
return utils.HandleReconcileError(log, err, "Conflict when updating Function labels, will retry automatically")
231-
}
232-
}
233-
234212
return ctrl.Result{}, nil
235213
}
236214

@@ -279,8 +257,8 @@ func buildFunctionConfigYaml(fn *fsv1alpha1.Function, operatorCfg Config) (strin
279257
if fn.Spec.DisplayName != "" {
280258
cfg["displayName"] = fn.Spec.DisplayName
281259
}
282-
if fn.Spec.Package != "" {
283-
cfg["package"] = fn.Spec.Package
260+
if fn.Spec.PackageRef.Name != "" {
261+
cfg["package"] = generatePackageLabel(fn)
284262
}
285263
out, err := yaml.Marshal(cfg)
286264
if err != nil {
@@ -306,6 +284,15 @@ func hasFunctionLabel(obj client.Object) bool {
306284
return ok
307285
}
308286

287+
// generatePackageLabel generates a package label in the format "{namespace}.{packageName}"
288+
func generatePackageLabel(fn *fsv1alpha1.Function) string {
289+
packageNamespace := fn.Spec.PackageRef.Namespace
290+
if packageNamespace == "" {
291+
packageNamespace = fn.Namespace
292+
}
293+
return fmt.Sprintf("%s.%s", packageNamespace, fn.Spec.PackageRef.Name)
294+
}
295+
309296
// SetupWithManager sets up the controller with the Manager.
310297
func (r *FunctionReconciler) SetupWithManager(mgr ctrl.Manager) error {
311298
functionLabelPredicate := predicate.NewPredicateFuncs(hasFunctionLabel)
@@ -327,22 +314,32 @@ func (r *FunctionReconciler) mapPackageToFunctions(ctx context.Context, obj clie
327314
return nil
328315
}
329316

330-
// Get Functions that reference this Package using label selector
317+
var requests []reconcile.Request
318+
319+
// Get Functions that reference this Package using the new label format {namespace}.{package name}
320+
packageLabel := fmt.Sprintf("%s.%s", packageObj.Namespace, packageObj.Name)
331321
var functions fsv1alpha1.FunctionList
332322
if err := r.List(ctx, &functions,
333-
client.InNamespace(packageObj.Namespace),
334-
client.MatchingLabels(map[string]string{"package": packageObj.Name})); err != nil {
323+
client.MatchingLabels(map[string]string{"package": packageLabel})); err != nil {
335324
return nil
336325
}
337326

338-
var requests []reconcile.Request
339327
for _, function := range functions.Items {
340-
requests = append(requests, reconcile.Request{
341-
NamespacedName: types.NamespacedName{
342-
Name: function.Name,
343-
Namespace: function.Namespace,
344-
},
345-
})
328+
// Check if this function actually references this package
329+
if function.Spec.PackageRef.Name == packageObj.Name {
330+
packageNamespace := function.Spec.PackageRef.Namespace
331+
if packageNamespace == "" {
332+
packageNamespace = function.Namespace
333+
}
334+
if packageNamespace == packageObj.Namespace {
335+
requests = append(requests, reconcile.Request{
336+
NamespacedName: types.NamespacedName{
337+
Name: function.Name,
338+
Namespace: function.Namespace,
339+
},
340+
})
341+
}
342+
}
346343
}
347344

348345
return requests

0 commit comments

Comments
 (0)