Skip to content
Open
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
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ ginkgo:
go install github.com/onsi/ginkgo/v2/ginkgo

test: ginkgo
ginkgo -r -v --skip-package=tests/e2e
ginkgo -r -v --skip-package=tests/e2e --skip-package=bench --label-filter "!e2e"

test-concurrent: ginkgo
ginkgo -r -v --nodes=4 --skip-package=bench --label-filter "!e2e"
Comment on lines +15 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether the repository currently has specs labeled ignore_local.
rg -n --glob '**/*_test.go' 'Label\("ignore_local"\)'

Repository: flanksource/duty

Length of output: 42


Add !ignore_local filter to match repository standards.

The repository standard (per learnings) is to use --label-filter='!ignore_local' when running tests with ginkgo. Update both targets to include this filter, e.g. --label-filter "!e2e && !ignore_local".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 15 - 18, The ginkgo commands in the Makefile (the
default ginkgo target and the test-concurrent target) are missing the
repository-standard label filter for local-ignored tests; update both ginkgo
invocations to use --label-filter "!e2e && !ignore_local" (i.e., add &&
!ignore_local to the existing --label-filter) so the lines that currently call
ginkgo -r -v --skip-package=... --label-filter "!e2e" include the combined
filter.



.PHONY: test-e2e
test-e2e: ginkgo
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ require (
)

require (
github.com/clipperhouse/stringish v0.1.1 // indirect
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the current state of go.mod and see line 114 in context
head -n 120 go.mod | tail -n 20

Repository: flanksource/duty

Length of output: 667


🏁 Script executed:

# Also check the full size of go.mod to understand the file better
wc -l go.mod

Repository: flanksource/duty

Length of output: 70


🏁 Script executed:

# Let's search for the stringish dependency to understand the context
rg "stringish" go.mod go.sum

Repository: flanksource/duty

Length of output: 319


🏁 Script executed:

# Show larger context around line 114 - the indirect require block
sed -n '110,130p' go.mod

Repository: flanksource/duty

Length of output: 879


🏁 Script executed:

# Check the entire indirect require block to see ordering
sed -n '/^require (/,/^)/p' go.mod | grep "indirect"

Repository: flanksource/duty

Length of output: 16297


🏁 Script executed:

# Look for CI configuration files that might show the lint failure
find . -maxdepth 2 -name ".github" -o -name "*.yml" -o -name "*.yaml" | head -20

Repository: flanksource/duty

Length of output: 132


🏁 Script executed:

# Check if there's a Makefile or script that runs go mod tidy
find . -maxdepth 2 -name "Makefile" -o -name "*.sh" | xargs grep -l "go mod tidy" 2>/dev/null || echo "No tidy scripts found"

Repository: flanksource/duty

Length of output: 70


🏁 Script executed:

# Check the Makefile for tidy/lint targets
grep -A 3 "tidy\|lint\|mod" Makefile | head -20

Repository: flanksource/duty

Length of output: 518


🏁 Script executed:

# Look for GitHub Actions workflows
ls -la .github/workflows/ 2>/dev/null || echo "No workflows directory"

Repository: flanksource/duty

Length of output: 535


🏁 Script executed:

# Check golangci.yaml for relevant lint rules
cat .golangci.yaml

Repository: flanksource/duty

Length of output: 671


🏁 Script executed:

# Check the lint.yml workflow for go mod tidy check
cat .github/workflows/lint.yml

Repository: flanksource/duty

Length of output: 961


Commit the tidied module files.

The go.mod file violates Go module hygiene. Line 114's indirect dependency is split across two require blocks and lacks proper alphabetical ordering (entries starting with "c" should precede those starting with "g"). Run go mod tidy to reorganize and merge the require blocks, then commit the result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go.mod` at line 114, The go.mod contains improperly organized require blocks
(the indirect entry "github.com/clipperhouse/stringish v0.1.1 // indirect" is
out of order and split across blocks); run `go mod tidy` to merge/clean the
require blocks and reorder entries alphabetically, also update/go mod tidy to
refresh go.sum, then commit the tidied go.mod and go.sum so the module files are
consistent.

github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ github.com/clarkmcc/gorm-sqlite v0.0.0-20240426202654-00ed082c0311/go.mod h1:HrR
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
Expand Down
4 changes: 3 additions & 1 deletion tests/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"go.opentelemetry.io/otel/trace"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/flanksource/commons/logger"
"github.com/flanksource/duty/context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -40,7 +41,8 @@ var _ = Describe("Context", func() {

Expect(c.GetObjectMeta().Name).To(Equal("test"))
Expect(c.IsDebug()).To(BeTrue())
Expect(c.IsTrace()).To(BeFalse())

Expect(c.IsTrace()).To(Equal(logger.IsTraceEnabled()))

Expect(c.GetName()).To(Equal("test"))
Expect(c.GetNamespace()).To(Equal("default"))
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/loki_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/flanksource/duty/logs/loki"
)

var _ = ginkgo.Describe("Loki Integration", ginkgo.Ordered, func() {
var _ = ginkgo.Describe("Loki Integration", ginkgo.Ordered, ginkgo.Label("e2e"), func() {
var (
lokiURL string
ctx context.Context
Expand Down
14 changes: 10 additions & 4 deletions tests/e2e/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ func TestE2E(t *testing.T) {
ginkgo.RunSpecs(t, "E2E Suite")
}

var _ = ginkgo.BeforeSuite(func() {
DefaultContext = setup.BeforeSuiteFn()
})
var setupOpts = setup.SetupOpts{DummyData: true}

var _ = ginkgo.AfterSuite(setup.AfterSuiteFn)
var _ = ginkgo.SynchronizedBeforeSuite(
func() []byte { return setup.SetupTemplate(setupOpts) },
func(data []byte) { DefaultContext = setup.SetupNode(data, setupOpts) },
)

var _ = ginkgo.SynchronizedAfterSuite(
setup.SynchronizedAfterSuiteAllNodes,
setup.SynchronizedAfterSuiteNode1,
)
2 changes: 1 addition & 1 deletion tests/setup/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func SetupDB(dbName string, args ...interface{}) (context.Context, error) {
return context.Context{}, fmt.Errorf("cannot create %s: %v", dbName, err)
}

shutdown.AddHookWithPriority("remote postgres", shutdown.PriorityCritical, func() {
shutdown.AddHookWithPriority("remove postgres db", shutdown.PriorityCritical, func() {
if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE %s (FORCE)", dbName)); err != nil {
logger.Errorf("execPostgres: %v", err)
}
Expand Down
241 changes: 241 additions & 0 deletions tests/setup/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package setup

import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"

embeddedPG "github.com/fergusstrange/embedded-postgres"
"github.com/flanksource/commons/logger"
"github.com/flanksource/commons/properties"
"github.com/flanksource/duty"
"github.com/flanksource/duty/context"
dutyKubernetes "github.com/flanksource/duty/kubernetes"
"github.com/flanksource/duty/shutdown"
"github.com/flanksource/duty/telemetry"
"github.com/flanksource/duty/tests/fixtures/dummy"
"github.com/onsi/ginkgo/v2"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

type SetupOpts struct {
DummyData bool
}

type templateInfo struct {
AdminURL string `json:"admin_url"`
TemplateDB string `json:"template_db"`
Port int `json:"port"`
}

func (t templateInfo) Marshal() []byte {
data, err := json.Marshal(t)
if err != nil {
panic(fmt.Sprintf("failed to marshal templateInfo: %v", err))
}
return data
}

func unmarshalTemplateInfo(data []byte) templateInfo {
var info templateInfo
if err := json.Unmarshal(data, &info); err != nil {
panic(fmt.Sprintf("failed to unmarshal templateInfo: %v", err))
}
return info
}

var (
adminURL string
nodeDBName string
)

func SetupTemplate(opts SetupOpts) []byte {
if err := properties.LoadFile(findFileInPath("test.properties", 2)); err != nil {
logger.Errorf("Failed to load test properties: %v", err)
}

defer telemetry.InitTracer()

var port int
if val, ok := os.LookupEnv(TEST_DB_PORT); ok {
parsed, err := strconv.ParseInt(val, 10, 32)
if err != nil {
panic(fmt.Sprintf("failed to parse TEST_DB_PORT: %v", err))
}
port = int(parsed)
} else {
port = duty.FreePort()
}

templateDB := "duty_test_template"

url := os.Getenv(DUTY_DB_URL)
if url != "" && !recreateDatabase {
// DUTY_DB_CREATE=false: use direct connection, no template
PgUrl = url
return templateInfo{AdminURL: url, TemplateDB: "", Port: port}.Marshal()
}

adminConn, err := ensurePostgres(port)
if err != nil {
panic(fmt.Sprintf("failed to start postgres: %v", err))
}
adminURL = adminConn

// Always recreate — dummy data uses uuid.New() so a cached template has stale UUIDs
_ = execPostgres(adminConn, fmt.Sprintf("ALTER DATABASE %s WITH is_template = false", templateDB))
_ = execPostgres(adminConn, fmt.Sprintf(
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()", templateDB))
_ = execPostgres(adminConn, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", templateDB))

if err := execPostgres(adminConn, fmt.Sprintf("CREATE DATABASE %s", templateDB)); err != nil {
panic(fmt.Sprintf("failed to create template db: %v", err))
}

templateURL := strings.Replace(adminConn, "/postgres", "/"+templateDB, 1)
if !strings.Contains(adminConn, "/postgres") {
templateURL = fmt.Sprintf("postgres://postgres:postgres@localhost:%d/%s?sslmode=disable", port, templateDB)
}

dbOptions := []duty.StartOption{duty.DisablePostgrest, duty.RunMigrations, duty.WithUrl(templateURL)}
if !disableRLS {
dbOptions = append(dbOptions, duty.EnableRLS)
}

ctx, stop, err := duty.Start(templateDB, dbOptions...)
if err != nil {
panic(fmt.Sprintf("failed to start duty for template: %v", err))
}

if err := ctx.DB().Exec("SET TIME ZONE 'UTC'").Error; err != nil {
panic(fmt.Sprintf("failed to set timezone: %v", err))
}

if opts.DummyData {
dummyData = dummy.GetStaticDummyData(ctx.DB())
if err := dummyData.Delete(ctx.DB()); err != nil {
logger.Errorf(err.Error())
}
if err := dummyData.Populate(ctx); err != nil {
panic(fmt.Sprintf("failed to populate dummy data: %v", err))
}
logger.Infof("Created dummy data in template (%d checks)", len(dummyData.Checks))
}

// Close all connections so the DB can be used as a template
stop()
_ = execPostgres(adminConn, fmt.Sprintf(
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()", templateDB))

if err := execPostgres(adminConn, fmt.Sprintf("ALTER DATABASE %s WITH is_template = true", templateDB)); err != nil {
panic(fmt.Sprintf("failed to mark template db: %v", err))
}

return templateInfo{AdminURL: adminConn, TemplateDB: templateDB, Port: port}.Marshal()
}

func SetupNode(data []byte, opts SetupOpts) context.Context {
info := unmarshalTemplateInfo(data)

if info.TemplateDB == "" {
// Direct connection mode (DUTY_DB_CREATE=false)
PgUrl = info.AdminURL
ctx, _, err := duty.Start("direct", duty.ClientOnly, duty.WithUrl(PgUrl))
if err != nil {
panic(fmt.Sprintf("failed to connect to db: %v", err))
}
return setupNodeContext(ctx, "direct")
Comment on lines +144 to +151
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Direct mode regresses the old setup contract.

Unlike tests/setup/common.go:SetupDB, the TemplateDB == "" branch on Line 147 goes straight into duty.ClientOnly, so an existing DUTY_DB_URL no longer gets migrations or opts.DummyData. That matches the current BeforeSuite panic on missing migration_logs/properties, and test-concurrent would also make every node share the same database on this path. Keep this branch on the same initialization flow as SetupDB, or explicitly reject parallel execution when no per-node clone can be created.

🧰 Tools
🪛 GitHub Actions: Test

[error] 149-149: PANICKED! BeforeSuite failed: failed to connect to db: ERROR: relation "migration_logs" does not exist (SQLSTATE 42P01); earlier errors include relation "properties" does not exist. This caused the OpenAPI/Duty test suite to abort with a failing BeforeSuite.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/setup/template.go` around lines 144 - 151, The direct-mode branch that
checks info.TemplateDB == "" currently starts duty in duty.ClientOnly via
duty.Start("direct", duty.ClientOnly, ...), which skips migrations and
opts.DummyData and allows all nodes to share one DB; change this branch to
follow the same initialization flow as SetupDB (i.e., invoke duty.Start with the
same mode/opts used in SetupDB so per-node clones, migrations and DummyData are
applied, then call setupNodeContext), or if you intentionally cannot create
per-node clones, make this branch explicitly reject parallel execution (e.g.,
panic or return an error) with a clear message; update the code paths around
TemplateDB, duty.Start, and setupNodeContext to reflect the chosen behavior.

}

adminURL = info.AdminURL
nodeDBName = fmt.Sprintf("duty_test_node%d", ginkgo.GinkgoParallelProcess())

// Drop and clone from template
_ = execPostgres(adminURL, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", nodeDBName))

// Terminate any lingering connections to the template before cloning
_ = execPostgres(adminURL, fmt.Sprintf(
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()", info.TemplateDB))

// Unmark template temporarily for cloning (some pg versions need this)
_ = execPostgres(adminURL, fmt.Sprintf("ALTER DATABASE %s WITH is_template = false", info.TemplateDB))
if err := execPostgres(adminURL, fmt.Sprintf("CREATE DATABASE %s TEMPLATE %s", nodeDBName, info.TemplateDB)); err != nil {
panic(fmt.Sprintf("failed to clone template: %v", err))
}
_ = execPostgres(adminURL, fmt.Sprintf("ALTER DATABASE %s WITH is_template = true", info.TemplateDB))

// Build node connection URL
if strings.Contains(adminURL, "/postgres") {
PgUrl = strings.Replace(adminURL, "/postgres", "/"+nodeDBName, 1)
} else {
PgUrl = fmt.Sprintf("postgres://postgres:postgres@localhost:%d/%s?sslmode=disable", info.Port, nodeDBName)
}

// Skip migrations — the clone is byte-for-byte identical to the template
ctx, _, err := duty.Start(nodeDBName, duty.ClientOnly, duty.WithUrl(PgUrl))
if err != nil {
panic(fmt.Sprintf("failed to connect to node db: %v", err))
}

return setupNodeContext(ctx, nodeDBName)
}

func setupNodeContext(ctx context.Context, dbName string) context.Context {
if err := ctx.DB().Exec("SET TIME ZONE 'UTC'").Error; err != nil {
panic(fmt.Sprintf("failed to set timezone: %v", err))
}

ctx = ctx.WithValue("db_name", dbName).WithValue("db_url", PgUrl)

clientset := fake.NewClientset(&v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "test-cm", Namespace: "default"},
Data: map[string]string{"foo": "bar"},
}, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "default"},
Data: map[string][]byte{"foo": []byte("secret")},
})

return ctx.WithLocalKubernetes(dutyKubernetes.NewKubeClient(logger.GetLogger("k8s"), clientset, nil))
}

func SynchronizedAfterSuiteAllNodes() {
if nodeDBName != "" && adminURL != "" {
if err := execPostgres(adminURL, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", nodeDBName)); err != nil {
logger.Errorf("failed to drop node db: %v", err)
}
}
}

func SynchronizedAfterSuiteNode1() {
shutdown.Shutdown()
}


func ensurePostgres(port int) (string, error) {
url := os.Getenv(DUTY_DB_URL)
if url != "" {
postgresDBUrl = url
return url, nil
}

if postgresServer == nil {
config, _ := GetEmbeddedPGConfig("postgres", port)

if v, ok := os.LookupEnv(DUTY_DB_DATA_DIR); ok {
config = config.DataPath(v)
}

postgresServer = embeddedPG.NewDatabase(config)
logger.Infof("starting embedded postgres on port %d", port)
if err := postgresServer.Start(); err != nil {
return "", err
}
logger.Infof("Started postgres on port %d", port)
}

return fmt.Sprintf("postgres://postgres:postgres@localhost:%d/postgres?sslmode=disable", port), nil
}
14 changes: 10 additions & 4 deletions tests/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ func TestDuty(t *testing.T) {
ginkgo.RunSpecs(t, "Duty Suite")
}

var _ = ginkgo.BeforeSuite(func() {
DefaultContext = setup.BeforeSuiteFn()
})
var setupOpts = setup.SetupOpts{DummyData: true}

var _ = ginkgo.AfterSuite(setup.AfterSuiteFn)
var _ = ginkgo.SynchronizedBeforeSuite(
func() []byte { return setup.SetupTemplate(setupOpts) },
func(data []byte) { DefaultContext = setup.SetupNode(data, setupOpts) },
)

var _ = ginkgo.SynchronizedAfterSuite(
setup.SynchronizedAfterSuiteAllNodes,
setup.SynchronizedAfterSuiteNode1,
)
Loading