From 0cbbb41da2865e51339c6e4b1baa4e031b13ee54 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 18 Mar 2021 12:40:29 -0400 Subject: [PATCH 1/9] Rearrange agent upgrade tests into groups for upgrade & bulk upgrade --- .../server/routes/agent/upgrade_handler.ts | 23 +- .../apis/agents/upgrade.ts | 896 +++++++++--------- 2 files changed, 457 insertions(+), 462 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 279018ef4212c..b8af265883091 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -92,21 +92,14 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< } try { - if (Array.isArray(agents)) { - await AgentService.sendUpgradeAgentsActions(soClient, esClient, { - agentIds: agents, - sourceUri, - version, - force, - }); - } else { - await AgentService.sendUpgradeAgentsActions(soClient, esClient, { - kuery: agents, - sourceUri, - version, - force, - }); - } + const agentOptions = Array.isArray(agents) ? { agentIds: agents } : { kuery: agents }; + const upgradeOptions = { + ...agentOptions, + sourceUri, + version, + force, + }; + await AgentService.sendUpgradeAgentsActions(soClient, esClient, upgradeOptions); const body: PostBulkAgentUpgradeResponse = {}; return response.ok({ body }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 9f2280ed9613e..592e8dce15037 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -23,7 +23,7 @@ export default function (providerContext: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - describe('fleet upgrade agent', () => { + describe('fleet upgrade', () => { skipIfNoDockerRegistry(providerContext); before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); @@ -36,485 +36,426 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); - it('should respond 200 to upgrade agent and update the agent SO', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + describe('one agent', () => { + it('should respond 200 to upgrade agent and update the agent SO', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, }, - }, - }); - await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersion, - source_uri: 'http://path/to/download', - }) - .expect(200); + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + source_uri: 'http://path/to/download', + }) + .expect(200); - const res = await supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'); - expect(typeof res.body.item.upgrade_started_at).to.be('string'); - }); - it('should respond 400 if upgrading agent with version the same as snapshot version', async () => { - const kibanaVersion = await kibanaServer.version.get(); - const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: kibanaVersion } } }, + const res = await supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'); + expect(typeof res.body.item.upgrade_started_at).to.be('string'); + }); + it('should respond 400 if upgrading agent with version the same as snapshot version', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: kibanaVersion } } }, + }, }, - }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersionSnapshot, + }) + .expect(400); }); - await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersionSnapshot, - }) - .expect(400); - }); - it('should respond 200 if upgrading agent with version the same as snapshot version and force flag is passed', async () => { - const kibanaVersion = await kibanaServer.version.get(); - const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: kibanaVersion } } }, + it('should respond 200 if upgrading agent with version the same as snapshot version and force flag is passed', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: kibanaVersion } } }, + }, }, - }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersionSnapshot, + force: true, + }) + .expect(200); }); - await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersionSnapshot, - force: true, - }) - .expect(200); - }); - it('should respond 200 if upgrading agent with version less than kibana snapshot version', async () => { - const kibanaVersion = await kibanaServer.version.get(); - const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); + it('should respond 200 if upgrading agent with version less than kibana snapshot version', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, }, - }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersionSnapshot, + }) + .expect(200); }); - await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersionSnapshot, - }) - .expect(200); - }); - it('should respond 200 to upgrade agent and update the agent SO without source_uri', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + it('should respond 200 to upgrade agent and update the agent SO without source_uri', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, }, - }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + }) + .expect(200); + const res = await supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'); + expect(typeof res.body.item.upgrade_started_at).to.be('string'); }); - await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersion, - }) - .expect(200); - const res = await supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'); - expect(typeof res.body.item.upgrade_started_at).to.be('string'); - }); - it('should respond 400 if trying to upgrade to a version that does not match installed kibana version', async () => { - const kibanaVersion = await kibanaServer.version.get(); - const higherVersion = semver.inc(kibanaVersion, 'patch'); - await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: higherVersion, - source_uri: 'http://path/to/download', - }) - .expect(400); - }); - it('should respond 400 if trying to upgrade an agent that is unenrolling', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({ - force: true, + it('should respond 400 if trying to upgrade to a version that does not match installed kibana version', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const higherVersion = semver.inc(kibanaVersion, 'patch'); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: higherVersion, + source_uri: 'http://path/to/download', + }) + .expect(400); }); - await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersion, - }) - .expect(400); - }); - it('should respond 400 if trying to upgrade an agent that is unenrolled', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - unenrolled_at: new Date().toISOString(), + it('should respond 400 if trying to upgrade an agent that is unenrolling', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({ + force: true, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + }) + .expect(400); + }); + it('should respond 400 if trying to upgrade an agent that is unenrolled', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + unenrolled_at: new Date().toISOString(), + }, }, - }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + }) + .expect(400); }); - await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersion, - }) - .expect(400); - }); - - it('should respond 400 if trying to upgrade an agent that is not upgradeable', async () => { - const kibanaVersion = await kibanaServer.version.get(); - const res = await supertest - .post(`/api/fleet/agents/agent1/upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersion, - }) - .expect(400); - expect(res.body.message).to.equal('agent agent1 is not upgradeable'); - }); - it('should respond 200 to bulk upgrade upgradeable agents and update the agent SOs', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, - }, - }, + it('should respond 400 if trying to upgrade an agent that is not upgradeable', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const res = await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + }) + .expect(400); + expect(res.body.message).to.equal('agent agent1 is not upgradeable'); }); - await es.update({ - id: 'agent2', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { - elastic: { - agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, - }, + + it('enrolled in a managed policy should respond 400 to upgrade and not update the agent SOs', async () => { + // update enrolled policy to managed + await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ + name: 'Test policy', + namespace: 'default', + is_managed: true, + }); + + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, }, }, - }, - }); - await supertest - .post(`/api/fleet/agents/bulk_upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersion, - agents: ['agent1', 'agent2'], - }) - .expect(200); + }); + // attempt to upgrade agent in managed policy + const { body } = await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ version: kibanaVersion }) + .expect(400); + expect(body.message).to.contain('Cannot upgrade agent agent1 in managed policy policy1'); - const [agent1data, agent2data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), - ]); - expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); + const agent1data = await supertest.get(`/api/fleet/agents/agent1`); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); + }); }); - it('should allow to upgrade multiple upgradeable agents by kuery', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + describe('multiple agents', () => { + it('should respond 200 to bulk upgrade upgradeable agents and update the agent SOs', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, }, - }, - }); - await es.update({ - id: 'agent2', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { - elastic: { - agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, + }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { + agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, + }, }, }, }, - }, - }); - await supertest - .post(`/api/fleet/agents/bulk_upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: 'active:true', - version: kibanaVersion, - }) - .expect(200); - const [agent1data, agent2data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), - ]); - expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); - }); + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + agents: ['agent1', 'agent2'], + }) + .expect(200); - it('should not upgrade an unenrolling agent during bulk_upgrade', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({ - force: true, - }); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, - }, - }, + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); - await es.update({ - id: 'agent2', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + + it('should allow to upgrade multiple upgradeable agents by kuery', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, }, - }, - }); - await supertest - .post(`/api/fleet/agents/bulk_upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: ['agent1', 'agent2'], - version: kibanaVersion, }); - const [agent1data, agent2data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), - ]); - expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); - }); - it('should not upgrade an unenrolled agent during bulk_upgrade', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - unenrolled_at: new Date().toISOString(), - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, - }, - }, - }); - await es.update({ - id: 'agent2', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { - elastic: { agent: { upgradeable: true, version: '0.0.0' } }, + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { + agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, + }, + }, }, }, - }, + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'active:true', + version: kibanaVersion, + }) + .expect(200); + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); - await supertest - .post(`/api/fleet/agents/bulk_upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: ['agent1', 'agent2'], - version: kibanaVersion, + + it('should not upgrade an unenrolling agent during bulk_upgrade', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({ + force: true, }); - const [agent1data, agent2data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), - ]); - expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); - }); - it('should not upgrade an non upgradeable agent during bulk_upgrade', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, }, - }, - }); - await es.update({ - id: 'agent2', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { - elastic: { - agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, - }, + }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, }, }, - }, + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent1', 'agent2'], + version: kibanaVersion, + }); + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); - await es.update({ - id: 'agent3', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: false, version: '0.0.0' } } }, + it('should not upgrade an unenrolled agent during bulk_upgrade', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + unenrolled_at: new Date().toISOString(), + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, }, - }, - }); - await supertest - .post(`/api/fleet/agents/bulk_upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: ['agent1', 'agent2', 'agent3'], - version: kibanaVersion, }); - const [agent1data, agent2data, agent3data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), - ]); - expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); - expect(typeof agent3data.body.item.upgrade_started_at).to.be('undefined'); - }); - it('should upgrade a non upgradeable agent during bulk_upgrade with force flag', async () => { - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, - }, - }, - }); - await es.update({ - id: 'agent2', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { - elastic: { - agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: '0.0.0' } }, }, }, }, - }, + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent1', 'agent2'], + version: kibanaVersion, + }); + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); - await es.update({ - id: 'agent3', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: false, version: '0.0.0' } } }, + it('should not upgrade an non upgradeable agent during bulk_upgrade', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, }, - }, - }); - await supertest - .post(`/api/fleet/agents/bulk_upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: ['agent1', 'agent2', 'agent3'], - version: kibanaVersion, - force: true, }); - const [agent1data, agent2data, agent3data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), - ]); - expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent3data.body.item.upgrade_started_at).to.be('string'); - }); - it('should respond 400 if trying to bulk upgrade to a version that does not match installed kibana version', async () => { - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { + agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, + }, + }, + }, }, - }, - }); - await es.update({ - id: 'agent2', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }); + await es.update({ + id: 'agent3', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: false, version: '0.0.0' } } }, + }, }, - }, - }); - await supertest - .post(`/api/fleet/agents/bulk_upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: ['agent1', 'agent2'], - version: '1.0.0', - force: true, - }) - .expect(400); - }); - - describe('fleet upgrade agent(s) in a managed policy', function () { - it('should respond 400 to bulk upgrade and not update the agent SOs', async () => { - // update enrolled policy to managed - await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ - name: 'Test policy', - namespace: 'default', - is_managed: true, }); - + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent1', 'agent2', 'agent3'], + version: kibanaVersion, + }); + const [agent1data, agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); + expect(typeof agent3data.body.item.upgrade_started_at).to.be('undefined'); + }); + it('should upgrade a non upgradeable agent during bulk_upgrade with force flag', async () => { const kibanaVersion = await kibanaServer.version.get(); await es.update({ id: 'agent1', @@ -540,27 +481,66 @@ export default function (providerContext: FtrProviderContext) { }, }, }); - // attempt to upgrade agent in managed policy - const { body } = await supertest + await es.update({ + id: 'agent3', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: false, version: '0.0.0' } } }, + }, + }, + }); + await supertest .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') .send({ + agents: ['agent1', 'agent2', 'agent3'], version: kibanaVersion, + force: true, + }); + const [agent1data, agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent3data.body.item.upgrade_started_at).to.be('string'); + }); + it('should respond 400 if trying to bulk upgrade to a version that does not match installed kibana version', async () => { + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }, + }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }, + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ agents: ['agent1', 'agent2'], + version: '1.0.0', + force: true, }) .expect(400); - expect(body.message).to.contain('Cannot upgrade agent in managed policy policy1'); - - const [agent1data, agent2data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent1`), - supertest.get(`/api/fleet/agents/agent2`), - ]); - - expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); - it('should respond 400 to upgrade and not update the agent SOs', async () => { + it('enrolled in a managed policy should respond 400 to bulk upgrade and not update the agent SOs', async () => { // update enrolled policy to managed await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ name: 'Test policy', @@ -579,16 +559,38 @@ export default function (providerContext: FtrProviderContext) { }, }, }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { + agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, + }, + }, + }, + }, + }); // attempt to upgrade agent in managed policy const { body } = await supertest - .post(`/api/fleet/agents/agent1/upgrade`) + .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') - .send({ version: kibanaVersion }) + .send({ + version: kibanaVersion, + agents: ['agent1', 'agent2'], + }) .expect(400); - expect(body.message).to.contain('Cannot upgrade agent agent1 in managed policy policy1'); + expect(body.message).to.contain('Cannot upgrade agent in managed policy policy1'); + + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`), + supertest.get(`/api/fleet/agents/agent2`), + ]); - const agent1data = await supertest.get(`/api/fleet/agents/agent1`); expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); }); }); From 059ed4ffcc01c5bee1fcb612a296e6a0afbcefcc Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 18 Mar 2021 14:26:13 -0400 Subject: [PATCH 2/9] Respect force flag in /agents/bulk_upgrade. Add test --- .../fleet/server/services/agents/upgrade.ts | 38 ++++++------- .../apis/agents/upgrade.ts | 54 ++++++++++++++++++- 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 12623be0ed044..22133bf20817e 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -83,29 +83,31 @@ export async function sendUpgradeAgentsActions( force?: boolean; } ) { - const kibanaVersion = appContextService.getKibanaVersion(); - // Filter out agents currently unenrolling, agents unenrolled, and agents not upgradeable - const agents = await getAgents(esClient, options); + // Full set of agents + const agentsGiven = await getAgents(esClient, options); - // upgradeable if they pass the version check + // Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check + const kibanaVersion = appContextService.getKibanaVersion(); const upgradeableAgents = options.force - ? agents - : agents.filter((agent) => isAgentUpgradeable(agent, kibanaVersion)); + ? agentsGiven + : agentsGiven.filter((agent) => isAgentUpgradeable(agent, kibanaVersion)); - // get any policy ids from upgradable agents - const policyIdsToGet = new Set( - upgradeableAgents.filter((agent) => agent.policy_id).map((agent) => agent.policy_id!) - ); + if (!options.force) { + // get any policy ids from upgradable agents + const policyIdsToGet = new Set( + upgradeableAgents.filter((agent) => agent.policy_id).map((agent) => agent.policy_id!) + ); - // get the agent policies for those ids - const agentPolicies = await agentPolicyService.getByIDs(soClient, Array.from(policyIdsToGet), { - fields: ['is_managed'], - }); + // get the agent policies for those ids + const agentPolicies = await agentPolicyService.getByIDs(soClient, Array.from(policyIdsToGet), { + fields: ['is_managed'], + }); - // throw if any of those agent policies are managed - for (const policy of agentPolicies) { - if (policy.is_managed) { - throw new IngestManagerError(`Cannot upgrade agent in managed policy ${policy.id}`); + // throw if any of those agent policies are managed + for (const policy of agentPolicies) { + if (policy.is_managed) { + throw new IngestManagerError(`Cannot upgrade agent in managed policy ${policy.id}`); + } } } diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 592e8dce15037..9a747fb11a6a6 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -540,7 +540,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - it('enrolled in a managed policy should respond 400 to bulk upgrade and not update the agent SOs', async () => { + it('enrolled in a managed policy bulk upgrade should respond with 400 and not update the agent SOs', async () => { // update enrolled policy to managed await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ name: 'Test policy', @@ -592,6 +592,58 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); + + it('enrolled in a managed policy bulk upgrade with force flag should respond with 200 and update the agent SOs', async () => { + // update enrolled policy to managed + await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ + name: 'Test policy', + namespace: 'default', + is_managed: true, + }); + + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }, + }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { + agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, + }, + }, + }, + }, + }); + // attempt to upgrade agent in managed policy + const { body } = await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + agents: ['agent1', 'agent2'], + force: true, + }); + expect(body).to.eql({}); + + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`), + supertest.get(`/api/fleet/agents/agent2`), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + }); }); }); } From e1b1142e258de744cc0b06ab4bac7998b6f01797 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Fri, 19 Mar 2021 15:34:20 -0400 Subject: [PATCH 3/9] WIP. reassign service is returning correct array of objects --- .../fleet/common/types/rest_spec/agent.ts | 9 +-- .../fleet/server/routes/agent/handlers.ts | 7 +- .../fleet/server/services/agents/crud.ts | 40 ++++++----- .../fleet/server/services/agents/reassign.ts | 67 +++++++++++++------ .../apis/agents/reassign.ts | 6 +- 5 files changed, 84 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index 93cbb8369a3b1..5a11c1776b031 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -164,12 +164,13 @@ export interface PostBulkAgentReassignRequest { }; } -export interface PostBulkAgentReassignResponse { - [key: string]: { +export type PostBulkAgentReassignResponse = Record< + Agent['id'], + { success: boolean; error?: Error; - }; -} + } +>; export interface GetOneAgentEventsRequest { params: { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index e6188a83c49e9..ddc926aa3ed8d 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -308,14 +308,15 @@ export const postBulkAgentsReassignHandler: RequestHandler< const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asInternalUser; + const agentOptions = Array.isArray(request.body.agents) + ? { agentIds: request.body.agents } + : { kuery: request.body.agents }; try { const results = await AgentService.reassignAgents( soClient, esClient, - Array.isArray(request.body.agents) - ? { agentIds: request.body.agents } - : { kuery: request.body.agents }, + agentOptions, request.body.policy_id ); diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 52a6b98bd0c41..1f9b9020fbf8c 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -68,22 +68,23 @@ export type GetAgentsOptions = }; export async function getAgents(esClient: ElasticsearchClient, options: GetAgentsOptions) { - let initialResults = []; - + let agents: Agent[] = []; if ('agentIds' in options) { - initialResults = await getAgentsById(esClient, options.agentIds); + agents = await getAgentsById(esClient, options.agentIds); } else if ('kuery' in options) { - initialResults = ( + agents = ( await getAllAgentsByKuery(esClient, { kuery: options.kuery, showInactive: options.showInactive ?? false, }) ).agents; } else { - throw new IngestManagerError('Cannot get agents'); + throw new IngestManagerError( + 'Either options.agentIds or options.kuery are required to get agents' + ); } - return initialResults; + return agents; } export async function getAgentsByKuery( @@ -187,7 +188,7 @@ export async function countInactiveAgents( export async function getAgentById(esClient: ElasticsearchClient, agentId: string) { const agentNotFoundError = new AgentNotFoundError(`Agent ${agentId} not found`); try { - const agentHit = await esClient.get>({ + const agentHit = await esClient.get({ index: AGENTS_INDEX, id: agentId, }); @@ -206,10 +207,17 @@ export async function getAgentById(esClient: ElasticsearchClient, agentId: strin } } -async function getAgentDocuments( +export function isAgentDocument( + maybeDocument: any +): maybeDocument is GetResponse { + return '_id' in maybeDocument && '_source' in maybeDocument; +} + +export type ESAgentDocumentResult = GetResponse; +export async function getAgentDocuments( esClient: ElasticsearchClient, agentIds: string[] -): Promise>> { +): Promise { const res = await esClient.mget>({ index: AGENTS_INDEX, body: { docs: agentIds.map((_id) => ({ _id })) }, @@ -220,14 +228,16 @@ async function getAgentDocuments( export async function getAgentsById( esClient: ElasticsearchClient, - agentIds: string[], - options: { includeMissing?: boolean } = { includeMissing: false } + agentIds: string[] ): Promise { const allDocs = await getAgentDocuments(esClient, agentIds); - const agentDocs = options.includeMissing - ? allDocs - : allDocs.filter((res) => res._id && res._source); - const agents = agentDocs.map((doc) => searchHitToAgent(doc)); + const agents = allDocs.reduce((results, doc) => { + if (isAgentDocument(doc)) { + results.push(searchHitToAgent(doc)); + } + + return results; + }, []); return agents; } diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 74e60c42b9973..05bb883f932d7 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -12,9 +12,17 @@ import type { Agent } from '../../types'; import { agentPolicyService } from '../agent_policy'; import { AgentReassignmentError } from '../../errors'; -import { getAgents, getAgentPolicyForAgent, updateAgent, bulkUpdateAgents } from './crud'; +import type { ESAgentDocumentResult } from './crud'; +import { + getAgentDocuments, + getAgentPolicyForAgent, + isAgentDocument, + updateAgent, + bulkUpdateAgents, +} from './crud'; import type { GetAgentsOptions } from './index'; import { createAgentAction, bulkCreateAgentActions } from './actions'; +import { searchHitToAgent } from './helpers'; export async function reassignAgent( soClient: SavedObjectsClientContract, @@ -67,7 +75,7 @@ export async function reassignAgentIsAllowed( export async function reassignAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: { agents: Agent[] } | GetAgentsOptions, + options: ({ agents: Agent[] } | GetAgentsOptions) & { force?: boolean }, newAgentPolicyId: string ): Promise<{ items: Array<{ id: string; success: boolean; error?: Error }> }> { const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); @@ -75,29 +83,48 @@ export async function reassignAgents( throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); } - const allResults = 'agents' in options ? options.agents : await getAgents(esClient, options); + let givenAgentsResults: Array = []; + if ('agents' in options) { + givenAgentsResults = options.agents; + } else if ('agentIds' in options) { + givenAgentsResults = await getAgentDocuments(esClient, options.agentIds); + } + // which are allowed to unenroll - const settled = await Promise.allSettled( - allResults.map((agent) => - reassignAgentIsAllowed(soClient, esClient, agent.id, newAgentPolicyId).then((_) => agent) - ) + const agentResults = await Promise.allSettled( + givenAgentsResults.map(async (result) => { + const agent: Agent = isAgentDocument(result) ? searchHitToAgent(result) : result; + + if (!agent.id) { + if ('_id' in result) { + throw new AgentReassignmentError(`Cannot find agent ${result._id}`); + } + throw new AgentReassignmentError(`Cannot find agent`); + } + if (agent.policy_id === newAgentPolicyId) { + throw new AgentReassignmentError(`${agent.id} is already assigned to ${newAgentPolicyId}`); + } + + const isAllowed = await reassignAgentIsAllowed( + soClient, + esClient, + agent.id, + newAgentPolicyId + ); + if (isAllowed) { + return agent; + } + throw new AgentReassignmentError(`${agent.id} may not be reassigned to ${newAgentPolicyId}`); + }) ); // Filter to agents that do not already use the new agent policy ID - const agentsToUpdate = allResults.filter((agent, index) => { - if (settled[index].status === 'fulfilled') { - if (agent.policy_id === newAgentPolicyId) { - settled[index] = { - status: 'rejected', - reason: new AgentReassignmentError( - `${agent.id} is already assigned to ${newAgentPolicyId}` - ), - }; - } else { - return true; - } + const agentsToUpdate = agentResults.reduce((updateable, result) => { + if (result.status === 'fulfilled' && result.value) { + updateable.push(result.value); } - }); + return updateable; + }, []); const res = await bulkUpdateAgents( esClient, diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 77da9ecce3294..358a9b1711aab 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -102,14 +102,14 @@ export default function (providerContext: FtrProviderContext) { }); it('should allow to reassign multiple agents by id -- some invalid', async () => { - await supertest + const { body } = await supertest .post(`/api/fleet/agents/bulk_reassign`) .set('kbn-xsrf', 'xxx') .send({ agents: ['agent2', 'INVALID_ID', 'agent3', 'MISSING_ID', 'etc'], policy_id: 'policy2', - }) - .expect(200); + }); + const [agent2data, agent3data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent2`), supertest.get(`/api/fleet/agents/agent3`), From ffd72d86b4e35b168aecd659b4adf6c4bbf65a2e Mon Sep 17 00:00:00 2001 From: John Schulz Date: Sun, 21 Mar 2021 07:56:43 -0400 Subject: [PATCH 4/9] Fix response & add tests --- .../fleet/server/routes/agent/handlers.ts | 19 ++++-- .../fleet/server/services/agents/crud.ts | 4 +- .../fleet/server/services/agents/reassign.ts | 64 ++++++++++++------- x-pack/plugins/fleet/server/types/index.tsx | 6 ++ .../apis/agents/reassign.ts | 20 +++++- 5 files changed, 81 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index ddc926aa3ed8d..001c55a4ab396 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -320,15 +320,20 @@ export const postBulkAgentsReassignHandler: RequestHandler< request.body.policy_id ); - const body: PostBulkAgentReassignResponse = results.items.reduce((acc, so) => { - return { - ...acc, - [so.id]: { - success: !so.error, - error: so.error || undefined, - }, + const body = results.items.reduce((acc, so) => { + acc[so.id] = { + success: !so.error, + error: so.error + ? { + name: so.error.name, + message: so.error.message, + // so.error.stack is also available + } + : undefined, }; + return acc; }, {}); + return response.ok({ body }); } catch (error) { return defaultIngestErrorHandler({ error, response }); diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 1f9b9020fbf8c..7a55ad985ed7c 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import type { SearchResponse, MGetResponse, GetResponse } from 'elasticsearch'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; -import type { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; +import type { AgentSOAttributes, Agent, BulkActionResult, ListWithKuery } from '../../types'; import { appContextService, agentPolicyService } from '../../services'; import type { FleetServerAgent } from '../../../common'; import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; @@ -285,7 +285,7 @@ export async function bulkUpdateAgents( agentId: string; data: Partial; }> -) { +): Promise<{ items: BulkActionResult[] }> { if (updateData.length === 0) { return { items: [] }; } diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 05bb883f932d7..5574c42ced053 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -8,15 +8,14 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; -import type { Agent } from '../../types'; +import type { Agent, BulkActionResult } from '../../types'; import { agentPolicyService } from '../agent_policy'; import { AgentReassignmentError } from '../../errors'; -import type { ESAgentDocumentResult } from './crud'; import { getAgentDocuments, + getAgents, getAgentPolicyForAgent, - isAgentDocument, updateAgent, bulkUpdateAgents, } from './crud'; @@ -77,30 +76,36 @@ export async function reassignAgents( esClient: ElasticsearchClient, options: ({ agents: Agent[] } | GetAgentsOptions) & { force?: boolean }, newAgentPolicyId: string -): Promise<{ items: Array<{ id: string; success: boolean; error?: Error }> }> { +): Promise<{ items: BulkActionResult[] }> { const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); if (!agentPolicy) { throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); } - let givenAgentsResults: Array = []; + const outgoingErrors: Record = {}; + let givenAgents: Agent[] = []; if ('agents' in options) { - givenAgentsResults = options.agents; + givenAgents = options.agents; } else if ('agentIds' in options) { - givenAgentsResults = await getAgentDocuments(esClient, options.agentIds); + const givenAgentsResults = await getAgentDocuments(esClient, options.agentIds); + for (const agentResult of givenAgentsResults) { + if (agentResult.found === false) { + outgoingErrors[agentResult._id] = new AgentReassignmentError( + `Cannot find agent ${agentResult._id}` + ); + } else { + givenAgents.push(searchHitToAgent(agentResult)); + } + } + } else if ('kuery' in options) { + givenAgents = await getAgents(esClient, options); } + const givenOrder = + 'agentIds' in options ? options.agentIds : givenAgents.map((agent) => agent.id); // which are allowed to unenroll const agentResults = await Promise.allSettled( - givenAgentsResults.map(async (result) => { - const agent: Agent = isAgentDocument(result) ? searchHitToAgent(result) : result; - - if (!agent.id) { - if ('_id' in result) { - throw new AgentReassignmentError(`Cannot find agent ${result._id}`); - } - throw new AgentReassignmentError(`Cannot find agent`); - } + givenAgents.map(async (agent, index) => { if (agent.policy_id === newAgentPolicyId) { throw new AgentReassignmentError(`${agent.id} is already assigned to ${newAgentPolicyId}`); } @@ -119,14 +124,17 @@ export async function reassignAgents( ); // Filter to agents that do not already use the new agent policy ID - const agentsToUpdate = agentResults.reduce((updateable, result) => { - if (result.status === 'fulfilled' && result.value) { - updateable.push(result.value); + const agentsToUpdate = agentResults.reduce((agents, result, index) => { + if (result.status === 'fulfilled') { + agents.push(result.value); + } else { + const id = givenAgents[index].id; + outgoingErrors[id] = result.reason; } - return updateable; + return agents; }, []); - const res = await bulkUpdateAgents( + await bulkUpdateAgents( esClient, agentsToUpdate.map((agent) => ({ agentId: agent.id, @@ -137,6 +145,18 @@ export async function reassignAgents( })) ); + const orderedOut = givenOrder.map((agentId) => { + const hasError = agentId in outgoingErrors; + const result: BulkActionResult = { + id: agentId, + success: !hasError, + }; + if (hasError) { + result.error = outgoingErrors[agentId]; + } + return result; + }); + const now = new Date().toISOString(); await bulkCreateAgentActions( soClient, @@ -148,5 +168,5 @@ export async function reassignAgents( })) ); - return res; + return { items: orderedOut }; } diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 885809d767323..84000532510d5 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -89,5 +89,11 @@ export type AgentPolicyUpdateHandler = ( agentPolicyId: string ) => Promise; +export interface BulkActionResult { + id: string; + success: boolean; + error?: Error; +} + export * from './models'; export * from './rest_spec'; diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 358a9b1711aab..30e71120660a9 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -102,14 +102,32 @@ export default function (providerContext: FtrProviderContext) { }); it('should allow to reassign multiple agents by id -- some invalid', async () => { + const inputAgentIds = ['agent2', 'INVALID_ID', 'agent3', 'MISSING_ID', 'etc']; const { body } = await supertest .post(`/api/fleet/agents/bulk_reassign`) .set('kbn-xsrf', 'xxx') .send({ - agents: ['agent2', 'INVALID_ID', 'agent3', 'MISSING_ID', 'etc'], + agents: inputAgentIds, policy_id: 'policy2', }); + expect(Object.keys(body)).to.eql(inputAgentIds); + expect(inputAgentIds.map((id) => ({ success: body[id].success }))).to.eql([ + { success: true }, + { success: false }, + { success: true }, + { success: false }, + { success: false }, + ]); + + expect(inputAgentIds.map((id) => Boolean(body[id].error))).to.eql([ + false, + true, + false, + true, + true, + ]); + const [agent2data, agent3data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent2`), supertest.get(`/api/fleet/agents/agent3`), From 338b2ecee789a82da6b14505bfcec12b4bc9168a Mon Sep 17 00:00:00 2001 From: John Schulz Date: Sun, 21 Mar 2021 09:54:19 -0400 Subject: [PATCH 5/9] Make test more explicit. Add variant for managed --- .../apis/agents/reassign.ts | 94 +++++++++++++++---- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 30e71120660a9..dd2cea163515b 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -101,32 +101,40 @@ export default function (providerContext: FtrProviderContext) { expect(agent3data.body.item.policy_id).to.eql('policy2'); }); - it('should allow to reassign multiple agents by id -- some invalid', async () => { - const inputAgentIds = ['agent2', 'INVALID_ID', 'agent3', 'MISSING_ID', 'etc']; + it('should allow to reassign multiple agents by id -- mix valid & invalid', async () => { const { body } = await supertest .post(`/api/fleet/agents/bulk_reassign`) .set('kbn-xsrf', 'xxx') .send({ - agents: inputAgentIds, + agents: ['agent2', 'INVALID_ID', 'agent3', 'MISSING_ID', 'etc'], policy_id: 'policy2', }); - expect(Object.keys(body)).to.eql(inputAgentIds); - expect(inputAgentIds.map((id) => ({ success: body[id].success }))).to.eql([ - { success: true }, - { success: false }, - { success: true }, - { success: false }, - { success: false }, - ]); - - expect(inputAgentIds.map((id) => Boolean(body[id].error))).to.eql([ - false, - true, - false, - true, - true, - ]); + expect(body).to.eql({ + agent2: { success: true }, + INVALID_ID: { + success: false, + error: { + name: 'AgentReassignmentError', + message: 'Cannot find agent INVALID_ID', + }, + }, + agent3: { success: true }, + MISSING_ID: { + success: false, + error: { + name: 'AgentReassignmentError', + message: 'Cannot find agent MISSING_ID', + }, + }, + etc: { + success: false, + error: { + name: 'AgentReassignmentError', + message: 'Cannot find agent etc', + }, + }, + }); const [agent2data, agent3data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent2`), @@ -136,6 +144,54 @@ export default function (providerContext: FtrProviderContext) { expect(agent3data.body.item.policy_id).to.eql('policy2'); }); + it('should allow to reassign multiple agents by id -- mixed invalid, managed, etc', async () => { + // agent1 is enrolled in policy1. set policy1 to managed + await supertest + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxx') + .send({ name: 'Test policy', namespace: 'default', is_managed: true }) + .expect(200); + + const { body } = await supertest + .post(`/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'INVALID_ID', 'agent3'], + policy_id: 'policy2', + }); + + expect(body).to.eql({ + agent2: { + success: false, + error: { + name: 'AgentReassignmentError', + message: 'Cannot reassign an agent from managed agent policy policy1', + }, + }, + INVALID_ID: { + success: false, + error: { + name: 'AgentReassignmentError', + message: 'Cannot find agent INVALID_ID', + }, + }, + agent3: { + success: false, + error: { + name: 'AgentReassignmentError', + message: 'Cannot reassign an agent from managed agent policy policy1', + }, + }, + }); + + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent2`), + supertest.get(`/api/fleet/agents/agent3`), + ]); + expect(agent2data.body.item.policy_id).to.eql('policy1'); + expect(agent3data.body.item.policy_id).to.eql('policy1'); + }); + it('should allow to reassign multiple agents by kuery', async () => { await supertest .post(`/api/fleet/agents/bulk_reassign`) From 3f0381cbe6fdf37230b4aa2e91c619eeaf7dd6d5 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 23 Mar 2021 14:37:19 -0400 Subject: [PATCH 6/9] /bulk_reassign API error is string; not object --- .../fleet/common/types/rest_spec/agent.ts | 2 +- .../fleet/server/routes/agent/handlers.ts | 8 +---- .../apis/agents/reassign.ts | 32 +++++-------------- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index 5a11c1776b031..b654c513e0afb 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -168,7 +168,7 @@ export type PostBulkAgentReassignResponse = Record< Agent['id'], { success: boolean; - error?: Error; + error?: string; } >; diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 001c55a4ab396..4de68d3894082 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -323,13 +323,7 @@ export const postBulkAgentsReassignHandler: RequestHandler< const body = results.items.reduce((acc, so) => { acc[so.id] = { success: !so.error, - error: so.error - ? { - name: so.error.name, - message: so.error.message, - // so.error.stack is also available - } - : undefined, + error: so.error ? so.error.toString() : undefined, }; return acc; }, {}); diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index dd2cea163515b..59a948c8296b6 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -114,25 +114,16 @@ export default function (providerContext: FtrProviderContext) { agent2: { success: true }, INVALID_ID: { success: false, - error: { - name: 'AgentReassignmentError', - message: 'Cannot find agent INVALID_ID', - }, + error: 'AgentReassignmentError: Cannot find agent INVALID_ID', }, agent3: { success: true }, MISSING_ID: { success: false, - error: { - name: 'AgentReassignmentError', - message: 'Cannot find agent MISSING_ID', - }, + error: 'AgentReassignmentError: Cannot find agent MISSING_ID', }, etc: { success: false, - error: { - name: 'AgentReassignmentError', - message: 'Cannot find agent etc', - }, + error: 'AgentReassignmentError: Cannot find agent etc', }, }); @@ -163,24 +154,17 @@ export default function (providerContext: FtrProviderContext) { expect(body).to.eql({ agent2: { success: false, - error: { - name: 'AgentReassignmentError', - message: 'Cannot reassign an agent from managed agent policy policy1', - }, + error: + 'AgentReassignmentError: Cannot reassign an agent from managed agent policy policy1', }, INVALID_ID: { success: false, - error: { - name: 'AgentReassignmentError', - message: 'Cannot find agent INVALID_ID', - }, + error: 'AgentReassignmentError: Cannot find agent INVALID_ID', }, agent3: { success: false, - error: { - name: 'AgentReassignmentError', - message: 'Cannot reassign an agent from managed agent policy policy1', - }, + error: + 'AgentReassignmentError: Cannot reassign an agent from managed agent policy policy1', }, }); From 82bef5d634c2ff2b9a4949b96eddb776014dcaec Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 23 Mar 2021 15:32:37 -0400 Subject: [PATCH 7/9] Add new response & test for bulk_uprade API --- .../common/services/is_agent_upgradeable.ts | 12 +- .../fleet/common/types/rest_spec/agent.ts | 10 +- .../server/routes/agent/upgrade_handler.ts | 10 +- .../fleet/server/services/agents/upgrade.ts | 110 +++++++++++++----- .../apis/agents/upgrade.ts | 76 +++++++++++- 5 files changed, 179 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts index 0350c47816f6d..bb117dd5c5071 100644 --- a/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts +++ b/x-pack/plugins/fleet/common/services/is_agent_upgradeable.ts @@ -17,13 +17,19 @@ export function isAgentUpgradeable(agent: Agent, kibanaVersion: string) { } else { return false; } - if (agent.unenrollment_started_at || agent.unenrolled_at) return false; - if (!agent.local_metadata.elastic.agent.upgradeable) return false; + if (agent.unenrollment_started_at || agent.unenrolled_at) { + return false; + } + if (!agent.local_metadata.elastic.agent.upgradeable) { + return false; + } // make sure versions are only the number before comparison const agentVersionNumber = semverCoerce(agentVersion); if (!agentVersionNumber) throw new Error('agent version is invalid'); const kibanaVersionNumber = semverCoerce(kibanaVersion); if (!kibanaVersionNumber) throw new Error('kibana version is invalid'); - return semverLt(agentVersionNumber, kibanaVersionNumber); + const isAgentLessThanKibana = semverLt(agentVersionNumber, kibanaVersionNumber); + + return isAgentLessThanKibana; } diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index b654c513e0afb..f6a8437ef9dd9 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -141,8 +141,14 @@ export interface PostBulkAgentUpgradeRequest { version: string; }; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PostBulkAgentUpgradeResponse {} + +export type PostBulkAgentUpgradeResponse = Record< + Agent['id'], + { + success: boolean; + error?: string; + } +>; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PostAgentUpgradeResponse {} diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index b8af265883091..0288bcdbe220f 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -99,9 +99,15 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< version, force, }; - await AgentService.sendUpgradeAgentsActions(soClient, esClient, upgradeOptions); + const results = await AgentService.sendUpgradeAgentsActions(soClient, esClient, upgradeOptions); + const body = results.items.reduce((acc, so) => { + acc[so.id] = { + success: !so.error, + error: so.error ? so.error.message || so.error.toString() : undefined, + }; + return acc; + }, {}); - const body: PostBulkAgentUpgradeResponse = {}; return response.ok({ body }); } catch (error) { return defaultIngestErrorHandler({ error, response }); diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index 6c3b404a5b6f3..ac0f39fc9687a 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -7,16 +7,23 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import type { AgentAction, AgentActionSOAttributes } from '../../types'; +import type { Agent, AgentAction, AgentActionSOAttributes, BulkActionResult } from '../../types'; import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../constants'; import { agentPolicyService } from '../../services'; -import { IngestManagerError } from '../../errors'; +import { AgentReassignmentError, IngestManagerError } from '../../errors'; import { isAgentUpgradeable } from '../../../common/services'; import { appContextService } from '../app_context'; import { bulkCreateAgentActions, createAgentAction } from './actions'; import type { GetAgentsOptions } from './crud'; -import { getAgents, updateAgent, bulkUpdateAgents, getAgentPolicyForAgent } from './crud'; +import { + getAgentDocuments, + getAgents, + updateAgent, + bulkUpdateAgents, + getAgentPolicyForAgent, +} from './crud'; +import { searchHitToAgent } from './helpers'; export async function sendUpgradeAgentAction({ soClient, @@ -77,39 +84,75 @@ export async function ackAgentUpgraded( export async function sendUpgradeAgentsActions( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: GetAgentsOptions & { + options: ({ agents: Agent[] } | GetAgentsOptions) & { sourceUri: string | undefined; version: string; force?: boolean; } ) { // Full set of agents - const agentsGiven = await getAgents(esClient, options); + const outgoingErrors: Record = {}; + let givenAgents: Agent[] = []; + if ('agents' in options) { + givenAgents = options.agents; + } else if ('agentIds' in options) { + const givenAgentsResults = await getAgentDocuments(esClient, options.agentIds); + for (const agentResult of givenAgentsResults) { + if (agentResult.found === false) { + outgoingErrors[agentResult._id] = new AgentReassignmentError( + `Cannot find agent ${agentResult._id}` + ); + } else { + givenAgents.push(searchHitToAgent(agentResult)); + } + } + } else if ('kuery' in options) { + givenAgents = await getAgents(esClient, options); + } + const givenOrder = + 'agentIds' in options ? options.agentIds : givenAgents.map((agent) => agent.id); + + // get any policy ids from upgradable agents + const policyIdsToGet = new Set( + givenAgents.filter((agent) => agent.policy_id).map((agent) => agent.policy_id!) + ); + + // get the agent policies for those ids + const agentPolicies = await agentPolicyService.getByIDs(soClient, Array.from(policyIdsToGet), { + fields: ['is_managed'], + }); + const managedPolicies = agentPolicies.reduce>((acc, policy) => { + acc[policy.id] = policy.is_managed; + return acc; + }, {}); // Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check const kibanaVersion = appContextService.getKibanaVersion(); - const upgradeableAgents = options.force - ? agentsGiven - : agentsGiven.filter((agent) => isAgentUpgradeable(agent, kibanaVersion)); - - if (!options.force) { - // get any policy ids from upgradable agents - const policyIdsToGet = new Set( - upgradeableAgents.filter((agent) => agent.policy_id).map((agent) => agent.policy_id!) - ); - - // get the agent policies for those ids - const agentPolicies = await agentPolicyService.getByIDs(soClient, Array.from(policyIdsToGet), { - fields: ['is_managed'], - }); + const agentResults = await Promise.allSettled( + givenAgents.map(async (agent) => { + const isAllowed = options.force || isAgentUpgradeable(agent, kibanaVersion); + if (!isAllowed) { + throw new IngestManagerError(`${agent.id} is not upgradeable`); + } - // throw if any of those agent policies are managed - for (const policy of agentPolicies) { - if (policy.is_managed) { - throw new IngestManagerError(`Cannot upgrade agent in managed policy ${policy.id}`); + if (agent.policy_id && managedPolicies[agent.policy_id]) { + throw new IngestManagerError(`Cannot upgrade agent in managed policy ${agent.policy_id}`); } + return agent; + }) + ); + + // Filter to agents that do not already use the new agent policy ID + const agentsToUpdate = agentResults.reduce((agents, result, index) => { + if (result.status === 'fulfilled') { + agents.push(result.value); + } else { + const id = givenAgents[index].id; + outgoingErrors[id] = result.reason; } - } + return agents; + }, []); + // Create upgrade action for each agent const now = new Date().toISOString(); const data = { @@ -120,7 +163,7 @@ export async function sendUpgradeAgentsActions( await bulkCreateAgentActions( soClient, esClient, - upgradeableAgents.map((agent) => ({ + agentsToUpdate.map((agent) => ({ agent_id: agent.id, created_at: now, data, @@ -129,9 +172,9 @@ export async function sendUpgradeAgentsActions( })) ); - return await bulkUpdateAgents( + await bulkUpdateAgents( esClient, - upgradeableAgents.map((agent) => ({ + agentsToUpdate.map((agent) => ({ agentId: agent.id, data: { upgraded_at: null, @@ -139,4 +182,17 @@ export async function sendUpgradeAgentsActions( }, })) ); + const orderedOut = givenOrder.map((agentId) => { + const hasError = agentId in outgoingErrors; + const result: BulkActionResult = { + id: agentId, + success: !hasError, + }; + if (hasError) { + result.error = outgoingErrors[agentId]; + } + return result; + }); + + return { items: orderedOut }; } diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 9a747fb11a6a6..1ce8485b5b60f 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -540,7 +540,11 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - it('enrolled in a managed policy bulk upgrade should respond with 400 and not update the agent SOs', async () => { + it('enrolled in a managed policy bulk upgrade should respond with 200 and object of results. Should not update the managed agent SOs', async () => { + // move agent2 to policy2 to keep it unmanaged + await supertest.put(`/api/fleet/agents/agent2/reassign`).set('kbn-xsrf', 'xxx').send({ + policy_id: 'policy2', + }); // update enrolled policy to managed await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ name: 'Test policy', @@ -567,7 +571,7 @@ export default function (providerContext: FtrProviderContext) { doc: { local_metadata: { elastic: { - agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, + agent: { upgradeable: true, version: '0.0.0' }, }, }, }, @@ -581,8 +585,12 @@ export default function (providerContext: FtrProviderContext) { version: kibanaVersion, agents: ['agent1', 'agent2'], }) - .expect(400); - expect(body.message).to.contain('Cannot upgrade agent in managed policy policy1'); + .expect(200); + + expect(body).to.eql({ + agent1: { success: false, error: 'Cannot upgrade agent in managed policy policy1' }, + agent2: { success: true }, + }); const [agent1data, agent2data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent1`), @@ -590,7 +598,65 @@ export default function (providerContext: FtrProviderContext) { ]); expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + }); + + it.skip('enrolled in a managed policy bulk upgrade force:true should respond with 200 and object of all success results. Should update the managed agent SOs', async () => { + // update enrolled policy to managed + await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ + name: 'Test policy', + namespace: 'default', + is_managed: true, + }); + + const kibanaVersion = await kibanaServer.version.get(); + await es.update({ + id: 'agent1', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }, + }); + await es.update({ + id: 'agent2', + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { + elastic: { + agent: { upgradeable: true, version: '0.0.0' }, + }, + }, + }, + }, + }); + // attempt to upgrade agent in managed policy + const { body } = await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + agents: ['agent1', 'agent2'], + force: true, + }) + .expect(200); + + expect(body).to.eql({ + agent1: { success: true }, + agent2: { success: true }, + }); + + const [agent1data, agent2data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`), + supertest.get(`/api/fleet/agents/agent2`), + ]); + + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); it('enrolled in a managed policy bulk upgrade with force flag should respond with 200 and update the agent SOs', async () => { From 871ebcbd38dd4ce42d105d731fc9e7eafdbfbe0f Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 23 Mar 2021 15:54:54 -0400 Subject: [PATCH 8/9] Unskip test & fix logic for force ugrade --- x-pack/plugins/fleet/server/services/agents/upgrade.ts | 2 +- x-pack/test/fleet_api_integration/apis/agents/upgrade.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index ac0f39fc9687a..14b8dfaed4d91 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -135,7 +135,7 @@ export async function sendUpgradeAgentsActions( throw new IngestManagerError(`${agent.id} is not upgradeable`); } - if (agent.policy_id && managedPolicies[agent.policy_id]) { + if (!options.force && agent.policy_id && managedPolicies[agent.policy_id]) { throw new IngestManagerError(`Cannot upgrade agent in managed policy ${agent.policy_id}`); } return agent; diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 1ce8485b5b60f..e808c699b0cfc 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -601,7 +601,7 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); - it.skip('enrolled in a managed policy bulk upgrade force:true should respond with 200 and object of all success results. Should update the managed agent SOs', async () => { + it('enrolled in a managed policy bulk upgrade force:true should respond with 200 and object of all success results. Should update the managed agent SOs', async () => { // update enrolled policy to managed await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ name: 'Test policy', From 3334dd1da2b9b21c13b14c0772475e13b2c34588 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 23 Mar 2021 19:00:50 -0400 Subject: [PATCH 9/9] Fix test for force upgrade --- .../apis/agents/upgrade.ts | 60 +------------------ 1 file changed, 3 insertions(+), 57 deletions(-) diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index e808c699b0cfc..41232f73efa5c 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -601,7 +601,7 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); - it('enrolled in a managed policy bulk upgrade force:true should respond with 200 and object of all success results. Should update the managed agent SOs', async () => { + it('enrolled in a managed policy bulk upgrade with force flag should respond with 200 and update the agent SOs', async () => { // update enrolled policy to managed await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ name: 'Test policy', @@ -628,7 +628,7 @@ export default function (providerContext: FtrProviderContext) { doc: { local_metadata: { elastic: { - agent: { upgradeable: true, version: '0.0.0' }, + agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, }, }, }, @@ -642,67 +642,13 @@ export default function (providerContext: FtrProviderContext) { version: kibanaVersion, agents: ['agent1', 'agent2'], force: true, - }) - .expect(200); + }); expect(body).to.eql({ agent1: { success: true }, agent2: { success: true }, }); - const [agent1data, agent2data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent1`), - supertest.get(`/api/fleet/agents/agent2`), - ]); - - expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); - }); - - it('enrolled in a managed policy bulk upgrade with force flag should respond with 200 and update the agent SOs', async () => { - // update enrolled policy to managed - await supertest.put(`/api/fleet/agent_policies/policy1`).set('kbn-xsrf', 'xxxx').send({ - name: 'Test policy', - namespace: 'default', - is_managed: true, - }); - - const kibanaVersion = await kibanaServer.version.get(); - await es.update({ - id: 'agent1', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, - }, - }, - }); - await es.update({ - id: 'agent2', - refresh: 'wait_for', - index: AGENTS_INDEX, - body: { - doc: { - local_metadata: { - elastic: { - agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') }, - }, - }, - }, - }, - }); - // attempt to upgrade agent in managed policy - const { body } = await supertest - .post(`/api/fleet/agents/bulk_upgrade`) - .set('kbn-xsrf', 'xxx') - .send({ - version: kibanaVersion, - agents: ['agent1', 'agent2'], - force: true, - }); - expect(body).to.eql({}); - const [agent1data, agent2data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent1`), supertest.get(`/api/fleet/agents/agent2`),