From 0c83f7f95430da38ad7132554be0a8b40192bfce Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:18:33 +1000 Subject: [PATCH] refactor: use strings.CutPrefix/CutSuffix for guard-then-trim pairs --- spinifex/daemon/instance_records_migrate.go | 9 ++--- spinifex/filterutil/filterutil.go | 4 +-- spinifex/gateway/bedrock/guardrail_invoke.go | 35 +++++++++++-------- spinifex/gateway/bedrock/invoke.go | 10 +++--- spinifex/gateway/bedrock/provider.go | 10 +++--- spinifex/gateway/bedrock/sse.go | 14 ++++---- spinifex/gateway/ecr/authorization.go | 5 +-- spinifex/gateway/ecr/registry.go | 4 +-- spinifex/gpu/iommu.go | 3 +- spinifex/handlers/ecs/scheduler.go | 5 +-- spinifex/handlers/eks/addon_state.go | 5 +-- spinifex/handlers/eks/cp_vpc_test.go | 4 +-- spinifex/handlers/eks/restore_snapshot.go | 5 +-- spinifex/handlers/eks/service_impl.go | 5 +-- spinifex/handlers/eks/token_webhook.go | 5 ++- spinifex/handlers/imds/action.go | 4 +-- spinifex/handlers/imds/metadata.go | 4 +-- spinifex/handlers/imds/resolver.go | 5 +-- .../handlers/ochrevector/hostport_test.go | 5 +-- spinifex/handlers/rds/store.go | 9 ++--- spinifex/instancetypes/generate.go | 12 ++++--- spinifex/network/host/imds_reconcile.go | 4 +-- .../services/viperblockd/nbdkit_recovery.go | 10 +++--- spinifex/utils/images.go | 5 +-- spinifex/vm/orphan_scan.go | 5 ++- tests/e2e/select-suites/selector.go | 6 ++-- tests/e2e/single/guestchurn_test.go | 5 +-- 27 files changed, 108 insertions(+), 89 deletions(-) diff --git a/spinifex/daemon/instance_records_migrate.go b/spinifex/daemon/instance_records_migrate.go index d5759f3e8..829e2bf77 100644 --- a/spinifex/daemon/instance_records_migrate.go +++ b/spinifex/daemon/instance_records_migrate.go @@ -94,7 +94,8 @@ func rekeyRecordSeparator(ctx context.Context, kvc migrate.KVContext) error { moved := 0 for _, key := range keys { - if !strings.HasPrefix(key, slashRecordPrefix) { + instanceID, ok := strings.CutPrefix(key, slashRecordPrefix) + if !ok { continue } @@ -106,7 +107,7 @@ func rekeyRecordSeparator(ctx context.Context, kvc migrate.KVContext) error { return fmt.Errorf("read %s: %w", key, err) } - dest := instanceRecordKey(strings.TrimPrefix(key, slashRecordPrefix)) + dest := instanceRecordKey(instanceID) if _, err := kvc.KV.Create(ctx, dest, entry.Value()); err != nil { if !errors.Is(err, jetstream.ErrKeyExists) { return fmt.Errorf("write %s: %w", dest, err) @@ -151,10 +152,10 @@ func carryNodeOwnershipForward(ctx context.Context, kvc migrate.KVContext) error seeded, owned := 0, 0 for _, key := range keys { - if !strings.HasPrefix(key, InstanceStatePrefix) { + nodeID, ok := strings.CutPrefix(key, InstanceStatePrefix) + if !ok { continue } - nodeID := strings.TrimPrefix(key, InstanceStatePrefix) if _, err := kvc.KV.Create(ctx, NodePresencePrefix+nodeID, marker); err != nil { // A node that has already upgraded wrote its own marker; it is the diff --git a/spinifex/filterutil/filterutil.go b/spinifex/filterutil/filterutil.go index 56bf0b003..52fbf4cdc 100644 --- a/spinifex/filterutil/filterutil.go +++ b/spinifex/filterutil/filterutil.go @@ -58,10 +58,10 @@ func MatchesAny(filterValues []string, value string) bool { // Each tag:Key filter uses OR logic across its values, with wildcard support. func MatchesTags(filters map[string][]string, tags map[string]string) bool { for name, values := range filters { - if !strings.HasPrefix(name, "tag:") { + tagKey, ok := strings.CutPrefix(name, "tag:") + if !ok { continue } - tagKey := name[4:] // strip "tag:" prefix tagValue, exists := tags[tagKey] if !exists { return false diff --git a/spinifex/gateway/bedrock/guardrail_invoke.go b/spinifex/gateway/bedrock/guardrail_invoke.go index 0d43503da..19c1937b6 100644 --- a/spinifex/gateway/bedrock/guardrail_invoke.go +++ b/spinifex/gateway/bedrock/guardrail_invoke.go @@ -192,11 +192,12 @@ func setAnthropicCompletionTexts(body []byte, texts []string) ([]byte, error) { // extractTokenUsage's own backend switch. ok is false when body doesn't // decode as backend's shape or backend has no guardrail support. func extractInvokePromptTexts(backend string, body []byte) (texts []string, ok bool) { + vendor, isVendor := strings.CutPrefix(backend, providerPrefix) switch { case backend == tierSelfHost: return extractLlamaPromptTexts(body) - case strings.HasPrefix(backend, providerPrefix): - switch strings.TrimPrefix(backend, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: return extractAnthropicPromptTexts(body) } @@ -206,11 +207,12 @@ func extractInvokePromptTexts(backend string, body []byte) (texts []string, ok b // extractInvokeCompletionTexts is extractInvokePromptTexts' OUTPUT sibling. func extractInvokeCompletionTexts(backend string, body []byte) (texts []string, ok bool) { + vendor, isVendor := strings.CutPrefix(backend, providerPrefix) switch { case backend == tierSelfHost: return extractLlamaCompletionTexts(body) - case strings.HasPrefix(backend, providerPrefix): - switch strings.TrimPrefix(backend, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: return extractAnthropicCompletionTexts(body) } @@ -222,11 +224,12 @@ func extractInvokeCompletionTexts(backend string, body []byte) (texts []string, // InvokeModel response body carrying message as the assistant text, for an // INPUT guardrail block -- the backend is never called. func invokeGuardrailBlockedResponse(backend, modelID, message string) ([]byte, error) { + vendor, isVendor := strings.CutPrefix(backend, providerPrefix) switch { case backend == tierSelfHost: return buildLlamaBlockedResponse(message) - case strings.HasPrefix(backend, providerPrefix): - switch strings.TrimPrefix(backend, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: return buildAnthropicBlockedResponse(modelID, message) } @@ -237,11 +240,12 @@ func invokeGuardrailBlockedResponse(backend, modelID, message string) ([]byte, e // invokeGuardrailBlockedCompletion rewrites respBody's assistant text // wholesale with message, for an OUTPUT guardrail block. func invokeGuardrailBlockedCompletion(backend string, respBody []byte, message string) ([]byte, error) { + vendor, isVendor := strings.CutPrefix(backend, providerPrefix) switch { case backend == tierSelfHost: return replaceLlamaCompletion(respBody, message) - case strings.HasPrefix(backend, providerPrefix): - switch strings.TrimPrefix(backend, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: return replaceAnthropicCompletion(respBody, message) } @@ -252,14 +256,15 @@ func invokeGuardrailBlockedCompletion(backend string, respBody []byte, message s // invokeGuardrailRedactedCompletion rewrites respBody's assistant text // positionally with texts, for an OUTPUT ANONYMIZE redaction. func invokeGuardrailRedactedCompletion(backend string, respBody []byte, texts []string) ([]byte, error) { + vendor, isVendor := strings.CutPrefix(backend, providerPrefix) switch { case backend == tierSelfHost: if len(texts) == 0 { return respBody, nil } return setLlamaCompletionText(respBody, texts[0]) - case strings.HasPrefix(backend, providerPrefix): - switch strings.TrimPrefix(backend, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: return setAnthropicCompletionTexts(respBody, texts) } @@ -271,6 +276,7 @@ func invokeGuardrailRedactedCompletion(backend string, respBody []byte, texts [] // invoke-stream chunk for buffered OUTPUT accumulation. ok=false is a benign // no-text chunk; decodeErr=true is a real JSON-decode failure (fail closed). func extractInvokeStreamChunkText(backend string, chunk []byte) (text string, ok bool, decodeErr bool) { + vendor, isVendor := strings.CutPrefix(backend, providerPrefix) switch { case backend == tierSelfHost: var c struct { @@ -280,8 +286,8 @@ func extractInvokeStreamChunkText(backend string, chunk []byte) (text string, ok return "", false, true } return c.Generation, c.Generation != "", false - case strings.HasPrefix(backend, providerPrefix): - switch strings.TrimPrefix(backend, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: var c struct { Type string `json:"type"` @@ -361,11 +367,12 @@ func anthropicGuardedStreamChunks(message string, blocked bool) ([][]byte, error // buildGuardedInvokeStreamChunks dispatches to the family-specific guarded // invoke-stream chunk builder for backend. func buildGuardedInvokeStreamChunks(backend, message string, blocked bool) ([][]byte, error) { + vendor, isVendor := strings.CutPrefix(backend, providerPrefix) switch { case backend == tierSelfHost: return llamaGuardedStreamChunks(message, blocked) - case strings.HasPrefix(backend, providerPrefix): - switch strings.TrimPrefix(backend, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: return anthropicGuardedStreamChunks(message, blocked) } diff --git a/spinifex/gateway/bedrock/invoke.go b/spinifex/gateway/bedrock/invoke.go index 0e010a617..e98880a23 100644 --- a/spinifex/gateway/bedrock/invoke.go +++ b/spinifex/gateway/bedrock/invoke.go @@ -135,6 +135,7 @@ func (rt *InvokeRouter) InvokeModel(ctx context.Context, accountID, modelID stri backend = entry.Provider var a InvokeAdapter + vendor, isVendor := strings.CutPrefix(entry.Provider, providerPrefix) switch { case entry.Provider == tierSelfHost: // Family validation first: a model this platform cannot serve at @@ -150,8 +151,8 @@ func (rt *InvokeRouter) InvokeModel(ctx context.Context, accountID, modelID stri return nil, "", err } defer release() - case strings.HasPrefix(entry.Provider, providerPrefix): - switch strings.TrimPrefix(entry.Provider, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: var key string var resolvable bool @@ -323,6 +324,7 @@ func (rt *InvokeStreamRouter) InvokeModelWithResponseStream(ctx context.Context, // once, either explicitly here or via the slot-releasing source wrapper // on the success path. var selfHostRelease func() + vendor, isVendor := strings.CutPrefix(entry.Provider, providerPrefix) switch { case entry.Provider == tierSelfHost: // Family validation first: a model this platform cannot serve at @@ -336,8 +338,8 @@ func (rt *InvokeStreamRouter) InvokeModelWithResponseStream(ctx context.Context, if err != nil { return nil, err } - case strings.HasPrefix(entry.Provider, providerPrefix): - switch strings.TrimPrefix(entry.Provider, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: key, ok, err := rt.resolver.Resolve(ctx, accountID, vendorAnthropic) if err != nil { diff --git a/spinifex/gateway/bedrock/provider.go b/spinifex/gateway/bedrock/provider.go index 355c1892e..e8535415c 100644 --- a/spinifex/gateway/bedrock/provider.go +++ b/spinifex/gateway/bedrock/provider.go @@ -133,6 +133,7 @@ func (rt *Router) Converse(ctx context.Context, accountID, modelID string, input backend = entry.Provider var p Provider + vendor, isVendor := strings.CutPrefix(entry.Provider, providerPrefix) switch { case entry.Provider == tierSelfHost: var release func() @@ -146,8 +147,8 @@ func (rt *Router) Converse(ctx context.Context, accountID, modelID string, input } else { p = newVLLMProvider(rt.endpointResolver) } - case strings.HasPrefix(entry.Provider, providerPrefix): - switch strings.TrimPrefix(entry.Provider, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: var key string var resolvable bool @@ -267,6 +268,7 @@ func (rt *Router) ConverseStream(ctx context.Context, accountID, modelID string, // whether the eventual source gets slot-release wrapping and whether an // error return past this point must release the slot explicitly. var selfHostRelease func() + vendor, isVendor := strings.CutPrefix(entry.Provider, providerPrefix) switch { case entry.Provider == tierSelfHost: selfHostRelease, err = admitSelfHost(ctx, rt.provisioned, ptAccountID, modelID, entry) @@ -278,8 +280,8 @@ func (rt *Router) ConverseStream(ctx context.Context, accountID, modelID string, } else { p = newVLLMProvider(rt.endpointResolver) } - case strings.HasPrefix(entry.Provider, providerPrefix): - switch strings.TrimPrefix(entry.Provider, providerPrefix) { + case isVendor: + switch vendor { case vendorAnthropic: key, ok, err := rt.resolver.Resolve(ctx, accountID, vendorAnthropic) if err != nil { diff --git a/spinifex/gateway/bedrock/sse.go b/spinifex/gateway/bedrock/sse.go index 1e1061250..3313c273f 100644 --- a/spinifex/gateway/bedrock/sse.go +++ b/spinifex/gateway/bedrock/sse.go @@ -49,15 +49,15 @@ func (s *sseScanner) Next() (sseEvent, bool, error) { } continue // keepalive blank line between records } - switch { - case strings.HasPrefix(line, ":"): - // comment/keepalive, ignore — does not start a record - case strings.HasPrefix(line, "event:"): + if strings.HasPrefix(line, ":") { + continue // comment/keepalive, does not start a record + } + if v, ok := strings.CutPrefix(line, "event:"); ok { sawAny = true - event = strings.TrimSpace(strings.TrimPrefix(line, "event:")) - case strings.HasPrefix(line, "data:"): + event = strings.TrimSpace(v) + } else if v, ok := strings.CutPrefix(line, "data:"); ok { sawAny = true - dataLines = append(dataLines, strings.TrimPrefix(strings.TrimPrefix(line, "data:"), " ")) + dataLines = append(dataLines, strings.TrimPrefix(v, " ")) } } diff --git a/spinifex/gateway/ecr/authorization.go b/spinifex/gateway/ecr/authorization.go index fdab10e28..bcc5f1d8c 100644 --- a/spinifex/gateway/ecr/authorization.go +++ b/spinifex/gateway/ecr/authorization.go @@ -114,6 +114,7 @@ func classifyTagsOperation(method, name, ref string) (ClassifiedOperation, bool) // classifyBlobOperation mirrors Registry.routeBlobs' dispatch exactly, so // every method/ref combination it accepts has a matching authorization case. func classifyBlobOperation(method, name, ref string, query url.Values) (ClassifiedOperation, bool) { + uploadID, isUpload := strings.CutPrefix(ref, "uploads/") switch { case ref == "uploads/" || ref == "uploads": if method != http.MethodPost { @@ -134,8 +135,8 @@ func classifyBlobOperation(method, name, ref string, query url.Values) (Classifi op.Requirements = append(op.Requirements, ActionRequirement{ActionBatchCheckLayerAvailability, ScopeSource}) } return op, true - case strings.HasPrefix(ref, "uploads/"): - if strings.TrimPrefix(ref, "uploads/") == "" { + case isUpload: + if uploadID == "" { return ClassifiedOperation{}, false } switch method { diff --git a/spinifex/gateway/ecr/registry.go b/spinifex/gateway/ecr/registry.go index f1a4907b6..caf7141f2 100644 --- a/spinifex/gateway/ecr/registry.go +++ b/spinifex/gateway/ecr/registry.go @@ -164,14 +164,14 @@ func splitV2Path(path string) (name, kind, ref string, ok bool) { func (reg *Registry) routeBlobs(w http.ResponseWriter, r *http.Request, name, ref string) { ctx := r.Context() + uploadID, isUpload := strings.CutPrefix(ref, "uploads/") switch { case ref == "uploads/" || ref == "uploads": if r.Method == http.MethodPost { reg.startUpload(w, r, name) return } - case strings.HasPrefix(ref, "uploads/"): - uploadID := strings.TrimPrefix(ref, "uploads/") + case isUpload: switch r.Method { case http.MethodPatch: reg.patchUpload(w, r, name, uploadID) diff --git a/spinifex/gpu/iommu.go b/spinifex/gpu/iommu.go index 33f12c23d..296068a9a 100644 --- a/spinifex/gpu/iommu.go +++ b/spinifex/gpu/iommu.go @@ -50,8 +50,7 @@ func groupMembers(sysfsRoot string, groupNum int) ([]IOMMUGroupMember, error) { // bind/unbind lifecycle while the endpoint is claimed. func isBridgeClass(class string) bool { s := strings.ToLower(strings.TrimSpace(class)) - s = strings.TrimPrefix(s, "0x") - return len(s) >= 2 && s[:2] == "06" + return strings.HasPrefix(strings.TrimPrefix(s, "0x"), "06") } // filterBridgeMembers returns members with bridge-class devices removed. diff --git a/spinifex/handlers/ecs/scheduler.go b/spinifex/handlers/ecs/scheduler.go index de834f800..75064f237 100644 --- a/spinifex/handlers/ecs/scheduler.go +++ b/spinifex/handlers/ecs/scheduler.go @@ -434,8 +434,5 @@ func (sc *Scheduler) convergeInstanceRoles(ctx context.Context) error { // accountIDFromBucket extracts the account ID from an ECS per-account bucket name. func accountIDFromBucket(bucket string) (string, bool) { - if !strings.HasPrefix(bucket, KVBucketECSAccountPrefix) { - return "", false - } - return strings.TrimPrefix(bucket, KVBucketECSAccountPrefix), true + return strings.CutPrefix(bucket, KVBucketECSAccountPrefix) } diff --git a/spinifex/handlers/eks/addon_state.go b/spinifex/handlers/eks/addon_state.go index 0ee6a21a2..eebb9b18a 100644 --- a/spinifex/handlers/eks/addon_state.go +++ b/spinifex/handlers/eks/addon_state.go @@ -97,11 +97,12 @@ func ListAddonRecords(ctx context.Context, kv jetstream.KeyValue, cluster string prefix := AddonsPrefix(cluster) out := make([]*AddonRecord, 0) for _, k := range keys { - if !strings.HasPrefix(k, prefix) { + rest, ok := strings.CutPrefix(k, prefix) + if !ok { continue } // Skip sub-keys (e.g. staged manifest); record keys are one segment under the prefix. - if strings.Contains(strings.TrimPrefix(k, prefix), "/") { + if strings.Contains(rest, "/") { continue } entry, err := kv.Get(ctx, k) diff --git a/spinifex/handlers/eks/cp_vpc_test.go b/spinifex/handlers/eks/cp_vpc_test.go index 80cbe150b..70f20c971 100644 --- a/spinifex/handlers/eks/cp_vpc_test.go +++ b/spinifex/handlers/eks/cp_vpc_test.go @@ -53,10 +53,10 @@ func ec2TagsFromMap(m map[string]string) []*ec2.Tag { func cpvpcMatchTagFilters(tagMap map[string]string, filters []*ec2.Filter) bool { for _, f := range filters { name := aws.StringValue(f.Name) - if !strings.HasPrefix(name, "tag:") { + key, isTag := strings.CutPrefix(name, "tag:") + if !isTag { continue } - key := strings.TrimPrefix(name, "tag:") got, ok := tagMap[key] if !ok { return false diff --git a/spinifex/handlers/eks/restore_snapshot.go b/spinifex/handlers/eks/restore_snapshot.go index c292519d6..a82e276a8 100644 --- a/spinifex/handlers/eks/restore_snapshot.go +++ b/spinifex/handlers/eks/restore_snapshot.go @@ -63,10 +63,11 @@ type etcdSnapshotKey struct { // vary in length, so comparing the whole basename would not be. func parseEtcdSnapshotKey(basename string) (etcdSnapshotKey, bool) { const prefix, suffix = "etcd-", ".snap" - if !strings.HasPrefix(basename, prefix) || !strings.HasSuffix(basename, suffix) { + rest, prefixed := strings.CutPrefix(basename, prefix) + mid, suffixed := strings.CutSuffix(rest, suffix) + if !prefixed || !suffixed { return etcdSnapshotKey{}, false } - mid := strings.TrimSuffix(strings.TrimPrefix(basename, prefix), suffix) i := strings.LastIndex(mid, "-") if i <= 0 || i == len(mid)-1 { return etcdSnapshotKey{}, false diff --git a/spinifex/handlers/eks/service_impl.go b/spinifex/handlers/eks/service_impl.go index a4a154d45..029df672e 100644 --- a/spinifex/handlers/eks/service_impl.go +++ b/spinifex/handlers/eks/service_impl.go @@ -2503,10 +2503,11 @@ func listClusterNames(ctx context.Context, kv jetstream.KeyValue) ([]string, err names := make([]string, 0, len(keys)) seen := make(map[string]struct{}, len(keys)) for _, k := range keys { - if !strings.HasPrefix(k, "clusters/") || !strings.HasSuffix(k, suffix) { + rest, prefixed := strings.CutPrefix(k, "clusters/") + name, suffixed := strings.CutSuffix(rest, suffix) + if !prefixed || !suffixed { continue } - name := strings.TrimSuffix(strings.TrimPrefix(k, "clusters/"), suffix) if name == "" || strings.Contains(name, "/") { continue } diff --git a/spinifex/handlers/eks/token_webhook.go b/spinifex/handlers/eks/token_webhook.go index 8f4e483e0..aa2d8b756 100644 --- a/spinifex/handlers/eks/token_webhook.go +++ b/spinifex/handlers/eks/token_webhook.go @@ -42,11 +42,10 @@ type TokenVerifyResponse struct { // `k8s-aws-v1.` bearer token. The encoding is base64url without // padding, matching the aws-iam-authenticator / aws-cli get-token output. func DecodeGetToken(token string) (string, error) { - token = strings.TrimSpace(token) - if !strings.HasPrefix(token, getTokenV1Prefix) { + encoded, ok := strings.CutPrefix(strings.TrimSpace(token), getTokenV1Prefix) + if !ok { return "", ErrMalformedToken } - encoded := strings.TrimPrefix(token, getTokenV1Prefix) raw, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil { return "", ErrMalformedToken diff --git a/spinifex/handlers/imds/action.go b/spinifex/handlers/imds/action.go index c851ad051..c8814d612 100644 --- a/spinifex/handlers/imds/action.go +++ b/spinifex/handlers/imds/action.go @@ -81,8 +81,8 @@ func imdsAction(method, path string) string { return "imds.public-keys" } - if strings.HasPrefix(path, prefixSecurityCreds) { - if len(path) > len(prefixSecurityCreds) { + if role, ok := strings.CutPrefix(path, prefixSecurityCreds); ok { + if role != "" { return "imds.iam.security-credentials.role" } return "imds.iam.security-credentials.list" diff --git a/spinifex/handlers/imds/metadata.go b/spinifex/handlers/imds/metadata.go index 7fdddeab9..b35160867 100644 --- a/spinifex/handlers/imds/metadata.go +++ b/spinifex/handlers/imds/metadata.go @@ -157,8 +157,8 @@ func (s *IMDSServiceImpl) dispatch(w http.ResponseWriter, r *http.Request, eni * slog.InfoContext(ctx, "IMDS: serving metadata request", "path", path, "instance_id", eni.instanceID, "private_ip", eni.privateIP, "public_ip", eni.publicIP) - if strings.HasPrefix(path, prefixSecurityCreds) && len(path) > len(prefixSecurityCreds) { - s.serveRoleCredentials(ctx, w, eni, strings.TrimPrefix(path, prefixSecurityCreds)) + if role, ok := strings.CutPrefix(path, prefixSecurityCreds); ok && role != "" { + s.serveRoleCredentials(ctx, w, eni, role) return } diff --git a/spinifex/handlers/imds/resolver.go b/spinifex/handlers/imds/resolver.go index 915646873..957afbaba 100644 --- a/spinifex/handlers/imds/resolver.go +++ b/spinifex/handlers/imds/resolver.go @@ -132,7 +132,8 @@ func (r *metadataResolver) findENIByID(ctx context.Context, eniID string) (strin suffix := "." + eniID for _, key := range keys { - if !strings.HasSuffix(key, suffix) { + accountID, ok := strings.CutSuffix(key, suffix) + if !ok { continue } entry, err := r.eniKV.Get(ctx, key) @@ -142,7 +143,7 @@ func (r *metadataResolver) findENIByID(ctx context.Context, eniID string) (strin } return "", nil, fmt.Errorf("get eni record %s: %w", key, err) } - return strings.TrimSuffix(key, suffix), entry.Value(), nil + return accountID, entry.Value(), nil } return "", nil, nil } diff --git a/spinifex/handlers/ochrevector/hostport_test.go b/spinifex/handlers/ochrevector/hostport_test.go index 2610bb7ff..cb4bdbbb9 100644 --- a/spinifex/handlers/ochrevector/hostport_test.go +++ b/spinifex/handlers/ochrevector/hostport_test.go @@ -168,13 +168,14 @@ func (f *fakeVPC) DescribeNetworkInterfaces(_ context.Context, in *ec2.DescribeN for _, filter := range in.Filters { name := aws.StringValue(filter.Name) var got string + tagKey, isTag := strings.CutPrefix(name, "tag:") switch { case name == "subnet-id": got = aws.StringValue(ni.SubnetId) case name == "description": got = aws.StringValue(ni.Description) - case strings.HasPrefix(name, "tag:"): - got = tagValue(ni.TagSet, strings.TrimPrefix(name, "tag:")) + case isTag: + got = tagValue(ni.TagSet, tagKey) default: return nil, fmt.Errorf("fakeVPC: unsupported ENI filter %q", name) } diff --git a/spinifex/handlers/rds/store.go b/spinifex/handlers/rds/store.go index 1fdd11801..9ab3ce773 100644 --- a/spinifex/handlers/rds/store.go +++ b/spinifex/handlers/rds/store.go @@ -306,10 +306,11 @@ func ListDBParameterGroupNames(ctx context.Context, kv jetstream.KeyValue) ([]st prefix := DBParameterGroupsPrefix() names := make([]string, 0, len(keys)) for _, key := range keys { - if !strings.HasPrefix(key, prefix) || !strings.HasSuffix(key, "/meta") { + rest, prefixed := strings.CutPrefix(key, prefix) + name, suffixed := strings.CutSuffix(rest, "/meta") + if !prefixed || !suffixed { continue } - name := strings.TrimSuffix(strings.TrimPrefix(key, prefix), "/meta") if name == "" || strings.Contains(name, "/") { continue } @@ -388,10 +389,10 @@ func listNames(ctx context.Context, kv jetstream.KeyValue, prefix string) ([]str } names := make([]string, 0, len(keys)) for _, key := range keys { - if !strings.HasPrefix(key, prefix) { + name, ok := strings.CutPrefix(key, prefix) + if !ok { continue } - name := strings.TrimPrefix(key, prefix) if name == "" || strings.Contains(name, "/") { continue } diff --git a/spinifex/instancetypes/generate.go b/spinifex/instancetypes/generate.go index facfcc6a4..1efa883c9 100644 --- a/spinifex/instancetypes/generate.go +++ b/spinifex/instancetypes/generate.go @@ -226,19 +226,23 @@ type MIGProfileSpec struct { MemoryMiB int64 } +// migTypePrefix marks an instance type name as a MIG profile type. +const migTypePrefix = "mig." + // IsMIGType reports whether the instance type name is a MIG profile type // (i.e. was produced by GenerateMIGTypes). func IsMIGType(instanceType string) bool { - return strings.HasPrefix(instanceType, "mig.") + return strings.HasPrefix(instanceType, migTypePrefix) } // MIGProfileFromType extracts the nvidia-smi profile name from a MIG instance // type name (e.g. "mig.1g.10gb" → "1g.10gb"). Returns "" for non-MIG types. func MIGProfileFromType(instanceType string) string { - if !IsMIGType(instanceType) { + profile, ok := strings.CutPrefix(instanceType, migTypePrefix) + if !ok { return "" } - return strings.TrimPrefix(instanceType, "mig.") + return profile } // GenerateMIGTypes returns one InstanceTypeInfo per unique MIG profile. Instance @@ -247,7 +251,7 @@ func MIGProfileFromType(instanceType string) string { func GenerateMIGTypes(profiles []MIGProfileSpec, arch string) map[string]*ec2.InstanceTypeInfo { types := make(map[string]*ec2.InstanceTypeInfo) for _, p := range profiles { - name := "mig." + p.Name + name := migTypePrefix + p.Name if _, exists := types[name]; exists { continue } diff --git a/spinifex/network/host/imds_reconcile.go b/spinifex/network/host/imds_reconcile.go index bed6ce501..0123c6c2f 100644 --- a/spinifex/network/host/imds_reconcile.go +++ b/spinifex/network/host/imds_reconcile.go @@ -48,11 +48,11 @@ func ListIMDSTaps(ctx context.Context, r Runner) ([]IMDSTapEndpoint, error) { // a port missing it can never be a member of the live set either way, and one // structurally broken port must not stall every reconcile pass. ifaceID, ok := row.externalIDs["iface-id"] - if !ok || !strings.HasPrefix(ifaceID, ovsIfaceIDPrefix) { + eniID, prefixed := strings.CutPrefix(ifaceID, ovsIfaceIDPrefix) + if !ok || !prefixed { slog.Warn("IMDS: skipping IMDS patch port with unexpected iface-id", "port", row.name, "iface_id", ifaceID) continue } - eniID := strings.TrimPrefix(ifaceID, ovsIfaceIDPrefix) taps = append(taps, IMDSTapEndpoint{ENIID: eniID, Endpoint: IMDSEndpointName(eniID)}) } return taps, nil diff --git a/spinifex/services/viperblockd/nbdkit_recovery.go b/spinifex/services/viperblockd/nbdkit_recovery.go index ac8d0134c..06877c8c5 100644 --- a/spinifex/services/viperblockd/nbdkit_recovery.go +++ b/spinifex/services/viperblockd/nbdkit_recovery.go @@ -87,6 +87,8 @@ func parseNbdkitCmdline(cmdline []byte) (disc discoveredNbdkit, ok bool) { args := bytes.Split(bytes.TrimRight(cmdline, "\x00"), []byte{0}) for i := 0; i < len(args); i++ { arg := string(args[i]) + volume, isVolume := strings.CutPrefix(arg, "volume=") + baseDir, isBaseDir := strings.CutPrefix(arg, "base_dir=") switch { case arg == "--unix" && i+1 < len(args): disc.Socket = string(args[i+1]) @@ -96,10 +98,10 @@ func parseNbdkitCmdline(cmdline []byte) (disc discoveredNbdkit, ok bool) { disc.Port = port } i++ - case strings.HasPrefix(arg, "volume="): - disc.Volume = strings.TrimPrefix(arg, "volume=") - case strings.HasPrefix(arg, "base_dir="): - disc.BaseDir = strings.TrimPrefix(arg, "base_dir=") + case isVolume: + disc.Volume = volume + case isBaseDir: + disc.BaseDir = baseDir case strings.HasSuffix(arg, ".so"): disc.Plugin = arg } diff --git a/spinifex/utils/images.go b/spinifex/utils/images.go index 31c6e50e5..67cdd89bb 100644 --- a/spinifex/utils/images.go +++ b/spinifex/utils/images.go @@ -194,10 +194,11 @@ func parseSumsFile(body []byte, filename string) (string, error) { } case 4: // BSD-style: " () = ". The "=" guard rejects PGP armor lines that happen to tokenise to 4 fields. - if fields[2] != "=" || !strings.HasPrefix(fields[1], "(") || !strings.HasSuffix(fields[1], ")") { + inner, open := strings.CutPrefix(fields[1], "(") + name, closed := strings.CutSuffix(inner, ")") + if fields[2] != "=" || !open || !closed { continue } - name := strings.TrimSuffix(strings.TrimPrefix(fields[1], "("), ")") if name == filename { return fields[3], nil } diff --git a/spinifex/vm/orphan_scan.go b/spinifex/vm/orphan_scan.go index 604576298..6545410cb 100644 --- a/spinifex/vm/orphan_scan.go +++ b/spinifex/vm/orphan_scan.go @@ -56,11 +56,10 @@ func pidFileOwners(runtimeDir string) map[int]string { return owners } for _, entry := range entries { - name := entry.Name() - if !strings.HasSuffix(name, ".pid") { + id, ok := strings.CutSuffix(entry.Name(), ".pid") + if !ok { continue } - id := strings.TrimSuffix(name, ".pid") if pid, err := utils.ReadPidFileFrom(runtimeDir, id); err == nil { owners[pid] = id } diff --git a/tests/e2e/select-suites/selector.go b/tests/e2e/select-suites/selector.go index 815086236..543b24144 100644 --- a/tests/e2e/select-suites/selector.go +++ b/tests/e2e/select-suites/selector.go @@ -285,10 +285,8 @@ func subjectsOverlap(a, b []string) bool { // matches anything sharing its prefix; two wildcards match if one prefix // contains the other. func subjectMatch(x, y string) bool { - xw := strings.HasSuffix(x, ".*") - yw := strings.HasSuffix(y, ".*") - xp := strings.TrimSuffix(x, ".*") - yp := strings.TrimSuffix(y, ".*") + xp, xw := strings.CutSuffix(x, ".*") + yp, yw := strings.CutSuffix(y, ".*") switch { case xw && yw: return xp == yp || strings.HasPrefix(xp, yp+".") || strings.HasPrefix(yp, xp+".") diff --git a/tests/e2e/single/guestchurn_test.go b/tests/e2e/single/guestchurn_test.go index 39b57199b..ae2a62647 100644 --- a/tests/e2e/single/guestchurn_test.go +++ b/tests/e2e/single/guestchurn_test.go @@ -389,10 +389,11 @@ func runGuestChurnRound(t *testing.T, fix *Fixture, instanceID, origType, keyPat // Bash strips the ".nano" suffix and appends ".small" — same family, // more RAM at matching vCPU. Avoids xlarge (16 GiB) which the CI host // can't satisfy. - if !strings.HasSuffix(origType, ".nano") { + family, ok := strings.CutSuffix(origType, ".nano") + if !ok { t.Fatalf("phase7b: expected discovered instance type to end with .nano, got %q", origType) } - modifyType := strings.TrimSuffix(origType, ".nano") + ".small" + modifyType := family + ".small" harness.Detail(t, "from_type", origType, "to_type", modifyType) // Stop the instance first — ModifyInstanceAttribute on a running