Add preferred endpoints logic to API/handler/DB and some CLI bits (#1529)

This commit is contained in:
Jeff Mitchell authored and GitHub committed 2021-09-20 12:48:38 -04:00
1 parent f9c620a5d5
commit 01d4383c1b
46 files changed
+1737 -231

No files matched your search

+13 -12
View File
@@ -13,18 +13,19 @@ import (
)
type HostSet struct {
Id string `json:"id,omitempty"`
HostCatalogId string `json:"host_catalog_id,omitempty"`
Scope *scopes.ScopeInfo `json:"scope,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
CreatedTime time.Time `json:"created_time,omitempty"`
UpdatedTime time.Time `json:"updated_time,omitempty"`
Version uint32 `json:"version,omitempty"`
Type string `json:"type,omitempty"`
HostIds []string `json:"host_ids,omitempty"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
AuthorizedActions []string `json:"authorized_actions,omitempty"`
Id string `json:"id,omitempty"`
HostCatalogId string `json:"host_catalog_id,omitempty"`
Scope *scopes.ScopeInfo `json:"scope,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
CreatedTime time.Time `json:"created_time,omitempty"`
UpdatedTime time.Time `json:"updated_time,omitempty"`
Version uint32 `json:"version,omitempty"`
Type string `json:"type,omitempty"`
HostIds []string `json:"host_ids,omitempty"`
PreferredEndpoints []string `json:"preferred_endpoints,omitempty"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
AuthorizedActions []string `json:"authorized_actions,omitempty"`
response *api.Response
}
+12
View File
@@ -97,3 +97,15 @@ func DefaultName() Option {
o.postMap["name"] = nil
}
}
func WithPreferredEndpoints(inPreferredEndpoints []string) Option {
return func(o *options) {
o.postMap["preferred_endpoints"] = inPreferredEndpoints
}
}
func DefaultPreferredEndpoints() Option {
return func(o *options) {
o.postMap["preferred_endpoints"] = nil
}
}
+1
View File
@@ -58,4 +58,5 @@ const (
ApplicationCredentialLibrariesField = "application_credential_libraries"
ApplicationCredentialSourceIdsField = "application_credential_source_ids"
ApplicationCredentialSourcesField = "application_credential_sources"
PreferredEndpointsField = "preferred_endpoints"
)
+12
View File
@@ -560,6 +560,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
Func: "create",
}, nil
},
"host-sets create plugin": func() (cli.Command, error) {
return &hostsetscmd.PluginCommand{
Command: base.NewCommand(ui),
Func: "create",
}, nil
},
"host-sets update": func() (cli.Command, error) {
return &hostsetscmd.Command{
Command: base.NewCommand(ui),
@@ -572,6 +578,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
Func: "update",
}, nil
},
"host-sets update plugin": func() (cli.Command, error) {
return &hostsetscmd.PluginCommand{
Command: base.NewCommand(ui),
Func: "update",
}, nil
},
"host-sets add-hosts": func() (cli.Command, error) {
return &hostsetscmd.Command{
Command: base.NewCommand(ui),
@@ -253,6 +253,9 @@ func printItemTable(result api.GenericResult) string {
if item.HostCatalogId != "" {
nonAttributeMap["Host Catalog ID"] = item.HostCatalogId
}
if item.PreferredEndpoints != nil {
nonAttributeMap["Preferred Endpoints"] = item.PreferredEndpoints
}
maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap)
@@ -0,0 +1,97 @@
package hostsetscmd
import (
"fmt"
"github.com/hashicorp/boundary/api/hostsets"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/libs/endpoint"
)
func init() {
extraPluginActionsFlagsMapFunc = extraPluginActionsFlagsMapFuncImpl
extraPluginFlagsFunc = extraPluginFlagsFuncImpl
extraPluginFlagsHandlingFunc = extraPluginFlagsHandlingFuncImpl
}
type extraPluginCmdVars struct {
flagPreferredEndpoints []string
}
func extraPluginActionsFlagsMapFuncImpl() map[string][]string {
return map[string][]string{
"create": {"preferred-endpoint"},
"update": {"preferred-endpoint"},
}
}
func (c *PluginCommand) extraPluginHelpFunc(helpMap map[string]func() string) string {
var helpStr string
switch c.Func {
case "create":
helpStr = base.WrapForHelpText([]string{
"Usage: boundary host-sets create plugin [options] [args]",
"",
" Create a host set of a type provided by a plugin. Example:",
"",
` $ boundary host-sets create plugin -plugin-id plg_1234567890 -name prodops -description "Plugin host-set for ProdOps"`,
"",
"",
})
case "update":
helpStr = base.WrapForHelpText([]string{
"Usage: boundary host-sets update plugin [options] [args]",
"",
" Update a host set of a type provided by a plugin given its ID. Example:",
"",
` $ boundary host-sets update plugin -id hsplg_1234567890 -name "devops" -description "Plugin host-set for DevOps"`,
"",
"",
})
default:
helpStr = helpMap[c.Func]()
}
return helpStr + c.Flags().Help()
}
func extraPluginFlagsFuncImpl(c *PluginCommand, set *base.FlagSets, f *base.FlagSet) {
fs := set.NewFlagSet("Plugin Host-Set Options")
for _, name := range flagsPluginMap[c.Func] {
switch name {
case "preferred-endpoint":
fs.StringSliceVar(&base.StringSliceVar{
Name: "preferred-endpoint",
Target: &c.flagPreferredEndpoints,
Usage: `An endpoint preference, specified by "cidr:<valid IPv4/6 CIDR>" ` +
`or "dns:<globbed name>", specifying which IP address or DNS name out ` +
`of a host's available possibilities should be preferred. May be specified ` +
`multiple times, which will build up an in-order set of preferences. ` +
`If no preferences are specified, a value will be chosen _at random_ from ` +
`all available values. May not be valid for all plugin types.`,
})
}
}
}
func extraPluginFlagsHandlingFuncImpl(c *PluginCommand, _ *base.FlagSets, opts *[]hostsets.Option) bool {
switch len(c.flagPreferredEndpoints) {
case 0:
case 1:
if c.flagPreferredEndpoints[0] == "null" {
*opts = append(*opts, hostsets.DefaultPreferredEndpoints())
break
}
fallthrough
default:
if _, err := endpoint.NewPreferencer(c.Context, endpoint.WithPreferenceOrder(c.flagPreferredEndpoints)); err != nil {
c.UI.Error(fmt.Sprintf("Unable to successfully validate preferred endpoints: %s", err))
return false
}
*opts = append(*opts, hostsets.WithPreferredEndpoints(c.flagPreferredEndpoints))
}
return true
}
@@ -0,0 +1,241 @@
// Code generated by "make cli"; DO NOT EDIT.
package hostsetscmd
import (
"errors"
"fmt"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/hostsets"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
func initPluginFlags() {
flagsOnce.Do(func() {
extraFlags := extraPluginActionsFlagsMapFunc()
for k, v := range extraFlags {
flagsPluginMap[k] = append(flagsPluginMap[k], v...)
}
})
}
var (
_ cli.Command = (*PluginCommand)(nil)
_ cli.CommandAutocomplete = (*PluginCommand)(nil)
)
type PluginCommand struct {
*base.Command
Func string
plural string
extraPluginCmdVars
}
func (c *PluginCommand) AutocompleteArgs() complete.Predictor {
initPluginFlags()
return complete.PredictAnything
}
func (c *PluginCommand) AutocompleteFlags() complete.Flags {
initPluginFlags()
return c.Flags().Completions()
}
func (c *PluginCommand) Synopsis() string {
if extra := extraPluginSynopsisFunc(c); extra != "" {
return extra
}
synopsisStr := "host set"
synopsisStr = fmt.Sprintf("%s %s", "plugin-type", synopsisStr)
return common.SynopsisFunc(c.Func, synopsisStr)
}
func (c *PluginCommand) Help() string {
initPluginFlags()
var helpStr string
helpMap := common.HelpMap("host set")
switch c.Func {
default:
helpStr = c.extraPluginHelpFunc(helpMap)
}
// Keep linter from complaining if we don't actually generate code using it
_ = helpMap
return helpStr
}
var flagsPluginMap = map[string][]string{
"create": {"host-catalog-id", "name", "description"},
"update": {"id", "name", "description", "version"},
}
func (c *PluginCommand) Flags() *base.FlagSets {
if len(flagsPluginMap[c.Func]) == 0 {
return c.FlagSet(base.FlagSetNone)
}
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
common.PopulateCommonFlags(c.Command, f, "plugin-type host set", flagsPluginMap, c.Func)
extraPluginFlagsFunc(c, set, f)
return set
}
func (c *PluginCommand) Run(args []string) int {
initPluginFlags()
switch c.Func {
case "":
return cli.RunResultHelp
}
c.plural = "plugin-type host set"
switch c.Func {
case "list":
c.plural = "plugin-type host sets"
}
f := c.Flags()
if err := f.Parse(args); err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
if strutil.StrListContains(flagsPluginMap[c.Func], "id") && c.FlagId == "" {
c.PrintCliError(errors.New("ID is required but not passed in via -id"))
return base.CommandUserError
}
var opts []hostsets.Option
if strutil.StrListContains(flagsPluginMap[c.Func], "host-catalog-id") {
switch c.Func {
case "create":
if c.FlagHostCatalogId == "" {
c.PrintCliError(errors.New("HostCatalog ID must be passed in via -host-catalog-id or BOUNDARY_HOST_CATALOG_ID"))
return base.CommandUserError
}
}
}
client, err := c.Client()
if err != nil {
c.PrintCliError(fmt.Errorf("Error creating API client: %s", err.Error()))
return base.CommandCliError
}
hostsetsClient := hostsets.NewClient(client)
switch c.FlagName {
case "":
case "null":
opts = append(opts, hostsets.DefaultName())
default:
opts = append(opts, hostsets.WithName(c.FlagName))
}
switch c.FlagDescription {
case "":
case "null":
opts = append(opts, hostsets.DefaultDescription())
default:
opts = append(opts, hostsets.WithDescription(c.FlagDescription))
}
if c.FlagFilter != "" {
opts = append(opts, hostsets.WithFilter(c.FlagFilter))
}
var version uint32
switch c.Func {
case "update":
switch c.FlagVersion {
case 0:
opts = append(opts, hostsets.WithAutomaticVersioning(true))
default:
version = uint32(c.FlagVersion)
}
}
if ok := extraPluginFlagsHandlingFunc(c, f, &opts); !ok {
return base.CommandUserError
}
var result api.GenericResult
switch c.Func {
case "create":
result, err = hostsetsClient.Create(c.Context, c.FlagHostCatalogId, opts...)
case "update":
result, err = hostsetsClient.Update(c.Context, c.FlagId, version, opts...)
}
result, err = executeExtraPluginActions(c, result, err, hostsetsClient, version, opts)
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil {
var opts []base.Option
c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural), opts...)
return base.CommandApiError
}
c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error()))
return base.CommandCliError
}
output, err := printCustomPluginActionOutput(c)
if err != nil {
c.PrintCliError(err)
return base.CommandUserError
}
if output {
return base.CommandSuccess
}
switch c.Func {
}
switch base.Format(c.UI) {
case "table":
c.UI.Output(printItemTable(result))
case "json":
if ok := c.PrintJsonItem(result); !ok {
return base.CommandCliError
}
}
return base.CommandSuccess
}
var (
extraPluginActionsFlagsMapFunc = func() map[string][]string { return nil }
extraPluginSynopsisFunc = func(*PluginCommand) string { return "" }
extraPluginFlagsFunc = func(*PluginCommand, *base.FlagSets, *base.FlagSet) {}
extraPluginFlagsHandlingFunc = func(*PluginCommand, *base.FlagSets, *[]hostsets.Option) bool { return true }
executeExtraPluginActions = func(_ *PluginCommand, inResult api.GenericResult, inErr error, _ *hostsets.Client, _ uint32, _ []hostsets.Option) (api.GenericResult, error) {
return inResult, inErr
}
printCustomPluginActionOutput = func(*PluginCommand) (bool, error) { return false, nil }
)
+14
View File
@@ -282,6 +282,20 @@ var inputStructs = map[string][]*cmdInfo{
HasDescription: true,
VersionedActions: []string{"update"},
},
{
ResourceType: resource.HostSet.String(),
Pkg: "hostsets",
StdActions: []string{"create", "update"},
SubActionPrefix: "plugin",
HasExtraCommandVars: true,
SkipNormalHelp: true,
HasExtraHelpFunc: true,
HasId: true,
HasName: true,
Container: "HostCatalog",
HasDescription: true,
VersionedActions: []string{"update"},
},
},
"hosts": {
{
@@ -0,0 +1,73 @@
begin;
create table host_set_preferred_endpoint (
create_time wt_timestamp,
host_set_id wt_public_id not null
constraint host_set_fkey
references host_set(public_id)
on delete cascade
on update cascade,
priority int not null
constraint priority_must_be_greater_than_zero
check(priority > 0),
condition text not null
constraint condition_must_not_be_too_short
check(length(trim(condition)) > 4) -- minimum is 'dns:*'
constraint condition_must_not_be_too_long
check(length(trim(condition)) < 255)
constraint condition_has_valid_prefix
check(
left(trim(condition), 4) = 'dns:'
or
left(trim(condition), 5) = 'cidr:'
)
constraint condition_does_not_contain_invalid_chars
check(
position('|' in trim(condition)) = 0
and
position('=' in trim(condition)) = 0
),
primary key(host_set_id, priority),
constraint host_set_preferred_endpoint_host_set_id_condition_uq
unique(host_set_id, condition)
);
-- host_set_immutable_preferred_endpoint() ensures that endpoint conditions
-- assigned to host sets are immutable.
create function
host_set_immutable_preferred_endpoint()
returns trigger
as $$
begin
raise exception 'preferred endpoints are immutable';
end;
$$ language plpgsql;
create trigger immutable_preferred_endpoint
before update on host_set_preferred_endpoint
for each row execute procedure host_set_immutable_preferred_endpoint();
-- host_plugin_host_set_with_value_obj is useful for reading a plugin host set with its
-- associated value objects (preferred endpoints) as columns with delimited
-- values. The delimiter depends on the value objects (e.g. if they need
-- ordering).
create view host_plugin_host_set_with_value_obj as
select
hs.public_id,
hs.catalog_id,
hs.name,
hs.description,
hs.create_time,
hs.update_time,
hs.version,
hs.attributes,
-- the string_agg(..) column will be null if there are no associated value objects
string_agg(distinct concat_ws('=', hspe.priority, hspe.condition), '|') as preferred_endpoints
from
host_plugin_set hs
left outer join host_set_preferred_endpoint hspe on hs.public_id = hspe.host_set_id
group by hs.public_id;
comment on view host_plugin_host_set_with_value_obj is
'host plugin host set with its associated value objects';
commit;
@@ -0,0 +1,166 @@
package migration
import (
"context"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_PreferredEndpointTable(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
rootWrapper := db.TestWrapper(t)
_, proj := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper))
hc := static.TestCatalogs(t, conn, proj.PublicId, 1)[0]
hs := static.TestSets(t, conn, hc.PublicId, 1)[0]
type testCondition struct {
condition string
priority uint32
}
insertTests := []struct {
testName string
hostSetId string
conditions []testCondition
wantErrContains string
}{
{
testName: "invalid host set id",
hostSetId: "hsst_1234567890",
conditions: []testCondition{
{
priority: 1,
condition: "dns:abcd",
},
{
priority: 2,
condition: "cidr:1.2.3.4",
},
},
wantErrContains: "host_set_fkey",
},
{
testName: "invalid condition prefix",
hostSetId: hs.PublicId,
conditions: []testCondition{
{
priority: 1,
condition: "dsn:abcd",
},
},
wantErrContains: "condition_has_valid_prefix",
},
{
testName: "invalid condition length",
hostSetId: hs.PublicId,
conditions: []testCondition{
{
priority: 1,
condition: "dns:",
},
},
wantErrContains: "condition_must_not_be_too_short",
},
{
testName: "invalid priority",
hostSetId: hs.PublicId,
conditions: []testCondition{
{
priority: 0,
condition: "dns:abcd",
},
},
wantErrContains: "priority must not be empty",
},
{
testName: "duplicate priority",
hostSetId: hs.PublicId,
conditions: []testCondition{
{
priority: 1,
condition: "dns:abcd",
},
{
priority: 1,
condition: "cidr:1.2.3.4",
},
},
wantErrContains: "host_set_preferred_endpoint_pkey",
},
{
testName: "invalid char 1",
hostSetId: hs.PublicId,
conditions: []testCondition{
{
priority: 1,
condition: "dns:ab|cd",
},
{
priority: 1,
condition: "cidr:1.2.3.4",
},
},
wantErrContains: "condition_does_not_contain_invalid_chars",
},
{
testName: "invalid char 2",
hostSetId: hs.PublicId,
conditions: []testCondition{
{
priority: 1,
condition: "dns:abcd",
},
{
priority: 1,
condition: "cidr:1.2=.3.4",
},
},
wantErrContains: "condition_does_not_contain_invalid_chars",
},
{
testName: "valid",
hostSetId: hs.PublicId,
conditions: []testCondition{
{
priority: 1,
condition: "dns:abcd",
},
{
priority: 2,
condition: "cidr:1.2.3.4",
},
},
},
}
for _, tt := range insertTests {
t.Run("insert: "+tt.testName, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
_, err := rw.Exec(ctx, "delete from host_set_preferred_endpoint where host_set_id = ?", []interface{}{tt.hostSetId})
require.NoError(err)
// Add items to insert
var items []interface{}
for _, cond := range tt.conditions {
ep := host.AllocPreferredEndpoint()
ep.HostSetId = tt.hostSetId
ep.Condition = cond.condition
ep.Priority = cond.priority
items = append(items, ep)
}
err = rw.CreateItems(ctx, items)
if tt.wantErrContains != "" {
require.Error(err)
assert.Contains(err.Error(), tt.wantErrContains)
return
}
require.NoError(err)
})
}
}
+72 -1
View File
@@ -4,7 +4,7 @@ package schema
func init() {
migrationStates["postgres"] = migrationState{
binarySchemaVersion: 17004,
binarySchemaVersion: 17005,
upMigrations: map[int][]byte{
1: []byte(`
create domain wt_public_id as text
@@ -7851,6 +7851,77 @@ alter table wh_session_connection_accumulating_fact
('host_plugin_catalog', 1),
('host_plugin_catalog_secret', 1),
('host_plugin_set', 1);
`),
17005: []byte(`
create table host_set_preferred_endpoint (
create_time wt_timestamp,
host_set_id wt_public_id not null
constraint host_set_fkey
references host_set(public_id)
on delete cascade
on update cascade,
priority int not null
constraint priority_must_be_greater_than_zero
check(priority > 0),
condition text not null
constraint condition_must_not_be_empty
check(length(trim(condition)) > 4) -- minimum is 'dns:*'
constraint condition_must_not_be_too_long
check(length(trim(condition)) < 255)
constraint condition_has_valid_prefix
check(
left(trim(condition), 4) = 'dns:'
or
left(trim(condition), 5) = 'cidr:'
)
constraint condition_does_not_contain_invalid_chars
check(
position('|' in trim(condition)) = 0
and
position('=' in trim(condition)) = 0
),
primary key(host_set_id, priority),
constraint host_set_preferred_endpoint_host_set_id_condition_uq
unique(host_set_id, condition)
);
-- host_set_immutable_preferred_endpoint() ensures that endpoint conditions
-- assigned to host sets are immutable.
create function
host_set_immutable_preferred_endpoint()
returns trigger
as $$
begin
raise exception 'preferred endpoints are immutable';
end;
$$ language plpgsql;
create trigger immutable_preferred_endpoint
before update on host_set_preferred_endpoint
for each row execute procedure host_set_immutable_preferred_endpoint();
-- host_plugin_host_set_with_value_obj is useful for reading a plugin host set with its
-- associated value objects (preferred endpoints) as columns with delimited
-- values. The delimiter depends on the value objects (e.g. if they need
-- ordering).
create view host_plugin_host_set_with_value_obj as
select
hs.public_id,
hs.catalog_id,
hs.name,
hs.description,
hs.create_time,
hs.update_time,
hs.version,
hs.attributes,
-- the string_agg(..) column will be null if there are no associated value objects
string_agg(distinct concat_ws('=', hspe.priority, hspe.condition), '|') as preferred_endpoints
from
host_plugin_set hs
left outer join host_set_preferred_endpoint hspe on hs.public_id = hspe.host_set_id
group by hs.public_id;
comment on view host_plugin_host_set_with_value_obj is
'host plugin host set with its associated value objects';
`),
2001: []byte(`
-- log_migration entries represent logs generated during migrations
+7
View File
@@ -3863,6 +3863,13 @@
"description": "Output only. A list of Hosts in this Host Set.",
"readOnly": true
},
"preferred_endpoints": {
"type": "array",
"items": {
"type": "string"
},
"description": "An ordered list of endpoint preferences used to choose from among\nmultiple possible endpoints for a host."
},
"attributes": {
"type": "object",
"description": "The attributes that are applicable for the specific Host Set type."
+46
View File
@@ -0,0 +1,46 @@
package host
// GetOpts - iterate the inbound Options and return a struct
func GetOpts(opt ...Option) (options, error) {
opts := getDefaultOptions()
for _, o := range opt {
if err := o(&opts); err != nil {
return opts, err
}
}
return opts, nil
}
// Option - how Options are passed as arguments.
type Option func(*options) error
// options = how options are represented
type options struct {
WithLimit int
WithOrderByCreateTime bool
Ascending bool
}
func getDefaultOptions() options {
return options{}
}
// WithLimit provides an option to provide a limit. Intentionally allowing
// negative integers. If WithLimit < 0, then unlimited results are
// returned. If WithLimit == 0, then default limits are used for results.
func WithLimit(l int) Option {
return func(o *options) error {
o.WithLimit = l
return nil
}
}
// WithOrderByCreateTime provides an option to specify ordering by the
// CreateTime field.
func WithOrderByCreateTime(ascending bool) Option {
return func(o *options) error {
o.WithOrderByCreateTime = true
o.Ascending = ascending
return nil
}
}
+34
View File
@@ -0,0 +1,34 @@
package host
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("WithLimit", func(t *testing.T) {
opts, err := GetOpts(WithLimit(1))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.WithLimit = 1
assert.Equal(t, opts, testOpts)
})
t.Run("WithOrderByCreateTime-desc", func(t *testing.T) {
opts, err := GetOpts(WithOrderByCreateTime(false))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.WithOrderByCreateTime = true
assert.Equal(t, opts, testOpts)
})
t.Run("WithOrderByCreateTime-asc", func(t *testing.T) {
opts, err := GetOpts(WithOrderByCreateTime(true))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.WithOrderByCreateTime = true
testOpts.Ascending = true
assert.Equal(t, opts, testOpts)
})
}
+1 -1
View File
@@ -70,7 +70,7 @@ func (c *HostCatalogSecret) SetTableName(n string) {
func (c *HostCatalogSecret) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "plugin.(HostCatalogSecret).encrypt"
if len(c.Secret) == 0 {
errors.New(ctx, errors.InvalidParameter, op, "no attributes defined")
return errors.New(ctx, errors.InvalidParameter, op, "no attributes defined")
}
if err := structwrapping.WrapStruct(ctx, cipher, c.HostCatalogSecret, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
@@ -122,6 +122,7 @@ func TestHostCatalogSecret_Custom_Queries(t *testing.T) {
plg := host.TestPlugin(t, conn, "test", "prefix")
cat := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId())
databaseWrapper, err := kkms.GetWrapper(ctx, prj.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
hcs, err := newHostCatalogSecret(ctx, cat.GetPublicId(), map[string]interface{}{"foo": "bar"})
require.NoError(t, err)
@@ -131,6 +132,7 @@ func TestHostCatalogSecret_Custom_Queries(t *testing.T) {
assert.NoError(t, err)
found, err := newHostCatalogSecret(ctx, cat.GetPublicId(), nil)
require.NoError(t, err)
assert.NoError(t, rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId()))
require.NoError(t, found.decrypt(ctx, databaseWrapper))
require.NoError(t, hcs.decrypt(ctx, databaseWrapper))
+1 -1
View File
@@ -186,7 +186,7 @@ func TestHostCatalog_Create_DuplicateNames(t *testing.T) {
require.NoError(t, err)
got.PublicId, err = newHostCatalogId(ctx, plg.GetIdPrefix())
require.NoError(t, err)
w.Create(ctx, got)
require.NoError(t, w.Create(ctx, got))
// Can't create another resource with the same name in the same scope
got.PublicId, err = newHostCatalogId(ctx, plg.GetIdPrefix())
+8 -6
View File
@@ -16,17 +16,18 @@ type HostSet struct {
tableName string `gorm:"-"`
}
// NewHostSet creates a new in memory HostSet assigned to catalogId.
// Attributes, name, and description are the only valid options. All other
// options are ignored.
// NewHostSet creates a new in memory HostSet assigned to catalogId. Attributes,
// name, description, and preferred endpoints are the only valid options. All
// other options are ignored.
func NewHostSet(ctx context.Context, catalogId string, opt ...Option) (*HostSet, error) {
const op = "plugin.NewHostSet"
opts := getOpts(opt...)
set := &HostSet{
HostSet: &store.HostSet{
CatalogId: catalogId,
Name: opts.withName,
Description: opts.withDescription,
CatalogId: catalogId,
Name: opts.withName,
Description: opts.withDescription,
PreferredEndpoints: opts.withPreferredEndpoints,
},
}
@@ -37,6 +38,7 @@ func NewHostSet(ctx context.Context, catalogId string, opt ...Option) (*HostSet,
}
set.Attributes = attrs
}
return set, nil
}
+16
View File
@@ -133,6 +133,22 @@ func TestHostSet_Create(t *testing.T) {
},
},
},
{
name: "valid-with-preferred-endpoints",
args: args{
catalogId: cat.GetPublicId(),
opts: []Option{
WithPreferredEndpoints([]string{"cidr:1.2.3.4"}),
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: cat.GetPublicId(),
PreferredEndpoints: []string{"cidr:1.2.3.4"},
Attributes: []byte("{}"),
},
},
},
}
for _, tt := range tests {
@@ -8,6 +8,7 @@ import (
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/host/plugin/store"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/plugin/host"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -94,12 +95,13 @@ func TestPluginSet_ImmutableFields(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
w := db.New(conn)
wrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrapper)
ts := timestamp.Timestamp{Timestamp: &timestamppb.Timestamp{Seconds: 0, Nanos: 0}}
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
plg := host.TestPlugin(t, conn, "test", "prefix")
cat := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
new := TestSet(t, conn, cat.GetPublicId())
new := TestSet(t, conn, kmsCache, cat)
tests := []struct {
name string
@@ -125,7 +127,7 @@ func TestPluginSet_ImmutableFields(t *testing.T) {
fieldMask: []string{"CreateTime"},
},
{
name: "plugin_host_catalog_id",
name: "host_plugin_catalog_id",
update: func() *HostSet {
c := new.testCloneHostSet()
c.CatalogId = "stc_01234567890"
+8 -10
View File
@@ -14,11 +14,11 @@ type Option func(*options)
// options = how options are represented
type options struct {
withName string
withDescription string
withLimit int
withAttributes map[string]interface{}
withSecrets map[string]interface{}
withName string
withDescription string
withAttributes map[string]interface{}
withSecrets map[string]interface{}
withPreferredEndpoints []string
}
func getDefaultOptions() options {
@@ -57,11 +57,9 @@ func WithSecrets(secrets map[string]interface{}) Option {
}
}
// WithLimit provides an option to provide a limit. Intentionally allowing
// negative integers. If WithLimit < 0, then unlimited results are
// returned. If WithLimit == 0, then default limits are used for results.
func WithLimit(l int) Option {
// WithPreferredEndpoints provides an optional preferred endpoints field.
func WithPreferredEndpoints(with []string) Option {
return func(o *options) {
o.withLimit = l
o.withPreferredEndpoints = with
}
}
+4 -4
View File
@@ -20,10 +20,10 @@ func Test_GetOpts(t *testing.T) {
testOpts.withDescription = "test desc"
assert.Equal(t, opts, testOpts)
})
t.Run("WithLimit", func(t *testing.T) {
opts := getOpts(WithLimit(5))
t.Run("WithPreferredEndpoints", func(t *testing.T) {
opts := getOpts(WithPreferredEndpoints([]string{"foo"}))
testOpts := getDefaultOptions()
testOpts.withLimit = 5
assert.Equal(t, opts, testOpts)
testOpts.withPreferredEndpoints = []string{"foo"}
assert.EqualValues(t, opts, testOpts)
})
}
+9 -5
View File
@@ -6,6 +6,7 @@ package plugin
import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/kms"
)
@@ -24,7 +25,7 @@ type Repository struct {
// only be used for one transaction and it is not safe for concurrent go
// routines to access it. WithLimit option is used as a repo wide default
// limit applied to all ListX methods.
func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) {
func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...host.Option) (*Repository, error) {
const op = "plugin.NewRepository"
switch {
case r == nil:
@@ -35,16 +36,19 @@ func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repo
return nil, errors.NewDeprecated(errors.InvalidParameter, op, "kms")
}
opts := getOpts(opt...)
if opts.withLimit == 0 {
opts, err := host.GetOpts(opt...)
if err != nil {
return nil, errors.WrapDeprecated(err, op)
}
if opts.WithLimit == 0 {
// zero signals the boundary defaults should be used.
opts.withLimit = db.DefaultLimit
opts.WithLimit = db.DefaultLimit
}
return &Repository{
reader: r,
writer: w,
kms: kms,
defaultLimit: opts.withLimit,
defaultLimit: opts.WithLimit,
}, nil
}
@@ -6,6 +6,7 @@ import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
hostplg "github.com/hashicorp/boundary/internal/plugin/host"
@@ -75,7 +76,9 @@ func (r *Repository) CreateCatalog(ctx context.Context, c *HostCatalog, _ ...Opt
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
hcSecret.encrypt(ctx, dbWrapper)
if err := hcSecret.encrypt(ctx, dbWrapper); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
}
var newHostCatalog *HostCatalog
@@ -146,20 +149,22 @@ func (r *Repository) LookupCatalog(ctx context.Context, id string, _ ...Option)
}
// ListCatalogs returns a slice of HostCatalogs for the scope IDs. WithLimit is the only option supported.
func (r *Repository) ListCatalogs(ctx context.Context, scopeIds []string, opt ...Option) ([]*HostCatalog, error) {
func (r *Repository) ListCatalogs(ctx context.Context, scopeIds []string, opt ...host.Option) ([]*HostCatalog, error) {
const op = "plugin.(Repository).ListCatalogs"
if len(scopeIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no scope id")
}
opts := getOpts(opt...)
opts, err := host.GetOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
limit := r.defaultLimit
if opts.withLimit != 0 {
if opts.WithLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
limit = opts.WithLimit
}
var hostCatalogs []*HostCatalog
err := r.reader.SearchWhere(ctx, &hostCatalogs, "scope_id in (?)", []interface{}{scopeIds}, db.WithLimit(limit))
if err != nil {
if err := r.reader.SearchWhere(ctx, &hostCatalogs, "scope_id in (?)", []interface{}{scopeIds}, db.WithLimit(limit)); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return hostCatalogs, nil
@@ -204,7 +204,7 @@ func TestRepository_CreateCatalog(t *testing.T) {
dbWrapper, err := kmsCache.GetWrapper(ctx, got.GetScopeId(), kms.KeyPurposeDatabase)
require.NoError(t, err)
cSecret.decrypt(ctx, dbWrapper)
require.NoError(t, cSecret.decrypt(ctx, dbWrapper))
assert.Equal(string(tt.wantSecret), string(cSecret.Secret))
})
}
+191 -26
View File
@@ -3,9 +3,14 @@ package plugin
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
hostplg "github.com/hashicorp/boundary/internal/plugin/host"
@@ -65,14 +70,51 @@ func (r *Repository) CreateSet(ctx context.Context, scopeId string, s *HostSet,
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var newHostSet *HostSet
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
newHostSet = s.clone()
err := w.Create(ctx, newHostSet, db.WithOplog(oplogWrapper, s.oplog(oplog.OpType_OP_TYPE_CREATE)))
var preferredEndpoints []interface{}
if s.PreferredEndpoints != nil {
preferredEndpoints = make([]interface{}, 0, len(s.PreferredEndpoints))
for i, e := range s.PreferredEndpoints {
obj, err := host.NewPreferredEndpoint(ctx, s.PublicId, uint32(i+1), e)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
preferredEndpoints = append(preferredEndpoints, obj)
}
}
var returnedHostSet *HostSet
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, len(preferredEndpoints)+2)
ticket, err := w.GetTicket(s)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
returnedHostSet = s.clone()
var hsOplogMsg oplog.Message
if err := w.Create(ctx, returnedHostSet, db.NewOplogMsg(&hsOplogMsg)); err != nil {
return errors.Wrap(ctx, err, op)
}
msgs = append(msgs, &hsOplogMsg)
if len(preferredEndpoints) > 0 {
peOplogMsgs := make([]*oplog.Message, 0, len(preferredEndpoints))
if err := w.CreateItems(ctx, preferredEndpoints, db.NewOplogMsgs(&peOplogMsgs)); err != nil {
return err
}
msgs = append(msgs, peOplogMsgs...)
}
metadata := s.oplog(oplog.OpType_OP_TYPE_CREATE)
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
@@ -83,49 +125,172 @@ func (r *Repository) CreateSet(ctx context.Context, scopeId string, s *HostSet,
}
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in catalog: %s", s.CatalogId)))
}
return newHostSet, nil
return returnedHostSet, nil
}
// LookupSet will look up a host set in the repository and return the host
// set. If the host set is not found, it will return nil, nil.
// All options are ignored.
func (r *Repository) LookupSet(ctx context.Context, publicId string, _ ...Option) (*HostSet, error) {
func (r *Repository) LookupSet(ctx context.Context, publicId string, opt ...host.Option) (*HostSet, error) {
const op = "plugin.(Repository).LookupSet"
if publicId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id")
}
s := allocHostSet()
s.PublicId = publicId
err := r.reader.LookupByPublicId(ctx, s)
if errors.IsNotFoundError(err) {
return nil, nil
}
sets, err := r.getSets(ctx, publicId, "", opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in %s", s.PublicId)))
return nil, errors.Wrap(ctx, err, op)
}
return s, nil
switch {
case len(sets) == 0:
return nil, nil // not an error to return no rows for a "lookup"
case len(sets) > 1:
return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("%s matched more than 1 ", publicId))
default:
return sets[0], nil
}
}
// ListSets returns a slice of HostSets for the catalogId. WithLimit is the
// only option supported.
func (r *Repository) ListSets(ctx context.Context, catalogId string, opt ...Option) ([]*HostSet, error) {
func (r *Repository) ListSets(ctx context.Context, catalogId string, opt ...host.Option) ([]*HostSet, error) {
const op = "plugin.(Repository).ListSets"
if catalogId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no catalog id")
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing catalog id")
}
opts := getOpts(opt...)
limit := r.defaultLimit
if opts.withLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
}
var sets []*HostSet
err := r.reader.SearchWhere(ctx, &sets, "catalog_id = ?", []interface{}{catalogId}, db.WithLimit(limit))
sets, err := r.getSets(ctx, "", catalogId, opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return sets, nil
}
func (r *Repository) getSets(ctx context.Context, publicId string, catalogId string, opt ...host.Option) ([]*HostSet, error) {
const op = "plugin.(Repository).getSets"
const aggregateDelimiter = "|"
const priorityDelimiter = "="
if publicId == "" && catalogId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing search criteria: both host set id and catalog id are empty")
}
if publicId != "" && catalogId != "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "searching for both a host set id and a catalog id is not supported")
}
opts, err := host.GetOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
limit := r.defaultLimit
if opts.WithLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.WithLimit
}
args := make([]interface{}, 0, 1)
var where string
switch {
case publicId != "":
where, args = "public_id = ?", append(args, publicId)
default:
where, args = "catalog_id = ?", append(args, catalogId)
}
dbArgs := []db.Option{db.WithLimit(limit)}
if opts.WithOrderByCreateTime {
if opts.Ascending {
dbArgs = append(dbArgs, db.WithOrder("create_time asc"))
} else {
dbArgs = append(dbArgs, db.WithOrder("create_time"))
}
}
var aggHostSets []*hostSetAgg
if err := r.reader.SearchWhere(ctx, &aggHostSets, where, args, dbArgs...); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in %s", publicId)))
}
if len(aggHostSets) == 0 {
return nil, nil
}
sets := make([]*HostSet, 0, len(aggHostSets))
for _, agg := range aggHostSets {
hs := allocHostSet()
hs.PublicId = agg.PublicId
hs.CatalogId = agg.CatalogId
hs.Name = agg.Name
hs.Description = agg.Description
hs.CreateTime = agg.CreateTime
hs.UpdateTime = agg.UpdateTime
hs.Version = agg.Version
hs.Attributes = agg.Attributes
if agg.PreferredEndpoints != "" {
eps := strings.Split(agg.PreferredEndpoints, aggregateDelimiter)
if len(eps) > 0 {
// We want to protect against someone messing with the DB
// and not panic, so we do a bit of a dance here
var sortErr error
sort.Slice(eps, func(i, j int) bool {
epi := strings.Split(eps[i], priorityDelimiter)
if len(epi) != 2 {
sortErr = errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("preferred endpoint %s had unexpected fields", eps[i]))
return false
}
epj := strings.Split(eps[j], priorityDelimiter)
if len(epj) != 2 {
sortErr = errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("preferred endpoint %s had unexpected fields", eps[j]))
return false
}
indexi, err := strconv.Atoi(epi[0])
if err != nil {
sortErr = errors.Wrap(ctx, err, op)
return false
}
indexj, err := strconv.Atoi(epj[0])
if err != nil {
sortErr = errors.Wrap(ctx, err, op)
return false
}
return indexi < indexj
})
if sortErr != nil {
return nil, sortErr
}
for i, ep := range eps {
// At this point they're in the correct order, but we still
// have to strip off the priority
eps[i] = strings.Split(ep, priorityDelimiter)[1]
}
hs.PreferredEndpoints = eps
}
}
sets = append(sets, hs)
}
return sets, nil
}
// hostSetAgg is a view that aggregates the host set's value objects in to
// string fields delimited with the aggregateDelimiter of "|"
type hostSetAgg struct {
PublicId string `gorm:"primary_key"`
CatalogId string
PluginId string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
Attributes []byte
PreferredEndpoints string
}
// TableName returns the table name for gorm
func (agg *hostSetAgg) TableName() string { return "host_plugin_host_set_with_value_obj" }
@@ -10,6 +10,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/host/plugin/store"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
@@ -60,8 +61,8 @@ func TestRepository_CreateSet(t *testing.T) {
name: "invalid-public-id-set",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
PublicId: "abcd_OOOOOOOOOO",
CatalogId: catalog.PublicId,
PublicId: "abcd_OOOOOOOOOO",
Attributes: attrs,
},
},
@@ -80,30 +81,47 @@ func TestRepository_CreateSet(t *testing.T) {
name: "valid-no-options",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
CatalogId: catalog.PublicId,
Attributes: attrs,
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
CatalogId: catalog.PublicId,
Attributes: attrs,
},
},
},
{
name: "valid-preferred-endpoints",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Attributes: attrs,
PreferredEndpoints: []string{"cidr:1.2.3.4/32", "dns:a.b.c"},
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Attributes: attrs,
PreferredEndpoints: []string{"cidr:1.2.3.4/32", "dns:a.b.c"},
},
},
},
{
name: "valid-with-name",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Name: "test-name-repo",
CatalogId: catalog.PublicId,
Name: "test-name-repo",
Attributes: attrs,
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Name: "test-name-repo",
CatalogId: catalog.PublicId,
Name: "test-name-repo",
Attributes: attrs,
},
},
@@ -114,14 +132,14 @@ func TestRepository_CreateSet(t *testing.T) {
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Description: ("test-description-repo"),
Attributes: attrs,
Attributes: attrs,
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Description: ("test-description-repo"),
Attributes: attrs,
Attributes: attrs,
},
},
},
@@ -163,8 +181,8 @@ func TestRepository_CreateSet(t *testing.T) {
in := &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Name: "test-name-repo",
CatalogId: catalog.PublicId,
Name: "test-name-repo",
Attributes: []byte("{}"),
},
}
@@ -195,7 +213,7 @@ func TestRepository_CreateSet(t *testing.T) {
in := &HostSet{
HostSet: &store.HostSet{
Name: "test-name-repo",
Name: "test-name-repo",
Attributes: []byte("{}"),
},
}
@@ -234,7 +252,7 @@ func TestRepository_LookupSet(t *testing.T) {
plg := hostplg.TestPlugin(t, conn, "lookup", "lookup")
catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
hostSet := TestSet(t, conn, catalog.PublicId)
hostSet := TestSet(t, conn, kms, catalog)
hostSetId, err := newHostSetId(ctx, plg.GetIdPrefix())
require.NoError(t, err)
@@ -273,7 +291,9 @@ func TestRepository_LookupSet(t *testing.T) {
return
}
require.NoError(err)
assert.EqualValues(tt.want, got)
if tt.want != nil {
assert.Empty(cmp.Diff(got, tt.want, protocmp.Transform()), "LookupSet(%q) got response %q, wanted %q", tt.in, got, tt.want)
}
})
}
}
@@ -291,15 +311,15 @@ func TestRepository_ListSets(t *testing.T) {
catalogB := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
hostSets := []*HostSet{
TestSet(t, conn, catalogA.PublicId),
TestSet(t, conn, catalogA.PublicId),
TestSet(t, conn, catalogA.PublicId),
TestSet(t, conn, kms, catalogA),
TestSet(t, conn, kms, catalogA),
TestSet(t, conn, kms, catalogA),
}
tests := []struct {
name string
in string
opts []Option
opts []host.Option
want []*HostSet
wantIsErr errors.Code
}{
@@ -310,7 +330,7 @@ func TestRepository_ListSets(t *testing.T) {
{
name: "Catalog-with-no-host-sets",
in: catalogB.PublicId,
want: []*HostSet{},
want: nil,
},
{
name: "Catalog-with-host-sets",
@@ -355,13 +375,13 @@ func TestRepository_ListSets_Limits(t *testing.T) {
count := 10
var hostSets []*HostSet
for i := 0; i < count; i++ {
hostSets = append(hostSets, TestSet(t, conn, catalog.PublicId))
hostSets = append(hostSets, TestSet(t, conn, kms, catalog))
}
tests := []struct {
name string
repoOpts []Option
listOpts []Option
repoOpts []host.Option
listOpts []host.Option
wantLen int
}{
{
@@ -370,34 +390,34 @@ func TestRepository_ListSets_Limits(t *testing.T) {
},
{
name: "With repo limit",
repoOpts: []Option{WithLimit(3)},
repoOpts: []host.Option{host.WithLimit(3)},
wantLen: 3,
},
{
name: "With negative repo limit",
repoOpts: []Option{WithLimit(-1)},
repoOpts: []host.Option{host.WithLimit(-1)},
wantLen: count,
},
{
name: "With List limit",
listOpts: []Option{WithLimit(3)},
listOpts: []host.Option{host.WithLimit(3)},
wantLen: 3,
},
{
name: "With negative List limit",
listOpts: []Option{WithLimit(-1)},
listOpts: []host.Option{host.WithLimit(-1)},
wantLen: count,
},
{
name: "With repo smaller than list limit",
repoOpts: []Option{WithLimit(2)},
listOpts: []Option{WithLimit(6)},
repoOpts: []host.Option{host.WithLimit(2)},
listOpts: []host.Option{host.WithLimit(6)},
wantLen: 6,
},
{
name: "With repo larger than list limit",
repoOpts: []Option{WithLimit(6)},
listOpts: []Option{WithLimit(2)},
repoOpts: []host.Option{host.WithLimit(6)},
listOpts: []host.Option{host.WithLimit(2)},
wantLen: 2,
},
}
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/kms"
)
@@ -21,7 +22,7 @@ func TestRepository_New(t *testing.T) {
r db.Reader
w db.Writer
kms *kms.Kms
opts []Option
opts []host.Option
}
tests := []struct {
@@ -50,7 +51,7 @@ func TestRepository_New(t *testing.T) {
r: rw,
w: rw,
kms: kmsCache,
opts: []Option{WithLimit(5)},
opts: []host.Option{host.WithLimit(5)},
},
want: &Repository{
reader: rw,
+39 -26
View File
@@ -183,9 +183,12 @@ type HostSet struct {
// version allows optimistic locking of the resource
// @inject_tag: `gorm:"default:null"`
Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"`
// attributes is a jsonb formatted field.
// attributes is a byte field containing marshaled JSON data
// @inject_tag: `gorm:"default:null"`
Attributes []byte `protobuf:"bytes,8,opt,name=attributes,proto3" json:"attributes,omitempty" gorm:"default:null"`
// preferred_endpoints stores string preference values
// @inject_tag: `gorm:"-"`
PreferredEndpoints []string `protobuf:"bytes,9,rep,name=preferred_endpoints,json=preferredEndpoints,proto3" json:"preferred_endpoints,omitempty" gorm:"-"`
}
func (x *HostSet) Reset() {
@@ -276,6 +279,13 @@ func (x *HostSet) GetAttributes() []byte {
return nil
}
func (x *HostSet) GetPreferredEndpoints() []string {
if x != nil {
return x.PreferredEndpoints
}
return nil
}
type HostCatalogSecret struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -419,7 +429,7 @@ var file_controller_storage_host_plugin_store_v1_host_proto_rawDesc = []byte{
0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x74, 0x74,
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x61,
0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0x81, 0x03, 0x0a, 0x07, 0x48, 0x6f,
0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0xb2, 0x03, 0x0a, 0x07, 0x48, 0x6f,
0x73, 0x74, 0x53, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d,
@@ -443,30 +453,33 @@ var file_controller_storage_host_plugin_store_v1_host_proto_rawDesc = []byte{
0x67, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a,
0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0x98, 0x02,
0x0a, 0x11, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x53, 0x65, 0x63,
0x72, 0x65, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67,
0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12,
0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06,
0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x65,
0x63, 0x72, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65,
0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65,
0x74, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70,
0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x73,
0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x0c, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a,
0x13, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f,
0x69, 0x6e, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x70, 0x72, 0x65, 0x66,
0x65, 0x72, 0x72, 0x65, 0x64, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0x98,
0x02, 0x0a, 0x11, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x53, 0x65,
0x63, 0x72, 0x65, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f,
0x67, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69,
0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72,
0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65,
0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c,
0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a,
0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73,
0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72,
0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x74, 0x53, 0x65, 0x63, 0x72,
0x65, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72,
0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f,
0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
+16 -15
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/plugin/host"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
@@ -39,29 +40,29 @@ func TestCatalog(t *testing.T, conn *gorm.DB, scopeId, pluginId string, opt ...O
// TestSet creates a plugin host sets in the provided DB
// with the provided catalog id. The catalog must have been created
// previously. The test will fail if any errors are encountered.
func TestSet(t *testing.T, conn *gorm.DB, catalogId string, opt ...Option) *HostSet {
func TestSet(t *testing.T, conn *gorm.DB, kmsCache *kms.Kms, hc *HostCatalog, opt ...Option) *HostSet {
t.Helper()
require := require.New(t)
ctx := context.Background()
w := db.New(conn)
rw := db.New(conn)
assert := assert.New(t)
set, err := NewHostSet(ctx, catalogId, opt...)
require.NoError(t, err)
assert.NotNil(set)
repo, err := NewRepository(rw, rw, kmsCache)
require.NoError(err)
cg := allocHostCatalog()
cg.PublicId = catalogId
require.NoError(t, w.LookupByPublicId(ctx, cg))
set, err := NewHostSet(ctx, hc.PublicId, opt...)
require.NoError(err)
require.NotNil(set)
plg := host.NewPlugin("", "")
plg.PublicId = cg.GetPluginId()
require.NoError(t, w.LookupByPublicId(ctx, plg))
plg.PublicId = hc.GetPluginId()
require.NoError(rw.LookupByPublicId(ctx, plg))
id, err := newHostSetId(ctx, plg.GetIdPrefix())
assert.NoError(err)
assert.NotEmpty(id)
set.PublicId = id
require.NoError(err)
require.NotEmpty(id)
set, err = repo.CreateSet(ctx, hc.ScopeId, set, opt...)
require.NoError(err)
require.NoError(t, w.Create(ctx, set))
return set
}
+3 -1
View File
@@ -7,6 +7,7 @@ import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/plugin/host"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -35,6 +36,7 @@ func Test_TestSet(t *testing.T) {
assert, require := assert.New(t), require.New(t)
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrapper)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
require.NotNil(prj)
assert.NotEmpty(prj.GetPublicId())
@@ -44,7 +46,7 @@ func Test_TestSet(t *testing.T) {
assert.NotEmpty(plg.GetPublicId())
c := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId())
set := TestSet(t, conn, c.GetPublicId(), WithName("foo"), WithDescription("bar"))
set := TestSet(t, conn, kmsCache, c, WithName("foo"), WithDescription("bar"))
assert.NotEmpty(set.GetPublicId())
assert.True(strings.HasPrefix(set.GetPublicId(), fmt.Sprintf("%s_%s", HostSetPrefix, "prefix")))
assert.Equal("foo", set.GetName())
+87
View File
@@ -0,0 +1,87 @@
package host
import (
"context"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host/store"
"github.com/hashicorp/boundary/internal/libs/endpoint"
"google.golang.org/protobuf/proto"
)
// defaultPreferredEndpointTableName defines the default table name for a
// PreferredEndpoint
const defaultPreferredEndpointTableName = "host_set_preferred_endpoint"
// PreferredEndpoint defines an endpoint condition for a host set.
// PreferredEndpoints are value objects of an HostSet, therefore there's no need
// for oplog metadata, since only the HostSet will have metadata because it's
// the root aggregate.
type PreferredEndpoint struct {
*store.PreferredEndpoint
tableName string
}
// NewPreferredEndpoint creates a new in memory preferred endpoint assigned to a
// HostSet. It supports no options.
func NewPreferredEndpoint(ctx context.Context, hostSetId string, priority uint32, condition string) (*PreferredEndpoint, error) {
const op = "host.NewPreferredEndpoint"
pe := &PreferredEndpoint{
PreferredEndpoint: &store.PreferredEndpoint{
HostSetId: hostSetId,
Priority: priority,
Condition: condition,
},
}
if err := pe.validate(ctx, op); err != nil {
return nil, err // intentionally not wrapped
}
return pe, nil
}
// validate the PreferredEndpoint. On success, it will return nil.
func (pe *PreferredEndpoint) validate(ctx context.Context, caller errors.Op) error {
if pe.HostSetId == "" {
return errors.New(ctx, errors.InvalidParameter, caller, "missing host set id")
}
if pe.Priority < 1 {
return errors.New(ctx, errors.InvalidParameter, caller, "invalid priority value")
}
if pe.Condition == "" {
return errors.New(ctx, errors.InvalidParameter, caller, "missing condition")
}
if _, err := endpoint.NewPreferencer(ctx, endpoint.WithPreferenceOrder([]string{pe.Condition})); err != nil {
return errors.Wrap(ctx, err, caller)
}
return nil
}
// AllocPreferredEndpoint makes an empty one in-memory
func AllocPreferredEndpoint() PreferredEndpoint {
return PreferredEndpoint{
PreferredEndpoint: &store.PreferredEndpoint{},
}
}
// Clone a PreferredEndpoint
func (c *PreferredEndpoint) Clone() *PreferredEndpoint {
cp := proto.Clone(c.PreferredEndpoint)
return &PreferredEndpoint{
PreferredEndpoint: cp.(*store.PreferredEndpoint),
}
}
// TableName returns the table name.
func (c *PreferredEndpoint) TableName() string {
if c.tableName != "" {
return c.tableName
}
return defaultPreferredEndpointTableName
}
// SetTableName sets the table name.
func (c *PreferredEndpoint) SetTableName(n string) {
c.tableName = n
}
+219
View File
@@ -0,0 +1,219 @@
package host_test
import (
"context"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/host/plugin"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
hostplg "github.com/hashicorp/boundary/internal/plugin/host"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPreferredEndpoint_Create(t *testing.T) {
t.Parallel()
ctx := context.TODO()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
rw := db.New(conn)
kmsCache := kms.TestKms(t, conn, wrapper)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
plg := hostplg.TestPlugin(t, conn, "create", "create")
catalog := plugin.TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
set := plugin.TestSet(t, conn, kmsCache, catalog)
type args struct {
hostSetId string
priority uint32
condition string
}
tests := []struct {
name string
args args
want *host.PreferredEndpoint
wantErr bool
wantIsErr errors.Code
create bool
wantCreateErr bool
wantCreateIsErr errors.Code
}{
{
name: "valid",
args: args{
hostSetId: set.PublicId,
priority: 1,
condition: "cidr:1.2.3.4",
},
create: true,
want: func() *host.PreferredEndpoint {
want := host.AllocPreferredEndpoint()
want.HostSetId = set.PublicId
want.Condition = "cidr:1.2.3.4"
want.Priority = 1
return &want
}(),
},
{
name: "dup", // must follow "valid" test. Url must be be unique for an OidcMethodId
args: args{
hostSetId: set.PublicId,
priority: 1,
condition: "cidr:1.2.3.4",
},
create: true,
want: func() *host.PreferredEndpoint {
want := host.AllocPreferredEndpoint()
want.HostSetId = set.PublicId
want.Condition = "cidr:1.2.3.4"
want.Priority = 1
return &want
}(),
wantCreateErr: true,
wantCreateIsErr: errors.NotUnique,
},
{
name: "bad-condition",
args: args{
hostSetId: set.PublicId,
priority: 1,
condition: "foobar:1.2.3.4",
},
wantErr: true,
wantIsErr: errors.InvalidParameter,
},
{
name: "empty-host-set",
args: args{
hostSetId: "",
},
wantErr: true,
wantIsErr: errors.InvalidParameter,
},
{
name: "invalid-priority",
args: args{
hostSetId: set.PublicId,
priority: 0,
},
wantErr: true,
wantIsErr: errors.InvalidParameter,
},
{
name: "empty-condition",
args: args{
hostSetId: set.PublicId,
priority: 1,
},
wantErr: true,
wantIsErr: errors.InvalidParameter,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := host.NewPreferredEndpoint(ctx, tt.args.hostSetId, tt.args.priority, tt.args.condition)
if tt.wantErr {
require.Error(err)
assert.True(errors.Match(errors.T(tt.wantIsErr), err))
return
}
require.NoError(err)
assert.Equal(tt.want, got)
if tt.create {
ctx := context.Background()
err = rw.Create(ctx, got)
if tt.wantCreateErr {
assert.Error(err)
assert.True(errors.Match(errors.T(tt.wantCreateIsErr), err))
return
} else {
assert.NoError(err)
}
found := host.AllocPreferredEndpoint()
require.NoError(rw.LookupWhere(ctx, &found, "host_set_id = ? and priority = ?", tt.args.hostSetId, tt.args.priority))
assert.Equal(got, &found)
}
})
}
}
func TestPreferredEndpoint_Delete(t *testing.T) {
t.Parallel()
ctx := context.TODO()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
rw := db.New(conn)
kmsCache := kms.TestKms(t, conn, wrapper)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
plg := hostplg.TestPlugin(t, conn, "create", "create")
catalog := plugin.TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
set := plugin.TestSet(t, conn, kmsCache, catalog)
peFunc := func(priority uint32, condition string) *host.PreferredEndpoint {
ep, err := host.NewPreferredEndpoint(ctx, set.PublicId, priority, condition)
require.NoError(t, err)
return ep
}
tests := []struct {
pes []*host.PreferredEndpoint
name string
overrides func(*host.PreferredEndpoint)
wantErr bool
wantRowsDeleted int
}{
{
name: "valid-single",
pes: []*host.PreferredEndpoint{peFunc(1, "cidr:1.2.3.4")},
wantErr: false,
wantRowsDeleted: 1,
},
{
name: "bad-host-set-id",
pes: []*host.PreferredEndpoint{peFunc(1, "cidr:1.2.3.4")},
overrides: func(pe *host.PreferredEndpoint) { pe.HostSetId = "bad-id" },
wantErr: false,
wantRowsDeleted: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
ctx := context.Background()
var eput *host.PreferredEndpoint
for _, pe := range tt.pes {
cloned := pe.Clone()
if eput == nil {
eput = cloned
}
require.NoError(rw.Create(ctx, &cloned))
}
if tt.overrides != nil {
tt.overrides(eput)
}
deletedRows, err := rw.Delete(ctx, &eput, db.WithWhere("host_set_id = ?", eput.HostSetId))
if tt.wantErr {
require.Error(err)
return
}
require.NoError(err)
if tt.wantRowsDeleted == 0 {
assert.Equal(tt.wantRowsDeleted, deletedRows)
return
}
require.Equal(tt.wantRowsDeleted, deletedRows)
found := host.AllocPreferredEndpoint()
err = rw.LookupWhere(ctx, &found, "host_set_id = ?", set.PublicId)
assert.True(errors.IsNotFoundError(err))
})
}
}
+99 -10
View File
@@ -199,6 +199,75 @@ func (x *Set) GetCatalogId() string {
return ""
}
type PreferredEndpoint struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The ID of the host set this belongs to.
// @inject_tag: `gorm:"primary_key"`
HostSetId string `protobuf:"bytes,1,opt,name=host_set_id,json=hostSetId,proto3" json:"host_set_id,omitempty" gorm:"primary_key"`
// The priority order, zero-indexed
// @inject_tag: `gorm:"primary_key"`
Priority uint32 `protobuf:"varint,2,opt,name=priority,proto3" json:"priority,omitempty" gorm:"primary_key"`
// The string text of the preference condition
// @inject_tag: `gorm:"not_null"`
Condition string `protobuf:"bytes,3,opt,name=condition,proto3" json:"condition,omitempty" gorm:"not_null"`
}
func (x *PreferredEndpoint) Reset() {
*x = PreferredEndpoint{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_storage_host_store_v1_host_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PreferredEndpoint) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PreferredEndpoint) ProtoMessage() {}
func (x *PreferredEndpoint) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_host_store_v1_host_proto_msgTypes[3]
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 PreferredEndpoint.ProtoReflect.Descriptor instead.
func (*PreferredEndpoint) Descriptor() ([]byte, []int) {
return file_controller_storage_host_store_v1_host_proto_rawDescGZIP(), []int{3}
}
func (x *PreferredEndpoint) GetHostSetId() string {
if x != nil {
return x.HostSetId
}
return ""
}
func (x *PreferredEndpoint) GetPriority() uint32 {
if x != nil {
return x.Priority
}
return 0
}
func (x *PreferredEndpoint) GetCondition() string {
if x != nil {
return x.Condition
}
return ""
}
var File_controller_storage_host_store_v1_host_proto protoreflect.FileDescriptor
var file_controller_storage_host_store_v1_host_proto_rawDesc = []byte{
@@ -219,11 +288,18 @@ var file_controller_storage_host_store_v1_host_proto_rawDesc = []byte{
0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61,
0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72,
0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73,
0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x22, 0x6d, 0x0a, 0x11, 0x50, 0x72, 0x65,
0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1e,
0x0a, 0x0b, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x74, 0x49, 0x64, 0x12, 0x1a,
0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f,
0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63,
0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70,
0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74,
0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -238,11 +314,12 @@ func file_controller_storage_host_store_v1_host_proto_rawDescGZIP() []byte {
return file_controller_storage_host_store_v1_host_proto_rawDescData
}
var file_controller_storage_host_store_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_controller_storage_host_store_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_controller_storage_host_store_v1_host_proto_goTypes = []interface{}{
(*Catalog)(nil), // 0: controller.storage.host.store.v1.Catalog
(*Host)(nil), // 1: controller.storage.host.store.v1.Host
(*Set)(nil), // 2: controller.storage.host.store.v1.Set
(*Catalog)(nil), // 0: controller.storage.host.store.v1.Catalog
(*Host)(nil), // 1: controller.storage.host.store.v1.Host
(*Set)(nil), // 2: controller.storage.host.store.v1.Set
(*PreferredEndpoint)(nil), // 3: controller.storage.host.store.v1.PreferredEndpoint
}
var file_controller_storage_host_store_v1_host_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
@@ -294,6 +371,18 @@ func file_controller_storage_host_store_v1_host_proto_init() {
return nil
}
}
file_controller_storage_host_store_v1_host_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PreferredEndpoint); 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{
@@ -301,7 +390,7 @@ func file_controller_storage_host_store_v1_host_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_storage_host_store_v1_host_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
+1 -1
View File
@@ -21,7 +21,7 @@ func NewPreferencer(ctx context.Context, opt ...Option) (*preferencer, error) {
const op = "endpoint.NewPreferencer"
opts, err := getOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
}
pref := &preferencer{
matchers: opts.withMatchers,
@@ -43,6 +43,10 @@ message HostSet {
// Output only. A list of Hosts in this Host Set.
repeated string host_ids = 100 [json_name="host_ids"];
// An ordered list of endpoint preferences used to choose from among
// multiple possible endpoints for a host.
repeated string preferred_endpoints = 101 [json_name="preferred_endpoints", (custom_options.v1.generate_sdk_option) = true];
// The attributes that are applicable for the specific Host Set type.
google.protobuf.Struct attributes = 110;
@@ -78,9 +78,13 @@ message HostSet {
// @inject_tag: `gorm:"default:null"`
uint32 version = 7;
// attributes is a jsonb formatted field.
// attributes is a byte field containing marshaled JSON data
// @inject_tag: `gorm:"default:null"`
bytes attributes = 8;
// preferred_endpoints stores string preference values
// @inject_tag: `gorm:"-"`
repeated string preferred_endpoints = 9;
}
message HostCatalogSecret {
@@ -109,4 +113,4 @@ message HostCatalogSecret {
// It must be set.
// @inject_tag: `gorm:"not_null"`
string key_id = 6;
}
}
@@ -33,3 +33,17 @@ message Set {
// @inject_tag: `gorm:"not_null"`
string catalog_id = 2;
}
message PreferredEndpoint {
// The ID of the host set this belongs to.
// @inject_tag: `gorm:"primary_key"`
string host_set_id = 1;
// The priority order, zero-indexed
// @inject_tag: `gorm:"primary_key"`
uint32 priority = 2;
// The string text of the preference condition
// @inject_tag: `gorm:"not_null"`
string condition = 3;
}
+3 -3
View File
@@ -12,7 +12,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/credential/vault"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/host/plugin"
hostplugin "github.com/hashicorp/boundary/internal/host/plugin"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
@@ -135,8 +135,8 @@ func New(ctx context.Context, conf *Config) (*Controller, error) {
c.StaticHostRepoFn = func() (*static.Repository, error) {
return static.NewRepository(dbase, dbase, c.kms)
}
c.PluginHostRepoFn = func() (*plugin.Repository, error) {
return plugin.NewRepository(dbase, dbase, c.kms)
c.PluginHostRepoFn = func() (*hostplugin.Repository, error) {
return hostplugin.NewRepository(dbase, dbase, c.kms)
}
c.HostPluginRepoFn = func() (*host.Repository, error) {
return host.NewRepository(dbase, dbase, c.kms)
@@ -67,7 +67,7 @@ type Service struct {
staticRepoFn common.StaticRepoFactory
pluginHostRepoFn common.PluginHostRepoFactory
pluginRepoFn common.HostPluginRepoFactory
pluginRepoFn common.HostPluginRepoFactory
iamRepoFn common.IamRepoFactory
}
@@ -272,7 +272,7 @@ func TestList(t *testing.T) {
var testPluginCatalogs []*pb.HostCatalog
name := "test"
plg := host.TestPlugin(t, conn, name, name)
for i := 0 ; i < 3; i++ {
for i := 0; i < 3; i++ {
hc := plugin.TestCatalog(t, conn, pWithCatalogs.GetPublicId(), plg.GetPublicId())
cat := &pb.HostCatalog{
Id: hc.GetPublicId(),
@@ -306,7 +306,7 @@ func TestList(t *testing.T) {
name = "different"
plg = host.TestPlugin(t, conn, name, name)
for i := 0 ; i < 3; i++ {
for i := 0; i < 3; i++ {
hc := plugin.TestCatalog(t, conn, pWithOtherCatalogs.GetPublicId(), plg.GetPublicId())
wantOtherCatalogs = append(wantOtherCatalogs, &pb.HostCatalog{
Id: hc.GetPublicId(),
@@ -13,6 +13,7 @@ import (
"github.com/hashicorp/boundary/internal/host/plugin"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/host/static/store"
"github.com/hashicorp/boundary/internal/libs/endpoint"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/servers/controller/auth"
@@ -80,6 +81,10 @@ func NewService(staticRepoFn common.StaticRepoFactory, pluginRepoFn common.Plugi
}
func (s Service) ListHostSets(ctx context.Context, req *pbs.ListHostSetsRequest) (*pbs.ListHostSetsResponse, error) {
return s.ListHostSetsWithOptions(ctx, req)
}
func (s Service) ListHostSetsWithOptions(ctx context.Context, req *pbs.ListHostSetsRequest, opt ...host.Option) (*pbs.ListHostSetsResponse, error) {
if err := validateListRequest(req); err != nil {
return nil, err
}
@@ -87,7 +92,7 @@ func (s Service) ListHostSets(ctx context.Context, req *pbs.ListHostSetsRequest)
if authResults.Error != nil {
return nil, authResults.Error
}
hl, err := s.listFromRepo(ctx, req.GetHostCatalogId())
hl, err := s.listFromRepo(ctx, req.GetHostCatalogId(), opt...)
if err != nil {
return nil, err
}
@@ -177,7 +182,7 @@ func (s Service) GetHostSet(ctx context.Context, req *pbs.GetHostSetRequest) (*p
func (s Service) CreateHostSet(ctx context.Context, req *pbs.CreateHostSetRequest) (*pbs.CreateHostSetResponse, error) {
const op = "host_sets.(Service).CreateHostSet"
if err := validateCreateRequest(req); err != nil {
if err := validateCreateRequest(ctx, req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetHostCatalogId(), action.Create)
@@ -218,7 +223,7 @@ func (s Service) CreateHostSet(ctx context.Context, req *pbs.CreateHostSetReques
func (s Service) UpdateHostSet(ctx context.Context, req *pbs.UpdateHostSetRequest) (*pbs.UpdateHostSetResponse, error) {
const op = "host_sets.(Service).UpdateHostSet"
if err := validateUpdateRequest(req); err != nil {
if err := validateUpdateRequest(ctx, req); err != nil {
return nil, err
}
cat, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update)
@@ -444,6 +449,9 @@ func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, it
hSet = out
case plugin.Subtype:
h, err := toStoragePluginSet(ctx, catalogId, item)
if err != nil {
return nil, err
}
repo, err := s.pluginRepoFn()
if err != nil {
return nil, err
@@ -505,7 +513,7 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool,
return rows > 0, nil
}
func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]host.Set, error) {
func (s Service) listFromRepo(ctx context.Context, catalogId string, opt ...host.Option) ([]host.Set, error) {
const op = "host_sets.(Service).listFromRepo"
var sets []host.Set
switch host.SubtypeFromId(catalogId) {
@@ -526,7 +534,7 @@ func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]host.Set
if err != nil {
return nil, err
}
sl, err := repo.ListSets(ctx, catalogId)
sl, err := repo.ListSets(ctx, catalogId, opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
@@ -742,9 +750,12 @@ func toProto(ctx context.Context, in host.Set, hosts []host.Host, opt ...handler
}
}
if outputFields.Has(globals.AttributesField) {
switch h := in.(type) {
case *plugin.HostSet:
switch h := in.(type) {
case *plugin.HostSet:
if outputFields.Has(globals.PreferredEndpointsField) {
out.PreferredEndpoints = h.GetPreferredEndpoints()
}
if outputFields.Has(globals.AttributesField) {
attrs := map[string]interface{}{}
err := json.Unmarshal(h.Attributes, &attrs)
if err != nil {
@@ -790,6 +801,9 @@ func toStoragePluginSet(ctx context.Context, catalogId string, item *pb.HostSet)
if item.GetAttributes() != nil {
opts = append(opts, plugin.WithAttributes(item.GetAttributes().AsMap()))
}
if item.GetPreferredEndpoints() != nil {
opts = append(opts, plugin.WithPreferredEndpoints(item.GetPreferredEndpoints()))
}
hs, err := plugin.NewHostSet(ctx, catalogId, opts...)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for creation"))
@@ -814,7 +828,7 @@ func validateGetRequest(req *pbs.GetHostSetRequest) error {
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix, pluginPrefix)
}
func validateCreateRequest(req *pbs.CreateHostSetRequest) error {
func validateCreateRequest(ctx context.Context, req *pbs.CreateHostSetRequest) error {
return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string {
badFields := map[string]string{}
pluginPrefix := plugin.HostCatalogPrefix
@@ -826,6 +840,12 @@ func validateCreateRequest(req *pbs.CreateHostSetRequest) error {
if !handlers.ValidId(handlers.Id(req.GetItem().GetHostCatalogId()), static.HostCatalogPrefix, pluginPrefix) {
badFields["host_catalog_id"] = "The field is incorrectly formatted."
}
if len(req.GetItem().GetPreferredEndpoints()) > 0 {
_, err := endpoint.NewPreferencer(ctx, endpoint.WithPreferenceOrder(req.GetItem().GetPreferredEndpoints()))
if err != nil {
badFields[globals.PreferredEndpointsField] = fmt.Errorf("Error parsing preferred endpoints: %w.", err).Error()
}
}
switch host.SubtypeFromId(req.GetItem().GetHostCatalogId()) {
case static.Subtype:
if req.GetItem().GetType() != "" && req.GetItem().GetType() != static.Subtype.String() {
@@ -850,17 +870,23 @@ func validateCreateRequest(req *pbs.CreateHostSetRequest) error {
})
}
func validateUpdateRequest(req *pbs.UpdateHostSetRequest) error {
func validateUpdateRequest(ctx context.Context, req *pbs.UpdateHostSetRequest) error {
return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string {
badFields := map[string]string{}
if len(req.GetItem().GetPreferredEndpoints()) > 0 {
_, err := endpoint.NewPreferencer(ctx, endpoint.WithPreferenceOrder(req.GetItem().GetPreferredEndpoints()))
if err != nil {
badFields[globals.PreferredEndpointsField] = fmt.Errorf("Error parsing preferred endpoints: %w.", err).Error()
}
}
switch host.SubtypeFromId(req.GetId()) {
case static.Subtype:
if req.GetItem().GetType() != "" && req.GetItem().GetType() != static.Subtype.String() {
badFields["type"] = "Cannot modify the resource type."
badFields[globals.TypeField] = "Cannot modify the resource type."
}
}
return badFields
}, static.HostSetPrefix)
}, static.HostSetPrefix, plugin.HostSetPrefix)
}
func validateDeleteRequest(req *pbs.DeleteHostSetRequest) error {
@@ -11,11 +11,12 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/db"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/host"
"github.com/hashicorp/boundary/internal/host/plugin"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/plugin/host"
hostplugin "github.com/hashicorp/boundary/internal/plugin/host"
"github.com/hashicorp/boundary/internal/servers/controller/auth"
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
"github.com/hashicorp/boundary/internal/servers/controller/handlers/host_sets"
@@ -150,20 +151,22 @@ func TestGet_Plugin(t *testing.T) {
}
name := "test"
plg := host.TestPlugin(t, conn, name, name)
prefEndpoints := []string{"cidr:1.2.3.4", "cidr:2.3.4.5/24"}
plg := hostplugin.TestPlugin(t, conn, name, name)
hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
hs := plugin.TestSet(t, conn, hc.GetPublicId())
hs := plugin.TestSet(t, conn, kms, hc, plugin.WithPreferredEndpoints(prefEndpoints))
toMerge := &pbs.GetHostSetRequest{}
pHost := &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Id: hs.GetPublicId(),
CreatedTime: hs.CreateTime.GetTimestamp(),
UpdatedTime: hs.UpdateTime.GetTimestamp(),
Type: name,
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
AuthorizedActions: testAuthorizedActions,
HostCatalogId: hc.GetPublicId(),
Id: hs.GetPublicId(),
CreatedTime: hs.CreateTime.GetTimestamp(),
UpdatedTime: hs.UpdateTime.GetTimestamp(),
Type: name,
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
PreferredEndpoints: prefEndpoints,
AuthorizedActions: testAuthorizedActions,
}
cases := []struct {
@@ -179,7 +182,7 @@ func TestGet_Plugin(t *testing.T) {
},
{
name: "Get a non existing Host Set",
req: &pbs.GetHostSetRequest{Id: static.HostSetPrefix + "_DoesntExis"},
req: &pbs.GetHostSetRequest{Id: plugin.HostSetPrefix + "_DoesntExis"},
res: nil,
err: handlers.ApiErrorWithCode(codes.NotFound),
},
@@ -191,7 +194,7 @@ func TestGet_Plugin(t *testing.T) {
},
{
name: "space in id",
req: &pbs.GetHostSetRequest{Id: static.HostPrefix + "_1 23456789"},
req: &pbs.GetHostSetRequest{Id: plugin.HostSetPrefix + "_1 23456789"},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
@@ -339,22 +342,24 @@ func TestList_Plugin(t *testing.T) {
return plugin.NewRepository(rw, rw, kms)
}
name := "test"
plg := host.TestPlugin(t, conn, name, name)
plg := hostplugin.TestPlugin(t, conn, name, name)
hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
hcNoHosts := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
preferredEndpoints := []string{"cidr:1.2.3.4", "dns:*.foobar.com"}
var wantHs []*pb.HostSet
for i := 0; i < 10; i++ {
h := plugin.TestSet(t, conn, hc.GetPublicId())
h := plugin.TestSet(t, conn, kms, hc, plugin.WithPreferredEndpoints(preferredEndpoints))
wantHs = append(wantHs, &pb.HostSet{
Id: h.GetPublicId(),
HostCatalogId: h.GetCatalogId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
CreatedTime: h.GetCreateTime().GetTimestamp(),
UpdatedTime: h.GetUpdateTime().GetTimestamp(),
Version: h.GetVersion(),
Type: name,
AuthorizedActions: testAuthorizedActions,
Id: h.GetPublicId(),
HostCatalogId: h.GetCatalogId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
CreatedTime: h.GetCreateTime().GetTimestamp(),
UpdatedTime: h.GetUpdateTime().GetTimestamp(),
Version: h.GetVersion(),
Type: name,
AuthorizedActions: testAuthorizedActions,
PreferredEndpoints: preferredEndpoints,
})
}
@@ -397,7 +402,7 @@ func TestList_Plugin(t *testing.T) {
require.NoError(err, "Couldn't create new host set service.")
// Test with non-anon user
got, gErr := s.ListHostSets(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req)
got, gErr := s.ListHostSetsWithOptions(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req, host.WithOrderByCreateTime(false))
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListHostSets(%q) got error %v, wanted %v", tc.req, gErr, tc.err)
@@ -699,7 +704,7 @@ func TestCreate_Plugin(t *testing.T) {
return plugin.NewRepository(rw, rw, kms)
}
name := "test"
plg := host.TestPlugin(t, conn, name, name)
plg := hostplugin.TestPlugin(t, conn, name, name)
hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
testAttrs, err := structpb.NewStruct(map[string]interface{}{
@@ -724,6 +729,8 @@ func TestCreate_Plugin(t *testing.T) {
})
require.NoError(t, err)
prefEndpoints := []string{"cidr:1.2.3.4", "cidr:2.3.4.5/24"}
defaultHcCreated := hc.GetCreateTime().GetTimestamp().AsTime()
cases := []struct {
@@ -774,6 +781,39 @@ func TestCreate_Plugin(t *testing.T) {
},
},
},
{
name: "With Preferred Endpoints",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: name,
PreferredEndpoints: prefEndpoints,
}},
res: &pbs.CreateHostSetResponse{
Uri: fmt.Sprintf("host-sets/%s_%s_", plugin.HostSetPrefix, name),
Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: name,
PreferredEndpoints: prefEndpoints,
AuthorizedActions: testAuthorizedActions,
},
},
},
{
name: "Bad Preferred Endpoints",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: name,
PreferredEndpoints: append(prefEndpoints, "foobar:1.2.3.4"),
}},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Create with mismatched type/name",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
@@ -1731,7 +1731,7 @@ func validateSetSetsRequest(req *pbs.SetTargetHostSetsRequest) error {
badFields[globals.VersionField] = "Required field."
}
for _, id := range req.GetHostSetIds() {
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
!strings.HasPrefix(id, fmt.Sprintf("%s_", plugin.HostSetPrefix)) {
badFields[globals.HostSetIdsField] = fmt.Sprintf("Incorrectly formatted host set identifier %q.", id)
break
@@ -1755,7 +1755,7 @@ func validateRemoveSetsRequest(req *pbs.RemoveTargetHostSetsRequest) error {
badFields[globals.HostSetIdsField] = "Must be non-empty."
}
for _, id := range req.GetHostSetIds() {
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
!strings.HasPrefix(id, fmt.Sprintf("%s_", plugin.HostSetPrefix)) {
badFields[globals.HostSetIdsField] = fmt.Sprintf("Incorrectly formatted host set identifier %q.", id)
break
@@ -1779,7 +1779,7 @@ func validateAddHostSourcesRequest(req *pbs.AddTargetHostSourcesRequest) error {
badFields[globals.HostSourceIdsField] = "Must be non-empty."
}
for _, id := range req.GetHostSourceIds() {
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
!strings.HasPrefix(id, fmt.Sprintf("%s_", plugin.HostSetPrefix)) {
badFields[globals.HostSourceIdsField] = fmt.Sprintf("Incorrectly formatted host source identifier %q.", id)
break
@@ -1800,7 +1800,7 @@ func validateSetHostSourcesRequest(req *pbs.SetTargetHostSourcesRequest) error {
badFields[globals.VersionField] = "Required field."
}
for _, id := range req.GetHostSourceIds() {
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
!strings.HasPrefix(id, fmt.Sprintf("%s_", plugin.HostSetPrefix)) {
badFields[globals.HostSourceIdsField] = fmt.Sprintf("Incorrectly formatted host source identifier %q.", id)
break
@@ -1824,7 +1824,7 @@ func validateRemoveHostSourcesRequest(req *pbs.RemoveTargetHostSourcesRequest) e
badFields[globals.HostSourceIdsField] = "Must be non-empty."
}
for _, id := range req.GetHostSourceIds() {
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) &&
!strings.HasPrefix(id, fmt.Sprintf("%s_", plugin.HostSetPrefix)) {
badFields[globals.HostSourceIdsField] = fmt.Sprintf("Incorrectly formatted host source identifier %q.", id)
break
@@ -983,7 +983,7 @@ func TestAddTargetHostSets(t *testing.T) {
plg := host.TestPlugin(t, conn, "test", "test")
pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
pluginHs := plugin.TestSet(t, conn, pluginHc.GetPublicId())
pluginHs := plugin.TestSet(t, conn, kms, pluginHc)
addCases := []struct {
name string
@@ -1115,7 +1115,7 @@ func TestSetTargetHostSets(t *testing.T) {
plg := host.TestPlugin(t, conn, "test", "test")
pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
pluginHs := plugin.TestSet(t, conn, pluginHc.GetPublicId())
pluginHs := plugin.TestSet(t, conn, kms, pluginHc)
setCases := []struct {
name string
@@ -1241,7 +1241,7 @@ func TestRemoveTargetHostSets(t *testing.T) {
plg := host.TestPlugin(t, conn, "test", "test")
pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
pluginHs := plugin.TestSet(t, conn, pluginHc.GetPublicId())
pluginHs := plugin.TestSet(t, conn, kms, pluginHc)
removeCases := []struct {
name string
@@ -1379,7 +1379,7 @@ func TestAddTargetHostSources(t *testing.T) {
plg := host.TestPlugin(t, conn, "test", "test")
pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
pluginHs := plugin.TestSet(t, conn, pluginHc.GetPublicId())
pluginHs := plugin.TestSet(t, conn, kms, pluginHc)
addCases := []struct {
name string
@@ -1511,7 +1511,7 @@ func TestSetTargetHostSources(t *testing.T) {
plg := host.TestPlugin(t, conn, "test", "test")
pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
pluginHs := plugin.TestSet(t, conn, pluginHc.GetPublicId())
pluginHs := plugin.TestSet(t, conn, kms, pluginHc)
setCases := []struct {
name string
@@ -1631,7 +1631,7 @@ func TestRemoveTargetHostSources(t *testing.T) {
plg := host.TestPlugin(t, conn, "test", "test")
pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
pluginHs := plugin.TestSet(t, conn, pluginHc.GetPublicId())
pluginHs := plugin.TestSet(t, conn, kms, pluginHc)
removeCases := []struct {
name string
@@ -52,6 +52,9 @@ type HostSet struct {
Type string `protobuf:"bytes,90,opt,name=type,proto3" json:"type,omitempty"`
// Output only. A list of Hosts in this Host Set.
HostIds []string `protobuf:"bytes,100,rep,name=host_ids,proto3" json:"host_ids,omitempty"`
// An ordered list of endpoint preferences used to choose from among
// multiple possible endpoints for a host.
PreferredEndpoints []string `protobuf:"bytes,101,rep,name=preferred_endpoints,proto3" json:"preferred_endpoints,omitempty"`
// The attributes that are applicable for the specific Host Set type.
Attributes *structpb.Struct `protobuf:"bytes,110,opt,name=attributes,proto3" json:"attributes,omitempty"`
// Output only. The available actions on this resource for this user.
@@ -160,6 +163,13 @@ func (x *HostSet) GetHostIds() []string {
return nil
}
func (x *HostSet) GetPreferredEndpoints() []string {
if x != nil {
return x.PreferredEndpoints
}
return nil
}
func (x *HostSet) GetAttributes() *structpb.Struct {
if x != nil {
return x.Attributes
@@ -194,7 +204,7 @@ var file_controller_api_resources_hostsets_v1_host_set_proto_rawDesc = []byte{
0x63, 0x6f, 0x70, 0x65, 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, 0xe8, 0x04, 0x0a, 0x07, 0x48, 0x6f, 0x73, 0x74, 0x53,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa0, 0x05, 0x0a, 0x07, 0x48, 0x6f, 0x73, 0x74, 0x53,
0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
0x69, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x61, 0x74, 0x61, 0x6c,
0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x6f, 0x73,
@@ -226,19 +236,23 @@ var file_controller_api_resources_hostsets_v1_host_set_proto_rawDesc = []byte{
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68,
0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x64, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x68,
0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x37, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69,
0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74,
0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73,
0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61,
0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x73, 0x42, 0x52, 0x5a, 0x50, 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, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72,
0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x73, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x65, 0x74, 0x73, 0x3b, 0x68, 0x6f, 0x73,
0x74, 0x73, 0x65, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x6f, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x36, 0x0a, 0x13, 0x70, 0x72, 0x65, 0x66, 0x65,
0x72, 0x72, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x65,
0x20, 0x03, 0x28, 0x09, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x13, 0x70, 0x72, 0x65, 0x66,
0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12,
0x37, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x6e, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x61, 0x74,
0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68,
0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac,
0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65,
0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x52, 0x5a, 0x50, 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, 0x73, 0x64, 0x6b, 0x2f, 0x70,
0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x68, 0x6f, 0x73, 0x74,
0x73, 0x65, 0x74, 0x73, 0x3b, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x65, 0x74, 0x73, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (