Skip to content

Commit b60f0cc

Browse files
committed
tmp
1 parent e5b7e5d commit b60f0cc

File tree

6 files changed

+467
-105
lines changed

6 files changed

+467
-105
lines changed

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,14 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified
174174
deploy: docker-build cluster grpcurl
175175
./hack/deploy_with_helm.sh
176176

177+
.PHONY: deploy-with-operator
177178
deploy-with-operator: docker-build build-operator cluster grpcurl
178179
./hack/deploy_with_operator.sh
179180

181+
.PHONY: operator-logs
182+
operator-logs:
183+
kubectl logs -n jumpstarter-operator-system -l app.kubernetes.io/name=jumpstarter-operator -f
184+
180185
deploy-with-operator-parallel:
181186
make deploy-with-operator -j5 --output-sync=target
182187

deploy/operator/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ module github.com/jumpstarter-dev/jumpstarter-controller/deploy/operator
33
go 1.24.0
44

55
require (
6+
github.com/go-logr/logr v1.4.2
67
github.com/jumpstarter-dev/jumpstarter-controller v0.7.1
78
github.com/onsi/ginkgo/v2 v2.22.2
89
github.com/onsi/gomega v1.36.2
10+
github.com/pmezard/go-difflib v1.0.0
911
k8s.io/api v0.33.0
1012
k8s.io/apimachinery v0.33.0
1113
k8s.io/apiserver v0.33.0
@@ -42,7 +44,6 @@ require (
4244
github.com/gin-gonic/gin v1.10.0 // indirect
4345
github.com/go-chi/chi/v5 v5.2.0 // indirect
4446
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
45-
github.com/go-logr/logr v1.4.2 // indirect
4647
github.com/go-logr/stdr v1.2.2 // indirect
4748
github.com/go-logr/zapr v1.3.0 // indirect
4849
github.com/go-openapi/jsonpointer v0.21.0 // indirect
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package jumpstarter
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/pmezard/go-difflib/difflib"
23+
appsv1 "k8s.io/api/apps/v1"
24+
corev1 "k8s.io/api/core/v1"
25+
rbacv1 "k8s.io/api/rbac/v1"
26+
"k8s.io/apimachinery/pkg/api/equality"
27+
"sigs.k8s.io/yaml"
28+
)
29+
30+
// deploymentNeedsUpdate checks if a deployment needs to be updated using K8s semantic equality.
31+
func deploymentNeedsUpdate(existing, desired *appsv1.Deployment) bool {
32+
// Compare labels (only if desired.Labels is non-nil)
33+
if desired.Labels != nil && !equality.Semantic.DeepEqual(existing.Labels, desired.Labels) {
34+
return true
35+
}
36+
37+
// Compare annotations (only if desired.Annotations is non-nil)
38+
if desired.Annotations != nil && !equality.Semantic.DeepEqual(existing.Annotations, desired.Annotations) {
39+
return true
40+
}
41+
42+
// Compare the entire Spec using K8s semantic equality (handles nil vs empty automatically)
43+
return !equality.Semantic.DeepEqual(existing.Spec, desired.Spec)
44+
}
45+
46+
// serviceAccountNeedsUpdate checks if a service account needs to be updated using K8s semantic equality.
47+
func serviceAccountNeedsUpdate(existing, desired *corev1.ServiceAccount) bool {
48+
// Compare labels (only if desired.Labels is non-nil)
49+
if desired.Labels != nil && !equality.Semantic.DeepEqual(existing.Labels, desired.Labels) {
50+
return true
51+
}
52+
53+
// Compare annotations (only if desired.Annotations is non-nil)
54+
if desired.Annotations != nil && !equality.Semantic.DeepEqual(existing.Annotations, desired.Annotations) {
55+
return true
56+
}
57+
58+
return false
59+
}
60+
61+
// roleNeedsUpdate checks if a role needs to be updated using K8s semantic equality.
62+
func roleNeedsUpdate(existing, desired *rbacv1.Role) bool {
63+
// Compare labels (only if desired.Labels is non-nil)
64+
if desired.Labels != nil && !equality.Semantic.DeepEqual(existing.Labels, desired.Labels) {
65+
return true
66+
}
67+
68+
// Compare annotations (only if desired.Annotations is non-nil)
69+
if desired.Annotations != nil && !equality.Semantic.DeepEqual(existing.Annotations, desired.Annotations) {
70+
return true
71+
}
72+
73+
// Compare rules (only if non-nil in desired)
74+
if desired.Rules != nil && !equality.Semantic.DeepEqual(existing.Rules, desired.Rules) {
75+
return true
76+
}
77+
78+
return false
79+
}
80+
81+
// roleBindingNeedsUpdate checks if a role binding needs to be updated using K8s semantic equality.
82+
func roleBindingNeedsUpdate(existing, desired *rbacv1.RoleBinding) bool {
83+
// Compare labels (only if desired.Labels is non-nil)
84+
if desired.Labels != nil && !equality.Semantic.DeepEqual(existing.Labels, desired.Labels) {
85+
return true
86+
}
87+
88+
// Compare annotations (only if desired.Annotations is non-nil)
89+
if desired.Annotations != nil && !equality.Semantic.DeepEqual(existing.Annotations, desired.Annotations) {
90+
return true
91+
}
92+
93+
// Compare subjects (only if non-nil in desired)
94+
if desired.Subjects != nil && !equality.Semantic.DeepEqual(existing.Subjects, desired.Subjects) {
95+
return true
96+
}
97+
98+
// Compare role ref (only if non-zero in desired)
99+
if desired.RoleRef.Name != "" && !equality.Semantic.DeepEqual(existing.RoleRef, desired.RoleRef) {
100+
return true
101+
}
102+
103+
return false
104+
}
105+
106+
// generateDiff creates a unified diff between existing and desired resources.
107+
// It works with any Kubernetes resource type.
108+
// Returns the diff string and any error encountered during serialization.
109+
func generateDiff[T any](existing, desired *T) (string, error) {
110+
// Serialize existing resource to YAML
111+
existingYAML, err := yaml.Marshal(existing)
112+
if err != nil {
113+
return "", fmt.Errorf("failed to marshal existing resource: %w", err)
114+
}
115+
116+
// Serialize desired resource to YAML
117+
desiredYAML, err := yaml.Marshal(desired)
118+
if err != nil {
119+
return "", fmt.Errorf("failed to marshal desired resource: %w", err)
120+
}
121+
122+
// Generate unified diff
123+
diff := difflib.UnifiedDiff{
124+
A: difflib.SplitLines(string(existingYAML)),
125+
B: difflib.SplitLines(string(desiredYAML)),
126+
FromFile: "Existing",
127+
ToFile: "Desired",
128+
Context: 3,
129+
}
130+
131+
return difflib.GetUnifiedDiffString(diff)
132+
}

deploy/operator/internal/controller/jumpstarter/endpoints/endpoints.go

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@ func NewReconciler(client client.Client, scheme *runtime.Scheme) *Reconciler {
4949
func (r *Reconciler) createOrUpdateService(ctx context.Context, service *corev1.Service, owner metav1.Object) error {
5050
log := logf.FromContext(ctx)
5151

52-
if owner != nil {
53-
if err := controllerutil.SetControllerReference(owner, service, r.Scheme); err != nil {
54-
return err
55-
}
56-
}
57-
5852
existingService := &corev1.Service{}
5953
existingService.Name = service.Name
6054
existingService.Namespace = service.Namespace
@@ -66,13 +60,12 @@ func (r *Reconciler) createOrUpdateService(ctx context.Context, service *corev1.
6660
existingService.Spec = service.Spec
6761
existingService.Labels = service.Labels
6862
existingService.Annotations = service.Annotations
63+
if owner != nil {
64+
if err := controllerutil.SetControllerReference(owner, existingService, r.Scheme); err != nil {
65+
return err
66+
}
67+
}
6968
} else {
70-
// Service exists, preserve immutable fields
71-
preservedClusterIP := existingService.Spec.ClusterIP
72-
preservedClusterIPs := existingService.Spec.ClusterIPs
73-
preservedIPFamilies := existingService.Spec.IPFamilies
74-
preservedIPFamilyPolicy := existingService.Spec.IPFamilyPolicy
75-
7669
// Preserve existing NodePorts to prevent "port already allocated" errors
7770
if service.Spec.Type == corev1.ServiceTypeNodePort || service.Spec.Type == corev1.ServiceTypeLoadBalancer {
7871
for i := range existingService.Spec.Ports {
@@ -83,15 +76,18 @@ func (r *Reconciler) createOrUpdateService(ctx context.Context, service *corev1.
8376
}
8477

8578
// Update all mutable fields
86-
existingService.Spec = service.Spec
79+
if service.Spec.LoadBalancerClass != nil && *service.Spec.LoadBalancerClass != "" {
80+
existingService.Spec.LoadBalancerClass = service.Spec.LoadBalancerClass
81+
}
82+
if service.Spec.ExternalTrafficPolicy != "" {
83+
existingService.Spec.ExternalTrafficPolicy = service.Spec.ExternalTrafficPolicy
84+
}
85+
86+
existingService.Spec.Ports = service.Spec.Ports
87+
existingService.Spec.Selector = service.Spec.Selector
88+
existingService.Spec.Type = service.Spec.Type
8789
existingService.Labels = service.Labels
8890
existingService.Annotations = service.Annotations
89-
90-
// Restore immutable fields
91-
existingService.Spec.ClusterIP = preservedClusterIP
92-
existingService.Spec.ClusterIPs = preservedClusterIPs
93-
existingService.Spec.IPFamilies = preservedIPFamilies
94-
existingService.Spec.IPFamilyPolicy = preservedIPFamilyPolicy
9591
}
9692

9793
return nil
@@ -105,25 +101,12 @@ func (r *Reconciler) createOrUpdateService(ctx context.Context, service *corev1.
105101
return err
106102
}
107103

108-
// Log the operation result
109-
switch op {
110-
case controllerutil.OperationResultCreated:
111-
log.Info("Created service",
112-
"name", service.Name,
113-
"namespace", service.Namespace,
114-
"type", service.Spec.Type,
115-
"selector", service.Spec.Selector)
116-
case controllerutil.OperationResultUpdated:
117-
log.Info("Updated service",
118-
"name", service.Name,
119-
"namespace", service.Namespace,
120-
"type", service.Spec.Type,
121-
"selector", service.Spec.Selector)
122-
case controllerutil.OperationResultNone:
123-
log.V(1).Info("Service already up to date",
124-
"name", service.Name,
125-
"namespace", service.Namespace)
126-
}
104+
log.Info("Service reconciled",
105+
"name", service.Name,
106+
"namespace", service.Namespace,
107+
"type", service.Spec.Type,
108+
"selector", service.Spec.Selector,
109+
"operation", op)
127110

128111
return nil
129112
}

0 commit comments

Comments
 (0)