From fcb61d4b67303698d9f71e1c434f01d8379637d9 Mon Sep 17 00:00:00 2001 From: Jim Date: Wed, 26 Aug 2020 13:58:46 -0400 Subject: [PATCH] targets repo (#298) --- Makefile | 4 +- internal/db/migrations/postgres.gen.go | 295 +++++++ .../migrations/postgres/40_targets.down.sql | 8 + .../db/migrations/postgres/40_targets.up.sql | 70 ++ .../migrations/postgres/41_targets.down.sql | 16 + .../db/migrations/postgres/41_targets.up.sql | 179 ++++ .../storage/target/store/v1/target.proto | 106 +++ internal/target/ids.go | 19 + internal/target/ids_test.go | 18 + internal/target/immutable_fields_test.go | 233 ++++++ internal/target/options.go | 96 +++ internal/target/options_test.go | 86 ++ internal/target/repository.go | 588 ++++++++++++++ internal/target/repository_tcp_target.go | 158 ++++ internal/target/repository_tcp_target_test.go | 421 ++++++++++ internal/target/repository_test.go | 764 ++++++++++++++++++ internal/target/store/target.pb.go | 526 ++++++++++++ internal/target/target.go | 90 +++ internal/target/target_host_set.go | 104 +++ internal/target/target_test.go | 39 + internal/target/tcp_target.go | 102 +++ internal/target/tcp_target_test.go | 393 +++++++++ internal/target/testing.go | 50 ++ internal/target/testing_test.go | 39 + 24 files changed, 4403 insertions(+), 1 deletion(-) create mode 100644 internal/db/migrations/postgres/40_targets.down.sql create mode 100644 internal/db/migrations/postgres/40_targets.up.sql create mode 100644 internal/db/migrations/postgres/41_targets.down.sql create mode 100644 internal/db/migrations/postgres/41_targets.up.sql create mode 100644 internal/proto/local/controller/storage/target/store/v1/target.proto create mode 100644 internal/target/ids.go create mode 100644 internal/target/ids_test.go create mode 100644 internal/target/immutable_fields_test.go create mode 100644 internal/target/options.go create mode 100644 internal/target/options_test.go create mode 100644 internal/target/repository.go create mode 100644 internal/target/repository_tcp_target.go create mode 100644 internal/target/repository_tcp_target_test.go create mode 100644 internal/target/repository_test.go create mode 100644 internal/target/store/target.pb.go create mode 100644 internal/target/target.go create mode 100644 internal/target/target_host_set.go create mode 100644 internal/target/target_test.go create mode 100644 internal/target/tcp_target.go create mode 100644 internal/target/tcp_target_test.go create mode 100644 internal/target/testing.go create mode 100644 internal/target/testing_test.go diff --git a/Makefile b/Makefile index acbe47dc6..852e5b2c0 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,9 @@ protobuild: @protoc-go-inject-tag -input=./internal/auth/store/account.pb.go @protoc-go-inject-tag -input=./internal/auth/password/store/password.pb.go @protoc-go-inject-tag -input=./internal/auth/password/store/argon2.pb.go - @protoc-go-inject-tag -input=./internal/kms/store/root_key.pb.go + @protoc-go-inject-tag -input=./internal/kms/store/root_key.pb.go + @protoc-go-inject-tag -input=./internal/target/store/target.pb.go + @rm -R ${TMP_DIR} protolint: diff --git a/internal/db/migrations/postgres.gen.go b/internal/db/migrations/postgres.gen.go index 438fa99a9..fccac5a56 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -2877,6 +2877,301 @@ before insert on kms_session_key_version ('kms_root_key_version', 1); commit; +`), + }, + "migrations/40_targets.down.sql": { + name: "40_targets.down.sql", + bytes: []byte(` +begin; + +drop function insert_target_subtype; +drop function delete_target_subtype; +drop function target_scope_valid; +drop function target_host_set_scope_valid + +commit; + +`), + }, + "migrations/40_targets.up.sql": { + name: "40_targets.up.sql", + bytes: []byte(` +begin; + + +-- insert_target_subtype() is a before insert trigger +-- function for subtypes of target +create or replace function + insert_target_subtype() + returns trigger +as $$ +begin + insert into target + (public_id, scope_id) + values + (new.public_id, new.scope_id); + return new; +end; +$$ language plpgsql; + +-- delete_target_subtype() is an after delete trigger +-- function for subtypes of host +create or replace function delete_target_subtype() + returns trigger +as $$ +begin + delete from target + where + public_id = old.public_id; + return null; -- result is ignored since this is an after trigger +end; +$$ language plpgsql; + +-- target_scope_valid() is a before insert trigger function for target +create or replace function + target_scope_valid() + returns trigger +as $$ +declare scope_type text; +begin + -- Fetch the type of scope + select isc.type from iam_scope isc where isc.public_id = new.scope_id into scope_type; + if scope_type = 'project' then + return new; + end if; + raise exception 'invalid target scope type % (must be project)', scope_type; +end; +$$ language plpgsql; + +-- target_host_set_scope_valid() is a before insert trigger function for target_host_set +create or replace function + target_host_set_scope_valid() + returns trigger +as $$ +begin + perform from + host_catalog hc, + host_set hs, + target t, + iam_scope s + where + hc.public_id = hs.catalog_id and + hc.scope_id = t.scope_id and + t.public_id = new.target_id; +if not found then + raise exception 'target scope and host set scope are not equal'; +end if; +return new; +end; +$$ language plpgsql; + +commit; +`), + }, + "migrations/41_targets.down.sql": { + name: "41_targets.down.sql", + bytes: []byte(` +begin; + +drop table target cascade; +drop table target_host_set cascade; +drop table target_tcp; +drop view target_all_subtypes; +drop view target_host_set_catalog; + + +delete +from oplog_ticket +where name in ( + 'target_tcp' + ); + +commit; +`), + }, + "migrations/41_targets.up.sql": { + name: "41_targets.up.sql", + bytes: []byte(` +/* + ┌─────────────────┐ +┌─────────────────┐ │ target_tcp │ +│ target │ ├─────────────────┤ +├─────────────────┤ │public_id │ +│public_id │┼─────────────○┼│scope_id │ +│scope_id │ │default_port │ +│ │ │name (not null) │ +└─────────────────┘ │description │ + ┼ └─────────────────┘ + │ + ○ + ╱│╲ +┌─────────────────┐ +│ target_host_set │ +├─────────────────┤ +│target_id │ +│host_set_id │ +│ │ +└─────────────────┘ + ╲│╱ + ○ + │ + │ + ┼ +┌─────────────────┐ +│ host_set │ +├─────────────────┤ +│public_id │ +│catalog_id │ +│ │ +└─────────────────┘ + +*/ + +begin; + +create table target ( + public_id wt_public_id primary key, + scope_id wt_scope_id not null + references iam_scope(public_id) + on delete cascade + on update cascade, + create_time wt_timestamp +); + +create trigger + immutable_columns +before +update on target + for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); + +create trigger + default_create_time_column +before +insert on target + for each row execute procedure default_create_time(); + +create trigger + target_scope_valid +before insert on target + for each row execute procedure target_scope_valid(); + + +create table target_host_set( + target_id wt_public_id + references target(public_id) + on delete cascade + on update cascade, + host_set_id wt_public_id + references host_set(public_id) + on delete cascade + on update cascade, + primary key(target_id, host_set_id), + create_time wt_timestamp +); + +create trigger + immutable_columns +before +update on target_host_set + for each row execute procedure immutable_columns('target_id', 'host_set_id', 'create_time'); + +create trigger + target_host_set_scope_valid +before +insert on target_host_set + for each row execute procedure target_host_set_scope_valid(); + +create table target_tcp ( + public_id wt_public_id primary key + references target(public_id) + on delete cascade + on update cascade, + scope_id wt_scope_id not null + references iam_scope(public_id) + on delete cascade + on update cascade, + name text not null, -- name is not optional for a target subtype + description text, + default_port int, -- default_port can be null + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + unique(scope_id, name) -- name must be unique within a scope +); + +create trigger + insert_target_subtype +before insert on target_tcp + for each row execute procedure insert_target_subtype(); + +create trigger + delete_target_subtype +after delete on target_tcp + for each row execute procedure delete_target_subtype(); + + -- define the immutable fields for target +create trigger + immutable_columns +before +update on target_tcp + for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); + +create trigger + update_version_column +after update on target_tcp + for each row execute procedure update_version_column(); + +create trigger + update_time_column +before update on target_tcp + for each row execute procedure update_time_column(); + +create trigger + default_create_time_column +before +insert on target_tcp + for each row execute procedure default_create_time(); + +create trigger + target_scope_valid +before insert on target_tcp + for each row execute procedure target_scope_valid(); + + +-- target_all_subtypes is a union of all target subtypes +create view target_all_subtypes +as +select + public_id, + scope_id, + name, + description, + default_port, + version, + create_time, + update_time, + 'tcp' as type + from target_tcp; + +create view target_set +as +select + hs.public_id, + hs.catalog_id, + ths.target_id +from + target_host_set ths, + host_set hs +where + hs.public_id = ths.host_set_id; + +insert into oplog_ticket + (name, version) +values + ('target_tcp', 1); + +commit; + `), }, } diff --git a/internal/db/migrations/postgres/40_targets.down.sql b/internal/db/migrations/postgres/40_targets.down.sql new file mode 100644 index 000000000..9b3dd1a01 --- /dev/null +++ b/internal/db/migrations/postgres/40_targets.down.sql @@ -0,0 +1,8 @@ +begin; + +drop function insert_target_subtype; +drop function delete_target_subtype; +drop function target_scope_valid; +drop function target_host_set_scope_valid + +commit; diff --git a/internal/db/migrations/postgres/40_targets.up.sql b/internal/db/migrations/postgres/40_targets.up.sql new file mode 100644 index 000000000..668d8558d --- /dev/null +++ b/internal/db/migrations/postgres/40_targets.up.sql @@ -0,0 +1,70 @@ +begin; + + +-- insert_target_subtype() is a before insert trigger +-- function for subtypes of target +create or replace function + insert_target_subtype() + returns trigger +as $$ +begin + insert into target + (public_id, scope_id) + values + (new.public_id, new.scope_id); + return new; +end; +$$ language plpgsql; + +-- delete_target_subtype() is an after delete trigger +-- function for subtypes of host +create or replace function delete_target_subtype() + returns trigger +as $$ +begin + delete from target + where + public_id = old.public_id; + return null; -- result is ignored since this is an after trigger +end; +$$ language plpgsql; + +-- target_scope_valid() is a before insert trigger function for target +create or replace function + target_scope_valid() + returns trigger +as $$ +declare scope_type text; +begin + -- Fetch the type of scope + select isc.type from iam_scope isc where isc.public_id = new.scope_id into scope_type; + if scope_type = 'project' then + return new; + end if; + raise exception 'invalid target scope type % (must be project)', scope_type; +end; +$$ language plpgsql; + +-- target_host_set_scope_valid() is a before insert trigger function for target_host_set +create or replace function + target_host_set_scope_valid() + returns trigger +as $$ +begin + perform from + host_catalog hc, + host_set hs, + target t, + iam_scope s + where + hc.public_id = hs.catalog_id and + hc.scope_id = t.scope_id and + t.public_id = new.target_id; +if not found then + raise exception 'target scope and host set scope are not equal'; +end if; +return new; +end; +$$ language plpgsql; + +commit; \ No newline at end of file diff --git a/internal/db/migrations/postgres/41_targets.down.sql b/internal/db/migrations/postgres/41_targets.down.sql new file mode 100644 index 000000000..2c220e685 --- /dev/null +++ b/internal/db/migrations/postgres/41_targets.down.sql @@ -0,0 +1,16 @@ +begin; + +drop table target cascade; +drop table target_host_set cascade; +drop table target_tcp; +drop view target_all_subtypes; +drop view target_host_set_catalog; + + +delete +from oplog_ticket +where name in ( + 'target_tcp' + ); + +commit; \ No newline at end of file diff --git a/internal/db/migrations/postgres/41_targets.up.sql b/internal/db/migrations/postgres/41_targets.up.sql new file mode 100644 index 000000000..b11a9e200 --- /dev/null +++ b/internal/db/migrations/postgres/41_targets.up.sql @@ -0,0 +1,179 @@ +/* + ┌─────────────────┐ +┌─────────────────┐ │ target_tcp │ +│ target │ ├─────────────────┤ +├─────────────────┤ │public_id │ +│public_id │┼─────────────○┼│scope_id │ +│scope_id │ │default_port │ +│ │ │name (not null) │ +└─────────────────┘ │description │ + ┼ └─────────────────┘ + │ + ○ + ╱│╲ +┌─────────────────┐ +│ target_host_set │ +├─────────────────┤ +│target_id │ +│host_set_id │ +│ │ +└─────────────────┘ + ╲│╱ + ○ + │ + │ + ┼ +┌─────────────────┐ +│ host_set │ +├─────────────────┤ +│public_id │ +│catalog_id │ +│ │ +└─────────────────┘ + +*/ + +begin; + +create table target ( + public_id wt_public_id primary key, + scope_id wt_scope_id not null + references iam_scope(public_id) + on delete cascade + on update cascade, + create_time wt_timestamp +); + +create trigger + immutable_columns +before +update on target + for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); + +create trigger + default_create_time_column +before +insert on target + for each row execute procedure default_create_time(); + +create trigger + target_scope_valid +before insert on target + for each row execute procedure target_scope_valid(); + + +create table target_host_set( + target_id wt_public_id + references target(public_id) + on delete cascade + on update cascade, + host_set_id wt_public_id + references host_set(public_id) + on delete cascade + on update cascade, + primary key(target_id, host_set_id), + create_time wt_timestamp +); + +create trigger + immutable_columns +before +update on target_host_set + for each row execute procedure immutable_columns('target_id', 'host_set_id', 'create_time'); + +create trigger + target_host_set_scope_valid +before +insert on target_host_set + for each row execute procedure target_host_set_scope_valid(); + +create table target_tcp ( + public_id wt_public_id primary key + references target(public_id) + on delete cascade + on update cascade, + scope_id wt_scope_id not null + references iam_scope(public_id) + on delete cascade + on update cascade, + name text not null, -- name is not optional for a target subtype + description text, + default_port int, -- default_port can be null + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + unique(scope_id, name) -- name must be unique within a scope +); + +create trigger + insert_target_subtype +before insert on target_tcp + for each row execute procedure insert_target_subtype(); + +create trigger + delete_target_subtype +after delete on target_tcp + for each row execute procedure delete_target_subtype(); + + -- define the immutable fields for target +create trigger + immutable_columns +before +update on target_tcp + for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); + +create trigger + update_version_column +after update on target_tcp + for each row execute procedure update_version_column(); + +create trigger + update_time_column +before update on target_tcp + for each row execute procedure update_time_column(); + +create trigger + default_create_time_column +before +insert on target_tcp + for each row execute procedure default_create_time(); + +create trigger + target_scope_valid +before insert on target_tcp + for each row execute procedure target_scope_valid(); + + +-- target_all_subtypes is a union of all target subtypes +create view target_all_subtypes +as +select + public_id, + scope_id, + name, + description, + default_port, + version, + create_time, + update_time, + 'tcp' as type + from target_tcp; + +create view target_set +as +select + hs.public_id, + hs.catalog_id, + ths.target_id +from + target_host_set ths, + host_set hs +where + hs.public_id = ths.host_set_id; + +insert into oplog_ticket + (name, version) +values + ('target_tcp', 1); + +commit; diff --git a/internal/proto/local/controller/storage/target/store/v1/target.proto b/internal/proto/local/controller/storage/target/store/v1/target.proto new file mode 100644 index 000000000..0aa9791ff --- /dev/null +++ b/internal/proto/local/controller/storage/target/store/v1/target.proto @@ -0,0 +1,106 @@ +syntax = "proto3"; + +package controller.storage.target.store.v1; +option go_package = "github.com/hashicorp/boundary/internal/target/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/custom_options/v1/options.proto"; + +// TargetView is a view that contains all the target subtypes +message TargetView { + // public_id is used to access the Target via an API + // @inject_tag: gorm:"primary_key" + string public_id = 10; + + // scope id for the Target + // @inject_tag: `gorm:"default:null"` + string scope_id = 20; + + // name is the optional friendly name used to + // access the Target via an API + // @inject_tag: `gorm:"default:null"` + string name = 30 + [(custom_options.v1.mask_mapping) = { this: "name" that: "name" }]; + + // description of the Target + // @inject_tag: `gorm:"default:null"` + string description = 40 [(custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + }]; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 50; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 60; + + // version allows optimistic locking of the Target when modifying the + // Target + // @inject_tag: `gorm:"default:null"` + uint32 version = 70; + + // type represents the type of the Target + // @inject_tag: `gorm:"default:null"` + string type = 80; + + // default port of the Target + // @inject_tag: `gorm:"default:null"` + uint32 default_port = 90; +} + +message TargetHostSet { + // target_id of the TargetHostSet + // @inject_tag: gorm:"primary_key" + string target_id = 10; + + // host_set_id of the TargetHostSet + // @inject_tag: gorm:"primary_key" + string host_set_id = 20; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 30; +} + +message TcpTarget { + // public_id is used to access the TargetTcp via an API + // @inject_tag: gorm:"primary_key" + string public_id = 10; + + // scope id for the TargetTcp + // @inject_tag: `gorm:"default:null"` + string scope_id = 20; + + // name is the optional friendly name used to + // access the TargetTcp via an API + // @inject_tag: `gorm:"default:null"` + string name = 30 + [(custom_options.v1.mask_mapping) = { this: "name" that: "name" }]; + + // description of the TargetTcp + // @inject_tag: `gorm:"default:null"` + string description = 40 [(custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + }]; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 50; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 60; + + // version allows optimistic locking of the TargetTcp when modifying the + // TargetTcp + // @inject_tag: `gorm:"default:null"` + uint32 version = 70; + + // default port of the TargetTcp + // @inject_tag: `gorm:"default:null"` + uint32 default_port = 80; +} \ No newline at end of file diff --git a/internal/target/ids.go b/internal/target/ids.go new file mode 100644 index 000000000..1db7cff97 --- /dev/null +++ b/internal/target/ids.go @@ -0,0 +1,19 @@ +package target + +import ( + "fmt" + + "github.com/hashicorp/boundary/internal/db" +) + +const ( + TcpTargetPrefix = "ttcp" +) + +func newTcpTargetId() (string, error) { + id, err := db.NewPublicId(TcpTargetPrefix) + if err != nil { + return "", fmt.Errorf("new tcp target id: %w", err) + } + return id, nil +} diff --git a/internal/target/ids_test.go b/internal/target/ids_test.go new file mode 100644 index 000000000..eac126b1a --- /dev/null +++ b/internal/target/ids_test.go @@ -0,0 +1,18 @@ +package target + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Ids(t *testing.T) { + t.Parallel() + t.Run("tcp", func(t *testing.T) { + id, err := newTcpTargetId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, TcpTargetPrefix+"_")) + }) +} diff --git a/internal/target/immutable_fields_test.go b/internal/target/immutable_fields_test.go new file mode 100644 index 000000000..8ec05c3f7 --- /dev/null +++ b/internal/target/immutable_fields_test.go @@ -0,0 +1,233 @@ +package target + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestTarget_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + rw := db.New(conn) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + new := TestTcpTarget(t, conn, proj.PublicId, testId(t)) + + var tests = []struct { + name string + update *TcpTarget + fieldMask []string + }{ + { + name: "public_id", + update: func() *TcpTarget { + target := new.Clone().(*TcpTarget) + target.PublicId = "p_thisIsNotAValidId" + return target + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *TcpTarget { + target := new.Clone().(*TcpTarget) + target.CreateTime = &ts + return target + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "scope_id", + update: func() *TcpTarget { + target := new.Clone().(*TcpTarget) + target.ScopeId = "o_thisIsNotAValidId" + return target + }(), + 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 := new.Clone() + orig.(*TcpTarget).SetTableName("target") + err := rw.LookupById(context.Background(), orig) + require.NoError(err) + + tt.update.SetTableName("target") + fmt.Println(tt.update.TableName(), tt.update) + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + after.(*TcpTarget).SetTableName("target") + err = rw.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig.(*TcpTarget), after.(*TcpTarget))) + + }) + } +} + +func TestTcpTarget_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + rw := db.New(conn) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + new := TestTcpTarget(t, conn, proj.PublicId, testId(t)) + + var tests = []struct { + name string + update *TcpTarget + fieldMask []string + }{ + { + name: "public_id", + update: func() *TcpTarget { + target := new.Clone().(*TcpTarget) + target.PublicId = "p_thisIsNotAValidId" + return target + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *TcpTarget { + target := new.Clone().(*TcpTarget) + target.CreateTime = &ts + return target + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "scope_id", + update: func() *TcpTarget { + target := new.Clone().(*TcpTarget) + target.ScopeId = "o_thisIsNotAValidId" + return target + }(), + 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 := new.Clone() + err := rw.LookupById(context.Background(), orig) + require.NoError(err) + + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + err = rw.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig.(*TcpTarget), after.(*TcpTarget))) + + }) + } +} + +func TestTargetHostSet_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + repo, err := NewRepository(rw, rw, testKms) + require.NoError(t, err) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + projTarget := TestTcpTarget(t, conn, proj.PublicId, testId(t)) + testCats := static.TestCatalogs(t, conn, proj.PublicId, 1) + hsets := static.TestSets(t, conn, testCats[0].GetPublicId(), 2) + require.Equal(t, 2, len(hsets)) + + updateTarget := TestTcpTarget(t, conn, proj.PublicId, testId(t)) + updateHset := hsets[1] + + _, gotHostSets, err := repo.AddTargeHostSets(context.Background(), projTarget.PublicId, 1, []string{hsets[0].PublicId}) + require.NoError(t, err) + require.Equal(t, 1, len(gotHostSets)) + new, err := NewTargetHostSet(projTarget.PublicId, gotHostSets[0].PublicId) + require.NoError(t, err) + + var tests = []struct { + name string + update *TargetHostSet + fieldMask []string + }{ + { + name: "target_id", + update: func() *TargetHostSet { + target := new.Clone().(*TargetHostSet) + target.TargetId = updateTarget.PublicId + return target + }(), + fieldMask: []string{"TargetId"}, + }, + { + name: "create time", + update: func() *TargetHostSet { + target := new.Clone().(*TargetHostSet) + target.CreateTime = &ts + return target + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "host_set_id", + update: func() *TargetHostSet { + target := new.Clone().(*TargetHostSet) + target.HostSetId = updateHset.PublicId + return target + }(), + fieldMask: []string{"HostSetId"}, + }, + } + 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 := rw.LookupWhere(context.Background(), orig, "target_id = ? and host_set_id = ?", new.TargetId, new.HostSetId) + require.NoError(err) + + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + err = rw.LookupWhere(context.Background(), after, "target_id = ? and host_set_id = ?", new.TargetId, new.HostSetId) + require.NoError(err) + assert.True(proto.Equal(orig.(*TargetHostSet), after.(*TargetHostSet))) + }) + } +} diff --git a/internal/target/options.go b/internal/target/options.go new file mode 100644 index 000000000..82140d8c0 --- /dev/null +++ b/internal/target/options.go @@ -0,0 +1,96 @@ +package target + +// 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 + withDefaultPort uint32 + withLimit int + withScopeId string + withUserId string + withTargetType *TargetType + withHostSets []string +} + +func getDefaultOptions() options { + return options{ + withName: "", + withDescription: "", + withLimit: 0, + withDefaultPort: 0, + withScopeId: "", + withUserId: "", + withTargetType: nil, + withHostSets: nil, + } +} + +// WithDescription provides an optional description +func WithDescription(desc string) Option { + return func(o *options) { + o.withDescription = desc + } +} + +// WithName provides an option to search by a friendly 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(limit int) Option { + return func(o *options) { + o.withLimit = limit + } +} + +// WithDefaultPort provides an option to specify the default target port. +func WithDefaultPort(p uint32) Option { + return func(o *options) { + o.withDefaultPort = p + } +} + +// WithScopeId provides an option to search by a scope id +func WithScopeId(scopeId string) Option { + return func(o *options) { + o.withScopeId = scopeId + } +} + +// WithUserId provides an option to search by a user public id +func WithUserId(userId string) Option { + return func(o *options) { + o.withUserId = userId + } +} + +// WithTargetType provides an option to search by a target type +func WithTargetType(t TargetType) Option { + return func(o *options) { + o.withTargetType = &t + } +} + +// WithHostSets provides an option for providing a list of host set ids +func WithHostSets(hs []string) Option { + return func(o *options) { + o.withHostSets = hs + } +} diff --git a/internal/target/options_test.go b/internal/target/options_test.go new file mode 100644 index 000000000..859c6be9e --- /dev/null +++ b/internal/target/options_test.go @@ -0,0 +1,86 @@ +package target + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test_GetOpts provides unit tests for GetOpts and all the options +func Test_GetOpts(t *testing.T) { + t.Parallel() + t.Run("WithName", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithName("test")) + testOpts := getDefaultOptions() + testOpts.withName = "test" + assert.Equal(opts, testOpts) + }) + t.Run("WithDescription", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithDescription("test desc")) + testOpts := getDefaultOptions() + testOpts.withDescription = "test desc" + assert.Equal(opts, testOpts) + }) + t.Run("WithLimit", func(t *testing.T) { + assert := assert.New(t) + // test default of 0 + opts := getOpts() + testOpts := getDefaultOptions() + testOpts.withLimit = 0 + assert.Equal(opts, testOpts) + + opts = getOpts(WithLimit(-1)) + testOpts = getDefaultOptions() + testOpts.withLimit = -1 + assert.Equal(opts, testOpts) + + opts = getOpts(WithLimit(1)) + testOpts = getDefaultOptions() + testOpts.withLimit = 1 + assert.Equal(opts, testOpts) + }) + t.Run("WithDefaultPort", func(t *testing.T) { + assert := assert.New(t) + // test default of 0 + opts := getOpts() + testOpts := getDefaultOptions() + testOpts.withDefaultPort = 0 + assert.Equal(opts, testOpts) + + opts = getOpts(WithDefaultPort(22)) + testOpts = getDefaultOptions() + testOpts.withDefaultPort = uint32(22) + assert.Equal(opts, testOpts) + }) + t.Run("WithUserId", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithUserId("testId")) + testOpts := getDefaultOptions() + testOpts.withUserId = "testId" + assert.Equal(opts, testOpts) + }) + t.Run("WithScopeId", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithScopeId("testId")) + testOpts := getDefaultOptions() + testOpts.withScopeId = "testId" + assert.Equal(opts, testOpts) + }) + t.Run("WithTargetType", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithTargetType(TcpTargetType)) + testOpts := getDefaultOptions() + target := TcpTargetType + testOpts.withTargetType = &target + assert.Equal(opts, testOpts) + }) + t.Run("WithHostSets", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithHostSets([]string{"alice", "bob"})) + testOpts := getDefaultOptions() + testOpts.withHostSets = []string{"alice", "bob"} + assert.Equal(opts, testOpts) + }) +} diff --git a/internal/target/repository.go b/internal/target/repository.go new file mode 100644 index 000000000..fdbdd3c34 --- /dev/null +++ b/internal/target/repository.go @@ -0,0 +1,588 @@ +package target + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +var ( + ErrMetadataScopeNotFound = errors.New("scope not found for metadata") +) + +// Clonable provides a cloning interface +type Cloneable interface { + Clone() interface{} +} + +// Repository is the target database repository +type Repository struct { + reader db.Reader + writer db.Writer + kms *kms.Kms + + // defaultLimit provides a default for limiting the number of results returned from the repo + defaultLimit int +} + +// NewRepository creates a new target Repository. Supports the options: WithLimit +// which sets a default limit on results returned by repo operations. +func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) { + if r == nil { + return nil, errors.New("error creating db repository with nil reader") + } + if w == nil { + return nil, errors.New("error creating db repository with nil writer") + } + if kms == nil { + return nil, errors.New("error creating db repository with nil kms") + } + opts := getOpts(opt...) + if opts.withLimit == 0 { + // zero signals the boundary defaults should be used. + opts.withLimit = db.DefaultLimit + } + return &Repository{ + reader: r, + writer: w, + kms: kms, + defaultLimit: opts.withLimit, + }, nil +} + +// LookupTarget will look up a target in the repository and return the target +// with its host set ids. If the target is not found, it will return nil, nil, nil. +// No options are currently supported. +func (r *Repository) LookupTarget(ctx context.Context, publicId string, opt ...Option) (Target, []*TargetSet, error) { + if publicId == "" { + return nil, nil, fmt.Errorf("lookup target: missing private id: %w", db.ErrInvalidParameter) + } + target := allocTargetView() + target.PublicId = publicId + var hostSets []*TargetSet + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + if err := read.LookupById(ctx, &target); err != nil { + return fmt.Errorf("lookup target: failed %w for %s", err, publicId) + } + var err error + if hostSets, err = fetchSets(ctx, read, target.PublicId); err != nil { + return err + } + return nil + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("lookup target: %w", err) + } + subType, err := target.targetSubType() + if err != nil { + return nil, nil, fmt.Errorf("lookup target: %w", err) + } + return subType, hostSets, nil +} + +func fetchSets(ctx context.Context, r db.Reader, targetId string) ([]*TargetSet, error) { + var hostSets []*TargetSet + if err := r.SearchWhere(ctx, &hostSets, "target_id = ?", []interface{}{targetId}); err != nil { + return nil, fmt.Errorf("fetch host sets: %w", err) + } + if len(hostSets) == 0 { + return nil, nil + } + return hostSets, nil +} + +// ListTargets in targets in a scope. Supports the WithScopeId, WithLimit, WithTargetType options. +func (r *Repository) ListTargets(ctx context.Context, opt ...Option) ([]Target, error) { + opts := getOpts(opt...) + if opts.withScopeId == "" && opts.withUserId == "" { + return nil, fmt.Errorf("list targets: must specify either a scope id or user id: %w", db.ErrInvalidParameter) + } + // TODO (jimlambrt 8/2020) - implement WithUserId() optional filtering. + var where []string + var args []interface{} + if opts.withScopeId != "" { + where, args = append(where, "scope_id = ?"), append(args, opts.withScopeId) + } + if opts.withTargetType != nil { + where, args = append(where, "type = ?"), append(args, opts.withTargetType.String()) + } + + var foundTargets []*targetView + err := r.list(ctx, &foundTargets, strings.Join(where, " and "), args, opt...) + if err != nil { + return nil, fmt.Errorf("list targets: %w", err) + } + + targets := make([]Target, 0, len(foundTargets)) + + for _, t := range foundTargets { + subType, err := t.targetSubType() + if err != nil { + return nil, fmt.Errorf("list targets: %w", err) + } + targets = append(targets, subType) + } + return targets, nil +} + +// list will return a listing of resources and honor the WithLimit option or the +// repo defaultLimit +func (r *Repository) list(ctx context.Context, resources interface{}, where string, args []interface{}, opt ...Option) error { + opts := getOpts(opt...) + limit := r.defaultLimit + var dbOpts []db.Option + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + dbOpts = append(dbOpts, db.WithLimit(limit)) + return r.reader.SearchWhere(ctx, resources, where, args, dbOpts...) +} + +// DeleteTarget will delete a target from the repository. +func (r *Repository) DeleteTarget(ctx context.Context, publicId string, opt ...Option) (int, error) { + if publicId == "" { + return db.NoRowsAffected, fmt.Errorf("delete target: missing public id %w", db.ErrInvalidParameter) + } + t := allocTargetView() + t.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, &t); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete target: failed %w for %s", err, publicId) + } + var metadata oplog.Metadata + var deleteTarget interface{} + switch t.Type { + case TcpTargetType.String(): + tcpT := allocTcpTarget() + tcpT.PublicId = publicId + deleteTarget = &tcpT + metadata = tcpT.oplog(oplog.OpType_OP_TYPE_DELETE) + default: + return db.NoRowsAffected, fmt.Errorf("delete target: %s is an unsupported target type %s", publicId, t.Type) + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, t.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete target: unable to get oplog wrapper: %w", err) + } + + var rowsDeleted int + var deleteResource interface{} + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + deleteResource = deleteTarget.(Cloneable).Clone() + rowsDeleted, err = w.Delete( + ctx, + deleteResource, + db.WithOplog(oplogWrapper, metadata), + ) + if err == nil && rowsDeleted > 1 { + // return err, which will result in a rollback of the delete + return db.ErrMultipleRecords + } + return err + }, + ) + return rowsDeleted, err +} + +// update a target in the db repository with an oplog entry. +// It currently supports no options. +func (r *Repository) update(ctx context.Context, target Target, version uint32, fieldMaskPaths []string, setToNullPaths []string, opt ...Option) (Target, []*TargetSet, int, error) { + if version == 0 { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update: version cannot be zero: %w", db.ErrInvalidParameter) + } + if target == nil { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update: target is nil: %w", db.ErrInvalidParameter) + } + cloner, ok := target.(Cloneable) + if !ok { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update: target is not Cloneable: %w", db.ErrInvalidParameter) + } + dbOpts := []db.Option{ + db.WithVersion(&version), + } + scopeId := target.GetScopeId() + if scopeId == "" { + t := allocTargetView() + t.PublicId = target.GetPublicId() + if err := r.reader.LookupByPublicId(ctx, &t); err != nil { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update: lookup failed %w for %s", err, t.PublicId) + } + scopeId = t.ScopeId + } + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, nil, db.NoRowsAffected, fmt.Errorf("unable to get oplog wrapper: %w", err) + } + metadata := target.oplog(oplog.OpType_OP_TYPE_UPDATE) + dbOpts = append(dbOpts, db.WithOplog(oplogWrapper, metadata)) + + var rowsUpdated int + var returnedTarget interface{} + var hostSets []*TargetSet + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + returnedTarget = cloner.Clone() + rowsUpdated, err = w.Update( + ctx, + returnedTarget, + fieldMaskPaths, + setToNullPaths, + dbOpts..., + ) + if err != nil { + return err + } + if err == nil && rowsUpdated > 1 { + // return err, which will result in a rollback of the update + return fmt.Errorf("error more than 1 target would have been updated: %w", db.ErrMultipleRecords) + } + var err error + if hostSets, err = fetchSets(ctx, reader, target.GetPublicId()); err != nil { + return err + } + return err + }, + ) + return returnedTarget.(Target), hostSets, rowsUpdated, err +} + +// AddTargeHostSets provides the ability to add host sets (hostSetIds) to a +// target (targetId). The target's current db version must match the +// targetVersion or an error will be returned. The target and a list of +// current host set ids will be returned on success. Zero is not a valid value +// for the WithVersion option and will return an error. +func (r *Repository) AddTargeHostSets(ctx context.Context, targetId string, targetVersion uint32, hostSetIds []string, opt ...Option) (Target, []*TargetSet, error) { + if targetId == "" { + return nil, nil, fmt.Errorf("add target host sets: missing target id: %w", db.ErrInvalidParameter) + } + if targetVersion == 0 { + return nil, nil, fmt.Errorf("add target host sets: version cannot be zero: %w", db.ErrInvalidParameter) + } + if len(hostSetIds) == 0 { + return nil, nil, fmt.Errorf("add target host sets: missing host set ids: %w", db.ErrInvalidParameter) + } + newHostSets := make([]interface{}, 0, len(hostSetIds)) + for _, id := range hostSetIds { + ths, err := NewTargetHostSet(targetId, id) + if err != nil { + return nil, nil, fmt.Errorf("add target host sets: unable to create in memory target host set: %w", err) + } + newHostSets = append(newHostSets, ths) + } + t := allocTargetView() + t.PublicId = targetId + if err := r.reader.LookupByPublicId(ctx, &t); err != nil { + return nil, nil, fmt.Errorf("add target host sets: failed %w for %s", err, targetId) + } + var metadata oplog.Metadata + var target interface{} + switch t.Type { + case TcpTargetType.String(): + tcpT := allocTcpTarget() + tcpT.PublicId = t.PublicId + tcpT.Version = targetVersion + 1 + target = &tcpT + metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) + default: + return nil, nil, fmt.Errorf("delete target host sets: %s is an unsupported target type %s", t.PublicId, t.Type) + } + oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) + if err != nil { + return nil, nil, fmt.Errorf("add target host sets: unable to get oplog wrapper: %w", err) + } + var currentHostSets []*TargetSet + var updatedTarget interface{} + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + msgs := make([]*oplog.Message, 0, 2) + targetTicket, err := w.GetTicket(target) + if err != nil { + return fmt.Errorf("add target host sets: unable to get ticket: %w", err) + } + updatedTarget = target.(Cloneable).Clone() + var targetOplogMsg oplog.Message + rowsUpdated, err := w.Update(ctx, updatedTarget, []string{"Version"}, nil, db.NewOplogMsg(&targetOplogMsg), db.WithVersion(&targetVersion)) + if err != nil { + return fmt.Errorf("add target host sets: unable to update target version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("add target host sets: updated target and %d rows updated", rowsUpdated) + } + msgs = append(msgs, &targetOplogMsg) + + hostSetsOplogMsgs := make([]*oplog.Message, 0, len(newHostSets)) + if err := w.CreateItems(ctx, newHostSets, db.NewOplogMsgs(&hostSetsOplogMsgs)); err != nil { + return fmt.Errorf("add target host sets: unable to add target host sets: %w", err) + } + msgs = append(msgs, hostSetsOplogMsgs...) + + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil { + return fmt.Errorf("add target host sets: unable to write oplog: %w", err) + } + currentHostSets, err = fetchSets(ctx, reader, targetId) + if err != nil { + return fmt.Errorf("add target host sets: unable to retrieve current host sets after adds: %w", err) + } + return nil + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("add target host sets: error creating sets: %w", err) + } + return updatedTarget.(Target), currentHostSets, nil +} + +// DeleteTargeHostSets deletes host sets from a target (targetId). The target's +// current db version must match the targetVersion or an error will be returned. +// Zero is not a valid value for the WithVersion option and will return an +// error. +func (r *Repository) DeleteTargeHostSets(ctx context.Context, targetId string, targetVersion uint32, hostSetIds []string, opt ...Option) (int, error) { + if targetId == "" { + return db.NoRowsAffected, fmt.Errorf("delete target host sets: missing target id: %w", db.ErrInvalidParameter) + } + if targetVersion == 0 { + return db.NoRowsAffected, fmt.Errorf("delete target host sets: version cannot be zero: %w", db.ErrInvalidParameter) + } + if len(hostSetIds) == 0 { + return db.NoRowsAffected, fmt.Errorf("delete target host sets: missing host set ids: %w", db.ErrInvalidParameter) + } + deleteTargeHostSets := make([]interface{}, 0, len(hostSetIds)) + for _, id := range hostSetIds { + ths, err := NewTargetHostSet(targetId, id) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete target host sets: unable to create in memory target host set: %w", err) + } + deleteTargeHostSets = append(deleteTargeHostSets, ths) + } + + t := allocTargetView() + t.PublicId = targetId + if err := r.reader.LookupByPublicId(ctx, &t); err != nil { + return db.NoRowsAffected, fmt.Errorf("delete target host sets: failed %w for %s", err, targetId) + } + + var metadata oplog.Metadata + var target interface{} + switch t.Type { + case TcpTargetType.String(): + tcpT := allocTcpTarget() + tcpT.PublicId = t.PublicId + tcpT.Version = targetVersion + 1 + target = &tcpT + metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) + default: + return db.NoRowsAffected, fmt.Errorf("delete target host sets: %s is an unsupported target type %s", t.PublicId, t.Type) + } + oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete target host sets: unable to get oplog wrapper: %w", err) + } + + var totalRowsDeleted int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + msgs := make([]*oplog.Message, 0, 2) + targetTicket, err := w.GetTicket(target) + if err != nil { + return fmt.Errorf("delete target host sets: unable to get ticket: %w", err) + } + updatedTarget := target.(Cloneable).Clone() + var targetOplogMsg oplog.Message + rowsUpdated, err := w.Update(ctx, updatedTarget, []string{"Version"}, nil, db.NewOplogMsg(&targetOplogMsg), db.WithVersion(&targetVersion)) + if err != nil { + return fmt.Errorf("delete target host sets: unable to update target version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("delete target host sets: updated target and %d rows updated", rowsUpdated) + } + msgs = append(msgs, &targetOplogMsg) + + hostSetsOplogMsgs := make([]*oplog.Message, 0, len(deleteTargeHostSets)) + rowsDeleted, err := w.DeleteItems(ctx, deleteTargeHostSets, db.NewOplogMsgs(&hostSetsOplogMsgs)) + if err != nil { + return fmt.Errorf("delete target host sets: unable to delete target host sets: %w", err) + } + if rowsDeleted != len(deleteTargeHostSets) { + return fmt.Errorf("delete target host sets: target host sets deleted %d did not match request for %d", rowsDeleted, len(deleteTargeHostSets)) + } + totalRowsDeleted += rowsDeleted + msgs = append(msgs, hostSetsOplogMsgs...) + + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil { + return fmt.Errorf("delete target host sets: unable to write oplog: %w", err) + } + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete target host sets: error deleting target host sets: %w", err) + } + return totalRowsDeleted, nil +} + +// SetTargetHostSets will set the target's host sets. Set add and/or delete +// target host sets as need to reconcile the existing sets with the sets +// requested. If hostSetIds is empty, the target host sets will be cleared. Zero +// is not a valid value for the WithVersion option and will return an error. +func (r *Repository) SetTargetHostSets(ctx context.Context, targetId string, targetVersion uint32, hostSetIds []string, opt ...Option) ([]*TargetSet, int, error) { + if targetId == "" { + return nil, db.NoRowsAffected, fmt.Errorf("set target host sets: missing target id: %w", db.ErrInvalidParameter) + } + if targetVersion == 0 { + return nil, db.NoRowsAffected, fmt.Errorf("set target host sets: version cannot be zero: %w", db.ErrInvalidParameter) + } + t := allocTargetView() + t.PublicId = targetId + if err := r.reader.LookupByPublicId(ctx, &t); err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set target host sets: failed %w for %s", err, targetId) + } + + // NOTE: calculating that to set can safely happen outside of the write + // transaction since we're using targetVersion to ensure that the only + // operate on the same set of data from these queries that calculate the + // set. + + // TODO(mgaffney) 08/2020: Use SQL to calculate changes. + foundThs, err := fetchSets(ctx, r.reader, targetId) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set target host sets: unable to search for existing target host sets: %w", err) + } + found := map[string]*TargetSet{} + for _, s := range foundThs { + found[s.PublicId] = s + } + addHostSets := make([]interface{}, 0, len(hostSetIds)) + for _, id := range hostSetIds { + if _, ok := found[id]; ok { + // found a match, so do nothing (we want to keep it), but remove it + // from found + delete(found, id) + continue + } + hs, err := NewTargetHostSet(targetId, id) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set target host set: unable to create in memory target host set: %w", err) + } + addHostSets = append(addHostSets, hs) + } + deleteHostSets := make([]interface{}, 0, len(hostSetIds)) + if len(found) > 0 { + for _, s := range found { + hs, err := NewTargetHostSet(targetId, s.PublicId) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set target host set: unable to create in memory target host set: %w", err) + } + deleteHostSets = append(deleteHostSets, hs) + } + } + if len(addHostSets) == 0 && len(deleteHostSets) == 0 { + return foundThs, db.NoRowsAffected, nil + } + + var metadata oplog.Metadata + var target interface{} + switch t.Type { + case TcpTargetType.String(): + tcpT := allocTcpTarget() + tcpT.PublicId = t.PublicId + tcpT.Version = targetVersion + 1 + target = &tcpT + metadata = tcpT.oplog(oplog.OpType_OP_TYPE_UPDATE) + default: + return nil, db.NoRowsAffected, fmt.Errorf("set target host sets: %s is an unsupported target type %s", t.PublicId, t.Type) + } + oplogWrapper, err := r.kms.GetWrapper(ctx, t.GetScopeId(), kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set target host sets: unable to get oplog wrapper: %w", err) + } + + var totalRowsAffected int + var currentHostSets []*TargetSet + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + msgs := make([]*oplog.Message, 0, 2) + targetTicket, err := w.GetTicket(target) + if err != nil { + return fmt.Errorf("set target host sets: unable to get ticket: %w", err) + } + updatedTarget := target.(Cloneable).Clone() + var targetOplogMsg oplog.Message + rowsUpdated, err := w.Update(ctx, updatedTarget, []string{"Version"}, nil, db.NewOplogMsg(&targetOplogMsg), db.WithVersion(&targetVersion)) + if err != nil { + return fmt.Errorf("set target host sets: unable to update target version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("set target host sets: updated target and %d rows updated", rowsUpdated) + } + msgs = append(msgs, &targetOplogMsg) + + // Write the new ones in + if len(addHostSets) > 0 { + hostSetOplogMsgs := make([]*oplog.Message, 0, len(addHostSets)) + if err := w.CreateItems(ctx, addHostSets, db.NewOplogMsgs(&hostSetOplogMsgs)); err != nil { + return fmt.Errorf("unable to add target host sets during set: %w", err) + } + totalRowsAffected += len(addHostSets) + msgs = append(msgs, hostSetOplogMsgs...) + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) + } + + // Anything we didn't take out of found needs to be removed + if len(deleteHostSets) > 0 { + hostSetOplogMsgs := make([]*oplog.Message, 0, len(deleteHostSets)) + rowsDeleted, err := w.DeleteItems(ctx, deleteHostSets, db.NewOplogMsgs(&hostSetOplogMsgs)) + if err != nil { + return fmt.Errorf("set target host sets: unable to delete target host set: %w", err) + } + if rowsDeleted != len(deleteHostSets) { + return fmt.Errorf("set target host sets: target host sets deleted %d did not match request for %d", rowsDeleted, len(deleteHostSets)) + } + totalRowsAffected += rowsDeleted + msgs = append(msgs, hostSetOplogMsgs...) + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) + } + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil { + return fmt.Errorf("set target host sets: unable to write oplog: %w", err) + } + + currentHostSets, err = fetchSets(ctx, reader, targetId) + if err != nil { + return fmt.Errorf("set target host sets: unable to retrieve current target host sets after set: %w", err) + } + return nil + }, + ) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("target host sets: error setting target host sets: %w", err) + } + return currentHostSets, totalRowsAffected, nil +} diff --git a/internal/target/repository_tcp_target.go b/internal/target/repository_tcp_target.go new file mode 100644 index 000000000..5eb40c88b --- /dev/null +++ b/internal/target/repository_tcp_target.go @@ -0,0 +1,158 @@ +package target + +import ( + "context" + "fmt" + "strings" + + dbcommon "github.com/hashicorp/boundary/internal/db/common" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// CreateTcpTarget inserts into the repository and returns the new Target with +// its list of host sets. WithHostSets is currently the only supported option. +func (r *Repository) CreateTcpTarget(ctx context.Context, target *TcpTarget, opt ...Option) (Target, []*TargetSet, error) { + opts := getOpts(opt...) + if target == nil { + return nil, nil, fmt.Errorf("create tcp target: missing target: %w", db.ErrInvalidParameter) + } + if target.TcpTarget == nil { + return nil, nil, fmt.Errorf("create tcp target: missing target store: %w", db.ErrInvalidParameter) + } + if target.ScopeId == "" { + return nil, nil, fmt.Errorf("create tcp target: scope id empty: %w", db.ErrInvalidParameter) + } + if target.Name == "" { + return nil, nil, fmt.Errorf("create tcp target: name empty: %w", db.ErrInvalidParameter) + } + if target.PublicId != "" { + return nil, nil, fmt.Errorf("create tcp target: public id not empty: %w", db.ErrInvalidParameter) + } + id, err := newTcpTargetId() + if err != nil { + return nil, nil, fmt.Errorf("create tcp target: %w", err) + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, target.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, nil, fmt.Errorf("create tcp target: unable to get oplog wrapper: %w", err) + } + t := target.Clone().(*TcpTarget) + t.PublicId = id + + newHostSets := make([]interface{}, 0, len(opts.withHostSets)) + for _, hsId := range opts.withHostSets { + hostSet, err := NewTargetHostSet(t.PublicId, hsId) + if err != nil { + return nil, nil, fmt.Errorf("create tcp target: unable to create in memory target host set: %w", err) + } + newHostSets = append(newHostSets, hostSet) + } + + metadata := t.oplog(oplog.OpType_OP_TYPE_CREATE) + var returnedTarget interface{} + var returnedHostSet []*TargetSet + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + targetTicket, err := w.GetTicket(t) + if err != nil { + return fmt.Errorf("create tcp target: unable to get ticket: %w", err) + } + msgs := make([]*oplog.Message, 0, 2) + var targetOplogMsg oplog.Message + returnedTarget = t.Clone() + if err := w.Create(ctx, returnedTarget, db.NewOplogMsg(&targetOplogMsg)); err != nil { + return err + } + msgs = append(msgs, &targetOplogMsg) + if len(newHostSets) > 0 { + hostSetOplogMsgs := make([]*oplog.Message, 0, len(newHostSets)) + if err := w.CreateItems(ctx, newHostSets, db.NewOplogMsgs(&hostSetOplogMsgs)); err != nil { + return fmt.Errorf("create tcp target: unable to add host sets: %w", err) + } + if returnedHostSet, err = fetchSets(ctx, read, t.PublicId); err != nil { + return fmt.Errorf("create tcp target: unable to read host sets: %w", err) + } + msgs = append(msgs, hostSetOplogMsgs...) + } + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, targetTicket, metadata, msgs); err != nil { + return fmt.Errorf("create tcp target: unable to write oplog: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("create tcp target: %w for %s target id id", err, t.PublicId) + } + return returnedTarget.(*TcpTarget), returnedHostSet, err +} + +// UpdateTcpTarget will update a target in the repository and return the written +// target. fieldMaskPaths provides field_mask.proto paths for fields that should +// be updated. Fields will be set to NULL if the field is a zero value and +// included in fieldMask. Name and Description are the only updatable fields, +// If no updatable fields are included in the fieldMaskPaths, then an error is +// returned. +func (r *Repository) UpdateTcpTarget(ctx context.Context, target *TcpTarget, version uint32, fieldMaskPaths []string, opt ...Option) (Target, []*TargetSet, int, error) { + if target == nil { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update tcp target: missing target %w", db.ErrInvalidParameter) + } + if target.TcpTarget == nil { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update tcp target: missing target store %w", db.ErrInvalidParameter) + } + if target.PublicId == "" { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update tcp target: missing target public id %w", db.ErrInvalidParameter) + } + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold("name", f): + case strings.EqualFold("description", f): + case strings.EqualFold("defaultport", f): + default: + return nil, nil, db.NoRowsAffected, fmt.Errorf("update tcp target: field: %s: %w", f, db.ErrInvalidFieldMask) + } + } + var dbMask, nullFields []string + dbMask, nullFields = dbcommon.BuildUpdatePaths( + map[string]interface{}{ + "Name": target.Name, + "Description": target.Description, + "DefaultPort": target.DefaultPort, + }, + fieldMaskPaths, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update tcp target: %w", db.ErrEmptyFieldMask) + } + var returnedTarget Target + var rowsUpdated int + var targetSets []*TargetSet + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(read db.Reader, w db.Writer) error { + var err error + t := target.Clone().(*TcpTarget) + returnedTarget, targetSets, rowsUpdated, err = r.update(ctx, t, version, dbMask, nullFields) + if err != nil { + return err + } + return nil + }, + ) + if err != nil { + if db.IsUniqueError(err) { + return nil, nil, db.NoRowsAffected, fmt.Errorf("update tcp target: target %s already exists in scope %s: %w", target.Name, target.ScopeId, db.ErrNotUnique) + } + return nil, nil, db.NoRowsAffected, fmt.Errorf("update tcp target: %w for %s", err, target.PublicId) + } + return returnedTarget.(Target), targetSets, rowsUpdated, err +} diff --git a/internal/target/repository_tcp_target_test.go b/internal/target/repository_tcp_target_test.go new file mode 100644 index 000000000..26585c5d0 --- /dev/null +++ b/internal/target/repository_tcp_target_test.go @@ -0,0 +1,421 @@ +package target + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestRepository_CreateTcpTarget(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, testKms) + require.NoError(t, err) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + cats := static.TestCatalogs(t, conn, proj.PublicId, 1) + hsets := static.TestSets(t, conn, cats[0].GetPublicId(), 2) + var sets []string + for _, s := range hsets { + sets = append(sets, s.PublicId) + } + + type args struct { + target *TcpTarget + opt []Option + } + tests := []struct { + name string + args args + wantHostSets []string + wantErr bool + wantIsError error + }{ + { + name: "valid-org", + args: args{ + target: func() *TcpTarget { + target, err := NewTcpTarget(proj.PublicId, "valid-org", + WithDescription("valid-org"), + WithDefaultPort(uint32(22))) + require.NoError(t, err) + return target + }(), + opt: []Option{WithHostSets(sets)}, + }, + wantErr: false, + wantHostSets: sets, + }, + { + name: "nil-target", + args: args{ + target: nil, + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "nil-target-store", + args: args{ + target: func() *TcpTarget { + target := &TcpTarget{} + return target + }(), + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "public-id-not-empty", + args: args{ + target: func() *TcpTarget { + target, err := NewTcpTarget(proj.PublicId, "valid-org", WithDescription("valid-org"), WithDefaultPort(uint32(22))) + require.NoError(t, err) + id, err := newTcpTargetId() + require.NoError(t, err) + target.PublicId = id + return target + }(), + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + { + name: "empty-scope-id", + args: args{ + target: func() *TcpTarget { + target := allocTcpTarget() + target.Name = "empty-scope-id" + require.NoError(t, err) + return &target + }(), + }, + wantErr: true, + wantIsError: db.ErrInvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + target, hostSets, err := repo.CreateTcpTarget(context.Background(), tt.args.target, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Nil(target) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + return + } + require.NoError(err) + assert.NotNil(target.GetPublicId()) + gotIds := make([]string, 0, len(hostSets)) + for _, s := range hostSets { + gotIds = append(gotIds, s.PublicId) + } + assert.Equal(tt.wantHostSets, gotIds) + + foundTarget, foundHostSets, err := repo.LookupTarget(context.Background(), target.GetPublicId()) + assert.NoError(err) + assert.True(proto.Equal(target.(*TcpTarget), foundTarget.(*TcpTarget))) + assert.Equal(hostSets, foundHostSets) + + err = db.TestVerifyOplog(t, rw, target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + + // TODO (jimlambrt 9/2020) - unfortunately, we can currently + // test to make sure that the oplog entry for a target host sets + // create exist because the db.TestVerifyOplog doesn't really + // support that level of testing and the previous call to + // CreateTcpTarget would create an oplog entry for the + // create on the target even if no host sets were added. Once + // TestVerifyOplog supports the appropriate granularity, we should + // add an appropriate assert. + }) + } +} + +func TestRepository_UpdateTcpTarget(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + + repo, err := NewRepository(rw, rw, testKms) + require.NoError(t, err) + id := testId(t) + + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + pubId := func(s string) *string { return &s } + + type args struct { + name string + description string + port uint32 + fieldMaskPaths []string + opt []Option + ScopeId string + PublicId *string + } + tests := []struct { + name string + newScopeId string + newName string + newTargetOpts []Option + args args + wantRowsUpdate int + wantErr bool + wantErrMsg string + wantIsError error + wantDup bool + }{ + { + name: "valid", + args: args{ + name: "valid" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "valid-no-op", + args: args{ + name: "valid-no-op" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + newName: "valid-no-op" + id, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "not-found", + args: args{ + name: "not-found" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + PublicId: func() *string { s := "1"; return &s }(), + }, + newScopeId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "update tcp target: update: lookup after write: record not found for 1", + wantIsError: db.ErrRecordNotFound, + }, + { + name: "null-name", + args: args{ + name: "", + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + newName: "null-name" + id, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "update tcp target: update: failed: pq: null value in column ", + }, + { + name: "null-description", + args: args{ + name: "null-description", + fieldMaskPaths: []string{"Description"}, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + newTargetOpts: []Option{WithDescription("null-description" + id)}, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "empty-field-mask", + args: args{ + name: "valid" + id, + fieldMaskPaths: []string{}, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "update tcp target: empty field mask", + wantIsError: db.ErrEmptyFieldMask, + }, + { + name: "nil-fieldmask", + args: args{ + name: "valid" + id, + fieldMaskPaths: nil, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "update tcp target: empty field mask", + wantIsError: db.ErrEmptyFieldMask, + }, + { + name: "read-only-fields", + args: args{ + name: "valid" + id, + fieldMaskPaths: []string{"CreateTime"}, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "update tcp target: field: CreateTime: invalid field mask", + wantIsError: db.ErrInvalidFieldMask, + }, + { + name: "unknown-fields", + args: args{ + name: "valid" + id, + fieldMaskPaths: []string{"Alice"}, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "update tcp target: field: Alice: invalid field mask", + wantIsError: db.ErrInvalidFieldMask, + }, + { + name: "no-public-id", + args: args{ + name: "valid" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + PublicId: pubId(""), + }, + newScopeId: proj.PublicId, + wantErr: true, + wantErrMsg: "update tcp target: missing target public id invalid parameter", + wantIsError: db.ErrInvalidParameter, + wantRowsUpdate: 0, + }, + { + name: "proj-scope-id-no-mask", + args: args{ + name: "proj-scope-id" + id, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + wantErr: true, + wantErrMsg: "update tcp target: empty field mask", + wantIsError: db.ErrEmptyFieldMask, + }, + { + name: "empty-scope-id-with-name-mask", + args: args{ + name: "empty-scope-id" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: "", + }, + newScopeId: proj.PublicId, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "dup-name", + args: args{ + name: "dup-name" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newScopeId: proj.PublicId, + wantErr: true, + wantDup: true, + wantErrMsg: " already exists in scope " + proj.PublicId, + wantIsError: db.ErrNotUnique, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + if tt.wantDup { + _ = TestTcpTarget(t, conn, proj.PublicId, tt.args.name) + } + + testCats := static.TestCatalogs(t, conn, proj.PublicId, 1) + hsets := static.TestSets(t, conn, testCats[0].GetPublicId(), 5) + testHostSetIds := make([]string, 0, len(hsets)) + for _, hs := range hsets { + testHostSetIds = append(testHostSetIds, hs.PublicId) + } + tt.newTargetOpts = append(tt.newTargetOpts, WithHostSets(testHostSetIds)) + name := tt.newName + if name == "" { + name = testId(t) + } + target := TestTcpTarget(t, conn, tt.newScopeId, name, tt.newTargetOpts...) + updateTarget := allocTcpTarget() + updateTarget.PublicId = target.PublicId + if tt.args.PublicId != nil { + updateTarget.PublicId = *tt.args.PublicId + } + updateTarget.ScopeId = tt.args.ScopeId + updateTarget.Name = tt.args.name + updateTarget.Description = tt.args.description + updateTarget.DefaultPort = tt.args.port + + targetAfterUpdate, hostSets, updatedRows, err := repo.UpdateTcpTarget(context.Background(), &updateTarget, target.Version, tt.args.fieldMaskPaths, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + if tt.wantIsError != nil { + assert.True(errors.Is(err, tt.wantIsError)) + } + assert.Nil(targetAfterUpdate) + assert.Equal(0, updatedRows) + assert.Contains(err.Error(), tt.wantErrMsg) + err = db.TestVerifyOplog(t, rw, target.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + return + } + require.NoError(err) + require.NotNil(targetAfterUpdate) + assert.Equal(tt.wantRowsUpdate, updatedRows) + afterUpdateIds := make([]string, 0, len(hostSets)) + for _, hs := range hostSets { + afterUpdateIds = append(afterUpdateIds, hs.PublicId) + } + assert.Equal(testHostSetIds, afterUpdateIds) + + switch tt.name { + case "valid-no-op": + assert.Equal(target.UpdateTime, targetAfterUpdate.(*TcpTarget).UpdateTime) + default: + assert.NotEqual(target.UpdateTime, targetAfterUpdate.(*TcpTarget).UpdateTime) + } + foundTarget, _, err := repo.LookupTarget(context.Background(), target.PublicId) + assert.NoError(err) + assert.True(proto.Equal(targetAfterUpdate.((*TcpTarget)), foundTarget.((*TcpTarget)))) + dbassert := dbassert.New(t, rw) + if tt.args.description == "" { + assert.Equal(foundTarget.GetDescription(), "") + dbassert.IsNull(foundTarget, "description") + } + err = db.TestVerifyOplog(t, rw, target.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + }) + } +} diff --git a/internal/target/repository_test.go b/internal/target/repository_test.go new file mode 100644 index 000000000..fab1901c2 --- /dev/null +++ b/internal/target/repository_test.go @@ -0,0 +1,764 @@ +package target + +import ( + "context" + "errors" + "sort" + "strconv" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewRepository(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + type args struct { + r db.Reader + w db.Writer + kms *kms.Kms + } + tests := []struct { + name string + args args + want *Repository + wantErr bool + wantErrString string + }{ + { + name: "valid", + args: args{ + r: rw, + w: rw, + kms: testKms, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: testKms, + defaultLimit: db.DefaultLimit, + }, + wantErr: false, + }, + { + name: "nil-kms", + args: args{ + r: rw, + w: rw, + kms: nil, + }, + want: nil, + wantErr: true, + wantErrString: "error creating db repository with nil kms", + }, + { + name: "nil-writer", + args: args{ + r: rw, + w: nil, + kms: testKms, + }, + want: nil, + wantErr: true, + wantErrString: "error creating db repository with nil writer", + }, + { + name: "nil-reader", + args: args{ + r: nil, + w: rw, + kms: testKms, + }, + want: nil, + wantErr: true, + wantErrString: "error creating db repository with nil reader", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewRepository(tt.args.r, tt.args.w, tt.args.kms) + if tt.wantErr { + require.Error(err) + assert.Equal(err.Error(), tt.wantErrString) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + }) + } +} + +func TestRepository_ListTargets(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + const testLimit = 10 + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + rw := db.New(conn) + repo, err := NewRepository(rw, rw, testKms) + require.NoError(t, err) + repo.defaultLimit = testLimit + + type args struct { + opt []Option + } + tests := []struct { + name string + createCnt int + createScopeId string + createScopeId2 string + grantUserId string + args args + wantCnt int + wantErr bool + }{ + { + name: "tcp-target", + createCnt: 5, + createScopeId: proj.PublicId, + args: args{ + opt: []Option{WithTargetType(TcpTargetType), WithScopeId(proj.PublicId)}, + }, + wantCnt: 5, + wantErr: false, + }, + { + name: "no-limit", + createCnt: testLimit + 1, + createScopeId: proj.PublicId, + args: args{ + opt: []Option{WithLimit(-1), WithScopeId(proj.PublicId)}, + }, + wantCnt: testLimit + 1, + wantErr: false, + }, + { + name: "default-limit", + createCnt: testLimit + 1, + createScopeId: proj.PublicId, + args: args{ + opt: []Option{WithScopeId(proj.PublicId)}, + }, + wantCnt: testLimit, + wantErr: false, + }, + { + name: "custom-limit", + createCnt: testLimit + 1, + createScopeId: proj.PublicId, + args: args{ + opt: []Option{WithLimit(3), WithScopeId(proj.PublicId)}, + }, + wantCnt: 3, + wantErr: false, + }, + { + name: "bad-org", + createCnt: 1, + createScopeId: proj.PublicId, + args: args{ + opt: []Option{WithScopeId("bad-id")}, + }, + wantCnt: 0, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(allocTcpTarget()).Error) + testGroups := []*TcpTarget{} + for i := 0; i < tt.createCnt; i++ { + switch { + case tt.createScopeId2 != "" && i%2 == 0: + testGroups = append(testGroups, TestTcpTarget(t, conn, tt.createScopeId2, strconv.Itoa(i))) + default: + testGroups = append(testGroups, TestTcpTarget(t, conn, tt.createScopeId, strconv.Itoa(i))) + } + } + assert.Equal(tt.createCnt, len(testGroups)) + got, err := repo.ListTargets(context.Background(), tt.args.opt...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, len(got)) + }) + } +} + +func TestRepository_DeleteTarget(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + repo, err := NewRepository(rw, rw, testKms) + require.NoError(t, err) + + type args struct { + target Target + opt []Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "valid"), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "no-public-id", + args: args{ + target: func() Target { + target := allocTcpTarget() + return &target + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantErrMsg: "delete target: missing public id invalid parameter", + }, + { + name: "not-found", + args: args{ + target: func() Target { + id, err := newTcpTargetId() + require.NoError(t, err) + target := allocTcpTarget() + target.PublicId = id + return &target + }(), + }, + wantRowsDeleted: 0, + wantErr: true, + wantErrMsg: "delete target: failed record not found for ", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + deletedRows, err := repo.DeleteTarget(context.Background(), tt.args.target.GetPublicId(), tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Equal(0, deletedRows) + assert.Contains(err.Error(), tt.wantErrMsg) + err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + return + } + assert.NoError(err) + assert.Equal(tt.wantRowsDeleted, deletedRows) + foundGroup, _, err := repo.LookupTarget(context.Background(), tt.args.target.GetPublicId()) + assert.Error(err) + assert.Nil(foundGroup) + assert.True(errors.Is(err, db.ErrRecordNotFound)) + + err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + }) + } +} + +func TestRepository_AddTargetHostSets(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + staticOrg, staticProj := iam.TestScopes(t, iamRepo) + repo, err := NewRepository(rw, rw, testKms) + require.NoError(t, err) + + createHostSetsFn := func(orgs, projects []string) []string { + results := []string{} + for _, publicId := range orgs { + cats := static.TestCatalogs(t, conn, publicId, 1) + hsets := static.TestSets(t, conn, cats[0].GetPublicId(), 1) + results = append(results, hsets[0].PublicId) + } + for _, publicId := range projects { + cats := static.TestCatalogs(t, conn, publicId, 1) + hsets := static.TestSets(t, conn, cats[0].GetPublicId(), 1) + results = append(results, hsets[0].PublicId) + } + return results + } + + type args struct { + targetVersion uint32 + wantTargetIds bool + opt []Option + } + tests := []struct { + name string + args args + wantErr bool + wantErrIs error + }{ + { + name: "valid", + args: args{ + targetVersion: 1, + wantTargetIds: true, + }, + wantErr: false, + }, + { + name: "bad-version", + args: args{ + targetVersion: 1000, + wantTargetIds: true, + }, + wantErr: true, + }, + { + name: "zero-version", + args: args{ + targetVersion: 0, + wantTargetIds: true, + }, + wantErr: true, + }, + { + name: "no-host-sets", + args: args{ + targetVersion: 1, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(allocTargetHostSet()).Error) + require.NoError(conn.Where("1=1").Delete(allocTcpTarget()).Error) + + projTarget := TestTcpTarget(t, conn, staticProj.PublicId, "static-proj") + + var hostSetIds []string + origTarget, origHostSet, err := repo.LookupTarget(context.Background(), projTarget.PublicId) + require.NoError(err) + require.Equal(0, len(origHostSet)) + + if tt.args.wantTargetIds { + hostSetIds = createHostSetsFn([]string{staticOrg.PublicId}, []string{staticProj.PublicId}) + } + + gotTarget, gotHostSets, err := repo.AddTargeHostSets(context.Background(), projTarget.PublicId, tt.args.targetVersion, hostSetIds, tt.args.opt...) + if tt.wantErr { + require.Error(err) + if tt.wantErrIs != nil { + assert.Truef(errors.Is(err, tt.wantErrIs), "unexpected error %s", err.Error()) + } + // test to see of the target version update oplog was not created + err = db.TestVerifyOplog(t, rw, projTarget.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + + // TODO (jimlambrt 9/2020) - unfortunately, we can currently + // test to make sure that the oplog entry for a target create + // doesn't exist because the db.TestVerifyOplog doesn't really + // support that level of testing and the previous call to + // TestTcpTarget would create an oplog entry for the + // create on the target. Once TestVerifyOplog supports the + // appropriate granularity, we should add an appropriate assert. + + return + } + require.NoError(err) + gotHostSet := map[string]*TargetSet{} + for _, s := range gotHostSets { + gotHostSet[s.PublicId] = s + } + + // TODO (jimlambrt 9/2020) - unfortunately, we can currently + // test to make sure that the oplog entry for a target create + // doesn't exist because the db.TestVerifyOplog doesn't really + // support that level of testing and the previous call to + // TestTcpTarget would create an oplog entry for the + // create on the target. Once TestVerifyOplog supports the + // appropriate granularity, we should add an appropriate assert. + + // test to see of the target version update oplog was created + err = db.TestVerifyOplog(t, rw, projTarget.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + + foundHostSets, err := fetchSets(context.Background(), rw, projTarget.PublicId) + require.NoError(err) + for _, s := range foundHostSets { + assert.NotEmpty(gotHostSet[s.PublicId]) + } + + target, ths, err := repo.LookupTarget(context.Background(), projTarget.PublicId) + require.NoError(err) + assert.Equal(tt.args.targetVersion+1, target.GetVersion()) + assert.Equal(origTarget.GetVersion(), target.GetVersion()-1) + assert.Equal(gotHostSets, ths) + assert.True(proto.Equal(gotTarget.(*TcpTarget), target.(*TcpTarget))) + }) + } +} + +func TestRepository_DeleteTargetHosts(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + repo, err := NewRepository(rw, rw, testKms) + require.NoError(t, err) + + type args struct { + target Target + targetIdOverride *string + targetVersionOverride *uint32 + createCnt int + deleteCnt int + opt []Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsErr error + }{ + { + name: "valid", + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "valid"), + createCnt: 5, + deleteCnt: 5, + }, + wantRowsDeleted: 5, + wantErr: false, + }, + { + name: "valid-keeping-some", + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "valid-keeping-some"), + createCnt: 5, + deleteCnt: 2, + }, + wantRowsDeleted: 2, + wantErr: false, + }, + { + name: "no-deletes", + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "no-deletes"), + createCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "not-found", + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "not-found"), + targetIdOverride: func() *string { id := testId(t); return &id }(), + createCnt: 5, + deleteCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + }, + { + name: "missing-target-id", + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "missing-target-id"), + targetIdOverride: func() *string { id := ""; return &id }(), + createCnt: 5, + deleteCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "zero-version", + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "zero-version"), + targetVersionOverride: func() *uint32 { v := uint32(0); return &v }(), + createCnt: 5, + deleteCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "bad-version", + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "bad-version"), + targetVersionOverride: func() *uint32 { v := uint32(1000); return &v }(), + createCnt: 5, + deleteCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + hsIds := make([]string, 0, tt.args.createCnt) + if tt.args.createCnt > 0 { + for i := 0; i < tt.args.createCnt; i++ { + cats := static.TestCatalogs(t, conn, proj.PublicId, 1) + hsets := static.TestSets(t, conn, cats[0].GetPublicId(), 1) + hsIds = append(hsIds, hsets[0].PublicId) + } + } + _, addedHostSets, err := repo.AddTargeHostSets(context.Background(), tt.args.target.GetPublicId(), 1, hsIds, tt.args.opt...) + require.NoError(err) + assert.Equal(tt.args.createCnt, len(addedHostSets)) + + deleteHostSets := make([]string, 0, tt.args.deleteCnt) + for i := 0; i < tt.args.deleteCnt; i++ { + deleteHostSets = append(deleteHostSets, hsIds[i]) + } + var targetId string + switch { + case tt.args.targetIdOverride != nil: + targetId = *tt.args.targetIdOverride + default: + targetId = tt.args.target.GetPublicId() + } + var targetVersion uint32 + switch { + case tt.args.targetVersionOverride != nil: + targetVersion = *tt.args.targetVersionOverride + default: + targetVersion = 2 + } + deletedRows, err := repo.DeleteTargeHostSets(context.Background(), targetId, targetVersion, deleteHostSets, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Equal(0, deletedRows) + if tt.wantIsErr != nil { + assert.Truef(errors.Is(err, tt.wantIsErr), "unexpected error %s", err.Error()) + } + // TODO (jimlambrt 9/2020) - unfortunately, we can currently + // test to make sure that the oplog entry for a target update + // doesn't exist because the db.TestVerifyOplog doesn't really + // support that level of testing and the previous call to + // repo.AddTargeHostSets() would create an oplog entry for the + // update to the target. Once TestVerifyOplog supports the + // appropriate granularity, we should add an appropriate assert. + + err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + return + } + require.NoError(err) + assert.Equal(tt.wantRowsDeleted, deletedRows) + + // TODO (jimlambrt 9/2020) - unfortunately, we can currently + // test to make sure that the oplog entry for a target update + // doesn't exist because the db.TestVerifyOplog doesn't really + // support that level of testing and the previous call to + // repo.AddTargeHostSets() would create an oplog entry for the + // update to the target. Once TestVerifyOplog supports the + // appropriate granularity,, we should add an appropriate assert. + + // we should find the oplog for the delete of target host sets + err = db.TestVerifyOplog(t, rw, tt.args.target.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + }) + } +} + +func TestRepository_SetTargetHostSets(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + testKms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, testKms) + require.NoError(t, err) + + iamRepo := iam.TestRepo(t, conn, wrapper) + _, proj := iam.TestScopes(t, iamRepo) + + testCats := static.TestCatalogs(t, conn, proj.PublicId, 1) + hsets := static.TestSets(t, conn, testCats[0].GetPublicId(), 5) + testHostSetIds := make([]string, 0, len(hsets)) + for _, hs := range hsets { + testHostSetIds = append(testHostSetIds, hs.PublicId) + } + + createHostSetsFn := func() []string { + results := []string{} + for i := 0; i < 10; i++ { + cats := static.TestCatalogs(t, conn, proj.PublicId, 1) + hsets := static.TestSets(t, conn, cats[0].GetPublicId(), 1) + results = append(results, hsets[0].PublicId) + } + return results + } + + setupFn := func(target Target) []*TargetSet { + hs := createHostSetsFn() + _, created, err := repo.AddTargeHostSets(context.Background(), target.GetPublicId(), 1, hs) + require.NoError(t, err) + require.Equal(t, 10, len(created)) + return created + } + type args struct { + target Target + targetVersion uint32 + hostSetIds []string + addToOrigHostSets bool + opt []Option + } + tests := []struct { + name string + setup func(Target) []*TargetSet + args args + wantAffectedRows int + wantErr bool + }{ + { + name: "clear", + setup: setupFn, + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "clear"), + targetVersion: 2, // yep, since setupFn will increment it to 2 + hostSetIds: []string{}, + }, + wantErr: false, + wantAffectedRows: 10, + }, + { + name: "no-change", + setup: setupFn, + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "no-change"), + targetVersion: 2, // yep, since setupFn will increment it to 2 + hostSetIds: []string{}, + addToOrigHostSets: true, + }, + wantErr: false, + wantAffectedRows: 0, + }, + { + name: "add-sets", + setup: setupFn, + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "add-sets"), + targetVersion: 2, // yep, since setupFn will increment it to 2 + hostSetIds: []string{testHostSetIds[0], testHostSetIds[1]}, + addToOrigHostSets: true, + }, + wantErr: false, + wantAffectedRows: 2, + }, + { + name: "add host sets with zero version", + setup: setupFn, + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "add host sets with zero version"), + targetVersion: 0, + hostSetIds: []string{testHostSetIds[0], testHostSetIds[1]}, + addToOrigHostSets: true, + }, + wantErr: true, + }, + { + name: "remove existing and add users and grps", + setup: setupFn, + args: args{ + target: TestTcpTarget(t, conn, proj.PublicId, "remove existing and add users and grps"), + targetVersion: 2, // yep, since setupFn will increment it to 2 + hostSetIds: []string{testHostSetIds[0], testHostSetIds[1]}, + addToOrigHostSets: false, + }, + wantErr: false, + wantAffectedRows: 12, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var origHostSets []*TargetSet + if tt.setup != nil { + origHostSets = tt.setup(tt.args.target) + } + if tt.args.addToOrigHostSets { + origIds := make([]string, 0, len(origHostSets)) + for _, s := range origHostSets { + origIds = append(origIds, s.PublicId) + } + tt.args.hostSetIds = append(tt.args.hostSetIds, origIds...) + } + origTarget, lookedUpHs, err := repo.LookupTarget(context.Background(), tt.args.target.GetPublicId()) + require.NoError(err) + assert.Equal(len(origHostSets), len(lookedUpHs)) + + got, affectedRows, err := repo.SetTargetHostSets(context.Background(), tt.args.target.GetPublicId(), tt.args.targetVersion, tt.args.hostSetIds, tt.args.opt...) + if tt.wantErr { + require.Error(err) + t.Log(err) + return + } + t.Log(err) + require.NoError(err) + assert.Equal(tt.wantAffectedRows, affectedRows) + assert.Equal(len(tt.args.hostSetIds), len(got)) + + var wantIds []string + wantIds = append(wantIds, tt.args.hostSetIds...) + sort.Strings(wantIds) + + var gotIds []string + if len(got) > 0 { + gotIds = make([]string, 0, len(got)) + for _, s := range got { + gotIds = append(gotIds, s.PublicId) + } + } + sort.Strings(gotIds) + assert.Equal(wantIds, gotIds) + + foundTarget, _, err := repo.LookupTarget(context.Background(), tt.args.target.GetPublicId()) + require.NoError(err) + if tt.name != "no-change" { + assert.Equalf(tt.args.targetVersion+1, foundTarget.GetVersion(), "%s unexpected version: %d/%d", tt.name, tt.args.targetVersion+1, foundTarget.GetVersion()) + assert.Equalf(origTarget.GetVersion(), foundTarget.GetVersion()-1, "%s unexpected version: %d/%d", tt.name, origTarget.GetVersion(), foundTarget.GetVersion()-1) + } + t.Logf("target: %v and origVersion/newVersion: %d/%d", foundTarget.GetPublicId(), origTarget.GetVersion(), foundTarget.GetVersion()) + }) + } +} diff --git a/internal/target/store/target.pb.go b/internal/target/store/target.pb.go new file mode 100644 index 000000000..f824fbc9f --- /dev/null +++ b/internal/target/store/target.pb.go @@ -0,0 +1,526 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.12.4 +// source: controller/storage/target/store/v1/target.proto + +package store + +import ( + proto "github.com/golang/protobuf/proto" + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/internal/gen/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) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +// TargetView is a view that contains all the target subtypes +type TargetView struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is used to access the Target via an 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"` + // scope id for the Target + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,20,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + // name is the optional friendly name used to + // access the Target via an API + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,30,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description of the Target + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,40,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,50,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,60,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // version allows optimistic locking of the Target when modifying the + // Target + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,70,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // type represents the type of the Target + // @inject_tag: `gorm:"default:null"` + Type string `protobuf:"bytes,80,opt,name=type,proto3" json:"type,omitempty" gorm:"default:null"` + // default port of the Target + // @inject_tag: `gorm:"default:null"` + DefaultPort uint32 `protobuf:"varint,90,opt,name=default_port,json=defaultPort,proto3" json:"default_port,omitempty" gorm:"default:null"` +} + +func (x *TargetView) Reset() { + *x = TargetView{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TargetView) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TargetView) ProtoMessage() {} + +func (x *TargetView) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_target_store_v1_target_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 TargetView.ProtoReflect.Descriptor instead. +func (*TargetView) Descriptor() ([]byte, []int) { + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{0} +} + +func (x *TargetView) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *TargetView) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *TargetView) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TargetView) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *TargetView) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *TargetView) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *TargetView) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *TargetView) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *TargetView) GetDefaultPort() uint32 { + if x != nil { + return x.DefaultPort + } + return 0 +} + +type TargetHostSet struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // target_id of the TargetHostSet + // @inject_tag: gorm:"primary_key" + TargetId string `protobuf:"bytes,10,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty" gorm:"primary_key"` + // host_set_id of the TargetHostSet + // @inject_tag: gorm:"primary_key" + HostSetId string `protobuf:"bytes,20,opt,name=host_set_id,json=hostSetId,proto3" json:"host_set_id,omitempty" gorm:"primary_key"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *TargetHostSet) Reset() { + *x = TargetHostSet{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TargetHostSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TargetHostSet) ProtoMessage() {} + +func (x *TargetHostSet) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_target_store_v1_target_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 TargetHostSet.ProtoReflect.Descriptor instead. +func (*TargetHostSet) Descriptor() ([]byte, []int) { + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{1} +} + +func (x *TargetHostSet) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *TargetHostSet) GetHostSetId() string { + if x != nil { + return x.HostSetId + } + return "" +} + +func (x *TargetHostSet) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +type TcpTarget struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is used to access the TargetTcp via an 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"` + // scope id for the TargetTcp + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,20,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + // name is the optional friendly name used to + // access the TargetTcp via an API + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,30,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description of the TargetTcp + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,40,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,50,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,60,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // version allows optimistic locking of the TargetTcp when modifying the + // TargetTcp + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,70,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // default port of the TargetTcp + // @inject_tag: `gorm:"default:null"` + DefaultPort uint32 `protobuf:"varint,80,opt,name=default_port,json=defaultPort,proto3" json:"default_port,omitempty" gorm:"default:null"` +} + +func (x *TcpTarget) Reset() { + *x = TcpTarget{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_target_store_v1_target_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TcpTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpTarget) ProtoMessage() {} + +func (x *TcpTarget) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_target_store_v1_target_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 TcpTarget.ProtoReflect.Descriptor instead. +func (*TcpTarget) Descriptor() ([]byte, []int) { + return file_controller_storage_target_store_v1_target_proto_rawDescGZIP(), []int{2} +} + +func (x *TcpTarget) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *TcpTarget) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *TcpTarget) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TcpTarget) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *TcpTarget) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *TcpTarget) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *TcpTarget) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *TcpTarget) GetDefaultPort() uint32 { + if x != nil { + return x.DefaultPort + } + return 0 +} + +var File_controller_storage_target_store_v1_target_proto protoreflect.FileDescriptor + +var file_controller_storage_target_store_v1_target_proto_rawDesc = []byte{ + 0x0a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 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, 0x74, 0x61, 0x72, 0x67, 0x65, 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, 0x97, 0x03, 0x0a, 0x0a, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x69, 0x65, + 0x77, 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, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x1e, 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, 0x28, + 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, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x32, 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, 0x3c, 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, 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, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x50, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x99, 0x01, 0x0a, + 0x0d, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x68, + 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 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, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x82, 0x03, 0x0a, 0x09, 0x54, 0x63, 0x70, + 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 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, 0x24, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x1e, 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, 0x28, 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, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x32, 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, 0x3c, 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, 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, 0x21, 0x0a, 0x0c, 0x64, 0x65, + 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x6f, 0x72, 0x74, 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, 0x74, 0x61, 0x72, 0x67, 0x65, 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_target_store_v1_target_proto_rawDescOnce sync.Once + file_controller_storage_target_store_v1_target_proto_rawDescData = file_controller_storage_target_store_v1_target_proto_rawDesc +) + +func file_controller_storage_target_store_v1_target_proto_rawDescGZIP() []byte { + file_controller_storage_target_store_v1_target_proto_rawDescOnce.Do(func() { + file_controller_storage_target_store_v1_target_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_target_store_v1_target_proto_rawDescData) + }) + return file_controller_storage_target_store_v1_target_proto_rawDescData +} + +var file_controller_storage_target_store_v1_target_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_storage_target_store_v1_target_proto_goTypes = []interface{}{ + (*TargetView)(nil), // 0: controller.storage.target.store.v1.TargetView + (*TargetHostSet)(nil), // 1: controller.storage.target.store.v1.TargetHostSet + (*TcpTarget)(nil), // 2: controller.storage.target.store.v1.TcpTarget + (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_target_store_v1_target_proto_depIdxs = []int32{ + 3, // 0: controller.storage.target.store.v1.TargetView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 1: controller.storage.target.store.v1.TargetView.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 2: controller.storage.target.store.v1.TargetHostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 3: controller.storage.target.store.v1.TcpTarget.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 4: controller.storage.target.store.v1.TcpTarget.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_controller_storage_target_store_v1_target_proto_init() } +func file_controller_storage_target_store_v1_target_proto_init() { + if File_controller_storage_target_store_v1_target_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_target_store_v1_target_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TargetView); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_target_store_v1_target_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TargetHostSet); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_target_store_v1_target_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TcpTarget); 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_target_store_v1_target_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_target_store_v1_target_proto_goTypes, + DependencyIndexes: file_controller_storage_target_store_v1_target_proto_depIdxs, + MessageInfos: file_controller_storage_target_store_v1_target_proto_msgTypes, + }.Build() + File_controller_storage_target_store_v1_target_proto = out.File + file_controller_storage_target_store_v1_target_proto_rawDesc = nil + file_controller_storage_target_store_v1_target_proto_goTypes = nil + file_controller_storage_target_store_v1_target_proto_depIdxs = nil +} diff --git a/internal/target/target.go b/internal/target/target.go new file mode 100644 index 000000000..ca4276d73 --- /dev/null +++ b/internal/target/target.go @@ -0,0 +1,90 @@ +package target + +import ( + "fmt" + + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target/store" +) + +// Target is a commmon interface for all target subtypes +type Target interface { + GetPublicId() string + GetScopeId() string + GetDefaultPort() uint32 + GetName() string + GetDescription() string + GetVersion() uint32 + oplog(op oplog.OpType) oplog.Metadata +} + +// TargetType defines the possible types for targets. +type TargetType uint32 + +const ( + UnknownTargetType TargetType = 0 + TcpTargetType TargetType = 1 +) + +// String returns a string representation of the target type. +func (t TargetType) String() string { + return [...]string{ + "unknown", + "tcp", + }[t] +} + +const ( + targetsViewDefaultTable = "target_all_subtypes" +) + +// targetView provides a common way to return targets regardless of their +// underlying type. +type targetView struct { + *store.TargetView + tableName string `gorm:"-"` +} + +// allocTargetView will allocate a target view +func allocTargetView() targetView { + return targetView{ + TargetView: &store.TargetView{}, + } +} + +// TableName provides an overridden gorm table name for targets. +func (t *targetView) TableName() string { + if t.tableName != "" { + return t.tableName + } + return targetsViewDefaultTable +} + +// SetTableName sets the table name for the resource. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (t *targetView) SetTableName(n string) { + switch n { + case "": + t.tableName = targetsViewDefaultTable + default: + t.tableName = n + } +} + +// targetSubType converts the target view to the concrete subtype +func (t *targetView) targetSubType() (Target, error) { + switch t.Type { + case TcpTargetType.String(): + tcpTarget := allocTcpTarget() + tcpTarget.PublicId = t.PublicId + tcpTarget.ScopeId = t.ScopeId + tcpTarget.Name = t.Name + tcpTarget.Description = t.Description + tcpTarget.DefaultPort = t.DefaultPort + tcpTarget.CreateTime = t.CreateTime + tcpTarget.UpdateTime = t.UpdateTime + tcpTarget.Version = t.Version + return &tcpTarget, nil + } + return nil, fmt.Errorf("%s is an unknown target subtype of %s", t.PublicId, t.Type) +} diff --git a/internal/target/target_host_set.go b/internal/target/target_host_set.go new file mode 100644 index 000000000..85f714a75 --- /dev/null +++ b/internal/target/target_host_set.go @@ -0,0 +1,104 @@ +package target + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + hostStore "github.com/hashicorp/boundary/internal/host/store" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target/store" + "google.golang.org/protobuf/proto" +) + +const ( + DefaultTargetHostSetTableName = "target_host_set" +) + +type TargetHostSet struct { + *store.TargetHostSet + tableName string `gorm:"-"` +} + +var _ db.VetForWriter = (*TargetHostSet)(nil) + +// TargetHostSet creates a new in memory target host set. No options are +// currently supported. +func NewTargetHostSet(targetId, hostSetId string, opt ...Option) (*TargetHostSet, error) { + if targetId == "" { + return nil, fmt.Errorf("new target host set: missing target id: %w", db.ErrInvalidParameter) + } + if hostSetId == "" { + return nil, fmt.Errorf("new target host set: missing hostSetId id: %w", db.ErrInvalidParameter) + } + t := &TargetHostSet{ + TargetHostSet: &store.TargetHostSet{ + TargetId: targetId, + HostSetId: hostSetId, + }, + } + return t, nil +} + +// allocTargetHostSet will allocate a target host set +func allocTargetHostSet() TargetHostSet { + return TargetHostSet{ + TargetHostSet: &store.TargetHostSet{}, + } +} + +// Clone creates a clone of the target host set +func (t *TargetHostSet) Clone() interface{} { + cp := proto.Clone(t.TargetHostSet) + return &TargetHostSet{ + TargetHostSet: cp.(*store.TargetHostSet), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the target +// host set before it's written. +func (t *TargetHostSet) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if opType == db.CreateOp { + if t.TargetId == "" { + return fmt.Errorf("target host set: vet for write: missing target id: %w", db.ErrInvalidParameter) + } + if t.HostSetId == "" { + return fmt.Errorf("target host set: vet for write: missing host set id: %w", db.ErrInvalidParameter) + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (t *TargetHostSet) TableName() string { + if t.tableName != "" { + return t.tableName + } + return DefaultTargetHostSetTableName +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (t *TargetHostSet) SetTableName(n string) { + t.tableName = n +} + +func (t *TargetHostSet) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{fmt.Sprintf("%s:%s", t.TargetId, t.HostSetId)}, + "resource-type": []string{"target host set"}, + "op-type": []string{op.String()}, + } + return metadata +} + +// TargetSet is returned from most repo operations as the target's host set. +type TargetSet struct { + *hostStore.Set +} + +// TableName returns the tablename to override the default gorm table name +func (ts *TargetSet) TableName() string { + return "target_set" +} diff --git a/internal/target/target_test.go b/internal/target/target_test.go new file mode 100644 index 000000000..d3a287c38 --- /dev/null +++ b/internal/target/target_test.go @@ -0,0 +1,39 @@ +package target + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTarget_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := targetsViewDefaultTable + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocTargetView() + require.Equal(defaultTableName, def.TableName()) + s := allocTargetView() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/target/tcp_target.go b/internal/target/tcp_target.go new file mode 100644 index 000000000..078af055b --- /dev/null +++ b/internal/target/tcp_target.go @@ -0,0 +1,102 @@ +package target + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/target/store" + "google.golang.org/protobuf/proto" +) + +const ( + DefaultTcpTableName = "target_tcp" +) + +type TcpTarget struct { + *store.TcpTarget + tableName string `gorm:"-"` +} + +var _ Target = (*TcpTarget)(nil) +var _ db.VetForWriter = (*TcpTarget)(nil) +var _ oplog.ReplayableMessage = (*TcpTarget)(nil) + +// NewTcpTarget creates a new in memory tcp target. WithName, WithDescription and +// WithDefaultPort options are supported +func NewTcpTarget(scopeId, name string, opt ...Option) (*TcpTarget, error) { + opts := getOpts(opt...) + if scopeId == "" { + return nil, fmt.Errorf("new tcp target: missing scope id: %w", db.ErrInvalidParameter) + } + if name == "" { + return nil, fmt.Errorf("new tcp target: missing name: %w", db.ErrInvalidParameter) + } + t := &TcpTarget{ + TcpTarget: &store.TcpTarget{ + ScopeId: scopeId, + Name: name, + Description: opts.withDescription, + DefaultPort: opts.withDefaultPort, + }, + } + return t, nil +} + +// allocTcpTarget will allocate a tcp target +func allocTcpTarget() TcpTarget { + return TcpTarget{ + TcpTarget: &store.TcpTarget{}, + } +} + +// Clone creates a clone of the TcpTarget +func (t *TcpTarget) Clone() interface{} { + cp := proto.Clone(t.TcpTarget) + return &TcpTarget{ + TcpTarget: cp.(*store.TcpTarget), + } +} + +// VetForWrite implements db.VetForWrite() interface and validates the tcp target +// before it's written. +func (t *TcpTarget) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if t.PublicId == "" { + return fmt.Errorf("tcp target vet for write: missing public id: %w", db.ErrInvalidParameter) + } + if opType == db.CreateOp { + if t.ScopeId == "" { + return fmt.Errorf("tcp target vet for write: missing scope id: %w", db.ErrInvalidParameter) + } + if t.Name == "" { + return fmt.Errorf("tcp target vet for write: missing name id: %w", db.ErrInvalidParameter) + } + } + return nil +} + +// TableName returns the tablename to override the default gorm table name +func (t *TcpTarget) TableName() string { + if t.tableName != "" { + return t.tableName + } + return DefaultTcpTableName +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage +// interface. If the caller attempts to set the name to "" the name will be +// reset to the default name. +func (t *TcpTarget) SetTableName(n string) { + t.tableName = n +} + +func (t *TcpTarget) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{t.PublicId}, + "resource-type": []string{"tcp target"}, + "op-type": []string{op.String()}, + "scope-id": []string{t.ScopeId}, + } + return metadata +} diff --git a/internal/target/tcp_target_test.go b/internal/target/tcp_target_test.go new file mode 100644 index 000000000..a92495afd --- /dev/null +++ b/internal/target/tcp_target_test.go @@ -0,0 +1,393 @@ +package target + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestTcpTarget_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + type args struct { + scopeId string + name string + opt []Option + } + tests := []struct { + name string + args args + want *TcpTarget + wantErr bool + wantIsErr error + create bool + wantCreateErr bool + }{ + { + name: "empty-scopeId", + args: args{}, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "valid-proj-scope", + args: args{ + scopeId: prj.PublicId, + name: "valid-proj-scope", + }, + want: func() *TcpTarget { + t := allocTcpTarget() + t.ScopeId = prj.PublicId + t.Name = "valid-proj-scope" + return &t + }(), + create: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewTcpTarget(tt.args.scopeId, tt.args.name, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Is(err, tt.wantIsErr)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + id, err := newTcpTargetId() + require.NoError(err) + got.PublicId = id + err = db.New(conn).Create(context.Background(), got) + if tt.wantCreateErr { + assert.Error(err) + return + } else { + assert.NoError(err) + } + } + }) + } +} + +func TestTcpTarget_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + tests := []struct { + name string + target *TcpTarget + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + target: TestTcpTarget(t, conn, proj.PublicId, testTargetName(t, proj.PublicId)), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + target: func() *TcpTarget { + target := allocTcpTarget() + id, err := newTcpTargetId() + require.NoError(t, err) + target.PublicId = id + target.ScopeId = proj.PublicId + target.Name = testTargetName(t, proj.PublicId) + return &target + }(), + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + deleteTarget := allocTcpTarget() + deleteTarget.PublicId = tt.target.PublicId + deletedRows, err := rw.Delete(context.Background(), &deleteTarget) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + foundTarget := allocTcpTarget() + foundTarget.PublicId = tt.target.PublicId + err = rw.LookupById(context.Background(), &foundTarget) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestTcpTarget_Update(t *testing.T) { + t.Parallel() + id := testId(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + type args struct { + name string + description string + fieldMaskPaths []string + nullPaths []string + ScopeId string + } + tests := []struct { + name string + args args + wantRowsUpdate int + wantErr bool + wantErrMsg string + wantDup bool + }{ + { + name: "valid", + args: args{ + name: "valid" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "proj-scope-id-not-in-mask", + args: args{ + name: "proj-scope-id" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "empty-scope-id", + args: args{ + name: "empty-scope-id" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: "", + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "dup-name", + args: args{ + name: "dup-name" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + wantErr: true, + wantDup: true, + wantErrMsg: `update: failed: pq: duplicate key value violates unique constraint "target_tcp_scope_id_name_key"`, + }, + { + name: "set description null", + args: args{ + name: "set description null" + id, + fieldMaskPaths: []string{"Name"}, + nullPaths: []string{"Description"}, + ScopeId: proj.PublicId, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "set name null", + args: args{ + description: "set description null" + id, + fieldMaskPaths: []string{"Description"}, + nullPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + wantErr: true, + wantErrMsg: `update: failed: pq: null value in column "name" violates not-null constraint`, + }, + { + name: "set description null", + args: args{ + name: "set name null" + id, + fieldMaskPaths: []string{"Name"}, + nullPaths: []string{"Description"}, + ScopeId: proj.PublicId, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + if tt.wantDup { + target := TestTcpTarget(t, conn, proj.PublicId, testTargetName(t, proj.PublicId)) + target.Name = tt.args.name + _, err := rw.Update(context.Background(), target, tt.args.fieldMaskPaths, tt.args.nullPaths) + require.NoError(err) + } + + id := testId(t) + target := TestTcpTarget(t, conn, proj.PublicId, id, WithDescription(id)) + + updateTarget := allocTcpTarget() + updateTarget.PublicId = target.PublicId + updateTarget.ScopeId = tt.args.ScopeId + updateTarget.Name = tt.args.name + updateTarget.Description = tt.args.description + + updatedRows, err := rw.Update(context.Background(), &updateTarget, tt.args.fieldMaskPaths, tt.args.nullPaths) + if tt.wantErr { + require.Error(err) + assert.Equal(0, updatedRows) + assert.Equal(tt.wantErrMsg, err.Error()) + err = db.TestVerifyOplog(t, rw, target.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + require.Error(err) + assert.Equal("record not found", err.Error()) + return + } + require.NoError(err) + assert.Equal(tt.wantRowsUpdate, updatedRows) + assert.NotEqual(target.UpdateTime, updateTarget.UpdateTime) + foundTarget := allocTcpTarget() + foundTarget.PublicId = target.GetPublicId() + err = rw.LookupByPublicId(context.Background(), &foundTarget) + require.NoError(err) + assert.True(proto.Equal(updateTarget, foundTarget)) + if len(tt.args.nullPaths) != 0 { + dbassert := dbassert.New(t, rw) + for _, f := range tt.args.nullPaths { + dbassert.IsNull(&foundTarget, f) + } + } + }) + } + t.Run("update dup names in diff scopes", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + id := testId(t) + _, proj2 := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + _ = TestTcpTarget(t, conn, proj2.PublicId, id, WithDescription(id)) + projTarget := TestTcpTarget(t, conn, proj.PublicId, id) + projTarget.Name = id + updatedRows, err := rw.Update(context.Background(), projTarget, []string{"Name"}, nil) + require.NoError(err) + assert.Equal(1, updatedRows) + + foundTarget := allocTcpTarget() + foundTarget.PublicId = projTarget.GetPublicId() + err = rw.LookupByPublicId(context.Background(), &foundTarget) + require.NoError(err) + assert.Equal(id, projTarget.Name) + }) +} + +func TestTcpTarget_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + t.Run("valid", func(t *testing.T) { + assert := assert.New(t) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + target := TestTcpTarget(t, conn, proj.PublicId, testTargetName(t, proj.PublicId)) + cp := target.Clone() + assert.True(proto.Equal(cp.(*TcpTarget).TcpTarget, target.TcpTarget)) + }) + t.Run("not-equal", func(t *testing.T) { + assert := assert.New(t) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + _, proj2 := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + target := TestTcpTarget(t, conn, proj.PublicId, testTargetName(t, proj.PublicId)) + target2 := TestTcpTarget(t, conn, proj2.PublicId, testTargetName(t, proj2.PublicId)) + + cp := target.Clone() + assert.True(!proto.Equal(cp.(*TcpTarget).TcpTarget, target2.TcpTarget)) + }) +} + +func TestTcpTable_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := DefaultTcpTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := allocTcpTarget() + require.Equal(defaultTableName, def.TableName()) + s := allocTcpTarget() + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} + +func TestTcpTarget_oplog(t *testing.T) { + id := testId(t) + tests := []struct { + name string + target *TcpTarget + op oplog.OpType + want oplog.Metadata + }{ + { + name: "simple", + target: func() *TcpTarget { + t := allocTcpTarget() + t.PublicId = id + t.ScopeId = id + return &t + }(), + op: oplog.OpType_OP_TYPE_CREATE, + want: oplog.Metadata{ + "resource-public-id": []string{id}, + "resource-type": []string{"tcp target"}, + "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, + "scope-id": []string{id}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + got := tt.target.oplog(tt.op) + assert.Equal(got, tt.want) + }) + } +} diff --git a/internal/target/testing.go b/internal/target/testing.go new file mode 100644 index 000000000..2f814f5c7 --- /dev/null +++ b/internal/target/testing.go @@ -0,0 +1,50 @@ +package target + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/go-uuid" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/require" +) + +func TestTcpTarget(t *testing.T, conn *gorm.DB, scopeId, name string, opt ...Option) *TcpTarget { + t.Helper() + opts := getOpts(opt...) + require := require.New(t) + rw := db.New(conn) + target, err := NewTcpTarget(scopeId, name, opt...) + require.NoError(err) + id, err := newTcpTargetId() + require.NoError(err) + target.PublicId = id + err = rw.Create(context.Background(), target) + require.NoError(err) + + if len(opts.withHostSets) > 0 { + newHostSets := make([]interface{}, 0, len(opts.withHostSets)) + for _, s := range opts.withHostSets { + hostSet, err := NewTargetHostSet(target.PublicId, s) + require.NoError(err) + newHostSets = append(newHostSets, hostSet) + } + err := rw.CreateItems(context.Background(), newHostSets) + require.NoError(err) + } + return target +} + +func testTargetName(t *testing.T, scopeId string) string { + t.Helper() + return fmt.Sprintf("%s-%s", scopeId, testId(t)) +} + +func testId(t *testing.T) string { + t.Helper() + id, err := uuid.GenerateUUID() + require.NoError(t, err) + return id +} diff --git a/internal/target/testing_test.go b/internal/target/testing_test.go new file mode 100644 index 000000000..4c932f222 --- /dev/null +++ b/internal/target/testing_test.go @@ -0,0 +1,39 @@ +package target + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/stretchr/testify/require" +) + +func Test_TestTcpTarget(t *testing.T) { + t.Helper() + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cats := static.TestCatalogs(t, conn, proj.PublicId, 1) + hsets := static.TestSets(t, conn, cats[0].GetPublicId(), 2) + var sets []string + for _, s := range hsets { + sets = append(sets, s.PublicId) + } + name := testTargetName(t, proj.PublicId) + target := TestTcpTarget(t, conn, proj.PublicId, name, WithHostSets(sets)) + require.NotNil(t) + require.NotEmpty(target.PublicId) + require.Equal(name, target.Name) + + rw := db.New(conn) + foundSets, err := fetchSets(context.Background(), rw, target.PublicId) + require.NoError(err) + foundIds := make([]string, 0, len(foundSets)) + for _, s := range foundSets { + foundIds = append(foundIds, s.PublicId) + } + require.Equal(sets, foundIds) +}