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
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import JsYaml from 'js-yaml';
import { describe, expect, it } from 'vitest';

import { renderTemplateInstanceYamls } from '@/utils/templateInstanceRendering';

describe('v2alpha template instance rendering', () => {
it('renders conditional template YAML before parsing and renames the Instance', () => {
const appYaml = `apiVersion: app.sealos.io/v1
kind: Instance
metadata:
name: fastgpt-default
spec: {}
---
apiVersion: v1
kind: Service
metadata:
name: \${{ defaults.app_name }}
spec:
ports:
- port: 80
\${{ if(inputs.agent_sandbox_baseurl) }}
- port: 3000
name: sandbox
\${{ endif() }}
`;

expect(() => JsYaml.loadAll(appYaml)).toThrow(/bad indentation|end of the stream/i);

const [yaml] = renderTemplateInstanceYamls({
appYaml,
defaults: {
app_name: 'fastgpt-brain'
},
extraLabels: {
'brain.io/managed-by': 'deployment-task'
},
inputs: {
agent_sandbox_baseurl: 'https://sandbox.example.com'
},
instanceName: 'fastgpt-brain',
templateEnvs: {}
});
const docs = JsYaml.loadAll(yaml).filter(Boolean) as any[];

expect(docs[0]).toMatchObject({
apiVersion: 'app.sealos.io/v1',
kind: 'Instance',
metadata: {
name: 'fastgpt-brain',
labels: {
'brain.io/managed-by': 'deployment-task'
}
}
});
expect(docs[1]).toMatchObject({
kind: 'Service',
metadata: {
name: 'fastgpt-brain'
},
spec: {
ports: [
{ port: 80 },
{
name: 'sandbox',
port: 3000
}
]
}
});
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { authSession } from '@/services/backend/auth';
import { getK8s } from '@/services/backend/kubernetes';
import { generateYamlList, parseTemplateString } from '@/utils/json-yaml';
import { validateExtraLabels } from '@/utils/common';
import { mapValues, reduce } from 'lodash';
import type { NextApiRequest, NextApiResponse } from 'next';
import { GetTemplateByName } from '../../getTemplateSource';
import JsYaml from 'js-yaml';
import { sendError, ErrorType, ErrorCode } from '@/types/v2alpha/error';
import { applyWithInstanceOwnerReferences } from '@/services/backend/instanceOwnerReferencesApply';
import { renderTemplateInstanceYamls } from '@/utils/templateInstanceRendering';

interface CreateInstanceRequest {
name: string;
Expand Down Expand Up @@ -281,48 +280,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const _defaults = mapValues(dataSource.defaults || {}, (value) => value?.value ?? '');
_defaults['app_name'] = trimmedName;

// Replace Instance metadata.name with user-provided name
// The appYaml contains Instance YAML with default random name, we need to update it
let updatedAppYaml: string;
// Render template conditionals before YAML parsing. Some templates contain conditional
// blocks that are not valid YAML until parseTemplateString removes or expands them.
let yamls: string[] = [];
try {
const yamlDocs = JsYaml.loadAll(appYaml).filter((doc) => doc);
if (yamlDocs.length === 0) {
return sendError(res, {
status: 500,
type: ErrorType.INTERNAL_ERROR,
code: ErrorCode.INTERNAL_ERROR,
message: 'Failed to parse template YAML: no valid documents found.'
});
}

let instanceFound = false;
const updatedDocs = yamlDocs.map((doc: any) => {
if (doc?.kind === 'Instance' && doc?.apiVersion === 'app.sealos.io/v1') {
instanceFound = true;
// Update Instance name to use user-provided name
return {
...doc,
metadata: {
...doc.metadata,
name: trimmedName
}
};
}
return doc;
yamls = renderTemplateInstanceYamls({
appYaml,
defaults: _defaults,
inputs: _inputs,
instanceName: trimmedName,
extraLabels,
templateEnvs: TemplateEnvs
});

if (!instanceFound) {
return sendError(res, {
status: 500,
type: ErrorType.INTERNAL_ERROR,
code: ErrorCode.INTERNAL_ERROR,
message: 'Failed to process template: Instance resource not found in template YAML.'
});
}

updatedAppYaml = updatedDocs.map((doc) => JsYaml.dump(doc)).join('---\n');
} catch (yamlErr: any) {
console.error('Failed to update Instance name in YAML:', yamlErr);
console.error('Failed to process template YAML:', yamlErr);
return sendError(res, {
status: 500,
type: ErrorType.INTERNAL_ERROR,
Expand All @@ -332,46 +303,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}

// Generate YAML from template
const generateStr = parseTemplateString(updatedAppYaml, {
...TemplateEnvs,
defaults: _defaults,
inputs: _inputs
});

if (!generateStr || generateStr.trim() === '') {
return sendError(res, {
status: 500,
type: ErrorType.INTERNAL_ERROR,
code: ErrorCode.INTERNAL_ERROR,
message: 'Failed to generate YAML from template: empty result.'
});
}

const correctYaml = generateYamlList(generateStr, trimmedName, extraLabels);

if (!correctYaml || correctYaml.length === 0) {
return sendError(res, {
status: 500,
type: ErrorType.INTERNAL_ERROR,
code: ErrorCode.INTERNAL_ERROR,
message: 'Failed to generate YAML list from template: no resources generated.'
});
}

const yamls = correctYaml
.map((item) => item.value)
.filter((yaml) => yaml && yaml.trim() !== '');

if (yamls.length === 0) {
return sendError(res, {
status: 500,
type: ErrorType.INTERNAL_ERROR,
code: ErrorCode.INTERNAL_ERROR,
message: 'Failed to generate valid YAML: all resources are empty.'
});
}

// Apply to Kubernetes
let createdResources: any[] = [];
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import JsYaml from 'js-yaml';

import { generateYamlList, parseTemplateString } from '@/utils/json-yaml';

type TemplateRenderContext = {
[key: string]: string | Record<string, string>;
};

type K8sResource = {
apiVersion?: string;
kind?: string;
metadata?: {
name?: string;
[key: string]: any;
};
[key: string]: any;
};

function isTemplateInstanceResource(resource: K8sResource): boolean {
return resource?.kind === 'Instance' && resource?.apiVersion === 'app.sealos.io/v1';
}

function applyInstanceNameToYamlList(yamlList: string[], instanceName: string): string[] {
let instanceFound = false;

const renamedYamlList = yamlList.map((yaml) => {
const docs = JsYaml.loadAll(yaml)
.filter((doc): doc is K8sResource => Boolean(doc))
.map((doc) => {
if (isTemplateInstanceResource(doc)) {
instanceFound = true;
return {
...doc,
metadata: {
...doc.metadata,
name: instanceName
}
};
}
return doc;
});

return docs.map((doc) => JsYaml.dump(doc)).join('---\n');
});

if (!instanceFound) {
throw new Error('Failed to process template: Instance resource not found in template YAML.');
}

return renamedYamlList;
}

export function renderTemplateInstanceYamls(input: {
appYaml: string;
defaults: Record<string, string>;
extraLabels?: Record<string, string>;
inputs: Record<string, string>;
instanceName: string;
templateEnvs: TemplateRenderContext;
}): string[] {
const generateStr = parseTemplateString(input.appYaml, {
...input.templateEnvs,
defaults: input.defaults,
inputs: input.inputs
});

if (!generateStr || generateStr.trim() === '') {
throw new Error('Failed to generate YAML from template: empty result.');
}

const correctYaml = generateYamlList(generateStr, input.instanceName, input.extraLabels);

if (!correctYaml || correctYaml.length === 0) {
throw new Error('Failed to generate YAML list from template: no resources generated.');
}

const yamls = correctYaml.map((item) => item.value).filter((yaml) => yaml && yaml.trim() !== '');

if (yamls.length === 0) {
throw new Error('Failed to generate valid YAML: all resources are empty.');
}

return applyInstanceNameToYamlList(yamls, input.instanceName);
}
Loading