Skip to content

Commit

Permalink
go/common/node: Add custom text (un)marshaler for RolesMask type
Browse files Browse the repository at this point in the history
This will result in easy to understand "roles" fields in various CLI
commands that output JSON.
  • Loading branch information
tjanez committed Sep 3, 2021
1 parent a120f14 commit 3731894
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 7 deletions.
4 changes: 4 additions & 0 deletions .changelog/4243.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
go/common/node: Add custom text (un)marshaler for `RolesMask` type

This will result in easy to understand `"roles"` fields in various CLI
commands that output JSON.
84 changes: 77 additions & 7 deletions go/common/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import (
)

var (
// ErrInvalidRole is the error returned when a node role is invalid.
ErrInvalidRole = errors.New("node: invalid role")
// ErrDuplicateRole is the error returned when a node role is duplicated.
ErrDuplicateRole = errors.New("node: duplicate role")

// ErrInvalidTEEHardware is the error returned when a TEE hardware
// implementation is invalid.
ErrInvalidTEEHardware = errors.New("node: invalid TEE implementation")
Expand Down Expand Up @@ -109,6 +114,16 @@ const (
// RoleReserved are all the bits of the Oasis node roles bitmask
// that are reserved and must not be used.
RoleReserved RolesMask = ((1 << 32) - 1) & ^((RoleStorageRPC << 1) - 1)

// Human friendly role names.
RoleComputeWorkerName = "compute"
RoleStorageWorkerName = "storage"
RoleKeyManagerName = "key-manager"
RoleValidatorName = "validator"
RoleConsensusRPCName = "consensus-rpc"
RoleStorageRPCName = "storage-rpc"

rolesMaskStringSep = ","
)

// Roles returns a list of available valid roles.
Expand Down Expand Up @@ -136,25 +151,80 @@ func (m RolesMask) String() string {

var ret []string
if m&RoleComputeWorker != 0 {
ret = append(ret, "compute")
ret = append(ret, RoleComputeWorkerName)
}
if m&RoleStorageWorker != 0 {
ret = append(ret, "storage")
ret = append(ret, RoleStorageWorkerName)
}
if m&RoleKeyManager != 0 {
ret = append(ret, "key-manager")
ret = append(ret, RoleKeyManagerName)
}
if m&RoleValidator != 0 {
ret = append(ret, "validator")
ret = append(ret, RoleValidatorName)
}
if m&RoleConsensusRPC != 0 {
ret = append(ret, "consensus-rpc")
ret = append(ret, RoleConsensusRPCName)
}
if m&RoleStorageRPC != 0 {
ret = append(ret, "storage-rpc")
ret = append(ret, RoleStorageRPCName)
}

return strings.Join(ret, rolesMaskStringSep)
}

// MarshalText encodes a RolesMask into text form.
func (m RolesMask) MarshalText() ([]byte, error) {
return []byte(m.String()), nil
}

func checkDuplicateRole(newRole RolesMask, curRoles RolesMask) error {
if curRoles&newRole != 0 {
return fmt.Errorf("%w: '%s'", ErrDuplicateRole, newRole)
}
return nil
}

return strings.Join(ret, ",")
// UnmarshalText decodes a text slice into a RolesMask.
func (m *RolesMask) UnmarshalText(text []byte) error {
*m = 0
roles := strings.Split(string(text), rolesMaskStringSep)
for _, role := range roles {
switch role {
case RoleComputeWorkerName:
if err := checkDuplicateRole(RoleComputeWorker, *m); err != nil {
return err
}
*m |= RoleComputeWorker
case RoleStorageWorkerName:
if err := checkDuplicateRole(RoleStorageWorker, *m); err != nil {
return err
}
*m |= RoleStorageWorker
case RoleKeyManagerName:
if err := checkDuplicateRole(RoleKeyManager, *m); err != nil {
return err
}
*m |= RoleKeyManager
case RoleValidatorName:
if err := checkDuplicateRole(RoleValidator, *m); err != nil {
return err
}
*m |= RoleValidator
case RoleConsensusRPCName:
if err := checkDuplicateRole(RoleConsensusRPC, *m); err != nil {
return err
}
*m |= RoleConsensusRPC
case RoleStorageRPCName:
if err := checkDuplicateRole(RoleStorageRPC, *m); err != nil {
return err
}
*m |= RoleStorageRPC
default:
return fmt.Errorf("%w: '%s'", ErrInvalidRole, role)
}
}
return nil
}

// ValidateBasic performs basic descriptor validity checks.
Expand Down
74 changes: 74 additions & 0 deletions go/common/node/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,80 @@ import (
"github.com/oasisprotocol/oasis-core/go/common/cbor"
)

func TestRolesMask(t *testing.T) {
require := require.New(t)

testVectors := []struct {
rolesMaskString string
rolesMask RolesMask
rolesMaskStringUnmarshable bool
rolesMaskStringCanonical bool
errMsg string
}{
// Valid single roles.
{"compute", 1, true, true, ""},
{"storage", 2, true, true, ""},
{"key-manager", 4, true, true, ""},
{"validator", 8, true, true, ""},
{"consensus-rpc", 16, true, true, ""},
{"storage-rpc", 32, true, true, ""},
// Valid multiple roles.
{"compute,storage", 3, true, true, ""},
{"compute,storage,validator", 11, true, true, ""},
{"compute,storage,validator,consensus-rpc", 27, true, true, ""},
{"validator,consensus-rpc", 24, true, true, ""},
{"storage,storage-rpc", 34, true, true, ""},

// Invalid - extra spaces.
{"compute ", 1, false, false, "node: invalid role: 'compute '"},
{"compute ,", 1, false, false, "node: invalid role: 'compute '"},
{" validator", 1, false, false, "node: invalid role: ' validator'"},
{"compute, storage", 1, false, false, "node: invalid role: ' storage'"},
// Invalid - unknown role.
{"master", 1, false, false, "node: invalid role: 'master'"},
// Invalid - role mask string not in canonical order.
{"storage-rpc,storage", 34, true, false, ""},
// Invalid - duplicate role in role mask string.
{"compute,compute", 8, false, false, "node: duplicate role: 'compute'"},
{"storage,storage", 8, false, false, "node: duplicate role: 'storage'"},
{"key-manager,key-manager", 8, false, false, "node: duplicate role: 'key-manager'"},
{"validator,validator", 8, false, false, "node: duplicate role: 'validator'"},
{"consensus-rpc,consensus-rpc", 8, false, false, "node: duplicate role: 'consensus-rpc'"},
{"storage-rpc,storage-rpc", 8, false, false, "node: duplicate role: 'storage-rpc'"},
{"compute,storage,compute", 1, false, false, "node: duplicate role: 'compute'"},
}

for _, v := range testVectors {
var unmarshaledRolesMask RolesMask
err := unmarshaledRolesMask.UnmarshalText([]byte(v.rolesMaskString))
if !v.rolesMaskStringUnmarshable {
require.EqualErrorf(
err,
v.errMsg,
"Unmarshaling invalid roles mask: '%s' should fail with expected error message",
v.rolesMaskString,
)
} else {
require.NoErrorf(err, "Failed to unmarshal a valid roles mask: '%s'", v.rolesMaskString)
require.Equal(
v.rolesMask,
unmarshaledRolesMask,
"Unmarshaled roles mask doesn't equal expected roles mask",
)
}

textRolesMask, err := v.rolesMask.MarshalText()
require.NoError(err, "Failed to marshal a valid roles mask: '%s'", v.rolesMask)
if v.rolesMaskStringCanonical {
require.Equal(
v.rolesMaskString,
string(textRolesMask),
"Marshaled roles mask doesn't equal expected text roles mask",
)
}
}
}

func TestNodeDescriptor(t *testing.T) {
require := require.New(t)

Expand Down

0 comments on commit 3731894

Please sign in to comment.