Skip to content

Commit 914d323

Browse files
authored
Merge branch 'main' into main
2 parents cf95fca + 3772334 commit 914d323

File tree

118 files changed

+3676
-1711
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+3676
-1711
lines changed

apps/kubernetes-provider/src/index.ts

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
import { PodCleaner } from "./podCleaner";
1818
import { TaskMonitor } from "./taskMonitor";
1919
import { UptimeHeartbeat } from "./uptimeHeartbeat";
20+
import { assertExhaustive } from "@trigger.dev/core";
21+
import { CustomLabelHelper } from "./labelHelper";
2022

2123
const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local";
2224
const NODE_NAME = process.env.NODE_NAME || "local";
@@ -37,7 +39,14 @@ const UPTIME_MAX_PENDING_ERRORS = Number(process.env.UPTIME_MAX_PENDING_ERRORS |
3739
const POD_EPHEMERAL_STORAGE_SIZE_LIMIT = process.env.POD_EPHEMERAL_STORAGE_SIZE_LIMIT || "10Gi";
3840
const POD_EPHEMERAL_STORAGE_SIZE_REQUEST = process.env.POD_EPHEMERAL_STORAGE_SIZE_REQUEST || "2Gi";
3941

42+
// Image config
4043
const PRE_PULL_DISABLED = process.env.PRE_PULL_DISABLED === "true";
44+
const ADDITIONAL_PULL_SECRETS = process.env.ADDITIONAL_PULL_SECRETS;
45+
const PAUSE_IMAGE = process.env.PAUSE_IMAGE || "registry.k8s.io/pause:3.9";
46+
const BUSYBOX_IMAGE = process.env.BUSYBOX_IMAGE || "registry.digitalocean.com/trigger/busybox";
47+
const DEPLOYMENT_IMAGE_PREFIX = process.env.DEPLOYMENT_IMAGE_PREFIX;
48+
const RESTORE_IMAGE_PREFIX = process.env.RESTORE_IMAGE_PREFIX;
49+
const UTILITY_IMAGE_PREFIX = process.env.UTILITY_IMAGE_PREFIX;
4150

4251
const logger = new SimpleLogger(`[${NODE_NAME}]`);
4352
logger.log(`running in ${RUNTIME_ENV} mode`);
@@ -65,6 +74,8 @@ class KubernetesTaskOperations implements TaskOperations {
6574
apps: k8s.AppsV1Api;
6675
};
6776

77+
#labelHelper = new CustomLabelHelper();
78+
6879
constructor(opts: { namespace?: string } = {}) {
6980
if (opts.namespace) {
7081
this.#namespace.metadata.name = opts.namespace;
@@ -103,7 +114,7 @@ class KubernetesTaskOperations implements TaskOperations {
103114
containers: [
104115
{
105116
name: this.#getIndexContainerName(opts.shortCode),
106-
image: opts.imageRef,
117+
image: getImageRef("deployment", opts.imageRef),
107118
ports: [
108119
{
109120
containerPort: 8000,
@@ -157,6 +168,7 @@ class KubernetesTaskOperations implements TaskOperations {
157168
name: containerName,
158169
namespace: this.#namespace.metadata.name,
159170
labels: {
171+
...this.#labelHelper.getAdditionalLabels("create"),
160172
...this.#getSharedLabels(opts),
161173
app: "task-run",
162174
"app.kubernetes.io/part-of": "trigger-worker",
@@ -170,7 +182,7 @@ class KubernetesTaskOperations implements TaskOperations {
170182
containers: [
171183
{
172184
name: containerName,
173-
image: opts.image,
185+
image: getImageRef("deployment", opts.image),
174186
ports: [
175187
{
176188
containerPort: 8000,
@@ -218,6 +230,7 @@ class KubernetesTaskOperations implements TaskOperations {
218230
name: `${this.#getRunContainerName(opts.runId)}-${opts.checkpointId.slice(-8)}`,
219231
namespace: this.#namespace.metadata.name,
220232
labels: {
233+
...this.#labelHelper.getAdditionalLabels("restore"),
221234
...this.#getSharedLabels(opts),
222235
app: "task-run",
223236
"app.kubernetes.io/part-of": "trigger-worker",
@@ -231,12 +244,12 @@ class KubernetesTaskOperations implements TaskOperations {
231244
initContainers: [
232245
{
233246
name: "pull-base-image",
234-
image: opts.imageRef,
247+
image: getImageRef("deployment", opts.imageRef),
235248
command: ["sleep", "0"],
236249
},
237250
{
238251
name: "populate-taskinfo",
239-
image: "registry.digitalocean.com/trigger/busybox",
252+
image: getImageRef("utility", BUSYBOX_IMAGE),
240253
imagePullPolicy: "IfNotPresent",
241254
command: ["/bin/sh", "-c"],
242255
args: ["printenv COORDINATOR_HOST | tee /etc/taskinfo/coordinator-host"],
@@ -252,7 +265,7 @@ class KubernetesTaskOperations implements TaskOperations {
252265
containers: [
253266
{
254267
name: this.#getRunContainerName(opts.runId),
255-
image: opts.checkpointRef,
268+
image: getImageRef("restore", opts.checkpointRef),
256269
ports: [
257270
{
258271
containerPort: 8000,
@@ -358,7 +371,7 @@ class KubernetesTaskOperations implements TaskOperations {
358371
initContainers: [
359372
{
360373
name: "prepull",
361-
image: opts.imageRef,
374+
image: getImageRef("deployment", opts.imageRef),
362375
command: ["/usr/bin/true"],
363376
resources: {
364377
limits: {
@@ -372,7 +385,7 @@ class KubernetesTaskOperations implements TaskOperations {
372385
containers: [
373386
{
374387
name: "pause",
375-
image: "registry.k8s.io/pause:3.9",
388+
image: getImageRef("utility", PAUSE_IMAGE),
376389
resources: {
377390
limits: {
378391
cpu: "1m",
@@ -403,17 +416,20 @@ class KubernetesTaskOperations implements TaskOperations {
403416
}
404417

405418
get #defaultPodSpec(): Omit<k8s.V1PodSpec, "containers"> {
419+
const pullSecrets = ["registry-trigger", "registry-trigger-failover"];
420+
421+
if (ADDITIONAL_PULL_SECRETS) {
422+
pullSecrets.push(...ADDITIONAL_PULL_SECRETS.split(","));
423+
}
424+
425+
const imagePullSecrets = pullSecrets.map(
426+
(name) => ({ name }) satisfies k8s.V1LocalObjectReference
427+
);
428+
406429
return {
407430
restartPolicy: "Never",
408431
automountServiceAccountToken: false,
409-
imagePullSecrets: [
410-
{
411-
name: "registry-trigger",
412-
},
413-
{
414-
name: "registry-trigger-failover",
415-
},
416-
],
432+
imagePullSecrets,
417433
nodeSelector: {
418434
nodetype: "worker",
419435
},
@@ -673,6 +689,26 @@ class KubernetesTaskOperations implements TaskOperations {
673689
}
674690
}
675691

692+
type ImageType = "deployment" | "restore" | "utility";
693+
694+
function getImagePrefix(type: ImageType) {
695+
switch (type) {
696+
case "deployment":
697+
return DEPLOYMENT_IMAGE_PREFIX;
698+
case "restore":
699+
return RESTORE_IMAGE_PREFIX;
700+
case "utility":
701+
return UTILITY_IMAGE_PREFIX;
702+
default:
703+
assertExhaustive(type);
704+
}
705+
}
706+
707+
function getImageRef(type: ImageType, ref: string) {
708+
const prefix = getImagePrefix(type);
709+
return prefix ? `${prefix}/${ref}` : ref;
710+
}
711+
676712
const provider = new ProviderShell({
677713
tasks: new KubernetesTaskOperations({
678714
namespace: KUBERNETES_NAMESPACE,
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { assertExhaustive } from "@trigger.dev/core";
2+
3+
const CREATE_LABEL_ENV_VAR_PREFIX = "DEPLOYMENT_LABEL_";
4+
const RESTORE_LABEL_ENV_VAR_PREFIX = "RESTORE_LABEL_";
5+
const LABEL_SAMPLE_RATE_POSTFIX = "_SAMPLE_RATE";
6+
7+
type OperationType = "create" | "restore";
8+
9+
type CustomLabel = {
10+
key: string;
11+
value: string;
12+
sampleRate: number;
13+
};
14+
15+
export class CustomLabelHelper {
16+
// Labels and sample rates are defined in environment variables so only need to be computed once
17+
private createLabels?: CustomLabel[];
18+
private restoreLabels?: CustomLabel[];
19+
20+
private getLabelPrefix(type: OperationType) {
21+
const prefix = type === "create" ? CREATE_LABEL_ENV_VAR_PREFIX : RESTORE_LABEL_ENV_VAR_PREFIX;
22+
return prefix.toLowerCase();
23+
}
24+
25+
private getLabelSampleRatePostfix() {
26+
return LABEL_SAMPLE_RATE_POSTFIX.toLowerCase();
27+
}
28+
29+
// Can only range from 0 to 1
30+
private fractionFromPercent(percent: number) {
31+
return Math.min(1, Math.max(0, percent / 100));
32+
}
33+
34+
private isLabelSampleRateEnvVar(key: string) {
35+
return key.toLowerCase().endsWith(this.getLabelSampleRatePostfix());
36+
}
37+
38+
private isLabelEnvVar(type: OperationType, key: string) {
39+
const prefix = this.getLabelPrefix(type);
40+
return key.toLowerCase().startsWith(prefix) && !this.isLabelSampleRateEnvVar(key);
41+
}
42+
43+
private getSampleRateEnvVarKey(type: OperationType, envKey: string) {
44+
return `${envKey.toLowerCase()}${this.getLabelSampleRatePostfix()}`;
45+
}
46+
47+
private getLabelNameFromEnvVarKey(type: OperationType, key: string) {
48+
return key
49+
.slice(this.getLabelPrefix(type).length)
50+
.toLowerCase()
51+
.replace(/___/g, ".")
52+
.replace(/__/g, "/")
53+
.replace(/_/g, "-");
54+
}
55+
56+
private getCaseInsensitiveEnvValue(key: string) {
57+
for (const [envKey, value] of Object.entries(process.env)) {
58+
if (envKey.toLowerCase() === key.toLowerCase()) {
59+
return value;
60+
}
61+
}
62+
}
63+
64+
/** Returns the sample rate for a given label as fraction of 100 */
65+
private getSampleRateFromEnvVarKey(type: OperationType, envKey: string) {
66+
// Apply default: always sample
67+
const DEFAULT_SAMPLE_RATE_PERCENT = 100;
68+
const defaultSampleRateFraction = this.fractionFromPercent(DEFAULT_SAMPLE_RATE_PERCENT);
69+
70+
const value = this.getCaseInsensitiveEnvValue(this.getSampleRateEnvVarKey(type, envKey));
71+
72+
if (!value) {
73+
return defaultSampleRateFraction;
74+
}
75+
76+
const sampleRatePercent = parseFloat(value || String(DEFAULT_SAMPLE_RATE_PERCENT));
77+
78+
if (isNaN(sampleRatePercent)) {
79+
return defaultSampleRateFraction;
80+
}
81+
82+
const fractionalSampleRate = this.fractionFromPercent(sampleRatePercent);
83+
84+
return fractionalSampleRate;
85+
}
86+
87+
private getCustomLabels(type: OperationType): CustomLabel[] {
88+
switch (type) {
89+
case "create":
90+
if (this.createLabels) {
91+
return this.createLabels;
92+
}
93+
break;
94+
case "restore":
95+
if (this.restoreLabels) {
96+
return this.restoreLabels;
97+
}
98+
break;
99+
default:
100+
assertExhaustive(type);
101+
}
102+
103+
const customLabels: CustomLabel[] = [];
104+
105+
for (const [envKey, value] of Object.entries(process.env)) {
106+
const key = envKey.toLowerCase();
107+
108+
// Only process env vars that start with the expected prefix
109+
if (!this.isLabelEnvVar(type, key)) {
110+
continue;
111+
}
112+
113+
// Skip sample rates - deal with them separately
114+
if (this.isLabelSampleRateEnvVar(key)) {
115+
continue;
116+
}
117+
118+
const labelName = this.getLabelNameFromEnvVarKey(type, key);
119+
const sampleRate = this.getSampleRateFromEnvVarKey(type, key);
120+
121+
const label = {
122+
key: labelName,
123+
value: value || "",
124+
sampleRate,
125+
} satisfies CustomLabel;
126+
127+
customLabels.push(label);
128+
}
129+
130+
return customLabels;
131+
}
132+
133+
getAdditionalLabels(type: OperationType): Record<string, string> {
134+
const labels = this.getCustomLabels(type);
135+
136+
const additionalLabels: Record<string, string> = {};
137+
138+
for (const { key, value, sampleRate } of labels) {
139+
// Always apply label if sample rate is 1
140+
if (sampleRate === 1) {
141+
additionalLabels[key] = value;
142+
continue;
143+
}
144+
145+
if (Math.random() <= sampleRate) {
146+
additionalLabels[key] = value;
147+
continue;
148+
}
149+
}
150+
151+
return additionalLabels;
152+
}
153+
}

apps/kubernetes-provider/tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"strict": true,
99
"skipLibCheck": true,
1010
"paths": {
11+
"@trigger.dev/core": ["../../packages/core/src"],
12+
"@trigger.dev/core/*": ["../../packages/core/src/*"],
1113
"@trigger.dev/core/v3": ["../../packages/core/src/v3"],
1214
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"]
1315
}

apps/webapp/app/components/runs/v3/RunInspector.tsx

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,11 @@ import { cn } from "~/utils/cn";
3232
import { formatCurrencyAccurate } from "~/utils/numberFormatter";
3333
import {
3434
v3RunDownloadLogsPath,
35-
v3RunPath,
3635
v3RunSpanPath,
3736
v3RunsPath,
3837
v3SchedulePath,
39-
v3TraceSpanPath,
4038
} from "~/utils/pathBuilder";
4139
import { TraceSpan } from "~/utils/taskEvent";
42-
import { SpanLink } from "~/v3/eventRepository.server";
4340
import { isFailedRunStatus, isFinalRunStatus } from "~/v3/taskStatus";
4441
import { RunTimelineEvent, RunTimelineLine } from "./InspectorTimeline";
4542
import { RunTag } from "./RunTag";
@@ -317,18 +314,6 @@ export function RunInspector({
317314
)}
318315
</Property.Value>
319316
</Property.Item>
320-
{span?.links && span.links.length > 0 && (
321-
<Property.Item>
322-
<Property.Label>Links</Property.Label>
323-
<Property.Value>
324-
<div className="space-y-1">
325-
{span.links.map((link, index) => (
326-
<SpanLinkElement key={index} link={link} />
327-
))}
328-
</div>
329-
</Property.Value>
330-
</Property.Item>
331-
)}
332317
<Property.Item>
333318
<Property.Label>Max duration</Property.Label>
334319
<Property.Value>
@@ -647,27 +632,3 @@ function PacketDisplay({
647632
}
648633
}
649634
}
650-
651-
function SpanLinkElement({ link }: { link: SpanLink }) {
652-
const organization = useOrganization();
653-
const project = useProject();
654-
655-
switch (link.type) {
656-
case "run": {
657-
return (
658-
<TextLink to={v3RunPath(organization, project, { friendlyId: link.runId })}>
659-
{link.title}
660-
</TextLink>
661-
);
662-
}
663-
case "span": {
664-
return (
665-
<TextLink to={v3TraceSpanPath(organization, project, link.traceId, link.spanId)}>
666-
{link.title}
667-
</TextLink>
668-
);
669-
}
670-
}
671-
672-
return null;
673-
}

0 commit comments

Comments
 (0)