Skip to content

Commit fbaa647

Browse files
committed
refactor: add rundeck action
JIRA: INFRA-3992
1 parent 07a3df0 commit fbaa647

File tree

2 files changed

+229
-0
lines changed

2 files changed

+229
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: "Trigger Rundeck job"
2+
description: "Trigger Rundeck job through Rundeck API with given parameters"
3+
inputs:
4+
server:
5+
description: "Which server should the job be run on (rundeck server hostname)"
6+
required: true
7+
project:
8+
description: "Rundeck project name containing the job to run"
9+
required: true
10+
job-group:
11+
description: "Job group name"
12+
required: true
13+
job-name:
14+
description: "Job name"
15+
required: true
16+
vault-url:
17+
description: "Vault URL for certificate retrieval"
18+
required: true
19+
outputs:
20+
job-execution-status:
21+
description: "Job execution status"
22+
value: ${{ steps.python.outputs.execution_status }}
23+
job-execution-url:
24+
description: "Job execution URL"
25+
value: ${{ steps.python.outputs.url }}
26+
plan-summary:
27+
description: "Overall plan summary extracted from job output"
28+
value: ${{ steps.python.outputs.plan_summary }}
29+
runs:
30+
using: "composite"
31+
steps:
32+
- name: Get certificate from Vault
33+
id: cert-from-vault
34+
uses: gooddata/github-actions-public/vault/cert@master
35+
with:
36+
vault-url: ${{ inputs.vault-url }}
37+
cn: gh.action
38+
role: github-common-action
39+
vault-auth-role: common-action
40+
- name: Join certificates
41+
id: join-certs
42+
run: |
43+
echo "${{ steps.cert-from-vault.outputs.certificate }}" > cert.pem
44+
echo "${{ steps.cert-from-vault.outputs.ca_chain }}" >> cert.pem
45+
echo "${{ steps.cert-from-vault.outputs.private_key }}" >> cert.pem
46+
shell: bash
47+
- name: Run python script
48+
id: python
49+
run: |
50+
python "${{ github.action_path }}/rundeck_job_trigger.py" \
51+
--cert-path cert.pem \
52+
--server "${{ inputs.server }}" \
53+
--project "${{ inputs.project }}" \
54+
--job-group "${{ inputs.job-group }}" \
55+
--job-name "${{ inputs.job-name }}"
56+
shell: bash
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
# Copyright: (c) 2025, GoodData
4+
5+
import requests
6+
import time
7+
import sys
8+
import os
9+
import json
10+
import re
11+
import argparse
12+
13+
from urllib.parse import urljoin
14+
15+
16+
# Rundeck execution statuses
17+
# Terminal statuses (execution is finished)
18+
TERMINAL_STATUSES = ['succeeded', 'failed', 'aborted', 'timedout', 'failed-with-retry', 'other']
19+
# Non-terminal statuses: 'running', 'scheduled' (execution is still active)
20+
21+
class RundeckJobTrigger(object):
22+
def __init__(self, rundeck_server, project, job_group, job_name, job_parameters, client_cert_pem):
23+
self.rundeck_server = rundeck_server
24+
self.project = project
25+
self.job_group = job_group
26+
self.job_name = job_name
27+
self.job_parameters = job_parameters
28+
self.ss = requests.Session()
29+
self.ss.cert = client_cert_pem
30+
# Set headers for JSON requests
31+
self.ss.headers.update({
32+
'Content-Type': 'application/json',
33+
'Accept': 'application/json'
34+
})
35+
36+
def find_job_id(self):
37+
"""Find job ID based on project, group, and name"""
38+
jobs_url = urljoin(self.rundeck_server, f'api/39/project/{self.project}/jobs')
39+
40+
print(f"Looking for job '{self.job_name}' in group '{self.job_group}' in project '{self.project}'")
41+
42+
jobs_req = self.ss.get(jobs_url)
43+
jobs_req.raise_for_status()
44+
45+
jobs_data = jobs_req.json()
46+
47+
for job in jobs_data:
48+
job_group = job.get('group', '') # Group can be empty string
49+
job_name = job.get('name', '')
50+
51+
if job_group == self.job_group and job_name == self.job_name:
52+
job_id = job.get('id')
53+
print(f"Found job ID: {job_id}")
54+
return job_id
55+
56+
raise Exception(f'Job not found: group="{self.job_group}", name="{self.job_name}" in project "{self.project}"')
57+
58+
def rundeck_run_job(self):
59+
# First find the job ID
60+
job_id = self.find_job_id()
61+
62+
# Rundeck API endpoint for running a job
63+
run_job_url = urljoin(self.rundeck_server, f'api/39/job/{job_id}/run')
64+
65+
# Prepare job execution request
66+
job_request = {}
67+
68+
if self.job_parameters:
69+
# Convert parameters to Rundeck options format
70+
job_request['options'] = self.job_parameters
71+
72+
print(f"Running job {job_id} ({self.job_group}/{self.job_name}) in project {self.project}")
73+
print(f"Parameters: {self.job_parameters}")
74+
75+
job_run_req = self.ss.post(run_job_url, json=job_request)
76+
job_run_req.raise_for_status()
77+
78+
execution_data = job_run_req.json()
79+
execution_id = execution_data.get('id')
80+
execution_url = execution_data.get('permalink')
81+
82+
if not execution_id:
83+
raise Exception('No execution ID returned from Rundeck')
84+
85+
print(f"Execution started with ID: {execution_id}")
86+
if execution_url:
87+
print(f"View execution: {execution_url}")
88+
89+
return self.check_execution_status(execution_id)
90+
91+
def check_execution_status(self, execution_id):
92+
execution_info_url = urljoin(self.rundeck_server, f'api/39/execution/{execution_id}')
93+
94+
print("Waiting for execution to complete...")
95+
while True:
96+
execution_req = self.ss.get(execution_info_url)
97+
execution_req.raise_for_status()
98+
99+
execution_data = execution_req.json()
100+
status = execution_data.get('status')
101+
102+
if status in TERMINAL_STATUSES:
103+
break
104+
105+
print(f"Execution status: {status}, waiting...")
106+
time.sleep(5)
107+
108+
109+
execution_result = {
110+
'job_execution_status': execution_data.get('status'),
111+
'job_execution_url': execution_data.get('permalink'),
112+
'execution_id': execution_data.get('id')
113+
}
114+
115+
return execution_result
116+
117+
118+
def parse_params(params_str):
119+
"""Parse parameters string as JSON"""
120+
if not params_str:
121+
return {}
122+
123+
try:
124+
return json.loads(params_str)
125+
except json.JSONDecodeError as e:
126+
raise ValueError(f"Invalid JSON parameters: {params_str}. Error: {e}")
127+
128+
129+
def main():
130+
parser = argparse.ArgumentParser(
131+
description='Trigger Rundeck job execution',
132+
formatter_class=argparse.RawDescriptionHelpFormatter
133+
)
134+
135+
parser.add_argument('--cert-path', required=True, help='Path to client certificate in PEM format')
136+
parser.add_argument('--server', required=True, help='Rundeck server hostname (without https://)')
137+
parser.add_argument('--project', required=True, help='Project name containing the job')
138+
parser.add_argument('--job-group', required=True, help='Job group name')
139+
parser.add_argument('--job-name', required=True, help='Job name')
140+
parser.add_argument('--params', default='{}', help='Job parameters as JSON string (e.g., \'{"key": "value"}\')')
141+
142+
args = parser.parse_args()
143+
144+
params = parse_params(args.params)
145+
146+
trigger = RundeckJobTrigger(
147+
rundeck_server=f"https://{args.server}",
148+
project=args.project,
149+
job_group=args.job_group,
150+
job_name=args.job_name,
151+
job_parameters=params,
152+
client_cert_pem=args.cert_path
153+
)
154+
155+
trigger_result = trigger.rundeck_run_job()
156+
157+
# Write outputs for GitHub Actions
158+
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
159+
print(f'execution_status={trigger_result["job_execution_status"]}', file=f)
160+
print(f'url={trigger_result["job_execution_url"]}', file=f)
161+
162+
# Check job status and exit with appropriate code
163+
job_status = trigger_result["job_execution_status"]
164+
if job_status == 'succeeded':
165+
print("Job completed successfully")
166+
sys.exit(0)
167+
else:
168+
print(f"Job failed with status: {job_status}")
169+
sys.exit(1)
170+
171+
172+
if __name__ == '__main__':
173+
main()

0 commit comments

Comments
 (0)