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
4 changes: 0 additions & 4 deletions .agents/resume

This file was deleted.

53 changes: 0 additions & 53 deletions .agents/setup

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/agent-ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@ jobs:
run: go vet ./...

- name: Install staticcheck
run: GOBIN=$PWD/.bin go install honnef.co/go/tools/cmd/staticcheck@latest
run: GOBIN=$PWD/.bin go install honnef.co/go/tools/cmd/staticcheck@v0.7.0

- name: Staticcheck
run: ./.bin/staticcheck ./...
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cli-ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@ jobs:
run: go vet ./...

- name: Install staticcheck
run: GOBIN=$PWD/.bin go install honnef.co/go/tools/cmd/staticcheck@latest
run: GOBIN=$PWD/.bin go install honnef.co/go/tools/cmd/staticcheck@v0.7.0

- name: Staticcheck
run: ./.bin/staticcheck ./...
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/updater-ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,7 +45,7 @@ jobs:
run: go vet ./...

- name: Install staticcheck
run: GOBIN=$PWD/.bin go install honnef.co/go/tools/cmd/staticcheck@latest
run: GOBIN=$PWD/.bin go install honnef.co/go/tools/cmd/staticcheck@v0.7.0

- name: Staticcheck
run: ./.bin/staticcheck ./...
Expand Down
17 changes: 17 additions & 0 deletions agent/internal/traefik/reload.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ import (
const (
lastReloadSuccessMetric = "traefik_config_last_reload_success"
pendingReloadMarkerName = ".routing-reload-pending"
metricsReadyTimeout = 15 * time.Second
)

var (
Expand All@@ -30,6 +31,22 @@ func LastSuccessfulReload() (time.Time, error) {
return readLastSuccessfulReload()
}

func waitForMetricsReady(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
var lastErr error
for {
if _, err := LastSuccessfulReload(); err == nil {
return nil
} else {
lastErr = err
}
if time.Now().After(deadline) {
return fmt.Errorf("traefik metrics did not become ready within %s: %w", timeout, lastErr)
}
time.Sleep(reloadPollInterval)
}
}

func fetchLastSuccessfulReload() (time.Time, error) {
response, err := metricsHTTPClient.Get(traefikMetricsURL)
if err != nil {
Expand Down
48 changes: 48 additions & 0 deletions agent/internal/traefik/reload_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,54 @@ traefik_config_last_reload_success 1.725e+09
}
}

func TestWaitForMetricsReadyRetriesTemporaryErrors(t *testing.T) {
originalReader := readLastSuccessfulReload
originalPollInterval := reloadPollInterval
t.Cleanup(func() {
readLastSuccessfulReload = originalReader
reloadPollInterval = originalPollInterval
})

attempts := 0
readLastSuccessfulReload = func() (time.Time, error) {
attempts++
if attempts < 3 {
return time.Time{}, os.ErrNotExist
}
return time.Now(), nil
}
reloadPollInterval = time.Millisecond

if err := waitForMetricsReady(50 * time.Millisecond); err != nil {
t.Fatal(err)
}
if attempts != 3 {
t.Fatalf("metrics read attempted %d times, want 3", attempts)
}
}

func TestWaitForMetricsReadyTimesOut(t *testing.T) {
originalReader := readLastSuccessfulReload
originalPollInterval := reloadPollInterval
t.Cleanup(func() {
readLastSuccessfulReload = originalReader
reloadPollInterval = originalPollInterval
})

readLastSuccessfulReload = func() (time.Time, error) {
return time.Time{}, os.ErrDeadlineExceeded
}
reloadPollInterval = time.Millisecond

err := waitForMetricsReady(5 * time.Millisecond)
if err == nil {
t.Fatal("metrics readiness wait unexpectedly succeeded")
}
if !strings.Contains(err.Error(), "traefik metrics did not become ready within 5ms") {
t.Fatalf("unexpected timeout error: %v", err)
}
}

func TestDynamicConfigReloadedRequiresReloadAtOrAfterNewestFile(t *testing.T) {
originalDir := dynamicConfigDir
originalReader := readLastSuccessfulReload
Expand Down
15 changes: 7 additions & 8 deletions agent/internal/traefik/static.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,6 @@ import (
"os"
"os/exec"
"reflect"
"time"

"gopkg.in/yaml.v3"
)
Expand All@@ -16,6 +15,7 @@ const (
metricsEntryPointAddr = "127.0.0.1:9100"
)

// Whole-number buckets must be ints to match yaml.v3's decoded types.
var prometheusLatencyBuckets = []interface{}{
0.005,
0.01,
Expand All@@ -26,12 +26,12 @@ var prometheusLatencyBuckets = []interface{}{
0.25,
0.5,
0.75,
1.0,
1,
2.5,
5.0,
10.0,
30.0,
60.0,
5,
10,
30,
60,
}

func validateStaticConfig(data []byte) error {
Expand DownExpand Up@@ -218,6 +218,5 @@ func ReloadTraefik() error {
return fmt.Errorf("failed to restart traefik: %w", err)
}
log.Printf("[traefik] restarted traefik to apply static config changes")
time.Sleep(2 * time.Second)
return nil
return waitForMetricsReady(metricsReadyTimeout)
}
18 changes: 16 additions & 2 deletions agent/internal/traefik/static_test.go
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
package traefik

import "testing"
import (
"testing"

"gopkg.in/yaml.v3"
)

func TestEnsurePrometheusMetricsConfigAddsPrivateMetricsEndpoint(t *testing.T) {
config := map[string]interface{}{
Expand DownExpand Up@@ -44,7 +48,17 @@ func TestEnsurePrometheusMetricsConfigIsStable(t *testing.T) {
if !ensurePrometheusMetricsConfig(config) {
t.Fatal("expected first call to modify config")
}
if ensurePrometheusMetricsConfig(config) {

data, err := yaml.Marshal(config)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
var roundTripped map[string]interface{}
if err := yaml.Unmarshal(data, &roundTripped); err != nil {
t.Fatalf("failed to unmarshal config: %v", err)
}

if ensurePrometheusMetricsConfig(roundTripped) {
t.Fatal("expected second call to be stable")
}
}
13 changes: 12 additions & 1 deletion web/actions/compose.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { db } from "@/db";
import { services } from "@/db/schema";
import { requireDeveloperRole } from "@/lib/auth";
import { parseComposeYaml } from "@/lib/compose-parser";
import { reportServerError } from "@/lib/server-errors";
import {
addServiceVolume,
createService,
Expand DownExpand Up@@ -144,6 +145,9 @@ export async function importCompose(
volume.containerPath,
);
} catch (e) {
reportServerError(e, "compose-import.volume.add", {
tags: { serviceId: result.id },
});
warnings.push({
service: finalName,
field: "volumes",
Expand DownExpand Up@@ -182,9 +186,16 @@ export async function importCompose(
for (const serviceId of createdServiceIds) {
try {
await db.delete(services).where(eq(services.id, serviceId));
} catch {}
} catch (cleanupError) {
reportServerError(cleanupError, "compose-import.cleanup", {
tags: { serviceId },
});
}
}

reportServerError(error, "compose-import.create", {
tags: { projectId, environmentId },
});
return {
success: false,
created: [],
Expand Down
4 changes: 4 additions & 0 deletions web/actions/projects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,6 +54,7 @@ import {
cleanupRegistryArtifactsForService,
prepareRegistryArtifactCleanup,
} from "@/lib/registry-retention";
import { reportServerError } from "@/lib/server-errors";
import {
deletePreviewService,
deletePreviewsForBaseService,
Expand DownExpand Up@@ -1748,6 +1749,9 @@ export async function abortRollout(serviceId: string) {
try {
await inngest.send(inngestEvents.rolloutCancelled.create({ rolloutId }));
} catch (error) {
reportServerError(error, "rollout.cancellation.dispatch", {
tags: { rolloutId, serviceId },
});
console.error(
`[rollout:${rolloutId}] failed to send cancellation:`,
error,
Expand Down
2 changes: 2 additions & 0 deletions web/app/api/builds/[buildId]/logs/route.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from "next/server";
import { invalidLogQueryResponse, normalizeLogSearch } from "@/lib/log-query";
import { reportServerError } from "@/lib/server-errors";
import { isLoggingEnabled, queryLogsByBuild } from "@/lib/victoria-logs";

export async function GET(
Expand DownExpand Up@@ -28,6 +29,7 @@ export async function GET(

return NextResponse.json({ logs });
} catch (error) {
reportServerError(error, "logs.build.query", { tags: { buildId } });
console.error("Failed to fetch build logs:", error);
return NextResponse.json(
{ message: "Failed to query build logs" },
Expand Down
4 changes: 4 additions & 0 deletions web/app/api/deployments/[id]/logs/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
normalizeLogCursor,
parseLogLimit,
} from "@/lib/log-query";
import { reportServerError } from "@/lib/server-errors";
import { isLoggingEnabled, queryLogsByDeployment } from "@/lib/victoria-logs";

export async function GET(
Expand DownExpand Up@@ -50,6 +51,9 @@ export async function GET(
hasMore: result.hasMore,
});
} catch (error) {
reportServerError(error, "logs.deployment.query", {
tags: { deploymentId },
});
console.error("[logs:deployment] failed to query logs:", error);
return Response.json(
{ message: "Failed to query deployment logs" },
Expand Down
4 changes: 4 additions & 0 deletions web/app/api/github/repos/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { db } from "@/db";
import { githubInstallations } from "@/db/schema";
import { eq } from "drizzle-orm";
import { getInstallationRepositories } from "@/lib/github";
import { reportServerError } from "@/lib/server-errors";

export async function GET() {
const session = await auth.api.getSession({
Expand DownExpand Up@@ -48,6 +49,9 @@ export async function GET() {
});
}
} catch (error) {
reportServerError(error, "github.repositories.list", {
tags: { installationId: installation.installationId },
});
console.error(
`[github:repos] failed to fetch repos for installation ${installation.installationId}:`,
error,
Expand Down
8 changes: 8 additions & 0 deletions web/app/api/github/setup/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { githubInstallations } from "@/db/schema";
import { requireRequestDeveloperRole } from "@/lib/api-auth";
import { reportServerError } from "@/lib/server-errors";

async function getInstallationDetails(installationId: number): Promise<{
account: { login: string; type: "User" | "Organization" };
Expand DownExpand Up@@ -40,6 +41,13 @@ async function getInstallationDetails(installationId: number): Promise<{
);

if (!response.ok) {
reportServerError(
new Error(
`GitHub installation lookup failed with status ${response.status}`,
),
"github.installation.get",
{ tags: { installationId } },
);
console.error(
`[github:setup] failed to get installation ${installationId}:`,
await response.text(),
Expand Down
2 changes: 2 additions & 0 deletions web/app/api/inngest/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ import {
rolloutWorkflow,
scheduledBackupsCheck,
scheduledDeploymentsCheck,
sentryFailureWorkflow,
serviceDeletionWorkflow,
serviceCommandRetention,
serviceCronDispatcher,
Expand DownExpand Up@@ -52,6 +53,7 @@ export const { GET, POST, PUT } = serve({
agentUpgradeTimeoutCheck,
registryArtifactRetention,
migrationWorkflow,
sentryFailureWorkflow,
backupWorkflow,
restoreWorkflow,
onRestoreFailed,
Expand Down
Loading
Loading