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

[bugfix, ui] Allow running jobs from a namespace-limited token #13659

Merged
merged 10 commits into from
Jul 11, 2022
27 changes: 24 additions & 3 deletions ui/app/abilities/job.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AbstractAbility from './abstract';
import { computed } from '@ember/object';
import { computed, get } from '@ember/object';
import { or } from '@ember/object/computed';

export default class Job extends AbstractAbility {
Expand Down Expand Up @@ -27,9 +27,30 @@ export default class Job extends AbstractAbility {
)
canDispatch;

@computed('[email protected]')
policyNamespacesIncludePermissions(policies = [], permissions = []) {
// For each policy record, extract all policies of all namespaces
const allNamespacePolicies = policies
.toArray()
.map((policy) => get(policy, 'rulesJSON.Namespaces'))
.flat()
.map(({ Capabilities }) => {
return Capabilities;
})
.flat()
.compact();

// Check for requested permissions
return allNamespacePolicies.some((policy) => {
return permissions.includes(policy);
});
}

@computed('token.selfTokenPolicies.[]')
get policiesSupportRunning() {
return this.namespaceIncludesCapability('submit-job');
return this.policyNamespacesIncludePermissions(
this.token.selfTokenPolicies,
['submit-job']
);
}

@computed('[email protected]')
Expand Down
7 changes: 5 additions & 2 deletions ui/app/adapters/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ export default class JobAdapter extends WatchableNamespaceIDs {
return this.ajax(url, 'DELETE');
}

parse(spec) {
const url = addToPath(this.urlForFindAll('job'), '/parse');
parse(spec, namespace = '*') {
const url = addToPath(
this.urlForFindAll('job'),
`/parse?namespace=${namespace}`
);
return this.ajax(url, 'POST', {
data: {
JobHCL: spec,
Expand Down
6 changes: 4 additions & 2 deletions ui/app/components/job-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export default class JobEditor extends Component {
try {
yield this.job.parse();
} catch (err) {
const error = messageFromAdapterError(err) || 'Could not parse input';
const error =
messageFromAdapterError(err, 'parse jobs') || 'Could not parse input';
this.set('parseError', error);
this.scrollToError();
return;
Expand All @@ -72,7 +73,8 @@ export default class JobEditor extends Component {
const plan = yield this.job.plan();
this.set('planOutput', plan);
} catch (err) {
const error = messageFromAdapterError(err) || 'Could not plan job';
const error =
messageFromAdapterError(err, 'plan jobs') || 'Could not plan job';
this.set('planError', error);
this.scrollToError();
}
Expand Down
47 changes: 47 additions & 0 deletions ui/app/controllers/jobs/run.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,56 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { scheduleOnce } from '@ember/runloop';
import { serialize } from 'nomad-ui/utils/qp-serialize';
import { get, set } from '@ember/object';

export default class RunController extends Controller {
@service router;
@service system;
@service store;

queryParams = [
{
qpNamespace: 'namespace',
},
];

onSubmit(id, namespace) {
this.router.transitionTo('jobs.job', `${id}@${namespace || 'default'}`);
}
@computed('qpNamespace')
get optionsNamespaces() {
const availableNamespaces = this.store
.peekAll('namespace')
.map((namespace) => ({
key: namespace.name,
label: namespace.name,
}));

availableNamespaces.unshift({
key: '*',
label: 'All (*)',
});

// Unset the namespace selection if it was server-side deleted
if (!availableNamespaces.mapBy('key').includes(this.qpNamespace)) {
scheduleOnce('actions', () => {
// eslint-disable-next-line ember/no-side-effects
set(this, 'qpNamespace', '*');
});
}

return availableNamespaces;
}

setFacetQueryParam(queryParam, selection) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgive the naming conventions (qpNamespace, queryParam, etc.) - will revise. Was pullig from jobs/volumes routes.

this.set(queryParam, serialize(selection));
const model = get(this, 'model');
set(
model,
'namespace',
this.store.peekAll('namespace').find((ns) => ns.id === this.qpNamespace)
);
}
}
4 changes: 3 additions & 1 deletion ui/app/models/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export default class Job extends Model {
parse() {
const definition = this._newDefinition;
let promise;
const namespace = this.get('namespace.id');

try {
// If the definition is already JSON then it doesn't need to be parsed.
Expand All @@ -258,9 +259,10 @@ export default class Job extends Model {
} catch (err) {
// If the definition is invalid JSON, assume it is HCL. If it is invalid
// in anyway, the parse endpoint will throw an error.

promise = this.store
.adapterFor('job')
.parse(this._newDefinition)
.parse(this._newDefinition, namespace)
.then((response) => {
this.set('_newDefinitionJSON', response);
this.setIdByPayload(response);
Expand Down
16 changes: 16 additions & 0 deletions ui/app/templates/jobs/run.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
<Breadcrumb @crumb={{hash label="Run" args=(array "jobs.run")}} />
{{page-title "Run a job"}}
<section class="section">
<div class="toolbar">
<div class="toolbar-item is-right-aligned is-mobile-full-width">
<div class="button-bar">
{{#if this.system.shouldShowNamespaces}}
<SingleSelectDropdown
data-test-namespace-facet
@label="Namespace"
@options={{this.optionsNamespaces}}
@selection={{this.qpNamespace}}
@onSelect={{action this.setFacetQueryParam "qpNamespace"}}
/>
{{/if}}
</div>
</div>
</div>

<JobEditor @job={{this.model}} @context="new" @onSubmit={{action this.onSubmit}} />
</section>