Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/instructions/focused-tests.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: Focused Test Execution
description: "Use when running, debugging, or suggesting focused tests in MOCO. Covers Makefile targets, FOCUS, envtest, CI workflow/action patterns, and avoiding raw go test detours."
applyTo: "**/*.go"
---

# Focused test execution

When running a specific MOCO test case, first use the same entry points as CI and the Makefile. Do not start with raw `go test` unless there is a clear reason.

## Preferred focused commands

- Controller envtest: `make controller-envtest FOCUS='part of spec name'`
- Clustering envtest: `make clustering-envtest FOCUS='part of spec name'`
- API envtest: `make api-envtest FOCUS='part of spec name'`
- Backup envtest: `make backup-envtest FOCUS='part of spec name'`
- Full envtest set: `make envtest`
- Small package tests/install/vet/gofmt: `make test`
- MySQL integration tests: `make test-bkop MYSQL_VERSION='8.4.8'` or `make test-dbop MYSQL_VERSION='8.4.8'`
- E2E and upgrade tests should follow `.github/actions/e2e/action.yaml` and `.github/actions/upgrade/action.yaml`; use the `e2e/Makefile` targets from inside `e2e/`.

## Why Make targets first

- `Makefile` exports `ENVTEST_KUBERNETES_VERSION` and `ENVTEST_ASSETS_DIR`; raw controller/envtest commands can fail with `could not parse "v" as version` if these are missing.
- The `*-envtest` targets run with the repo's expected flags: `-race`, `-count 1`, `-ginkgo.randomize-all`, `-ginkgo.v`, and usually `-ginkgo.fail-fast`.
- `controller-envtest` also sets `DEBUG_CONTROLLER=1`.
- CI calls `make lint`, `make test`, `make check-generate`, and `make envtest` after `.github/actions/setup-aqua`, so local verification should mirror those paths when practical.

## If raw `go test` is necessary

Only use raw `go test` for a quick diagnostic. For envtest packages, carry over the Makefile environment explicitly:

```sh
ENVTEST_KUBERNETES_VERSION=1.35.0 ENVTEST_ASSETS_DIR="$PWD/bin" \
go test ./controllers -run TestAPIs -ginkgo.focus 'part of spec name'
```
5 changes: 2 additions & 3 deletions clustering/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,9 @@ var _ = Describe("manager", func() {
ctx, cancel := context.WithCancel(ctx)
stopFunc = cancel
go func() {
defer GinkgoRecover()
err := mgr.Start(ctx)
if err != nil {
panic(err)
}
Expect(err).NotTo(HaveOccurred())
}()
time.Sleep(10 * time.Millisecond)
})
Expand Down
5 changes: 3 additions & 2 deletions cmd/moco-controller/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ func subMain(ns, addr string, port int) error {
clusterMgr := clustering.NewClusterManager(config.interval, mgr, opf, af, clusterLog)
defer clusterMgr.StopAll()

ctx := ctrl.SetupSignalHandler()

if err = (&controllers.MySQLClusterReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand All @@ -117,7 +119,7 @@ func subMain(ns, addr string, port int) error {
MaxConcurrentReconciles: config.maxConcurrentReconciles,
MySQLConfigMapHistoryLimit: config.mySQLConfigMapHistoryLimit,
DisableDefaultSecurityContext: config.disableDefaultSecurityContext,
}).SetupWithManager(mgr); err != nil {
}).SetupWithManager(ctx, mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "MySQLCluster")
return err
}
Expand Down Expand Up @@ -169,7 +171,6 @@ func subMain(ns, addr string, port int) error {
metrics.Register(k8smetrics.Registry)

setupLog.Info("starting manager")
ctx := ctrl.SetupSignalHandler()
go reloader.Run(ctx, 1*time.Hour)
if err := mgr.Start(ctx); err != nil {
setupLog.Error(err, "problem running manager")
Expand Down
53 changes: 30 additions & 23 deletions controllers/mysqlcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1950,7 +1950,7 @@ func setControllerReferenceWithPVC(cluster *mocov1beta2.MySQLCluster, pvc *corev
}

// SetupWithManager sets up the controller with the Manager.
func (r *MySQLClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
func (r *MySQLClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
certHandler := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request {
// the certificate name is formatted as "moco-agent-<cluster.Namespace>.<cluster.Name>"
if a.GetNamespace() != r.SystemNamespace {
Expand All @@ -1970,38 +1970,45 @@ func (r *MySQLClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
}
})

configMapHandler := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request {
requestsForIndexedClusters := func(ctx context.Context, namespace, field, value string) []reconcile.Request {
clusters := &mocov1beta2.MySQLClusterList{}
if err := r.List(ctx, clusters, client.InNamespace(a.GetNamespace())); err != nil {
if err := r.List(ctx, clusters, client.InNamespace(namespace), client.MatchingFields{field: value}); err != nil {
return nil
}
var req []reconcile.Request

req := make([]reconcile.Request, 0, len(clusters.Items))
for _, c := range clusters.Items {
if c.Spec.MySQLConfigMapName == nil {
continue
}
if *c.Spec.MySQLConfigMapName == a.GetName() {
req = append(req, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&c)})
}
req = append(req, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&c)})
}
return req
})
}

backupPolicyHandler := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request {
clusters := &mocov1beta2.MySQLClusterList{}
if err := r.List(ctx, clusters, client.InNamespace(a.GetNamespace())); err != nil {
if err := mgr.GetFieldIndexer().IndexField(ctx, &mocov1beta2.MySQLCluster{}, "spec.mysqlConfigMapName", func(rawObj client.Object) []string {
c := rawObj.(*mocov1beta2.MySQLCluster)
if c.Spec.MySQLConfigMapName == nil {
return nil
}
var req []reconcile.Request
for _, c := range clusters.Items {
if c.Spec.BackupPolicyName == nil {
continue
}
if *c.Spec.BackupPolicyName == a.GetName() {
req = append(req, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&c)})
}
return []string{*c.Spec.MySQLConfigMapName}
}); err != nil {
return fmt.Errorf("failed to index MySQLCluster by spec.mysqlConfigMapName: %w", err)
}

configMapHandler := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request {
return requestsForIndexedClusters(ctx, a.GetNamespace(), "spec.mysqlConfigMapName", a.GetName())
})

if err := mgr.GetFieldIndexer().IndexField(ctx, &mocov1beta2.MySQLCluster{}, "spec.backupPolicyName", func(rawObj client.Object) []string {
c := rawObj.(*mocov1beta2.MySQLCluster)
if c.Spec.BackupPolicyName == nil {
return nil
}
return req
return []string{*c.Spec.BackupPolicyName}
}); err != nil {
return fmt.Errorf("failed to index MySQLCluster by spec.backupPolicyName: %w", err)
}

backupPolicyHandler := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request {
return requestsForIndexedClusters(ctx, a.GetNamespace(), "spec.backupPolicyName", a.GetName())
})

return ctrl.NewControllerManagedBy(mgr).
Expand Down
140 changes: 140 additions & 0 deletions controllers/mysqlcluster_controller_slowdown_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package controllers

import (
"context"
"errors"
"fmt"
"time"

mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2"
"github.com/cybozu-go/moco/pkg/constants"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/config"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

func testNewStoppedMySQLCluster(ns, name, configMapName string) *mocov1beta2.MySQLCluster {
cluster := testNewMySQLCluster(ns)
cluster.Name = name
cluster.Finalizers = nil
cluster.Annotations = map[string]string{constants.AnnReconciliationStopped: "true"}
cluster.Spec.MySQLConfigMapName = new(configMapName)
return cluster
}

func testDeleteAllMySQLClusters(ctx context.Context, ns string) {
clusters := &mocov1beta2.MySQLClusterList{}
err := k8sClient.List(ctx, clusters, client.InNamespace(ns))
ExpectWithOffset(1, err).NotTo(HaveOccurred())
for _, cluster := range clusters.Items {
if len(cluster.Finalizers) == 0 {
continue
}
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
latest := &mocov1beta2.MySQLCluster{}
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(&cluster), latest); err != nil {
return client.IgnoreNotFound(err)
}
latest.Finalizers = nil
return k8sClient.Update(ctx, latest)
})
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}
err = k8sClient.DeleteAllOf(ctx, &mocov1beta2.MySQLCluster{}, client.InNamespace(ns))
ExpectWithOffset(1, err).NotTo(HaveOccurred())
EventuallyWithOffset(1, func() (int, error) {
clusters := &mocov1beta2.MySQLClusterList{}
if err := k8sClient.List(ctx, clusters, client.InNamespace(ns)); err != nil {
return 0, err
}
return len(clusters.Items), nil
}).Should(BeZero())
}

var _ = Describe("MySQLCluster reconciler startup", func() {
const largeScaleStartupObjectCount = 1000

BeforeEach(func(ctx SpecContext) {
testDeleteAllMySQLClusters(ctx, "test")
err := k8sClient.DeleteAllOf(ctx, &corev1.ConfigMap{}, client.InNamespace("test"))
Expect(err).NotTo(HaveOccurred())
})

AfterEach(func(ctx SpecContext) {
testDeleteAllMySQLClusters(ctx, "test")
err := k8sClient.DeleteAllOf(ctx, &corev1.ConfigMap{}, client.InNamespace("test"))
Expect(err).NotTo(HaveOccurred())
})

It("should start with many MySQLClusters and ConfigMaps", func(ctx SpecContext) {
By("creating many MySQLClusters and ConfigMaps before the manager starts")
for i := range largeScaleStartupObjectCount {
configMapName := fmt.Sprintf("startup-config-%04d", i)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: configMapName},
Data: map[string]string{"my.cnf": "[mysqld]\n"},
}
err := k8sClient.Create(ctx, cm)
Expect(err).NotTo(HaveOccurred())

cluster := testNewStoppedMySQLCluster("test", fmt.Sprintf("startup-%04d", i), configMapName)
err = k8sClient.Create(ctx, cluster)
Expect(err).NotTo(HaveOccurred())
}

mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme,
LeaderElection: false,
Metrics: metricsserver.Options{
BindAddress: "0",
},
Controller: config.Controller{
SkipNameValidation: new(true),
},
})
Expect(err).ToNot(HaveOccurred())

mockMgr := &mockManager{
clusters: make(map[string]struct{}),
}
mysqlr := &MySQLClusterReconciler{
Client: mgr.GetClient(),
Scheme: scheme,
Recorder: mgr.GetEventRecorderFor("moco-controller"),
SystemNamespace: testMocoSystemNamespace,
ClusterManager: mockMgr,
AgentImage: testAgentImage,
BackupImage: testBackupImage,
FluentBitImage: testFluentBitImage,
ExporterImage: testExporterImage,
MySQLConfigMapHistoryLimit: 2,
}
err = mysqlr.SetupWithManager(ctx, mgr)
Expect(err).ToNot(HaveOccurred())

startCtx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
defer GinkgoRecover()
err := mgr.Start(startCtx)
Expect(err).NotTo(HaveOccurred())
}()

By("waiting for the controller to start and process an initial cluster event")
start := time.Now()
Eventually(func() error {
if !mockMgr.getKeys()["test/startup-0000"] {
GinkgoLogr.Info("controller has not processed the startup cluster yet")
return errors.New("controller has not processed the startup cluster yet")
}
return nil
}, 15*time.Second, 2*time.Second).Should(Succeed())
GinkgoLogr.Info("controller has processed the startup cluster", "elapsed", time.Since(start))
})
})
7 changes: 3 additions & 4 deletions controllers/mysqlcluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,15 @@ var _ = Describe("MySQLCluster reconciler", func() {
ExporterImage: testExporterImage,
MySQLConfigMapHistoryLimit: 2,
}
err = mysqlr.SetupWithManager(mgr)
err = mysqlr.SetupWithManager(ctx, mgr)
Expect(err).ToNot(HaveOccurred())

ctx, cancel := context.WithCancel(ctx)
stopFunc = cancel
go func() {
defer GinkgoRecover()
err := mgr.Start(ctx)
if err != nil {
panic(err)
}
Expect(err).NotTo(HaveOccurred())
}()
time.Sleep(100 * time.Millisecond)
})
Expand Down
5 changes: 2 additions & 3 deletions controllers/partition_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,9 @@ func setupNewManager(ctx context.Context, updateInterval time.Duration) context.

ctx, cancel := context.WithCancel(ctx)
go func() {
defer GinkgoRecover()
err := mgr.Start(ctx)
if err != nil {
panic(err)
}
Expect(err).NotTo(HaveOccurred())
}()
return cancel
}
Expand Down
5 changes: 2 additions & 3 deletions controllers/pod_watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,9 @@ var _ = Describe("PodWatcher", func() {
ctx, cancel := context.WithCancel(ctx)
stopFunc = cancel
go func() {
defer GinkgoRecover()
err := mgr.Start(ctx)
if err != nil {
panic(err)
}
Expect(err).NotTo(HaveOccurred())
}()
time.Sleep(100 * time.Millisecond)
})
Expand Down
Loading