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

Hw2024/job history #9033

Closed
wants to merge 11 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ List<ImportedJob<J>> decodeFormat(String format, java.io.InputStream inputStream
*/
String exportAsXml(List<J> list);

String exportAsJson(List<J> list);

Map jobMapToXMap(Map map, boolean preserveUuid, String replaceId, String stripJobRef);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import org.rundeck.app.jobs.options.RemoteUrlAuthenticationType
import rundeck.data.job.query.RdJobQueryInput
import rundeck.data.util.OptionsParserUtil
import rundeck.services.*
import rundeck.services.component.JobHistoryService
import rundeck.services.optionvalues.OptionValuesService

import javax.servlet.http.HttpServletResponse
Expand Down Expand Up @@ -124,6 +125,7 @@ class ScheduledExecutionController extends ControllerBase{
ConfigurationService configurationService
JobDataProvider jobDataProvider
ReferencedExecutionDataProvider referencedExecutionDataProvider
JobHistoryService jobHistoryService


def index = { redirect(controller:'menu',action:'jobs',params:params) }
Expand Down Expand Up @@ -5591,6 +5593,57 @@ return.''',
}
return apiJobExecutionsResult(true)
}

/**
* API: /api/job/{id}/history , version 1
*/
def apiJobHistory() {
if (!apiService.requireApi(request, response)) {
return
}
if (!apiService.requireParameters(params, response, ['id'])) {
return
}

response.contentType = 'application/json;charset=UTF-8'
response.outputStream.withWriter('UTF-8') { writer ->
rundeckJobDefinitionManager.exportAs("json", jobHistoryService.getJobHistory(params.id), writer)
}
flush(response)
}

/**
* API: /api/job/{id}/history , version 1
*/
def apiJobHistoryDelete() {
if (!apiService.requireApi(request, response)) {
return
}
if (!apiService.requireParameters(params, response, ['id'])) {
return
}
jobHistoryService.deleteJobHistory(params.id)

response.contentType = 'application/json;charset=UTF-8'
response.status = 200
}

/**
* API: /api/job/{id}/history , version 1
*/
def apiJobHistoryDeleteById() {
if (!apiService.requireApi(request, response)) {
return
}
if (!apiService.requireParameters(params, response, ['historyId'])) {
return
}
jobHistoryService.deleteJobHistoryById(params.historyId)

response.contentType = 'application/json;charset=UTF-8'
response.status = 200
}

/**
* non-api interface to job executions results
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ class UrlMappings {
action = [GET: 'apiJobExecutions', DELETE: 'apiJobExecutionsDelete', POST: 'apiJobRun']
}

"/api/$api_version/job/$id/history"(controller: 'scheduledExecution') {
action = [GET: 'apiJobHistory', DELETE: 'apiJobHistoryDelete']
}

"/api/$api_version/job/history/$historyId"(controller: 'scheduledExecution') {
action = [DELETE: 'apiJobHistoryDeleteById']
}

"/api/$api_version/job/$id/scm/$integration/status"(controller: 'scm', action: 'apiJobStatus')
"/api/$api_version/job/$id/scm/$integration/diff"(controller: 'scm', action: 'apiJobDiff')
"/api/$api_version/job/$id/scm/$integration/action/$actionId/input"(controller: 'scm', action: 'apiJobActionInput')
Expand Down
11 changes: 11 additions & 0 deletions rundeckapp/grails-app/domain/rundeck/JobHistory.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package rundeck

class JobHistory {

String jobUuid
String jobDefinition
String userName
Date dateCreated
Date lastUpdated

}
14 changes: 13 additions & 1 deletion rundeckapp/grails-app/domain/rundeck/ScheduledExecution.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,16 @@ class ScheduledExecution extends ExecutionContext implements JobData, EmbeddedJs

String maxMultipleExecutions
String pluginConfig
String modifierUserName
String modifiedDate
Long historyId

static transients = ['userRoles', 'adhocExecutionType', 'notifySuccessRecipients', 'notifyFailureRecipients',
'notifyStartRecipients', 'notifySuccessUrl', 'notifyFailureUrl', 'notifyStartUrl',
'crontabString', 'notifyAvgDurationRecipients', 'notifyAvgDurationUrl',
'notifyRetryableFailureRecipients', 'notifyRetryableFailureUrl', 'notifyFailureAttach',
'notifySuccessAttach', 'notifyRetryableFailureAttach',
'pluginConfigMap', 'components']
'pluginConfigMap', 'components', 'modifierUserName', 'modifiedDate', 'historyId']

static constraints = {
importFrom SharedProjectNameConstraints
Expand Down Expand Up @@ -250,6 +253,15 @@ class ScheduledExecution extends ExecutionContext implements JobData, EmbeddedJs
map.scheduleEnabled = hasScheduleEnabled()
map.executionEnabled = hasExecutionEnabled()
map.nodeFilterEditable = hasNodeFilterEditable()
if(modifierUserName){
map.modifierUserName = modifierUserName
}
if(modifiedDate){
map.modifiedDate = modifiedDate
}
if(historyId){
map.historyId = historyId
}

if(groupPath){
map.group=groupPath
Expand Down
1 change: 1 addition & 0 deletions rundeckapp/grails-app/migrations/changelog.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,5 @@ databaseChangeLog = {
include file: 'core/DBChangelogPrimaryKey.groovy'
include file: 'core/BaseReportSpi.groovy'
include file: 'core/RemoveFilters-5.0.groovy'
include file: 'core/JobHistory.groovy'
}
39 changes: 39 additions & 0 deletions rundeckapp/grails-app/migrations/core/JobHistory.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
databaseChangeLog = {

changeSet(author: "rundeckuser (generated)", id: "5.2.0-job-history-creation") {
preConditions(onFail: "MARK_RAN"){
not{
tableExists (tableName:"job_history")
}
}
createTable(tableName: "job_history") {
column(autoIncrement: "true", name: "id", type: '${number.type}') {
constraints(nullable: "false", primaryKey: "true", primaryKeyName: "job_history_PK")
}

column(name: "version", type: '${number.type}') {
constraints(nullable: "false")
}

column(name: "job_uuid", type: '${varchar255.type}') {
constraints(nullable: "false")
}

column(name: "job_definition", type: '${bytearray.type}'){
constraints(nullable: "false")
}

column(name: "user_name", type: '${varchar255.type}') {
constraints(nullable: "false")
}

column(name: "date_created", type: '${timestamp.type}') {
constraints(nullable: "false")
}

column(name: "last_updated", type: '${timestamp.type}') {
constraints(nullable: "false")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package rundeck.services.component

import com.dtolabs.rundeck.core.authorization.AuthContext
import com.dtolabs.rundeck.core.authorization.UserAndRolesAuthContext
import org.rundeck.app.components.RundeckJobDefinitionManager
import org.rundeck.app.components.jobs.JobDefinitionComponent
import org.springframework.context.ApplicationContextAware
import rundeck.JobHistory
import rundeck.ScheduledExecution

import java.text.DateFormat
import java.text.SimpleDateFormat

class JobHistoryService implements JobDefinitionComponent, ApplicationContextAware{

RundeckJobDefinitionManager rundeckJobDefinitionManager
static final String componentName = "JobHistory"
static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"

@Override
String getName() {
return componentName
}

@Override
Map exportCanonicalMap(Map jobDataMap) {
return null
}

@Override
Map exportXMap(Map jobXMap) {
return null
}

@Override
Map importXMap(Map jobXMap, Map partialMap) {
return null
}

@Override
Object importCanonicalMap(Object job, Map jobDataMap) {
return null
}

@Override
Object updateJob(Object job, Object imported, Object associate, Map params) {
return null
}

@Override
void persist(Object job, Object associate, UserAndRolesAuthContext authContext) {

}

@Override
void wasPersisted(Object job, Object associate, UserAndRolesAuthContext authContext) {
saveJobHistory(job, authContext.getUsername())
}

@Override
void willDeleteJob(Object job, AuthContext authContext) {

}

@Override
void didDeleteJob(Object job, AuthContext authContext) {
deleteJobHistory(job.uuid)
}

/**
* It saves the job definition as yaml
* @param scheduledExecution
* @param user
*/
void saveJobHistory(ScheduledExecution scheduledExecution, String user) {
def jobDef = rundeckJobDefinitionManager.exportAsJson([scheduledExecution])
JobHistory jh = new JobHistory()
jh.userName = user
jh.jobDefinition = jobDef
jh.jobUuid = scheduledExecution.uuid
jh.save()
}

/**
* It removes the history when a job gets deleted
* @param jobUuid
*/
void deleteJobHistory(String jobUuid){
JobHistory.executeUpdate("delete JobHistory jh where jh.jobUuid = :jobUuid", [jobUuid:jobUuid])
}

void deleteJobHistoryById(String historyId){
JobHistory.findById(historyId.toInteger()).delete(flush:true)
}

/**
* It retrieves all job histories and then parse the json stored in the DB to an actual ScheduledExecution
* It adds the history parameters to the scheduledExecution so it can be shown in the request
* @param jobUuid
* @return
*/
def getJobHistory(String jobUuid){
def histories = []
DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT)
JobHistory.findAllByJobUuid(jobUuid, [order: "dateCreated"]).each {
def scheduleDefs = rundeckJobDefinitionManager.decodeFormat("json", it.jobDefinition)
scheduleDefs[0].job.modifierUserName = it.userName
scheduleDefs[0].job.modifiedDate = dateFormat.format(it.dateCreated)
scheduleDefs[0].job.historyId = it.id
histories.add(scheduleDefs[0].job)
}
return histories
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ class RundeckJobDefinitionManager implements JobDefinitionManager<ScheduledExecu
exportAs('yaml', list)
}

@Override
String exportAsJson(List<ScheduledExecution> list) {
exportAs('json', list)
}

@Override
String exportAs(String format, List<ScheduledExecution> list) {
def writer = new StringWriter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import org.rundeck.app.jobs.options.ApiTokenReporter
import org.rundeck.app.jobs.options.JobOptionConfigRemoteUrl
import org.rundeck.app.jobs.options.RemoteUrlAuthenticationType
import rundeck.services.*
import rundeck.services.component.JobHistoryService
import rundeck.services.feature.FeatureService
import rundeck.services.optionvalues.OptionValuesService
import spock.lang.Unroll
Expand All @@ -75,7 +76,7 @@ import java.lang.annotation.Annotation
*/
class ScheduledExecutionControllerSpec extends RundeckHibernateSpec implements ControllerUnitTest<ScheduledExecutionController>{

List<Class> getDomainClasses() { [ScheduledExecution, Option, Workflow, CommandExec, Execution, JobExec, ReferencedExecution, ScheduledExecutionStats] }
List<Class> getDomainClasses() { [ScheduledExecution, Option, Workflow, CommandExec, Execution, JobExec, ReferencedExecution, ScheduledExecutionStats, JobHistory] }

def setup() {
mockCodec(URIComponentCodec)
Expand Down Expand Up @@ -120,6 +121,35 @@ class ScheduledExecutionControllerSpec extends RundeckHibernateSpec implements C
'actionMenuFragment' | RundeckAccess.Job.AUTH_APP_READ_OR_VIEW
}

def "job history"() {
given:
def jobUuid = "13ffa515-98be-4988-8e80-af0ebabf0bb1"
controller.apiService = Mock(ApiService){
1 * requireApi(_,_) >> true
1 * requireParameters(_, _, ['id']) >> true
}
JobHistory jh = new JobHistory(userName: "pepito", jobDefinition: "- defaultTab: nodes\n" +
" description: ''", dateCreated: new Date(), id: 1)
jh.save(flush:true)
controller.jobHistoryService = Mock(JobHistoryService){
getJobHistory(jobUuid) >> [jh]
}
controller.rundeckJobDefinitionManager = Mock(RundeckJobDefinitionManager){
exportAs(_,_,_)>>{
it[2].write('test output')
}
}
controller.response.format = "json"
when:
request.api_version = 45
request.method = 'GET'
params.id = jobUuid

controller.apiJobHistory()
then:
response.text == 'test output'
}

def "workflow json"() {
given:
ScheduledExecution job = new ScheduledExecution(createJobParams())
Expand Down Expand Up @@ -1615,7 +1645,7 @@ class ScheduledExecutionControllerSpec extends RundeckHibernateSpec implements C
controller.pluginService = Mock(PluginService)
controller.featureService = Mock(FeatureService)
controller.referencedExecutionDataProvider = new GormReferencedExecutionDataProvider()




Expand Down