Skip to content

Commit

Permalink
feat: automatically resolve cluster in talosctl calls
Browse files Browse the repository at this point in the history
`talosctl --cluster` flag is now optional, Omni will automatically
resolve the cluster if the machine is a part of one.
Fixes: #620

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
  • Loading branch information
Unix4ever committed Oct 30, 2024
1 parent 8da2328 commit 58159e4
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ run:
timeout: 10m
issues-exit-code: 1
tests: true
build-tags: [ ]
modules-download-mode: readonly
build-tags: [ ]

# output configuration options
output:
Expand Down
23 changes: 23 additions & 0 deletions cmd/integration-test/pkg/tests/talos.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,29 @@ func AssertTalosAPIAccessViaOmni(testCtx context.Context, omniClient *client.Cli
assertTalosAPI(ctx, t, c)
})

t.Run("InstanceWideTalosconfigWithoutCluster", func(t *testing.T) {
ctx, cancel := context.WithTimeout(testCtx, backoff.DefaultConfig.MaxDelay+10*time.Second)
t.Cleanup(cancel)

data, err := omniClient.Management().Talosconfig(ctx)
require.NoError(t, err)
assert.NotEmpty(t, data)

config, err := clientconfig.FromBytes(data)
require.NoError(t, err)

require.NoError(t, talosAPIKeyPrepare(ctx, "default"))

c, err := talosclient.New(ctx, talosclient.WithConfig(config))
require.NoError(t, err)

t.Cleanup(func() {
require.NoError(t, c.Close())
})

assertTalosAPI(ctx, t, c)
})

t.Run("ClusterWideTalosconfig", func(t *testing.T) {
ctx, cancel := context.WithTimeout(testCtx, backoff.DefaultConfig.MaxDelay+10*time.Second)
t.Cleanup(cancel)
Expand Down
85 changes: 84 additions & 1 deletion internal/backend/dns/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"sync"

"github.com/cosi-project/runtime/pkg/resource"
Expand All @@ -34,6 +35,8 @@ type Info struct {

address string
managementEndpoint string

Ambiguous bool
}

// NewInfo exports unexported.
Expand All @@ -55,13 +58,43 @@ func (i Info) GetAddress() string {
return i.managementEndpoint
}

type resolverMap map[string][]resource.ID

func (m resolverMap) get(key string) []resource.ID {
return m[key]
}

func (m resolverMap) add(key string, id resource.ID) {
if slices.Index(m[key], id) != -1 {
return
}

m[key] = append(m[key], id)
}

func (m resolverMap) remove(key string, id resource.ID) {
ids, ok := m[key]
if !ok {
return
}

ids = slices.DeleteFunc(ids, func(item string) bool { return item == id })
if len(ids) == 0 {
delete(m, key)
} else {
m[key] = ids
}
}

// Service is the DNS service.
type Service struct {
omniState state.State
logger *zap.Logger

recordToMachineID map[record]resource.ID
machineIDToInfo map[resource.ID]Info
addressToID resolverMap
nodenameToID resolverMap

lock sync.Mutex
}
Expand All @@ -73,6 +106,8 @@ func NewService(omniState state.State, logger *zap.Logger) *Service {
logger: logger,
recordToMachineID: make(map[record]string),
machineIDToInfo: make(map[string]Info),
addressToID: make(resolverMap),
nodenameToID: make(resolverMap),
}
}

Expand Down Expand Up @@ -165,9 +200,11 @@ func (d *Service) updateEntryByIdentity(res *omni.ClusterMachineIdentity) {

info.Cluster = clusterName
info.ID = res.Metadata().ID()
info.Name = nodeName

previousAddress := info.address
previousNodename := info.Name

info.Name = nodeName

nodeIPs := res.TypedSpec().Value.NodeIps
if len(nodeIPs) == 0 {
Expand All @@ -190,12 +227,16 @@ func (d *Service) updateEntryByIdentity(res *omni.ClusterMachineIdentity) {
name: res.Metadata().ID(),
}] = res.Metadata().ID()

d.nodenameToID.add(nodeName, res.Metadata().ID())

// create entry by address
if info.address != "" {
d.recordToMachineID[record{
cluster: clusterName,
name: info.address,
}] = res.Metadata().ID()

d.addressToID.add(info.address, res.Metadata().ID())
}

// cleanup old entry by address
Expand All @@ -204,6 +245,18 @@ func (d *Service) updateEntryByIdentity(res *omni.ClusterMachineIdentity) {
cluster: clusterName,
name: previousAddress,
})

d.addressToID.remove(previousAddress, res.Metadata().ID())
}

// cleanup old entry by nodename
if previousNodename != "" && previousNodename != info.Name {
delete(d.recordToMachineID, record{
cluster: clusterName,
name: previousNodename,
})

d.nodenameToID.remove(previousNodename, res.Metadata().ID())
}

d.logger.Debug(
Expand Down Expand Up @@ -258,6 +311,9 @@ func (d *Service) deleteIdentityMappings(id resource.ID) {
cluster: info.Cluster,
name: info.address,
})

d.addressToID.remove(info.address, id)
d.nodenameToID.remove(info.Name, id)
}

info.address = ""
Expand Down Expand Up @@ -293,6 +349,27 @@ func (d *Service) deleteMachineMappings(id resource.ID) {
)
}

func (d *Service) resolveByAddressOrNodename(name string) (Info, bool) {
for _, resolver := range []resolverMap{
d.addressToID,
d.nodenameToID,
} {
ids := resolver.get(name)
if len(ids) > 1 {
return Info{
Name: name,
Ambiguous: true,
}, true
}

if len(ids) > 0 {
return d.machineIDToInfo[ids[0]], true
}
}

return Info{}, false
}

// Resolve returns the dns.Info for the given node name, address or machine UUID.
func (d *Service) Resolve(clusterName, name string) Info {
d.lock.Lock()
Expand All @@ -303,6 +380,12 @@ func (d *Service) Resolve(clusterName, name string) Info {
name: name,
}]

if !ok {
if info, found := d.resolveByAddressOrNodename(name); found {
return info
}
}

if !ok {
return d.machineIDToInfo[name]
}
Expand Down
2 changes: 1 addition & 1 deletion internal/backend/dns/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func (suite *ServiceSuite) assertResolveAddress(cluster, node, expected string)
resolved := suite.dnsService.Resolve(cluster, node)

if resolved.GetAddress() != expected {
return retry.ExpectedErrorf("expected %s, got %s", expected, resolved)
return retry.ExpectedErrorf("expected %s, got %s", expected, resolved.GetAddress())
}

return nil
Expand Down
30 changes: 29 additions & 1 deletion internal/backend/grpc/router/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
package router

import (
"errors"
"fmt"
"strings"

"github.com/siderolabs/gen/xslices"
Expand All @@ -31,6 +33,32 @@ type resolvedNodeInfo struct {
nodeOk bool
}

func (r resolvedNodeInfo) getNode() (dns.Info, error) {
if r.nodeOk {
return r.node, nil
}

if len(r.nodes) > 0 {
var clusterName string

for _, n := range r.nodes {
if n.Ambiguous {
return n, nil
}

if clusterName != "" && clusterName != n.Cluster {
return dns.Info{}, fmt.Errorf("all nodes should be in the same cluster, found clusters %q and %q", clusterName, n.Cluster)
}

clusterName = n.Cluster
}

return r.nodes[0], nil
}

return dns.Info{}, errors.New("node not found")
}

func resolveNodes(dnsService NodeResolver, md metadata.MD) resolvedNodeInfo {
var (
node string
Expand Down Expand Up @@ -61,7 +89,7 @@ func resolveNodes(dnsService NodeResolver, md metadata.MD) resolvedNodeInfo {
resolved = dnsService.Resolve(cluster, val)
}

if resolved.GetAddress() == "" {
if resolved.GetAddress() == "" && !resolved.Ambiguous {
return dns.NewInfo(
cluster,
val,
Expand Down
21 changes: 19 additions & 2 deletions internal/backend/grpc/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,27 @@ func (r *Router) Director(ctx context.Context, fullMethodName string) (proxy.Mod
func (r *Router) getTalosBackend(ctx context.Context, md metadata.MD) ([]proxy.Backend, error) {
clusterName := getClusterName(md)

id := fmt.Sprintf("cluster-%s", clusterName)
id := "cluster-" + clusterName

if clusterName == "" {
id = fmt.Sprintf("machine-%s", getNodeID(md))
resolved := resolveNodes(r.nodeResolver, md)

node, err := resolved.getNode()
if err != nil {
return nil, err
}

if node.Ambiguous {
return nil, fmt.Errorf("name or address %q is ambiguous, please specify the cluster name explicitly", node.Name)
}

clusterName = node.Cluster

if node.Cluster != "" {
id = "cluster-" + clusterName
} else {
id = "machine-" + node.ID
}
}

if backend, ok := r.talosBackends.Get(id); ok {
Expand Down

0 comments on commit 58159e4

Please sign in to comment.