diff --git a/lib/dns/server.go b/lib/dns/server.go index 74f1814f..eeab54c4 100644 --- a/lib/dns/server.go +++ b/lib/dns/server.go @@ -37,7 +37,7 @@ const ( // InstanceResolver provides instance IP resolution. // This interface is implemented by the instances package. type InstanceResolver interface { - // ResolveInstanceIP resolves an instance name or ID to its IP address. + // ResolveInstanceIP resolves an instance name or ID to its IP address for DNS. ResolveInstanceIP(ctx context.Context, nameOrID string) (string, error) } diff --git a/lib/instances/fork.go b/lib/instances/fork.go index c7dbef65..4ce7ee6e 100644 --- a/lib/instances/fork.go +++ b/lib/instances/fork.go @@ -7,7 +7,6 @@ import ( "fmt" "hash/crc32" "os" - "path/filepath" "strings" "time" @@ -368,25 +367,6 @@ func validateForkVolumeSafety(volumes []VolumeAttachment) error { return nil } -func (m *manager) instanceNameExists(name string) (bool, error) { - metaFiles, err := m.listMetadataFiles() - if err != nil { - return false, err - } - - for _, metaFile := range metaFiles { - id := filepath.Base(filepath.Dir(metaFile)) - meta, err := m.loadMetadata(id) - if err != nil { - continue - } - if meta.Name == name { - return true, nil - } - } - return false, nil -} - func resolveForkTargetState(requested State, sourceState State) (State, error) { if requested == "" { switch sourceState { diff --git a/lib/instances/ingress_resolver.go b/lib/instances/ingress_resolver.go index 47d9200a..17c2bf7a 100644 --- a/lib/instances/ingress_resolver.go +++ b/lib/instances/ingress_resolver.go @@ -5,6 +5,8 @@ import ( "fmt" ) +const dnsMinIDPrefixLength = 8 + // IngressResolver provides instance resolution for the ingress package. // It implements ingress.InstanceResolver interface without importing the ingress package // to avoid import cycles. @@ -12,6 +14,10 @@ type IngressResolver struct { manager Manager } +type minPrefixInstanceManager interface { + getInstanceWithMinIDPrefix(ctx context.Context, idOrName string, minPrefixLength int) (*Instance, error) +} + // NewIngressResolver creates a new IngressResolver that wraps an instance manager. func NewIngressResolver(manager Manager) *IngressResolver { return &IngressResolver{manager: manager} @@ -19,7 +25,12 @@ func NewIngressResolver(manager Manager) *IngressResolver { // ResolveInstanceIP resolves an instance name, ID, or ID prefix to its IP address. func (r *IngressResolver) ResolveInstanceIP(ctx context.Context, nameOrID string) (string, error) { - inst, err := r.manager.GetInstance(ctx, nameOrID) + manager, ok := r.manager.(minPrefixInstanceManager) + if !ok { + return "", fmt.Errorf("instance resolver does not support DNS-safe lookup") + } + + inst, err := manager.getInstanceWithMinIDPrefix(ctx, nameOrID, dnsMinIDPrefixLength) if err != nil { return "", fmt.Errorf("instance not found: %s", nameOrID) } diff --git a/lib/instances/manager.go b/lib/instances/manager.go index 495d8a4e..61774dad 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -299,6 +299,15 @@ func (m *manager) maybePersistBootMarkers(ctx context.Context, id string) { m.persistBootMarkers(ctx, id) } +func (m *manager) finalizeResolvedInstance(ctx context.Context, inst *Instance) { + if inst.State == StateStopped && inst.ExitCode != nil { + m.maybePersistExitInfo(ctx, inst.Id) + } + if (inst.State == StateRunning || inst.State == StateInitializing) && inst.BootMarkersHydrated { + m.maybePersistBootMarkers(ctx, inst.Id) + } +} + func (m *manager) recordImageUsage(ctx context.Context, imageInfo *images.Image) { if m.imageUsageRecorder == nil || imageInfo == nil { return @@ -499,66 +508,36 @@ func (m *manager) ListInstances(ctx context.Context, filter *ListInstancesFilter // Lookup order: exact ID match -> exact name match -> ID prefix match. // Returns ErrAmbiguousName if prefix matches multiple instances. func (m *manager) GetInstance(ctx context.Context, idOrName string) (*Instance, error) { + return m.getInstanceWithMinIDPrefix(ctx, idOrName, 1) +} + +func (m *manager) getInstanceWithMinIDPrefix(ctx context.Context, idOrName string, minPrefixLength int) (*Instance, error) { // 1. Try exact ID match first (most common case) lock := m.getInstanceLock(idOrName) lock.RLock() inst, err := m.getInstance(ctx, idOrName) lock.RUnlock() if err == nil { - // If VM is stopped with unpersisted exit info, persist under write lock. - // This handles the "app exited on its own" case where stopInstance wasn't called. - if inst.State == StateStopped && inst.ExitCode != nil { - m.maybePersistExitInfo(ctx, inst.Id) - } - if (inst.State == StateRunning || inst.State == StateInitializing) && inst.BootMarkersHydrated { - m.maybePersistBootMarkers(ctx, inst.Id) - } + m.finalizeResolvedInstance(ctx, inst) return inst, nil } - // 2. List all instances for name and prefix matching - instances, err := m.ListInstances(ctx, nil) + // 2. Resolve exact name or ID prefix from metadata only, then hydrate the + // single matched instance. + meta, err := m.findInstanceMetadataByNameOrIDPrefix(idOrName, minPrefixLength) if err != nil { return nil, err } - // 3. Try exact name match - var nameMatches []Instance - for _, inst := range instances { - if inst.Name == idOrName { - nameMatches = append(nameMatches, inst) - } - } - if len(nameMatches) == 1 { - inst := &nameMatches[0] - if inst.State == StateStopped && inst.ExitCode != nil { - m.maybePersistExitInfo(ctx, inst.Id) - } - return inst, nil - } - if len(nameMatches) > 1 { - return nil, ErrAmbiguousName - } - - // 4. Try ID prefix match - var prefixMatches []Instance - for _, inst := range instances { - if len(idOrName) > 0 && len(inst.Id) >= len(idOrName) && inst.Id[:len(idOrName)] == idOrName { - prefixMatches = append(prefixMatches, inst) - } - } - if len(prefixMatches) == 1 { - inst := &prefixMatches[0] - if inst.State == StateStopped && inst.ExitCode != nil { - m.maybePersistExitInfo(ctx, inst.Id) - } - return inst, nil - } - if len(prefixMatches) > 1 { - return nil, ErrAmbiguousName + resolvedLock := m.getInstanceLock(meta.Id) + resolvedLock.RLock() + inst, err = m.getInstance(ctx, meta.Id) + resolvedLock.RUnlock() + if err != nil { + return nil, err } - - return nil, ErrNotFound + m.finalizeResolvedInstance(ctx, inst) + return inst, nil } // StreamInstanceLogs streams instance logs from the specified source diff --git a/lib/instances/query.go b/lib/instances/query.go index 0a38771c..2986f175 100644 --- a/lib/instances/query.go +++ b/lib/instances/query.go @@ -649,6 +649,87 @@ func (m *manager) listInstances(ctx context.Context) ([]Instance, error) { return result, nil } +func (m *manager) findInstanceMetadataByExactName(name string) (*metadata, error) { + files, err := m.listMetadataFiles() + if err != nil { + return nil, err + } + + for _, file := range files { + id := filepath.Base(filepath.Dir(file)) + meta, err := m.loadMetadata(id) + if err != nil { + continue + } + if meta.Name == name { + return meta, nil + } + } + return nil, ErrNotFound +} + +func (m *manager) findInstanceMetadataByNameOrIDPrefix(idOrName string, minPrefixLength int) (*metadata, error) { + files, err := m.listMetadataFiles() + if err != nil { + return nil, err + } + if minPrefixLength < 1 { + minPrefixLength = 1 + } + + var nameMatch *metadata + var prefixMatch *metadata + nameMatches := 0 + prefixMatches := 0 + + for _, file := range files { + id := filepath.Base(filepath.Dir(file)) + meta, err := m.loadMetadata(id) + if err != nil { + continue + } + + if meta.Name == idOrName { + nameMatches++ + if nameMatches == 1 { + nameMatch = meta + } + } + + if len(idOrName) >= minPrefixLength && strings.HasPrefix(meta.Id, idOrName) { + prefixMatches++ + if prefixMatches == 1 { + prefixMatch = meta + } + } + } + + if nameMatches == 1 { + return nameMatch, nil + } + if nameMatches > 1 { + return nil, ErrAmbiguousName + } + if prefixMatches == 1 { + return prefixMatch, nil + } + if prefixMatches > 1 { + return nil, ErrAmbiguousName + } + return nil, ErrNotFound +} + +func (m *manager) instanceNameExists(name string) (bool, error) { + _, err := m.findInstanceMetadataByExactName(name) + if err == nil { + return true, nil + } + if err == ErrNotFound { + return false, nil + } + return false, err +} + // getInstance returns a single instance by ID func (m *manager) getInstance(ctx context.Context, id string) (*Instance, error) { log := logger.FromContext(ctx)