Repository: kubernetes-sigs/kro

Issue:
Add Support for Immutable Fields in SimpleSchema
### Feature Description

**Problem Statement**:

Currently, kro's SimpleSchema lacks the ability to mark fields as immutable, meaning users cannot prevent certain fields from being modified after resource creation. 
This is an issue for resource identifiers. Fields like name, id, etc should never change after creation as they often serve as unique identifiers.
Without immutable field support, users must rely on external validation or manual processes to prevent unwanted modifications, leading to potential runtime errors and system instability.


**Proposed Solution**:
Add an immutable marker to SimpleSchema that generates CEL validation rules to prevent field modifications:

```
spec:
  schema:
    spec:
      resourceId: string | required=true immutable=true description="Unique resource identifier"
      name: string | immutable=true description="Resource name"
      replicas: integer | default=3 description="Number of replicas (mutable)"
```
Implementation Details:
• Add MarkerTypeImmutable constant to markers.go
• Implement parsing logic for immutable=true/false values
• Generate CEL validation rule: "self == oldSelf" with message "field is immutable"


* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue
* If you are interested in working on this feature, please leave a comment


Hints from issue comments:
@a-hilaly: This was merged thank you @Soumya-Vaidya !

Solution patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9538029..5dfccc2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -66,7 +66,7 @@ Looking at the existing issues is a great way to find something to work on. As t
 
 ## Code of Conduct
 
-This project has adopted the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).
+This project has adopted the [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)
 
 
 ## Security
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
deleted file mode 100644
index cc05050..0000000
--- a/MAINTAINERS.md
+++ /dev/null
@@ -1,16 +0,0 @@
-The current Maintainers Group for the kro project consists of:
-
-| Name                                                    | Employer    |
-|---------------------------------------------------------|-------------|
-| [@a-hilaly](https://github.com/a-hilaly)                | AWS         |
-| [@barney-s](https://github.com/barney-s)                | Google      |
-| [@bridgetkromhout](https://github.com/bridgetkromhout)  | Microsoft   |
-| [@cheftako](https://github.com/cheftako)                | Google      |
-| [@jlbutler](https://github.com/jlbutler)                | AWS         |
-| [@matthchr](https://github.com/matthchr)                | Microsoft   |
-| [@nicslatts](https://github.com/nicslatts)              | Astronomer  |
-
-Emeritus maintainers:
-
-[@eqe-aws](https://github.com/eqe-aws)
-[@rynowak](https://github.com/rynowak)
diff --git a/OWNERS.md b/OWNERS.md
new file mode 100644
index 0000000..17d9b02
--- /dev/null
+++ b/OWNERS.md
@@ -0,0 +1,9 @@
+# See the OWNERS_ALIASES docs: https://git.k8s.io/community/contributors/guide/owners.md#owners_aliases
+
+approvers:
+  - kro-maintainers
+
+reviewers:
+  - kro-reviewers
+
+# XXX TODO add sig leads to approvers
diff --git a/OWNERS_ALIASES.md b/OWNERS_ALIASES.md
new file mode 100644
index 0000000..4e535f3
--- /dev/null
+++ b/OWNERS_ALIASES.md
@@ -0,0 +1,20 @@
+# See the OWNERS_ALIASES docs: https://git.k8s.io/community/contributors/guide/owners.md#owners_aliases
+
+aliases:
+  kro-maintainers:
+    - a-hilaly
+    - barney-s
+    - bridgetkromhout
+    - cheftako
+    - jlbutler
+    - matthchr
+    - nicslatts
+  kro-reviewers:
+    - jakobmoellerdev
+    - michaelhtm
+    - n3wscott
+  emeritus-karpenter-maintainers:
+    - eqe-aws
+    - rynowak
+
+# XXX TODO add sig leads to aliases
diff --git a/README.md b/README.md
index 903031d..aca6505 100644
--- a/README.md
+++ b/README.md
@@ -8,15 +8,15 @@
 [![Build and Publish](https://github.com/kro-run/kro/actions/workflows/build-push-image.yaml/badge.svg?branch=main)](https://github.com/kro-run/kro/actions/workflows/build-push-image.yaml)
 ![GitHub Repo stars](https://img.shields.io/github/stars/kro-run/kro)
 
-This project aims to simplify the creation and management of complex custom resources for Kubernetes.
+Kube Resource Orchestrator (kro) is a subproject of [Kubernetes SIG Cloud Provider](https://github.com/kubernetes/community/blob/master/sig-cloud-provider/README.md). This project aims to simplify the creation and management of complex custom resources for Kubernetes.
 
-Kube Resource Orchestrator (**kro**) helps you to define complex multi-resource constructs as reusable components in your applications and systems. It does this by providing a Kubernetes-native, vendor agnostic way to define groupings of Kubernetes resources. 
+Kro helps you to define complex multi-resource constructs as reusable components in your applications and systems. It does this by providing a Kubernetes-native, vendor agnostic way to define groupings of Kubernetes resources.
 
-kro's fundamental custom resource is the *ResourceGraphDefinition*. A ResourceGraphDefinition defines collections of underlying Kubernetes resources. It can define any Kubernetes resources, either native or custom, and can specify the dependencies between them. This lets you define complex custom resources, and include default configurations for their use.
+Kro's fundamental custom resource is the *ResourceGraphDefinition*. A ResourceGraphDefinition defines collections of underlying Kubernetes resources. It can define any Kubernetes resources, either native or custom, and can specify the dependencies between them. This lets you define complex custom resources, and include default configurations for their use.
 
 The kro controller will determine the dependencies between resources, establish the correct order of operations to create and configure them, and then dynamically create and manage all of the underlying resources for you.
 
-kro is Kubernetes native and integrates seamlessly with existing tools to preserve familiar processes and interfaces.
+Kro is Kubernetes native and integrates seamlessly with existing tools to preserve familiar processes and interfaces.
 
 ## Documentation
 
@@ -92,9 +92,13 @@ Please join our community meeting.
 [zoom]: https://us06web.zoom.us/j/85388697226?pwd=9Xxz1F0FcNUq8zFGrsRqkHMhFZTpuj.1
 [youtube]: https://www.youtube.com/channel/UCUlcI3NYq9ehl5wsdfbJzSA
 
+### Code of conduct
+
+Participation in the project community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md).
+
 ## Security
 
-See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
+See [CONTRIBUTING](CONTRIBUTING.md#security) and [SECURITY](SECURITY.md) for information.
 
 ## License
 
diff --git a/cmd/controller/main.go b/cmd/controller/main.go
index a9f52af..82eb130 100644
--- a/cmd/controller/main.go
+++ b/cmd/controller/main.go
@@ -63,6 +63,7 @@ func main() {
 	var (
 		metricsAddr                                 string
 		enableLeaderElection                        bool
+		leaderElectionNamespace                     string
 		probeAddr                                   string
 		allowCRDDeletion                            bool
 		resourceGraphDefinitionConcurrentReconciles int
@@ -87,6 +88,10 @@ func main() {
 	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
 		"Enable leader election for controller manager. "+
 			"Enabling this will ensure there is only one active controller manager.")
+	flag.StringVar(&leaderElectionNamespace, "leader-election-namespace", "",
+		"Specific namespace that the controller will utilize to manage the coordination.k8s.io/lease object for"+
+			"leader election. By default it will try to use the namespace of the service account mounted"+
+			" to the controller pod.")
 	flag.BoolVar(&allowCRDDeletion, "allow-crd-deletion", false, "allow kro to delete CRDs")
 	flag.IntVar(&resourceGraphDefinitionConcurrentReconciles,
 		"resource-graph-definition-concurrent-reconciles", 1,
@@ -147,9 +152,10 @@ func main() {
 		Metrics: metricsserver.Options{
 			BindAddress: metricsAddr,
 		},
-		HealthProbeBindAddress: probeAddr,
-		LeaderElection:         enableLeaderElection,
-		LeaderElectionID:       "6f0f64a5.kro.run",
+		HealthProbeBindAddress:  probeAddr,
+		LeaderElection:          enableLeaderElection,
+		LeaderElectionID:        "6f0f64a5.kro.run",
+		LeaderElectionNamespace: leaderElectionNamespace,
 		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
 		// when the Manager ends. This requires the binary to immediately end when the
 		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml
index 1f04e18..1c78706 100644
--- a/helm/templates/deployment.yaml
+++ b/helm/templates/deployment.yaml
@@ -91,6 +91,10 @@ spec:
             - "$(KRO_CLIENT_BURST)"
             {{- if .Values.config.enableLeaderElection }}
             - --leader-elect
+            {{- if ne .Values.config.leaderElectionNamespace "" }}
+            - --leader-election-namespace
+            - {{ .Values.config.leaderElectionNamespace | quote }}
+            {{- end }}
             {{- end }}
           livenessProbe:
             httpGet:
diff --git a/helm/values.yaml b/helm/values.yaml
index c98864d..bd2d301 100644
--- a/helm/values.yaml
+++ b/helm/values.yaml
@@ -101,7 +101,11 @@ config:
   # The number of requests that can be stored for processing before the server starts enforcing the QPS limit
   clientBurst: 150
   # Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.
-  enableLeaderElection: false
+  enableLeaderElection: true
+  # Leader election can be scoped to a specific namespace. By default, the controller
+  # will attempt to use the namespace of the service account mounted to the Controller
+  # pod.
+  leaderElectionNamespace: ""
   # The address the metric endpoint binds to
   metricsBindAddress: :8078
   # The address the probe endpoint binds to
diff --git a/pkg/simpleschema/markers.go b/pkg/simpleschema/markers.go
index 80e5ec7..fb079d0 100644
--- a/pkg/simpleschema/markers.go
+++ b/pkg/simpleschema/markers.go
@@ -55,12 +55,14 @@ const (
 	MarkerTypeValidation MarkerType = "validation"
 	// MarkerTypeEnum represents the `enum` marker.
 	MarkerTypeEnum MarkerType = "enum"
+	// MarkerTypeImmutable represents the `immutable` marker.
+	MarkerTypeImmutable MarkerType = "immutable"
 )
 
 func markerTypeFromString(s string) (MarkerType, error) {
 	switch MarkerType(s) {
 	case MarkerTypeRequired, MarkerTypeDefault, MarkerTypeDescription,
-		MarkerTypeMinimum, MarkerTypeMaximum, MarkerTypeValidation, MarkerTypeEnum:
+		MarkerTypeMinimum, MarkerTypeMaximum, MarkerTypeValidation, MarkerTypeEnum, MarkerTypeImmutable:
 		return MarkerType(s), nil
 	default:
 		return "", fmt.Errorf("unknown marker type: %s", s)
diff --git a/pkg/simpleschema/transform.go b/pkg/simpleschema/transform.go
index 33bf5ae..d6ade51 100644
--- a/pkg/simpleschema/transform.go
+++ b/pkg/simpleschema/transform.go
@@ -275,6 +275,20 @@ func (tf *transformer) applyMarkers(schema *extv1.JSONSchemaProps, markers []*Ma
 				},
 			}
 			schema.XValidations = validation
+		case MarkerTypeImmutable:
+			isImmutable, err := strconv.ParseBool(marker.Value)
+			if err != nil {
+				return fmt.Errorf("failed to parse immutable marker value: %w", err)
+			}
+			if isImmutable {
+				immutableValidation := []extv1.ValidationRule{
+					{
+						Rule:    "self == oldSelf",
+						Message: "field is immutable",
+					},
+				}
+				schema.XValidations = append(schema.XValidations, immutableValidation...)
+			}
 		case MarkerTypeEnum:
 			var enumJSONValues []extv1.JSON
 
diff --git a/website/docs/docs/concepts/10-simple-schema.md b/website/docs/docs/concepts/10-simple-schema.md
index 06e66e4..9b8fd92 100644
--- a/website/docs/docs/concepts/10-simple-schema.md
+++ b/website/docs/docs/concepts/10-simple-schema.md
@@ -19,7 +19,7 @@ spec:
     kind: WebApplication
     spec:
       # Basic types
-      name: string | required=true description="My Name"
+      name: string | required=true immutable=true description="My Name"
       replicas: integer | default=1 minimum=1 maximum=100
       image: string | required=true
 
@@ -193,6 +193,7 @@ mode: string | enum="debug,info,warn,error" default="info"
 - `enum="value1,value2"`: Allowed values
 - `minimum=value`: Minimum value for numbers
 - `maximum=value`: Maximum value for numbers
+- `immutable=true`: Field cannot be changed after creation
 
 Multiple markers can be combined using the `|` separator.
 
@@ -200,6 +201,7 @@ For example:
 
 ```yaml
 name: string | required=true default="app" description="Application name"
+id: string | required=true immutable=true description="Unique identifier"
 replicas: integer | default=3 minimum=1 maximum=10
 price: float | minimum=0.01 maximum=999.99
 mode: string | enum="debug,info,warn,error" default="info"



Test patch:
diff --git a/pkg/simpleschema/markers_test.go b/pkg/simpleschema/markers_test.go
index e922dbc..c68dbd0 100644
--- a/pkg/simpleschema/markers_test.go
+++ b/pkg/simpleschema/markers_test.go
@@ -125,6 +125,15 @@ func TestParseMarkers(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name:  "immutable marker with other markers",
+			input: "immutable=true required=false",
+			want: []*Marker{
+				{MarkerType: MarkerTypeImmutable, Key: "immutable", Value: "true"},
+				{MarkerType: MarkerTypeRequired, Key: "required", Value: "false"},
+			},
+			wantErr: false,
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/pkg/simpleschema/transform_test.go b/pkg/simpleschema/transform_test.go
index 0c44c5b..0227e69 100644
--- a/pkg/simpleschema/transform_test.go
+++ b/pkg/simpleschema/transform_test.go
@@ -485,6 +485,73 @@ func TestBuildOpenAPISchema(t *testing.T) {
 			want:    nil,
 			wantErr: true,
 		},
+		{
+			name: "Simple immutable field",
+			obj: map[string]interface{}{
+				"id": "string | immutable=true",
+			},
+			want: &extv1.JSONSchemaProps{
+				Type: "object",
+				Properties: map[string]extv1.JSONSchemaProps{
+					"id": {
+						Type: "string",
+						XValidations: []extv1.ValidationRule{
+							{
+								Rule:    "self == oldSelf",
+								Message: "field is immutable",
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Simple immutable field with false value",
+			obj: map[string]interface{}{
+				"name": "string | immutable=false",
+			},
+			want: &extv1.JSONSchemaProps{
+				Type: "object",
+				Properties: map[string]extv1.JSONSchemaProps{
+					"name": {
+						Type: "string",
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Immutable with other markers",
+			obj: map[string]interface{}{
+				"resourceId": `string | required=true immutable=true description="Unique resource identifier"`,
+			},
+			want: &extv1.JSONSchemaProps{
+				Type:     "object",
+				Required: []string{"resourceId"},
+				Properties: map[string]extv1.JSONSchemaProps{
+					"resourceId": {
+						Type:        "string",
+						Description: "Unique resource identifier",
+						XValidations: []extv1.ValidationRule{
+							{
+								Rule:    "self == oldSelf",
+								Message: "field is immutable",
+							},
+						},
+					},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Invalid immutable value",
+			obj: map[string]interface{}{
+				"id": "string | immutable=invalid",
+			},
+			want:    nil,
+			wantErr: true,
+		},
 		{
 			name: "Custom simple type (required)",
 			obj: map[string]interface{}{