package proxy import ( "context" "fmt" "slices" "strings" "github.com/psviderski/uncloud/internal/machine/api/pb" ) // MachineTarget represents a resolved machine target. type MachineTarget struct { ID, Name, Addr string } // MachinesNotFoundError indicates that one or more requested machines were not found. type MachinesNotFoundError struct { NotFound []string } func (e *MachinesNotFoundError) Error() string { if len(e.NotFound) == 1 { return fmt.Sprintf("machine not found: %s", e.NotFound[0]) } return fmt.Sprintf("machines not found: %s", strings.Join(e.NotFound, ", ")) } // MachineMapper provides access to machine information in the cluster. type MachineMapper interface { // MapMachines resolves a list of machine names/IDs (or "*") to a list of machine targets. // Returns MachinesNotFoundError if any requested machine is not found (except when "*" is used). MapMachines(ctx context.Context, namesOrIDs []string) ([]MachineTarget, error) } // Store is the interface required by MachineMapper to access the cluster store. type Store interface { ListMachines(ctx context.Context) ([]*pb.MachineInfo, error) } // CorrosionMapper implements MachineMapper using the corrosion store. type CorrosionMapper struct { store Store } func NewCorrosionMapper(store Store) *CorrosionMapper { return &CorrosionMapper{store: store} } func (m *CorrosionMapper) MapMachines(ctx context.Context, namesOrIDs []string) ([]MachineTarget, error) { if len(namesOrIDs) == 0 { return nil, fmt.Errorf("no machines specified") } machines, err := m.store.ListMachines(ctx) if err != nil { return nil, fmt.Errorf("list machines: %w", err) } allTargets := make([]MachineTarget, 0, len(machines)) for _, machine := range machines { ip, err := machine.Network.ManagementIp.ToAddr() if err != nil { return nil, fmt.Errorf("invalid management IP for machine '%s' in store: %w", machine.Name, err) } allTargets = append(allTargets, MachineTarget{ ID: machine.Id, Name: machine.Name, Addr: ip.String(), }) } if slices.Contains(namesOrIDs, "*") { if len(allTargets) == 0 { return nil, fmt.Errorf("no machines in cluster") } return allTargets, nil } // Build a map for lookup (keyed by both ID and name) targetByLookup := make(map[string]MachineTarget, len(allTargets)*2) for _, t := range allTargets { targetByLookup[t.ID] = t targetByLookup[t.Name] = t } // Resolve each requested machine. targets := make([]MachineTarget, 0, len(namesOrIDs)) var notFound []string seenTarget := make(map[string]struct{}, len(namesOrIDs)) for _, nameOrID := range namesOrIDs { if t, ok := targetByLookup[nameOrID]; ok { if _, seen := seenTarget[t.ID]; !seen { targets = append(targets, t) seenTarget[t.ID] = struct{}{} } } else { notFound = append(notFound, nameOrID) } } if len(notFound) > 0 { return nil, &MachinesNotFoundError{NotFound: notFound} } return targets, nil }