Skip to content
Open
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
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 17 additions & 8 deletions src/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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"]:

Expand All @@ -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,
Expand All @@ -67,24 +76,24 @@ 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,
job_schedule.every_n_minutes,
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
Expand Down
4 changes: 2 additions & 2 deletions src/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 7 additions & 2 deletions src/sql-deployment-tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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}")
30 changes: 15 additions & 15 deletions test/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down