diff --git a/README.md b/README.md index 97f33f1..7a765ed 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,54 @@ Secrets/tokens can then be injected at deployment time using the sql-deployment-tools deploy --replacement-tokens '{"SECRET_VALUE": "***"}' ``` +### Deploy a SQL Agent Job ONLY + +You don't always have to provide an *.ispac file to use this tool. +You can use this tool to deploy an agent job on its own. + +#### Example configuration for an Agent Only Solution + +```toml +# Required only if any of your job steps point to SSIS Packages +project = "My Integration Services Project" + +# Required only if any of your job steps point to SSIS Packages +folder = "def" + +# Required only if any of your job steps point to SSIS Packages +environment = "default" + +[job] +name = "whatever" +description = "cool" +enabled = true +notification_email_address = "{NotificationEmailAddress}" + +[[job.steps]] +name = "todo" +type = "T-SQL" +tsql_command = "UPDATE foo SET bar = 'foobar'" + +[[job.steps]] +name = "2" +type = "T-SQL" +tsql_command = "SELECT TOP 10 * FROM sys.objects" + +[[job.schedules]] +name = "name1" +every_n_minutes = 12 + +[[job.schedules]] +name = "name2" +every_n_minutes = 111 +``` + +Then run the following command: + +```bash +sql-deployment-tools deploy --replacement-tokens '{"SECRET_VALUE": "***"}' +``` + ### Example Connection String ```text diff --git a/src/deploy.py b/src/deploy.py index 651011e..1c56e4c 100644 --- a/src/deploy.py +++ b/src/deploy.py @@ -11,8 +11,6 @@ def deploy_ssis( project_name = ssis_deployment.project folder_name = ssis_deployment.folder environment_name = ssis_deployment.environment - job_name = ssis_deployment.job.name - job_description = ssis_deployment.job.description db.ssis_create_folder(folder_name) @@ -34,9 +32,20 @@ def deploy_ssis( is_sensitive=parameter.sensitive, ) + +def deploy_agent_job(connection_string: str, agent_deployment: SsisDeployment): + + db = Database(connection_string) + + job_name = agent_deployment.job.name + job_description = agent_deployment.job.description + folder_name = agent_deployment.folder if agent_deployment.folder else None + project_name = agent_deployment.project if agent_deployment.project else None + environment = agent_deployment.environment if agent_deployment.environment else None + db.agent_create_job(job_name, job_description) - for job_step in ssis_deployment.job.steps: + for job_step in agent_deployment.job.steps: if job_step._type not in ["SSIS", "T-SQL"]: @@ -52,7 +61,7 @@ def deploy_ssis( folder_name, project_name, job_step.ssis_package, - environment_name, + environment, job_step.retry_attempts, job_step.retry_interval, job_step.proxy, @@ -67,7 +76,7 @@ def deploy_ssis( job_step.retry_interval, ) - for job_schedule in ssis_deployment.job.schedules: + for job_schedule in agent_deployment.job.schedules: db.agent_create_job_schedule_occurs_every_n_minutes( job_name, job_schedule.name, @@ -75,16 +84,16 @@ def deploy_ssis( job_schedule.start_time, ) - if ssis_deployment.job.notification_email_address: + if agent_deployment.job.notification_email_address: try: - db.agent_create_operator(ssis_deployment.job.notification_email_address) + db.agent_create_operator(agent_deployment.job.notification_email_address) except SqlAgentOperatorException as ex: # TODO: log a warning properly, not just print the message print(ex) try: db.agent_create_notification( - job_name, ssis_deployment.job.notification_email_address + job_name, agent_deployment.job.notification_email_address ) except SqlAgentOperatorException as ex: # TODO: log a warning properly, not just print the message diff --git a/src/model.py b/src/model.py index 326fd15..38bd3ca 100644 --- a/src/model.py +++ b/src/model.py @@ -76,8 +76,8 @@ class Job: @dataclass_json @dataclass class SsisDeployment: - project: str - folder: str + project: typing.Optional[str] = None + folder: typing.Optional[str] = None job: Job = field(default_factory=dict) parameters: typing.List[Parameter] = field(default_factory=list) environment: str = "default" diff --git a/src/sql-deployment-tools.py b/src/sql-deployment-tools.py index 6800b12..4e5cccf 100644 --- a/src/sql-deployment-tools.py +++ b/src/sql-deployment-tools.py @@ -4,7 +4,7 @@ import sys from src.config import ConfigurationError, load_configuration -from src.deploy import deploy_ssis +from src.deploy import deploy_agent_job, deploy_ssis if __name__ == "__main__": parent_parser = argparse.ArgumentParser(description="SSIS Deployment Helper") @@ -85,7 +85,12 @@ configuration = configuration.format(**args.replacement_tokens) ssis_deployment = load_configuration(configuration) - deploy_ssis(connection_string, args.ispac, ssis_deployment) + + if args.ispac: + deploy_ssis(connection_string, args.ispac, ssis_deployment) + + if ssis_deployment.job is not None: + deploy_agent_job(connection_string, ssis_deployment) else: raise NotImplementedError(f"Command not supported: {args.command}") diff --git a/test/test_model.py b/test/test_model.py index 39b5443..d6769ed 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -231,45 +231,45 @@ def test_SsisDeployment_parameter_count_is_set_correctly(self): actual = len(load_configuration(toml.dumps(TEST_CONFIG)).parameters) assert actual == expected - def test_SsisDeployment_throws_an_exception_when_project_name_is_not_set(self): + def test_SsisDeployment_throws_no_exception_when_project_name_is_not_set(self): """ - Test project is not optional. + Test project is optional. """ config = copy.deepcopy(TEST_CONFIG) config["project"] = None - with pytest.raises(ConfigurationError): - load_configuration(toml.dumps(config)) + actual = load_configuration(toml.dumps(config)) + assert_type(actual, SsisDeployment) - def test_SsisDeployment_throws_an_exception_when_project_is_not_in_config(self): + def test_SsisDeployment_throws_no_exception_when_project_is_not_in_config(self): """ - Test project is not optional. + Test project is optional. """ config = copy.deepcopy(TEST_CONFIG) del config["project"] - with pytest.raises(ConfigurationError): - load_configuration(toml.dumps(config)) + actual = load_configuration(toml.dumps(config)) + assert_type(actual, SsisDeployment) - def test_SsisDeployment_throws_an_exception_when_folder_is_not_set(self): + def test_SsisDeployment_throws_no_exception_when_folder_is_not_set(self): """ - Test folder is not optional. + Test folder is optional. """ config = copy.deepcopy(TEST_CONFIG) config["folder"] = None - with pytest.raises(ConfigurationError): - load_configuration(toml.dumps(config)) + actual = load_configuration(toml.dumps(config)) + assert_type(actual, SsisDeployment) - def test_SsisDeployment_throws_an_exception_when_folder_is_in_config(self): + def test_SsisDeployment_throws_no_exception_when_folder_is_in_config(self): """ Test folder is not optional. """ config = copy.deepcopy(TEST_CONFIG) del config["folder"] - with pytest.raises(ConfigurationError): - load_configuration(toml.dumps(config)) + actual = load_configuration(toml.dumps(config)) + assert_type(actual, SsisDeployment) def test_SsisDeployment_environment_is_optional(self): """