Skip to content

Commit

Permalink
Fb/hdi enable tenant (#64)
Browse files Browse the repository at this point in the history
* start of hdi enable tenant

* basic wiring for hdi-enable-all

* outline code

* early stages

* early stages 2

* early stages 3

* early stages 4

* workable impl 1

* workable impl 2

* workable impl 3

* workable impl 4

* naming

* add db_tenant_id

* only show db_tenant_id column of some binding has a tenantId

* docs

* remove enablement polling

* promise all

* re-record hdi snapshots, to reflect hb_tenant_id

* rename command to --hdi-enable-native
  • Loading branch information
rlindner81 authored Feb 20, 2024
1 parent 4e8e0b8 commit c4acb06
Show file tree
Hide file tree
Showing 9 changed files with 4,787 additions and 5,260 deletions.
16 changes: 15 additions & 1 deletion docs/hana-management/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ Commands for this area are:
hdirt --hdi-rebind-tenant TENANT_ID [PARAMS] rebind tenant hdi container instances
hdira --hdi-rebind-all [PARAMS] rebind all hdi container instances
--hdi-repair-bindings [PARAMS] create missing and delete ambiguous bindings
--hdi-enable-native [TENANT_ID] enable hana native tenants
* --hdi-delete-tenant TENANT_ID delete hdi container instance and bindings for tenant
* --hdi-delete-all delete all hdi container instances and bindings
... [TENANT_ID] filter list for tenant id
... [TENANT_ID] filter for tenant id
... [PARAMS] create binding with custom parameters
... --reveal show passwords
... --time list includes timestamps
Expand Down Expand Up @@ -147,6 +148,19 @@ manager. In other words, `mtx hdirt <tenant_id> '{"special":true}'` corresponds
cf bind-service <service-manager> <hdi-shared service-instance of tenant_id> -c '{"special":true}'
```

## HDI Enable

The enablement command `mtx --hdi-enable-native` is a convenience functionality for enabling HANA native tenant
capabilities for all tenants or, if used with a TENANT_ID filter, for a single tenant. The enablement is split into two
parts. The first part happens synchronously, and in it the database is actually set up to support new capabilities. The
second part happens asynchronously, and in it the tenant data is (re-)encrypted in the background. In other words,
during the first part the tenant data is not accessible and during the second part the database can be used normally.

The enablement convenience command will temporarily remove the relevant bindings, to protect the database from
application accesses during the first part of the enablement. When the command finishes, only the first part of the
process is finished. The second part will still happen in the background for a while, depending on how many tenants
and data the database needs to process. The temporarily removed bindings will be restored automatically.

## HDI Delete

The deletion commands are only sensible for cleanup after some mocking/testing purposes.
Expand Down
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ commands:
hdirt --hdi-rebind-tenant TENANT_ID [PARAMS] rebind tenant hdi container instances
hdira --hdi-rebind-all [PARAMS] rebind all hdi container instances
--hdi-repair-bindings [PARAMS] create missing and delete ambiguous bindings
--hdi-enable-native [TENANT_ID] enable hana native tenants
* --hdi-delete-tenant TENANT_ID delete hdi container instance and bindings for tenant
* --hdi-delete-all delete all hdi container instances and bindings
... [TENANT_ID] filter list for tenant id
... [TENANT_ID] filter for tenant id
... [PARAMS] create binding with custom parameters
... --reveal show passwords
... --time list includes timestamps
Expand Down
3 changes: 2 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ commands:
hdirt --hdi-rebind-tenant TENANT_ID [PARAMS] rebind tenant hdi container instances
hdira --hdi-rebind-all [PARAMS] rebind all hdi container instances
--hdi-repair-bindings [PARAMS] create missing and delete ambiguous bindings
--hdi-enable-native [TENANT_ID] enable hana native tenants
* --hdi-delete-tenant TENANT_ID delete hdi container instance and bindings for tenant
* --hdi-delete-all delete all hdi container instances and bindings
... [TENANT_ID] filter list for tenant id
... [TENANT_ID] filter for tenant id
... [PARAMS] create binding with custom parameters
... --reveal show passwords
... --time list includes timestamps
Expand Down
6 changes: 6 additions & 0 deletions src/cliOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ module.exports = {
callback: hdi.hdiRepairBindings,
useCache: false,
},
HDI_ENABLE_NATIVE: {
commandVariants: ["--hdi-enable-native"],
optionalPassArgs: [PASS_ARG.TENANT_ID],
callback: hdi.hdiEnableNative,
useCache: false,
},
HDI_DELETE_TENANT: {
commandVariants: ["--hdi-delete-tenant"],
requiredPassArgs: [PASS_ARG.TENANT_ID],
Expand Down
139 changes: 114 additions & 25 deletions src/submodules/hanaManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,9 @@ const _createBindingServiceManager = async (
method: "POST",
url: sm_url,
pathname: `/v1/service_bindings`,
query: {
async: false,
},
query: { async: false },
auth: { token },
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
service_instance_id,
Expand Down Expand Up @@ -104,9 +100,7 @@ const _deleteBindingServiceManager = async (sm_url, token, id) => {
method: "DELETE",
url: sm_url,
pathname: `/v1/service_bindings/${id}`,
query: {
async: false,
},
query: { async: false },
auth: { token },
});
};
Expand All @@ -116,9 +110,7 @@ async function _deleteInstanceServiceManager(sm_url, token, id) {
method: "DELETE",
url: sm_url,
pathname: `/v1/service_instances/${id}`,
query: {
async: false,
},
query: { async: false },
auth: { token },
});
}
Expand Down Expand Up @@ -257,15 +249,15 @@ const _hdiRebindAllServiceManager = async (context, parameters) => {
);
};

const _hdiRepairBindingsServiceManager = async (context, parameters) => {
const _hdiRepairBindingsServiceManager = async (context, { instances, bindings, parameters } = {}) => {
const {
cfService: { credentials },
} = await context.getHdiInfo();
const { sm_url } = credentials;
const token = await context.getCachedUaaTokenFromCredentials(credentials);

const instances = await _hdiInstancesServiceManager(context);
const bindings = await _hdiBindingsServiceManager(context);
instances = instances ?? (await _hdiInstancesServiceManager(context));
bindings = bindings ?? (await _hdiBindingsServiceManager(context));
const bindingsByInstance = _getBindingsByInstance(bindings);
instances.sort(compareForServiceManagerTenantId);

Expand Down Expand Up @@ -459,12 +451,17 @@ function _getBindingsByInstance(bindings) {
}

const hdiListServiceManager = async (context, filterTenantId, doTimestamps) => {
const instances = await _hdiInstancesServiceManager(context, { filterTenantId });
const bindings = await _hdiBindingsServiceManager(context, { filterTenantId });
const [instances, bindings] = await Promise.all([
_hdiInstancesServiceManager(context, { filterTenantId }),
_hdiBindingsServiceManager(context, { filterTenantId }),
]);

const bindingsByInstance = _getBindingsByInstance(bindings);
instances.sort(compareForServiceManagerTenantId);

const doShowDbTenantColumn = bindings.some((binding) => binding.credentials?.tenantId);
const headerRow = ["tenant_id", "host", "schema", "ready"];
doShowDbTenantColumn && headerRow.splice(1, 0, "db_tenant_id");
doTimestamps && headerRow.push("created_on", "updated_on");
const nowDate = new Date();
const instanceMap = (instance) => {
Expand All @@ -475,6 +472,7 @@ const hdiListServiceManager = async (context, filterTenantId, doTimestamps) => {
binding ? binding.credentials?.schema : "",
instance.ready,
];
doShowDbTenantColumn && row.splice(1, 0, binding ? binding.credentials?.tenantId ?? "" : "missing binding");
doTimestamps && row.push(...formatTimestampsWithRelativeDays([instance.created_at, instance.updated_at], nowDate));
return row;
};
Expand Down Expand Up @@ -502,8 +500,10 @@ const hdiList = async (context, [tenantId], [doTimestamps]) =>
: hdiListInstanceManager(context, tenantId, doTimestamps);

const _hdiLongListServiceManager = async (context, filterTenantId, doReveal) => {
const instances = await _hdiInstancesServiceManager(context, { filterTenantId, doEnsureTenantLabel: false });
const bindings = await _hdiBindingsServiceManager(context, { filterTenantId, doReveal, doEnsureTenantLabel: false });
const [instances, bindings] = await Promise.all([
_hdiInstancesServiceManager(context, { filterTenantId, doEnsureTenantLabel: false }),
_hdiBindingsServiceManager(context, { filterTenantId, doReveal, doEnsureTenantLabel: false }),
]);
return `
=== container instance${instances.length === 1 ? "" : "s"} ===
Expand All @@ -521,8 +521,10 @@ const hdiLongList = async (context, [filterTenantId], [doReveal]) =>
: _formatOutput(await _hdiContainersInstanceManager(context, { filterTenantId, doReveal }));

const _hdiListRelationsServiceManager = async (context, filterTenantId, doTimestamps) => {
const instances = await _hdiInstancesServiceManager(context, { filterTenantId });
const bindings = await _hdiBindingsServiceManager(context, { filterTenantId });
const [instances, bindings] = await Promise.all([
_hdiInstancesServiceManager(context, { filterTenantId }),
_hdiBindingsServiceManager(context, { filterTenantId }),
]);
const bindingsByInstance = _getBindingsByInstance(bindings);
instances.sort(compareForServiceManagerTenantId);

Expand Down Expand Up @@ -565,7 +567,7 @@ const hdiListRelations = async (context, [tenantId], [doTimestamps]) => {
};

const hdiRebindTenant = async (context, [tenantId, rawParameters]) => {
assert(isValidTenantId(tenantId), `argument "${tenantId}" is not a valid hdi tenantId`);
assert(isValidTenantId(tenantId), `argument "${tenantId}" is not a valid hdi tenant id`);
const parameters = tryJsonParse(rawParameters);
assert(!rawParameters || isObject(parameters), `argument "${rawParameters}" needs to be a valid JSON object`);
return (await _isServiceManager(context))
Expand All @@ -584,16 +586,16 @@ const hdiRepairBindings = async (context, [rawParameters]) => {
assert(await _isServiceManager(context), "repair bindings is only supported for service-manager");
const parameters = tryJsonParse(rawParameters);
assert(!rawParameters || isObject(parameters), `argument "${rawParameters}" needs to be a valid JSON object`);
return await _hdiRepairBindingsServiceManager(context, parameters);
return await _hdiRepairBindingsServiceManager(context, { parameters });
};

const hdiTunnelTenant = async (context, [tenantId], [doReveal]) => {
assert(isValidTenantId(tenantId), `argument "${tenantId}" is not a valid hdi tenantId`);
assert(isValidTenantId(tenantId), `argument "${tenantId}" is not a valid hdi tenant id`);
return _hdiTunnel(context, tenantId, doReveal);
};

const hdiDeleteTenant = async (context, [tenantId]) => {
assert(isValidTenantId(tenantId), "TENANT_ID is not a valid hdi tenantId", tenantId);
assert(isValidTenantId(tenantId), `argument "${tenantId}" is not a valid hdi tenant id`);
return await _hdiDelete(context, tenantId);
};

Expand Down Expand Up @@ -643,6 +645,92 @@ const hdiDeleteAllInstanceManager = async (context) => {
const hdiDeleteAll = async (context) =>
(await _isServiceManager(context)) ? hdiDeleteAllServiceManager(context) : hdiDeleteAllInstanceManager(context);

const hdiEnableNative = async (context, [tenantId]) => {
assert(await _isServiceManager(context), "enable tenant is only supported for service-manager");
assert(!tenantId || isValidTenantId(tenantId), `argument "${tenantId}" is not a valid hdi tenant id`);

const {
cfService: { credentials },
} = await context.getHdiInfo();
const { sm_url } = credentials;
const token = await context.getCachedUaaTokenFromCredentials(credentials);

// get all instances and bindings
const [instances, bindings] = await Promise.all([
_hdiInstancesServiceManager(context, { filterTenantId: tenantId }),
_hdiBindingsServiceManager(context, { filterTenantId: tenantId }),
]);

// filter instances and bindings
const migrationInstances = [];
const alreadyMigratedTenants = [];
await limiter(hdiRequestConcurrency, instances, async (instance) => {
const parametersResponse = await request({
url: sm_url,
pathname: `/v1/service_instances/${instance.id}/parameters`,
auth: { token },
logged: false,
});
const parameters = await parametersResponse.json();
if (parameters.enableTenant) {
alreadyMigratedTenants.push(instance.labels.tenant_id[0]);
} else {
migrationInstances.push(instance);
}
});
const migrationTenants = migrationInstances.map((instance) => instance.labels.tenant_id[0]);
const migrationBindings = bindings.filter((binding) =>
migrationInstances.some((instance) => instance.id === binding.service_instance_id)
);

if (alreadyMigratedTenants.length) {
console.log("skipping %i already enabled tenants", alreadyMigratedTenants.length);
}
if (migrationInstances.length && migrationTenants.length) {
console.log("enabling %i tenants %s", migrationTenants.length, migrationTenants.join(", "));
} else {
return;
}

// delete all bindings related to migration instances
console.log("deleting %i bindings to protect enablement", migrationBindings.length);
await limiter(
hdiRequestConcurrency,
migrationBindings,
async (binding) => await _deleteBindingServiceManager(sm_url, token, binding.id)
);

// send enable tenant patch request and poll until succeeded
try {
await limiter(hdiRequestConcurrency, migrationInstances, async (instance) => {
const enableResponse = await request({
method: "PATCH",
url: sm_url,
pathname: `/v1/service_instances/${instance.id}`,
query: { async: false },
auth: { token },
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
parameters: {
enableTenant: true,
},
}),
});
const checkData = await enableResponse.json();
assert(
checkData.last_operation?.state === "succeeded",
"enable tenant operation was not marked succeeded\n%j",
checkData
);
});
} finally {
// repair bindings for migrated tenants
if (migrationInstances.length) {
await _hdiRepairBindingsServiceManager(context, { instances: migrationInstances, bindings: [] });
}
}
};

module.exports = {
hdiList,
hdiLongList,
Expand All @@ -653,4 +741,5 @@ module.exports = {
hdiTunnelTenant,
hdiDeleteTenant,
hdiDeleteAll,
hdiEnableNative,
};
Loading

0 comments on commit c4acb06

Please sign in to comment.