Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions tenant-rename/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## Rename tenant

Rename a tenant in-place in the database.

This doesn't support consortia.

Disable old tenant `$OLDTENANT` using `$TOKEN` at `$OKAPI`:

```
curl -w'\n\n' -sS -HX-Okapi-Token:$TOKEN $OKAPI/_/proxy/tenants/$OLDTENANT/modules > modules-enable.json
jq '. |= map(.action = "disable")' < modules-enable.json > modules-disable.json
curl -w'\n\n' -sS -D - -HX-Okapi-Token:$TOKEN $OKAPI/_/proxy/tenants/$OLDTENANT/install -d @modules-disable.json
curl -w'\n\n' -sS -D - -HX-Okapi-Token:$TOKEN $OKAPI/_/proxy/tenants/$OLDTENANT -XDELETE
```

Set the connection parameters using the environment variables `PGDATABASE`, `PGHOST`, `PGPORT`, and `PGUSER`
or as arguments to the following commands.

Start psql, load `tenant-rename.sql` and call `tenant_rename`:

```
psql --host= --port= --username= <<EOF
\i tenant-rename.sql
call tenant_rename('$OLDTENANT', '$NEWTENANT');
EOF
```

Enable new tenant `$NEWTENANT` using `$TOKEN` at `$OKAPI` (this requires Okapi >= 6.2.3):

```
curl -w'\n\n' -sS -D - -HX-Okapi-Token:$TOKEN $OKAPI/_/proxy/tenants -d "{\"id\":\"$NEWTENANT\"}"
curl -w'\n\n' -sS -D - -HX-Okapi-Token:$TOKEN $OKAPI/_/proxy/tenants/$NEWTENANT/install -d @modules-enable.json
```

## Dump tenant

Dump $TENANT schemas from $FOLIODB:

```
pg_dump --host= --port= --username= --extension='*' --schema=public "--schema=${TENANT}_mod_*" "$FOLIODB" > schemas_${TENANT}.sql
```

Pick one of the two following role dump methods - either with or without passwords.

Dump $TENANT roles, don't dump passwords, they are not needed if the `tenant-rename.sql` script runs after the restore:

```
pg_dumpall --roles-only --no-role-passwords --host= --port= --username= \
| sed 's/ NOSUPERUSER / /; s/ NOCREATEDB / /; s/ NOREPLICATION / /; s/ NOBYPASSRLS / /;' \
| grep -E -e '^\\' -e '^SET ' -e "^(CREATE ROLE|ALTER ROLE|GRANT) ${TENANT}_mod_" > roles_${TENANT}.sql

```

Dump $TENANT roles, dump includes passwords, this requires superuser permissions, otherwise you get "pg\_dumpall: error: query failed: ERROR: permission denied for table pg\_authid":

```
pg_dumpall --roles-only --host= --port= --username= \
| sed 's/ NOSUPERUSER / /; s/ NOCREATEDB / /; s/ NOREPLICATION / /; s/ NOBYPASSRLS / /;' \
| grep -E -e '^\\' -e '^SET ' -e "^(CREATE ROLE|ALTER ROLE|GRANT) ${TENANT}_mod_" > roles_${TENANT}.sql
```

## Restore tenant

If missing, use psql to create `folio` role and $FOLIODB=`folio` database, but with better password:

```
CREATE ROLE folio WITH PASSWORD 'folio123' LOGIN SUPERUSER;
CREATE DATABASE folio WITH OWNER folio;
```

Use psql to restore the tentant $TENANT from the .sql files into $FOLIODB:

```
cat roles_${TENANT}.sql schemas_${TENANT}.sql | psql --host= --port= --username= "$FOLIODB"
```
205 changes: 205 additions & 0 deletions tenant-rename/tenant-rename.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
CREATE OR REPLACE PROCEDURE tenant_rename(oldtenant text, newtenant text) AS
$PROCEDURE$
DECLARE
regexp text := concat('^', oldtenant, '_(mod|mgr)_');
record RECORD;
newschema text;
sql text;
config text;
BEGIN
-- reject characters that require masking in regexp or start with digit
-- tenant starting with digit is invalid schema name in PostgreSQL
IF oldtenant !~ '^[\w_][\w\d_]*$' THEN
RAISE 'Invalid character in oldtenant: %', oldtenant;
END IF;
IF newtenant !~ '^[\w_][\w\d_]*$' THEN
RAISE 'Invalid character in newtenant: %', newtenant;
END IF;

-- exclusive write lock on all tables
RAISE INFO 'Waiting for write locks ...';
FOR record IN
SELECT schemaname, tablename FROM pg_tables WHERE schemaname ~ regexp
LOOP
EXECUTE format('LOCK TABLE %I.%I IN EXCLUSIVE MODE', record.schemaname, record.tablename);
END LOOP;
RAISE INFO 'Holding all write locks.';

-- rename each role and change role password
FOR record IN
SELECT rolname AS oldschema FROM pg_roles WHERE rolname ~ regexp
LOOP
newschema := regexp_replace(record.oldschema, concat('^', oldtenant), newtenant);
EXECUTE format('ALTER ROLE %I RENAME TO %I', record.oldschema, newschema);
EXECUTE format('ALTER ROLE %I SET search_path TO %I', newschema, newschema);
EXECUTE format('ALTER ROLE %I PASSWORD %L', newschema, newtenant);
END LOOP;

-- rename schema name in source code and config (eg. {search_path=diku_mod_foo}) of functions and procedures
FOR record IN
SELECT pg_proc.oid, nspname AS oldschema, prosrc, proconfig FROM pg_proc, pg_namespace
WHERE pronamespace = pg_namespace.oid AND nspname ~ regexp
LOOP
newschema := regexp_replace(record.oldschema, concat('^', oldtenant), newtenant);
-- \m and \M match at begin and end of a word, word = [a-zA-Z0-9_]+
sql := regexp_replace(record.prosrc, concat('\m', record.oldschema, '\M'), newschema, 'g');
-- https://github.com/folio-org/mod-data-export/blob/v5.3.0/src/main/resources/db/changelog/changes/slice_instances_all_ids.sql#L8
-- https://github.com/folio-org/mod-data-export/blob/v5.3.0/src/main/resources/db/changelog/changes/slice_holdings_all_ids.sql#L8
sql := replace(sql, concat(' ', oldtenant, '_mod_inventory_storage.'),
concat(' ', newtenant, '_mod_inventory_storage.') );
IF pg_typeof(record.proconfig) = 'text[]'::regtype THEN
config := regexp_replace(record.proconfig[1], concat('\m', record.oldschema, '\M'), newschema, 'g');
CONTINUE WHEN sql IS NOT DISTINCT FROM record.prosrc AND
config IS NOT DISTINCT FROM record.proconfig[1];
UPDATE pg_proc SET prosrc = sql, proconfig[1] = config WHERE oid = record.oid;
ELSE
config := regexp_replace(record.proconfig, concat('\m', record.oldschema, '\M'), newschema, 'g');
CONTINUE WHEN sql IS NOT DISTINCT FROM record.prosrc AND
config IS NOT DISTINCT FROM record.proconfig;
UPDATE pg_proc SET prosrc = sql, proconfig = config WHERE oid = record.oid;
END IF;
END LOOP;

-- rename schema name in rmb_internal_index.def
-- https://github.com/folio-org/mod-circulation-storage/blob/v17.4.0/src/main/resources/templates/db_scripts/index_dateLostItemShouldBeBilled.sql#L10
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname ~ regexp AND tablename = 'rmb_internal_index'
LOOP
newschema := regexp_replace(record.oldschema, concat('^', oldtenant), newtenant);
EXECUTE format('UPDATE %I.rmb_internal_index SET def = replace(replace(def, %L, %L), %L, %L)',
record.oldschema,
concat(' ', record.oldschema, '.'),
concat(' ', newschema, '.'),
concat('(', record.oldschema, '.'),
concat('(', newschema, '.'));
END LOOP;

-- rename schema name in rmb_job.jsonb->>'tenant'
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname ~ regexp AND tablename = 'rmb_job'
LOOP
EXECUTE format($$ UPDATE %I.rmb_job
SET jsonb = jsonb_set(jsonb, '{tenant}', to_jsonb(%L::text))
WHERE jsonb->>'tenant' = %L $$,
record.oldschema, newtenant, oldtenant);
END LOOP;

-- delete wrong md5sum in the `databasechangelog` liquibase table
-- liquibase will replace it with the new md5sum on next run
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname ~ regexp AND tablename = 'databasechangelog'
LOOP
EXECUTE format('UPDATE %I.databasechangelog SET md5sum = NULL', record.oldschema);
END LOOP;

-- rename tenant name in mod_agreements log_entry_additional_info
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_agreements') AND tablename = 'log_entry_additional_info'
LOOP
EXECUTE format($$ UPDATE %I.log_entry_additional_info
SET additional_info_elt = %L
WHERE additional_info_idx = 'tenantId' AND additional_info_elt = %L $$,
record.oldschema, concat(newtenant, '_mod_agreements'), concat(oldtenant, '_mod_agreements'));
EXECUTE format($$ UPDATE %I.log_entry_additional_info
SET additional_info_elt = %L
WHERE additional_info_idx IN ('tenant', '{tenant}') AND additional_info_elt = %L $$,
record.oldschema, newtenant, oldtenant);
END LOOP;

-- rename tenant name in mod_fqm_manager entity_type_definition
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_fqm_manager') AND tablename = 'entity_type_definition'
LOOP
EXECUTE format($$ UPDATE %I.entity_type_definition SET definition = regexp_replace(definition::text, %L, %L, 'g')::json $$,
record.oldschema,
concat('\m', oldtenant, '_mod_'),
concat(newtenant, '_mod_'));
END LOOP;

-- rename tenant name in mod_pubsub audit_message.tenant_id
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_pubsub') AND tablename = 'audit_message'
LOOP
EXECUTE format('UPDATE %I.audit_message SET tenant_id = %L WHERE tenant_id = %L',
record.oldschema, newtenant, oldtenant);
END LOOP;

-- rename tenant name in mod_search consortium_instance.tenant_id
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_search') AND tablename = 'consortium_instance'
LOOP
EXECUTE format('UPDATE %I.consortium_instance SET tenant_id = %L WHERE tenant_id = %L',
record.oldschema, newtenant, oldtenant);
END LOOP;

-- rename tenant name in mod_search holding.tenant_id
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_search') AND tablename = 'holding'
LOOP
EXECUTE format('UPDATE %I.holding SET tenant_id = %L WHERE tenant_id = %L',
record.oldschema, newtenant, oldtenant);
END LOOP;

-- rename tenant name in mod_search instance.tenant_id
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_search') AND tablename = 'instance'
LOOP
EXECUTE format('UPDATE %I.instance SET tenant_id = %L WHERE tenant_id = %L',
record.oldschema, newtenant, oldtenant);
END LOOP;

-- rename tenant name in mod_search instance_classification.tenant_id
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_search') AND tablename = 'instance_classification'
LOOP
EXECUTE format('UPDATE %I.instance_classification SET tenant_id = %L WHERE tenant_id = %L',
record.oldschema, newtenant, oldtenant);
END LOOP;

-- rename tenant name in mod_search merge_range.tenant_id
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_search') AND tablename = 'merge_range'
LOOP
EXECUTE format('UPDATE %I.merge_range SET tenant_id = %L WHERE tenant_id = %L',
record.oldschema, newtenant, oldtenant);
END LOOP;

-- rename tenant name in mod_source_record_manager journal_records.tenant_id
FOR record IN
SELECT schemaname AS oldschema FROM pg_tables
WHERE schemaname = concat(oldtenant, '_mod_source_record_manager') AND tablename = 'journal_records'
LOOP
EXECUTE format('UPDATE %I.journal_records SET tenant_id = %L WHERE tenant_id = %L',
record.oldschema, newtenant, oldtenant);
END LOOP;

-- rename schemata
FOR record IN
SELECT schema_name AS oldschema FROM information_schema.schemata WHERE schema_name ~ regexp
LOOP
newschema := regexp_replace(record.oldschema, concat('^', oldtenant), newtenant);
EXECUTE format('ALTER SCHEMA %I RENAME TO %I', record.oldschema, newschema);
END LOOP;

-- rename tenant in column mod_agreements__system.known_tenant.at_name
BEGIN
UPDATE mod_agreements__system.known_tenant SET at_name = newtenant WHERE at_name = oldtenant;
EXCEPTION
-- ignore if schema or table doesn't exist
WHEN OTHERS THEN NULL;
END;

RAISE INFO 'Renames completed.';
END;
$PROCEDURE$ LANGUAGE plpgsql;