diff --git a/Makefile b/Makefile index b0bc24aef..75dfd98ef 100644 --- a/Makefile +++ b/Makefile @@ -121,6 +121,9 @@ protobuild: @protoc-go-inject-tag -input=./internal/db/db_test/db_test.pb.go @protoc-go-inject-tag -input=./internal/host/store/host.pb.go @protoc-go-inject-tag -input=./internal/host/static/store/static.pb.go + @protoc-go-inject-tag -input=./internal/host/plugin/store/host.pb.go + @protoc-go-inject-tag -input=./internal/plugin/host/store/plugin.pb.go + @protoc-go-inject-tag -input=./internal/plugin/store/plugin.pb.go @protoc-go-inject-tag -input=./internal/authtoken/store/authtoken.pb.go @protoc-go-inject-tag -input=./internal/auth/store/account.pb.go @protoc-go-inject-tag -input=./internal/auth/password/store/password.pb.go diff --git a/internal/db/schema/migrations/postgres/16/01_plugin.up.sql b/internal/db/schema/migrations/postgres/16/01_plugin.up.sql new file mode 100644 index 000000000..9ac669031 --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/01_plugin.up.sql @@ -0,0 +1,78 @@ +begin; + + -- We are updating the plugin table here to have a scope id, since + -- subtypes of plugin should be scoped. We use 'global' as the default + -- just for the purpose of the migration since the scope cannot be null + -- and the one plugin that already exists (pi_system) can be globally + -- scoped. The immediately following statement removes this default. + -- We add a name so we can enforce that all plugins have a unique name + -- in a specific scope across all plugin subtypes. + alter table plugin + add column name wt_name, + add column scope_id wt_scope_id + not null + default 'global' + constraint iam_scope_global_fkey + references iam_scope_global(scope_id) + on delete cascade + on update cascade; + + alter table plugin + alter column scope_id drop default; + + -- Add constraints that enforce names are unique across all plugin subtypes + -- in a specific scope. + alter table plugin + add constraint plugin_scope_id_public_id_uq + unique (scope_id, public_id), + add constraint plugin_scope_id_name_uq + unique (scope_id, name); + + + -- insert, update, and delete plugin_subtypes are created since we are adding + -- subtyped plugins and we need to keep the base table plugin in sync with all + -- subtype tables. + create function insert_plugin_subtype() + returns trigger + as $$ + begin + insert into plugin + (public_id, scope_id, name) + values + (new.public_id, new.scope_id, new.name); + return new; + end; + $$ language plpgsql; + comment on function insert_plugin_subtype() is + 'insert_plugin_subtype() inserts sub type name into the base type plugin table'; + + create function update_plugin_subtype() + returns trigger + as $$ + begin + update plugin set name = new.name where public_id = new.public_id and new.name != name; + return new; + end; + $$ language plpgsql; + comment on function update_plugin_subtype() is + 'update_plugin_subtype() will update base plugin type name column with new values from sub type'; + + -- delete_plugin_subtype() is an after delete trigger function + -- for subtypes of plugin + create function delete_plugin_subtype() + returns trigger + as $$ + begin + delete from plugin + where public_id = old.public_id; + return null; -- result is ignored since this is an after trigger + end; + $$ language plpgsql; + comment on function delete_plugin_subtype is + 'delete_plugin_subtype() is an after trigger function for subytypes of plugin'; + + insert into oplog_ticket (name, version) + values + ('plugin', 1); + +commit; diff --git a/internal/db/schema/migrations/postgres/16/02_hostplugin.up.sql b/internal/db/schema/migrations/postgres/16/02_hostplugin.up.sql new file mode 100644 index 000000000..dd3f59077 --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/02_hostplugin.up.sql @@ -0,0 +1,89 @@ +begin; + +/* + ┌──────────────────┐ + │ plugin │ + ├──────────────────┤ + │public_id (pk) │ + │scope_id (fk) │ + └──────────────────┘ + ┼ + ┼ + │ + ┼ + ┼ + ┌──────────────────┐ + │ plugin_host │ + ├──────────────────┤ + │public_id (pk) │ + │scope_id (fk) │ + │name │ + │description │ + │version │ + │plugin_name │ + │id_prefix │ + └──────────────────┘ +*/ + create table plugin_host ( + public_id wt_plugin_id primary key, + scope_id wt_scope_id not null + -- TODO: Allow plugins to be created in different scopes and + -- constrain the host-catalog's plugin reference accordingly. + constraint iam_scope_global_fkey + references iam_scope_global(scope_id) + on delete cascade + on update cascade, + name wt_name, + description text, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + plugin_name text not null + constraint plugin_name_must_be_not_empty + check(length(trim(plugin_name)) > 0) + constraint plugin_name_must_be_lowercase + check(lower(trim(plugin_name)) = plugin_name) + constraint plugin_host_plugin_name_uq + unique, + id_prefix text not null + constraint plugin_id_prefix_must_be_not_empty + check(length(trim(id_prefix)) > 0) + constraint plugin_id_prefix_must_fit_format + check (id_prefix ~ '^[a-z0-9]*$') + constraint plugin_host_id_prefix_uq + unique, + constraint plugin_fkey + foreign key (scope_id, public_id) + references plugin(scope_id, public_id) + on delete cascade + on update cascade, + constraint plugin_host_scope_id_name_uq + unique(scope_id, name) + ); + + create trigger update_version_column after update on plugin_host + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on plugin_host + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on plugin_host + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on plugin_host + for each row execute procedure immutable_columns('public_id', 'create_time', 'plugin_name'); + + create trigger insert_plugin_subtype before insert on plugin_host + for each row execute procedure insert_plugin_subtype(); + + create trigger update_plugin_subtype before update on plugin_host + for each row execute procedure update_plugin_subtype(); + + create trigger delete_plugin_subtype after delete on plugin_host + for each row execute procedure delete_plugin_subtype(); + + insert into oplog_ticket (name, version) + values + ('plugin_host', 1); + +commit; diff --git a/internal/db/schema/migrations/postgres/16/03_host.up.sql b/internal/db/schema/migrations/postgres/16/03_host.up.sql new file mode 100644 index 000000000..2597f9700 --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/03_host.up.sql @@ -0,0 +1,61 @@ +begin; + + -- We are adding the name to the base host catalog type. This allows the db + -- to ensure that catalog names are unique in a scope across all subtypes. + alter table host_catalog + add column name wt_name; + + alter table host_catalog + add constraint host_catalog_scope_id_name_uq + unique (scope_id, name); + + -- Now that we've added the name column to the base type, we copy + -- the name from the static host catalog table into the base table. + update host_catalog + set name = st.name + from + static_host_catalog st + where + host_catalog.public_id = st.public_id and + st.name is not null; + + -- Replace the insert_host_catalog_subtype function defined in 0/20_host.up.sql + -- to include the name. + -- insert_host_catalog_subtype() is a before insert trigger + -- function for subtypes of host_catalog + create or replace function insert_host_catalog_subtype() + returns trigger + as $$ + begin + insert into host_catalog + (public_id, scope_id, name) + values + (new.public_id, new.scope_id, new.name); + return new; + end; + $$ language plpgsql; + comment on function insert_host_catalog_subtype() is + 'insert_host_catalog_subtype() inserts sub type name into the base type host catalog table'; + + -- Now that we are tracking the name, which is mutable, we need to also + -- update the base table when the subtype tables are updated. + -- update_host_catalog_subtype() is intended to be used as a before update + -- trigger for all host catalog sub types. The purpose is to ensure that the + -- base table for host catalog to contain the updated names for each host catalog + -- in order to enforce uniqueness across all host catalogs, regardless of subtype, + -- in a given scope. + create function update_host_catalog_subtype() + returns trigger + as $$ + begin + update host_catalog set name = new.name where public_id = new.public_id and new.name != name; + return new; + end; + $$ language plpgsql; + comment on function update_host_catalog_subtype() is + 'update_host_catalog_subtype() will update base host catalog type name column with new values from sub type'; + + create trigger update_host_catalog_subtype before update on static_host_catalog + for each row execute procedure update_host_catalog_subtype(); + +commit; diff --git a/internal/db/schema/migrations/postgres/16/04_plugin_host.up.sql b/internal/db/schema/migrations/postgres/16/04_plugin_host.up.sql new file mode 100644 index 000000000..3b4323de7 --- /dev/null +++ b/internal/db/schema/migrations/postgres/16/04_plugin_host.up.sql @@ -0,0 +1,167 @@ +begin; + +/* + ┌──────────────────┐ + │ plugin_host │ + ├──────────────────┤ + │public_id (pk) │ + │... │ + └──────────────────┘ + ┼ + ┼ + ○ + ╱│╲ + ┌─────────────────────┐ + │ host_plugin_catalog │ + ├─────────────────────┤ ┌───────────────────────────┐ + ┌────────────────┐ │public_id (pk) │ │host_plugin_catalog_secret │ + │host_catalog │ │plugin_id (fk) │ ├───────────────────────────┤ + ├────────────────┤ │scope_id (fk) │ │host_catalog_id (pk, fk) │ + │public_id │┼┼──○┼│name │┼┼──○┼│secret │ + │scope_id │ │description │ │key_id (fk) │ + └────────────────┘ │attributes │ └───────────────────────────┘ + ┼ └─────────────────────┘ + ┼ ┼ + │ ┼ + │ ○ + ○ ╱│╲ + ╱│╲ ┌────────────────────┐ + ┌────────────────┐ │ host_plugin_set │ + │ host_set │ ├────────────────────┤ + ├────────────────┤ │public_id (pk) │ + │public_id │ │host_catalog_id (fk)│ + │host_catalog_id │┼┼──○┼│name │ + └────────────────┘ │description │ + │attributes │ + └────────────────────┘ + +*/ + + create table host_plugin_catalog ( + public_id wt_public_id primary key, + scope_id wt_scope_id not null + constraint iam_scope_fkey + references iam_scope (public_id) + on delete cascade + on update cascade, + plugin_id wt_plugin_id not null + constraint plugin_host_fkey + references plugin_host (public_id) + on delete cascade + on update cascade, + name wt_name, + description text, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + attributes bytea not null + constraint attributes_must_not_be_empty + check(length(attributes) > 0), + constraint host_catalog_fkey + foreign key (scope_id, public_id) + references host_catalog (scope_id, public_id) + on delete cascade + on update cascade, + constraint host_plugin_catalog_scope_id_name_uq + unique(scope_id, name) + ); + + create trigger update_version_column after update on host_plugin_catalog + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on host_plugin_catalog + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on host_plugin_catalog + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on host_plugin_catalog + for each row execute procedure immutable_columns('public_id', 'scope_id', 'plugin_id', 'create_time'); + + create trigger insert_host_catalog_subtype before insert on host_plugin_catalog + for each row execute procedure insert_host_catalog_subtype(); + + create trigger update_host_catalog_subtype before update on host_plugin_catalog + for each row execute procedure update_host_catalog_subtype(); + + create trigger delete_host_catalog_subtype after delete on host_plugin_catalog + for each row execute procedure delete_host_catalog_subtype(); + + create table host_plugin_catalog_secret ( + catalog_id wt_public_id primary key + constraint host_plugin_catalog_fkey + references host_plugin_catalog (public_id) + on delete cascade + on update cascade, + create_time wt_timestamp, + update_time wt_timestamp, + secret bytea not null -- encrypted value + constraint secret_must_not_be_empty + check(length(secret) > 0), + key_id wt_private_id not null + constraint kms_database_key_version_fkey + references kms_database_key_version (private_id) + on delete restrict + on update cascade + ); + + create trigger update_time_column before update on host_plugin_catalog_secret + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on host_plugin_catalog_secret + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on host_plugin_catalog_secret + for each row execute procedure immutable_columns('catalog_id', 'create_time'); + + create table host_plugin_set ( + public_id wt_public_id primary key, + catalog_id wt_public_id not null + constraint host_plugin_catalog_fkey + references host_plugin_catalog (public_id) + on delete cascade + on update cascade, + name wt_name, + description text, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + attributes bytea not null + constraint attributes_must_not_be_empty + check(length(attributes) > 0), + constraint host_plugin_set_catalog_id_name_uq + unique(catalog_id, name), + constraint host_set_fkey + foreign key (catalog_id, public_id) + references host_set (catalog_id, public_id) + on delete cascade + on update cascade, + constraint host_plugin_set_catalog_id_public_id_uq + unique(catalog_id, public_id) + ); + + create trigger update_version_column after update on host_plugin_set + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on host_plugin_set + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on host_plugin_set + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on host_plugin_set + for each row execute procedure immutable_columns('public_id', 'catalog_id','create_time'); + + create trigger insert_host_set_subtype before insert on host_plugin_set + for each row execute procedure insert_host_set_subtype(); + + create trigger delete_host_set_subtype after delete on host_plugin_set + for each row execute procedure delete_host_set_subtype(); + + insert into oplog_ticket (name, version) + values + ('host_plugin_catalog', 1), + ('host_plugin_catalog_secret', 1), + ('host_plugin_set', 1); + +commit; diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index 852676e63..d6373fb49 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -4,7 +4,7 @@ package schema func init() { migrationStates["postgres"] = migrationState{ - binarySchemaVersion: 15002, + binarySchemaVersion: 16004, upMigrations: map[int][]byte{ 1: []byte(` create domain wt_public_id as text @@ -6938,6 +6938,393 @@ alter table wh_host_dimension end; $$ language plpgsql; +`), + 16001: []byte(` +-- We are updating the plugin table here to have a scope id, since + -- subtypes of plugin should be scoped. We use 'global' as the default + -- just for the purpose of the migration since the scope cannot be null + -- and the one plugin that already exists (pi_system) can be globally + -- scoped. The immediately following statement removes this default. + -- We add a name so we can enforce that all plugins have a unique name + -- in a specific scope across all plugin subtypes. + alter table plugin + add column name wt_name, + add column scope_id wt_scope_id + not null + default 'global' + constraint iam_scope_global_fkey + references iam_scope_global(scope_id) + on delete cascade + on update cascade; + + alter table plugin + alter column scope_id drop default; + + -- Add constraints that enforce names are unique across all plugin subtypes + -- in a specific scope. + alter table plugin + add constraint plugin_scope_id_public_id_uq + unique (scope_id, public_id), + add constraint plugin_scope_id_name_uq + unique (scope_id, name); + + + -- insert, update, and delete plugin_subtypes are created since we are adding + -- subtyped plugins and we need to keep the base table plugin in sync with all + -- subtype tables. + create function insert_plugin_subtype() + returns trigger + as $$ + begin + insert into plugin + (public_id, scope_id, name) + values + (new.public_id, new.scope_id, new.name); + return new; + end; + $$ language plpgsql; + comment on function insert_plugin_subtype() is + 'insert_plugin_subtype() inserts sub type name into the base type plugin table'; + + create function update_plugin_subtype() + returns trigger + as $$ + begin + update plugin set name = new.name where public_id = new.public_id and new.name != name; + return new; + end; + $$ language plpgsql; + comment on function update_plugin_subtype() is + 'update_plugin_subtype() will update base plugin type name column with new values from sub type'; + + -- delete_plugin_subtype() is an after delete trigger function + -- for subtypes of plugin + create function delete_plugin_subtype() + returns trigger + as $$ + begin + delete from plugin + where public_id = old.public_id; + return null; -- result is ignored since this is an after trigger + end; + $$ language plpgsql; + comment on function delete_plugin_subtype is + 'delete_plugin_subtype() is an after trigger function for subytypes of plugin'; + + insert into oplog_ticket (name, version) + values + ('plugin', 1); +`), + 16002: []byte(` +/* + ┌──────────────────┐ + │ plugin │ + ├──────────────────┤ + │public_id (pk) │ + │scope_id (fk) │ + └──────────────────┘ + ┼ + ┼ + │ + ┼ + ┼ + ┌──────────────────┐ + │ plugin_host │ + ├──────────────────┤ + │public_id (pk) │ + │scope_id (fk) │ + │name │ + │description │ + │version │ + │plugin_name │ + │id_prefix │ + └──────────────────┘ +*/ + create table plugin_host ( + public_id wt_plugin_id primary key, + scope_id wt_scope_id not null + -- TODO: Allow plugins to be created in different scopes and + -- constrain the host-catalog's plugin reference accordingly. + constraint iam_scope_global_fkey + references iam_scope_global(scope_id) + on delete cascade + on update cascade, + name wt_name, + description text, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + plugin_name text not null + constraint plugin_name_must_be_not_empty + check(length(trim(plugin_name)) > 0) + constraint plugin_name_must_be_lowercase + check(lower(trim(plugin_name)) = plugin_name) + constraint plugin_host_name_uq + unique, + id_prefix text not null + constraint plugin_id_prefix_must_be_not_empty + check(length(trim(id_prefix)) > 0) + constraint plugin_id_prefix_must_fit_format + check (id_prefix ~ '^[a-z0-9]*$') + constraint plugin_host_id_prefix_uq + unique, + constraint plugin_fkey + foreign key (scope_id, public_id) + references plugin(scope_id, public_id) + on delete cascade + on update cascade, + constraint plugin_host_scope_id_name_uq + unique(scope_id, name) + ); + + create trigger update_version_column after update on plugin_host + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on plugin_host + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on plugin_host + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on plugin_host + for each row execute procedure immutable_columns('public_id', 'create_time', 'plugin_name'); + + create trigger insert_plugin_subtype before insert on plugin_host + for each row execute procedure insert_plugin_subtype(); + + create trigger update_plugin_subtype before update on plugin_host + for each row execute procedure update_plugin_subtype(); + + create trigger delete_plugin_subtype after delete on plugin_host + for each row execute procedure delete_plugin_subtype(); + + insert into oplog_ticket (name, version) + values + ('plugin_host', 1); +`), + 16003: []byte(` +-- We are adding the name to the base host catalog type. This allows the db + -- to ensure that catalog names are unique in a scope across all subtypes. + alter table host_catalog + add column name wt_name; + + alter table host_catalog + add constraint host_catalog_scope_id_name_uq + unique (scope_id, name); + + -- Now that we've added the name column to the base type, we copy + -- the name from the static host catalog table into the base table. + update host_catalog + set name = st.name + from + static_host_catalog st + where + host_catalog.public_id = st.public_id and + st.name is not null; + + -- Replace the insert_host_catalog_subtype function defined in 0/20_host.up.sql + -- to include the name. + -- insert_host_catalog_subtype() is a before insert trigger + -- function for subtypes of host_catalog + create or replace function insert_host_catalog_subtype() + returns trigger + as $$ + begin + insert into host_catalog + (public_id, scope_id, name) + values + (new.public_id, new.scope_id, new.name); + return new; + end; + $$ language plpgsql; + comment on function insert_host_catalog_subtype() is + 'insert_host_catalog_subtype() inserts sub type name into the base type host catalog table'; + + -- Now that we are tracking the name, which is mutable, we need to also + -- update the base table when the subtype tables are updated. + -- update_host_catalog_subtype() is intended to be used as a before update + -- trigger for all host catalog sub types. The purpose is to ensure that the + -- base table for host catalog to contain the updated names for each host catalog + -- in order to enforce uniqueness across all host catalogs, regardless of subtype, + -- in a given scope. + create function update_host_catalog_subtype() + returns trigger + as $$ + begin + update host_catalog set name = new.name where public_id = new.public_id and new.name != name; + return new; + end; + $$ language plpgsql; + comment on function update_host_catalog_subtype() is + 'update_host_catalog_subtype() will update base host catalog type name column with new values from sub type'; + + create trigger update_host_catalog_subtype before update on static_host_catalog + for each row execute procedure update_host_catalog_subtype(); +`), + 16004: []byte(` +/* + ┌──────────────────┐ + │ plugin_host │ + ├──────────────────┤ + │public_id (pk) │ + │... │ + └──────────────────┘ + ┼ + ┼ + ○ + ╱│╲ + ┌─────────────────────┐ + │ host_plugin_catalog │ + ├─────────────────────┤ ┌───────────────────────────┐ + ┌────────────────┐ │public_id (pk) │ │host_plugin_catalog_secret │ + │host_catalog │ │plugin_id (fk) │ ├───────────────────────────┤ + ├────────────────┤ │scope_id (fk) │ │host_catalog_id (pk, fk) │ + │public_id │┼┼──○┼│name │┼┼──○┼│secret │ + │scope_id │ │description │ │key_id (fk) │ + └────────────────┘ │attributes │ └───────────────────────────┘ + ┼ └─────────────────────┘ + ┼ ┼ + │ ┼ + │ ○ + ○ ╱│╲ + ╱│╲ ┌────────────────────┐ + ┌────────────────┐ │ host_plugin_set │ + │ host_set │ ├────────────────────┤ + ├────────────────┤ │public_id (pk) │ + │public_id │ │host_catalog_id (fk)│ + │host_catalog_id │┼┼──○┼│name │ + └────────────────┘ │description │ + │attributes │ + └────────────────────┘ + +*/ + + create table host_plugin_catalog ( + public_id wt_public_id primary key, + scope_id wt_scope_id not null + constraint iam_scope_fkey + references iam_scope (public_id) + on delete cascade + on update cascade, + plugin_id wt_plugin_id not null + constraint plugin_host_fkey + references plugin_host (public_id) + on delete cascade + on update cascade, + name wt_name, + description text, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + attributes bytea not null + constraint attributes_must_not_empty + check(length(attributes) > 0), + constraint host_catalog_fkey + foreign key (scope_id, public_id) + references host_catalog (scope_id, public_id) + on delete cascade + on update cascade, + constraint host_plugin_catalog_scope_id_name_uq + unique(scope_id, name) + ); + + create trigger update_version_column after update on host_plugin_catalog + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on host_plugin_catalog + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on host_plugin_catalog + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on host_plugin_catalog + for each row execute procedure immutable_columns('public_id', 'scope_id', 'plugin_id', 'create_time'); + + create trigger insert_host_catalog_subtype before insert on host_plugin_catalog + for each row execute procedure insert_host_catalog_subtype(); + + create trigger update_host_catalog_subtype before update on host_plugin_catalog + for each row execute procedure update_host_catalog_subtype(); + + create trigger delete_host_catalog_subtype after delete on host_plugin_catalog + for each row execute procedure delete_host_catalog_subtype(); + + create table host_plugin_catalog_secret ( + catalog_id wt_public_id primary key + constraint host_plugin_catalog_fkey + references host_plugin_catalog (public_id) + on delete cascade + on update cascade, + create_time wt_timestamp, + update_time wt_timestamp, + secret bytea not null -- encrypted value + constraint secret_must_not_be_empty + check(length(secret) > 0), + key_id wt_private_id not null + constraint kms_database_key_version_fkey + references kms_database_key_version (private_id) + on delete restrict + on update cascade + ); + + create trigger update_time_column before update on host_plugin_catalog_secret + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on host_plugin_catalog_secret + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on host_plugin_catalog_secret + for each row execute procedure immutable_columns('catalog_id', 'create_time'); + + create table host_plugin_set ( + public_id wt_public_id primary key, + catalog_id wt_public_id not null + constraint host_plugin_catalog_fkey + references host_plugin_catalog (public_id) + on delete cascade + on update cascade, + name wt_name, + description text, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + attributes bytea not null + constraint attributes_must_not_empty + check(length(attributes) > 0), + constraint host_plugin_set_catalog_id_name_uq + unique(catalog_id, name), + constraint host_set_fkey + foreign key (catalog_id, public_id) + references host_set (catalog_id, public_id) + on delete cascade + on update cascade, + constraint host_plugin_set_catalog_id_public_id_uq + unique(catalog_id, public_id) + ); + + create trigger update_version_column after update on host_plugin_set + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on host_plugin_set + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on host_plugin_set + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on host_plugin_set + for each row execute procedure immutable_columns('public_id', 'catalog_id','create_time'); + + create trigger insert_host_set_subtype before insert on host_plugin_set + for each row execute procedure insert_host_set_subtype(); + + create trigger delete_host_set_subtype after delete on host_plugin_set + for each row execute procedure delete_host_set_subtype(); + + insert into oplog_ticket (name, version) + values + ('host_plugin_catalog', 1), + ('host_plugin_catalog_secret', 1), + ('host_plugin_set', 1); `), 2001: []byte(` -- log_migration entries represent logs generated during migrations diff --git a/internal/host/plugin/host_catalog.go b/internal/host/plugin/host_catalog.go new file mode 100644 index 000000000..878e16274 --- /dev/null +++ b/internal/host/plugin/host_catalog.go @@ -0,0 +1,66 @@ +// Package plugin provides a plugin host catalog, and plugin host set resource +// which are used to interact with a host plugin as well as a repository to +// perform CRUDL and custom actions on these resource types. +package plugin + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host/plugin/store" + "google.golang.org/protobuf/proto" +) + +// A HostCatalog contains plugin host sets. It is owned by +// a scope. +type HostCatalog struct { + *store.HostCatalog + tableName string `gorm:"-"` +} + +// NewHostCatalog creates a new in memory HostCatalog assigned to a scopeId +// and pluginId. Name and description are the only valid options. All other +// options are ignored. +func NewHostCatalog(ctx context.Context, pluginId, scopeId string, opt ...Option) (*HostCatalog, error) { + const op = "plugin.NewHostCatalog" + opts := getOpts(opt...) + hc := &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PluginId: pluginId, + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, + }, + } + + if opts.withAttributes != nil { + attrs, err := json.Marshal(opts.withAttributes) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter)) + } + hc.Attributes = attrs + } + return hc, nil +} + +func (c *HostCatalog) clone() *HostCatalog { + cp := proto.Clone(c.HostCatalog) + return &HostCatalog{ + HostCatalog: cp.(*store.HostCatalog), + } +} + +// TableName returns the table name for the host catalog. +func (c *HostCatalog) TableName() string { + if c.tableName != "" { + return c.tableName + } + return "host_plugin_catalog" +} + +// SetTableName sets the table name. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (c *HostCatalog) SetTableName(n string) { + c.tableName = n +} diff --git a/internal/host/plugin/host_catalog_secret.go b/internal/host/plugin/host_catalog_secret.go new file mode 100644 index 000000000..74ecacd43 --- /dev/null +++ b/internal/host/plugin/host_catalog_secret.go @@ -0,0 +1,92 @@ +package plugin + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host/plugin/store" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/structwrapping" +) + +// HostCatalogSecret contains the encrypted secret for a host catalog. +// It is owned by a HostCatalog. +type HostCatalogSecret struct { + *store.HostCatalogSecret + tableName string `gorm:"-"` +} + +// newHostCatalogSecret creates an in memory host catalog secret. +// All options are ignored. +func newHostCatalogSecret(ctx context.Context, catalogId string, attributes map[string]interface{}, _ ...Option) (*HostCatalogSecret, error) { + const op = "plugin.newHostCatlogSecret" + hcs := &HostCatalogSecret{ + HostCatalogSecret: &store.HostCatalogSecret{ + CatalogId: catalogId, + }, + } + + if attributes != nil { + attrs, err := json.Marshal(attributes) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter)) + } + hcs.Secret = attrs + } + return hcs, nil +} + +// TableName returns the table name for the host catalog. +func (c *HostCatalogSecret) TableName() string { + if c.tableName != "" { + return c.tableName + } + return "host_plugin_catalog_secret" +} + +// SetTableName sets the table name. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (c *HostCatalogSecret) SetTableName(n string) { + c.tableName = n +} + +func (c *HostCatalogSecret) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "plugin.(HostCatalogSecret).encrypt" + if len(c.Secret) == 0 { + errors.New(ctx, errors.InvalidParameter, op, "no attributes defined") + } + if err := structwrapping.WrapStruct(ctx, cipher, c.HostCatalogSecret, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + c.KeyId = cipher.KeyID() + c.Secret = nil + return nil +} + +func (c *HostCatalogSecret) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "plugin.(HostCatalogSecret).decrypt" + if err := structwrapping.UnwrapStruct(ctx, cipher, c.HostCatalogSecret, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + c.CtSecret = nil + return nil +} + +func (c *HostCatalogSecret) insertQuery() (query string, queryValues []interface{}) { + query = upsertHostCatalogSecretQuery + queryValues = []interface{}{ + c.CatalogId, + c.CtSecret, + c.KeyId, + } + return +} + +func (c *HostCatalogSecret) deleteQuery() (query string, queryValues []interface{}) { + query = deleteHostCatalogSecretQuery + queryValues = []interface{}{ + c.CatalogId, + } + return +} diff --git a/internal/host/plugin/host_catalog_secret_test.go b/internal/host/plugin/host_catalog_secret_test.go new file mode 100644 index 000000000..243485efa --- /dev/null +++ b/internal/host/plugin/host_catalog_secret_test.go @@ -0,0 +1,163 @@ +package plugin + +import ( + "context" + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestHostCatalogSecret_New(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kkms := kms.TestKms(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test", "test") + cat := TestCatalog(t, conn, plg.GetPublicId(), prj.GetPublicId()) + + type args struct { + catalogId string + attrs map[string]interface{} + } + + tests := []struct { + name string + args args + want *HostCatalogSecret + wantErr bool + wantEncryptErr bool + }{ + { + name: "blank-catalog-id", + args: args{ + catalogId: "", + }, + want: &HostCatalogSecret{ + HostCatalogSecret: &store.HostCatalogSecret{}, + }, + wantEncryptErr: true, + }, + { + name: "no-attributes", + args: args{ + catalogId: cat.GetPublicId(), + }, + want: &HostCatalogSecret{ + HostCatalogSecret: &store.HostCatalogSecret{ + CatalogId: cat.GetPublicId(), + }, + }, + wantEncryptErr: true, + }, + { + name: "valid", + args: args{ + catalogId: cat.GetPublicId(), + attrs: map[string]interface{}{"foo": "bar"}, + }, + want: &HostCatalogSecret{ + HostCatalogSecret: &store.HostCatalogSecret{ + CatalogId: cat.GetPublicId(), + Secret: func() []byte { + b, err := json.Marshal(map[string]interface{}{"foo": "bar"}) + require.NoError(t, err) + return b + }(), + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + databaseWrapper, err := kkms.GetWrapper(ctx, prj.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + require.NotNil(t, databaseWrapper) + + got, err := newHostCatalogSecret(context.Background(), tt.args.catalogId, tt.args.attrs) + if tt.wantErr { + assert.Error(t, err) + require.Nil(t, got) + return + } + require.NoError(t, err) + require.NotNil(t, got) + + assert.Empty(t, got.CtSecret) + assert.Equal(t, tt.want, got) + + err = got.encrypt(ctx, databaseWrapper) + if tt.wantEncryptErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NoError(t, got.decrypt(ctx, databaseWrapper)) + }) + } +} + +func TestHostCatalogSecret_Custom_Queries(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kkms := kms.TestKms(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test", "prefix") + cat := TestCatalog(t, conn, plg.GetPublicId(), prj.GetPublicId()) + databaseWrapper, err := kkms.GetWrapper(ctx, prj.PublicId, kms.KeyPurposeDatabase) + + hcs, err := newHostCatalogSecret(ctx, cat.GetPublicId(), map[string]interface{}{"foo": "bar"}) + require.NoError(t, err) + assert.NoError(t, hcs.encrypt(ctx, databaseWrapper)) + q, v := hcs.insertQuery() + _, err = rw.Exec(ctx, q, v) + assert.NoError(t, err) + + found, err := newHostCatalogSecret(ctx, cat.GetPublicId(), nil) + assert.NoError(t, rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId())) + require.NoError(t, found.decrypt(ctx, databaseWrapper)) + require.NoError(t, hcs.decrypt(ctx, databaseWrapper)) + // update the created/updated time from the original + hcs.CreateTime, hcs.UpdateTime = found.CreateTime, found.UpdateTime + assert.Empty(t, cmp.Diff(hcs, found, protocmp.Transform())) + + // Update the secret and see the value updated. + updated, err := newHostCatalogSecret(ctx, cat.GetPublicId(), map[string]interface{}{"updated": "value"}) + require.NoError(t, err) + assert.NoError(t, updated.encrypt(ctx, databaseWrapper)) + q, v = updated.insertQuery() + _, err = rw.Exec(ctx, q, v) + assert.NoError(t, err) + + assert.NoError(t, rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId())) + require.NoError(t, found.decrypt(ctx, databaseWrapper)) + require.NoError(t, updated.decrypt(ctx, databaseWrapper)) + // set the created time to the first and update time from the newly found + updated.CreateTime, updated.UpdateTime = hcs.CreateTime, found.UpdateTime + assert.Empty(t, cmp.Diff(updated, found, protocmp.Transform())) + + // Try to delete this secret. + q, v = updated.deleteQuery() + _, err = rw.Exec(ctx, q, v) + assert.NoError(t, err) + err = rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId()) + assert.Error(t, err) + assert.True(t, errors.IsNotFoundError(err)) +} diff --git a/internal/host/plugin/host_catalog_test.go b/internal/host/plugin/host_catalog_test.go new file mode 100644 index 000000000..c3244b6cd --- /dev/null +++ b/internal/host/plugin/host_catalog_test.go @@ -0,0 +1,326 @@ +package plugin + +import ( + "context" + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestHostCatalog_Create(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test", "prefix") + + type args struct { + pluginId string + scopeId string + opts []Option + } + + tests := []struct { + name string + args args + want *HostCatalog + wantErr bool + wantCreateErr bool + }{ + { + name: "blank-scopeId", + args: args{ + pluginId: plg.GetPublicId(), + scopeId: "", + }, + want: &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PluginId: plg.GetPublicId(), + Attributes: []byte("{}"), + }, + }, + wantCreateErr: true, + }, + { + name: "blank-pluginId", + args: args{ + pluginId: "", + scopeId: prj.GetPublicId(), + }, + want: &HostCatalog{ + HostCatalog: &store.HostCatalog{ + ScopeId: prj.GetPublicId(), + Attributes: []byte("{}"), + }, + }, + wantCreateErr: true, + }, + { + name: "valid-no-options", + args: args{ + pluginId: plg.GetPublicId(), + scopeId: prj.GetPublicId(), + }, + want: &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PluginId: plg.GetPublicId(), + ScopeId: prj.GetPublicId(), + Attributes: []byte("{}"), + }, + }, + }, + { + name: "valid-with-name", + args: args{ + pluginId: plg.GetPublicId(), + scopeId: prj.GetPublicId(), + opts: []Option{ + WithName("test-name"), + }, + }, + want: &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PluginId: plg.GetPublicId(), + ScopeId: prj.GetPublicId(), + Name: "test-name", + Attributes: []byte("{}"), + }, + }, + }, + { + name: "valid-with-description", + args: args{ + pluginId: plg.GetPublicId(), + scopeId: prj.GetPublicId(), + opts: []Option{ + WithDescription("test-description"), + }, + }, + want: &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PluginId: plg.GetPublicId(), + ScopeId: prj.GetPublicId(), + Description: "test-description", + Attributes: []byte("{}"), + }, + }, + }, + { + name: "valid-with-attributes", + args: args{ + pluginId: plg.GetPublicId(), + scopeId: prj.GetPublicId(), + opts: []Option{ + WithAttributes(map[string]interface{}{"foo": "bar"}), + }, + }, + want: func() *HostCatalog { + hc := &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PluginId: plg.GetPublicId(), + ScopeId: prj.GetPublicId(), + }, + } + var err error + hc.Attributes, err = json.Marshal(map[string]interface{}{"foo": "bar"}) + require.NoError(t, err) + return hc + }(), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := NewHostCatalog(context.Background(), tt.args.pluginId, tt.args.scopeId, tt.args.opts...) + require.NoError(t, err) + require.NotNil(t, got) + + assert.Emptyf(t, got.PublicId, "PublicId set") + assert.Equal(t, tt.want, got) + + id, err := newHostCatalogId() + assert.NoError(t, err) + + tt.want.PublicId = id + got.PublicId = id + + w := db.New(conn) + err = w.Create(context.Background(), got) + if tt.wantCreateErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + found := &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PublicId: id, + }, + } + require.NoError(t, w.LookupById(context.Background(), found)) + assert.Empty(t, cmp.Diff(got.HostCatalog, found.HostCatalog, protocmp.Transform()), "%q compared to %q", got.Attributes, found.Attributes) + } + }) + } +} + +func TestHostCatalog_Create_DuplicateNames(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + _, prj2 := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test1", "prefix1") + plg2 := host.TestPlugin(t, conn, "test2", "prefix2") + + got, err := NewHostCatalog(context.Background(), plg.GetPublicId(), prj.GetPublicId(), WithName("duplicate")) + require.NoError(t, err) + got.PublicId, err = newHostCatalogId() + require.NoError(t, err) + w.Create(ctx, got) + + // Can't create another resource with the same pluginName in the same scope + got.PublicId, err = newHostCatalogId() + require.NoError(t, err) + assert.Error(t, w.Create(ctx, got)) + + // Can't create another resource with same pluginName in same scope even for different plugin + got, err = NewHostCatalog(context.Background(), plg2.GetPublicId(), prj.GetPublicId(), WithName("duplicate")) + require.NoError(t, err) + got.PublicId, err = newHostCatalogId() + require.NoError(t, err) + assert.Error(t, w.Create(ctx, got)) + + // Can create another resource with same pluginName in different scope even for same plugin + got, err = NewHostCatalog(context.Background(), plg.GetPublicId(), prj2.GetPublicId(), WithName("duplicate")) + require.NoError(t, err) + got.PublicId, err = newHostCatalogId() + require.NoError(t, err) + assert.NoError(t, w.Create(ctx, got)) +} + +func TestHostCatalog_Delete(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test", "prefix") + cat := TestCatalog(t, conn, plg.GetPublicId(), prj.GetPublicId()) + ignoredCat := TestCatalog(t, conn, plg.GetPublicId(), prj.GetPublicId()) + _ = ignoredCat + tests := []struct { + name string + cat *HostCatalog + rowsDeleted int + }{ + { + name: "valid", + cat: &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PublicId: cat.GetPublicId(), + }, + }, + rowsDeleted: 1, + }, + { + name: "bad-id", + cat: &HostCatalog{ + HostCatalog: &store.HostCatalog{ + PublicId: "bad-id", + }, + }, + rowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + w := db.New(conn) + deletedRows, err := w.Delete(ctx, tt.cat) + require.NoError(t, err) + assert.Equal(t, tt.rowsDeleted, deletedRows) + + err = w.LookupById(ctx, tt.cat) + require.Error(t, err) + assert.True(t, errors.IsNotFoundError(err)) + }) + } +} + +func TestHostCatalog_Delete_Cascading(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + wrapper := db.TestWrapper(t) + ctx := context.Background() + + t.Run("delete-scope", func(t *testing.T) { + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "deletescope", "deletescope") + cat := TestCatalog(t, conn, plg.GetPublicId(), prj.GetPublicId()) + + deleted, err := w.Delete(ctx, prj) + require.NoError(t, err) + require.Equal(t, 1, deleted) + + err = w.LookupById(ctx, cat) + require.Error(t, err) + assert.True(t, errors.IsNotFoundError(err)) + }) + + t.Run("delete-plugin", func(t *testing.T) { + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "deleteplugin", "deleteplugin") + cat := TestCatalog(t, conn, plg.GetPublicId(), prj.GetPublicId()) + + deleted, err := w.Delete(ctx, plg) + require.NoError(t, err) + require.Equal(t, 1, deleted) + + err = w.LookupById(ctx, cat) + require.Error(t, err) + assert.True(t, errors.IsNotFoundError(err)) + }) +} + +func TestHostCatalog_SetTableName(t *testing.T) { + defaultTableName := "host_plugin_catalog" + tests := []struct { + name string + initialName string + setNameTo string + want string + }{ + { + name: "new-name", + initialName: "", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + initialName: "initial", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + def, err := NewHostCatalog(context.Background(), "", "") + require.NoError(t, err) + require.Equal(t, defaultTableName, def.TableName()) + s := &HostCatalog{ + HostCatalog: &store.HostCatalog{}, + tableName: tt.initialName, + } + s.SetTableName(tt.setNameTo) + assert.Equal(t, tt.want, s.TableName()) + }) + } +} diff --git a/internal/host/plugin/host_set.go b/internal/host/plugin/host_set.go new file mode 100644 index 000000000..e40290bbb --- /dev/null +++ b/internal/host/plugin/host_set.go @@ -0,0 +1,53 @@ +package plugin + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host/plugin/store" +) + +// A HostSet is a collection of hosts from the set's catalog. +type HostSet struct { + *store.HostSet + tableName string `gorm:"-"` +} + +// NewHostSet creates a new in memory HostSet assigned to catalogId. +// Attributes, name, and description are the only valid options. All other +// options are ignored. +func NewHostSet(ctx context.Context, catalogId string, opt ...Option) (*HostSet, error) { + const op = "plugin.NewHostSet" + opts := getOpts(opt...) + set := &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalogId, + Name: opts.withName, + Description: opts.withDescription, + }, + } + + if opts.withAttributes != nil { + attrs, err := json.Marshal(opts.withAttributes) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter)) + } + set.Attributes = attrs + } + return set, nil +} + +// TableName returns the table name for the host set. +func (s *HostSet) TableName() string { + if s.tableName != "" { + return s.tableName + } + return "host_plugin_set" +} + +// SetTableName sets the table name. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (s *HostSet) SetTableName(n string) { + s.tableName = n +} diff --git a/internal/host/plugin/host_set_test.go b/internal/host/plugin/host_set_test.go new file mode 100644 index 000000000..7a31b75f4 --- /dev/null +++ b/internal/host/plugin/host_set_test.go @@ -0,0 +1,201 @@ +package plugin + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHostSet_Create(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test", "prefix") + cat := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + cat2 := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + + type args struct { + catalogId string + opts []Option + } + + tests := []struct { + name string + args args + want *HostSet + wantErr bool + }{ + { + name: "blank-catalogId", + args: args{ + catalogId: "", + }, + want: &HostSet{HostSet: &store.HostSet{ + Attributes: []byte("{}"), + }}, + wantErr: true, + }, + { + name: "valid-no-options", + args: args{ + catalogId: cat.GetPublicId(), + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: cat.GetPublicId(), + Attributes: []byte("{}"), + }, + }, + }, + { + name: "valid-with-name", + args: args{ + catalogId: cat.GetPublicId(), + opts: []Option{ + WithName("test-name"), + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: cat.GetPublicId(), + Name: "test-name", + Attributes: []byte("{}"), + }, + }, + }, + { + name: "duplicate-name", + args: args{ + catalogId: cat.GetPublicId(), + opts: []Option{ + WithName("test-name"), + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: cat.GetPublicId(), + Name: "test-name", + Attributes: []byte("{}"), + }, + }, + wantErr: true, + }, + { + name: "valid-duplicate-name-different-catalog", + args: args{ + catalogId: cat2.GetPublicId(), + opts: []Option{ + WithName("test-name"), + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: cat2.GetPublicId(), + Name: "test-name", + Attributes: []byte("{}"), + }, + }, + }, + { + name: "valid-with-description", + args: args{ + catalogId: cat.GetPublicId(), + opts: []Option{ + WithDescription("test-description"), + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: cat.GetPublicId(), + Description: "test-description", + Attributes: []byte("{}"), + }, + }, + }, + { + name: "valid-with-attributes", + args: args{ + catalogId: cat.GetPublicId(), + opts: []Option{ + WithAttributes(map[string]interface{}{"foo": "bar"}), + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: cat.GetPublicId(), + Attributes: []byte(`{"foo":"bar"}`), + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + got, err := NewHostSet(ctx, tt.args.catalogId, tt.args.opts...) + require.NoError(t, err) + require.NotNil(t, got) + assert.Emptyf(t, got.PublicId, "PublicId set") + assert.Equal(t, tt.want, got) + + id, err := newHostSetId() + assert.NoError(t, err) + + tt.want.PublicId = id + got.PublicId = id + + w := db.New(conn) + err = w.Create(context.Background(), got) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestHostSet_SetTableName(t *testing.T) { + defaultTableName := "host_plugin_set" + tests := []struct { + name string + initialName string + setNameTo string + want string + }{ + { + name: "new-name", + initialName: "", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + initialName: "initial", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := &HostSet{ + HostSet: &store.HostSet{}, + } + require.Equal(defaultTableName, def.TableName()) + s := &HostSet{ + HostSet: &store.HostSet{}, + tableName: tt.initialName, + } + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/host/plugin/ids.go b/internal/host/plugin/ids.go new file mode 100644 index 000000000..c4d2af7e7 --- /dev/null +++ b/internal/host/plugin/ids.go @@ -0,0 +1,41 @@ +package plugin + +import ( + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host" + "github.com/hashicorp/boundary/internal/types/subtypes" +) + +func init() { + if err := host.Register(Subtype, HostCatalogPrefix, HostSetPrefix); err != nil { + panic(err) + } +} + +// PublicId prefixes for the resources in the plugin package. +const ( + // TODO: Pull these out of being constants and have them derivable at run time. + HostCatalogPrefix = "hcplg" + HostSetPrefix = "hsplg" + + Subtype = subtypes.Subtype("plugin") +) + +func newHostCatalogId() (string, error) { + // TODO: Take in a prefix portion as an argument. + id, err := db.NewPublicId(HostCatalogPrefix) + if err != nil { + return "", errors.WrapDeprecated(err, "plugin.newHostCatalogId") + } + return id, nil +} + +func newHostSetId() (string, error) { + // TODO: Take in a prefix portion as an argument. + id, err := db.NewPublicId(HostSetPrefix) + if err != nil { + return "", errors.WrapDeprecated(err, "plugin.newHostSetId") + } + return id, nil +} diff --git a/internal/host/plugin/immutable_fields_test.go b/internal/host/plugin/immutable_fields_test.go new file mode 100644 index 000000000..677ece4f3 --- /dev/null +++ b/internal/host/plugin/immutable_fields_test.go @@ -0,0 +1,163 @@ +package plugin + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestPluginCatalog_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + wrapper := db.TestWrapper(t) + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + o, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test", "prefix") + plg2 := host.TestPlugin(t, conn, "test2", "prefix2") + new := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + + tests := []struct { + name string + update *HostCatalog + fieldMask []string + }{ + { + name: "public_id", + update: func() *HostCatalog { + c := new.clone() + c.PublicId = "hc_thisIsNotAValidId" + return c + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *HostCatalog { + c := new.clone() + c.CreateTime = &ts + return c + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "scope id", + update: func() *HostCatalog { + c := new.clone() + c.ScopeId = o.PublicId + return c + }(), + fieldMask: []string{"ScopeId"}, + }, + { + name: "plugin id", + update: func() *HostCatalog { + c := new.clone() + c.PluginId = plg2.GetPublicId() + return c + }(), + fieldMask: []string{"PluginId"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := new.clone() + err := w.LookupById(context.Background(), orig) + require.NoError(err) + + rowsUpdated, err := w.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.clone() + err = w.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig, after)) + }) + } +} + +func TestPluginSet_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + wrapper := db.TestWrapper(t) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test", "prefix") + cat := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + new := TestSet(t, conn, cat.GetPublicId()) + + tests := []struct { + name string + update *HostSet + fieldMask []string + }{ + { + name: "public_id", + update: func() *HostSet { + c := new.testCloneHostSet() + c.PublicId = "hc_thisIsNotAValidId" + return c + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *HostSet { + c := new.testCloneHostSet() + c.CreateTime = &ts + return c + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "plugin_host_catalog_id", + update: func() *HostSet { + c := new.testCloneHostSet() + c.CatalogId = "stc_01234567890" + return c + }(), + fieldMask: []string{"CatalogId"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := new.testCloneHostSet() + err := w.LookupById(context.Background(), orig) + require.NoError(err) + + rowsUpdated, err := w.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.testCloneHostSet() + err = w.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig, after)) + }) + } +} + +func (c *HostSet) testCloneHostSet() *HostSet { + cp := proto.Clone(c.HostSet) + return &HostSet{ + HostSet: cp.(*store.HostSet), + } +} diff --git a/internal/host/plugin/options.go b/internal/host/plugin/options.go new file mode 100644 index 000000000..df3436ba5 --- /dev/null +++ b/internal/host/plugin/options.go @@ -0,0 +1,59 @@ +package plugin + +// getOpts - iterate the inbound Options and return a struct +func getOpts(opt ...Option) options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments. +type Option func(*options) + +// options = how options are represented +type options struct { + withName string + withDescription string + withLimit int + withAttributes map[string]interface{} +} + +func getDefaultOptions() options { + return options{ + withDescription: "", + withName: "", + withAttributes: make(map[string]interface{}), + } +} + +// WithDescription provides an optional description. +func WithDescription(desc string) Option { + return func(o *options) { + o.withDescription = desc + } +} + +// WithName provides an optional name. +func WithName(name string) Option { + return func(o *options) { + o.withName = name + } +} + +// WithAttributes provides an optional attributes field. +func WithAttributes(attrs map[string]interface{}) Option { + return func(o *options) { + o.withAttributes = attrs + } +} + +// WithLimit provides an option to provide a limit. Intentionally allowing +// negative integers. If WithLimit < 0, then unlimited results are +// returned. If WithLimit == 0, then default limits are used for results. +func WithLimit(l int) Option { + return func(o *options) { + o.withLimit = l + } +} diff --git a/internal/host/plugin/options_test.go b/internal/host/plugin/options_test.go new file mode 100644 index 000000000..4cda5a9ff --- /dev/null +++ b/internal/host/plugin/options_test.go @@ -0,0 +1,29 @@ +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_GetOpts(t *testing.T) { + t.Parallel() + t.Run("WithName", func(t *testing.T) { + opts := getOpts(WithName("test")) + testOpts := getDefaultOptions() + testOpts.withName = "test" + assert.Equal(t, opts, testOpts) + }) + t.Run("WithDescription", func(t *testing.T) { + opts := getOpts(WithDescription("test desc")) + testOpts := getDefaultOptions() + testOpts.withDescription = "test desc" + assert.Equal(t, opts, testOpts) + }) + t.Run("WithLimit", func(t *testing.T) { + opts := getOpts(WithLimit(5)) + testOpts := getDefaultOptions() + testOpts.withLimit = 5 + assert.Equal(t, opts, testOpts) + }) +} diff --git a/internal/host/plugin/query.go b/internal/host/plugin/query.go new file mode 100644 index 000000000..bf3db9dfe --- /dev/null +++ b/internal/host/plugin/query.go @@ -0,0 +1,19 @@ +package plugin + +const ( + upsertHostCatalogSecretQuery = ` +insert into host_plugin_catalog_secret + (catalog_id, secret, key_id) +values + ($1, $2, $3) +on conflict (catalog_id) do update + set secret = excluded.secret, + key_id = excluded.key_id +returning *; +` + + deleteHostCatalogSecretQuery = ` +delete from host_plugin_catalog_secret + where catalog_id = $1; +` +) diff --git a/internal/host/plugin/store/host.pb.go b/internal/host/plugin/store/host.pb.go new file mode 100644 index 000000000..c9a0ea5db --- /dev/null +++ b/internal/host/plugin/store/host.pb.go @@ -0,0 +1,566 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.15.8 +// source: controller/storage/host/plugin/store/v1/host.proto + +// Package store provides protobufs for storing types in the plugin host +// package. + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type HostCatalog struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // The scope_id of the owning scope and must be set. + // @inject_tag: `gorm:"not_null"` + ScopeId string `protobuf:"bytes,6,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"` + // The public id of the plugin this catalog uses. + // @inject_tag: `gorm:"not_null"` + PluginId string `protobuf:"bytes,7,opt,name=plugin_id,json=pluginId,proto3" json:"plugin_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,8,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // attributes is a jsonb formatted field. + // @inject_tag: `gorm:"default:null"` + Attributes []byte `protobuf:"bytes,9,opt,name=attributes,proto3" json:"attributes,omitempty" gorm:"default:null"` +} + +func (x *HostCatalog) Reset() { + *x = HostCatalog{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HostCatalog) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostCatalog) ProtoMessage() {} + +func (x *HostCatalog) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostCatalog.ProtoReflect.Descriptor instead. +func (*HostCatalog) Descriptor() ([]byte, []int) { + return file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP(), []int{0} +} + +func (x *HostCatalog) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *HostCatalog) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *HostCatalog) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *HostCatalog) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *HostCatalog) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *HostCatalog) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *HostCatalog) GetPluginId() string { + if x != nil { + return x.PluginId + } + return "" +} + +func (x *HostCatalog) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *HostCatalog) GetAttributes() []byte { + if x != nil { + return x.Attributes + } + return nil +} + +type HostSet struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within + // catalog_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // catalog_id is the public_id of the owning + // plugin_host_catalog and must be set. + // @inject_tag: `gorm:"not_null"` + CatalogId string `protobuf:"bytes,6,opt,name=catalog_id,json=catalogId,proto3" json:"catalog_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // attributes is a jsonb formatted field. + // @inject_tag: `gorm:"default:null"` + Attributes []byte `protobuf:"bytes,8,opt,name=attributes,proto3" json:"attributes,omitempty" gorm:"default:null"` +} + +func (x *HostSet) Reset() { + *x = HostSet{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HostSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostSet) ProtoMessage() {} + +func (x *HostSet) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostSet.ProtoReflect.Descriptor instead. +func (*HostSet) Descriptor() ([]byte, []int) { + return file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP(), []int{1} +} + +func (x *HostSet) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *HostSet) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *HostSet) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *HostSet) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *HostSet) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *HostSet) GetCatalogId() string { + if x != nil { + return x.CatalogId + } + return "" +} + +func (x *HostSet) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *HostSet) GetAttributes() []byte { + if x != nil { + return x.Attributes + } + return nil +} + +type HostCatalogSecret struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // catalog_id is the public id of the catalog containing this secret. + // @inject_tag: `gorm:"primary_key"` + CatalogId string `protobuf:"bytes,1,opt,name=catalog_id,json=catalogId,proto3" json:"catalog_id,omitempty" gorm:"primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // attributes is the plain-text of the attribute data. We are not storing + // this plain-text value in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,secret_data"` + Secret []byte `protobuf:"bytes,4,opt,name=secret,proto3" json:"secret,omitempty" gorm:"-" wrapping:"pt,secret_data"` + // ct_attributes is the ciphertext of the attribute data stored in the db. + // @inject_tag: `gorm:"column:secret;not_null" wrapping:"ct,secret_data"` + CtSecret []byte `protobuf:"bytes,5,opt,name=ct_secret,json=ctSecret,proto3" json:"ct_secret,omitempty" gorm:"column:secret;not_null" wrapping:"ct,secret_data"` + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,6,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` +} + +func (x *HostCatalogSecret) Reset() { + *x = HostCatalogSecret{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HostCatalogSecret) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostCatalogSecret) ProtoMessage() {} + +func (x *HostCatalogSecret) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostCatalogSecret.ProtoReflect.Descriptor instead. +func (*HostCatalogSecret) Descriptor() ([]byte, []int) { + return file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP(), []int{2} +} + +func (x *HostCatalogSecret) GetCatalogId() string { + if x != nil { + return x.CatalogId + } + return "" +} + +func (x *HostCatalogSecret) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *HostCatalogSecret) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *HostCatalogSecret) GetSecret() []byte { + if x != nil { + return x.Secret + } + return nil +} + +func (x *HostCatalogSecret) GetCtSecret() []byte { + if x != nil { + return x.CtSecret + } + return nil +} + +func (x *HostCatalogSecret) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +var File_controller_storage_host_plugin_store_v1_host_proto protoreflect.FileDescriptor + +var file_controller_storage_host_plugin_store_v1_host_proto_rawDesc = []byte{ + 0x0a, 0x32, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x27, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9e, 0x03, 0x0a, 0x0b, 0x48, 0x6f, 0x73, + 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, + 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, + 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x64, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0x81, 0x03, 0x0a, 0x07, 0x48, 0x6f, + 0x73, 0x74, 0x53, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, + 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, + 0x67, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, + 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0x98, 0x02, + 0x0a, 0x11, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x53, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, + 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_controller_storage_host_plugin_store_v1_host_proto_rawDescOnce sync.Once + file_controller_storage_host_plugin_store_v1_host_proto_rawDescData = file_controller_storage_host_plugin_store_v1_host_proto_rawDesc +) + +func file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP() []byte { + file_controller_storage_host_plugin_store_v1_host_proto_rawDescOnce.Do(func() { + file_controller_storage_host_plugin_store_v1_host_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_host_plugin_store_v1_host_proto_rawDescData) + }) + return file_controller_storage_host_plugin_store_v1_host_proto_rawDescData +} + +var file_controller_storage_host_plugin_store_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_storage_host_plugin_store_v1_host_proto_goTypes = []interface{}{ + (*HostCatalog)(nil), // 0: controller.storage.host.plugin.store.v1.HostCatalog + (*HostSet)(nil), // 1: controller.storage.host.plugin.store.v1.HostSet + (*HostCatalogSecret)(nil), // 2: controller.storage.host.plugin.store.v1.HostCatalogSecret + (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_host_plugin_store_v1_host_proto_depIdxs = []int32{ + 3, // 0: controller.storage.host.plugin.store.v1.HostCatalog.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 1: controller.storage.host.plugin.store.v1.HostCatalog.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 2: controller.storage.host.plugin.store.v1.HostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 3: controller.storage.host.plugin.store.v1.HostSet.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 4: controller.storage.host.plugin.store.v1.HostCatalogSecret.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 5: controller.storage.host.plugin.store.v1.HostCatalogSecret.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_controller_storage_host_plugin_store_v1_host_proto_init() } +func file_controller_storage_host_plugin_store_v1_host_proto_init() { + if File_controller_storage_host_plugin_store_v1_host_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HostCatalog); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HostSet); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HostCatalogSecret); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_host_plugin_store_v1_host_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_host_plugin_store_v1_host_proto_goTypes, + DependencyIndexes: file_controller_storage_host_plugin_store_v1_host_proto_depIdxs, + MessageInfos: file_controller_storage_host_plugin_store_v1_host_proto_msgTypes, + }.Build() + File_controller_storage_host_plugin_store_v1_host_proto = out.File + file_controller_storage_host_plugin_store_v1_host_proto_rawDesc = nil + file_controller_storage_host_plugin_store_v1_host_proto_goTypes = nil + file_controller_storage_host_plugin_store_v1_host_proto_depIdxs = nil +} diff --git a/internal/host/plugin/testing.go b/internal/host/plugin/testing.go new file mode 100644 index 000000000..9dad4eae6 --- /dev/null +++ b/internal/host/plugin/testing.go @@ -0,0 +1,50 @@ +package plugin + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCatalog creates count number of plugin host catalogs to the provided DB +// with the provided scope id. If any errors are encountered during the creation of +// the host catalog, the test will fail. +func TestCatalog(t *testing.T, conn *gorm.DB, pluginId, scopeId string, opt ...Option) *HostCatalog { + t.Helper() + ctx := context.Background() + cat, err := NewHostCatalog(ctx, pluginId, scopeId, opt...) + require.NoError(t, err) + assert.NotNil(t, cat) + id, err := newHostCatalogId() + assert.NoError(t, err) + assert.NotEmpty(t, id) + cat.PublicId = id + + w := db.New(conn) + require.NoError(t, w.Create(ctx, cat)) + return cat +} + +// TestSet creates count number of plugin host sets in the provided DB +// with the provided catalog id. The catalog must have been created +// previously. The test will fail if any errors are encountered. +func TestSet(t *testing.T, conn *gorm.DB, catalogId string, opt ...Option) *HostSet { + t.Helper() + ctx := context.Background() + assert := assert.New(t) + set, err := NewHostSet(ctx, catalogId, opt...) + require.NoError(t, err) + assert.NotNil(set) + id, err := newHostSetId() + assert.NoError(err) + assert.NotEmpty(id) + set.PublicId = id + + w := db.New(conn) + require.NoError(t, w.Create(ctx, set)) + return set +} diff --git a/internal/host/plugin/testing_test.go b/internal/host/plugin/testing_test.go new file mode 100644 index 000000000..fe939e159 --- /dev/null +++ b/internal/host/plugin/testing_test.go @@ -0,0 +1,48 @@ +package plugin + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_TestCatalogs(t *testing.T) { + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NotNil(proj) + assert.NotEmpty(proj.GetPublicId()) + + plg := host.TestPlugin(t, conn, "test", "prefix") + require.NotNil(plg) + assert.NotEmpty(plg.GetPublicId()) + + cs := TestCatalog(t, conn, plg.GetPublicId(), proj.GetPublicId(), WithName("foo"), WithDescription("bar")) + assert.NotEmpty(cs.GetPublicId()) + assert.Equal("foo", cs.GetName()) + assert.Equal("bar", cs.GetDescription()) +} + +func Test_TestSet(t *testing.T) { + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NotNil(prj) + assert.NotEmpty(prj.GetPublicId()) + + plg := host.TestPlugin(t, conn, "test", "prefix") + require.NotNil(plg) + assert.NotEmpty(plg.GetPublicId()) + + c := TestCatalog(t, conn, plg.GetPublicId(), prj.GetPublicId()) + set := TestSet(t, conn, c.GetPublicId(), WithName("foo"), WithDescription("bar")) + assert.NotEmpty(set.GetPublicId()) + assert.Equal("foo", set.GetName()) + assert.Equal("bar", set.GetDescription()) +} diff --git a/internal/plugin/doc.go b/internal/plugin/doc.go new file mode 100644 index 000000000..85e195c9f --- /dev/null +++ b/internal/plugin/doc.go @@ -0,0 +1,3 @@ +// Package plugin contains other packages related to the different plugin +// types and any helpers related to working with non type specific plugins. +package plugin diff --git a/internal/plugin/host/doc.go b/internal/plugin/host/doc.go new file mode 100644 index 000000000..3ca79e39c --- /dev/null +++ b/internal/plugin/host/doc.go @@ -0,0 +1,4 @@ +// Package host provides a plugin type used to interface with boundary's host +// related resources. Additionally it provides a repository for performing +// CRUDL and custom operations on this plugin type. +package host diff --git a/internal/plugin/host/ids.go b/internal/plugin/host/ids.go new file mode 100644 index 000000000..c042b3c2a --- /dev/null +++ b/internal/plugin/host/ids.go @@ -0,0 +1,19 @@ +package host + +import ( + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" +) + +// PublicId prefixes for the resources in the plugin package. +const ( + PluginPrefix = "plgh" +) + +func newPluginId() (string, error) { + id, err := db.NewPublicId(PluginPrefix) + if err != nil { + return "", errors.WrapDeprecated(err, "plugin.newPluginId") + } + return id, nil +} diff --git a/internal/plugin/host/immutable_fields_test.go b/internal/plugin/host/immutable_fields_test.go new file mode 100644 index 000000000..d4e48b6e8 --- /dev/null +++ b/internal/plugin/host/immutable_fields_test.go @@ -0,0 +1,85 @@ +package host + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/plugin/host/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestHostPlugin_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + plg := TestPlugin(t, conn, "test", "prefix") + + newPlugin := plg + + tests := []struct { + name string + update *Plugin + fieldMask []string + }{ + { + name: "public_id", + update: func() *Plugin { + c := newPlugin.testClonePlugin() + c.PublicId = "hc_thisIsNotAValidId" + return c + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *Plugin { + c := newPlugin.testClonePlugin() + c.CreateTime = &ts + return c + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "pluginName", + update: func() *Plugin { + c := newPlugin.testClonePlugin() + c.PluginName = "different pluginName" + return c + }(), + fieldMask: []string{"PluginName"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := newPlugin.testClonePlugin() + err := w.LookupById(context.Background(), orig) + require.NoError(err) + + rowsUpdated, err := w.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := newPlugin.testClonePlugin() + err = w.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig, after)) + }) + } +} + +func (c *Plugin) testClonePlugin() *Plugin { + cp := proto.Clone(c.Plugin) + return &Plugin{ + Plugin: cp.(*store.Plugin), + } +} diff --git a/internal/plugin/host/options.go b/internal/plugin/host/options.go new file mode 100644 index 000000000..3832ef15d --- /dev/null +++ b/internal/plugin/host/options.go @@ -0,0 +1,50 @@ +package host + +// getOpts - iterate the inbound Options and return a struct +func getOpts(opt ...Option) options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments. +type Option func(*options) + +// options = how options are represented +type options struct { + withName string + withDescription string + withLimit int +} + +func getDefaultOptions() options { + return options{ + withDescription: "", + withName: "", + } +} + +// WithDescription provides an optional description. +func WithDescription(desc string) Option { + return func(o *options) { + o.withDescription = desc + } +} + +// WithName provides an optional name. +func WithName(name string) Option { + return func(o *options) { + o.withName = name + } +} + +// WithLimit provides an option to provide a limit. Intentionally allowing +// negative integers. If WithLimit < 0, then unlimited results are +// returned. If WithLimit == 0, then default limits are used for results. +func WithLimit(l int) Option { + return func(o *options) { + o.withLimit = l + } +} diff --git a/internal/plugin/host/options_test.go b/internal/plugin/host/options_test.go new file mode 100644 index 000000000..fed3d1c9f --- /dev/null +++ b/internal/plugin/host/options_test.go @@ -0,0 +1,29 @@ +package host + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_GetOpts(t *testing.T) { + t.Parallel() + t.Run("WithName", func(t *testing.T) { + opts := getOpts(WithName("test")) + testOpts := getDefaultOptions() + testOpts.withName = "test" + assert.Equal(t, opts, testOpts) + }) + t.Run("WithDescription", func(t *testing.T) { + opts := getOpts(WithDescription("test desc")) + testOpts := getDefaultOptions() + testOpts.withDescription = "test desc" + assert.Equal(t, opts, testOpts) + }) + t.Run("WithLimit", func(t *testing.T) { + opts := getOpts(WithLimit(5)) + testOpts := getDefaultOptions() + testOpts.withLimit = 5 + assert.Equal(t, opts, testOpts) + }) +} diff --git a/internal/plugin/host/plugin.go b/internal/plugin/host/plugin.go new file mode 100644 index 000000000..ddabc58ec --- /dev/null +++ b/internal/plugin/host/plugin.go @@ -0,0 +1,43 @@ +package host + +import ( + "github.com/hashicorp/boundary/internal/plugin/host/store" + "github.com/hashicorp/boundary/internal/types/scope" +) + +// A Plugin enables additional logic to be used by boundary. +// It is owned by a scope. +type Plugin struct { + *store.Plugin + tableName string `gorm:"-"` +} + +// NewPlugin creates a new in memory Plugin assigned to the global scope. +// Name, Description are the only allowed option. All other options are ignored. +func NewPlugin(pluginName, idPrefix string, opt ...Option) *Plugin { + opts := getOpts(opt...) + p := &Plugin{ + Plugin: &store.Plugin{ + PluginName: pluginName, + IdPrefix: idPrefix, + ScopeId: scope.Global.String(), + Name: opts.withName, + Description: opts.withDescription, + }, + } + return p +} + +// TableName returns the table name for the host plugin. +func (c *Plugin) TableName() string { + if c.tableName != "" { + return c.tableName + } + return "plugin_host" +} + +// SetTableName sets the table name. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (c *Plugin) SetTableName(n string) { + c.tableName = n +} diff --git a/internal/plugin/host/plugin_test.go b/internal/plugin/host/plugin_test.go new file mode 100644 index 000000000..01bccd8ae --- /dev/null +++ b/internal/plugin/host/plugin_test.go @@ -0,0 +1,275 @@ +package host + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/plugin/host/store" +) + +func TestPlugin_Create(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + + type args struct { + pluginName string + idPrefix string + semVer string + opts []Option + } + + tests := []struct { + name string + args args + want *Plugin + wantErr bool + }{ + { + name: "blank-pluginName", + args: args{ + idPrefix: "prefix", + }, + want: &Plugin{ + Plugin: &store.Plugin{ + IdPrefix: "prefix", + ScopeId: scope.Global.String(), + }, + }, + wantErr: true, + }, + { + name: "blank-idPrefix", + args: args{ + pluginName: "plugin name", + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "plugin name", + ScopeId: scope.Global.String(), + }, + }, + wantErr: true, + }, + { + name: "idprefix-capitalized", + args: args{ + pluginName: "idprefixcapitalized", + idPrefix: "IdPrefixCapitalized", + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "idprefixcapitalized", + IdPrefix: "IdPrefixCapitalized", + ScopeId: scope.Global.String(), + }, + }, + wantErr: true, + }, + { + name: "idprefix-space", + args: args{ + pluginName: "idprefix space", + idPrefix: "idprefix space", + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "idprefix space", + IdPrefix: "idprefix space", + ScopeId: scope.Global.String(), + }, + }, + wantErr: true, + }, + { + name: "pluginName-capitalized", + args: args{ + pluginName: "PluginNameCapitalized", + idPrefix: "pluginnamecapitalized", + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "PluginNameCapitalized", + IdPrefix: "pluginnamecapitalized", + ScopeId: scope.Global.String(), + }, + }, + wantErr: true, + }, + { + name: "valid-no-options", + args: args{ + pluginName: "validnooptions", + idPrefix: "validnooptions", + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "validnooptions", + IdPrefix: "validnooptions", + ScopeId: scope.Global.String(), + }, + }, + }, + { + name: "valid-with-description", + args: args{ + pluginName: "validwithdescription", + idPrefix: "validwithdescription", + opts: []Option{ + WithDescription("description"), + }, + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "validwithdescription", + IdPrefix: "validwithdescription", + ScopeId: scope.Global.String(), + Description: "description", + }, + }, + }, + { + name: "valid-pluginName-name-option", + args: args{ + pluginName: "validpluginnamenameoption", + idPrefix: "validpluginnamenameoption", + opts: []Option{ + WithName("valid-pluginName-name-option"), + WithDescription("description"), + }, + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "validpluginnamenameoption", + IdPrefix: "validpluginnamenameoption", + ScopeId: scope.Global.String(), + Name: "valid-pluginName-name-option", + Description: "description", + }, + }, + }, + // This must be run after the "valid-no-options" test + { + name: "duplicate-pluginName", + args: args{ + pluginName: "validnooptions", + idPrefix: "duplicatepluginname", + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "validnooptions", + IdPrefix: "duplicatepluginname", + ScopeId: scope.Global.String(), + }, + }, + wantErr: true, + }, + { + name: "duplicate-idPrefix", + args: args{ + pluginName: "duplicateidprefix", + idPrefix: "validnooptions", + }, + want: &Plugin{ + Plugin: &store.Plugin{ + PluginName: "duplicateidprefix", + IdPrefix: "validnooptions", + ScopeId: scope.Global.String(), + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got := NewPlugin(tt.args.pluginName, tt.args.idPrefix, tt.args.opts...) + require.NotNil(t, got) + require.Emptyf(t, got.PublicId, "PublicId set") + + id, err := newPluginId() + require.NoError(t, err) + got.PublicId = id + + tt.want.PublicId = id + assert.Equal(t, tt.want, got) + + w := db.New(conn) + err = w.Create(context.Background(), got) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestPlugin_Update(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + ctx := context.Background() + plg := TestPlugin(t, conn, "delete-plugin", "prefix") + assert.Equal(t, uint32(1), plg.Version) + + plg.Name = "New" + plg.Description = "Description" + rowCount, err := w.Update(ctx, plg, []string{"Name", "Description"}, nil) + require.NoError(t, err) + assert.Equal(t, 1, rowCount) + + assert.Equal(t, uint32(2), plg.Version) + assert.Equal(t, "New", plg.Name) + assert.Equal(t, "Description", plg.Description) +} + +func TestPlugin_Delete(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + ctx := context.Background() + plg := TestPlugin(t, conn, "delete-plugin", "prefix") + + deleted, err := w.Delete(ctx, plg) + require.NoError(t, err) + require.Equal(t, 1, deleted) +} + +func TestPlugin_SetTableName(t *testing.T) { + defaultTableName := "plugin_host" + tests := []struct { + name string + initialName string + setNameTo string + want string + }{ + { + name: "new-pluginName", + initialName: "", + setNameTo: "new-pluginName", + want: "new-pluginName", + }, + { + name: "reset to default", + initialName: "initial", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := NewPlugin("", "prefix") + require.Equal(defaultTableName, def.TableName()) + s := &Plugin{ + Plugin: &store.Plugin{}, + tableName: tt.initialName, + } + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/plugin/host/store/plugin.pb.go b/internal/plugin/host/store/plugin.pb.go new file mode 100644 index 000000000..05c63df53 --- /dev/null +++ b/internal/plugin/host/store/plugin.pb.go @@ -0,0 +1,267 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.15.8 +// source: controller/storage/plugin/host/store/v1/plugin.proto + +// Package store provides protobufs for storing types in the host package. + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Plugin struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,50,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // The scope_id of the owning scope and must be set. + // @inject_tag: `gorm:"not_null"` + ScopeId string `protobuf:"bytes,60,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"` + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,70,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // plugin_name is required. The value must be unique across all host plugins + // @inject_tag: `gorm:"default:null"` + PluginName string `protobuf:"bytes,80,opt,name=plugin_name,json=pluginName,proto3" json:"plugin_name,omitempty" gorm:"default:null"` + // id_prefix is required. The value must be unique across all host plugins + // @inject_tag: `gorm:"default:null"` + IdPrefix string `protobuf:"bytes,90,opt,name=id_prefix,json=idPrefix,proto3" json:"id_prefix,omitempty" gorm:"default:null"` +} + +func (x *Plugin) Reset() { + *x = Plugin{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_plugin_host_store_v1_plugin_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Plugin) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Plugin) ProtoMessage() {} + +func (x *Plugin) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_plugin_host_store_v1_plugin_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Plugin.ProtoReflect.Descriptor instead. +func (*Plugin) Descriptor() ([]byte, []int) { + return file_controller_storage_plugin_host_store_v1_plugin_proto_rawDescGZIP(), []int{0} +} + +func (x *Plugin) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Plugin) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Plugin) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Plugin) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Plugin) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Plugin) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *Plugin) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Plugin) GetPluginName() string { + if x != nil { + return x.PluginName + } + return "" +} + +func (x *Plugin) GetIdPrefix() string { + if x != nil { + return x.IdPrefix + } + return "" +} + +var File_controller_storage_plugin_host_store_v1_plugin_proto protoreflect.FileDescriptor + +var file_controller_storage_plugin_host_store_v1_plugin_proto_rawDesc = []byte{ + 0x0a, 0x34, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x68, 0x6f, 0x73, 0x74, + 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x27, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x2e, 0x68, 0x6f, 0x73, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, + 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, + 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9a, 0x03, 0x0a, + 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, + 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, + 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, + 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x46, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x69, 0x64, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x69, 0x64, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2f, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_plugin_host_store_v1_plugin_proto_rawDescOnce sync.Once + file_controller_storage_plugin_host_store_v1_plugin_proto_rawDescData = file_controller_storage_plugin_host_store_v1_plugin_proto_rawDesc +) + +func file_controller_storage_plugin_host_store_v1_plugin_proto_rawDescGZIP() []byte { + file_controller_storage_plugin_host_store_v1_plugin_proto_rawDescOnce.Do(func() { + file_controller_storage_plugin_host_store_v1_plugin_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_plugin_host_store_v1_plugin_proto_rawDescData) + }) + return file_controller_storage_plugin_host_store_v1_plugin_proto_rawDescData +} + +var file_controller_storage_plugin_host_store_v1_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_plugin_host_store_v1_plugin_proto_goTypes = []interface{}{ + (*Plugin)(nil), // 0: controller.storage.plugin.host.store.v1.Plugin + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_plugin_host_store_v1_plugin_proto_depIdxs = []int32{ + 1, // 0: controller.storage.plugin.host.store.v1.Plugin.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 1: controller.storage.plugin.host.store.v1.Plugin.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_controller_storage_plugin_host_store_v1_plugin_proto_init() } +func file_controller_storage_plugin_host_store_v1_plugin_proto_init() { + if File_controller_storage_plugin_host_store_v1_plugin_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_plugin_host_store_v1_plugin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Plugin); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_plugin_host_store_v1_plugin_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_plugin_host_store_v1_plugin_proto_goTypes, + DependencyIndexes: file_controller_storage_plugin_host_store_v1_plugin_proto_depIdxs, + MessageInfos: file_controller_storage_plugin_host_store_v1_plugin_proto_msgTypes, + }.Build() + File_controller_storage_plugin_host_store_v1_plugin_proto = out.File + file_controller_storage_plugin_host_store_v1_plugin_proto_rawDesc = nil + file_controller_storage_plugin_host_store_v1_plugin_proto_goTypes = nil + file_controller_storage_plugin_host_store_v1_plugin_proto_depIdxs = nil +} diff --git a/internal/plugin/host/testing.go b/internal/plugin/host/testing.go new file mode 100644 index 000000000..ff585ed4b --- /dev/null +++ b/internal/plugin/host/testing.go @@ -0,0 +1,22 @@ +package host + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/require" +) + +func TestPlugin(t *testing.T, conn *gorm.DB, name string, prefix string) *Plugin { + t.Helper() + p := NewPlugin(name, prefix) + id, err := newPluginId() + require.NoError(t, err) + p.PublicId = id + + w := db.New(conn) + require.NoError(t, w.Create(context.Background(), p)) + return p +} diff --git a/internal/plugin/host/testing_test.go b/internal/plugin/host/testing_test.go new file mode 100644 index 000000000..916883773 --- /dev/null +++ b/internal/plugin/host/testing_test.go @@ -0,0 +1,18 @@ +package host + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_TestPlugins(t *testing.T) { + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + + plg := TestPlugin(t, conn, "test", "prefix") + require.NotNil(plg) + assert.NotEmpty(plg.GetPublicId()) +} diff --git a/internal/plugin/immutable_fields_test.go b/internal/plugin/immutable_fields_test.go new file mode 100644 index 000000000..31a2299fe --- /dev/null +++ b/internal/plugin/immutable_fields_test.go @@ -0,0 +1,72 @@ +package plugin + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/plugin/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestPlugin_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + plg := testPlugin(t, conn, "test") + + newPlugin := plg + + tests := []struct { + name string + update *plugin + fieldMask []string + }{ + { + name: "public_id", + update: func() *plugin { + c := newPlugin.testClonePlugin() + c.PublicId = "pi_thisIsNotAValidId" + return c + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "scope", + update: func() *plugin { + c := newPlugin.testClonePlugin() + c.ScopeId = "o_1234567890" + return c + }(), + fieldMask: []string{"ScopeId"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := newPlugin.testClonePlugin() + err := w.LookupById(context.Background(), orig) + require.NoError(err) + + rowsUpdated, err := w.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := newPlugin.testClonePlugin() + err = w.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig, after)) + }) + } +} + +func (c *plugin) testClonePlugin() *plugin { + cp := proto.Clone(c.Plugin) + return &plugin{ + Plugin: cp.(*store.Plugin), + } +} diff --git a/internal/plugin/store/plugin.pb.go b/internal/plugin/store/plugin.pb.go new file mode 100644 index 000000000..54d39c526 --- /dev/null +++ b/internal/plugin/store/plugin.pb.go @@ -0,0 +1,182 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.15.8 +// source: controller/storage/plugin/store/v1/plugin.proto + +// Package store provides protobufs for storing types in the host package. + +package store + +import ( + _ "github.com/hashicorp/boundary/internal/db/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a typeless plugin. In practice this should never be used directly. +// This is included for testing purposes only. +type Plugin struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // The scope_id of the owning scope and must be set. + // @inject_tag: `gorm:"not_null"` + ScopeId string `protobuf:"bytes,20,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"` + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,30,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` +} + +func (x *Plugin) Reset() { + *x = Plugin{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_plugin_store_v1_plugin_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Plugin) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Plugin) ProtoMessage() {} + +func (x *Plugin) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_plugin_store_v1_plugin_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Plugin.ProtoReflect.Descriptor instead. +func (*Plugin) Descriptor() ([]byte, []int) { + return file_controller_storage_plugin_store_v1_plugin_proto_rawDescGZIP(), []int{0} +} + +func (x *Plugin) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Plugin) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *Plugin) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_controller_storage_plugin_store_v1_plugin_proto protoreflect.FileDescriptor + +var file_controller_storage_plugin_store_v1_plugin_proto_rawDesc = []byte{ + 0x0a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x22, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x54, 0x0a, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x19, 0x0a, + 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x3b, 0x5a, 0x39, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_controller_storage_plugin_store_v1_plugin_proto_rawDescOnce sync.Once + file_controller_storage_plugin_store_v1_plugin_proto_rawDescData = file_controller_storage_plugin_store_v1_plugin_proto_rawDesc +) + +func file_controller_storage_plugin_store_v1_plugin_proto_rawDescGZIP() []byte { + file_controller_storage_plugin_store_v1_plugin_proto_rawDescOnce.Do(func() { + file_controller_storage_plugin_store_v1_plugin_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_plugin_store_v1_plugin_proto_rawDescData) + }) + return file_controller_storage_plugin_store_v1_plugin_proto_rawDescData +} + +var file_controller_storage_plugin_store_v1_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_plugin_store_v1_plugin_proto_goTypes = []interface{}{ + (*Plugin)(nil), // 0: controller.storage.plugin.store.v1.Plugin +} +var file_controller_storage_plugin_store_v1_plugin_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_controller_storage_plugin_store_v1_plugin_proto_init() } +func file_controller_storage_plugin_store_v1_plugin_proto_init() { + if File_controller_storage_plugin_store_v1_plugin_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_plugin_store_v1_plugin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Plugin); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_plugin_store_v1_plugin_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_plugin_store_v1_plugin_proto_goTypes, + DependencyIndexes: file_controller_storage_plugin_store_v1_plugin_proto_depIdxs, + MessageInfos: file_controller_storage_plugin_store_v1_plugin_proto_msgTypes, + }.Build() + File_controller_storage_plugin_store_v1_plugin_proto = out.File + file_controller_storage_plugin_store_v1_plugin_proto_rawDesc = nil + file_controller_storage_plugin_store_v1_plugin_proto_goTypes = nil + file_controller_storage_plugin_store_v1_plugin_proto_depIdxs = nil +} diff --git a/internal/plugin/testing.go b/internal/plugin/testing.go new file mode 100644 index 000000000..01d5a3929 --- /dev/null +++ b/internal/plugin/testing.go @@ -0,0 +1,55 @@ +package plugin + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/plugin/store" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/require" +) + +// A typeless plugin used for tests. +type plugin struct { + *store.Plugin + tableName string `gorm:"-"` +} + +// newPlugin is used in tests and creates a typeless plugin in the global scope. +func newPlugin(name string) *plugin { + p := &plugin{ + Plugin: &store.Plugin{ + ScopeId: scope.Global.String(), + Name: name, + }, + } + return p +} + +// TableName returns the table name for the host plugin. +func (c *plugin) TableName() string { + if c.tableName != "" { + return c.tableName + } + return "plugin" +} + +// SetTableName sets the table name. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (c *plugin) SetTableName(n string) { + c.tableName = n +} + +func testPlugin(t *testing.T, conn *gorm.DB, name string) *plugin { + t.Helper() + p := newPlugin(name) + id, err := db.NewPublicId("plg") + require.NoError(t, err) + p.PublicId = id + + w := db.New(conn) + require.NoError(t, w.Create(context.Background(), p)) + return p +} diff --git a/internal/plugin/testing_test.go b/internal/plugin/testing_test.go new file mode 100644 index 000000000..a047dcc0a --- /dev/null +++ b/internal/plugin/testing_test.go @@ -0,0 +1,18 @@ +package plugin + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_testPlugin(t *testing.T) { + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + + plg := testPlugin(t, conn, "test") + require.NotNil(plg) + assert.NotEmpty(plg.GetPublicId()) +} diff --git a/internal/proto/local/controller/storage/host/plugin/store/v1/host.proto b/internal/proto/local/controller/storage/host/plugin/store/v1/host.proto new file mode 100644 index 000000000..e9cb28b64 --- /dev/null +++ b/internal/proto/local/controller/storage/host/plugin/store/v1/host.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; + +// Package store provides protobufs for storing types in the plugin host +// package. +package controller.storage.host.plugin.store.v1; +option go_package = "github.com/hashicorp/boundary/internal/host/plugin/store;store"; + +import "google/protobuf/struct.proto"; +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/custom_options/v1/options.proto"; + +message HostCatalog { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 1; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + string name = 4 [(custom_options.v1.mask_mapping) = {this:"name" that: "name"}]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 5 [(custom_options.v1.mask_mapping) = {this:"description" that: "description"}]; + + // The scope_id of the owning scope and must be set. + // @inject_tag: `gorm:"not_null"` + string scope_id = 6; + + // The public id of the plugin this catalog uses. + // @inject_tag: `gorm:"not_null"` + string plugin_id = 7; + + // version allows optimistic locking of the resource + // @inject_tag: `gorm:"default:null"` + uint32 version = 8; + + // attributes is a jsonb formatted field. + // @inject_tag: `gorm:"default:null"` + bytes attributes = 9; +} + +message HostSet { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 1; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // name is optional. If set, it must be unique within + // catalog_id. + // @inject_tag: `gorm:"default:null"` + string name = 4 [(custom_options.v1.mask_mapping) = {this:"name" that: "name"}]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 5 [(custom_options.v1.mask_mapping) = {this:"description" that: "description"}]; + + // catalog_id is the public_id of the owning + // plugin_host_catalog and must be set. + // @inject_tag: `gorm:"not_null"` + string catalog_id = 6; + + // version allows optimistic locking of the resource + // @inject_tag: `gorm:"default:null"` + uint32 version = 7; + + // attributes is a jsonb formatted field. + // @inject_tag: `gorm:"default:null"` + bytes attributes = 8; +} + +message HostCatalogSecret { + // catalog_id is the public id of the catalog containing this secret. + // @inject_tag: `gorm:"primary_key"` + string catalog_id = 1; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // attributes is the plain-text of the attribute data. We are not storing + // this plain-text value in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,secret_data"` + bytes secret = 4; + + // ct_attributes is the ciphertext of the attribute data stored in the db. + // @inject_tag: `gorm:"column:secret;not_null" wrapping:"ct,secret_data"` + bytes ct_secret = 5; + + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string key_id = 6; +} diff --git a/internal/proto/local/controller/storage/plugin/host/store/v1/plugin.proto b/internal/proto/local/controller/storage/plugin/host/store/v1/plugin.proto new file mode 100644 index 000000000..75d2efc8e --- /dev/null +++ b/internal/proto/local/controller/storage/plugin/host/store/v1/plugin.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +// Package store provides protobufs for storing types in the host package. +package controller.storage.plugin.host.store.v1; +option go_package = "github.com/hashicorp/boundary/internal/plugin/host/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/custom_options/v1/options.proto"; + +message Plugin { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 20; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 30; + + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + string name = 4 [(custom_options.v1.mask_mapping) = {this:"Name" that: "name"}]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 50 [(custom_options.v1.mask_mapping) = {this:"Description" that: "description"}]; + + // The scope_id of the owning scope and must be set. + // @inject_tag: `gorm:"not_null"` + string scope_id = 60; + + // @inject_tag: `gorm:"default:null"` + uint32 version = 70; + + // plugin_name is required. The value must be unique across all host plugins + // @inject_tag: `gorm:"default:null"` + string plugin_name = 80; + + // id_prefix is required. The value must be unique across all host plugins + // @inject_tag: `gorm:"default:null"` + string id_prefix = 90; +} \ No newline at end of file diff --git a/internal/proto/local/controller/storage/plugin/store/v1/plugin.proto b/internal/proto/local/controller/storage/plugin/store/v1/plugin.proto new file mode 100644 index 000000000..8a349da46 --- /dev/null +++ b/internal/proto/local/controller/storage/plugin/store/v1/plugin.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +// Package store provides protobufs for storing types in the host package. +package controller.storage.plugin.store.v1; +option go_package = "github.com/hashicorp/boundary/internal/plugin/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +// This is a typeless plugin. In practice this should never be used directly. +// This is included for testing purposes only. +message Plugin { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // The scope_id of the owning scope and must be set. + // @inject_tag: `gorm:"not_null"` + string scope_id = 20; + + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + string name = 30; +} \ No newline at end of file diff --git a/internal/scheduler/job/additional_verification_test.go b/internal/scheduler/job/additional_verification_test.go index 420ff74f0..9e89b4d4c 100644 --- a/internal/scheduler/job/additional_verification_test.go +++ b/internal/scheduler/job/additional_verification_test.go @@ -121,7 +121,7 @@ func TestPlugin(t *testing.T) { // Create test plugin id testPluginId := "pi_test1234" - numRows, err := rw.Exec(context.Background(), "insert into plugin(public_id) values (?)", []interface{}{testPluginId}) + numRows, err := rw.Exec(context.Background(), "insert into plugin(public_id, scope_id) values (?, 'global')", []interface{}{testPluginId}) require.NoError(err) assert.Equal(1, numRows) @@ -154,7 +154,7 @@ func TestPlugin(t *testing.T) { // Create test plugin id testPluginId := "pi_test1234" - numRows, err := rw.Exec(context.Background(), "insert into plugin(public_id) values (?)", []interface{}{testPluginId}) + numRows, err := rw.Exec(context.Background(), "insert into plugin(public_id, scope_id) values (?, 'global')", []interface{}{testPluginId}) assert.NoError(err) assert.Equal(1, numRows) @@ -177,7 +177,7 @@ func TestPlugin(t *testing.T) { // Create test plugin id testPluginId := "pi_test1234" - numRows, err := rw.Exec(context.Background(), "insert into plugin(public_id) values (?)", []interface{}{testPluginId}) + numRows, err := rw.Exec(context.Background(), "insert into plugin(public_id, scope_id) values (?, 'global')", []interface{}{testPluginId}) assert.NoError(err) assert.Equal(1, numRows)