Skip to content

Commit

Permalink
Allow scheduling a (example) product via the web UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Martchus committed Sep 18, 2024
1 parent 4b4ed38 commit 3ba945a
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 7 deletions.
4 changes: 4 additions & 0 deletions assets/assetpack.def
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
< ../node_modules/ace-builds/src-min/mode-perl.js
< ../node_modules/ace-builds/src-min/mode-yaml.js
< ../node_modules/ace-builds/src-min/mode-diff.js
< ../node_modules/ace-builds/src-min/mode-ini.js

! step_edit.js
< javascripts/needleeditor.js
Expand Down Expand Up @@ -161,6 +162,9 @@
< javascripts/running.js
< javascripts/disable_status_updates.js [mode==test]

! create_tests.js
< javascripts/create_tests.js

! job_next_previous.js
< javascripts/job_next_previous.js

Expand Down
63 changes: 63 additions & 0 deletions assets/javascripts/create_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
function getNonEmptyFormParams(form) {
const formData = new FormData(form);
const queryParams = new URLSearchParams();
for (const [key, value] of formData) {
if (value.length > 0) {
queryParams.append(key, value);
}
}
return queryParams;
}

function setupAceEditor(elementID, mode) {
const element = document.getElementById(elementID);
const initialValue = element.textContent;
const editor = ace.edit(element, {
mode: mode,
maxLines: Infinity,
tabSize: 2,
useSoftTabs: true
});
editor.session.setUseWrapMode(true);
editor.initialValue = initialValue;
return editor;
}

function setupCreateTestsForm() {
window.scenarioDefinitionsEditor = setupAceEditor('create-tests-scenario-definitions', 'ace/mode/yaml');
window.settingsEditor = setupAceEditor('create-tests-settings', 'ace/mode/ini');
}

function resetCreateTestsForm() {
window.scenarioDefinitionsEditor.setValue(window.scenarioDefinitionsEditor.initialValue, -1);
window.settingsEditor.setValue(window.settingsEditor.initialValue, -1);
}

function createTests(form) {
event.preventDefault();

const scenarioDefinitions = window.scenarioDefinitionsEditor.getValue();
const queryParams = getNonEmptyFormParams(form);
window.settingsEditor
.getValue()
.split('\n')
.map(line => line.split('=', 2))
.forEach(setting => queryParams.append(setting[0].trim(), (setting[1] ?? '').trim()));
queryParams.append('async', true);
if (scenarioDefinitions.length > 0) {
queryParams.append('SCENARIO_DEFINITIONS_YAML', scenarioDefinitions);
}
$.ajax({
url: form.dataset.postUrl,
method: form.method,
data: queryParams.toString(),
success: function (response) {
const id = response.scheduled_product_id;
const url = `${form.dataset.productlogUrl}?id=${id}`;
addFlash('info', `Tests have been scheduled, checkout the <a href="${url}">product log</a> for details.`);
},
error: function (xhr, ajaxOptions, thrownError) {
addFlash('danger', 'Unable to create tests: ' + (xhr.responseJSON?.error ?? xhr.responseText ?? thrownError));
}
});
}
11 changes: 9 additions & 2 deletions lib/OpenQA/Setup.pm
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,15 @@ sub read_config ($app) {
influxdb => {
ignored_failed_minion_jobs => '',
},
carry_over => \%CARRY_OVER_DEFAULTS
);
carry_over => \%CARRY_OVER_DEFAULTS,
'test_presets/example' => {
title => 'Create example test',
info => 'Parameters to create an example test have been pre-filled in the following form. '
. 'You can simply submit the form as-is to test your openQA setup.',
casedir => 'https://github.com/os-autoinst/os-autoinst-distri-example.git',
distri => 'example',
build => 'openqa',
});

# in development mode we use fake auth and log to stderr
my %mode_defaults = (
Expand Down
1 change: 1 addition & 0 deletions lib/OpenQA/WebAPI.pm
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ sub startup ($self) {
$r->get('/search')->name('search')->to(template => 'search/search');

$r->get('/tests')->name('tests')->to('test#list');
$r->get('/tests/create')->name('tests_create')->to('test#create');
# we have to set this and some later routes up differently on Mojo
# < 9 and Mojo >= 9.11
if ($Mojolicious::VERSION > 9.10) {
Expand Down
48 changes: 47 additions & 1 deletion lib/OpenQA/WebAPI/Controller/Test.pm
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use OpenQA::Utils;
use OpenQA::Jobs::Constants;
use OpenQA::Schema::Result::Jobs;
use OpenQA::Schema::Result::JobDependencies;
use OpenQA::Utils qw(determine_web_ui_web_socket_url get_ws_status_only_url);
use OpenQA::YAML qw(load_yaml);
use OpenQA::Utils qw(determine_web_ui_web_socket_url get_ws_status_only_url testcasedir);
use Mojo::ByteStream;
use Mojo::Util 'xml_escape';
use Mojo::File 'path';
Expand Down Expand Up @@ -116,6 +117,51 @@ sub list {
my ($self) = @_;
}

sub _load_test_preset ($self, $preset_key) {
return undef unless defined $preset_key;

Check warning on line 121 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L120-L121

Added lines #L120 - L121 were not covered by tests
# avoid reading INI file again on subsequent calls
state %presets;
return $presets{$preset_key} if exists $presets{$preset_key};
$presets{$preset_key} = undef;

Check warning on line 125 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L123-L125

Added lines #L123 - L125 were not covered by tests
# read preset from an INI section [test_presets/…] or fallback to defaults assigned on setup
my $config = $self->app->config;
return undef unless my $ini_config = $config->{ini_config};
my $ini_key = "test_presets/$preset_key";

Check warning on line 129 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L127-L129

Added lines #L127 - L129 were not covered by tests
return $presets{$preset_key}
= $ini_config->SectionExists($ini_key)
? {map { ($_ => $ini_config->val($ini_key, $_)) } $ini_config->Parameters($ini_key)}
: $config->{$ini_key};

Check warning on line 133 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L132-L133

Added lines #L132 - L133 were not covered by tests
}

sub _load_scenario_definitions ($self, $preset) {
return undef if exists $preset->{scenario_definitions};
return undef unless my $casedir = testcasedir($preset->{distri}, $preset->{version});
my $defs_yaml = eval { path($casedir, 'scenario-definitions.yaml')->slurp('UTF-8') };
$preset->{scenario_definitions} = $defs_yaml;
return $self->stash(flash_error => "Unable to read scenario definitions for the specified preset: $@") if $@;
my $defs = eval { load_yaml(string => $defs_yaml) };
return $self->stash(flash_error => "Unable to parse scenario definitions for the specified preset: $@") if $@;
my $e = join("\n", @{$self->app->validate_yaml($defs, 'JobScenarios-01.yaml')});
return $self->stash(flash_error => "Unable to validate scenarios definitions of the specified preset:\n$e") if $e;
return undef unless my @products = values %{$defs->{products}};
return undef unless my @job_templates = keys %{$defs->{job_templates}};
$preset->{$_} //= $products[0]->{$_} for qw(distri version flavor arch);
$preset->{test} //= $job_templates[0];

Check warning on line 149 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L136-L149

Added lines #L136 - L149 were not covered by tests
}

sub create ($self) {
my $preset_key = $self->param('preset');
my $preset = $self->_load_test_preset($preset_key);
if (defined $preset) {
$self->stash(flash_info => $preset->{info});
$self->_load_scenario_definitions($preset);

Check warning on line 157 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L152-L157

Added lines #L152 - L157 were not covered by tests
}
elsif (defined $preset_key) {
$self->stash(flash_error => "The specified preset '$preset_key' does not exist.");

Check warning on line 160 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L160

Added line #L160 was not covered by tests
}
$self->stash(preset => ($preset // {}));

Check warning on line 162 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L162

Added line #L162 was not covered by tests
}

sub get_match_param {
my ($self) = @_;

Expand Down
3 changes: 3 additions & 0 deletions t/config.t
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ subtest 'Test configuration default modes' => sub {
$test_config->{logging}->{level} = "debug";
$test_config->{global}->{service_port_delta} = 2;
is ref delete $config->{global}->{auto_clone_regex}, 'Regexp', 'auto_clone_regex parsed as regex';
ok delete $config->{'test_presets/example'}, 'default values for example tests assigned';
is_deeply $config, $test_config, '"test" configuration';

# Test configuration generation with "development" mode
Expand All @@ -193,6 +194,7 @@ subtest 'Test configuration default modes' => sub {
$test_config->{_openid_secret} = $config->{_openid_secret};
$test_config->{global}->{service_port_delta} = 2;
delete $config->{global}->{auto_clone_regex};
delete $config->{'test_presets/example'};
is_deeply $config, $test_config, 'right "development" configuration';

# Test configuration generation with an unknown mode (should fallback to default)
Expand All @@ -203,6 +205,7 @@ subtest 'Test configuration default modes' => sub {
$test_config->{auth}->{method} = "OpenID";
$test_config->{global}->{service_port_delta} = 2;
delete $config->{global}->{auto_clone_regex};
delete $config->{'test_presets/example'};
delete $test_config->{logging};
is_deeply $config, $test_config, 'right default configuration';
};
Expand Down
2 changes: 1 addition & 1 deletion templates/webapi/layouts/flash_messages.html.ep
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
% if (my $msg = flash('info')) {
% if (my $msg = flash('info') || stash('flash_info')) {
<div class="alert alert-primary alert-dismissible fade show" role="alert">
<span><%= $msg %></span>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
Expand Down
17 changes: 14 additions & 3 deletions templates/webapi/layouts/navbar.html.ep
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
% my $current_user = current_user;
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="<%= icon_url 'logo.svg'%>" alt="openQA"></a>
Expand All @@ -9,7 +10,17 @@
<li class='nav-item' id="all_tests">
%= link_to 'All Tests' => url_for('tests') => class => 'nav-link', title => 'Lists all tests grouped by state'
</li>

% if ($current_user && $current_user->is_operator) {
<li class="nav-item dropdown" id="create-tests-action">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false"
title="Creates one or multiple tests">Create …</a>
<div class="dropdown-menu">
%= link_to 'Example test' => url_for('tests_create')->query({preset => 'example'}) => class => 'dropdown-item'
%= link_to 'Tests from scenario definitions' => url_for('tests_create') => class => 'dropdown-item'
</div>
</li>
% }
<li class="nav-item dropdown" id="job_groups">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false" data-submenu
Expand Down Expand Up @@ -42,11 +53,11 @@
<input type="search" name="q" id="global-search" class="form-control navbar-input" value="<%= $self->param('q') %>" placeholder="Type to search" aria-label="Global search input">
</form>
</li>
% if (current_user) {
% if ($current_user) {
<li class="nav-item dropdown" id="user-action">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false"
title="Contains the Activity View and various configuration pages">Logged in as <%= current_user->name %></a>
title="Contains the Activity View and various configuration pages">Logged in as <%= $current_user->name %></a>
<div class="dropdown-menu">
%= tag 'h3' => class => 'dropdown-header' => 'Operators Menu'
%= link_to 'Activity View' => url_for('activity_view') => class => 'dropdown-item' => id => 'activity_view', title => 'Gives you an overview of your current jobs'
Expand Down
78 changes: 78 additions & 0 deletions templates/webapi/test/create.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
% layout 'bootstrap';
% title $preset->{title} // 'Create tests';
% content_for 'head' => begin
%= asset 'ace.js'
%= asset 'ace.css'
%= asset 'create_tests.js'
% end
% content_for 'ready_function' => begin
setupCreateTestsForm();
% end

<h2><%= title %></h2>

<div id="flash-messages">
%= include 'layouts/flash_messages';
</div>

<form class="row g-3" method="post" onsubmit="createTests(this)" onreset="resetCreateTestsForm(this)" data-post-url="<%= url_for('apiv1_create_iso') %>" data-productlog-url="<%= url_for('admin_product_log') %>">
<div class="col-md-6">
<label for="create-tests-distri" class="form-label"><strong>Distribution (<code>DISTRI</code>)</strong></label>
<%= help_popover('Distribution' => '<p>Creates only tests for the specified distribution. This is a mandatory parameter.</p>', undef, undef, 'left') %>
<input type="text" class="form-control" id="create-tests-distri" value="<%= $preset->{distri} // '' %>" name="DISTRI">
</div>
<div class="col-md-6">
<label for="create-tests-version" class="form-label"><strong>Version (<code>VERSION</code>)</strong></label>
<%= help_popover('Version' => '<p>Creates only tests with the specified version. This is a mandatory parameter.</p>', undef, undef, 'left') %>
<input type="text" class="form-control" id="create-tests-version" value="<%= $preset->{version} // '' %>" name="VERSION">
</div>
<div class="col-md-6">
<label for="create-tests-flavor" class="form-label"><strong>Flavor (<code>FLAVOR</code>)</strong></label>
<%= help_popover('Flavor' => '<p>Creates only tests with the specified flavor (e.g. "DVD" or "NET"). This is a mandatory parameter.</p>', undef, undef, 'left') %>
<input type="text" class="form-control" id="create-tests-flavor" value="<%= $preset->{flavor} // '' %>" name="FLAVOR">
</div>
<div class="col-md-6">
<label for="create-tests-arch" class="form-label"><strong>Architecture (<code>ARCH</code>)</strong></label>
<%= help_popover('Architecture' => '<p>Creates only tests with the specified architecture. This is a mandatory parameter.</p>', undef, undef, 'left') %>
<input type="text" class="form-control" id="create-tests-arch" value="<%= $preset->{arch} // '' %>" name="ARCH">
</div>
<div class="col-md-6">
<label for="create-tests-build" class="form-label"><strong>Build (<code>BUILD</code>)</strong></label>
<%= help_popover('Build' => '<p>Sets the <code>BUILD</code> setting of all created tests.</p>', undef, undef, 'left') %>
<input type="text" class="form-control" id="create-tests-build" value="<%= $preset->{build} // '' %>" name="BUILD">
</div>
<div class="col-md-6">
<label for="create-tests-test" class="form-label"><strong>Test names (comman-separated, <code>TEST</code>)</strong></label>
<%= help_popover('Test names' => '<p>This setting allows to creates only a specific set of tests from the scenario definitions by specifying the names of the tests to create specifically.</p>', undef, undef, 'left') %>
<input type="text" class="form-control" id="create-tests-test" value="<%= $preset->{test} // '' %>" name="TEST">
</div>
<div class="col-md-6">
<label for="create-tests-casedir" class="form-label"><strong>Test repository (<code>CASEDIR</code>)</strong></label>
<%= help_popover('Test repository' => '<p>Specifies the URL of the Git repository containing tests. May also be left blank or point to a local directory.</p>',
'https://open.qa/docs/#_triggering_tests_based_on_an_any_remote_git_refspec_or_open_github_pull_request', 'the documentation', 'left') %>
<input type="text" class="form-control" id="create-tests-casedir" value="<%= $preset->{casedir} // '' %>" name="CASEDIR">
</div>
<div class="col-md-6">
<label for="create-tests-needlesdir" class="form-label"><strong>Needles repository (<code>NEEDLES_DIR</code>)</strong></label>
<%= help_popover('Test repository' => '<p>Specifies the URL of the Git repository containing needles if those are provided in a separate repository. The same rules as for the test repository apply.</p>',
undef, undef, 'left') %>
<input type="text" class="form-control" id="create-tests-needlesdir" value="<%= $preset->{needles_dir} // '' %>" name="NEEDLES_DIR">
</div>
<div class="col-md-6">
<label for="create-tests-settings" class="form-label"><strong>Additional settings</strong></label>
<%= help_popover('Test repository' => '<p>Specifies additional settings as <code>KEY=value</code>-pairs that will be assigned to each job. ' .
'Some settings also influence the job creation itself, e.g. <code>_OBSOLETE</code>.</p>',
'https://open.qa/docs/#_spawning_multiple_jobs_based_on_templates_isos_post', 'the documentation', 'left') %>
<textarea type="text" class="form-control key-value-pairs" id="create-tests-settings" rows="15"></textarea>
</div>
<div class="col-md-6">
<label for="create-tests-scenario-definitions" class="form-label"><strong>Scenario definitions</strong></label>
<%= help_popover('Scenario definitions' => '<p>A YAML document that defines sets of settings. These settings are then combined to create one or more test jobs.</p>',
'https://open.qa/docs/#scenarios_yaml', 'the documentation', 'left') %>
<textarea type="text" class="form-control" id="create-tests-scenario-definitions" rows="15"><%= $preset->{scenario_definitions} // '' %></textarea>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Create tests</button>
<button type="reset" class="btn btn-light">Reset form</button>
</div>
</form>

0 comments on commit 3ba945a

Please sign in to comment.