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
5 changes: 5 additions & 0 deletions src/server/lib/kubernetes/jobFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface JobConfig {
ttl?: number;
labels: Record<string, string>;
annotations?: Record<string, string>;
podAnnotations?: Record<string, string>;
initContainers?: any[];
containers: any[];
volumes?: any[];
Expand All @@ -46,6 +47,7 @@ export function createKubernetesJob(config: JobConfig): V1Job {
ttl,
labels,
annotations = {},
podAnnotations = {},
initContainers = [],
containers,
volumes = [],
Expand Down Expand Up @@ -82,6 +84,7 @@ export function createKubernetesJob(config: JobConfig): V1Job {
'app.kubernetes.io/component': component,
...(labels['lc-service'] && { 'lc-service': labels['lc-service'] }),
},
...(Object.keys(podAnnotations).length > 0 && { annotations: podAnnotations }),
},
spec: {
serviceAccountName: serviceAccount,
Expand Down Expand Up @@ -117,6 +120,7 @@ export interface BuildJobConfig {
initContainers: any[];
containers: any[];
volumes?: any[];
podAnnotations?: Record<string, string>;
}

export function createBuildJob(config: BuildJobConfig): V1Job {
Expand All @@ -143,6 +147,7 @@ export function createBuildJob(config: BuildJobConfig): V1Job {
'lifecycle.io/dockerfile': config.dockerfilePath,
'lifecycle.io/ecr-repo': config.ecrRepo,
},
podAnnotations: config.podAnnotations,
initContainers: config.initContainers,
containers: config.containers,
volumes: config.volumes || [{ name: 'workspace', emptyDir: {} }],
Expand Down
139 changes: 139 additions & 0 deletions src/server/lib/nativeBuild/__tests__/buildkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,145 @@ describe('build resource precedence', () => {
});
});

describe('build pod annotations', () => {
const mockDeploy = {
deployable: { name: 'test-service' },
$fetchGraph: jest.fn(),
build: { isStatic: false },
} as any;

const baseOptions: BuildkitBuildOptions = {
ecrRepo: 'test-repo',
ecrDomain: '123456789.dkr.ecr.us-east-1.amazonaws.com',
envVars: { NODE_ENV: 'production' },
dockerfilePath: 'Dockerfile',
tag: 'v1.0.0',
revision: 'abc123def456789',
repo: 'owner/repo',
branch: 'main',
namespace: 'env-test-123',
buildId: '456',
deployUuid: 'test-service-abc123',
jobTimeout: 1800,
};

beforeEach(() => {
jest.clearAllMocks();
(getGitHubToken as jest.Mock).mockResolvedValue('github-token-123');
(shellPromise as jest.Mock).mockResolvedValue('');
(waitForJobAndGetLogs as jest.Mock).mockResolvedValue({
logs: 'Build completed successfully',
success: true,
});
});

it('includes safe-to-evict annotation on pod template by default', async () => {
(GlobalConfigService.getInstance as jest.Mock).mockReturnValue({
getAllConfigs: jest.fn().mockResolvedValue({}),
});

await buildkitBuild(mockDeploy, baseOptions);

const kubectlCalls = (shellPromise as jest.Mock).mock.calls;
const applyCall = kubectlCalls.find((call) => call[0].includes('kubectl apply'));
const fullCommand = applyCall[0];

expect(fullCommand).toContain('cluster-autoscaler.kubernetes.io/safe-to-evict: "false"');
});

it('applies custom podAnnotations from global config to pod template', async () => {
const globalConfig = {
buildDefaults: {
podAnnotations: {
'custom-annotation/team': 'platform',
'custom-annotation/cost-center': 'engineering',
},
},
};
(GlobalConfigService.getInstance as jest.Mock).mockReturnValue({
getAllConfigs: jest.fn().mockResolvedValue(globalConfig),
});

await buildkitBuild(mockDeploy, baseOptions);

const kubectlCalls = (shellPromise as jest.Mock).mock.calls;
const applyCall = kubectlCalls.find((call) => call[0].includes('kubectl apply'));
const fullCommand = applyCall[0];

expect(fullCommand).toContain('custom-annotation/team: "platform"');
expect(fullCommand).toContain('custom-annotation/cost-center: "engineering"');
expect(fullCommand).toContain('cluster-autoscaler.kubernetes.io/safe-to-evict: "false"');
});

it('applies per-service podAnnotations from options', async () => {
(GlobalConfigService.getInstance as jest.Mock).mockReturnValue({
getAllConfigs: jest.fn().mockResolvedValue({}),
});

await buildkitBuild(mockDeploy, {
...baseOptions,
podAnnotations: { 'my-org/service-tier': 'critical' },
});

const kubectlCalls = (shellPromise as jest.Mock).mock.calls;
const applyCall = kubectlCalls.find((call) => call[0].includes('kubectl apply'));
const fullCommand = applyCall[0];

expect(fullCommand).toContain('my-org/service-tier: "critical"');
expect(fullCommand).toContain('cluster-autoscaler.kubernetes.io/safe-to-evict: "false"');
});

it('per-service podAnnotations override global config podAnnotations', async () => {
const globalConfig = {
buildDefaults: {
podAnnotations: {
'my-org/team': 'platform',
},
},
};
(GlobalConfigService.getInstance as jest.Mock).mockReturnValue({
getAllConfigs: jest.fn().mockResolvedValue(globalConfig),
});

await buildkitBuild(mockDeploy, {
...baseOptions,
podAnnotations: { 'my-org/team': 'frontend' },
});

const kubectlCalls = (shellPromise as jest.Mock).mock.calls;
const applyCall = kubectlCalls.find((call) => call[0].includes('kubectl apply'));
const fullCommand = applyCall[0];

expect(fullCommand).toContain('my-org/team: "frontend"');
expect(fullCommand).not.toContain('my-org/team: "platform"');
});

it('hardcoded safe-to-evict cannot be overridden by global config or options', async () => {
const globalConfig = {
buildDefaults: {
podAnnotations: {
'cluster-autoscaler.kubernetes.io/safe-to-evict': 'true',
},
},
};
(GlobalConfigService.getInstance as jest.Mock).mockReturnValue({
getAllConfigs: jest.fn().mockResolvedValue(globalConfig),
});

await buildkitBuild(mockDeploy, {
...baseOptions,
podAnnotations: { 'cluster-autoscaler.kubernetes.io/safe-to-evict': 'true' },
});

const kubectlCalls = (shellPromise as jest.Mock).mock.calls;
const applyCall = kubectlCalls.find((call) => call[0].includes('kubectl apply'));
const fullCommand = applyCall[0];

expect(fullCommand).toContain('cluster-autoscaler.kubernetes.io/safe-to-evict: "false"');
expect(fullCommand).not.toContain('cluster-autoscaler.kubernetes.io/safe-to-evict: "true"');
});
});

describe('generateSecretArgsScript', () => {
it('returns comment when no secret keys provided', () => {
expect(generateSecretArgsScript(undefined)).toBe('# No secret env keys');
Expand Down
7 changes: 7 additions & 0 deletions src/server/lib/nativeBuild/engines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface NativeBuildOptions {
};
secretRefs?: string[];
secretEnvKeys?: string[];
podAnnotations?: Record<string, string>;
}

interface BuildEngine {
Expand Down Expand Up @@ -279,6 +280,11 @@ export async function buildWithEngine(
const serviceAccount = options.serviceAccount || buildDefaults.serviceAccount || 'native-build-sa';
const jobTimeout = options.jobTimeout || buildDefaults.jobTimeout || 2100;
const resources = options.resources || buildDefaults.resources?.[engineName] || DEFAULT_BUILD_RESOURCES[engineName];
const podAnnotations = {
...buildDefaults.podAnnotations,
...options.podAnnotations,
'cluster-autoscaler.kubernetes.io/safe-to-evict': 'false',
};

const cacheRegistry = options.cacheRegistry || buildDefaults.cacheRegistry;

Expand Down Expand Up @@ -424,6 +430,7 @@ export async function buildWithEngine(
emptyDir: {},
},
],
podAnnotations,
});

const jobYaml = yaml.dump(job, { quotingType: '"', forceQuotes: true });
Expand Down
1 change: 1 addition & 0 deletions src/server/models/yaml/YamlService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export interface Builder {
readonly requests?: Record<string, string>;
readonly limits?: Record<string, string>;
};
readonly podAnnotations?: Record<string, string>;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/server/services/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,7 @@ export default class DeployService extends BaseService {
deployUuid: deploy.uuid,
cacheRegistry: buildDefaults?.cacheRegistry,
resources: deployable.builder?.resources,
podAnnotations: deployable.builder?.podAnnotations,
secretRefs: buildSecretNames,
secretEnvKeys: Array.from(secretEnvKeys),
};
Expand Down
1 change: 1 addition & 0 deletions src/server/services/types/globalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export type BuildDefaults = {
jobTimeout?: number;
serviceAccount?: string;
cacheRegistry?: string;
podAnnotations?: Record<string, string>;
resources?: {
buildkit?: ResourceRequirements;
kaniko?: ResourceRequirements;
Expand Down