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

RSE-873: Scheduled forecast GUI errors #8998

Merged
merged 6 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.rundeck.util.annotations.APITest
import org.rundeck.util.common.execution.ExecutionStatus
import org.rundeck.util.common.jobs.JobUtils
import org.rundeck.util.common.WaitingTime
import org.rundeck.util.common.projects.ProjectUtils
import org.rundeck.util.container.BaseContainer

import java.time.LocalDateTime
Expand All @@ -24,7 +25,7 @@ class ExecutionSpec extends BaseContainer {
client.apiVersion = client.finalApiVersion
}

def "run command, get execution"() {
def "run command get execution"() {
when: "run a command"
def adhoc = post("/project/${PROJECT_NAME}/run/command?exec=echo+testing+execution+api", Map)
then:
Expand Down Expand Up @@ -348,6 +349,62 @@ class ExecutionSpec extends BaseContainer {
}
}

def "executions-running for list projects"() {
given:
String projectNameSuffix = "project-api-forecast"
ProjectUtils.createProjectsWithJobsScheduled(projectNameSuffix, 4, 2, client)
and:
assert ProjectUtils.projectCountExecutions("*", 1, client)
when:
def response1 = doGet("/project/*/executions/running?includePostponed=true")
def response2 = doGet("/project/${projectNameSuffix}-1/executions/running?includePostponed=true")
def response3 = doGet("/project/${projectNameSuffix}-1,${projectNameSuffix}-2/executions/running?includePostponed=true")
def response4 = doGet("/project/${projectNameSuffix}-1,${projectNameSuffix}-2,${projectNameSuffix}-3/executions/running?includePostponed=true")
then:
verifyAll {
jsonValue(response1.body()).executions.size() >= 1
jsonValue(response2.body()).executions.size() >= 1
jsonValue(response3.body()).executions.size() >= 1
jsonValue(response4.body()).executions.size() >= 1
}
cleanup:
(2..4).each {
updateConfigurationProject("${projectNameSuffix}-${it}", [
"project.disable.schedule": "true",
"project.later.schedule.enable": "false",
"project.disable.executions": "true"
])
hold 5 //Wait until the executions stop
deleteProject("${projectNameSuffix}-${it}")
}
}

def "executions-running when project is disabled"() {
given:
deleteProject("project-api-forecast-1")
when:
def response = doGet("/project/project-api-forecast-1/executions/running?includePostponed=true")
then:
verifyAll {
!response.successful
response.code() == 404
def json2 = jsonValue(response.body())
json2.errorCode == 'api.error.project.disabled'
}
}

def "executions-running when project is deleted"() {
when:
def response = doGet("/project/any-api-project/executions/running?includePostponed=true")
then:
verifyAll {
!response.successful
response.code() == 404
def json2 = jsonValue(response.body())
json2.errorCode == 'api.error.item.doesnotexist'
}
}

void testExecQuery(String xargs = null, Integer expect = null) {
def url = "/project/test-executions-query/executions"
def response = doGet(xargs ? "${url}?${xargs}" : url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,6 @@ class JobUtils {
// Throw an exception if the import failed
throw new IllegalArgumentException("Job import failed. HTTP Status Code: " + responseImport.code());
}
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.rundeck.util.common.projects

import org.rundeck.util.common.jobs.JobUtils
import org.rundeck.util.container.RdClient

class ProjectUtils {

/**
* Creates projects with scheduled jobs.
*
* @param suffixProjectName The suffix to be added to project names.
* @param projectsCount The number of projects to create.
* @param jobsCountsPerProject The number of jobs to schedule per project.
* @param client The RdClient object used to interact with the Rundeck API.
* @throws RuntimeException If failed to create a project.
*/
static void createProjectsWithJobsScheduled(String suffixProjectName, int projectsCount, int jobsCountsPerProject, RdClient client) {
(1..projectsCount).each {it ->
def projectName = "${suffixProjectName}-${it}".toString()
def getProject = client.doGet("/project/${projectName}")
if (getProject.code() == 404) {
def post = client.doPost("/projects", [name: projectName])
if (!post.successful) {
throw new RuntimeException("Failed to create project: ${post.body().string()}")
}
}
(1..jobsCountsPerProject).each {it2 ->
def pathFile = JobUtils.updateJobFileToImport("api-test-executions-running-scheduled.xml", projectName)
def imported = JobUtils.jobImportFile(projectName, pathFile, client)
}
}
}

/**
* Retrieves the count of running executions for a given project, waiting until the count exceeds a specified value or a timeout occurs.
*
* @param projectName The name of the project to query. Must not be null.
* @param valueMoreThan The threshold value for the count of running executions. Must be greater than zero.
* @param client The RdClient instance used to make HTTP requests. Must not be null.
* @return True if the count of running executions exceeds the specified value within the timeout period, false otherwise.
* @throws RuntimeException if fetching running executions fails or if a timeout occurs.
*/
static def projectCountExecutions = (String projectName, int valueMoreThan, RdClient client) -> {
def startTime = System.currentTimeMillis()
def timeout = 10000
def pollingInterval = 1000
while (true) {
def response = client.doGet("/project/${projectName}/executions/running?includePostponed=true")
if (!response.successful) {
throw new RuntimeException("Failed to get running executions: ${response.body().string()}")
}
def valueCount = client.jsonValue(response.body(), Map).paging.count
if (valueCount > valueMoreThan) {
return Boolean.TRUE
}
if (System.currentTimeMillis() - startTime > timeout) {
throw new RuntimeException("Timeout: No running executions found within ${timeout} milliseconds.")
}
sleep pollingInterval
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -303,17 +303,55 @@ abstract class BaseContainer extends Specification implements ClientProvider {
}
}

/**
* Deletes the specified project.
* This method sends a DELETE request to remove the project with the given name.
* If the deletion operation fails, a RuntimeException is thrown.
*
* @param projectName the name of the project to be deleted. Must not be null.
* @throws RuntimeException if the project deletion fails.
* The exception contains a detailed message obtained from the server's response.
*/
void deleteProject(String projectName) {
def response = client.doDelete("/project/${projectName}")
if (!response.successful) {
throw new RuntimeException("Failed to delete project: ${response.body().string()}")
}
}

/**
* Updates the configuration of a project with the provided settings.
*
* This method sends a PUT request to update the configuration of the specified project
* with the provided settings. The configuration data is replaced entirely with the submitted values.
*
* @param projectName The name of the project whose configuration is to be updated. Must not be null.
* @param body A map containing the configuration settings to be applied to the project.
* The content of this map should represent the entire configuration data to replace.
* The structure of the map should match the expected format for the project configuration.
* Must not be null.
* @throws RuntimeException if updating the project configuration fails.
* The exception contains a detailed message obtained from the server's response.
*/
void updateConfigurationProject(String projectName, Map body) {
def responseDisable = client.doPutWithJsonBody("/project/${projectName}/config", body)
if (!responseDisable.successful) {
throw new RuntimeException("Failed to disable scheduled execution: ${responseDisable.body().string()}")
}
}

def setupSpec() {
startEnvironment()
}

/**
* Pauses the execution for a specified number of seconds.
* This method utilizes the sleep function to pause the current thread for the given duration.
* If the thread is interrupted while sleeping, it catches the InterruptedException and logs the error.
*
* @param seconds the number of seconds to pause the execution. This value should be positive.
* @throws IllegalArgumentException if the `seconds` parameter is negative, as `Duration.ofSeconds` cannot process negative values.
*/
void hold(int seconds) {
try {
sleep Duration.ofSeconds(seconds).toMillis()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,6 @@ abstract class BasePage {
.until(ExpectedConditions.elementToBeClickable(locator))
}

WebElement waitForPresenceOfElementLocated(By locator) {
avelasquezr marked this conversation as resolved.
Show resolved Hide resolved
new WebDriverWait(context.driver, Duration.ofSeconds(30))
.until(ExpectedConditions.presenceOfElementLocated(locator))
}

boolean waitForUrlToContain(String text) {
new WebDriverWait(context.driver, Duration.ofSeconds(30))
.until(ExpectedConditions.urlContains(text))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<joblist>
<job>
<defaultTab>nodes</defaultTab>
<description></description>
<executionEnabled>true</executionEnabled>
<id>d9500676-1cba-4e67-adec-1c28ad935048</id>
<loglevel>INFO</loglevel>
<name>Simple Scheduled job</name>
<nodeFilterEditable>false</nodeFilterEditable>
<plugins />
<schedule crontab="*/4 * * ? * * *"/>
<scheduleEnabled>true</scheduleEnabled>
<multipleExecutions>true</multipleExecutions>
<sequence keepgoing='false'>
<command>
<exec>echo '1'; sleep 5</exec>
</command>
</sequence>
<uuid>xml-uuid</uuid>
</job>
</joblist>
Original file line number Diff line number Diff line change
Expand Up @@ -3578,10 +3578,26 @@ if executed in cluster mode.
def allProjects = params.project == '*'
//test valid project
if (!allProjects) {
if (!apiService.requireExists(response, frameworkService.existsFrameworkProject(params.project), ['project', params.project])) {
return
def projects = params.project.split(',')
def error = projects.find { project ->
if (!apiService.requireExists(response, frameworkService.existsFrameworkProject(project), ['project', project])) {
return true
}
def disabled = frameworkService.isFrameworkProjectDisabled(project)
if (disabled) {
apiService.renderErrorFormat(response, [
status: HttpServletResponse.SC_NOT_FOUND,
code: 'api.error.project.disabled',
args: [project]
])
return true
}
if (!apiAuthorizedForEventRead(project)) {
return true
}
return false
}
if (!apiAuthorizedForEventRead(params.project)) {
if (error) {
return
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ApiProjectSelectInterceptor {
ApiProjectSelectInterceptor() {
match(uri: '/api/**')
.excludes(controller: 'project', action: 'apiProjectCreate', method: 'POST')
.excludes(controller: 'menu', action: 'apiExecutionsRunningv14')
.excludes(projectWithWildcard)
}

Expand Down