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 4 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,58 @@ class ExecutionSpec extends BaseContainer {
}
}

def "executions-running for list projects"() {
given:
String projectNameSuffix = "project-api-forecast"
ProjectUtils.createProjectsWithJobsScheduled(projectNameSuffix, 4, 2, client)
and:
hold 10
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 {disableScheduledAndDeleteProject("${projectNameSuffix}-${it}", [
"project.disable.schedule": "true",
"project.later.schedule.enable": "false",
"project.disable.executions": "true"
])}
}

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,61 @@ 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()}")
}
}

/**
* Disables scheduled executions for a specific project and then deletes the project.
* This method first makes a PUT request to update the project's configuration,
* specifically to disable all scheduled executions. If this operation is successful,
* it proceeds to delete the project with a DELETE request. If any of the operations fail,
* a RuntimeException is thrown.
*
* @param projectName the name of the project to be disabled and deleted. Must not be null.
* @param body a map containing the configuration to be updated in the project before deletion.
* Specifically, this map should include the necessary properties to disable
* scheduled executions. The exact contents of the map will depend on the client API and
* the project configuration.
* @throws RuntimeException if disabling scheduled executions or deleting the project fails.
* The exception contains a detailed message obtained from the server's response.
*/
void disableScheduledAndDeleteProject(String projectName, Map body) {
avelasquezr marked this conversation as resolved.
Show resolved Hide resolved
def responseDisable = client.doPutWithJsonBody("/project/${projectName}/config", body)
if (!responseDisable.successful) {
throw new RuntimeException("Failed to disable scheduled execution: ${responseDisable.body().string()}")
}
hold 5
def response = client.doDelete("/project/${projectName}")
if (!response.successful) {
throw new RuntimeException("Failed to delete project: ${response.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,27 @@ 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
}
def authorized = !apiAuthorizedForEventRead(project)
avelasquezr marked this conversation as resolved.
Show resolved Hide resolved
if (authorized) {
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