Merge pull request #10893 from hashicorp/f-ui/namespace-acl-bug
edit ember-can to add additional attribute for namespace
This commit is contained in:
commit
0ccf60444d
3
.changelog/10893.txt
Normal file
3
.changelog/10893.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: Fixes bug where UI was not detecting namespace-specific capabilities.
|
||||
```
|
|
@ -19,9 +19,14 @@ export default class TaskGroupRow extends Component {
|
|||
@oneWay('taskGroup.count') count;
|
||||
@alias('taskGroup.job.runningDeployment') runningDeployment;
|
||||
|
||||
@computed('runningDeployment')
|
||||
get namespace() {
|
||||
return this.get('taskGroup.job.namespace.name');
|
||||
}
|
||||
|
||||
@computed('runningDeployment', 'namespace')
|
||||
get tooltipText() {
|
||||
if (this.can.cannot('scale job')) return "You aren't allowed to scale task groups";
|
||||
if (this.can.cannot('scale job', null, { namespace: this.namespace }))
|
||||
return "You aren't allowed to scale task groups";
|
||||
if (this.runningDeployment) return 'You cannot scale task groups during a deployment';
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -66,9 +66,10 @@ export default class TaskGroupController extends Controller.extend(
|
|||
})
|
||||
shouldShowScaleEventTimeline;
|
||||
|
||||
@computed('model.job.runningDeployment')
|
||||
@computed('model.job.{namespace,runningDeployment}')
|
||||
get tooltipText() {
|
||||
if (this.can.cannot('scale job')) return "You aren't allowed to scale task groups";
|
||||
if (this.can.cannot('scale job', null, { namespace: this.model.job.namespace.get('name') }))
|
||||
return "You aren't allowed to scale task groups";
|
||||
if (this.model.job.runningDeployment) return 'You cannot scale task groups during a deployment';
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ export default class RunRoute extends Route {
|
|||
},
|
||||
];
|
||||
|
||||
beforeModel() {
|
||||
if (this.can.cannot('run job')) {
|
||||
beforeModel(transition) {
|
||||
if (this.can.cannot('run job', null, { namespace: transition.to.queryParams.namespace })) {
|
||||
this.transitionTo('jobs');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</span>
|
||||
<div class="pull-right">
|
||||
{{#unless this.isCurrent}}
|
||||
{{#if (can "run job")}}
|
||||
{{#if (can "run job" namespace=this.version.job.namespace)}}
|
||||
<TwoStepButton
|
||||
data-test-revert-to
|
||||
@classes={{hash
|
||||
|
|
|
@ -8,24 +8,25 @@
|
|||
{{#if this.taskGroup.scaling}}
|
||||
<div
|
||||
data-test-scale-controls
|
||||
class="button-bar is-shadowless is-text bumper-left {{if (or this.runningDeployment (cannot "scale job")) "tooltip multiline"}}"
|
||||
class="button-bar is-shadowless is-text bumper-left {{if (or this.runningDeployment (cannot "scale job" namespace=this.namespace)) "tooltip multiline"}}"
|
||||
aria-label={{this.tooltipText}}>
|
||||
<button
|
||||
data-test-scale="decrement"
|
||||
role="button"
|
||||
aria-label="decrement"
|
||||
class="button is-xsmall is-light"
|
||||
disabled={{or this.isMinimum this.runningDeployment (cannot "scale job")}}
|
||||
disabled={{or this.isMinimum this.runningDeployment (cannot "scale job" namespace=this.namespace)}}
|
||||
onclick={{action "countDown"}}
|
||||
type="button">
|
||||
{{x-icon "minus-plain" class="is-text"}}
|
||||
</button>
|
||||
<button
|
||||
data-test-scale-controls-increment
|
||||
data-test-scale="increment"
|
||||
role="button"
|
||||
aria-label="increment"
|
||||
class="button is-xsmall is-light"
|
||||
disabled={{or this.isMaximum this.runningDeployment (cannot "scale job")}}
|
||||
disabled={{or this.isMaximum this.runningDeployment (cannot "scale job" namespace=this.namespace)}}
|
||||
onclick={{action "countUp"}}
|
||||
type="button">
|
||||
{{x-icon "plus-plain" class="is-text"}}
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
</div>
|
||||
{{#if (media "isMobile")}}
|
||||
<div class="toolbar-item is-right-aligned">
|
||||
{{#if (can "run job")}}
|
||||
<LinkTo @route="jobs.run" data-test-run-job class="button is-primary">Run Job</LinkTo>
|
||||
{{#if (can "run job" namespace=this.qpNamespace)}}
|
||||
<LinkTo @route="jobs.run" @query={{hash namespace=this.qpNamespace}} data-test-run-job class="button is-primary">Run Job</LinkTo>
|
||||
{{else}}
|
||||
<button
|
||||
data-test-run-job
|
||||
|
@ -66,8 +66,8 @@
|
|||
</div>
|
||||
{{#if (not (media "isMobile"))}}
|
||||
<div class="toolbar-item is-right-aligned">
|
||||
{{#if (can "run job")}}
|
||||
<LinkTo @route="jobs.run" data-test-run-job class="button is-primary">Run Job</LinkTo>
|
||||
{{#if (can "run job" namespace=this.qpNamespace)}}
|
||||
<LinkTo @route="jobs.run" @query={{hash namespace=this.qpNamespace}} data-test-run-job class="button is-primary">Run Job</LinkTo>
|
||||
{{else}}
|
||||
<button
|
||||
data-test-run-job
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
@max={{this.model.scaling.max}}
|
||||
@value={{this.model.count}}
|
||||
@class="is-primary is-small"
|
||||
@disabled={{or this.model.job.runningDeployment (cannot "scale job")}}
|
||||
@disabled={{or this.model.job.runningDeployment (cannot "scale job" namespace=this.model.job.namespace.name)}}
|
||||
@onChange={{action "scaleTaskGroup"}}>
|
||||
Count
|
||||
</StepperInput>
|
||||
|
|
|
@ -253,4 +253,72 @@ module('Acceptance | job detail (with namespaces)', function(hooks) {
|
|||
0
|
||||
);
|
||||
});
|
||||
|
||||
test('when the dynamic autoscaler is applied, you can scale a task within the job detail page', async function(assert) {
|
||||
const SCALE_AND_WRITE_NAMESPACE = 'scale-and-write-namespace';
|
||||
const READ_ONLY_NAMESPACE = 'read-only-namespace';
|
||||
const clientToken = server.create('token');
|
||||
|
||||
const namespace = server.create('namespace', { id: SCALE_AND_WRITE_NAMESPACE });
|
||||
const secondNamespace = server.create('namespace', { id: READ_ONLY_NAMESPACE });
|
||||
|
||||
job = server.create('job', {
|
||||
groupCount: 0,
|
||||
createAllocations: false,
|
||||
shallow: true,
|
||||
noActiveDeployment: true,
|
||||
namespaceId: SCALE_AND_WRITE_NAMESPACE,
|
||||
});
|
||||
|
||||
const job2 = server.create('job', {
|
||||
groupCount: 0,
|
||||
createAllocations: false,
|
||||
shallow: true,
|
||||
noActiveDeployment: true,
|
||||
namespaceId: READ_ONLY_NAMESPACE,
|
||||
});
|
||||
const scalingGroup2 = server.create('task-group', {
|
||||
job: job2,
|
||||
name: 'scaling',
|
||||
count: 1,
|
||||
shallow: true,
|
||||
withScaling: true,
|
||||
});
|
||||
job2.update({ taskGroupIds: [scalingGroup2.id] });
|
||||
|
||||
const policy = server.create('policy', {
|
||||
id: 'something',
|
||||
name: 'something',
|
||||
rulesJSON: {
|
||||
Namespaces: [
|
||||
{
|
||||
Name: SCALE_AND_WRITE_NAMESPACE,
|
||||
Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
|
||||
},
|
||||
{
|
||||
Name: READ_ONLY_NAMESPACE,
|
||||
Capabilities: ['list-jobs', 'read-job'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const scalingGroup = server.create('task-group', {
|
||||
job,
|
||||
name: 'scaling',
|
||||
count: 1,
|
||||
shallow: true,
|
||||
withScaling: true,
|
||||
});
|
||||
job.update({ taskGroupIds: [scalingGroup.id] });
|
||||
|
||||
clientToken.policyIds = [policy.id];
|
||||
clientToken.save();
|
||||
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
||||
|
||||
await JobDetail.visit({ id: job.id, namespace: namespace.name });
|
||||
assert.notOk(JobDetail.incrementButton.isDisabled);
|
||||
|
||||
await JobDetail.visit({ id: job2.id, namespace: secondNamespace.name });
|
||||
assert.ok(JobDetail.incrementButton.isDisabled);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -106,4 +106,38 @@ module('Acceptance | job run', function(hooks) {
|
|||
await JobRun.visit();
|
||||
assert.equal(currentURL(), '/jobs');
|
||||
});
|
||||
|
||||
test('when using client token user can still go to job page if they have correct permissions', async function(assert) {
|
||||
const clientTokenWithPolicy = server.create('token');
|
||||
const newNamespace = 'second-namespace';
|
||||
|
||||
server.create('namespace', { id: newNamespace });
|
||||
server.create('job', {
|
||||
groupCount: 0,
|
||||
createAllocations: false,
|
||||
shallow: true,
|
||||
noActiveDeployment: true,
|
||||
namespaceId: newNamespace,
|
||||
});
|
||||
|
||||
const policy = server.create('policy', {
|
||||
id: 'something',
|
||||
name: 'something',
|
||||
rulesJSON: {
|
||||
Namespaces: [
|
||||
{
|
||||
Name: newNamespace,
|
||||
Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
clientTokenWithPolicy.policyIds = [policy.id];
|
||||
clientTokenWithPolicy.save();
|
||||
window.localStorage.nomadTokenSecret = clientTokenWithPolicy.secretId;
|
||||
|
||||
await JobRun.visit({ namespace: newNamespace });
|
||||
assert.equal(currentURL(), `/jobs/run?namespace=${newNamespace}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -52,7 +52,7 @@ module('Acceptance | job versions', function(hooks) {
|
|||
test('all versions but the current one have a button to revert to that version', async function(assert) {
|
||||
let versionRowToRevertTo;
|
||||
|
||||
Versions.versions.forEach((versionRow) => {
|
||||
Versions.versions.forEach(versionRow => {
|
||||
if (versionRow.number === job.version) {
|
||||
assert.ok(versionRow.revertToButton.isHidden);
|
||||
} else {
|
||||
|
@ -67,7 +67,9 @@ module('Acceptance | job versions', function(hooks) {
|
|||
await versionRowToRevertTo.revertToButton.idle();
|
||||
await versionRowToRevertTo.revertToButton.confirm();
|
||||
|
||||
const revertRequest = this.server.pretender.handledRequests.find(request => request.url.includes('revert'));
|
||||
const revertRequest = this.server.pretender.handledRequests.find(request =>
|
||||
request.url.includes('revert')
|
||||
);
|
||||
|
||||
assert.equal(revertRequest.url, `/v1/job/${job.id}/revert?namespace=${namespace.id}`);
|
||||
|
||||
|
@ -81,7 +83,9 @@ module('Acceptance | job versions', function(hooks) {
|
|||
});
|
||||
|
||||
test('when reversion fails, the error message from the API is piped through to the alert', async function(assert) {
|
||||
const versionRowToRevertTo = Versions.versions.filter(versionRow => versionRow.revertToButton.isPresent)[0];
|
||||
const versionRowToRevertTo = Versions.versions.filter(
|
||||
versionRow => versionRow.revertToButton.isPresent
|
||||
)[0];
|
||||
|
||||
if (versionRowToRevertTo) {
|
||||
const message = 'A plaintext error message';
|
||||
|
@ -104,7 +108,9 @@ module('Acceptance | job versions', function(hooks) {
|
|||
});
|
||||
|
||||
test('when reversion has no effect, the error message explains', async function(assert) {
|
||||
const versionRowToRevertTo = Versions.versions.filter(versionRow => versionRow.revertToButton.isPresent)[0];
|
||||
const versionRowToRevertTo = Versions.versions.filter(
|
||||
versionRow => versionRow.revertToButton.isPresent
|
||||
)[0];
|
||||
|
||||
if (versionRowToRevertTo) {
|
||||
// The default Mirage implementation updates the job version as passed in, this does nothing
|
||||
|
@ -116,7 +122,10 @@ module('Acceptance | job versions', function(hooks) {
|
|||
assert.ok(Layout.inlineError.isShown);
|
||||
assert.ok(Layout.inlineError.isWarning);
|
||||
assert.ok(Layout.inlineError.title.includes('Reversion Had No Effect'));
|
||||
assert.equal(Layout.inlineError.message, 'Reverting to an identical older version doesn’t produce a new version');
|
||||
assert.equal(
|
||||
Layout.inlineError.message,
|
||||
'Reverting to an identical older version doesn’t produce a new version'
|
||||
);
|
||||
} else {
|
||||
assert.expect(0);
|
||||
}
|
||||
|
@ -154,7 +163,9 @@ module('Acceptance | job versions (with client token)', function(hooks) {
|
|||
});
|
||||
|
||||
test('reversion buttons are disabled when the token lacks permissions', async function(assert) {
|
||||
const versionRowWithReversion = Versions.versions.filter(versionRow => versionRow.revertToButton.isPresent)[0];
|
||||
const versionRowWithReversion = Versions.versions.filter(
|
||||
versionRow => versionRow.revertToButton.isPresent
|
||||
)[0];
|
||||
|
||||
if (versionRowWithReversion) {
|
||||
assert.ok(versionRowWithReversion.revertToButtonIsDisabled);
|
||||
|
@ -164,4 +175,50 @@ module('Acceptance | job versions (with client token)', function(hooks) {
|
|||
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
test('reversion buttons are available when the client token has permissions', async function(assert) {
|
||||
const REVERT_NAMESPACE = 'revert-namespace';
|
||||
window.localStorage.clear();
|
||||
const clientToken = server.create('token');
|
||||
|
||||
server.create('namespace', { id: REVERT_NAMESPACE });
|
||||
|
||||
const job = server.create('job', {
|
||||
groupCount: 0,
|
||||
createAllocations: false,
|
||||
shallow: true,
|
||||
noActiveDeployment: true,
|
||||
namespaceId: REVERT_NAMESPACE,
|
||||
});
|
||||
|
||||
const policy = server.create('policy', {
|
||||
id: 'something',
|
||||
name: 'something',
|
||||
rulesJSON: {
|
||||
Namespaces: [
|
||||
{
|
||||
Name: REVERT_NAMESPACE,
|
||||
Capabilities: ['submit-job'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
clientToken.policyIds = [policy.id];
|
||||
clientToken.save();
|
||||
|
||||
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
||||
|
||||
versions = server.db.jobVersions.where({ jobId: job.id });
|
||||
await Versions.visit({ id: job.id, namespace: REVERT_NAMESPACE });
|
||||
const versionRowWithReversion = Versions.versions.filter(
|
||||
versionRow => versionRow.revertToButton.isPresent
|
||||
)[0];
|
||||
|
||||
if (versionRowWithReversion) {
|
||||
assert.ok(versionRowWithReversion.revertToButtonIsDisabled);
|
||||
} else {
|
||||
assert.expect(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -356,6 +356,42 @@ module('Acceptance | jobs list', function(hooks) {
|
|||
assert.equal(currentURL(), `/csi/volumes?namespace=${namespace.id}`);
|
||||
});
|
||||
|
||||
test('when the user has a client token that has a namespace with a policy to run a job', async function(assert) {
|
||||
const READ_AND_WRITE_NAMESPACE = 'read-and-write-namespace';
|
||||
const READ_ONLY_NAMESPACE = 'read-only-namespace';
|
||||
|
||||
server.create('namespace', { id: READ_AND_WRITE_NAMESPACE });
|
||||
server.create('namespace', { id: READ_ONLY_NAMESPACE });
|
||||
|
||||
const policy = server.create('policy', {
|
||||
id: 'something',
|
||||
name: 'something',
|
||||
rulesJSON: {
|
||||
Namespaces: [
|
||||
{
|
||||
Name: READ_AND_WRITE_NAMESPACE,
|
||||
Capabilities: ['submit-job'],
|
||||
},
|
||||
{
|
||||
Name: READ_ONLY_NAMESPACE,
|
||||
Capabilities: ['list-job'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
clientToken.policyIds = [policy.id];
|
||||
clientToken.save();
|
||||
|
||||
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
||||
|
||||
await JobsList.visit({ namespace: READ_AND_WRITE_NAMESPACE });
|
||||
assert.notOk(JobsList.runJobButton.isDisabled);
|
||||
|
||||
await JobsList.visit({ namespace: READ_ONLY_NAMESPACE });
|
||||
assert.ok(JobsList.runJobButton.isDisabled);
|
||||
});
|
||||
|
||||
pageSizeSelect({
|
||||
resourceName: 'job',
|
||||
pageObject: JobsList,
|
||||
|
|
|
@ -82,7 +82,9 @@ module('Acceptance | task group detail', function(hooks) {
|
|||
test('/jobs/:id/:task-group should list high-level metrics for the allocation', async function(assert) {
|
||||
const totalCPU = tasks.mapBy('resources.CPU').reduce(sum, 0);
|
||||
const totalMemory = tasks.mapBy('resources.MemoryMB').reduce(sum, 0);
|
||||
const totalMemoryMax = tasks.map(t => t.resources.MemoryMaxMB || t.resources.MemoryMB).reduce(sum, 0);
|
||||
const totalMemoryMax = tasks
|
||||
.map(t => t.resources.MemoryMaxMB || t.resources.MemoryMB)
|
||||
.reduce(sum, 0);
|
||||
const totalDisk = taskGroup.ephemeralDisk.SizeMB;
|
||||
|
||||
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
|
||||
|
@ -148,6 +150,88 @@ module('Acceptance | task group detail', function(hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test('when the user has a client token that has a namespace with a policy to run and scale a job the autoscaler options should be available', async function(assert) {
|
||||
window.localStorage.clear();
|
||||
|
||||
const SCALE_AND_WRITE_NAMESPACE = 'scale-and-write-namespace';
|
||||
const READ_ONLY_NAMESPACE = 'read-only-namespace';
|
||||
const clientToken = server.create('token');
|
||||
|
||||
server.create('namespace', { id: SCALE_AND_WRITE_NAMESPACE });
|
||||
const secondNamespace = server.create('namespace', { id: READ_ONLY_NAMESPACE });
|
||||
|
||||
job = server.create('job', {
|
||||
groupCount: 0,
|
||||
createAllocations: false,
|
||||
shallow: true,
|
||||
noActiveDeployment: true,
|
||||
namespaceId: SCALE_AND_WRITE_NAMESPACE,
|
||||
});
|
||||
const scalingGroup = server.create('task-group', {
|
||||
job,
|
||||
name: 'scaling',
|
||||
count: 1,
|
||||
shallow: true,
|
||||
withScaling: true,
|
||||
});
|
||||
job.update({ taskGroupIds: [scalingGroup.id] });
|
||||
|
||||
const job2 = server.create('job', {
|
||||
groupCount: 0,
|
||||
createAllocations: false,
|
||||
shallow: true,
|
||||
noActiveDeployment: true,
|
||||
namespaceId: READ_ONLY_NAMESPACE,
|
||||
});
|
||||
const scalingGroup2 = server.create('task-group', {
|
||||
job: job2,
|
||||
name: 'scaling',
|
||||
count: 1,
|
||||
shallow: true,
|
||||
withScaling: true,
|
||||
});
|
||||
job2.update({ taskGroupIds: [scalingGroup2.id] });
|
||||
|
||||
const policy = server.create('policy', {
|
||||
id: 'something',
|
||||
name: 'something',
|
||||
rulesJSON: {
|
||||
Namespaces: [
|
||||
{
|
||||
Name: SCALE_AND_WRITE_NAMESPACE,
|
||||
Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
|
||||
},
|
||||
{
|
||||
Name: READ_ONLY_NAMESPACE,
|
||||
Capabilities: ['list-jobs', 'read-job'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
clientToken.policyIds = [policy.id];
|
||||
clientToken.save();
|
||||
|
||||
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
||||
|
||||
await TaskGroup.visit({
|
||||
id: job.id,
|
||||
name: scalingGroup.name,
|
||||
namespace: SCALE_AND_WRITE_NAMESPACE,
|
||||
});
|
||||
|
||||
assert.equal(currentURL(), `/jobs/${job.id}/scaling?namespace=${SCALE_AND_WRITE_NAMESPACE}`);
|
||||
assert.notOk(TaskGroup.countStepper.increment.isDisabled);
|
||||
|
||||
await TaskGroup.visit({
|
||||
id: job2.id,
|
||||
name: scalingGroup2.name,
|
||||
namespace: secondNamespace.name,
|
||||
});
|
||||
assert.equal(currentURL(), `/jobs/${job2.id}/scaling?namespace=${READ_ONLY_NAMESPACE}`);
|
||||
assert.ok(TaskGroup.countStepper.increment.isDisabled);
|
||||
});
|
||||
|
||||
test('/jobs/:id/:task-group should list one page of allocations for the task group', async function(assert) {
|
||||
server.createList('allocation', TaskGroup.pageSize, {
|
||||
jobId: job.id,
|
||||
|
|
|
@ -38,6 +38,11 @@ export default create({
|
|||
tooltipText: attribute('aria-label'),
|
||||
},
|
||||
|
||||
incrementButton: {
|
||||
scope: '[data-test-scale-controls-increment]',
|
||||
isDisabled: property('disabled'),
|
||||
},
|
||||
|
||||
dispatchButton: {
|
||||
scope: '[data-test-dispatch-button]',
|
||||
isDisabled: property('disabled'),
|
||||
|
|
|
@ -22,6 +22,7 @@ export default create({
|
|||
search: fillable('.search-box input'),
|
||||
|
||||
countStepper: stepperInput('[data-test-task-group-count-stepper]'),
|
||||
incrementButton: { scope: '[data-test-stepper-increment]' },
|
||||
|
||||
tasksCount: text('[data-test-task-group-tasks]'),
|
||||
cpu: text('[data-test-task-group-cpu]'),
|
||||
|
|
Loading…
Reference in a new issue