Skip to content

Commit 0f86cc0

Browse files
authored
feat: Add automatic package label management and Package-to-Function mapping (#201)
### Motivation This PR addresses the need for better relationship management between Functions and Packages in the FunctionStream operator. Previously, there was no automatic way to track which Functions depend on which Packages, making it difficult to: 1. Automatically trigger Function reconciliations when their referenced Packages are updated 2. Maintain consistent labeling for Functions based on their Package dependencies 3. Efficiently query Functions that depend on a specific Package The changes also remove the overly restrictive validation that prevented Package updates when Functions referenced them, replacing it with a more flexible approach that automatically handles Package updates. ### Modifications 1. **Automatic Package Label Management**: - Functions now automatically receive a `package` label that matches their `spec.package` field - Labels are updated when the Package reference changes - Existing labels are preserved when adding the package label 2. **Package-to-Function Mapping**: - Added `mapPackageToFunctions` function that finds all Functions referencing a specific Package using label selectors - Added Package watcher in the Function controller to trigger reconciliations when Packages are updated - This enables automatic deployment updates when Package images change 3. **Enhanced Testing**: - Added comprehensive test cases for automatic label management - Added tests for Package-to-Function mapping functionality - Added tests for cross-namespace Package updates - Added tests for Package change propagation to multiple Functions 4. **Deployment Improvements**: - Removed redundant `ImagePullPolicy` settings that were causing issues - Streamlined container configuration These changes provide a more robust and maintainable way to manage Function-Package relationships while enabling automatic updates when Package configurations change. --------- Signed-off-by: EvanWave <[email protected]>
1 parent 9393c4b commit 0f86cc0

File tree

8 files changed

+524
-80
lines changed

8 files changed

+524
-80
lines changed

.github/workflows/lint.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2024 Function Stream Org.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: Lint
16+
17+
on:
18+
pull_request:
19+
20+
jobs:
21+
lint:
22+
name: Run on Ubuntu
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Clone the code
26+
uses: actions/checkout@v4
27+
28+
- name: Setup Go
29+
uses: actions/setup-go@v5
30+
with:
31+
go-version-file: ./operator/go.mod
32+
33+
- name: Run linter
34+
uses: golangci/golangci-lint-action@v6
35+
working-directory: ./operator/
36+
with:
37+
version: v1.63.4

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,6 @@ dist
108108
bin/
109109
.DS_Store
110110

111-
benchmark/*.pprof
111+
benchmark/*.pprof
112+
113+
operator/vendor/

operator/examples/function.yaml

Lines changed: 0 additions & 17 deletions
This file was deleted.

operator/examples/package.yaml

Lines changed: 0 additions & 15 deletions
This file was deleted.

operator/internal/controller/function_controller.go

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ package controller
1919
import (
2020
"context"
2121
"fmt"
22+
"reflect"
23+
2224
"github.com/FunctionStream/function-stream/operator/utils"
2325
"k8s.io/apimachinery/pkg/util/json"
24-
"reflect"
2526

2627
"gopkg.in/yaml.v3"
2728
appsv1 "k8s.io/api/apps/v1"
@@ -33,8 +34,10 @@ import (
3334
ctrl "sigs.k8s.io/controller-runtime"
3435
"sigs.k8s.io/controller-runtime/pkg/builder"
3536
"sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/handler"
3638
logf "sigs.k8s.io/controller-runtime/pkg/log"
3739
"sigs.k8s.io/controller-runtime/pkg/predicate"
40+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3841

3942
fsv1alpha1 "github.com/FunctionStream/function-stream/operator/api/v1alpha1"
4043
)
@@ -81,7 +84,17 @@ func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
8184
return ctrl.Result{}, err
8285
}
8386

84-
// 2. Get Package
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+
}
96+
97+
// 3. Get Package
8598
var pkg fsv1alpha1.Package
8699
if err := r.Get(ctx, types.NamespacedName{Name: fn.Spec.Package, Namespace: req.Namespace}, &pkg); err != nil {
87100
log.Error(err, "Failed to get Package", "package", fn.Spec.Package)
@@ -95,14 +108,14 @@ func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
95108
return ctrl.Result{}, fmt.Errorf("package %s has no image", fn.Spec.Package)
96109
}
97110

98-
// 3. Build config yaml content
111+
// 4. Build config yaml content
99112
configYaml, err := buildFunctionConfigYaml(&fn, r.Config)
100113
if err != nil {
101114
log.Error(err, "Failed to marshal config yaml")
102115
return ctrl.Result{}, err
103116
}
104117

105-
// 4. Build Deployment
118+
// 5. Build Deployment
106119
deployName := fmt.Sprintf("function-%s", fn.Name)
107120
var replicas int32 = 1
108121
labels := map[string]string{
@@ -132,19 +145,17 @@ EOF
132145
},
133146
Spec: corev1.PodSpec{
134147
InitContainers: []corev1.Container{{
135-
Name: "init-config",
136-
Image: image,
137-
ImagePullPolicy: corev1.PullIfNotPresent,
138-
Command: []string{"/bin/sh", "-c", initCommand},
148+
Name: "init-config",
149+
Image: image,
150+
Command: []string{"/bin/sh", "-c", initCommand},
139151
VolumeMounts: []corev1.VolumeMount{{
140152
Name: "function-config",
141153
MountPath: "/config",
142154
}},
143155
}},
144156
Containers: []corev1.Container{{
145-
Name: "function",
146-
Image: image,
147-
ImagePullPolicy: corev1.PullIfNotPresent,
157+
Name: "function",
158+
Image: image,
148159
VolumeMounts: []corev1.VolumeMount{{
149160
Name: "function-config",
150161
MountPath: "/config",
@@ -168,7 +179,7 @@ EOF
168179
return ctrl.Result{}, err
169180
}
170181

171-
// 5. Create or Update Deployment
182+
// 6. Create or Update Deployment
172183
var existingDeploy appsv1.Deployment
173184
deployErr := r.Get(ctx, types.NamespacedName{Name: deployName, Namespace: fn.Namespace}, &existingDeploy)
174185
if deployErr == nil {
@@ -199,6 +210,24 @@ EOF
199210
}
200211
}
201212

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

@@ -280,6 +309,38 @@ func (r *FunctionReconciler) SetupWithManager(mgr ctrl.Manager) error {
280309
return ctrl.NewControllerManagedBy(mgr).
281310
For(&fsv1alpha1.Function{}).
282311
Owns(&appsv1.Deployment{}, builder.WithPredicates(functionLabelPredicate)).
312+
Watches(
313+
&fsv1alpha1.Package{},
314+
handler.EnqueueRequestsFromMapFunc(r.mapPackageToFunctions),
315+
).
283316
Named("function").
284317
Complete(r)
285318
}
319+
320+
// mapPackageToFunctions maps Package changes to related Functions
321+
func (r *FunctionReconciler) mapPackageToFunctions(ctx context.Context, obj client.Object) []reconcile.Request {
322+
packageObj, ok := obj.(*fsv1alpha1.Package)
323+
if !ok {
324+
return nil
325+
}
326+
327+
// Get Functions that reference this Package using label selector
328+
var functions fsv1alpha1.FunctionList
329+
if err := r.List(ctx, &functions,
330+
client.InNamespace(packageObj.Namespace),
331+
client.MatchingLabels(map[string]string{"package": packageObj.Name})); err != nil {
332+
return nil
333+
}
334+
335+
var requests []reconcile.Request
336+
for _, function := range functions.Items {
337+
requests = append(requests, reconcile.Request{
338+
NamespacedName: types.NamespacedName{
339+
Name: function.Name,
340+
Namespace: function.Namespace,
341+
},
342+
})
343+
}
344+
345+
return requests
346+
}

0 commit comments

Comments
 (0)