From 5efb6e81f0596efa3300af5b830c573403c947d9 Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 16:53:04 +0800 Subject: [PATCH 01/11] fix: reconcile LogSet dependencies and CN metrics drift Refs #615 --- pkg/controllers/cnset/controller.go | 49 +++++++++++- pkg/controllers/cnset/controller_test.go | 76 ++++++++++++++++++- .../common/statefulset_predicate.go | 37 +++++++++ .../common/statefulset_predicate_test.go | 40 ++++++++++ pkg/controllers/dnset/controller.go | 38 +++++++++- pkg/controllers/dnset/controller_test.go | 30 +++++++- 6 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 pkg/controllers/common/statefulset_predicate.go create mode 100644 pkg/controllers/common/statefulset_predicate_test.go diff --git a/pkg/controllers/cnset/controller.go b/pkg/controllers/cnset/controller.go index b36965b8..25bfcbfe 100644 --- a/pkg/controllers/cnset/controller.go +++ b/pkg/controllers/cnset/controller.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ package cnset import ( + "context" "fmt" "strconv" "time" @@ -40,7 +41,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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/handler" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // reconcile configuration @@ -205,11 +210,21 @@ func (c *Actor) syncMetricService(ctx *recon.Context[*v1alpha1.CNSet]) error { }, } return recon.CreateOwnedOrUpdate(ctx, svc, func() error { + if svc.Labels == nil { + svc.Labels = map[string]string{} + } + for key, value := range common.SubResourceLabels(cn) { + svc.Labels[key] = value + } + svc.Spec.Selector = common.SubResourceLabels(cn) svc.Spec.Type = corev1.ServiceTypeClusterIP svc.Spec.Ports = []corev1.ServicePort{{ Name: "metric", Port: int32(common.MetricsPort), }} + if err := controllerutil.SetControllerReference(cn, svc, ctx.Client.Scheme()); err != nil { + return err + } if cn.Spec.PromDiscoveredByService() { if svc.Annotations == nil { svc.Annotations = map[string]string{} @@ -335,7 +350,9 @@ func (c *Actor) Reconcile(mgr manager.Manager) error { err := recon.Setup[*v1alpha1.CNSet](&v1alpha1.CNSet{}, "cnset", mgr, c, recon.WithBuildFn(func(b *builder.Builder) { b.Owns(&kruisev1alpha1.CloneSet{}). - Owns(&corev1.Service{}) + Owns(&corev1.Service{}). + Watches(&kruise.StatefulSet{}, handler.EnqueueRequestsFromMapFunc(requestsForLogSetStatefulSet(mgr.GetClient())), + builder.WithPredicates(common.LogSetStatefulSetChangedPredicate())) })) if err != nil { return err @@ -343,6 +360,34 @@ func (c *Actor) Reconcile(mgr manager.Manager) error { return nil } + +func requestsForLogSetStatefulSet(reader client.Reader) handler.MapFunc { + return func(ctx context.Context, object client.Object) []reconcile.Request { + sts, ok := object.(*kruise.StatefulSet) + if !ok { + return nil + } + owner := metav1.GetControllerOf(sts) + if owner == nil || owner.APIVersion != v1alpha1.GroupVersion.String() || owner.Kind != "LogSet" { + return nil + } + + sets := &v1alpha1.CNSetList{} + if err := reader.List(ctx, sets, client.InNamespace(sts.Namespace)); err != nil { + log.FromContext(ctx).Error(err, "list CNSets for LogSet StatefulSet", "statefulset", client.ObjectKeyFromObject(sts)) + return nil + } + + requests := make([]reconcile.Request, 0, len(sets.Items)) + for i := range sets.Items { + set := &sets.Items[i] + if set.Deps.LogSet != nil && set.Deps.LogSet.Name == owner.Name { + requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(set)}) + } + } + return requests + } +} func syncCloneSet(ctx *recon.Context[*v1alpha1.CNSet], cs *kruisev1alpha1.CloneSet) error { cn := ctx.Obj pooling := cn.Spec.PodManagementPolicy != nil && *cn.Spec.PodManagementPolicy == v1alpha1.PodManagementPolicyPooling diff --git a/pkg/controllers/cnset/controller_test.go b/pkg/controllers/cnset/controller_test.go index 3ddf3fdf..235fbaac 100644 --- a/pkg/controllers/cnset/controller_test.go +++ b/pkg/controllers/cnset/controller_test.go @@ -37,6 +37,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) func baseCNSetForMetricSvcTest() *v1alpha1.CNSet { @@ -123,6 +124,49 @@ func Test_syncMetricService(t *testing.T) { g.Expect(svc.Annotations).NotTo(HaveKey(common.PrometheusPortAnno)) }, }, + { + name: "repairs drift while preserving user metadata", + cnset: baseCNSetForMetricSvcTest(), + client: &fake.Client{ + Client: fake.KubeClientBuilder().WithScheme(s).WithObjects( + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: metricSvcName(baseCNSetForMetricSvcTest()), + Labels: map[string]string{"drifted": "true"}, + Annotations: map[string]string{"user.example.com/keep": "yes"}, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, + Selector: map[string]string{"wrong": "selector"}, + Ports: []corev1.ServicePort{{ + Name: "wrong", + Port: 1234, + }}, + }, + }, + ).Build(), + }, + setup: enableCNPromServiceDiscovery, + expect: func(g *WithT, cn *v1alpha1.CNSet, cli client.Client, err error) { + g.Expect(err).To(BeNil()) + svc := &corev1.Service{} + g.Expect(cli.Get(context.Background(), client.ObjectKeyFromObject(&corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Namespace: cn.Namespace, + Name: metricSvcName(cn), + }}), svc)).To(Succeed()) + g.Expect(svc.Labels).To(HaveKeyWithValue("drifted", "true")) + for key, value := range common.SubResourceLabels(cn) { + g.Expect(svc.Labels).To(HaveKeyWithValue(key, value)) + } + g.Expect(svc.Spec.Selector).To(Equal(common.SubResourceLabels(cn))) + g.Expect(svc.Spec.Type).To(Equal(corev1.ServiceTypeClusterIP)) + g.Expect(svc.Spec.Ports).To(Equal([]corev1.ServicePort{{Name: "metric", Port: int32(common.MetricsPort)}})) + g.Expect(svc.Annotations).To(HaveKeyWithValue("user.example.com/keep", "yes")) + g.Expect(svc.Annotations).To(HaveKeyWithValue(common.PrometheusScrapeAnno, "true")) + g.Expect(metav1.IsControlledBy(svc, cn)).To(BeTrue()) + }, + }, { name: "removes stale prom annotations when export disabled", cnset: baseCNSetForMetricSvcTest(), @@ -181,6 +225,32 @@ func Test_syncMetricService(t *testing.T) { } } +func TestRequestsForLogSetStatefulSet(t *testing.T) { + s := newScheme() + logSet := &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "log", UID: "log-uid"}} + matching := &v1alpha1.CNSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "matching"}, + Deps: v1alpha1.CNSetDeps{LogSetRef: logSet.AsDependency()}, + } + unrelated := &v1alpha1.CNSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "unrelated"}, + Deps: v1alpha1.CNSetDeps{LogSetRef: v1alpha1.LogSetRef{LogSet: &v1alpha1.LogSet{ + ObjectMeta: metav1.ObjectMeta{Name: "other"}, + }}}, + } + cli := fake.KubeClientBuilder().WithScheme(s).WithObjects(matching, unrelated).Build() + sts := &kruisev1.StatefulSet{ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "log-log", + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(logSet, + v1alpha1.GroupVersion.WithKind("LogSet"))}, + }} + + requests := requestsForLogSetStatefulSet(cli)(context.Background(), sts) + g := NewGomegaWithT(t) + g.Expect(requests).To(Equal([]reconcile.Request{{NamespacedName: client.ObjectKeyFromObject(matching)}})) +} + func TestCNSetActor_Observe(t *testing.T) { s := newScheme() tpl := &v1alpha1.CNSet{ @@ -231,7 +301,7 @@ func TestCNSetActor_Observe(t *testing.T) { client: &fake.Client{ Client: fake.KubeClientBuilder().WithScheme(s).Build(), }, - expect: func(g *WithT, action recon.Action[*v1alpha1.CNSet], cli client.Client, err error) { + expect: func(g *WithT, action recon.Action[*v1alpha1.CNSet], _ client.Client, err error) { g.Expect(err).To(BeNil()) g.Expect(action.String()).To(ContainSubstring("Create")) }, @@ -242,7 +312,7 @@ func TestCNSetActor_Observe(t *testing.T) { client: &fake.Client{ Client: fake.KubeClientBuilder().WithScheme(s).Build(), }, - expect: func(g *WithT, action recon.Action[*v1alpha1.CNSet], cli client.Client, err error) { + expect: func(g *WithT, action recon.Action[*v1alpha1.CNSet], _ client.Client, err error) { g.Expect(err).To(BeNil()) g.Expect(action.String()).To(ContainSubstring("Create")) }, @@ -309,7 +379,7 @@ func TestCNSetActor_Observe(t *testing.T) { }, ).Build(), }, - expect: func(g *WithT, action recon.Action[*v1alpha1.CNSet], cli client.Client, err error) { + expect: func(g *WithT, action recon.Action[*v1alpha1.CNSet], _ client.Client, err error) { g.Expect(err).To(BeNil()) g.Expect(action.String()).To(ContainSubstring("Update")) }, diff --git a/pkg/controllers/common/statefulset_predicate.go b/pkg/controllers/common/statefulset_predicate.go new file mode 100644 index 00000000..071e1111 --- /dev/null +++ b/pkg/controllers/common/statefulset_predicate.go @@ -0,0 +1,37 @@ +// Copyright 2026 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "github.com/openkruise/kruise-api/apps/v1beta1" + "k8s.io/apimachinery/pkg/api/equality" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// LogSetStatefulSetChangedPredicate selects events that can change the LogSet +// service-addresses consumed by CNSet and DNSet configuration. +func LogSetStatefulSetChangedPredicate() predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(event.CreateEvent) bool { return true }, + DeleteFunc: func(event.DeleteEvent) bool { return true }, + UpdateFunc: func(e event.UpdateEvent) bool { + oldSts, oldOK := e.ObjectOld.(*v1beta1.StatefulSet) + newSts, newOK := e.ObjectNew.(*v1beta1.StatefulSet) + return oldOK && newOK && !equality.Semantic.DeepEqual(oldSts.Spec.ReserveOrdinals, newSts.Spec.ReserveOrdinals) + }, + GenericFunc: func(event.GenericEvent) bool { return false }, + } +} diff --git a/pkg/controllers/common/statefulset_predicate_test.go b/pkg/controllers/common/statefulset_predicate_test.go new file mode 100644 index 00000000..e4790d79 --- /dev/null +++ b/pkg/controllers/common/statefulset_predicate_test.go @@ -0,0 +1,40 @@ +// Copyright 2026 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package common + +import ( + "testing" + + kruisev1 "github.com/openkruise/kruise-api/apps/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +func TestLogSetStatefulSetChangedPredicate(t *testing.T) { + p := LogSetStatefulSetChangedPredicate() + oldSts := &kruisev1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "log"}} + newSts := oldSts.DeepCopy() + newSts.Spec.ReserveOrdinals = []int{1} + + if !p.Update(event.UpdateEvent{ObjectOld: oldSts, ObjectNew: newSts}) { + t.Fatal("reserveOrdinals change must trigger reconciliation") + } + + statusOnly := newSts.DeepCopy() + statusOnly.Status.ReadyReplicas = 1 + if p.Update(event.UpdateEvent{ObjectOld: newSts, ObjectNew: statusOnly}) { + t.Fatal("status-only change must not trigger dependent reconciliation") + } +} diff --git a/pkg/controllers/dnset/controller.go b/pkg/controllers/dnset/controller.go index 927ed308..3dac14a0 100644 --- a/pkg/controllers/dnset/controller.go +++ b/pkg/controllers/dnset/controller.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ package dnset import ( + "context" "strconv" "time" @@ -36,7 +37,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const ( @@ -301,7 +305,9 @@ func (d *Actor) Reconcile(mgr manager.Manager) error { err := recon.Setup[*v1alpha1.DNSet](&v1alpha1.DNSet{}, "dnset", mgr, d, recon.WithBuildFn(func(b *builder.Builder) { b.Owns(&kruise.StatefulSet{}). - Owns(&corev1.Service{}) + Owns(&corev1.Service{}). + Watches(&kruise.StatefulSet{}, handler.EnqueueRequestsFromMapFunc(requestsForLogSetStatefulSet(mgr.GetClient())), + builder.WithPredicates(common.LogSetStatefulSetChangedPredicate())) })) if err != nil { return err @@ -309,3 +315,31 @@ func (d *Actor) Reconcile(mgr manager.Manager) error { return nil } + +func requestsForLogSetStatefulSet(reader client.Reader) handler.MapFunc { + return func(ctx context.Context, object client.Object) []reconcile.Request { + sts, ok := object.(*kruise.StatefulSet) + if !ok { + return nil + } + owner := metav1.GetControllerOf(sts) + if owner == nil || owner.APIVersion != v1alpha1.GroupVersion.String() || owner.Kind != "LogSet" { + return nil + } + + sets := &v1alpha1.DNSetList{} + if err := reader.List(ctx, sets, client.InNamespace(sts.Namespace)); err != nil { + log.FromContext(ctx).Error(err, "list DNSets for LogSet StatefulSet", "statefulset", client.ObjectKeyFromObject(sts)) + return nil + } + + requests := make([]reconcile.Request, 0, len(sets.Items)) + for i := range sets.Items { + set := &sets.Items[i] + if set.Deps.LogSet != nil && set.Deps.LogSet.Name == owner.Name { + requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(set)}) + } + } + return requests + } +} diff --git a/pkg/controllers/dnset/controller_test.go b/pkg/controllers/dnset/controller_test.go index de49d56d..1fe2c687 100644 --- a/pkg/controllers/dnset/controller_test.go +++ b/pkg/controllers/dnset/controller_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ package dnset import ( + "context" "testing" "k8s.io/apimachinery/pkg/api/resource" @@ -34,8 +35,35 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +func TestRequestsForLogSetStatefulSet(t *testing.T) { + s := newScheme() + logSet := &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "log", UID: "log-uid"}} + matching := &v1alpha1.DNSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "matching"}, + Deps: v1alpha1.DNSetDeps{LogSetRef: logSet.AsDependency()}, + } + unrelated := &v1alpha1.DNSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "unrelated"}, + Deps: v1alpha1.DNSetDeps{LogSetRef: v1alpha1.LogSetRef{LogSet: &v1alpha1.LogSet{ + ObjectMeta: metav1.ObjectMeta{Name: "other"}, + }}}, + } + cli := fake.KubeClientBuilder().WithScheme(s).WithObjects(matching, unrelated).Build() + sts := &kruisev1.StatefulSet{ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "log-log", + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(logSet, + v1alpha1.GroupVersion.WithKind("LogSet"))}, + }} + + requests := requestsForLogSetStatefulSet(cli)(context.Background(), sts) + g := NewGomegaWithT(t) + g.Expect(requests).To(Equal([]reconcile.Request{{NamespacedName: client.ObjectKeyFromObject(matching)}})) +} + func TestDNSetActor_Observe(t *testing.T) { s := newScheme() tpl := &v1alpha1.DNSet{ From f5261f15abcc99ce3dce548f152c61d77b65db37 Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 16:53:17 +0800 Subject: [PATCH 02/11] fix: harden Kruise admission and chart deployment Resolve StorageClass watch permissions without enabling PVC auto-resize, define webhook outage behavior, and make chart packaging deterministic. Refs #610 Refs #615 --- .github/workflows/release_chart.yml | 13 +- charts/kruise/templates/rbac_role.yaml | 7 +- .../templates/webhookconfiguration.yaml | 8 +- .../kruise-webhook-availability.md | 41 ++++++ hack/lib.sh | 18 ++- hack/package-chart.sh | 41 ++++++ hack/test-kruise-webhook-outage.sh | 138 ++++++++++++++++++ hack/verify-chart.sh | 87 +++++++++++ 8 files changed, 332 insertions(+), 21 deletions(-) create mode 100644 docs/troubleshooting/kruise-webhook-availability.md create mode 100755 hack/package-chart.sh create mode 100755 hack/test-kruise-webhook-outage.sh create mode 100755 hack/verify-chart.sh diff --git a/.github/workflows/release_chart.yml b/.github/workflows/release_chart.yml index 6475f4c7..693850da 100644 --- a/.github/workflows/release_chart.yml +++ b/.github/workflows/release_chart.yml @@ -18,12 +18,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 with: fetch-depth: 0 - name: Install Helm - uses: azure/setup-helm@f382f75448129b3be48f8121b9857be18d815a82 # tag=v3.4 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # tag=v5.0.1 - name: Configure Git run: | @@ -32,13 +32,10 @@ jobs: - name: Package charts run: | - mkdir -p charts/matrixone-operator/charts .cr-release-packages .cr-index + mkdir -p .cr-release-packages .cr-index helm package charts/kruise \ - --destination charts/matrixone-operator/charts - helm package charts/kruise \ - --destination .cr-release-packages - helm package charts/matrixone-operator \ --destination .cr-release-packages + ./hack/package-chart.sh .cr-release-packages - name: Run Artifact Hub lint run: | @@ -47,7 +44,7 @@ jobs: rm -f ./ah - name: Install chart-releaser - uses: helm/chart-releaser-action@98bccfd32b0f76149d188912ac8e45ddd3f8695f # tag=v1.4.1 + uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # tag=v1.7.0 with: install_only: true env: diff --git a/charts/kruise/templates/rbac_role.yaml b/charts/kruise/templates/rbac_role.yaml index 695de735..86722313 100644 --- a/charts/kruise/templates/rbac_role.yaml +++ b/charts/kruise/templates/rbac_role.yaml @@ -790,7 +790,9 @@ rules: - get - patch - update -{{- if (contains "StatefulSetAutoResizePVCGate=true" .Values.featureGates) }} +# Kruise v1.8.3 starts the StorageClass informer even when PVC auto-resize is +# disabled. Keep this read-only permission unconditional; it does not enable +# StatefulSetAutoResizePVCGate or permit PVC mutations (issue #610). - apiGroups: - storage.k8s.io resources: @@ -799,7 +801,6 @@ rules: - get - list - watch -{{- end }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding @@ -1061,4 +1062,4 @@ rules: - delete - deletecollection - patch - - update \ No newline at end of file + - update diff --git a/charts/kruise/templates/webhookconfiguration.yaml b/charts/kruise/templates/webhookconfiguration.yaml index 62e35e17..dd041685 100644 --- a/charts/kruise/templates/webhookconfiguration.yaml +++ b/charts/kruise/templates/webhookconfiguration.yaml @@ -18,7 +18,9 @@ webhooks: namespace: {{ .Values.installation.namespace }} path: /mutate-pod timeoutSeconds: {{ .Values.webhookConfiguration.timeoutSeconds }} - failurePolicy: Fail + # Pod admission is fail-open so a Kruise webhook outage cannot block Pod + # creation across the cluster. Kruise custom-resource webhooks remain Fail. + failurePolicy: Ignore name: mpod.kb.io namespaceSelector: matchExpressions: @@ -523,7 +525,7 @@ webhooks: name: kruise-webhook-service namespace: {{ .Values.installation.namespace }} path: /validate-pod - failurePolicy: Fail + failurePolicy: Ignore timeoutSeconds: {{ .Values.webhookConfiguration.timeoutSeconds }} name: vpod.kb.io namespaceSelector: @@ -555,7 +557,7 @@ webhooks: name: kruise-webhook-service namespace: {{ .Values.installation.namespace }} path: /validate-pod - failurePolicy: Fail + failurePolicy: Ignore timeoutSeconds: {{ .Values.webhookConfiguration.timeoutSeconds }} name: vpodeviction.kb.io namespaceSelector: diff --git a/docs/troubleshooting/kruise-webhook-availability.md b/docs/troubleshooting/kruise-webhook-availability.md new file mode 100644 index 00000000..026d8d54 --- /dev/null +++ b/docs/troubleshooting/kruise-webhook-availability.md @@ -0,0 +1,41 @@ +# Kruise webhook availability policy + +The bundled Kruise chart deliberately uses different failure policies based on +the scope of the admitted resource. + +## Built-in Kubernetes resources + +Webhooks for Pods, Pod eviction, Deployments, ReplicaSets, StatefulSets, +Namespaces, Services, Ingresses, and CustomResourceDefinitions use +`failurePolicy: Ignore`. + +This keeps core Kubernetes API operations available while the Kruise webhook +service is starting, upgrading, or temporarily unavailable. Features implemented +by those webhooks, such as Pod mutation and deletion protection, are not +guaranteed during the outage and resume when the webhook recovers. + +## Kruise custom resources + +Webhooks whose rules only target `apps.kruise.io` or `policy.kruise.io` resources +use `failurePolicy: Fail`. Their defaulting and validation are part of the +Kruise resource contract, and an outage therefore blocks changes only to the +affected Kruise APIs instead of blocking general Kubernetes workloads. + +## StorageClass informer + +Kruise v1.8.3 starts a read-only StorageClass informer even when +`StatefulSetAutoResizePVCGate` is disabled. The bundled ClusterRole grants +unconditional `get`, `list`, and `watch` access to StorageClasses so the informer +can run without repeated authorization errors. This permission does not enable +PVC auto-resize and grants no PVC mutation verb. + +The chart regression checks can be run with: + +```sh +make verify-chart +``` + +The Kind E2E workflow also disconnects the Kruise webhook Service temporarily. +It verifies that ordinary Pod admission remains available, Kruise custom +resources remain fail-closed, and a Helm upgrade restores the Service and its +admission path. Run that integration coverage with `make e2e-kind`. diff --git a/hack/lib.sh b/hack/lib.sh index ce51c8f2..cf957061 100644 --- a/hack/lib.sh +++ b/hack/lib.sh @@ -129,12 +129,10 @@ function e2e::run() { function e2e::install() { local chart_root + local operator_chart chart_root=$(mktemp -d) - mkdir -p "${chart_root}/matrixone-operator/charts" - cp charts/matrixone-operator/Chart.yaml charts/matrixone-operator/values.yaml "${chart_root}/matrixone-operator/" - cp -R charts/matrixone-operator/templates "${chart_root}/matrixone-operator/" - if ! helm package charts/kruise --destination "${chart_root}/matrixone-operator/charts"; then + if ! operator_chart=$(./hack/package-chart.sh "${chart_root}"); then rm -rf -- "${chart_root}" return 1 fi @@ -142,13 +140,19 @@ function e2e::install() { echo "> Create operator namespace" kubectl create ns "${OPNAMESPACE}" echo "> Install mo operator" - if ! helm install mo "${chart_root}/matrixone-operator" --set image.repository="${REPO}" --set image.tag="${TAG}" -n "${OPNAMESPACE}"; then + if ! helm install mo "${operator_chart}" --set image.repository="${REPO}" --set image.tag="${TAG}" -n "${OPNAMESPACE}"; then + rm -rf -- "${chart_root}" + return 1 + fi + if ! e2e::wait-webhook-ready; then + rm -rf -- "${chart_root}" + return 1 + fi + if ! ./hack/test-kruise-webhook-outage.sh "${operator_chart}" mo "${OPNAMESPACE}"; then rm -rf -- "${chart_root}" return 1 fi rm -rf -- "${chart_root}" - - e2e::wait-webhook-ready } function e2e::wait-webhook-ready() { diff --git a/hack/package-chart.sh b/hack/package-chart.sh new file mode 100755 index 00000000..c237fe14 --- /dev/null +++ b/hack/package-chart.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Copyright 2026 Matrix Origin +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd) +SOURCE_ROOT=${SOURCE_ROOT:-${REPO_ROOT}} +OUTPUT_DIR=${1:-"${REPO_ROOT}/charts"} +STAGING_ROOT=$(mktemp -d) +trap 'rm -rf -- "${STAGING_ROOT}"' EXIT + +OPERATOR_SOURCE="${SOURCE_ROOT}/charts/matrixone-operator" +KRUISE_SOURCE="${SOURCE_ROOT}/charts/kruise" +OPERATOR_STAGING="${STAGING_ROOT}/matrixone-operator" + +mkdir -p "${OPERATOR_STAGING}/charts" "${OUTPUT_DIR}" +cp "${OPERATOR_SOURCE}/Chart.yaml" "${OPERATOR_SOURCE}/values.yaml" "${OPERATOR_STAGING}/" +cp -R "${OPERATOR_SOURCE}/templates" "${OPERATOR_STAGING}/" +if [[ -f "${OPERATOR_SOURCE}/.helmignore" ]]; then + cp "${OPERATOR_SOURCE}/.helmignore" "${OPERATOR_STAGING}/" +fi + +# Package only the dependency declared by this repository. In particular, do +# not copy or inspect OPERATOR_SOURCE/charts, which may contain ignored archives +# left by an earlier local build. +helm package "${KRUISE_SOURCE}" --destination "${OPERATOR_STAGING}/charts" >/dev/null +helm package "${OPERATOR_STAGING}" --destination "${OUTPUT_DIR}" | awk '{print $NF}' diff --git a/hack/test-kruise-webhook-outage.sh b/hack/test-kruise-webhook-outage.sh new file mode 100755 index 00000000..27d46303 --- /dev/null +++ b/hack/test-kruise-webhook-outage.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash + +# Copyright 2026 Matrix Origin +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +if [[ $# -ne 3 ]]; then + echo "usage: $0 OPERATOR_CHART RELEASE RELEASE_NAMESPACE" >&2 + exit 2 +fi + +operator_chart=$1 +release=$2 +release_namespace=$3 +kruise_namespace=kruise-system +test_namespace=kruise-webhook-outage-test +webhook_service=kruise-webhook-service +manager_deployment=kruise-controller-manager +needs_recovery=true + +recover() { + local status=$? + kubectl delete namespace "${test_namespace}" --ignore-not-found --wait=false >/dev/null 2>&1 || true + if [[ "${needs_recovery}" == true ]]; then + echo "> Restore Kruise webhook after outage test" + helm upgrade "${release}" "${operator_chart}" -n "${release_namespace}" --reuse-values \ + --wait --timeout=5m >/dev/null 2>&1 || true + fi + exit "${status}" +} +trap recover EXIT + +echo "> Simulate Kruise webhook service outage" +# Change a chart-managed selector to remove all endpoints without stopping the +# controller. Helm upgrade must restore the declared selector value. +kubectl -n "${kruise_namespace}" patch service "${webhook_service}" --type=merge \ + -p '{"spec":{"selector":{"control-plane":"reliability-outage"}}}' >/dev/null + +for _ in $(seq 1 30); do + endpoints=$(kubectl -n "${kruise_namespace}" get endpoints "${webhook_service}" \ + -o jsonpath='{.subsets}' 2>/dev/null || true) + [[ -z "${endpoints}" ]] && break + sleep 1 +done +if [[ -n "${endpoints:-}" ]]; then + echo "Kruise webhook endpoints did not become empty" >&2 + exit 1 +fi + +kubectl create namespace "${test_namespace}" >/dev/null +kubectl -n "${test_namespace}" apply -f - >/dev/null <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: admitted-during-outage +spec: + restartPolicy: Never + containers: + - name: main + image: busybox:1.36 + command: ["sh", "-c", "exit 0"] +EOF + +set +e +failure_output=$(kubectl -n "${test_namespace}" apply -f - 2>&1 <<'EOF' +apiVersion: apps.kruise.io/v1alpha1 +kind: CloneSet +metadata: + name: rejected-during-outage +spec: + replicas: 0 + selector: + matchLabels: + app: rejected-during-outage + template: + metadata: + labels: + app: rejected-during-outage + spec: + containers: + - name: main + image: busybox:1.36 +EOF +) +failure_status=$? +set -e +if [[ ${failure_status} -eq 0 ]]; then + echo "Kruise custom resource unexpectedly passed admission during webhook outage" >&2 + exit 1 +fi +if ! grep -Eq 'failed calling webhook|no endpoints available|context deadline exceeded' <<<"${failure_output}"; then + echo "Kruise custom resource failed for an unexpected reason: ${failure_output}" >&2 + exit 1 +fi + +echo "> Upgrade release and recover Kruise webhook service" +helm upgrade "${release}" "${operator_chart}" -n "${release_namespace}" --reuse-values \ + --wait --timeout=5m >/dev/null +kubectl -n "${kruise_namespace}" rollout status deployment "${manager_deployment}" --timeout=5m >/dev/null + +for _ in $(seq 1 60); do + endpoints=$(kubectl -n "${kruise_namespace}" get endpoints "${webhook_service}" \ + -o jsonpath='{.subsets}' 2>/dev/null || true) + [[ -n "${endpoints}" ]] && break + sleep 1 +done +if [[ -z "${endpoints:-}" ]]; then + echo "Kruise webhook endpoints did not recover after upgrade" >&2 + exit 1 +fi +needs_recovery=false + +kubectl -n "${test_namespace}" apply -f - >/dev/null <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: admitted-after-recovery +spec: + restartPolicy: Never + containers: + - name: main + image: busybox:1.36 + command: ["sh", "-c", "exit 0"] +EOF + +echo "> Kruise webhook outage and recovery behavior verified" diff --git a/hack/verify-chart.sh b/hack/verify-chart.sh new file mode 100755 index 00000000..a321aef4 --- /dev/null +++ b/hack/verify-chart.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +# Copyright 2026 Matrix Origin +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd) +TEST_ROOT=$(mktemp -d) +trap 'rm -rf -- "${TEST_ROOT}"' EXIT + +bash -n "${REPO_ROOT}/hack/package-chart.sh" \ + "${REPO_ROOT}/hack/test-kruise-webhook-outage.sh" \ + "${REPO_ROOT}/hack/lib.sh" + +helm template test "${REPO_ROOT}/charts/kruise" >"${TEST_ROOT}/kruise.yaml" + +# Built-in API operations must remain available during a webhook outage. Kruise +# custom resources remain fail-closed because their webhooks own their contract. +awk ' + /^[[:space:]]+failurePolicy:/ { policy = $2 } + /^[[:space:]]+name: [a-z].*\.kb\.io$/ { + name = $2 + expected = "Fail" + if (name == "mpod.kb.io" || name == "vpod.kb.io" || name == "vpodeviction.kb.io" || + name ~ /^vbuiltin/ || name == "vcustomresourcedefinition.kb.io" || + name == "vnamespace.kb.io" || name == "vingress.kb.io" || name == "vservice.kb.io") { + expected = "Ignore" + } + if (policy != expected) { + printf "webhook %s has failurePolicy %s, want %s\n", name, policy, expected > "/dev/stderr" + failed = 1 + } + } + END { exit failed } +' "${TEST_ROOT}/kruise.yaml" + +if grep -q 'StatefulSetAutoResizePVCGate=true' "${TEST_ROOT}/kruise.yaml"; then + echo "StatefulSetAutoResizePVCGate must remain disabled by default" >&2 + exit 1 +fi + +if ! awk ' + /- storageclasses$/ { in_rule = 1; next } + in_rule && /- get$/ { get = 1 } + in_rule && /- list$/ { list = 1 } + in_rule && /- watch$/ { watch = 1 } + in_rule && /^---$/ { exit !(get && list && watch) } + END { if (in_rule) exit !(get && list && watch) } +' "${TEST_ROOT}/kruise.yaml"; then + echo "Kruise must have read-only get/list/watch access to StorageClasses" >&2 + exit 1 +fi + +# Reproduce a dirty developer workspace in a temporary source tree. The stale +# archive must not be copied into the operator package. +mkdir -p "${TEST_ROOT}/source/charts" "${TEST_ROOT}/packages" +cp -R "${REPO_ROOT}/charts/matrixone-operator" "${TEST_ROOT}/source/charts/" +cp -R "${REPO_ROOT}/charts/kruise" "${TEST_ROOT}/source/charts/" +mkdir -p "${TEST_ROOT}/source/charts/matrixone-operator/charts" +printf 'stale archive\n' >"${TEST_ROOT}/source/charts/matrixone-operator/charts/stale-9.9.9.tgz" + +SOURCE_ROOT="${TEST_ROOT}/source" "${REPO_ROOT}/hack/package-chart.sh" "${TEST_ROOT}/packages" >/dev/null +operator_package=$(find "${TEST_ROOT}/packages" -maxdepth 1 -name 'matrixone-operator-*.tgz' -print -quit) +if [[ -z "${operator_package}" ]]; then + echo "operator package was not created" >&2 + exit 1 +fi + +dependencies=$(tar -tzf "${operator_package}" | awk -F/ '$1 == "matrixone-operator" && $2 == "charts" && $3 != "" { print $3 }' | sort -u) +if [[ "${dependencies}" != "kruise" ]]; then + echo "unexpected packaged dependencies:" >&2 + printf '%s\n' "${dependencies}" >&2 + exit 1 +fi From cec4ccc909a833640c7dfaad09bd9141092f98d0 Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 16:53:36 +0800 Subject: [PATCH 03/11] ci: pin and align verification toolchains Use explicit supported Action revisions, pin setup-envtest and golangci-lint, and document the local checks mirrored by CI. Refs #603 Refs #615 --- .github/actions/checks/action.yml | 18 +---- .github/actions/dev_env/action.yml | 4 +- .github/actions/e2e/action.yml | 6 +- .github/env | 2 +- .github/workflows/compatibility.yml | 4 +- .github/workflows/merge.yml | 6 +- .github/workflows/release_image.yml | 18 ++--- .github/workflows/test_workflows.yml | 6 +- .gitignore | 10 ++- .golangci.yml | 79 +++++++++++--------- Makefile | 34 ++++++--- api/Makefile | 11 ++- docs/dev/dev_guide.md | 14 ++++ pkg/controllers/cnpool/controller.go | 6 +- pkg/controllers/cnstore/controller.go | 4 +- pkg/controllers/cnstore/pooling.go | 4 +- pkg/controllers/logset/controller_test.go | 4 +- pkg/controllers/logset/sts_test.go | 6 +- pkg/controllers/mocluster/controller_test.go | 14 ++-- pkg/webhook/common.go | 4 +- 20 files changed, 143 insertions(+), 111 deletions(-) diff --git a/.github/actions/checks/action.yml b/.github/actions/checks/action.yml index 03d88694..d6db2897 100644 --- a/.github/actions/checks/action.yml +++ b/.github/actions/checks/action.yml @@ -13,31 +13,19 @@ runs: uses: ./.github/actions/dev_env - name: check_license_header - uses: apache/skywalking-eyes/header@v0.6.0 + uses: apache/skywalking-eyes/header@a196742f472feaffafea537ce5a2a4c3c53a8de4 # tag=v0.9.0 env: GITHUB_TOKEN: ${{ inputs.github_token }} with: log: info config: .licenserc.yml - - name: setup go version - uses: actions/setup-go@v3 - with: - check-latest: true - go-version: - ${{ env.golang-version }} - - name: verify shell: bash run: make verify - # disable action cache due to https://github.com/golangci/golangci-lint-action/issues/244 - # upgrade due to https://github.com/golangci/golangci-lint/issues/2374 - # skip cache due to https://github.com/golangci/golangci-lint-action/issues/23 - name: golangci_lint - uses: golangci/golangci-lint-action@v3.1.0 + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # tag=v9.3.0 with: - version: v1.50.0 + version: v2.1.6 args: --timeout 10m0s - skip-pkg-cache: true - skip-build-cache: true diff --git a/.github/actions/dev_env/action.yml b/.github/actions/dev_env/action.yml index 5405cd95..5a11de44 100644 --- a/.github/actions/dev_env/action.yml +++ b/.github/actions/dev_env/action.yml @@ -10,8 +10,8 @@ runs: run: cat ".github/env" >> $GITHUB_ENV - name: setup go version - uses: actions/setup-go@v3 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # tag=v7.0.0 with: - check-latest: true + check-latest: false go-version: ${{ env.golang-version }} diff --git a/.github/actions/e2e/action.yml b/.github/actions/e2e/action.yml index a55f96bc..2e3a7cf1 100644 --- a/.github/actions/e2e/action.yml +++ b/.github/actions/e2e/action.yml @@ -13,14 +13,14 @@ runs: uses: ./.github/actions/dev_env - name: setup helm - uses: azure/setup-helm@v1 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # tag=v5.0.1 with: version: '${{ env.helm-version }}' - name: setup kind - uses: engineerd/setup-kind@v0.5.0 + uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # tag=v1.14.0 with: - skipClusterCreation: true + install_only: true version: v0.15.0 - name: Run kind test diff --git a/.github/env b/.github/env index 63ed3569..66fd36b7 100644 --- a/.github/env +++ b/.github/env @@ -1,4 +1,4 @@ -golang-version=1.22 +golang-version=1.23.1 kind-version=v0.11.1 kind-image=kindest/node:v1.23.0 helm-version=v3.8.1 diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index abf8c64a..99a341bb 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -10,7 +10,7 @@ on: default: '["0.8.0", "1.0.0-rc1"]' concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event-name }} + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true jobs: @@ -21,7 +21,7 @@ jobs: matrix: moVersion: ${{ fromJSON(github.event.inputs.moVersions)}} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: run uses: ./.github/actions/e2e with: diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index c4e0a193..0522ab51 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -8,10 +8,10 @@ jobs: add_labels: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-ecosystem/action-add-labels@v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 + - uses: actions-ecosystem/action-add-labels@18f1af5e3544586314bbe15c0273249c770b2daf # tag=v1.1.3 if: ${{ startsWith(github.event.comment.body, '/merge') }} with: github_token: ${{ secrets.GITHUB_TOKEN }} labels: | - can-merge \ No newline at end of file + can-merge diff --git a/.github/workflows/release_image.yml b/.github/workflows/release_image.yml index a757d986..ec6da0ab 100644 --- a/.github/workflows/release_image.yml +++ b/.github/workflows/release_image.yml @@ -14,12 +14,12 @@ jobs: name: push_image steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 with: fetch-depth: 0 - name: Docker meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # tag=v6.2.0 id: meta with: images: | @@ -31,42 +31,42 @@ jobs: type=semver,pattern={{version}} - name: Set up QEMU - uses: docker/setup-qemu-action@master + uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # tag=v4.3.0 with: image: tonistiigi/binfmt:latest platforms: linux/amd64,linux/arm64 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # tag=v4.3.0 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # tag=v4.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to Alicloud Container Registry id: login_alicr - uses: docker/login-action@v2 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # tag=v4.6.0 with: registry: registry.cn-hangzhou.aliyuncs.com username: ${{ secrets.ACR_USERNAME }} password: ${{ secrets.ACR_TOKEN }} - name: Go Build Cache for Docker - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # tag=v4.3.0 with: path: go-build-cache key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }} - name: inject go-build-cache into docker - uses: reproducible-containers/buildkit-cache-dance@v3.1.0 + uses: reproducible-containers/buildkit-cache-dance@5b81f4d29dc8397a7d341dba3aeecc7ec54d6361 # tag=v3.3.0 with: cache-source: go-build-cache - name: Build - uses: docker/build-push-action@v2 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # tag=v7.3.0 env: # Alicloud ACR rejects the provenance attestation manifest that buildx # attaches by default since BuildKit v0.11 ("unknown manifest class for diff --git a/.github/workflows/test_workflows.yml b/.github/workflows/test_workflows.yml index d6244cbe..57101c1c 100644 --- a/.github/workflows/test_workflows.yml +++ b/.github/workflows/test_workflows.yml @@ -6,7 +6,7 @@ on: - main concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event-name }} + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true jobs: @@ -14,7 +14,7 @@ jobs: name: checks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - uses: ./.github/actions/checks with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -23,7 +23,7 @@ jobs: name: e2e runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: run uses: ./.github/actions/e2e with: diff --git a/.gitignore b/.gitignore index 50e2ac5c..5da41b17 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,11 @@ charts/matrixone-operator/charts/ *.tgz e2e*.xml e2e.test -docs/troubleshooting/ -docs/draft \ No newline at end of file +# Operational troubleshooting documents are versioned; ignore only drafts and +# generated local test data/tools. +docs/troubleshooting/**/*.draft.md +docs/troubleshooting/**/.tools/ +docs/troubleshooting/**/.work/ +docs/troubleshooting/**/artifacts/ +docs/troubleshooting/**/__pycache__/ +docs/draft/ diff --git a/.golangci.yml b/.golangci.yml index 93bcf414..772c08ac 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,42 +1,47 @@ +version: "2" run: - timeout: 30m - go: '1.21' - skip-files: - - "^zz_generated.*" - -output: - sort-results: true - + go: "1.23" linters: - disable-all: true + default: none enable: - revive - -issues: - exclude-rules: - - path: _test\.go - linters: - - errcheck - -linters-settings: - staticcheck: - go: "1.19" - checks: [ - "all", - "-S1*", - "-ST1*", - "-SA5011", - "-SA1019", - "-SA2002" - ] - - revive: + settings: + revive: + rules: + - name: unused-parameter + arguments: + - allowRegex: ^_ + severity: warning + disabled: false + staticcheck: + checks: + - all + - -S1* + - -SA1019 + - -SA2002 + - -SA5011 + - -ST1* + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling rules: - - name: unused-parameter - severity: warning - disabled: false - arguments: - - allowRegex: "^_" - - unused: - go: "1.19" + - linters: + - errcheck + path: _test\.go + paths: + - ^zz_generated.* + - third_party$ + - builtin$ + - examples$ +formatters: + exclusions: + generated: lax + paths: + - ^zz_generated.* + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index 90f5c1c5..5bff5cb2 100644 --- a/Makefile +++ b/Makefile @@ -55,9 +55,12 @@ generate-mockgen: mockgen ## General gomock(https://github.com/golang/mock) file $(MOCKGEN) -source=./runtime/pkg/reconciler/event.go -package fake > ./runtime/pkg/fake/event.go # helm package -helm-pkg: manifests generate helm-lint - helm dependency build charts/matrixone-operator - helm package -u charts/matrixone-operator -d charts/ +helm-pkg: manifests generate verify-chart + ./hack/package-chart.sh charts/ + +.PHONY: verify-chart +verify-chart: + ./hack/verify-chart.sh # Generated artifacts that must be committed whenever their sources or generators change. @@ -82,13 +85,13 @@ verify-generated: fi # Make sure the generated files are up to date before open PR -reviewable: ci-reviewable go-lint check-license test +reviewable: ci-reviewable verify-generated verify-chart go-lint check-license ci-reviewable: generate manifests docs test go mod tidy # Check whether the pull request is reviewable in CI, go-lint is delibrately excluded since we already have golangci-lint action -verify: ci-reviewable +verify: ci-reviewable verify-generated verify-chart echo "checking that branch is clean" test -z "$$(git status --porcelain)" || (echo "unclean working tree, did you forget to run make reviewable?" && exit 1) echo "branch is clean" @@ -118,11 +121,16 @@ $(LOCALBIN): mkdir -p $(LOCALBIN) ENVTEST ?= $(LOCALBIN)/setup-envtest +SETUP_ENVTEST_MODULE = sigs.k8s.io/controller-runtime/tools/setup-envtest +SETUP_ENVTEST_VERSION = v0.0.0-20230503192624-935faeba7003 .PHONY: envtest -envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. -$(ENVTEST): $(LOCALBIN) - test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest +envtest: $(LOCALBIN) ## Install the pinned setup-envtest version if necessary. + @actual_version="$$(go version -m "$(ENVTEST)" 2>/dev/null | awk -v module="$(SETUP_ENVTEST_MODULE)" '$$1 == "mod" && $$2 == module { print $$3 }')"; \ + if [ "$$actual_version" != "$(SETUP_ENVTEST_VERSION)" ]; then \ + echo "Installing $(SETUP_ENVTEST_MODULE)@$(SETUP_ENVTEST_VERSION) (found: $${actual_version:-none})"; \ + GOBIN=$(LOCALBIN) go install $(SETUP_ENVTEST_MODULE)@$(SETUP_ENVTEST_VERSION); \ + fi # TODO: include E2E test: api-test unit @@ -189,8 +197,14 @@ license-eye: ## Download license-eye locally if necessary $(call go-get-tool,$(LICENSE_EYE),github.com/apache/skywalking-eyes/cmd/license-eye@v0.4.0) GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint -golangci-lint: - $(call go-get-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2) +GOLANGCI_LINT_MODULE = github.com/golangci/golangci-lint/v2 +GOLANGCI_LINT_VERSION = v2.1.6 +golangci-lint: $(LOCALBIN) + @actual_version="$$(go version -m "$(GOLANGCI_LINT)" 2>/dev/null | awk -v module="$(GOLANGCI_LINT_MODULE)" '$$1 == "mod" && $$2 == module { print $$3 }')"; \ + if [ "$$actual_version" != "$(GOLANGCI_LINT_VERSION)" ]; then \ + echo "Installing $(GOLANGCI_LINT_MODULE)/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) (found: $${actual_version:-none})"; \ + GOBIN=$(PROJECT_DIR)/bin go install $(GOLANGCI_LINT_MODULE)/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION); \ + fi # go-get-tool will 'go get' any package $2 and install it to $1. PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) diff --git a/api/Makefile b/api/Makefile index dcbd18d2..362a2f73 100644 --- a/api/Makefile +++ b/api/Makefile @@ -28,11 +28,16 @@ $(LOCALBIN): mkdir -p $(LOCALBIN) ENVTEST ?= $(LOCALBIN)/setup-envtest +SETUP_ENVTEST_MODULE = sigs.k8s.io/controller-runtime/tools/setup-envtest +SETUP_ENVTEST_VERSION = v0.0.0-20230503192624-935faeba7003 .PHONY: envtest -envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. -$(ENVTEST): $(LOCALBIN) - test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest +envtest: $(LOCALBIN) ## Install the pinned setup-envtest version if necessary. + @actual_version="$$(go version -m "$(ENVTEST)" 2>/dev/null | awk -v module="$(SETUP_ENVTEST_MODULE)" '$$1 == "mod" && $$2 == module { print $$3 }')"; \ + if [ "$$actual_version" != "$(SETUP_ENVTEST_VERSION)" ]; then \ + echo "Installing $(SETUP_ENVTEST_MODULE)@$(SETUP_ENVTEST_VERSION) (found: $${actual_version:-none})"; \ + GOBIN=$(LOCALBIN) go install $(SETUP_ENVTEST_MODULE)@$(SETUP_ENVTEST_VERSION); \ + fi CONTROLLER_GEN = $(LOCALBIN)/controller-gen CONTROLLER_GEN_MODULE = sigs.k8s.io/controller-tools diff --git a/docs/dev/dev_guide.md b/docs/dev/dev_guide.md index a88063dc..72d87b19 100644 --- a/docs/dev/dev_guide.md +++ b/docs/dev/dev_guide.md @@ -216,3 +216,17 @@ import ( ## Package management You can use [go workspace](https://golang.google.cn/doc/tutorial/workspaces) for using new dependency. + +## Verify a change + +Run the same generated-artifact, unit, Chart, and lint checks used by CI before +opening a pull request: + +```shell +make reviewable +``` + +CI runs `make verify` from a clean checkout and rejects any generated files or +module metadata that the pinned tools would change. Use `make verify-chart` for +the faster, Chart-only policy and deterministic-packaging checks. The Kind E2E +workflow additionally covers Kruise webhook outage, Helm upgrade, and recovery. diff --git a/pkg/controllers/cnpool/controller.go b/pkg/controllers/cnpool/controller.go index 62830dfb..49cc6bdb 100644 --- a/pkg/controllers/cnpool/controller.go +++ b/pkg/controllers/cnpool/controller.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -460,7 +460,7 @@ func deletionCost(pod *corev1.Pod) int { func (r *Actor) Start(mgr manager.Manager) error { return recon.Setup[*v1alpha1.CNPool](&v1alpha1.CNPool{}, "cn-pool-manager", mgr, r, recon.WithBuildFn(func(b *builder.Builder) { - b.Watches(&corev1.Pod{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + b.Watches(&corev1.Pod{}, handler.EnqueueRequestsFromMapFunc(func(_ context.Context, object client.Object) []reconcile.Request { pod, ok := object.(*corev1.Pod) if !ok { return nil @@ -476,7 +476,7 @@ func (r *Actor) Start(mgr manager.Manager) error { }, }} })) - b.Watches(&v1alpha1.CNClaim{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + b.Watches(&v1alpha1.CNClaim{}, handler.EnqueueRequestsFromMapFunc(func(_ context.Context, object client.Object) []reconcile.Request { claim, ok := object.(*v1alpha1.CNClaim) if !ok { return nil diff --git a/pkg/controllers/cnstore/controller.go b/pkg/controllers/cnstore/controller.go index 9cea25d4..8a46461f 100644 --- a/pkg/controllers/cnstore/controller.go +++ b/pkg/controllers/cnstore/controller.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -506,7 +506,7 @@ func (c *withCNSet) syncStats(ctx *recon.Context[*corev1.Pod]) error { uid := v1alpha1.GetCNPodUUID(pod) moVersion := common.GetSemanticVersion(&pod.ObjectMeta) var queryAddress string - if err := c.withMOClientSet(ctx, func(ctx context.Context, handler *mocli.ClientSet) error { + if err := c.withMOClientSet(ctx, func(_ context.Context, handler *mocli.ClientSet) error { cn, ok := handler.StoreCache.GetCN(uid) if !ok { return gerrors.Errorf("CN with uuid %s not found", uid) diff --git a/pkg/controllers/cnstore/pooling.go b/pkg/controllers/cnstore/pooling.go index 8758d1ac..e9e770d6 100644 --- a/pkg/controllers/cnstore/pooling.go +++ b/pkg/controllers/cnstore/pooling.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ func (c *withCNSet) poolingCNReconcile(ctx *recon.Context[*corev1.Pod]) error { uid := v1alpha1.GetCNPodUUID(pod) var ready bool - if err := c.withMOClientSet(ctx, func(ctx context.Context, h *mocli.ClientSet) error { + if err := c.withMOClientSet(ctx, func(_ context.Context, h *mocli.ClientSet) error { _, ready = h.StoreCache.GetCN(uid) return nil }); err != nil { diff --git a/pkg/controllers/logset/controller_test.go b/pkg/controllers/logset/controller_test.go index 4d586143..5a5c35e1 100644 --- a/pkg/controllers/logset/controller_test.go +++ b/pkg/controllers/logset/controller_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -117,7 +117,7 @@ func TestLogSetActor_Observe(t *testing.T) { name: "scale out", logset: tpl, client: &fake.Client{ - MockPatch: func(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOption) error { + MockPatch: func(_ context.Context, _ runtime.Object, _ client.Patch, _ ...client.PatchOption) error { return nil }, Client: fake.KubeClientBuilder().WithScheme(s).WithObjects( diff --git a/pkg/controllers/logset/sts_test.go b/pkg/controllers/logset/sts_test.go index affde553..01ee3112 100644 --- a/pkg/controllers/logset/sts_test.go +++ b/pkg/controllers/logset/sts_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -171,7 +171,7 @@ func Test_syncPodSpec(t *testing.T) { }, }} for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(_ *testing.T) { podSpec := tt.args.spec.DeepCopy() syncPodSpec(tt.args.ls, podSpec) if diff := cmp.Diff(podSpec, tt.want); diff != "" { @@ -294,7 +294,7 @@ func Test_syncPersistentVolumeClaim(t *testing.T) { // TODO: Add test cases. } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(_ *testing.T) { syncPersistentVolumeClaim(tt.args.ls, tt.args.sts) }) } diff --git a/pkg/controllers/mocluster/controller_test.go b/pkg/controllers/mocluster/controller_test.go index 7b32f46f..3217ab6d 100644 --- a/pkg/controllers/mocluster/controller_test.go +++ b/pkg/controllers/mocluster/controller_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -126,7 +126,7 @@ func TestMatrixOneClusterActor_Observe(t *testing.T) { }, }, }, - expect: func(g *WithT, mo *v1alpha1.MatrixOneCluster, err error, c client.Client) { + expect: func(g *WithT, mo *v1alpha1.MatrixOneCluster, err error, _ client.Client) { g.Expect(recon.IsReady(&mo.Status)).To(BeTrue()) g.Expect(err).To(Succeed()) }, @@ -162,7 +162,7 @@ func TestMatrixOneClusterActor_Observe(t *testing.T) { }, }, }, - expect: func(g *WithT, mo *v1alpha1.MatrixOneCluster, err error, c client.Client) { + expect: func(g *WithT, mo *v1alpha1.MatrixOneCluster, _ error, _ client.Client) { g.Expect(recon.IsSynced(&mo.Status)).To(BeFalse()) cond, ok := recon.GetCondition(&mo.Status, recon.ConditionTypeReady) g.Expect(ok).To(BeTrue()) @@ -200,7 +200,7 @@ func TestMatrixOneClusterActor_Observe(t *testing.T) { }, }, }, - expect: func(g *WithT, mo *v1alpha1.MatrixOneCluster, err error, c client.Client) { + expect: func(g *WithT, mo *v1alpha1.MatrixOneCluster, _ error, _ client.Client) { g.Expect(recon.IsSynced(&mo.Status)).To(BeFalse()) cond, ok := recon.GetCondition(&mo.Status, recon.ConditionTypeSynced) g.Expect(ok).To(BeTrue()) @@ -219,7 +219,7 @@ func TestMatrixOneClusterActor_Observe(t *testing.T) { return m }(), objects: nil, - expect: func(g *WithT, _ *v1alpha1.MatrixOneCluster, err error, c client.Client) { + expect: func(g *WithT, _ *v1alpha1.MatrixOneCluster, _ error, c client.Client) { dn := &v1alpha1.DNSet{} g.Expect(c.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, dn)).To(Succeed()) g.Expect(dn.Spec.NodeSelector).To(Equal(map[string]string{"global-label": "global-value"})) @@ -239,7 +239,7 @@ func TestMatrixOneClusterActor_Observe(t *testing.T) { return m }(), objects: nil, - expect: func(g *WithT, _ *v1alpha1.MatrixOneCluster, err error, c client.Client) { + expect: func(g *WithT, _ *v1alpha1.MatrixOneCluster, _ error, c client.Client) { dn := &v1alpha1.DNSet{} g.Expect(c.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, dn)).To(Succeed()) g.Expect(*dn.Spec.Overlay.ImagePullPolicy).To(Equal(corev1.PullIfNotPresent)) @@ -259,7 +259,7 @@ func TestMatrixOneClusterActor_Observe(t *testing.T) { return m }(), objects: nil, - expect: func(g *WithT, _ *v1alpha1.MatrixOneCluster, err error, c client.Client) { + expect: func(g *WithT, _ *v1alpha1.MatrixOneCluster, _ error, c client.Client) { ls := &v1alpha1.LogSet{} g.Expect(c.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, ls)).To(Succeed()) g.Expect(*ls.Spec.PVCRetentionPolicy).To(Equal(v1alpha1.PVCRetentionPolicyRetain)) diff --git a/pkg/webhook/common.go b/pkg/webhook/common.go index 05740c2e..a02c3826 100644 --- a/pkg/webhook/common.go +++ b/pkg/webhook/common.go @@ -1,4 +1,4 @@ -// Copyright 2025 Matrix Origin +// Copyright 2025-2026 Matrix Origin // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -217,7 +217,7 @@ func validateMainContainerOverlay(overlay *v1alpha1.MainContainerOverlay, path * // validate VolumeMounts errs = append(errs, validateSliceWithConvert(overlay.VolumeMounts, path.Child("volumeMounts"), apiscorev1.Convert_v1_VolumeMount_To_core_VolumeMount, - func(mounts []core.VolumeMount, subPath *field.Path) field.ErrorList { + func(_ []core.VolumeMount, _ *field.Path) field.ErrorList { // TODO: complete params with Container //return corevalidation.ValidateVolumeMounts(mounts, nil, nil, nil, path.Child("volumeMounts")) return nil From a4575c4475c81aa24108ed4b560e0ae96f127e8f Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 17:01:45 +0800 Subject: [PATCH 04/11] ci: resolve pinned Go version before setup Expose the version file value as a composite-step output so setup-go does not evaluate an unset dynamic environment expression. Refs #615 --- .github/actions/dev_env/action.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/actions/dev_env/action.yml b/.github/actions/dev_env/action.yml index 5a11de44..19c627a9 100644 --- a/.github/actions/dev_env/action.yml +++ b/.github/actions/dev_env/action.yml @@ -6,12 +6,14 @@ runs: steps: - name: import env variables + id: versions shell: bash - run: cat ".github/env" >> $GITHUB_ENV + run: | + cat ".github/env" >> "$GITHUB_ENV" + sed -n 's/^golang-version=/go_version=/p' ".github/env" >> "$GITHUB_OUTPUT" - name: setup go version uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # tag=v7.0.0 with: check-latest: false - go-version: - ${{ env.golang-version }} + go-version: ${{ steps.versions.outputs.go_version }} From f98e5d94b478eacc86eb33803d7b19d8b6d9b2ef Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 17:05:42 +0800 Subject: [PATCH 05/11] ci: isolate license action toolchain side effect Run the header action after repository verification because the action installs Go 1.25 internally and otherwise replaces the pinned Go 1.23.1 toolchain. Refs #615 --- .github/actions/checks/action.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/actions/checks/action.yml b/.github/actions/checks/action.yml index d6db2897..d392ec65 100644 --- a/.github/actions/checks/action.yml +++ b/.github/actions/checks/action.yml @@ -12,14 +12,6 @@ runs: - name: pre_env uses: ./.github/actions/dev_env - - name: check_license_header - uses: apache/skywalking-eyes/header@a196742f472feaffafea537ce5a2a4c3c53a8de4 # tag=v0.9.0 - env: - GITHUB_TOKEN: ${{ inputs.github_token }} - with: - log: info - config: .licenserc.yml - - name: verify shell: bash run: make verify @@ -29,3 +21,13 @@ runs: with: version: v2.1.6 args: --timeout 10m0s + + # The header action installs its own Go toolchain while building license-eye, + # so keep it last to avoid changing the toolchain used by repository checks. + - name: check_license_header + uses: apache/skywalking-eyes/header@a196742f472feaffafea537ce5a2a4c3c53a8de4 # tag=v0.9.0 + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + with: + log: info + config: .licenserc.yml From f7233f43f597199a89dca2119408c00605df70e6 Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 17:15:05 +0800 Subject: [PATCH 06/11] ci: use supported envtest release index Pin setup-envtest to a Go 1.23 compatible revision that reads the current controller-tools release index, and fail before tests when assets cannot be resolved. Refs #603 Refs #615 --- Makefile | 5 +++-- api/Makefile | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 5bff5cb2..06f735e9 100644 --- a/Makefile +++ b/Makefile @@ -122,7 +122,7 @@ $(LOCALBIN): ENVTEST ?= $(LOCALBIN)/setup-envtest SETUP_ENVTEST_MODULE = sigs.k8s.io/controller-runtime/tools/setup-envtest -SETUP_ENVTEST_VERSION = v0.0.0-20230503192624-935faeba7003 +SETUP_ENVTEST_VERSION = v0.0.0-20250517180713-32e5e9e948a5 .PHONY: envtest envtest: $(LOCALBIN) ## Install the pinned setup-envtest version if necessary. @@ -137,7 +137,8 @@ test: api-test unit # Run unit tests unit: generate fmt vet manifests envtest - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" CGO_ENABLED=0 go test ./pkg/... -coverprofile cover.out + @assets="$$( $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" && \ + KUBEBUILDER_ASSETS="$$assets" CGO_ENABLED=0 go test ./pkg/... -coverprofile cover.out api-test: cd api && make test diff --git a/api/Makefile b/api/Makefile index 362a2f73..3834fd6b 100644 --- a/api/Makefile +++ b/api/Makefile @@ -15,7 +15,8 @@ generate: controller-gen .PHONY: test test: manifests generate envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out + @assets="$$( $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" && \ + KUBEBUILDER_ASSETS="$$assets" go test ./... -coverprofile cover.out .PHONY: docs docs: crd-ref-docs @@ -29,7 +30,7 @@ $(LOCALBIN): ENVTEST ?= $(LOCALBIN)/setup-envtest SETUP_ENVTEST_MODULE = sigs.k8s.io/controller-runtime/tools/setup-envtest -SETUP_ENVTEST_VERSION = v0.0.0-20230503192624-935faeba7003 +SETUP_ENVTEST_VERSION = v0.0.0-20250517180713-32e5e9e948a5 .PHONY: envtest envtest: $(LOCALBIN) ## Install the pinned setup-envtest version if necessary. From 6652f9a0218db7b57e0f869701636abe1f2fcf00 Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 18:27:22 +0800 Subject: [PATCH 07/11] test: isolate reliability scenarios from reconciliation --- Makefile | 10 +++++++-- hack/lib.sh | 21 ++++++++++++++++++- pkg/controllers/cnset/controller.go | 2 +- .../common/statefulset_predicate.go | 13 +++++++----- .../common/statefulset_predicate_test.go | 11 ++++++++-- pkg/controllers/dnset/controller.go | 2 +- test/e2e/matrixonecluster_test.go | 6 +++++- 7 files changed, 52 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 06f735e9..d00a27a9 100644 --- a/Makefile +++ b/Makefile @@ -186,8 +186,14 @@ run-pgd: build-pgd docker run --privileged --name playground -p 6001:6001 --rm -it matrixorigin/operator-playground:latest GINKGO = $(shell pwd)/bin/ginkgo -ginkgo: - $(call go-get-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo@v2.9.2) +GINKGO_MODULE = github.com/onsi/ginkgo/v2 +GINKGO_VERSION = v2.9.5 +ginkgo: $(LOCALBIN) + @actual_version="$$(go version -m "$(GINKGO)" 2>/dev/null | awk -v module="$(GINKGO_MODULE)" '$$1 == "mod" && $$2 == module { print $$3 }')"; \ + if [ "$$actual_version" != "$(GINKGO_VERSION)" ]; then \ + echo "Installing $(GINKGO_MODULE)/ginkgo@$(GINKGO_VERSION) (found: $${actual_version:-none})"; \ + GOBIN=$(PROJECT_DIR)/bin go install $(GINKGO_MODULE)/ginkgo@$(GINKGO_VERSION); \ + fi MOCKGEN = $(shell pwd)/bin/mockgen mockgen: ## Download mockgen locally if necessary diff --git a/hack/lib.sh b/hack/lib.sh index cf957061..4f613b64 100644 --- a/hack/lib.sh +++ b/hack/lib.sh @@ -148,11 +148,22 @@ function e2e::install() { rm -rf -- "${chart_root}" return 1 fi - if ! ./hack/test-kruise-webhook-outage.sh "${operator_chart}" mo "${OPNAMESPACE}"; then + rm -rf -- "${chart_root}" +} + +function e2e::test-kruise-webhook-outage() { + local chart_root + local operator_chart + local status=0 + chart_root=$(mktemp -d) + + if ! operator_chart=$(./hack/package-chart.sh "${chart_root}"); then rm -rf -- "${chart_root}" return 1 fi + ./hack/test-kruise-webhook-outage.sh "${operator_chart}" mo "${OPNAMESPACE}" || status=$? rm -rf -- "${chart_root}" + return "${status}" } function e2e::wait-webhook-ready() { @@ -219,7 +230,15 @@ function e2e::workflow() { trap "e2e::cleanup" EXIT e2e::install || return 1 local run_status=0 + local outage_status=0 e2e::run || run_status=$? + # Run the disruptive outage/upgrade scenario after the established E2E suite + # so it cannot change the suite's initial cluster state. Preserve an earlier + # suite failure while still collecting the outage-test result. + e2e::test-kruise-webhook-outage || outage_status=$? + if [[ "${run_status}" -eq 0 && "${outage_status}" -ne 0 ]]; then + run_status=${outage_status} + fi trap - EXIT e2e::cleanup || return 1 return "${run_status}" diff --git a/pkg/controllers/cnset/controller.go b/pkg/controllers/cnset/controller.go index 25bfcbfe..9830ff69 100644 --- a/pkg/controllers/cnset/controller.go +++ b/pkg/controllers/cnset/controller.go @@ -352,7 +352,7 @@ func (c *Actor) Reconcile(mgr manager.Manager) error { b.Owns(&kruisev1alpha1.CloneSet{}). Owns(&corev1.Service{}). Watches(&kruise.StatefulSet{}, handler.EnqueueRequestsFromMapFunc(requestsForLogSetStatefulSet(mgr.GetClient())), - builder.WithPredicates(common.LogSetStatefulSetChangedPredicate())) + builder.WithPredicates(common.LogSetReserveOrdinalsChangedPredicate())) })) if err != nil { return err diff --git a/pkg/controllers/common/statefulset_predicate.go b/pkg/controllers/common/statefulset_predicate.go index 071e1111..454d5aa4 100644 --- a/pkg/controllers/common/statefulset_predicate.go +++ b/pkg/controllers/common/statefulset_predicate.go @@ -21,12 +21,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" ) -// LogSetStatefulSetChangedPredicate selects events that can change the LogSet -// service-addresses consumed by CNSet and DNSet configuration. -func LogSetStatefulSetChangedPredicate() predicate.Predicate { +// LogSetReserveOrdinalsChangedPredicate selects updates that change the LogSet +// service-addresses consumed by CNSet and DNSet configuration. Create and delete +// events are intentionally ignored: the owning LogSet already drives dependency +// creation and teardown, and enqueueing dependants from those events would alter +// their existing lifecycle ordering. +func LogSetReserveOrdinalsChangedPredicate() predicate.Predicate { return predicate.Funcs{ - CreateFunc: func(event.CreateEvent) bool { return true }, - DeleteFunc: func(event.DeleteEvent) bool { return true }, + CreateFunc: func(event.CreateEvent) bool { return false }, + DeleteFunc: func(event.DeleteEvent) bool { return false }, UpdateFunc: func(e event.UpdateEvent) bool { oldSts, oldOK := e.ObjectOld.(*v1beta1.StatefulSet) newSts, newOK := e.ObjectNew.(*v1beta1.StatefulSet) diff --git a/pkg/controllers/common/statefulset_predicate_test.go b/pkg/controllers/common/statefulset_predicate_test.go index e4790d79..f05b3bb2 100644 --- a/pkg/controllers/common/statefulset_predicate_test.go +++ b/pkg/controllers/common/statefulset_predicate_test.go @@ -22,12 +22,19 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" ) -func TestLogSetStatefulSetChangedPredicate(t *testing.T) { - p := LogSetStatefulSetChangedPredicate() +func TestLogSetReserveOrdinalsChangedPredicate(t *testing.T) { + p := LogSetReserveOrdinalsChangedPredicate() oldSts := &kruisev1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "log"}} newSts := oldSts.DeepCopy() newSts.Spec.ReserveOrdinals = []int{1} + if p.Create(event.CreateEvent{Object: oldSts}) { + t.Fatal("StatefulSet creation must not change dependent lifecycle ordering") + } + if p.Delete(event.DeleteEvent{Object: oldSts}) { + t.Fatal("StatefulSet deletion must not change dependent lifecycle ordering") + } + if !p.Update(event.UpdateEvent{ObjectOld: oldSts, ObjectNew: newSts}) { t.Fatal("reserveOrdinals change must trigger reconciliation") } diff --git a/pkg/controllers/dnset/controller.go b/pkg/controllers/dnset/controller.go index 3dac14a0..e33bb0d1 100644 --- a/pkg/controllers/dnset/controller.go +++ b/pkg/controllers/dnset/controller.go @@ -307,7 +307,7 @@ func (d *Actor) Reconcile(mgr manager.Manager) error { b.Owns(&kruise.StatefulSet{}). Owns(&corev1.Service{}). Watches(&kruise.StatefulSet{}, handler.EnqueueRequestsFromMapFunc(requestsForLogSetStatefulSet(mgr.GetClient())), - builder.WithPredicates(common.LogSetStatefulSetChangedPredicate())) + builder.WithPredicates(common.LogSetReserveOrdinalsChangedPredicate())) })) if err != nil { return err diff --git a/test/e2e/matrixonecluster_test.go b/test/e2e/matrixonecluster_test.go index 2c8ded83..2e013b27 100644 --- a/test/e2e/matrixonecluster_test.go +++ b/test/e2e/matrixonecluster_test.go @@ -295,7 +295,11 @@ var _ = Describe("MatrixOneCluster test", func() { }, teardownClusterTimeout, pollInterval).Should(Succeed(), "cluster should be teardown") }) - It("Should create all sub-resources properly with maximum cluster name length", func() { + // This spec creates a complete MO cluster and has repeatedly exhausted its + // readiness timeout when competing with three other specs on the single-node + // Kind cluster. Its assertion is about generated resource names, not parallel + // reconciliation, so isolate it from the shared-cluster resource contention. + It("Should create all sub-resources properly with maximum cluster name length", Serial, func() { By("Create cluster with maximum name length") minioSecret := e2eutil.MinioSecret(env.Namespace) minioProvider := e2eutil.MinioShareStorage(minioSecret.Name) From 7b07e1b88e19f50cc0cfc96c652d41a42bcc81ee Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 18:44:43 +0800 Subject: [PATCH 08/11] fix: preserve controller and admission invariants --- .gitignore | 13 +++-- Makefile | 17 +++++-- charts/kruise/Chart.yaml | 2 +- .../templates/webhookconfiguration.yaml | 8 ++- charts/matrixone-operator/Chart.yaml | 2 +- .../kruise-webhook-availability.md | 33 +++++++----- hack/test-kruise-webhook-outage.sh | 33 +++++++++--- hack/verify-chart.sh | 44 ++++++++++++---- pkg/controllers/cnset/controller.go | 33 +++++++----- pkg/controllers/cnset/controller_test.go | 50 ++++++++++++++++--- .../common/statefulset_predicate.go | 41 ++++++++++++++- .../common/statefulset_predicate_test.go | 35 ++++++++++++- pkg/controllers/dnset/controller.go | 12 ++--- pkg/controllers/dnset/controller_test.go | 10 ++-- 14 files changed, 253 insertions(+), 80 deletions(-) diff --git a/.gitignore b/.gitignore index 5da41b17..521c3670 100644 --- a/.gitignore +++ b/.gitignore @@ -19,11 +19,10 @@ charts/matrixone-operator/charts/ *.tgz e2e*.xml e2e.test -# Operational troubleshooting documents are versioned; ignore only drafts and -# generated local test data/tools. -docs/troubleshooting/**/*.draft.md -docs/troubleshooting/**/.tools/ -docs/troubleshooting/**/.work/ -docs/troubleshooting/**/artifacts/ -docs/troubleshooting/**/__pycache__/ +# Operational troubleshooting documents are versioned as reviewed, top-level +# Markdown files. Ignore evidence, logs, tools, and other generated subtrees by +# default so environment data cannot be committed accidentally. +docs/troubleshooting/* +!docs/troubleshooting/*.md +docs/troubleshooting/*.draft.md docs/draft/ diff --git a/Makefile b/Makefile index d00a27a9..282447f4 100644 --- a/Makefile +++ b/Makefile @@ -84,14 +84,23 @@ verify-generated: exit 1; \ fi -# Make sure the generated files are up to date before open PR -reviewable: ci-reviewable verify-generated verify-chart go-lint check-license +# Make sure the generated files are up to date before open PR. Keep the steps in +# the recipe so `make -j reviewable` cannot run generators and their checks at +# the same time. +reviewable: ci-reviewable + $(MAKE) verify-generated + $(MAKE) verify-chart + $(MAKE) go-lint + $(MAKE) check-license ci-reviewable: generate manifests docs test go mod tidy -# Check whether the pull request is reviewable in CI, go-lint is delibrately excluded since we already have golangci-lint action -verify: ci-reviewable verify-generated verify-chart +# Check whether the pull request is reviewable in CI. go-lint is deliberately +# excluded since the workflow runs golangci-lint as a separate action. +verify: ci-reviewable + $(MAKE) verify-generated + $(MAKE) verify-chart echo "checking that branch is clean" test -z "$$(git status --porcelain)" || (echo "unclean working tree, did you forget to run make reviewable?" && exit 1) echo "branch is clean" diff --git a/charts/kruise/Chart.yaml b/charts/kruise/Chart.yaml index b1d0084d..76e4711b 100644 --- a/charts/kruise/Chart.yaml +++ b/charts/kruise/Chart.yaml @@ -21,4 +21,4 @@ kubeVersion: '>= 1.18.0-0' name: kruise sources: - https://github.com/openkruise/kruise -version: 1.8.3 +version: 1.8.3-mo.1 diff --git a/charts/kruise/templates/webhookconfiguration.yaml b/charts/kruise/templates/webhookconfiguration.yaml index dd041685..62e35e17 100644 --- a/charts/kruise/templates/webhookconfiguration.yaml +++ b/charts/kruise/templates/webhookconfiguration.yaml @@ -18,9 +18,7 @@ webhooks: namespace: {{ .Values.installation.namespace }} path: /mutate-pod timeoutSeconds: {{ .Values.webhookConfiguration.timeoutSeconds }} - # Pod admission is fail-open so a Kruise webhook outage cannot block Pod - # creation across the cluster. Kruise custom-resource webhooks remain Fail. - failurePolicy: Ignore + failurePolicy: Fail name: mpod.kb.io namespaceSelector: matchExpressions: @@ -525,7 +523,7 @@ webhooks: name: kruise-webhook-service namespace: {{ .Values.installation.namespace }} path: /validate-pod - failurePolicy: Ignore + failurePolicy: Fail timeoutSeconds: {{ .Values.webhookConfiguration.timeoutSeconds }} name: vpod.kb.io namespaceSelector: @@ -557,7 +555,7 @@ webhooks: name: kruise-webhook-service namespace: {{ .Values.installation.namespace }} path: /validate-pod - failurePolicy: Ignore + failurePolicy: Fail timeoutSeconds: {{ .Values.webhookConfiguration.timeoutSeconds }} name: vpodeviction.kb.io namespaceSelector: diff --git a/charts/matrixone-operator/Chart.yaml b/charts/matrixone-operator/Chart.yaml index b23b1100..877cedee 100644 --- a/charts/matrixone-operator/Chart.yaml +++ b/charts/matrixone-operator/Chart.yaml @@ -8,6 +8,6 @@ kubeVersion: ">=1.19.0-0" icon: https://raw.githubusercontent.com/matrixorigin/artwork/main/docs/overview/logo.png dependencies: - name: kruise - version: "1.8.3" + version: "1.8.3-mo.1" repository: "https://matrixorigin.github.io/matrixone-operator" condition: kruise.enabled diff --git a/docs/troubleshooting/kruise-webhook-availability.md b/docs/troubleshooting/kruise-webhook-availability.md index 026d8d54..c4cd55b0 100644 --- a/docs/troubleshooting/kruise-webhook-availability.md +++ b/docs/troubleshooting/kruise-webhook-availability.md @@ -1,18 +1,25 @@ # Kruise webhook availability policy The bundled Kruise chart deliberately uses different failure policies based on -the scope of the admitted resource. +the scope of the admitted resource. MatrixOne-specific packaging and RBAC +changes use Chart version `1.8.3-mo.1` while retaining Kruise app version +`1.8.3`, so the artifact cannot be confused with the upstream `1.8.3` Chart. -## Built-in Kubernetes resources +## Pod admission -Webhooks for Pods, Pod eviction, Deployments, ReplicaSets, StatefulSets, -Namespaces, Services, Ingresses, and CustomResourceDefinitions use -`failurePolicy: Ignore`. +The `mpod.kb.io`, `vpod.kb.io`, and `vpodeviction.kb.io` webhooks use +`failurePolicy: Fail`. A Kruise webhook outage therefore blocks Pod creation, +update, deletion, and eviction requests covered by those rules. + +This preserves the contract of the enabled `PodUnavailableBudgetDeleteGate` and +`PodUnavailableBudgetUpdateGate` features. It also prevents Pods from being +created without SidecarSet, WorkloadSpread, PersistentPodState, or other Kruise +mutations that cannot be applied retroactively after the webhook recovers. -This keeps core Kubernetes API operations available while the Kruise webhook -service is starting, upgrading, or temporarily unavailable. Features implemented -by those webhooks, such as Pod mutation and deletion protection, are not -guaranteed during the outage and resume when the webhook recovers. +Other bundled webhooks for built-in Deployments, ReplicaSets, StatefulSets, +Namespaces, Services, Ingresses, and CustomResourceDefinitions use +`failurePolicy: Ignore`. API resources not selected by any Kruise webhook remain +available during the outage. ## Kruise custom resources @@ -36,6 +43,8 @@ make verify-chart ``` The Kind E2E workflow also disconnects the Kruise webhook Service temporarily. -It verifies that ordinary Pod admission remains available, Kruise custom -resources remain fail-closed, and a Helm upgrade restores the Service and its -admission path. Run that integration coverage with `make e2e-kind`. +It verifies that unrelated core API operations remain available, Pod and Kruise +custom-resource admission remain fail-closed, and a Helm upgrade restores the +Service and Pod admission path. The outage scenario runs after the established +operator E2E suite so it cannot alter that suite's initial state. Run the +integration coverage with `make e2e-kind`. diff --git a/hack/test-kruise-webhook-outage.sh b/hack/test-kruise-webhook-outage.sh index 27d46303..2eb07960 100755 --- a/hack/test-kruise-webhook-outage.sh +++ b/hack/test-kruise-webhook-outage.sh @@ -35,8 +35,10 @@ recover() { kubectl delete namespace "${test_namespace}" --ignore-not-found --wait=false >/dev/null 2>&1 || true if [[ "${needs_recovery}" == true ]]; then echo "> Restore Kruise webhook after outage test" - helm upgrade "${release}" "${operator_chart}" -n "${release_namespace}" --reuse-values \ - --wait --timeout=5m >/dev/null 2>&1 || true + if ! helm upgrade "${release}" "${operator_chart}" -n "${release_namespace}" --reuse-values \ + --wait --timeout=5m >/dev/null; then + echo "error: failed to restore Kruise webhook after outage test" >&2 + fi fi exit "${status}" } @@ -50,7 +52,7 @@ kubectl -n "${kruise_namespace}" patch service "${webhook_service}" --type=merge for _ in $(seq 1 30); do endpoints=$(kubectl -n "${kruise_namespace}" get endpoints "${webhook_service}" \ - -o jsonpath='{.subsets}' 2>/dev/null || true) + -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null || true) [[ -z "${endpoints}" ]] && break sleep 1 done @@ -60,11 +62,19 @@ if [[ -n "${endpoints:-}" ]]; then fi kubectl create namespace "${test_namespace}" >/dev/null -kubectl -n "${test_namespace}" apply -f - >/dev/null <<'EOF' + +# Resources outside the Kruise webhook rules remain available. +kubectl -n "${test_namespace}" create configmap admitted-during-outage \ + --from-literal=status=ok >/dev/null + +# Pod admission remains fail-closed because Kruise mutation and PUB validation +# cannot be repaired retroactively after an outage. +set +e +pod_failure_output=$(kubectl -n "${test_namespace}" apply -f - 2>&1 <<'EOF' apiVersion: v1 kind: Pod metadata: - name: admitted-during-outage + name: rejected-during-outage spec: restartPolicy: Never containers: @@ -72,6 +82,17 @@ spec: image: busybox:1.36 command: ["sh", "-c", "exit 0"] EOF +) +pod_failure_status=$? +set -e +if [[ ${pod_failure_status} -eq 0 ]]; then + echo "Pod unexpectedly passed fail-closed Kruise admission during webhook outage" >&2 + exit 1 +fi +if ! grep -Eq 'failed calling webhook|no endpoints available|context deadline exceeded' <<<"${pod_failure_output}"; then + echo "Pod failed for an unexpected reason: ${pod_failure_output}" >&2 + exit 1 +fi set +e failure_output=$(kubectl -n "${test_namespace}" apply -f - 2>&1 <<'EOF' @@ -112,7 +133,7 @@ kubectl -n "${kruise_namespace}" rollout status deployment "${manager_deployment for _ in $(seq 1 60); do endpoints=$(kubectl -n "${kruise_namespace}" get endpoints "${webhook_service}" \ - -o jsonpath='{.subsets}' 2>/dev/null || true) + -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null || true) [[ -n "${endpoints}" ]] && break sleep 1 done diff --git a/hack/verify-chart.sh b/hack/verify-chart.sh index a321aef4..17e895ec 100755 --- a/hack/verify-chart.sh +++ b/hack/verify-chart.sh @@ -27,15 +27,16 @@ bash -n "${REPO_ROOT}/hack/package-chart.sh" \ helm template test "${REPO_ROOT}/charts/kruise" >"${TEST_ROOT}/kruise.yaml" -# Built-in API operations must remain available during a webhook outage. Kruise -# custom resources remain fail-closed because their webhooks own their contract. +# Pod admission and Kruise custom resources remain fail-closed because their +# mutation/validation contracts cannot be repaired retroactively. Other built-in +# validators remain fail-open. The rendered chart currently emits failurePolicy +# before name inside each webhook block, which this check intentionally parses. awk ' /^[[:space:]]+failurePolicy:/ { policy = $2 } /^[[:space:]]+name: [a-z].*\.kb\.io$/ { name = $2 expected = "Fail" - if (name == "mpod.kb.io" || name == "vpod.kb.io" || name == "vpodeviction.kb.io" || - name ~ /^vbuiltin/ || name == "vcustomresourcedefinition.kb.io" || + if (name ~ /^vbuiltin/ || name == "vcustomresourcedefinition.kb.io" || name == "vnamespace.kb.io" || name == "vingress.kb.io" || name == "vservice.kb.io") { expected = "Ignore" } @@ -52,13 +53,29 @@ if grep -q 'StatefulSetAutoResizePVCGate=true' "${TEST_ROOT}/kruise.yaml"; then exit 1 fi +# Parse RBAC rule boundaries so verbs from an adjacent rule cannot produce a +# false positive when the upstream template changes ordering. if ! awk ' - /- storageclasses$/ { in_rule = 1; next } - in_rule && /- get$/ { get = 1 } - in_rule && /- list$/ { list = 1 } - in_rule && /- watch$/ { watch = 1 } - in_rule && /^---$/ { exit !(get && list && watch) } - END { if (in_rule) exit !(get && list && watch) } + function finish_rule() { + if (has_storage_group && has_storageclasses) { + found = 1 + if (!(has_get && has_list && has_watch)) failed = 1 + } + has_storage_group = has_storageclasses = has_get = has_list = has_watch = 0 + } + /^- apiGroups:$/ { finish_rule(); section = "apiGroups"; next } + /^ resources:$/ { section = "resources"; next } + /^ verbs:$/ { section = "verbs"; next } + /^ - / { + value = substr($0, 7) + if (section == "apiGroups" && value == "storage.k8s.io") has_storage_group = 1 + if (section == "resources" && value == "storageclasses") has_storageclasses = 1 + if (section == "verbs" && value == "get") has_get = 1 + if (section == "verbs" && value == "list") has_list = 1 + if (section == "verbs" && value == "watch") has_watch = 1 + } + /^---$/ { finish_rule(); section = "" } + END { finish_rule(); exit failed || !found } ' "${TEST_ROOT}/kruise.yaml"; then echo "Kruise must have read-only get/list/watch access to StorageClasses" >&2 exit 1 @@ -85,3 +102,10 @@ if [[ "${dependencies}" != "kruise" ]]; then printf '%s\n' "${dependencies}" >&2 exit 1 fi + +# Chart.lock is intentionally ignored by this repository. A developer-local +# lock file must not leak into the deterministic package. +if tar -tzf "${operator_package}" | awk '/\/Chart.lock$/ { found = 1 } END { exit !found }'; then + echo "ignored Chart.lock leaked into the operator package" >&2 + exit 1 +fi diff --git a/pkg/controllers/cnset/controller.go b/pkg/controllers/cnset/controller.go index 9830ff69..c491755d 100644 --- a/pkg/controllers/cnset/controller.go +++ b/pkg/controllers/cnset/controller.go @@ -199,24 +199,37 @@ func (c *Actor) Observe(ctx *recon.Context[*v1alpha1.CNSet]) (recon.Action[*v1al // "metric") can find CN targets the same way it already does for DN/Log (issue #600). func (c *Actor) syncMetricService(ctx *recon.Context[*v1alpha1.CNSet]) error { cn := ctx.Obj + labels := common.SubResourceLabels(cn) + // Do not depend on TypeMeta being populated on objects read from cache. + labels[common.ComponentLabelKey] = "CNSet" svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: cn.Namespace, Name: metricSvcName(cn), - Labels: common.SubResourceLabels(cn), + Labels: labels, }, Spec: corev1.ServiceSpec{ - Selector: common.SubResourceLabels(cn), + Selector: labels, }, } return recon.CreateOwnedOrUpdate(ctx, svc, func() error { + if owner := metav1.GetControllerOf(svc); owner != nil && !metav1.IsControlledBy(svc, cn) { + // A metrics-only name collision must not block CN scale, rollout, or + // configuration reconciliation. Do not mutate or steal a Service that + // is controlled by another object. + ctx.Log.Info("skip CN metrics Service owned by another controller", + "service", client.ObjectKeyFromObject(svc), "owner", owner) + return nil + } + // The Service spec is controller-managed; unrelated labels and + // annotations remain user-managed and are preserved. if svc.Labels == nil { svc.Labels = map[string]string{} } - for key, value := range common.SubResourceLabels(cn) { + for key, value := range labels { svc.Labels[key] = value } - svc.Spec.Selector = common.SubResourceLabels(cn) + svc.Spec.Selector = labels svc.Spec.Type = corev1.ServiceTypeClusterIP svc.Spec.Ports = []corev1.ServicePort{{ Name: "metric", @@ -363,25 +376,21 @@ func (c *Actor) Reconcile(mgr manager.Manager) error { func requestsForLogSetStatefulSet(reader client.Reader) handler.MapFunc { return func(ctx context.Context, object client.Object) []reconcile.Request { - sts, ok := object.(*kruise.StatefulSet) + owner, ok := common.LogSetStatefulSetOwner(object) if !ok { return nil } - owner := metav1.GetControllerOf(sts) - if owner == nil || owner.APIVersion != v1alpha1.GroupVersion.String() || owner.Kind != "LogSet" { - return nil - } sets := &v1alpha1.CNSetList{} - if err := reader.List(ctx, sets, client.InNamespace(sts.Namespace)); err != nil { - log.FromContext(ctx).Error(err, "list CNSets for LogSet StatefulSet", "statefulset", client.ObjectKeyFromObject(sts)) + if err := reader.List(ctx, sets); err != nil { + log.FromContext(ctx).Error(err, "list CNSets for LogSet StatefulSet", "statefulset", client.ObjectKeyFromObject(object)) return nil } requests := make([]reconcile.Request, 0, len(sets.Items)) for i := range sets.Items { set := &sets.Items[i] - if set.Deps.LogSet != nil && set.Deps.LogSet.Name == owner.Name { + if common.ReferencesLogSet(set.Deps.LogSetRef, set.Namespace, owner) { requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(set)}) } } diff --git a/pkg/controllers/cnset/controller_test.go b/pkg/controllers/cnset/controller_test.go index 235fbaac..2cecb135 100644 --- a/pkg/controllers/cnset/controller_test.go +++ b/pkg/controllers/cnset/controller_test.go @@ -69,6 +69,7 @@ func enableCNPromServiceDiscovery(cn *v1alpha1.CNSet) { func Test_syncMetricService(t *testing.T) { s := newScheme() labels := common.SubResourceLabels(baseCNSetForMetricSvcTest()) + labels[common.ComponentLabelKey] = "CNSet" tests := []struct { name string @@ -156,10 +157,10 @@ func Test_syncMetricService(t *testing.T) { Name: metricSvcName(cn), }}), svc)).To(Succeed()) g.Expect(svc.Labels).To(HaveKeyWithValue("drifted", "true")) - for key, value := range common.SubResourceLabels(cn) { + for key, value := range labels { g.Expect(svc.Labels).To(HaveKeyWithValue(key, value)) } - g.Expect(svc.Spec.Selector).To(Equal(common.SubResourceLabels(cn))) + g.Expect(svc.Spec.Selector).To(Equal(labels)) g.Expect(svc.Spec.Type).To(Equal(corev1.ServiceTypeClusterIP)) g.Expect(svc.Spec.Ports).To(Equal([]corev1.ServicePort{{Name: "metric", Port: int32(common.MetricsPort)}})) g.Expect(svc.Annotations).To(HaveKeyWithValue("user.example.com/keep", "yes")) @@ -205,6 +206,41 @@ func Test_syncMetricService(t *testing.T) { g.Expect(svc.Annotations).NotTo(HaveKey(common.PrometheusPortAnno)) }, }, + { + name: "does not mutate service controlled by another owner", + cnset: baseCNSetForMetricSvcTest(), + client: &fake.Client{ + Client: fake.KubeClientBuilder().WithScheme(s).WithObjects( + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: metricSvcName(baseCNSetForMetricSvcTest()), + Labels: map[string]string{"foreign": "owner"}, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "example.com/v1", + Kind: "Example", + Name: "foreign", + UID: "foreign-uid", + Controller: pointer.Bool(true), + }}, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, + Selector: map[string]string{"foreign": "selector"}, + Ports: []corev1.ServicePort{{Name: "foreign", Port: 1234}}, + }, + }, + ).Build(), + }, + expect: func(g *WithT, cn *v1alpha1.CNSet, cli client.Client, err error) { + g.Expect(err).To(BeNil()) + svc := &corev1.Service{} + g.Expect(cli.Get(context.Background(), client.ObjectKey{Namespace: cn.Namespace, Name: metricSvcName(cn)}, svc)).To(Succeed()) + g.Expect(svc.Labels).To(Equal(map[string]string{"foreign": "owner"})) + g.Expect(svc.Spec.Selector).To(Equal(map[string]string{"foreign": "selector"})) + g.Expect(svc.Spec.Type).To(Equal(corev1.ServiceTypeNodePort)) + }, + }, } for _, tt := range tests { @@ -227,20 +263,20 @@ func Test_syncMetricService(t *testing.T) { func TestRequestsForLogSetStatefulSet(t *testing.T) { s := newScheme() - logSet := &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "log", UID: "log-uid"}} + logSet := &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{Namespace: "provider", Name: "log", UID: "log-uid"}} matching := &v1alpha1.CNSet{ - ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "matching"}, + ObjectMeta: metav1.ObjectMeta{Namespace: "consumer", Name: "matching"}, Deps: v1alpha1.CNSetDeps{LogSetRef: logSet.AsDependency()}, } unrelated := &v1alpha1.CNSet{ - ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "unrelated"}, + ObjectMeta: metav1.ObjectMeta{Namespace: "consumer", Name: "unrelated"}, Deps: v1alpha1.CNSetDeps{LogSetRef: v1alpha1.LogSetRef{LogSet: &v1alpha1.LogSet{ - ObjectMeta: metav1.ObjectMeta{Name: "other"}, + ObjectMeta: metav1.ObjectMeta{Namespace: "other", Name: "log"}, }}}, } cli := fake.KubeClientBuilder().WithScheme(s).WithObjects(matching, unrelated).Build() sts := &kruisev1.StatefulSet{ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", + Namespace: "provider", Name: "log-log", OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(logSet, v1alpha1.GroupVersion.WithKind("LogSet"))}, diff --git a/pkg/controllers/common/statefulset_predicate.go b/pkg/controllers/common/statefulset_predicate.go index 454d5aa4..f9db2f7a 100644 --- a/pkg/controllers/common/statefulset_predicate.go +++ b/pkg/controllers/common/statefulset_predicate.go @@ -15,12 +15,45 @@ package common import ( + "github.com/matrixorigin/matrixone-operator/api/core/v1alpha1" "github.com/openkruise/kruise-api/apps/v1beta1" "k8s.io/apimachinery/pkg/api/equality" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" ) +// LogSetStatefulSetOwner returns the LogSet controller key for a Kruise +// StatefulSet. It rejects similarly named resources owned by another API. +func LogSetStatefulSetOwner(object client.Object) (client.ObjectKey, bool) { + sts, ok := object.(*v1beta1.StatefulSet) + if !ok { + return client.ObjectKey{}, false + } + owner := sts.GetOwnerReferences() + for i := range owner { + if owner[i].Controller != nil && *owner[i].Controller && + owner[i].APIVersion == v1alpha1.GroupVersion.String() && owner[i].Kind == "LogSet" { + return client.ObjectKey{Namespace: sts.Namespace, Name: owner[i].Name}, true + } + } + return client.ObjectKey{}, false +} + +// ReferencesLogSet reports whether an internal LogSet reference points to key. +// Older embedded references may omit namespace; those are local to the +// dependent object for compatibility with the established API semantics. +func ReferencesLogSet(ref v1alpha1.LogSetRef, dependentNamespace string, key client.ObjectKey) bool { + if ref.LogSet == nil { + return false + } + namespace := ref.LogSet.Namespace + if namespace == "" { + namespace = dependentNamespace + } + return namespace == key.Namespace && ref.LogSet.Name == key.Name +} + // LogSetReserveOrdinalsChangedPredicate selects updates that change the LogSet // service-addresses consumed by CNSet and DNSet configuration. Create and delete // events are intentionally ignored: the owning LogSet already drives dependency @@ -33,7 +66,13 @@ func LogSetReserveOrdinalsChangedPredicate() predicate.Predicate { UpdateFunc: func(e event.UpdateEvent) bool { oldSts, oldOK := e.ObjectOld.(*v1beta1.StatefulSet) newSts, newOK := e.ObjectNew.(*v1beta1.StatefulSet) - return oldOK && newOK && !equality.Semantic.DeepEqual(oldSts.Spec.ReserveOrdinals, newSts.Spec.ReserveOrdinals) + if !oldOK || !newOK { + return false + } + if _, ok := LogSetStatefulSetOwner(newSts); !ok { + return false + } + return !equality.Semantic.DeepEqual(oldSts.Spec.ReserveOrdinals, newSts.Spec.ReserveOrdinals) }, GenericFunc: func(event.GenericEvent) bool { return false }, } diff --git a/pkg/controllers/common/statefulset_predicate_test.go b/pkg/controllers/common/statefulset_predicate_test.go index f05b3bb2..71fa80eb 100644 --- a/pkg/controllers/common/statefulset_predicate_test.go +++ b/pkg/controllers/common/statefulset_predicate_test.go @@ -17,14 +17,22 @@ package common import ( "testing" + "github.com/matrixorigin/matrixone-operator/api/core/v1alpha1" kruisev1 "github.com/openkruise/kruise-api/apps/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" ) func TestLogSetReserveOrdinalsChangedPredicate(t *testing.T) { p := LogSetReserveOrdinalsChangedPredicate() - oldSts := &kruisev1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "log"}} + logSet := &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{Namespace: "provider", Name: "log", UID: "log-uid"}} + oldSts := &kruisev1.StatefulSet{ObjectMeta: metav1.ObjectMeta{ + Namespace: "provider", + Name: "log-log", + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(logSet, + v1alpha1.GroupVersion.WithKind("LogSet"))}, + }} newSts := oldSts.DeepCopy() newSts.Spec.ReserveOrdinals = []int{1} @@ -44,4 +52,29 @@ func TestLogSetReserveOrdinalsChangedPredicate(t *testing.T) { if p.Update(event.UpdateEvent{ObjectOld: newSts, ObjectNew: statusOnly}) { t.Fatal("status-only change must not trigger dependent reconciliation") } + + notLogSet := newSts.DeepCopy() + notLogSet.OwnerReferences[0].Kind = "DNSet" + if p.Update(event.UpdateEvent{ObjectOld: oldSts, ObjectNew: notLogSet}) { + t.Fatal("StatefulSets not controlled by a LogSet must be ignored") + } +} + +func TestReferencesLogSet(t *testing.T) { + key := client.ObjectKey{Namespace: "provider", Name: "log"} + if !ReferencesLogSet(v1alpha1.LogSetRef{LogSet: &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{ + Namespace: "provider", Name: "log", + }}}, "consumer", key) { + t.Fatal("explicit cross-namespace reference must match") + } + if !ReferencesLogSet(v1alpha1.LogSetRef{LogSet: &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{ + Name: "log", + }}}, "provider", key) { + t.Fatal("empty namespace must fall back to the dependent namespace") + } + if ReferencesLogSet(v1alpha1.LogSetRef{LogSet: &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{ + Namespace: "other", Name: "log", + }}}, "consumer", key) { + t.Fatal("same-name LogSet in another namespace must not match") + } } diff --git a/pkg/controllers/dnset/controller.go b/pkg/controllers/dnset/controller.go index e33bb0d1..a89a4169 100644 --- a/pkg/controllers/dnset/controller.go +++ b/pkg/controllers/dnset/controller.go @@ -318,25 +318,21 @@ func (d *Actor) Reconcile(mgr manager.Manager) error { func requestsForLogSetStatefulSet(reader client.Reader) handler.MapFunc { return func(ctx context.Context, object client.Object) []reconcile.Request { - sts, ok := object.(*kruise.StatefulSet) + owner, ok := common.LogSetStatefulSetOwner(object) if !ok { return nil } - owner := metav1.GetControllerOf(sts) - if owner == nil || owner.APIVersion != v1alpha1.GroupVersion.String() || owner.Kind != "LogSet" { - return nil - } sets := &v1alpha1.DNSetList{} - if err := reader.List(ctx, sets, client.InNamespace(sts.Namespace)); err != nil { - log.FromContext(ctx).Error(err, "list DNSets for LogSet StatefulSet", "statefulset", client.ObjectKeyFromObject(sts)) + if err := reader.List(ctx, sets); err != nil { + log.FromContext(ctx).Error(err, "list DNSets for LogSet StatefulSet", "statefulset", client.ObjectKeyFromObject(object)) return nil } requests := make([]reconcile.Request, 0, len(sets.Items)) for i := range sets.Items { set := &sets.Items[i] - if set.Deps.LogSet != nil && set.Deps.LogSet.Name == owner.Name { + if common.ReferencesLogSet(set.Deps.LogSetRef, set.Namespace, owner) { requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(set)}) } } diff --git a/pkg/controllers/dnset/controller_test.go b/pkg/controllers/dnset/controller_test.go index 1fe2c687..0b41438e 100644 --- a/pkg/controllers/dnset/controller_test.go +++ b/pkg/controllers/dnset/controller_test.go @@ -40,20 +40,20 @@ import ( func TestRequestsForLogSetStatefulSet(t *testing.T) { s := newScheme() - logSet := &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "log", UID: "log-uid"}} + logSet := &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{Namespace: "provider", Name: "log", UID: "log-uid"}} matching := &v1alpha1.DNSet{ - ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "matching"}, + ObjectMeta: metav1.ObjectMeta{Namespace: "consumer", Name: "matching"}, Deps: v1alpha1.DNSetDeps{LogSetRef: logSet.AsDependency()}, } unrelated := &v1alpha1.DNSet{ - ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "unrelated"}, + ObjectMeta: metav1.ObjectMeta{Namespace: "consumer", Name: "unrelated"}, Deps: v1alpha1.DNSetDeps{LogSetRef: v1alpha1.LogSetRef{LogSet: &v1alpha1.LogSet{ - ObjectMeta: metav1.ObjectMeta{Name: "other"}, + ObjectMeta: metav1.ObjectMeta{Namespace: "other", Name: "log"}, }}}, } cli := fake.KubeClientBuilder().WithScheme(s).WithObjects(matching, unrelated).Build() sts := &kruisev1.StatefulSet{ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", + Namespace: "provider", Name: "log-log", OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(logSet, v1alpha1.GroupVersion.WithKind("LogSet"))}, From 24ddbea2d65acbf26a98925c0d87bb28f899a924 Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 19:04:26 +0800 Subject: [PATCH 09/11] test: sever cached webhook connections during outage --- hack/test-kruise-webhook-outage.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hack/test-kruise-webhook-outage.sh b/hack/test-kruise-webhook-outage.sh index 2eb07960..1acb998b 100755 --- a/hack/test-kruise-webhook-outage.sh +++ b/hack/test-kruise-webhook-outage.sh @@ -45,10 +45,14 @@ recover() { trap recover EXIT echo "> Simulate Kruise webhook service outage" -# Change a chart-managed selector to remove all endpoints without stopping the -# controller. Helm upgrade must restore the declared selector value. +# Change a chart-managed selector to remove all endpoints. Recreate the manager +# Pods as well so the API server cannot reuse an admission connection that was +# established before the Service was isolated. Helm upgrade must restore the +# declared selector value. kubectl -n "${kruise_namespace}" patch service "${webhook_service}" --type=merge \ -p '{"spec":{"selector":{"control-plane":"reliability-outage"}}}' >/dev/null +kubectl -n "${kruise_namespace}" delete pod \ + -l control-plane=controller-manager --wait=true --timeout=2m >/dev/null for _ in $(seq 1 30); do endpoints=$(kubectl -n "${kruise_namespace}" get endpoints "${webhook_service}" \ From da6b66ee02d7cb71fdf65fbfce42acf03bb43826 Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 22:58:09 +0800 Subject: [PATCH 10/11] refactor: defer Kruise RBAC customization --- charts/kruise/Chart.yaml | 2 +- charts/kruise/templates/rbac_role.yaml | 7 ++-- charts/matrixone-operator/Chart.yaml | 2 +- .../kruise-webhook-availability.md | 12 +------ hack/verify-chart.sh | 33 ------------------- 5 files changed, 6 insertions(+), 50 deletions(-) diff --git a/charts/kruise/Chart.yaml b/charts/kruise/Chart.yaml index 76e4711b..b1d0084d 100644 --- a/charts/kruise/Chart.yaml +++ b/charts/kruise/Chart.yaml @@ -21,4 +21,4 @@ kubeVersion: '>= 1.18.0-0' name: kruise sources: - https://github.com/openkruise/kruise -version: 1.8.3-mo.1 +version: 1.8.3 diff --git a/charts/kruise/templates/rbac_role.yaml b/charts/kruise/templates/rbac_role.yaml index 86722313..695de735 100644 --- a/charts/kruise/templates/rbac_role.yaml +++ b/charts/kruise/templates/rbac_role.yaml @@ -790,9 +790,7 @@ rules: - get - patch - update -# Kruise v1.8.3 starts the StorageClass informer even when PVC auto-resize is -# disabled. Keep this read-only permission unconditional; it does not enable -# StatefulSetAutoResizePVCGate or permit PVC mutations (issue #610). +{{- if (contains "StatefulSetAutoResizePVCGate=true" .Values.featureGates) }} - apiGroups: - storage.k8s.io resources: @@ -801,6 +799,7 @@ rules: - get - list - watch +{{- end }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding @@ -1062,4 +1061,4 @@ rules: - delete - deletecollection - patch - - update + - update \ No newline at end of file diff --git a/charts/matrixone-operator/Chart.yaml b/charts/matrixone-operator/Chart.yaml index 877cedee..b23b1100 100644 --- a/charts/matrixone-operator/Chart.yaml +++ b/charts/matrixone-operator/Chart.yaml @@ -8,6 +8,6 @@ kubeVersion: ">=1.19.0-0" icon: https://raw.githubusercontent.com/matrixorigin/artwork/main/docs/overview/logo.png dependencies: - name: kruise - version: "1.8.3-mo.1" + version: "1.8.3" repository: "https://matrixorigin.github.io/matrixone-operator" condition: kruise.enabled diff --git a/docs/troubleshooting/kruise-webhook-availability.md b/docs/troubleshooting/kruise-webhook-availability.md index c4cd55b0..74f356a2 100644 --- a/docs/troubleshooting/kruise-webhook-availability.md +++ b/docs/troubleshooting/kruise-webhook-availability.md @@ -1,9 +1,7 @@ # Kruise webhook availability policy The bundled Kruise chart deliberately uses different failure policies based on -the scope of the admitted resource. MatrixOne-specific packaging and RBAC -changes use Chart version `1.8.3-mo.1` while retaining Kruise app version -`1.8.3`, so the artifact cannot be confused with the upstream `1.8.3` Chart. +the scope of the admitted resource. ## Pod admission @@ -28,14 +26,6 @@ use `failurePolicy: Fail`. Their defaulting and validation are part of the Kruise resource contract, and an outage therefore blocks changes only to the affected Kruise APIs instead of blocking general Kubernetes workloads. -## StorageClass informer - -Kruise v1.8.3 starts a read-only StorageClass informer even when -`StatefulSetAutoResizePVCGate` is disabled. The bundled ClusterRole grants -unconditional `get`, `list`, and `watch` access to StorageClasses so the informer -can run without repeated authorization errors. This permission does not enable -PVC auto-resize and grants no PVC mutation verb. - The chart regression checks can be run with: ```sh diff --git a/hack/verify-chart.sh b/hack/verify-chart.sh index 17e895ec..18bdf273 100755 --- a/hack/verify-chart.sh +++ b/hack/verify-chart.sh @@ -48,39 +48,6 @@ awk ' END { exit failed } ' "${TEST_ROOT}/kruise.yaml" -if grep -q 'StatefulSetAutoResizePVCGate=true' "${TEST_ROOT}/kruise.yaml"; then - echo "StatefulSetAutoResizePVCGate must remain disabled by default" >&2 - exit 1 -fi - -# Parse RBAC rule boundaries so verbs from an adjacent rule cannot produce a -# false positive when the upstream template changes ordering. -if ! awk ' - function finish_rule() { - if (has_storage_group && has_storageclasses) { - found = 1 - if (!(has_get && has_list && has_watch)) failed = 1 - } - has_storage_group = has_storageclasses = has_get = has_list = has_watch = 0 - } - /^- apiGroups:$/ { finish_rule(); section = "apiGroups"; next } - /^ resources:$/ { section = "resources"; next } - /^ verbs:$/ { section = "verbs"; next } - /^ - / { - value = substr($0, 7) - if (section == "apiGroups" && value == "storage.k8s.io") has_storage_group = 1 - if (section == "resources" && value == "storageclasses") has_storageclasses = 1 - if (section == "verbs" && value == "get") has_get = 1 - if (section == "verbs" && value == "list") has_list = 1 - if (section == "verbs" && value == "watch") has_watch = 1 - } - /^---$/ { finish_rule(); section = "" } - END { finish_rule(); exit failed || !found } -' "${TEST_ROOT}/kruise.yaml"; then - echo "Kruise must have read-only get/list/watch access to StorageClasses" >&2 - exit 1 -fi - # Reproduce a dirty developer workspace in a temporary source tree. The stale # archive must not be copied into the operator package. mkdir -p "${TEST_ROOT}/source/charts" "${TEST_ROOT}/packages" From 5d0b3bedc39b5aa70e6ac1e9ebb1e9892e57a6d0 Mon Sep 17 00:00:00 2001 From: lr90 Date: Wed, 2 Sep 2026 23:47:39 +0800 Subject: [PATCH 11/11] fix: keep CN metric selectors aligned with pods --- pkg/controllers/cnset/controller.go | 12 ++++---- pkg/controllers/cnset/controller_test.go | 37 +++++++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/pkg/controllers/cnset/controller.go b/pkg/controllers/cnset/controller.go index c491755d..07e1e924 100644 --- a/pkg/controllers/cnset/controller.go +++ b/pkg/controllers/cnset/controller.go @@ -99,7 +99,7 @@ func (c *Actor) Observe(ctx *recon.Context[*v1alpha1.CNSet]) (recon.Action[*v1al return nil, errors.WrapPrefix(err, "sync service", 0) } - if err := c.syncMetricService(ctx); err != nil { + if err := c.syncMetricService(ctx, cs.Spec.Template.Labels); err != nil { return nil, errors.WrapPrefix(err, "sync metric service", 0) } @@ -197,10 +197,12 @@ func (c *Actor) Observe(ctx *recon.Context[*v1alpha1.CNSet]) (recon.Action[*v1al // syncMetricService reconciles a dedicated ClusterIP Service exposing the CN metrics port, // so that Service-based Prometheus discovery (e.g. ServiceMonitor matching on port name // "metric") can find CN targets the same way it already does for DN/Log (issue #600). -func (c *Actor) syncMetricService(ctx *recon.Context[*v1alpha1.CNSet]) error { +func (c *Actor) syncMetricService(ctx *recon.Context[*v1alpha1.CNSet], podSelector map[string]string) error { cn := ctx.Obj labels := common.SubResourceLabels(cn) - // Do not depend on TypeMeta being populated on objects read from cache. + // ServiceMonitor selects the Service by component, while the Service itself + // must select the labels of the existing CloneSet Pods. Keep these concerns + // separate so an incomplete TypeMeta cannot disconnect metrics endpoints. labels[common.ComponentLabelKey] = "CNSet" svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -209,7 +211,7 @@ func (c *Actor) syncMetricService(ctx *recon.Context[*v1alpha1.CNSet]) error { Labels: labels, }, Spec: corev1.ServiceSpec{ - Selector: labels, + Selector: podSelector, }, } return recon.CreateOwnedOrUpdate(ctx, svc, func() error { @@ -229,7 +231,7 @@ func (c *Actor) syncMetricService(ctx *recon.Context[*v1alpha1.CNSet]) error { for key, value := range labels { svc.Labels[key] = value } - svc.Spec.Selector = labels + svc.Spec.Selector = podSelector svc.Spec.Type = corev1.ServiceTypeClusterIP svc.Spec.Ports = []corev1.ServicePort{{ Name: "metric", diff --git a/pkg/controllers/cnset/controller_test.go b/pkg/controllers/cnset/controller_test.go index 2cecb135..23303eac 100644 --- a/pkg/controllers/cnset/controller_test.go +++ b/pkg/controllers/cnset/controller_test.go @@ -42,6 +42,10 @@ import ( func baseCNSetForMetricSvcTest() *v1alpha1.CNSet { return &v1alpha1.CNSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.GroupVersion.String(), + Kind: "CNSet", + }, ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "test", @@ -255,12 +259,43 @@ func Test_syncMetricService(t *testing.T) { eventEmitter := fake.NewMockEventEmitter(mockCtrl) ctx := fake.NewContext(cn, tt.client, eventEmitter) - err := (&Actor{}).syncMetricService(ctx) + err := (&Actor{}).syncMetricService(ctx, common.SubResourceLabels(cn)) tt.expect(g, cn, tt.client, err) }) } } +func TestMetricServiceSelectorMatchesCNPodLabels(t *testing.T) { + for _, tc := range []struct { + name string + withTypeMeta bool + }{ + {name: "without TypeMeta", withTypeMeta: false}, + {name: "with TypeMeta", withTypeMeta: true}, + } { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + cn := baseCNSetForMetricSvcTest() + if !tc.withTypeMeta { + cn.TypeMeta = metav1.TypeMeta{} + } + cli := &fake.Client{Client: fake.KubeClientBuilder().WithScheme(newScheme()).Build()} + ctx := fake.NewContext(cn, cli, fake.NewMockEventEmitter(gomock.NewController(t))) + + cnPods := buildCNSet(cn, &corev1.Service{}).Spec.Template.Labels + g.Expect((&Actor{}).syncMetricService(ctx, cnPods)).To(Succeed()) + svc := &corev1.Service{} + g.Expect(cli.Get(context.Background(), client.ObjectKey{ + Namespace: cn.Namespace, + Name: metricSvcName(cn), + }, svc)).To(Succeed()) + + g.Expect(svc.Spec.Selector).To(Equal(cnPods)) + g.Expect(svc.Labels).To(HaveKeyWithValue(common.ComponentLabelKey, "CNSet")) + }) + } +} + func TestRequestsForLogSetStatefulSet(t *testing.T) { s := newScheme() logSet := &v1alpha1.LogSet{ObjectMeta: metav1.ObjectMeta{Namespace: "provider", Name: "log", UID: "log-uid"}}