-- The store of Terraform modules, their instances and the instances' variable files: the tables tf_modules, -- tf_instances, tf_variable_files and tf_variable_revisions, and the functions and procedures the store calls. The -- store applies the file on its first use in the schema its connection's search path names; every statement can run -- again. CREATE TABLE IF NOT EXISTS tf_modules ( name text PRIMARY KEY CHECK (name ~ '^[a-z][a-z0-9-]*$'), root text NOT NULL CHECK (root <> ''), description text NOT NULL DEFAULT '', configurator_url text ); CREATE TABLE IF NOT EXISTS tf_instances ( module text NOT NULL REFERENCES tf_modules (name), name text NOT NULL CHECK (name ~ '^[a-z][a-z0-9-]*$'), state_schema text NOT NULL UNIQUE CHECK (state_schema ~ '^[a-z_][a-z0-9_]*$'), created timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (module, name) ); CREATE TABLE IF NOT EXISTS tf_variable_files ( module text NOT NULL, instance text NOT NULL, name text NOT NULL CHECK (name ~ '^[A-Za-z0-9_.-]+\.tfvars\.json$'), content jsonb NOT NULL CHECK (jsonb_typeof(content) = 'object'), previous jsonb CHECK (jsonb_typeof(previous) = 'object'), updated timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (module, instance, name), FOREIGN KEY (module, instance) REFERENCES tf_instances (module, name) ON DELETE CASCADE ); -- Every write of an instance's variable files: all its files after the write, by name, the schema version of their -- layout, null when not known, and the application that wrote them. CREATE TABLE IF NOT EXISTS tf_variable_revisions ( module text NOT NULL, instance text NOT NULL, revision integer NOT NULL CHECK (revision > 0), files jsonb NOT NULL CHECK (jsonb_typeof(files) = 'object'), schema integer CHECK (schema > 0), producer text NOT NULL CHECK (producer <> ''), saved timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (module, instance, revision), FOREIGN KEY (module, instance) REFERENCES tf_instances (module, name) ON DELETE CASCADE ); -- The files of an instance that has no revision are its revision 1, of an unknown schema and producer. INSERT INTO tf_variable_revisions (module, instance, revision, files, schema, producer, saved) SELECT f.module, f.instance, 1, jsonb_object_agg(f.name, f.content), NULL, 'unknown', max(f.updated) FROM tf_variable_files f WHERE NOT EXISTS (SELECT 1 FROM tf_variable_revisions r WHERE r.module = f.module AND r.instance = f.instance) GROUP BY f.module, f.instance ON CONFLICT DO NOTHING; -- The modules, by name. CREATE OR REPLACE FUNCTION tf_modules_list() RETURNS TABLE (name text, root text, description text, configurator_url text) LANGUAGE sql STABLE SET search_path FROM CURRENT AS $$ SELECT m.name, m.root, m.description, m.configurator_url FROM tf_modules m ORDER BY m.name $$; -- Creates a module, or changes the one of that name. CREATE OR REPLACE PROCEDURE tf_module_put(module_name text, module_root text, module_description text, module_configurator_url text) LANGUAGE sql SET search_path FROM CURRENT AS $$ INSERT INTO tf_modules (name, root, description, configurator_url) VALUES (module_name, module_root, module_description, module_configurator_url) ON CONFLICT (name) DO UPDATE SET root = excluded.root, description = excluded.description, configurator_url = excluded.configurator_url $$; -- Deletes a module; the foreign key of tf_instances refuses it while the module has instances. CREATE OR REPLACE PROCEDURE tf_module_delete(module_name text) LANGUAGE sql SET search_path FROM CURRENT AS $$ DELETE FROM tf_modules WHERE name = module_name $$; -- The instances of a module, by name. CREATE OR REPLACE FUNCTION tf_instances_list(module_name text) RETURNS TABLE (module text, name text, state_schema text, created timestamptz) LANGUAGE sql STABLE SET search_path FROM CURRENT AS $$ SELECT i.module, i.name, i.state_schema, i.created FROM tf_instances i WHERE i.module = module_name ORDER BY i.name $$; -- Creates an instance of a module with the schema of its state. CREATE OR REPLACE PROCEDURE tf_instance_create(module_name text, instance_name text, instance_state_schema text) LANGUAGE sql SET search_path FROM CURRENT AS $$ INSERT INTO tf_instances (module, name, state_schema) VALUES (module_name, instance_name, instance_state_schema) $$; -- Deletes an instance with its variable files. CREATE OR REPLACE PROCEDURE tf_instance_delete(module_name text, instance_name text) LANGUAGE sql SET search_path FROM CURRENT AS $$ DELETE FROM tf_instances WHERE module = module_name AND name = instance_name $$; -- The variable files of an instance, by name: content, whether the content it replaced is kept, and the time of the -- last write. CREATE OR REPLACE FUNCTION tf_variable_files_read(module_name text, instance_name text) RETURNS TABLE (name text, content jsonb, previous boolean, updated timestamptz) LANGUAGE sql STABLE SET search_path FROM CURRENT AS $$ SELECT f.name, f.content, f.previous IS NOT NULL, f.updated FROM tf_variable_files f WHERE f.module = module_name AND f.instance = instance_name ORDER BY f.name $$; -- Keeps other writers out until the end of the transaction, from the comparison of the version to the commit. CREATE OR REPLACE PROCEDURE tf_variable_files_lock() LANGUAGE sql SET search_path FROM CURRENT AS $$ LOCK TABLE tf_variable_files IN EXCLUSIVE MODE $$; -- Writes a variable file of an instance, keeping the content it replaces as previous. CREATE OR REPLACE PROCEDURE tf_variable_file_put(module_name text, instance_name text, file_name text, file_content jsonb) LANGUAGE sql SET search_path FROM CURRENT AS $$ INSERT INTO tf_variable_files (module, instance, name, content) VALUES (module_name, instance_name, file_name, file_content) ON CONFLICT (module, instance, name) DO UPDATE SET previous = tf_variable_files.content, content = excluded.content, updated = now() $$; -- Deletes a variable file of an instance. CREATE OR REPLACE PROCEDURE tf_variable_file_delete(module_name text, instance_name text, file_name text) LANGUAGE sql SET search_path FROM CURRENT AS $$ DELETE FROM tf_variable_files WHERE module = module_name AND instance = instance_name AND name = file_name $$; -- Records the variable files of an instance as its next revision, with the schema of their layout and their producer; -- called with the lock of tf_variable_files_lock held. CREATE OR REPLACE PROCEDURE tf_variable_revision_add(module_name text, instance_name text, revision_schema integer, revision_producer text) LANGUAGE sql SET search_path FROM CURRENT AS $$ INSERT INTO tf_variable_revisions (module, instance, revision, files, schema, producer) SELECT module_name, instance_name, (SELECT coalesce(max(r.revision), 0) + 1 FROM tf_variable_revisions r WHERE r.module = module_name AND r.instance = instance_name), (SELECT coalesce(jsonb_object_agg(f.name, f.content), '{}'::jsonb) FROM tf_variable_files f WHERE f.module = module_name AND f.instance = instance_name), revision_schema, revision_producer $$; -- The revisions of an instance, newest first, with the names of their files. CREATE OR REPLACE FUNCTION tf_variable_revisions_list(module_name text, instance_name text) RETURNS TABLE (revision integer, schema integer, producer text, saved timestamptz, names text[]) LANGUAGE sql STABLE SET search_path FROM CURRENT AS $$ SELECT r.revision, r.schema, r.producer, r.saved, ARRAY(SELECT jsonb_object_keys(r.files) ORDER BY 1) FROM tf_variable_revisions r WHERE r.module = module_name AND r.instance = instance_name ORDER BY r.revision DESC $$; -- A revision of an instance with its files. CREATE OR REPLACE FUNCTION tf_variable_revision_get(module_name text, instance_name text, revision_number integer) RETURNS TABLE (revision integer, schema integer, producer text, saved timestamptz, files jsonb) LANGUAGE sql STABLE SET search_path FROM CURRENT AS $$ SELECT r.revision, r.schema, r.producer, r.saved, r.files FROM tf_variable_revisions r WHERE r.module = module_name AND r.instance = instance_name AND r.revision = revision_number $$;