Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new import command to import your existing Checkly resources to the CLI [sc-23506] #1045

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0caedf4
feat: add Construct -> AST-like source code generation for all constr…
sorccu Mar 17, 2025
b38fae1
fix: avoid circular dependencies by duplicating Array code for Args
sorccu Mar 17, 2025
2c27982
refactor: rename Program.value to Program.section because it's more a…
sorccu Mar 17, 2025
a4f058b
feat: output format improvements
sorccu Mar 17, 2025
79dda1a
feat: generate all alert channels with variables that can be looked u…
sorccu Mar 18, 2025
58268e0
refactor: remove unused import
sorccu Mar 18, 2025
3bd3dd0
feat: add basic CLI commands to manage imports
sorccu Mar 21, 2025
3af6119
chore: fix lints
sorccu Mar 21, 2025
9c45217
feat: new codegen suited for API resources
sorccu Mar 21, 2025
aeb4d6e
feat: specialize alert channel codegens
sorccu Mar 21, 2025
ec18c4b
fix: wrong codegen name for a few resources
sorccu Mar 21, 2025
0a87d17
feat: output generated code
sorccu Mar 21, 2025
c42dfe1
fix: don't generate duplicate imports
sorccu Mar 21, 2025
716358d
feat: output empty arrays as a simple `[]`
sorccu Mar 21, 2025
4ef0dc1
fix: forgot to output email address in email alert
sorccu Mar 21, 2025
104b31a
refactor: change temporary default filename to .check.ts for convenience
sorccu Mar 21, 2025
5215744
refactor: all codegens now extend a Codegen class
sorccu Mar 24, 2025
42e988b
refactor: place abstract codegen under internal construct folder
sorccu Mar 24, 2025
79198b4
refactor: change .codegen.ts suffix to -codegen.ts because it's less …
sorccu Mar 24, 2025
f66b78c
chore: formatting
sorccu Mar 24, 2025
c1e3807
feat: allow checks and groups to refer to resources they are linked to
sorccu Mar 25, 2025
ebc69b0
fix(tests): help output tests need to consider the new import command
sorccu Mar 25, 2025
79ec93b
fix(tests): long line needs wrapping
sorccu Mar 25, 2025
a7cfd6a
fix: plan numbering for apply/commit was incorrectly static
sorccu Mar 25, 2025
80277f5
chore: small formatting change to avoid nitpicks
sorccu Mar 25, 2025
805a4c9
feat: sort generated objects by optional order and then by key
sorccu Mar 25, 2025
0db361b
fix: incorrect retry strategy builder for generated linear retry stra…
sorccu Mar 25, 2025
2850f6d
feat: use AlertChannel/PrivateLocation.fromId() for non-imported reso…
sorccu Mar 25, 2025
a252605
fix: generate relevant import for fromId() references
sorccu Mar 25, 2025
57927ef
fix: typo
sorccu Mar 28, 2025
7135c3f
feat: only include locked/secret for KV if true since default is false
sorccu Mar 28, 2025
36224c2
feat: skip generating default values for ApiCheck's request
sorccu Mar 28, 2025
a71c9f3
feat: handle setup/teardown snippets for ApiCheck (only as string for…
sorccu Mar 28, 2025
33e0344
feat: export all generated variables for easier usage from other files
sorccu Mar 28, 2025
15ca798
fix: remove extra line end after variable declaration, section takes …
sorccu Mar 28, 2025
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
14 changes: 14 additions & 0 deletions packages/cli/e2e/__tests__/help.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ describe('help', () => {
<value>".`)
})

it('should print import topic help', async () => {
const { stdout } = await runChecklyCli({
args: ['import', '--help'],
})
// use a 80 char line output
expect(stdout).toContain(`COMMANDS
import apply Attach imported resources into your project in a pending state.
import cancel Cancels an ongoing import plan that has not been committed yet.
import commit Permanently commit imported resources into your project.
import plan Begin the import process by creating a plan.`)
})

it('should print core and additional commands and topic', async () => {
const { stdout } = await runChecklyCli({
args: ['--help'],
Expand All @@ -36,6 +48,8 @@ describe('help', () => {
destroy Destroy your project with all its related resources.
env Manage Checkly environment variables.
help Display help for checkly.
import Import existing resources from your Checkly account to your
project.
login Login to your Checkly account or create a new one.
logout Log out and clear any local credentials.
runtimes List all supported runtimes and dependencies.
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"topics": {
"env": {
"description": "Manage Checkly environment variables."
},
"import": {
"description": "Import existing resources from your Checkly account to your project."
}
},
"helpClass": "./dist/help/help-extension",
Expand Down
96 changes: 96 additions & 0 deletions packages/cli/src/commands/import/apply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Flags, ux } from '@oclif/core'
import prompts from 'prompts'

import * as api from '../../rest/api'
import { AuthCommand } from '../authCommand'
import commonMessages from '../../messages/common-messages'
import { splitConfigFilePath } from '../../services/util'
import { loadChecklyConfig } from '../../services/checkly-config-loader'
import { ImportPlan } from '../../rest/projects'

export default class ImportApplyCommand extends AuthCommand {
static hidden = false
static description = 'Attach imported resources into your project in a pending state.'

static flags = {
config: Flags.string({
char: 'c',
description: commonMessages.configFile,
}),
}

async run (): Promise<void> {
const { flags } = await this.parse(ImportApplyCommand)
const {
config: configFilename,
} = flags

const { configDirectory, configFilenames } = splitConfigFilePath(configFilename)
const {
config: checklyConfig,
} = await loadChecklyConfig(configDirectory, configFilenames)

const {
logicalId,
} = checklyConfig

const { data: unappliedPlans } = await api.projects.findImportPlans(logicalId, {
onlyUnapplied: true,
})

const plan = await this.#selectPlan(unappliedPlans)

if (this.fancy) {
ux.action.start('Applying plan')
}

try {
await api.projects.applyImportPlan(plan.id)

if (this.fancy) {
ux.action.stop('✅ ')
}
} catch (err) {
if (this.fancy) {
ux.action.stop('❌')
}

throw err
}
}

async #selectPlan (plans: ImportPlan[]): Promise<ImportPlan> {
const choices: prompts.Choice[] = plans.map((plan, index) => ({
title: `Plan #${index + 1} from ${new Date(plan.createdAt)}`,
value: plan.id,
description: `ID: ${plan.id}`,
}))

choices.unshift({
title: 'Exit without applying',
value: 'exit',
description: 'No changes will be made.',
})

const plansById = plans.reduce((m, plan) => m.set(plan.id, plan), new Map<string, ImportPlan>())

const { planId } = await prompts({
name: 'planId',
type: 'select',
message: `Found ${plans.length} unapplied plan(s). Which one to apply?`,
choices,
})

if (planId === 'exit') {
this.log('Exiting without making any changes.')
this.exit(0)
}

const plan = plansById.get(planId)
if (plan === undefined) {
throw new Error('Bug: plan ID missing from plan map')
}

return plan
}
}
112 changes: 112 additions & 0 deletions packages/cli/src/commands/import/cancel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Flags, ux } from '@oclif/core'
import prompts from 'prompts'
import logSymbols from 'log-symbols'

import * as api from '../../rest/api'
import { AuthCommand } from '../authCommand'
import commonMessages from '../../messages/common-messages'
import { splitConfigFilePath } from '../../services/util'
import { loadChecklyConfig } from '../../services/checkly-config-loader'
import { ImportPlan } from '../../rest/projects'

export default class ImportCancelCommand extends AuthCommand {
static hidden = false
static description = 'Cancels an ongoing import plan that has not been committed yet.'

static flags = {
config: Flags.string({
char: 'c',
description: commonMessages.configFile,
}),
}

async run (): Promise<void> {
const { flags } = await this.parse(ImportCancelCommand)
const {
config: configFilename,
} = flags

const { configDirectory, configFilenames } = splitConfigFilePath(configFilename)
const {
config: checklyConfig,
} = await loadChecklyConfig(configDirectory, configFilenames)

const {
logicalId,
} = checklyConfig

const { data: cancelablePlans } = await api.projects.findImportPlans(logicalId, {
onlyUncommitted: true,
})

const plans = await this.#selectPlans(cancelablePlans)

if (this.fancy) {
ux.action.start('Canceling selected plan(s)')
}

try {
for (const plan of plans) {
await api.projects.cancelImportPlan(plan.id)
this.log(`${logSymbols.success} Canceled plan ${plan.id}`)
}

if (this.fancy) {
ux.action.stop('✅ ')
}
} catch (err) {
if (this.fancy) {
ux.action.stop('❌')
}

throw err
}
}

async #selectPlans (plans: ImportPlan[]): Promise<ImportPlan[]> {
const choices: prompts.Choice[] = plans.map(plan => ({
title: `Plan #1 from ${new Date(plan.createdAt)}`,
value: plan.id,
description: `ID: ${plan.id}`,
}))

choices.unshift({
title: 'Exit without canceling',
value: 'exit',
description: 'No changes will be made.',
})

if (plans.length > 0) {
choices.push({
title: 'Cancel all plans',
value: 'all',
description: 'All uncommitted plans will be canceled.',
})
}

const plansById = plans.reduce((m, plan) => m.set(plan.id, plan), new Map<string, ImportPlan>())

const { planId } = await prompts({
name: 'planId',
type: 'select',
message: `Found ${plans.length} cancelable plan(s). Which one to cancel?`,
choices,
})

if (planId === 'exit') {
this.log('Exiting without making any changes.')
this.exit(0)
}

if (planId === 'all') {
return plans
}

const plan = plansById.get(planId)
if (plan === undefined) {
throw new Error('Bug: plan ID missing from plan map')
}

return [plan]
}
}
104 changes: 104 additions & 0 deletions packages/cli/src/commands/import/commit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Flags, ux } from '@oclif/core'
import prompts from 'prompts'
import logSymbols from 'log-symbols'

import * as api from '../../rest/api'
import { AuthCommand } from '../authCommand'
import commonMessages from '../../messages/common-messages'
import { splitConfigFilePath } from '../../services/util'
import { loadChecklyConfig } from '../../services/checkly-config-loader'
import { ImportPlan } from '../../rest/projects'

export default class ImportCommitCommand extends AuthCommand {
static hidden = false
static description = 'Permanently commit imported resources into your project.'

static flags = {
config: Flags.string({
char: 'c',
description: commonMessages.configFile,
}),
}

async run (): Promise<void> {
const { flags } = await this.parse(ImportCommitCommand)
const {
config: configFilename,
} = flags

const { configDirectory, configFilenames } = splitConfigFilePath(configFilename)
const {
config: checklyConfig,
} = await loadChecklyConfig(configDirectory, configFilenames)

const {
logicalId,
} = checklyConfig

const { data } = await api.projects.findImportPlans(logicalId, {
onlyUncommitted: true,
})

// Uncommitted plans also include unapplied plans, filter them out.
const uncommittedPlans = data.filter(plan => {
return plan.appliedAt
})

const plan = await this.#selectPlan(uncommittedPlans)

if (this.fancy) {
ux.action.start('Committing plan')
}

try {
await api.projects.commitImportPlan(plan.id)

if (this.fancy) {
ux.action.stop('✅ ')
}
} catch (err) {
if (this.fancy) {
ux.action.stop('❌')
}

throw err
}

this.log(`${logSymbols.success} All imported plan resources are now fully managed by the Checkly CLI.`)
}

async #selectPlan (plans: ImportPlan[]): Promise<ImportPlan> {
const choices: prompts.Choice[] = plans.map((plan, index) => ({
title: `Plan #${index + 1} from ${new Date(plan.createdAt)}`,
value: plan.id,
description: `ID: ${plan.id}`,
}))

choices.unshift({
title: 'Exit without committing',
value: 'exit',
description: 'No changes will be made.',
})

const plansById = plans.reduce((m, plan) => m.set(plan.id, plan), new Map<string, ImportPlan>())

const { planId } = await prompts({
name: 'planId',
type: 'select',
message: `Found ${plans.length} applied plan(s). Which one to commit?`,
choices,
})

if (planId === 'exit') {
this.log('Exiting without making any changes.')
this.exit(0)
}

const plan = plansById.get(planId)
if (plan === undefined) {
throw new Error('Bug: plan ID missing from plan map')
}

return plan
}
}
Loading
Loading