Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { UseFormReturn } from "react-hook-form";
import Editor from "@monaco-editor/react";
import yaml from "js-yaml";
import { z } from "zod";

import { useTheme } from "~/components/ThemeProvider";
import { FormField } from "~/components/ui/form";
import { argoWorkflowsJobAgentConfig } from "../deploymentJobAgentConfig";

const DEFAULT_CONFIG = {
apiVersion: "argoproj.io/v1alpha1",
kind: "Workflow",
metadata: {
generateName: "{{.deployment.slug}}-{{.environment.name}}-",
labels: {
"ctrlplane.dev/job-id": "{{.job.id}}",
deployment: "{{.deployment.name}}",
environment: "{{.environment.name}}",
},
},
spec: {
entrypoint: "run",
arguments: {
parameters: [
{ name: "job_id", value: "{{.job.id}}" },
{ name: "version_tag", value: "{{.version.tag}}" },
],
},
templates: [
{
name: "run",
container: {
image: "alpine:3.20",
command: ["sh", "-c"],
args: [
"echo Deploying {{.deployment.name}} version {{.version.tag}} to {{.resource.name}}",
],
},
},
],
},
};

const formSchema = z.object({
jobAgentId: z.string(),
jobAgentConfig: z.record(z.string(), z.any()),
});

const argoWorkflowsFormSchema = z.object({
jobAgentId: z.string(),
jobAgentConfig: argoWorkflowsJobAgentConfig,
});

function getConfigString(config: { template?: string }): string {
const template = config.template ?? "";
if (template && template.trim()) return template;
return yaml.dump(DEFAULT_CONFIG);
}

type Form = UseFormReturn<z.infer<typeof formSchema>>;
type ArgoWorkflowsForm = UseFormReturn<z.infer<typeof argoWorkflowsFormSchema>>;

type ArgoWorkflowsConfigProps = { form: Form };

export function ArgoWorkflowsConfig({ form }: ArgoWorkflowsConfigProps) {
const { theme } = useTheme();
const argoForm = form as unknown as ArgoWorkflowsForm;

return (
<FormField
control={argoForm.control}
name="jobAgentConfig"
render={({ field: { value, onChange } }) => {
const configString = getConfigString(value);

const handleChange = (newValue: string) =>
onChange({ type: "argo-workflows", template: newValue });

return (
<div className="border">
<Editor
language="plaintext"
theme={theme === "dark" ? "vs-dark" : "vs"}
options={{ minimap: { enabled: false } }}
value={configString}
onChange={(newValue) => handleChange(newValue ?? "")}
height="600px"
/>
</div>
);
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const argoCdJobAgentConfig = z.object({
template: z.string(),
});

export const argoWorkflowsJobAgentConfig = z.object({
type: z.literal("argo-workflows"),
template: z.string(),
});

export const tfeJobAgentConfig = z.object({
type: z.literal("tfe"),
template: z.string(),
Expand All @@ -24,6 +29,7 @@ export const customJobAgentConfig = z
export const deploymentJobAgentConfig = z.discriminatedUnion("type", [
githubAppJobAgentConfig,
argoCdJobAgentConfig,
argoWorkflowsJobAgentConfig,
tfeJobAgentConfig,
customJobAgentConfig,
]);
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { useWorkspace } from "~/components/WorkspaceProvider";
import { useDeployment } from "../_components/DeploymentProvider";
import { ArgoCDConfig } from "./_components/ArgoCD";
import { ArgoWorkflowsConfig } from "./_components/ArgoWorkflows";
import { GithubAgentConfig } from "./_components/GithubAgentConfig";
import { TerraformCloudConfig } from "./_components/TerraformCloudConfig";
import { useAllJobAgents, useSelectedJobAgent } from "./_hooks/job-agents";
Expand Down Expand Up @@ -95,6 +96,9 @@ function JobAgentConfigSection({
{selectedJobAgent?.type === "argo-cd" && (
<ArgoCDConfig form={form} />
)}
{selectedJobAgent?.type === "argo-workflows" && (
<ArgoWorkflowsConfig form={form} />
)}
{selectedJobAgent?.type === "tfe" && (
<TerraformCloudConfig form={form} />
)}
Expand Down
138 changes: 138 additions & 0 deletions apps/web/app/routes/ws/runners/ArgoWorkflows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

import { trpc } from "~/api/trpc";
import { Button } from "~/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "~/components/ui/form";
import { Input } from "~/components/ui/input";
import { useWorkspace } from "~/components/WorkspaceProvider";

const argoWorkflowsSchema = z.object({
name: z.string().min(1),
serverUrl: z.string().url(),
apiKey: z.string(),
namespace: z.string().optional(),
});

export function ArgoWorkflowsDialog({
children,
}: {
children: React.ReactNode;
}) {
const { workspace } = useWorkspace();
const form = useForm({
resolver: zodResolver(argoWorkflowsSchema),
defaultValues: { name: "", serverUrl: "", apiKey: "", namespace: "" },
});

const { mutateAsync, isPending } = trpc.jobAgents.create.useMutation();

const onSubmit = form.handleSubmit((data) => {
const namespace = data.namespace?.trim() ?? "";
return mutateAsync({
workspaceId: workspace.id,
name: data.name,
type: "argo-workflows",
config: {
type: "argo-workflows",
serverUrl: data.serverUrl,
apiKey: data.apiKey,
...(namespace ? { namespace } : {}),
},
}).then(() => toast.success("Job agent creation queued successfully"));
});

return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Argo Workflows</DialogTitle>
<DialogDescription>
Configure an Argo Workflows runner to submit workflows to your
cluster.
</DialogDescription>
</DialogHeader>

<Form {...form}>
<form onSubmit={onSubmit} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>

<FormField
control={form.control}
name="serverUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Server URL</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>API Key</FormLabel>
<FormControl>
<Input {...field} type="password" />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="namespace"
render={({ field }) => (
<FormItem>
<FormLabel>Namespace (optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="default" />
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
type="submit"
disabled={isPending || !form.formState.isDirty}
>
Save
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
10 changes: 10 additions & 0 deletions apps/web/app/routes/ws/runners/CreateJobAgent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { ArgoCDDialog } from "./ArgoCD";
import { ArgoWorkflowsDialog } from "./ArgoWorkflows";

export function CreateJobAgent() {
return (
Expand All @@ -25,6 +26,15 @@ export function CreateJobAgent() {
Argo CD
</DropdownMenuItem>
</ArgoCDDialog>
<ArgoWorkflowsDialog>
<DropdownMenuItem
className="flex items-center gap-2"
onSelect={(e) => e.preventDefault()}
>
<SiArgo className="size-4 text-orange-400" />
Argo Workflows
</DropdownMenuItem>
</ArgoWorkflowsDialog>
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
18 changes: 18 additions & 0 deletions apps/web/app/routes/ws/runners/JobAgentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {
ArgoCDConfig,
argoCdJobAgentConfig,
} from "./card-contents/ArgoCDConfig";
import {
ArgoWorkflowsConfig,
argoWorkflowsJobAgentConfig,
} from "./card-contents/ArgoWorkflowsConfig";
import { CopyIdSection } from "./card-contents/CopyID";
import {
GithubConfig,
Expand All @@ -31,6 +35,15 @@ function ConfigSection({ config, id }: { config: JobAgentConfig; id: string }) {
</div>
);

const argoWorkflowsParseResult = argoWorkflowsJobAgentConfig.safeParse(config);
if (argoWorkflowsParseResult.success)
return (
<div className="space-y-2 text-xs">
<CopyIdSection id={id} />
<ArgoWorkflowsConfig config={argoWorkflowsParseResult.data} />
</div>
);

const githubParseResult = githubJobAgentConfig.safeParse(config);
if (githubParseResult.success)
return (
Expand Down Expand Up @@ -125,6 +138,11 @@ function TypeIcon({ config }: { config: JobAgentConfig }) {
if (argoCdParseResult.success)
return <SiArgo className="size-4 text-orange-400" />;

const argoWorkflowsParseResult =
argoWorkflowsJobAgentConfig.safeParse(config);
if (argoWorkflowsParseResult.success)
return <SiArgo className="size-4 text-orange-400" />;

const tfeParseResult = tfeJobAgentConfig.safeParse(config);
if (tfeParseResult.success)
return <SiTerraform className="size-4 text-purple-400" />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";

export const argoCdJobAgentConfig = z
.object({
type: z.literal("argo-cd"),
serverUrl: z.string(),
apiKey: z.string(),
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { z } from "zod";

export const argoWorkflowsJobAgentConfig = z
.object({
type: z.literal("argo-workflows"),
serverUrl: z.string(),
apiKey: z.string(),
namespace: z.string().optional(),
})
.passthrough();

type ArgoWorkflowsConfig = z.infer<typeof argoWorkflowsJobAgentConfig>;

export function ArgoWorkflowsConfig({ config }: { config: ArgoWorkflowsConfig }) {
return (
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Server URL</span>
<a
href={`https://${config.serverUrl}`}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline-offset-2 hover:underline"
>
{config.serverUrl}
</a>
</div>
{config.namespace && (
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Namespace</span>
<span className="font-mono">{config.namespace}</span>
</div>
)}
</div>
);
}
Loading
Loading