mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-09 08:01:02 +00:00
feat(easytier-go): expose CreateInstanceTOML and ShowNodeInfo (#2557)
This commit is contained in:
@@ -55,6 +55,17 @@ connection, err := instance.Dial(ctx, "tcp4", "10.144.0.2:8080")
|
||||
packets, err := instance.ListenPacket("udp4", ":5353")
|
||||
```
|
||||
|
||||
`CreateInstanceTOML` loads a native EasyTier TOML document. Pass an empty
|
||||
`instanceID` to allocate a UUID. Existing `instance_id` and `instance_name`
|
||||
keys in the document are replaced by the host.
|
||||
|
||||
```go
|
||||
instance, err := host.CreateInstanceTOML(ctx, "office", "", configTOML)
|
||||
```
|
||||
|
||||
`Instance.ShowNodeInfo` returns this instance's virtual IPv4 address and
|
||||
advertised hostname.
|
||||
|
||||
### Web Client management
|
||||
|
||||
A host can also connect to an EasyTier Web configuration server. The embedded
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/EasyTier/EasyTier/easytier-go/proto/common"
|
||||
|
||||
"github.com/EasyTier/EasyTier/easytier-go/internal/artifact"
|
||||
"github.com/EasyTier/EasyTier/easytier-go/internal/contextutil"
|
||||
@@ -132,6 +135,66 @@ func (host *Host) CreateInstance(
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// CreateInstanceTOML creates an EasyTier instance from a TOML configuration.
|
||||
// instanceName is used as the instance_name field. If instanceID is empty, a
|
||||
// new UUID is generated. Existing instance_id and instance_name keys in the
|
||||
// TOML are replaced by bindInstanceIdentity.
|
||||
func (host *Host) CreateInstanceTOML(
|
||||
ctx context.Context,
|
||||
instanceName string,
|
||||
instanceID string,
|
||||
configTOML string,
|
||||
) (*Instance, error) {
|
||||
if host == nil {
|
||||
return nil, fmt.Errorf("create instance with nil EasyTier host")
|
||||
}
|
||||
if ctx == nil {
|
||||
return nil, fmt.Errorf("create EasyTier instance with nil context")
|
||||
}
|
||||
if strings.TrimSpace(configTOML) == "" {
|
||||
return nil, fmt.Errorf("EasyTier TOML configuration is empty")
|
||||
}
|
||||
if instanceName == "" {
|
||||
instanceName = "easytier"
|
||||
}
|
||||
var (
|
||||
id *common.UUID
|
||||
idString string
|
||||
err error
|
||||
)
|
||||
if instanceID != "" {
|
||||
id, idString, err = parseInstanceUUID(instanceID)
|
||||
} else {
|
||||
id, idString, err = newInstanceUUID()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtime, err := host.engine.CreateInstance(
|
||||
ctx,
|
||||
bindInstanceIdentity(stripInstanceIdentity(configTOML), idString, instanceName),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instance := &Instance{
|
||||
engine: runtime,
|
||||
id: idString,
|
||||
manager: host.manager,
|
||||
}
|
||||
if err := host.manager.register(&managedInstance{
|
||||
id: id,
|
||||
instance: instance,
|
||||
owner: instanceOwnerApplication,
|
||||
source: manage.ConfigSource_ConfigSourceUser,
|
||||
name: instanceName,
|
||||
}); err != nil {
|
||||
_ = runtime.Close(contextutil.WithoutCancel(ctx))
|
||||
return nil, err
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// Instances returns a stable snapshot of all application- and Web-owned instances.
|
||||
func (host *Host) Instances() []*Instance {
|
||||
if host == nil || host.manager == nil {
|
||||
|
||||
@@ -140,3 +140,21 @@ func quoteTOMLString(value string) string {
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func stripInstanceIdentity(configTOML string) string {
|
||||
var kept []string
|
||||
for _, line := range strings.Split(configTOML, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "instance_id") || strings.HasPrefix(trimmed, "instance_name") {
|
||||
key := trimmed
|
||||
if idx := strings.IndexByte(trimmed, '='); idx >= 0 {
|
||||
key = strings.TrimSpace(trimmed[:idx])
|
||||
}
|
||||
if key == "instance_id" || key == "instance_name" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
kept = append(kept, line)
|
||||
}
|
||||
return strings.TrimSpace(strings.Join(kept, "\n"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStripInstanceIdentityRemovesIdentityKeys(t *testing.T) {
|
||||
input := `instance_id = "87ede5a2-9c3d-492d-9bbe-989b9d07e742"
|
||||
instance_name = "old-name"
|
||||
hostname = "node-a"
|
||||
instance_identity = "keep"
|
||||
|
||||
[network_identity]
|
||||
network_name = "example"
|
||||
`
|
||||
got := stripInstanceIdentity(input)
|
||||
if strings.Contains(got, "instance_id =") {
|
||||
t.Fatalf("instance_id was not stripped:\n%s", got)
|
||||
}
|
||||
if strings.Contains(got, "instance_name =") {
|
||||
t.Fatalf("instance_name was not stripped:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, `hostname = "node-a"`) {
|
||||
t.Fatalf("hostname was stripped:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, `instance_identity = "keep"`) {
|
||||
t.Fatalf("unrelated key was stripped:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "[network_identity]") {
|
||||
t.Fatalf("network identity section was stripped:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripInstanceIdentityIgnoresCommentedAndPrefixedKeys(t *testing.T) {
|
||||
input := `# instance_id = "commented"
|
||||
instance_id_backup = "keep"
|
||||
`
|
||||
got := stripInstanceIdentity(input)
|
||||
if !strings.Contains(got, `# instance_id = "commented"`) {
|
||||
t.Fatalf("comment was stripped:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, `instance_id_backup = "keep"`) {
|
||||
t.Fatalf("prefixed key was stripped:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindInstanceIdentityPrependsCanonicalKeys(t *testing.T) {
|
||||
got := bindInstanceIdentity("hostname = \"node\"\n", "11111111-2222-4333-8444-555555555555", "mihomo")
|
||||
wantPrefix := `instance_id = "11111111-2222-4333-8444-555555555555"
|
||||
instance_name = "mihomo"
|
||||
`
|
||||
if !strings.HasPrefix(got, wantPrefix) {
|
||||
t.Fatalf("bindInstanceIdentity() = %q, want prefix %q", got, wantPrefix)
|
||||
}
|
||||
if !strings.Contains(got, `hostname = "node"`) {
|
||||
t.Fatalf("original config was lost: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
@@ -710,6 +711,26 @@ func peerRoutePairs(
|
||||
return pairs
|
||||
}
|
||||
|
||||
func parseInstanceUUID(text string) (*common.UUID, string, error) {
|
||||
normalized := strings.ReplaceAll(strings.TrimSpace(text), "-", "")
|
||||
if len(normalized) != 32 {
|
||||
return nil, "", fmt.Errorf("invalid EasyTier instance ID %q", text)
|
||||
}
|
||||
var value [16]byte
|
||||
decoded, err := hex.DecodeString(normalized)
|
||||
if err != nil || len(decoded) != 16 {
|
||||
return nil, "", fmt.Errorf("invalid EasyTier instance ID %q", text)
|
||||
}
|
||||
copy(value[:], decoded)
|
||||
id := &common.UUID{
|
||||
Part1: binary.BigEndian.Uint32(value[0:4]),
|
||||
Part2: binary.BigEndian.Uint32(value[4:8]),
|
||||
Part3: binary.BigEndian.Uint32(value[8:12]),
|
||||
Part4: binary.BigEndian.Uint32(value[12:16]),
|
||||
}
|
||||
return id, uuidString(id), nil
|
||||
}
|
||||
|
||||
func newInstanceUUID() (*common.UUID, string, error) {
|
||||
var value [16]byte
|
||||
if _, err := rand.Read(value[:]); err != nil {
|
||||
|
||||
@@ -23,6 +23,9 @@ type PeerInfo = apiinstance.PeerInfo
|
||||
// Route describes one route visible to an EasyTier instance.
|
||||
type Route = apiinstance.Route
|
||||
|
||||
// NodeInfo describes this EasyTier instance.
|
||||
type NodeInfo = apiinstance.NodeInfo
|
||||
|
||||
// ListPeer returns the peers visible to this EasyTier instance.
|
||||
func (instance *Instance) ListPeer(
|
||||
ctx context.Context,
|
||||
@@ -65,6 +68,19 @@ func (instance *Instance) listRouteResponse(
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ShowNodeInfo returns this instance's node information, including its
|
||||
// virtual IPv4 address and advertised hostname.
|
||||
func (instance *Instance) ShowNodeInfo(ctx context.Context) (*NodeInfo, error) {
|
||||
response, err := instance.showNodeInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response.NodeInfo == nil {
|
||||
return nil, fmt.Errorf("EasyTier ShowNodeInfo returned no node info")
|
||||
}
|
||||
return response.NodeInfo, nil
|
||||
}
|
||||
|
||||
func (instance *Instance) showNodeInfo(
|
||||
ctx context.Context,
|
||||
) (*apiinstance.ShowNodeInfoResponse, error) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseInstanceUUIDAcceptsCanonicalAndHex(t *testing.T) {
|
||||
canonical := "87ede5a2-9c3d-492d-9bbe-989b9d07e742"
|
||||
id, text, err := parseInstanceUUID(canonical)
|
||||
if err != nil {
|
||||
t.Fatalf("parse canonical UUID: %v", err)
|
||||
}
|
||||
if text != canonical {
|
||||
t.Fatalf("canonical UUID string = %q, want %q", text, canonical)
|
||||
}
|
||||
if id == nil {
|
||||
t.Fatal("parsed UUID was nil")
|
||||
}
|
||||
|
||||
_, compact, err := parseInstanceUUID("87EDE5A29C3D492D9BBE989B9D07E742")
|
||||
if err != nil {
|
||||
t.Fatalf("parse compact UUID: %v", err)
|
||||
}
|
||||
if compact != canonical {
|
||||
t.Fatalf("compact UUID string = %q, want %q", compact, canonical)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInstanceUUIDRejectsInvalidValues(t *testing.T) {
|
||||
for _, input := range []string{"", "not-a-uuid", "87ede5a2-9c3d-492d-9bbe", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"} {
|
||||
if _, _, err := parseInstanceUUID(input); err == nil {
|
||||
t.Fatalf("parseInstanceUUID(%q) succeeded", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInstanceUUIDRoundTrip(t *testing.T) {
|
||||
id, text, err := newInstanceUUID()
|
||||
if err != nil {
|
||||
t.Fatalf("newInstanceUUID: %v", err)
|
||||
}
|
||||
parsed, parsedText, err := parseInstanceUUID(text)
|
||||
if err != nil {
|
||||
t.Fatalf("parse generated UUID: %v", err)
|
||||
}
|
||||
if parsedText != text {
|
||||
t.Fatalf("round-trip UUID string = %q, want %q", parsedText, text)
|
||||
}
|
||||
if parsed.GetPart1() != id.GetPart1() || parsed.GetPart2() != id.GetPart2() ||
|
||||
parsed.GetPart3() != id.GetPart3() || parsed.GetPart4() != id.GetPart4() {
|
||||
t.Fatalf("round-trip UUID parts = %+v, want %+v", parsed, id)
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,6 @@ type PeerInfo = internalhost.PeerInfo
|
||||
|
||||
// Route describes one route visible to an EasyTier instance.
|
||||
type Route = internalhost.Route
|
||||
|
||||
// NodeInfo describes this EasyTier instance.
|
||||
type NodeInfo = internalhost.NodeInfo
|
||||
|
||||
Reference in New Issue
Block a user