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
9 changes: 7 additions & 2 deletions src/pages/api/v1/builds/[uuid]/services/[name]/buildLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,13 @@ async function getNativeBuildJobs(serviceName: string, namespace: string): Promi

if (startedAt) {
const startTime = new Date(startedAt).getTime();
const endTime = completedAt ? new Date(completedAt).getTime() : Date.now();
duration = Math.floor((endTime - startTime) / 1000);

if (completedAt) {
const endTime = new Date(completedAt).getTime();
duration = Math.floor((endTime - startTime) / 1000);
} else if (status === 'Active') {
duration = Math.floor((Date.now() - startTime) / 1000);
}
}

let podName: string | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,13 @@ async function getDeploymentJobs(serviceName: string, namespace: string): Promis

if (startedAt) {
const startTime = new Date(startedAt).getTime();
const endTime = completedAt ? new Date(completedAt).getTime() : Date.now();
duration = Math.floor((endTime - startTime) / 1000);

if (completedAt) {
const endTime = new Date(completedAt).getTime();
duration = Math.floor((endTime - startTime) / 1000);
} else if (status === 'Active') {
duration = Math.floor((Date.now() - startTime) / 1000);
}
}

let podName: string | undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/server/db/migrations/001_seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ export async function up(knex: Knex): Promise<any> {
INSERT INTO global_config (key, config, "createdAt", "updatedAt", "deletedAt", description) VALUES ('lifecycleDefaults', '{"defaultUUID":"dev-0","defaultPublicUrl":"dev-0.app.0env.com","cfStepType":"helm:1.1.12","ecrDomain":"${
IS_DEV ? '10.96.188.230:5000' : 'distribution.0env.com'
}","ecrRegistry":"default","buildPipeline":"","deployCluster":"lifecycle-gke","helmDeployPipeline":"replace_me"}', now(), now(), null, 'Default values for lifecycle');
INSERT INTO global_config (key, config, "createdAt", "updatedAt", "deletedAt", description) VALUES ('helmDefaults', '{"version":"3.12.0","nativeHelm":{"enabled":true,"defaultArgs":"--wait --timeout 30m","defaultHelmVersion":"3.12.0"}}', now(), now(), null, 'Default configuration for helm deployments.');
INSERT INTO global_config (key, config, "createdAt", "updatedAt", "deletedAt", description) VALUES ('helmDefaults', '{"version":"3.12.0","nativeHelm":{"enabled":true,"defaultArgs":"--wait --reset-values --timeout 30m","defaultHelmVersion":"3.12.0"}}', now(), now(), null, 'Default configuration for helm deployments.');
INSERT INTO global_config (key, config, "createdAt", "updatedAt", "deletedAt", description) VALUES ('socat-tunneller', '{"version":"3.7.2","args":"--force --timeout 60m0s --wait","action":"install","chart":{"name":"isotoma/socat-tunneller","repoUrl":" https://isotoma.github.io/charts","version":"0.2.0","values":[],"valueFiles":[]},"label":"podAnnotations","tolerations":"tolerations","affinity":"affinity","nodeSelector":"nodeSelector"}', now(), now(), null, 'soca-tunneller configuration for db-tunnels with helm');
INSERT INTO global_config (key, config, "createdAt", "updatedAt", "deletedAt", description) VALUES ('lifecycleIgnores', '{"github":{"branches":[],"events":["closed","deleted"],"organizations":[],"botUsers":[]}}', now(), now(), null, 'Data values for Lifecycle to ignore');
INSERT INTO global_config (key, config, "createdAt", "updatedAt", "deletedAt", description) VALUES ('deletePendingHelmReleaseStep', '{"delete":true,"static_delete":true}', now(), now(), null, 'If deletePendingHelmReleaseStep is set to true');
Expand Down
1 change: 0 additions & 1 deletion src/server/lib/codefresh/__fixtures__/codefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export const deploy = {

export const checkoutStep = {
fail_fast: true,
git: CF.CHECKOUT.GIT,
repo,
revision,
stage: CF.CHECKOUT.CHECKOUT_STAGE,
Expand Down
124 changes: 124 additions & 0 deletions src/server/lib/codefresh/__tests__/kubeContextStep.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Copyright 2025 GoodRx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import mockRedisClient from 'server/lib/__mocks__/redisClientMock';
mockRedisClient();

// Mock the GlobalConfigService before importing
jest.mock('server/services/globalConfig', () => ({
__esModule: true,
default: {
getInstance: jest.fn(),
},
}));

// Mock shared/config before importing
jest.mock('shared/config', () => ({
ENVIRONMENT: 'production',
}));

import { kubeContextStep } from 'server/lib/codefresh';
import GlobalConfigService from 'server/services/globalConfig';

const MockedGlobalConfigService = GlobalConfigService as jest.MockedClass<typeof GlobalConfigService>;

describe('kubeContextStep', () => {
let mockGetAllConfigs: jest.Mock;
let mockInstance: Partial<GlobalConfigService>;

beforeEach(() => {
jest.clearAllMocks();

mockGetAllConfigs = jest.fn();
mockInstance = {
getAllConfigs: mockGetAllConfigs,
};

(MockedGlobalConfigService.getInstance as jest.Mock).mockReturnValue(mockInstance);
});

describe('gitOrg handling', () => {
const context = 'test-context';
const cluster = 'test-cluster';

it('should use the org value when app_setup.org is a valid string', async () => {
mockGetAllConfigs.mockResolvedValue({
app_setup: { org: 'test-org' },
});

const result = await kubeContextStep({ context, cluster });

expect(result).toEqual({
title: 'Set kube context',
type: 'test-org/kube-context:0.0.2',
arguments: {
app: context,
cluster,
aws_access_key_id: '${{DEPLOYMENT_AWS_ACCESS_KEY_ID}}',
aws_secret_access_key: '${{DEPLOYMENT_AWS_SECRET_ACCESS_KEY}}',
},
});
});

it('should use fallback when app_setup does not exist', async () => {
mockGetAllConfigs.mockResolvedValue({});

const result = await kubeContextStep({ context, cluster });

expect(result.type).toBe('REPLACE_ME_ORG/kube-context:0.0.2');
});

it('should use fallback when app_setup.org is undefined', async () => {
mockGetAllConfigs.mockResolvedValue({
app_setup: {},
});

const result = await kubeContextStep({ context, cluster });

expect(result.type).toBe('REPLACE_ME_ORG/kube-context:0.0.2');
});

it('should use fallback when app_setup.org is an empty string', async () => {
mockGetAllConfigs.mockResolvedValue({
app_setup: { org: '' },
});

const result = await kubeContextStep({ context, cluster });

expect(result.type).toBe('REPLACE_ME_ORG/kube-context:0.0.2');
});

it('should use fallback when app_setup.org is whitespace only', async () => {
mockGetAllConfigs.mockResolvedValue({
app_setup: { org: ' ' },
});

const result = await kubeContextStep({ context, cluster });

expect(result.type).toBe('REPLACE_ME_ORG/kube-context:0.0.2');
});

it('should trim whitespace from valid org values', async () => {
mockGetAllConfigs.mockResolvedValue({
app_setup: { org: ' test-org ' },
});

const result = await kubeContextStep({ context, cluster });

expect(result.type).toBe('test-org/kube-context:0.0.2');
});
});
});
2 changes: 0 additions & 2 deletions src/server/lib/codefresh/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export const CODEFRESH_PATH = `${TMP_PATH}/codefresh`;

export const CF = {
CHECKOUT: {
// this will be the Codefresh git org
GIT: 'REPLACE_ME_ORG',
PATH: `${TMP_PATH}/codefresh`,
CHECKOUT_STAGE: 'Checkout',
TYPE: 'git-clone',
Expand Down
7 changes: 4 additions & 3 deletions src/server/lib/codefresh/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,20 @@ export const triggerPipeline = async (
return buildId;
};

export function kubeContextStep({ context, cluster }: { context: string; cluster: string }) {
export async function kubeContextStep({ context, cluster }: { context: string; cluster: string }) {
let awsAccessKeyId = '${{DEPLOYMENT_AWS_ACCESS_KEY_ID}}';
let awsSecretAccessKey = '${{DEPLOYMENT_AWS_SECRET_ACCESS_KEY}}';

if (ENVIRONMENT === 'staging') {
awsAccessKeyId = '${{STG_AWS_ACCESS_KEY_ID}}';
awsSecretAccessKey = '${{STG_AWS_SECRET_ACCESS_KEY}}';
}

const { app_setup } = await GlobalConfigService.getInstance().getAllConfigs();
const gitOrg = (app_setup?.org && app_setup.org.trim()) || 'REPLACE_ME_ORG';
return {
title: 'Set kube context',
// this is a custom step setup to update kube context
type: 'REPLACE_ME_IF_NEEDED/kube-context:0.0.2',
type: `${gitOrg}/kube-context:0.0.2`,
arguments: {
app: context,
cluster,
Expand Down
1 change: 1 addition & 0 deletions src/server/lib/codefresh/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type ContainerBuildOptions = {
dockerfilePath: string;
ecrDomain: string;
envVars: Record<string, string>;
gitOrg?: string;
initDockerfilePath: string;
repo: string;
revision: string;
Expand Down
16 changes: 14 additions & 2 deletions src/server/lib/codefresh/utils/__tests__/generateYaml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,24 @@ describe('generateYaml', () => {

it('should generate yaml', () => {
constructBuildArgs = jest.spyOn(utils, 'constructBuildArgs').mockReturnValue([]);
generateCheckoutStepSpy = jest.spyOn(utils, 'generateCheckoutStep').mockReturnValue(checkoutStep);
generateCheckoutStepSpy = jest.spyOn(utils, 'generateCheckoutStep').mockReturnValue({
...checkoutStep,
git: 'REPLACE_ME_ORG',
});
generateBuildStepSpy = jest.spyOn(utils, 'generateBuildStep').mockReturnValue(buildStep);
generateAfterBuildStepSpy = jest.spyOn(utils, 'generateAfterBuildStep').mockReturnValue(afterBuildStep);
constructStagesSpy = jest.spyOn(utils, 'constructStages').mockReturnValue(['Checkout', 'Build', 'PostBuild']);
const result = generateYaml(generateYamlOptions);
expect(result).toEqual(yamlContent);
expect(result).toEqual({
...yamlContent,
steps: {
...yamlContent.steps,
Checkout: {
...checkoutStep,
git: 'REPLACE_ME_ORG',
},
},
});
expect(constructBuildArgs).toHaveBeenCalledWith({});
expect(generateCheckoutStepSpy).toHaveBeenCalled();
expect(generateBuildStepSpy).toHaveBeenCalled();
Expand Down
8 changes: 6 additions & 2 deletions src/server/lib/codefresh/utils/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ describe('constructBuildArgs', () => {
});

test('generateCheckoutStep', () => {
const result = utils.generateCheckoutStep(revision, repo);
expect(result).toEqual(checkoutStep);
const gitOrg = 'test-git-org';
const result = utils.generateCheckoutStep(revision, repo, gitOrg);
expect(result).toEqual({
...checkoutStep,
git: gitOrg,
});
});

test('generateBuildStep', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/server/lib/codefresh/utils/determineCheckoutStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@

import { generateCheckoutStep } from 'server/lib/codefresh/utils';

export const determineCheckoutStep = (revision: string, repo: string) => generateCheckoutStep(revision, repo);
export const determineCheckoutStep = (revision: string, repo: string, gitOrg: string) =>
generateCheckoutStep(revision, repo, gitOrg);
3 changes: 2 additions & 1 deletion src/server/lib/codefresh/utils/generateYaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const generateYaml = (options: ContainerBuildOptions) => {
const {
ecrRepo,
envVars,
gitOrg = 'REPLACE_ME_ORG',
repo,
revision,
tag,
Expand Down Expand Up @@ -81,7 +82,7 @@ export const generateYaml = (options: ContainerBuildOptions) => {
mode: 'parallel',
stages: constructStages({ initDockerfilePath, afterBuildPipelineId }),
steps: {
Checkout: generateCheckoutStep(revision, repo),
Checkout: generateCheckoutStep(revision, repo, gitOrg),
Build: generateBuildStep(buildOptions),
...(initDockerfilePath && {
InitContainer: generateBuildStep({
Expand Down
4 changes: 2 additions & 2 deletions src/server/lib/codefresh/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export const constructBuildArgs = (envVars = {}) => {
return envVarsItems?.length > 0 ? Object.keys(envVars).map((k) => `${k}=\${{${k}}}`) : [];
};

export const generateCheckoutStep = (revision: string, repo: string) => ({
export const generateCheckoutStep = (revision: string, repo: string, gitOrg: string) => ({
...CF_CHECKOUT_STEP,
working_directory: '.',
git: CF.CHECKOUT.GIT,
git: gitOrg,
repo,
revision,
type: CF.CHECKOUT.TYPE,
Expand Down
7 changes: 4 additions & 3 deletions src/server/lib/helm/helm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export async function generateCodefreshRunCommand(deploy: Deploy): Promise<strin
*/
export async function generateHelmCodefreshYamlNoCheckout(deploy: Deploy): Promise<Record<string, unknown>> {
const configs = await GlobalConfigService.getInstance().getAllConfigs();
const kubeContext = kubeContextStep({ context: deploy.uuid, cluster: configs.lifecycleDefaults.deployCluster });
const kubeContext = await kubeContextStep({ context: deploy.uuid, cluster: configs.lifecycleDefaults.deployCluster });
const helmDeploy = await helmDeployStep(deploy);
delete helmDeploy.working_directory;
kubeContext['stage'] = 'Checkout';
Expand Down Expand Up @@ -374,10 +374,11 @@ export async function generateHelmCodefreshYamlWithCheckout(deploy: Deploy): Pro

const repositoryName = deploy?.repository?.fullName;
const revision = deploy.sha;
const gitOrg = (configs?.app_setup?.org && configs.app_setup.org.trim()) || 'REPLACE_ME_ORG';

const Checkout = generateCheckoutStep(revision, repositoryName);
const Checkout = generateCheckoutStep(revision, repositoryName, gitOrg);
delete Checkout.stage;
const kubeContext = kubeContextStep({ context: deploy.uuid, cluster: configs.lifecycleDefaults.deployCluster });
const kubeContext = await kubeContextStep({ context: deploy.uuid, cluster: configs.lifecycleDefaults.deployCluster });
const addHelmReleaseDeletionStep = deploy?.build?.isStatic
? configs?.deletePendingHelmReleaseStep?.static_delete
: configs?.deletePendingHelmReleaseStep?.delete;
Expand Down
8 changes: 5 additions & 3 deletions src/server/lib/kubernetes/jobFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { V1Job } from '@kubernetes/client-node';
import { normalizeKubernetesLabelValue } from './utils';

export interface JobConfig {
name: string;
Expand Down Expand Up @@ -134,7 +135,7 @@ export function createBuildJob(config: BuildJobConfig): V1Job {
'lc-deploy-uuid': config.deployUuid,
'lc-build-id': String(config.buildId),
'git-sha': config.shortSha,
'git-branch': config.branch,
'git-branch': normalizeKubernetesLabelValue(config.branch),
'builder-engine': config.engine,
'build-method': 'native',
},
Expand All @@ -153,6 +154,7 @@ export interface HelmJobConfig {
namespace: string;
serviceAccount: string;
serviceName: string;
buildUUID: string;
isStatic: boolean;
timeout?: number;
gitUsername?: string;
Expand All @@ -174,13 +176,13 @@ export function createHelmJob(config: HelmJobConfig): V1Job {
const timeout = config.timeout || 1800; // 30 minutes default

const labels: Record<string, string> = {
'lc-uuid': config.name.split('-')[0],
'lc-uuid': config.buildUUID,
service: config.serviceName,
};

if (config.deployMetadata) {
labels['git-sha'] = config.deployMetadata.sha;
labels['git-branch'] = config.deployMetadata.branch;
labels['git-branch'] = normalizeKubernetesLabelValue(config.deployMetadata.branch);
labels['deploy-id'] = config.deployMetadata.deployId || '';
labels['deployable-id'] = config.deployMetadata.deployableId;
}
Expand Down
Loading