Skip to content
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ local
.tsbuildinfo
**/checkly-github-report.md
**/checkly-summary.md
**/e2e/__tests__/fixtures/empty-project/e2e-test-project-*
**/e2e/__tests__/fixtures/empty-project/e2e-test-project-*
storage
htpasswd
55 changes: 48 additions & 7 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import * as api from '../rest/api'
import { runtimes } from '../rest/api'
import config from '../services/config'
import prompts from 'prompts'
import { Flags, ux } from '@oclif/core'
import { AuthCommand } from './authCommand'
import { parseProject } from '../services/project-parser'
import { loadChecklyConfig } from '../services/checkly-config-loader'
import { runtimes } from '../rest/api'
import type { Runtime } from '../rest/runtimes'
import {
Check, AlertChannelSubscription, AlertChannel, CheckGroup, Dashboard,
MaintenanceWindow, PrivateLocation, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment,
Project, ProjectData, BrowserCheck,
AlertChannel,
AlertChannelSubscription,
BrowserCheck,
Check,
CheckGroup,
Dashboard,
MaintenanceWindow,
PrivateLocation,
PrivateLocationCheckAssignment,
PrivateLocationGroupAssignment,
Project,
ProjectData,
ProjectPayload,
} from '../constructs'
import chalk from 'chalk'
import { splitConfigFilePath, getGitInformation } from '../services/util'
import { getGitInformation, splitConfigFilePath } from '../services/util'
import commonMessages from '../messages/common-messages'
import { ProjectDeployResponse } from '../rest/projects'
import { uploadSnapshots } from '../services/snapshot-service'
import { DeployPreview } from '../services/deploy-preview'
import { TableCli } from '../services/table-cli'

// eslint-disable-next-line no-restricted-syntax
enum ResourceDeployStatus {
Expand Down Expand Up @@ -136,7 +148,8 @@ export default class Deploy extends AuthCommand {
try {
const { data } = await api.projects.deploy({ ...projectPayload, repoInfo }, { dryRun: preview, scheduleOnDeploy })
if (preview || output) {
this.log(this.formatPreview(data, project))
const preview = await this.formatPreview(data, project, projectPayload)
this.log(preview)
}
if (!preview) {
await ux.wait(500)
Expand All @@ -161,7 +174,11 @@ export default class Deploy extends AuthCommand {
}
}

private formatPreview (previewData: ProjectDeployResponse, project: Project): string {
private async formatPreview (
previewData: ProjectDeployResponse,
project: Project,
projectPayload: ProjectPayload,
): Promise<string> {
// Current format of the data is: { checks: { logical-id-1: 'UPDATE' }, groups: { another-logical-id: 'CREATE' } }
// We convert it into update: [{ logicalId, resourceType, construct }, ...], create: [], delete: []
// This makes it easier to display.
Expand Down Expand Up @@ -257,10 +274,34 @@ export default class Deploy extends AuthCommand {
output.push('')
}
if (sortedUpdating.length) {
const deployPreviewInst = new DeployPreview(projectPayload)
const deployPreviewDiff = await deployPreviewInst.getDiff()
const table = new TableCli<{
paramName: string;
currentValue: string;
newValue: string;
}>()
output.push(chalk.bold.magenta('Update and Unchanged:'))
for (const { logicalId, construct } of sortedUpdating) {
output.push(` ${construct.constructor.name}: ${logicalId}`)
}
deployPreviewDiff.forEach((resource) => {
if (resource.diffResult) {
output.push(` ${resource.resourceType}: ${resource.logicalId}`)
const outputTable = table.drawTable(
Object.entries(resource.diffResult).map(([key, value]) => {
return {
paramName: key,
currentValue: value?.[0] !== undefined ? String(value[0]) : '',
newValue: value?.[1] !== undefined ? String(value[1]) : '',
}
}),
['paramName', 'currentValue', 'newValue'],
['Param Name', 'Current Value', 'New Value'],
)
output.push(outputTable.join('\n'))
}
})
output.push('')
}
if (skipping.length) {
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/constructs/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,8 @@ export class Session {
return Session.privateLocations
}
}

export type ProjectPayload = {
project: Pick<Project, 'logicalId' | 'name' | 'repoUrl'>
resources: ResourceSync[]
}
39 changes: 39 additions & 0 deletions packages/cli/src/rest/alert-channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { AxiosInstance } from 'axios'

interface Subscription {
id: number;
checkId: string;
activated: boolean;
groupId: string | null;
}

export interface AlertChannelApi {
id: number;
type: string;
config: {
number?: string;
address?: string;
};
created_at: string;
updated_at: string | null;
sendRecovery: boolean;
sendFailure: boolean;
sendDegraded: boolean;
sslExpiry: boolean;
sslExpiryThreshold: number;
autoSubscribe: boolean;
subscriptions: Subscription[];
}

class AlertChannels {
protected api: AxiosInstance
constructor (api: AxiosInstance) {
this.api = api
}

getAll () {
return this.api.get<AlertChannelApi[]>('/v1/alert-channels')
}
}

export default AlertChannels
2 changes: 2 additions & 0 deletions packages/cli/src/rest/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import TestSessions from './test-sessions'
import EnvironmentVariables from './environment-variables'
import HeartbeatChecks from './heartbeat-checks'
import ChecklyStorage from './checkly-storage'
import AlertChannels from './alert-channels'

export function getDefaults () {
const apiKey = config.getApiKey()
Expand Down Expand Up @@ -100,3 +101,4 @@ export const testSessions = new TestSessions(api)
export const environmentVariables = new EnvironmentVariables(api)
export const heartbeatCheck = new HeartbeatChecks(api)
export const checklyStorage = new ChecklyStorage(api)
export const alertChannels = new AlertChannels(api)
1 change: 1 addition & 0 deletions packages/cli/src/rest/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface ResourceSync {
member: boolean,
payload: any,
}

export interface ProjectSync {
project: Project,
resources: Array<ResourceSync>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface CompareObjectsWithExistingKeysCase {
input: {
obj1: object
obj2: object
}
expected: Record<string, [any, any]> | null
}

export const compareObjectsWithExistingKeysCases: CompareObjectsWithExistingKeysCase[] = [
{
input: {
obj1: { a: 1, b: { c: 2, d: 3 } },
obj2: { a: 1, b: { c: 2, d: 4 } },
},
expected: { 'b.d': [4, 3] },
},
{
input: {
obj1: { a: 1, b: 2 },
obj2: { a: 1, b: 3 },
},
expected: { b: [3, 2] },
},
{
input: {
obj1: { a: { b: { c: 1 } } },
obj2: { a: { b: { c: 2 } } },
},
expected: { 'a.b.c': [2, 1] },
},
{
input: {
obj1: { a: 1, b: 2 },
obj2: { a: 1, b: 2 },
},
expected: null,
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export interface UniqValFromArrByKeyCase {
input: {
arr: any[]
key: string
}
expected: any[]
}

export const uniqValFromArrByKeyCases: UniqValFromArrByKeyCase[] = [
{
input: {
arr: [
{ value1: 'alert-channel', id: 1 },
{ value1: 'check', id: 2 },
{ value1: 'alert-channel', id: 3 },
],
key: 'value1',
},
expected: ['alert-channel', 'check'],
},
{
input: {
arr: [
{ value2: 'A', value: 10 },
{ value2: 'B', value: 20 },
{ value2: 'A', value: 30 },
],
key: 'value2',
},
expected: ['A', 'B'],
},
{
input: {
arr: [
{ value2: 'A', value: 10 },
{ value2: 'B', value: 20 },
{ value2: 'A', value: 30 },
],
key: 'value',
},
expected: [10, 20, 30],
},
]
22 changes: 21 additions & 1 deletion packages/cli/src/services/__tests__/util.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as path from 'path'
import { pathToPosix, isFileSync } from '../util'
import { pathToPosix, isFileSync, uniqValFromArrByKey, compareObjectsWithExistingKeys } from '../util'
import { uniqValFromArrByKeyCases } from './__testcases__/uniqvalromarrbykey.case'
import { compareObjectsWithExistingKeysCases } from './__testcases__/compareobjectswithexistingkeys.case'

describe('util', () => {
describe('pathToPosix()', () => {
Expand All @@ -21,4 +23,22 @@ describe('util', () => {
expect(isFileSync('some random string')).toBeFalsy()
})
})

describe('uniqValFromArrByKey()', () => {
uniqValFromArrByKeyCases.forEach(({ input, expected }, index) => {
it(`should return unique values from array grouped by key for test case ${index + 1}`, () => {
const result = uniqValFromArrByKey(input.arr, input.key)
expect(result).toEqual(expected)
})
})
})

describe('compareObjectsWithExistingKeys()', () => {
compareObjectsWithExistingKeysCases.forEach(({ input, expected }, index) => {
it(`should compare objects and return differences for test case ${index + 1}`, () => {
const result = compareObjectsWithExistingKeys(input.obj1, input.obj2)
expect(result).toEqual(expected)
})
})
})
})
57 changes: 57 additions & 0 deletions packages/cli/src/services/deploy-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as api from '../rest/api'
import { DiffResult, utilsService } from './util'
import { ProjectData, ProjectPayload } from '../constructs'
import { ResourceSync } from '../rest/projects'
import { AlertChannelApi } from '../rest/alert-channels'

export type ResourcesTypes = keyof ProjectData

export interface DeployPreviewDiff {
resourceType: ResourcesTypes
logicalId: string
diffResult: DiffResult
}

export class DeployPreview {
readonly resources: ResourceSync[] = []
private serverStateAlertChannel: AlertChannelApi[] = []
constructor (projectPayload: ProjectPayload) {
this.resources = projectPayload.resources
}

private async getServerStateByResourceType (resourceType: ResourcesTypes) {
if (resourceType === 'alert-channel') {
const { data: serverStateAlertChannel } = await this.getAlertChannelsServerState()
this.serverStateAlertChannel = serverStateAlertChannel
}
}

public getUniqueResourceType (): string[] {
return utilsService.uniqValFromArrByKey(this.resources, 'type')
}

private getAlertChannelsServerState () {
return api.alertChannels.getAll()
}

private getResourcesAndServerStateDiff (resource: ResourceSync): DeployPreviewDiff {
let diffResult: DiffResult = null
if (resource.type === 'alert-channel' && this.serverStateAlertChannel.length) {
const serverStateItem = this.serverStateAlertChannel.find((item) => item.type === resource.payload.type)
if (serverStateItem) {
diffResult = utilsService.compareObjectsWithExistingKeys(resource.payload, serverStateItem)
}
}
return {
resourceType: resource.type as ResourcesTypes,
logicalId: resource.logicalId,
diffResult,
}
}

public async getDiff (): Promise<DeployPreviewDiff[]> {
const resourcesTypes = this.getUniqueResourceType() as ResourcesTypes[]
await Promise.all(resourcesTypes.map(this.getServerStateByResourceType.bind(this)))
return this.resources.map(this.getResourcesAndServerStateDiff.bind(this))
}
}
Loading