Skip to content

Commit 1a45385

Browse files
authored
Merge pull request #62 from dbt-labs/feature/new-commands
Add the commands unlink and deactivate-jobs
2 parents 284d81d + 5c37733 commit 1a45385

4 files changed

Lines changed: 181 additions & 35 deletions

File tree

README.md

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,10 @@ With this package's approach, people don't need to learn another tool and can co
2525

2626
### Installation
2727

28-
This package uses `poetry` for dependency management.
29-
In the near future the package might be added to PyPi but for now the installation is manual, as follows:
28+
- Create a Python virtual environment and activate it
29+
- Run `pip install git+https://github.com/dbt-labs/dbt-jobs-as-code.git`
3030

31-
1. clone this repository
32-
2. run `poetry install`
33-
3. run `poetry run dbt-jobs-as-code` to see the different list of commands available
31+
The CLI is now available as `dbt-jobs-as-code`
3432

3533
### Pre-requisites
3634

@@ -43,31 +41,72 @@ The following environment variables are used to run the code:
4341

4442
The CLI comes with a few different commands
4543

46-
- `poetry run python src/main.py validate <config_file.yml>`: validates that the YAML file has the correct structure
47-
- it is possible to run the validation offline, without doing any API call
48-
- or online using `--online`, in order to check that the different IDs provided are correct
49-
- `poetry run python src/main.py plan <config_file.yml>`: returns the list of actions create/update/delete that are required to have dbt Cloud reflecting the configuration file
50-
- this command doesn't modify the dbt Cloud jobs
51-
- `poetry run python src/main.py sync <config_file.yml>`: create/update/delete jobs and env vars overwrites in jobs to align dbt Cloud with the configuration file
52-
- ⚠️ this command will modify your dbt Cloud jobs if the current configuration is different from the YAML file
53-
- `poetry run python src/main.py import-jobs --config <config_file.yml>` or `poetry run python src/main.py import-jobs --account-id <account-id>`: Queries dbt Cloud and provide the YAML definition for those jobs. It includes the env var overwrite at the job level if some have been defined
54-
- it is possible to restrict the list of dbt Cloud Job IDs by adding `... -j 101 -j 123 -j 234`
55-
- once the YAML has been retrieved, it is possible to copy/paste it in a local YAML file to create/update the local jobs definition.
56-
- to move some ui-jobs to jobs-as-code, perform the following steps:
57-
- run the command to import the jobs
58-
- copy paste the job/jobs into a YAML file
59-
- change the `import_` id of the job in the YML file to another unique identifier
60-
- rename the job in the UI to end with `[[new_job_identifier]]`
61-
- run a `plan` command to verify that no changes are required for the given job
44+
#### `validate`
45+
46+
Command: `dbt-jobs-as-code validate <config_file.yml>`
47+
48+
Validates that the YAML file has the correct structure
49+
50+
- it is possible to run the validation offline, without doing any API call
51+
- or online using `--online`, in order to check that the different IDs provided are correct
52+
53+
#### `plan`
54+
55+
Command: `dbt-jobs-as-code plan <config_file.yml>`
56+
57+
Returns the list of actions create/update/delete that are required to have dbt Cloud reflecting the configuration file
58+
59+
- this command doesn't modify the dbt Cloud jobs
60+
61+
#### `sync`
62+
63+
Command: `dbt-jobs-as-code sync <config_file.yml>`
64+
65+
Create/update/delete jobs and env vars overwrites in jobs to align dbt Cloud with the configuration file
66+
67+
- ⚠️ this command will modify your dbt Cloud jobs if the current configuration is different from the YAML file
68+
69+
#### `import-jobs`
70+
71+
Command: `dbt-jobs-as-code import-jobs --config <config_file.yml>` or `dbt-jobs-as-code import-jobs --account-id <account-id>`
72+
73+
Queries dbt Cloud and provide the YAML definition for those jobs. It includes the env var overwrite at the job level if some have been defined
74+
75+
- it is possible to restrict the list of dbt Cloud Job IDs by adding `... -j 101 -j 123 -j 234`
76+
- once the YAML has been retrieved, it is possible to copy/paste it in a local YAML file to create/update the local jobs definition.
77+
78+
To move some ui-jobs to jobs-as-code, perform the following steps:
79+
80+
- run the command to import the jobs
81+
- copy paste the job/jobs into a YAML file
82+
- change the `import_` id of the job in the YML file to another unique identifier
83+
- rename the job in the UI to end with `[[new_job_identifier]]`
84+
- run a `plan` command to verify that no changes are required for the given job
85+
86+
#### `unlink`
87+
88+
Command: `dbt-jobs-as-code unlink --config <config_file.yml>` or `dbt-jobs-as-code unlink --account-id <account-id>`
89+
90+
Unlinking jobs removes the `[[ ... ]]` part of the job name in dbt Cloud.
91+
92+
⚠️ This can't be rolled back by the tool. Doing a `unlink` followed by a `sync` will create new instances of the jobs, with the `[[<identifier>]]` part
93+
94+
- it is possible to restrict the list of jobs to unlink by adding the job identifiers to unlink `... -i import_1 -i my_job_2`
95+
96+
#### `deactivate-jobs`
97+
98+
Command: `dbt-jobs-as-code deactivate-jobs --account-id 1234 --job-id 12 --job-id 34 --job-id 56`
99+
100+
This command can be used to deactivate both the schedule and the CI triggers for dbt Cloud jobs. This can be useful when moving jobs from one project to another. When the new jobs have been created, this command can be used to deactivate the jobs from the old project.
62101

63102
### Job Configuration YAML Schema
64103

65104
The file `src/schemas/load_job_schema.json` is a JSON Schema file that can be used to verify that the YAML config files syntax is correct.
66105

67-
To use it in VSCode, install the extension `YAML` and add the following line at the top of your YAML config file (change the path if need be):
106+
To use it in VSCode, install [the extension `YAML`](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) and add the following line at the top of your YAML config file (change the path if need be):
68107

69108
```yaml
70-
# yaml-language-server: $schema=../src/schemas/load_job_schema.json
109+
# yaml-language-server: $schema=https://raw.githubusercontent.com/dbt-labs/dbt-jobs-as-code/main/src/schemas/load_job_schema.json
71110
```
72111

73112
## Running the tool as part of CI/CD

src/client/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
CustomEnvironmentVariablePayload,
99
)
1010
from src.schemas.job import JobDefinition
11-
from src.schemas import check_env_var_same
1211

1312

1413
class DBTCloud:
@@ -138,7 +137,7 @@ def get_jobs(self) -> List[JobDefinition]:
138137

139138
return [JobDefinition(**job) for job in jobs]
140139

141-
def get_job(self, job_id: int) -> Dict:
140+
def get_job(self, job_id: int) -> JobDefinition:
142141
"""Generate a Job based on a dbt Cloud job."""
143142

144143
self._check_for_creds()
@@ -150,7 +149,7 @@ def get_job(self, job_id: int) -> Dict:
150149
"Content-Type": "application/json",
151150
},
152151
)
153-
return response.json()["data"]
152+
return JobDefinition(**response.json()["data"])
154153

155154
def get_env_vars(
156155
self, project_id: int, job_id: int

src/main.py

Lines changed: 113 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
from ruamel.yaml import YAML
32
import sys
43

54
from loguru import logger
@@ -87,7 +86,6 @@ def build_change_set(config):
8786

8887
# Replicate the env vars from the YML to dbt Cloud
8988
for job in defined_jobs.values():
90-
9189
if job.identifier in mapping_job_identifier_job_id: # the job already exists
9290
job_id = mapping_job_identifier_job_id[job.identifier]
9391
all_env_vars_for_job = dbt_cloud.get_env_vars(project_id=job.project_id, job_id=job_id)
@@ -130,7 +128,6 @@ def build_change_set(config):
130128

131129
# Delete the env vars from dbt Cloud that are not in the yml
132130
for job in defined_jobs.values():
133-
134131
# we only delete env var overwrite if the job already exists
135132
if job.identifier in mapping_job_identifier_job_id:
136133
job_id = mapping_job_identifier_job_id[job.identifier]
@@ -270,11 +267,7 @@ def validate(config, online):
270267

271268
# In case deferral jobs are mentioned, check that they exist
272269
deferral_envs = set(
273-
[
274-
job.deferring_environment_id
275-
for job in defined_jobs
276-
if job.deferring_environment_id
277-
]
270+
[job.deferring_environment_id for job in defined_jobs if job.deferring_environment_id]
278271
)
279272
if deferral_envs:
280273
logger.info(f"Checking that Deferring Env IDs are valid")
@@ -342,5 +335,117 @@ def import_jobs(config, account_id, job_id):
342335
export_jobs_yml(cloud_jobs)
343336

344337

338+
@cli.command()
339+
@click.option("--config", type=click.File("r"), help="The path to your YML jobs config file.")
340+
@click.option("--account-id", type=int, help="The ID of your dbt Cloud account.")
341+
@click.option("--dry-run", is_flag=True, help="In dry run mode we don't update dbt Cloud.")
342+
@click.option(
343+
"--identifier",
344+
"-i",
345+
type=str,
346+
multiple=True,
347+
help="[Optional] The identifiers we want to unlink. If not provided, all jobs are unlinked.",
348+
)
349+
def unlink(config, account_id, dry_run, identifier):
350+
"""
351+
Unlink the YML file to dbt Cloud.
352+
All relevant jobs get the part [[...]] removed from their name
353+
"""
354+
355+
# we get the account id either from a parameter (e.g if the config file doesn't exist) or from the config file
356+
if account_id:
357+
cloud_account_id = account_id
358+
elif config:
359+
defined_jobs = load_job_configuration(config).jobs.values()
360+
cloud_account_id = list(defined_jobs)[0].account_id
361+
else:
362+
raise click.BadParameter("Either --config or --account-id must be provided")
363+
364+
# we get the account id from the config file
365+
defined_jobs = load_job_configuration(config).jobs.values()
366+
cloud_account_id = list(defined_jobs)[0].account_id
367+
368+
dbt_cloud = DBTCloud(
369+
account_id=cloud_account_id,
370+
api_key=os.environ.get("DBT_API_KEY"),
371+
base_url=os.environ.get("DBT_BASE_URL", "https://cloud.getdbt.com"),
372+
)
373+
cloud_jobs = dbt_cloud.get_jobs()
374+
selected_jobs = [job for job in cloud_jobs if job.identifier is not None]
375+
logger.info(f"Getting the jobs definition from dbt Cloud")
376+
377+
if identifier:
378+
selected_jobs = [job for job in selected_jobs if job.identifier in identifier]
379+
380+
for cloud_job in selected_jobs:
381+
current_identifier = cloud_job.identifier
382+
# by removing the identifier, we unlink the job from the YML file
383+
cloud_job.identifier = None
384+
if dry_run:
385+
logger.info(
386+
f"Would unlink/rename the job {cloud_job.id}:{cloud_job.name} [[{current_identifier}]]"
387+
)
388+
else:
389+
logger.info(
390+
f"Unlinking/Renaming the job {cloud_job.id}:{cloud_job.name} [[{current_identifier}]]"
391+
)
392+
dbt_cloud.update_job(job=cloud_job)
393+
394+
if len(selected_jobs) == 0:
395+
logger.info(f"No jobs to unlink")
396+
elif not dry_run:
397+
logger.success(f"Updated all jobs!")
398+
399+
400+
@cli.command()
401+
@click.option("--config", type=click.File("r"), help="The path to your YML jobs config file.")
402+
@click.option("--account-id", type=int, help="The ID of your dbt Cloud account.")
403+
@click.option(
404+
"--job-id",
405+
"-j",
406+
type=int,
407+
multiple=True,
408+
help="[Optional] The ID of the job to deactivate.",
409+
)
410+
def deactivate_jobs(config, account_id, job_id):
411+
"""
412+
Deactivate jobs triggers in dbt Cloud (schedule and CI/CI triggers)
413+
"""
414+
415+
# we get the account id either from a parameter (e.g if the config file doesn't exist) or from the config file
416+
if account_id:
417+
cloud_account_id = account_id
418+
elif config:
419+
defined_jobs = load_job_configuration(config).jobs.values()
420+
cloud_account_id = list(defined_jobs)[0].account_id
421+
else:
422+
raise click.BadParameter("Either --config or --account-id must be provided")
423+
424+
dbt_cloud = DBTCloud(
425+
account_id=cloud_account_id,
426+
api_key=os.environ.get("DBT_API_KEY"),
427+
base_url=os.environ.get("DBT_BASE_URL", "https://cloud.getdbt.com"),
428+
)
429+
cloud_jobs = dbt_cloud.get_jobs()
430+
431+
selected_cloud_jobs = [job for job in cloud_jobs if job.id in job_id]
432+
433+
for cloud_job in selected_cloud_jobs:
434+
if (
435+
cloud_job.triggers.git_provider_webhook
436+
or cloud_job.triggers.github_webhook
437+
or cloud_job.triggers.schedule
438+
):
439+
logger.info(f"Deactivating the job {cloud_job.id}:{cloud_job.name}")
440+
cloud_job.triggers.github_webhook = False
441+
cloud_job.triggers.git_provider_webhook = False
442+
cloud_job.triggers.schedule = False
443+
dbt_cloud.update_job(job=cloud_job)
444+
else:
445+
logger.info(f"The job {cloud_job.id}:{cloud_job.name} is already deactivated")
446+
447+
logger.success(f"Deactivated all jobs!")
448+
449+
345450
if __name__ == "__main__":
346451
cli()

src/schemas/job.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ def to_payload(self):
6464

6565
# Rewrite the job name to embed the job ID from job.yml
6666
payload = self.copy()
67-
payload.name = f"{self.name} [[{self.identifier}]]"
67+
# if there is an identifier, add it to the name
68+
# otherwise, it means that we are "unlinking" the job from the job.yml
69+
if self.identifier:
70+
payload.name = f"{self.name} [[{self.identifier}]]"
6871
return payload.json(exclude={"identifier", "custom_environment_variables"})
6972

7073
def to_load_format(self):

0 commit comments

Comments
 (0)