mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat(templates): git compose import, six stack seeds, empty-default vars (#63)
* feat(templates): git compose import, six stack seeds, empty-default vars
- POST /templates/import/git fetches a compose file from any git remote
(clone URL, ssh, owner/repo via connected providers — private repos
included), ref selects branch or commit. Path defaults through
compose.yaml/yml then docker-compose.yml/yaml. Reuses the build-on-node
checkout machinery; repo-relative paths are containment-checked.
- CLI `templates import-git <repo> [path] [--ref]`, MCP
containr_templates_import_git, OpenAPI entry, and a paste/git toggle
in the template import dialog.
- {{VAR:-}} with an empty default now resolves instead of reporting the
var missing — matches shell :- semantics.
- SeedTemplates gains six official graph stacks — n8n (pg+redis+worker),
Gitea, Nextcloud, Vaultwarden, Umami, Immich — wired with
{{service.*}} refs and {{secret}} credentials; no hardcoded passwords.
- TestSeedTemplatesResolveCleanly parses, topo-sorts, validates, and
resolves every seeded graph — catalog drift can't merge silently.
- templates/n8n.md compose block had invalid YAML (mixed depends_on
list/map) — fixed so the documented stack actually parses.
Verified live: import/git on a dumb-HTTP repo returns the converted
graph (db image → managed postgresql), path/ref/traversal/unauth edges
return clean errors, tpl-n8n plan resolves with zero unresolved vars.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* ci: pre-pull compose-smoke images via gcr mirror
Anonymous Docker Hub pulls rate-limit shared CI egress IPs — the compose
boot smoke flaked on traefik:pull. postgres/traefik are library/* images
served by mirror.gcr.io without auth; compose reuses the retagged local
images, falling back to a normal pull when the mirror misses.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* ci: route docker.io through gcr mirror for compose smoke
Pull-time pre-pulls don't cover BuildKit's base-image metadata lookups
(node:22-alpine, nginx:alpine, golang:1.26-alpine all 429). Daemon-level
registry-mirrors covers pulls and resolution in one move.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* ci: mirror docker.io in buildx for image publish job
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* ci: route smoke pulls/builds through mirror.gcr.io via compose overlay
daemon.json registry-mirrors isn't honored by the runner's containerd
image store. Qualify refs directly: ARG-parameterized Dockerfile FROMs
(defaults unchanged) plus a CI-only compose overlay that points image
pulls and build args at mirror.gcr.io.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
c5562109bf
commit
4071ed2991
17 files changed
+603
-35
No files matched your search
@@ -1,5 +1,7 @@
|
||||
# Build stage
|
||||
FROM node:22-alpine AS builder
|
||||
ARG NODE_IMAGE=node:22-alpine
|
||||
ARG NGINX_IMAGE=nginx:alpine
|
||||
FROM ${NODE_IMAGE} AS builder
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
@@ -24,7 +26,7 @@ ENV VITE_AUTH_URL=${VITE_AUTH_URL}
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
FROM ${NGINX_IMAGE}
|
||||
|
||||
# Copy custom nginx config
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getCurrentUserProfile,
|
||||
getTemplateById,
|
||||
importComposeTemplate,
|
||||
importGitComposeTemplate,
|
||||
listProjects,
|
||||
listTemplates,
|
||||
updateTemplate,
|
||||
@@ -154,6 +155,10 @@ export function TemplatesPage() {
|
||||
const [composeYaml, setComposeYaml] = useState('');
|
||||
const [composeWarnings, setComposeWarnings] = useState<string[]>([]);
|
||||
const [composeError, setComposeError] = useState<string | null>(null);
|
||||
const [composeMode, setComposeMode] = useState<'paste' | 'git'>('paste');
|
||||
const [gitRepo, setGitRepo] = useState('');
|
||||
const [gitPath, setGitPath] = useState('');
|
||||
const [gitRef, setGitRef] = useState('');
|
||||
|
||||
const [lastDeployment, setLastDeployment] = useState<{
|
||||
projectId: string;
|
||||
@@ -364,7 +369,10 @@ export function TemplatesPage() {
|
||||
});
|
||||
|
||||
const composeImportMutation = useMutation({
|
||||
mutationFn: (yaml: string) => importComposeTemplate(yaml),
|
||||
mutationFn: () =>
|
||||
composeMode === 'git'
|
||||
? importGitComposeTemplate(gitRepo.trim(), gitPath.trim(), gitRef.trim())
|
||||
: importComposeTemplate(composeYaml),
|
||||
onSuccess: (result) => {
|
||||
setComposeWarnings(result.warnings);
|
||||
setComposeError(null);
|
||||
@@ -1107,7 +1115,7 @@ export function TemplatesPage() {
|
||||
<div>
|
||||
<h2 className="text-[15px] font-semibold text-[var(--text-primary)]">Import docker-compose</h2>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">
|
||||
Paste a compose file — known database images become managed databases, everything else becomes services.
|
||||
Paste a compose file or pull one from a git repo — known database images become managed databases.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -1118,13 +1126,67 @@ export function TemplatesPage() {
|
||||
<X size={15} />
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={composeYaml}
|
||||
onChange={(e) => setComposeYaml(e.target.value)}
|
||||
spellCheck={false}
|
||||
placeholder={'services:\n db:\n image: postgres:16\n web:\n image: myapp:latest\n depends_on: [db]'}
|
||||
className="mono min-h-[320px] flex-1 resize-none bg-[var(--bg-void)] p-4 text-[12.5px] leading-relaxed text-[var(--text-secondary)] outline-none"
|
||||
/>
|
||||
<div className="flex gap-1 border-b border-[var(--border-subtle)] px-5 pt-3">
|
||||
{(['paste', 'git'] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => setComposeMode(m)}
|
||||
className={`rounded-t-[var(--radius-md)] px-3 py-1.5 text-xs font-medium ${
|
||||
composeMode === m
|
||||
? 'bg-[var(--surface-muted)] text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{m === 'paste' ? 'Paste YAML' : 'From git repo'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{composeMode === 'paste' ? (
|
||||
<textarea
|
||||
value={composeYaml}
|
||||
onChange={(e) => setComposeYaml(e.target.value)}
|
||||
spellCheck={false}
|
||||
placeholder={'services:\n db:\n image: postgres:16\n web:\n image: myapp:latest\n depends_on: [db]'}
|
||||
className="mono min-h-[320px] flex-1 resize-none bg-[var(--bg-void)] p-4 text-[12.5px] leading-relaxed text-[var(--text-secondary)] outline-none"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex min-h-[320px] flex-1 flex-col gap-3 bg-[var(--bg-void)] p-4">
|
||||
<label className="text-xs text-[var(--text-tertiary)]">
|
||||
Repository
|
||||
<input
|
||||
value={gitRepo}
|
||||
onChange={(e) => setGitRepo(e.target.value)}
|
||||
spellCheck={false}
|
||||
placeholder="owner/repo or https://github.com/owner/repo.git"
|
||||
className="mono mt-1 w-full rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--bg-base)] px-3 py-2 text-[12.5px] text-[var(--text-secondary)] outline-none focus:border-[var(--accent-primary)]"
|
||||
/>
|
||||
</label>
|
||||
<label className="text-xs text-[var(--text-tertiary)]">
|
||||
Compose file path <span className="opacity-60">(optional — compose.yaml / docker-compose.yml searched)</span>
|
||||
<input
|
||||
value={gitPath}
|
||||
onChange={(e) => setGitPath(e.target.value)}
|
||||
spellCheck={false}
|
||||
placeholder="deploy/compose.yml"
|
||||
className="mono mt-1 w-full rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--bg-base)] px-3 py-2 text-[12.5px] text-[var(--text-secondary)] outline-none focus:border-[var(--accent-primary)]"
|
||||
/>
|
||||
</label>
|
||||
<label className="text-xs text-[var(--text-tertiary)]">
|
||||
Branch or commit <span className="opacity-60">(optional)</span>
|
||||
<input
|
||||
value={gitRef}
|
||||
onChange={(e) => setGitRef(e.target.value)}
|
||||
spellCheck={false}
|
||||
placeholder="main"
|
||||
className="mono mt-1 w-full rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--bg-base)] px-3 py-2 text-[12.5px] text-[var(--text-secondary)] outline-none focus:border-[var(--accent-primary)]"
|
||||
/>
|
||||
</label>
|
||||
<p className="text-[11px] text-[var(--text-tertiary)]">
|
||||
Private repos use your connected git providers for credentials.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{composeError ? (
|
||||
<div className="border-t border-[var(--border-subtle)] bg-[var(--error-soft)] px-5 py-2.5 text-xs text-[var(--error)]">
|
||||
{composeError}
|
||||
@@ -1140,8 +1202,8 @@ export function TemplatesPage() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => composeImportMutation.mutate(composeYaml)}
|
||||
disabled={!composeYaml.trim() || composeImportMutation.isPending}
|
||||
onClick={() => composeImportMutation.mutate()}
|
||||
disabled={(composeMode === 'paste' ? !composeYaml.trim() : !gitRepo.trim()) || composeImportMutation.isPending}
|
||||
className="inline-flex items-center gap-1.5 rounded-[var(--radius-md)] bg-[var(--accent-primary)] px-4 py-2 text-xs font-semibold text-[var(--accent-on)] disabled:opacity-50"
|
||||
>
|
||||
{composeImportMutation.isPending ? <Loader2 size={13} className="animate-spin" /> : <FileCode size={13} />}
|
||||
|
||||
@@ -3243,6 +3243,80 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/templates/import/git": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Import a compose file from a git repository
|
||||
* @description Fetches a compose file out of a git repository and runs it through
|
||||
* the same conversion as POST /templates/import/compose. `repo`
|
||||
* accepts a clone URL, ssh remote, or `owner/repo` shorthand —
|
||||
* connected git providers (including GitHub App installs) supply
|
||||
* credentials, so private repositories work. `path` defaults to
|
||||
* `compose.yaml`, `compose.yml`, `docker-compose.yml`, then
|
||||
* `docker-compose.yaml`; `ref` selects a branch or commit SHA.
|
||||
*/
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @description Clone URL, ssh remote, or owner/repo shorthand */
|
||||
repo: string;
|
||||
/** @description Repo-relative compose file path (searched by convention when empty) */
|
||||
path?: string;
|
||||
/** @description Branch name or 40-char commit SHA */
|
||||
ref?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Converted config */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
config?: components["schemas"]["TemplateConfig"];
|
||||
variables?: Record<string, never>[];
|
||||
warnings?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Invalid repo reference, compose file missing, or conversion error */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Git fetch failed */
|
||||
502: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/templates/deploy": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -1924,6 +1924,21 @@ export async function importComposeTemplate(composeYaml: string): Promise<Compos
|
||||
return { config: payload.config, warnings: payload.warnings ?? [] };
|
||||
}
|
||||
|
||||
export async function importGitComposeTemplate(
|
||||
repo: string,
|
||||
path: string,
|
||||
ref: string,
|
||||
): Promise<ComposeImportResult> {
|
||||
const payload = await requestJson<{ config?: Record<string, unknown>; warnings?: string[] }>(
|
||||
'/templates/import/git',
|
||||
{ method: 'POST', body: JSON.stringify({ repo, path: path || undefined, ref: ref || undefined }) },
|
||||
);
|
||||
if (!payload.config || typeof payload.config !== 'object') {
|
||||
throw new ApiError('Git import response is invalid', 500);
|
||||
}
|
||||
return { config: payload.config, warnings: payload.warnings ?? [] };
|
||||
}
|
||||
|
||||
export type ProjectComposeImportResult = {
|
||||
created: Array<{ key: string; kind: string; id: string; name: string; deployment_id?: string }>;
|
||||
warnings: string[];
|
||||
|
||||
Reference in new issue
Block a user