2023-03-28 18:39:22 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
2023-08-11 13:12:13 +00:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-03-28 18:39:22 +00:00
|
|
|
|
2014-01-10 23:13:37 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
2020-06-09 16:44:31 +00:00
|
|
|
"net/url"
|
2017-06-09 18:36:00 +00:00
|
|
|
"strconv"
|
2022-06-01 17:17:14 +00:00
|
|
|
"strings"
|
2016-04-23 23:01:59 +00:00
|
|
|
|
2017-07-06 10:34:00 +00:00
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
2017-04-19 23:00:11 +00:00
|
|
|
"github.com/hashicorp/consul/api"
|
2024-02-12 19:27:25 +00:00
|
|
|
"github.com/hashicorp/consul/internal/dnsutil"
|
2014-01-10 23:13:37 +00:00
|
|
|
)
|
|
|
|
|
2020-06-10 18:07:15 +00:00
|
|
|
const (
|
|
|
|
serviceHealth = "service"
|
|
|
|
connectHealth = "connect"
|
|
|
|
ingressHealth = "ingress"
|
|
|
|
)
|
|
|
|
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) HealthChecksInState(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2014-01-10 23:13:37 +00:00
|
|
|
// Set default DC
|
2014-02-05 22:36:13 +00:00
|
|
|
args := structs.ChecksInStateRequest{}
|
2019-12-10 02:26:41 +00:00
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-07-27 21:41:46 +00:00
|
|
|
s.parseSource(req, &args.Source)
|
2017-01-14 01:08:43 +00:00
|
|
|
args.NodeMetaFilters = s.parseMetaFilter(req)
|
2014-04-21 19:25:36 +00:00
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
2014-04-21 19:40:11 +00:00
|
|
|
return nil, nil
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Pull out the service name
|
2022-06-01 17:17:14 +00:00
|
|
|
args.State = strings.TrimPrefix(req.URL.Path, "/v1/health/state/")
|
2014-01-10 23:13:37 +00:00
|
|
|
if args.State == "" {
|
2022-04-29 17:42:49 +00:00
|
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing check state"}
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make the RPC request
|
2014-02-05 22:36:13 +00:00
|
|
|
var out structs.IndexedHealthChecks
|
2014-04-21 19:40:11 +00:00
|
|
|
defer setMeta(resp, &out.QueryMeta)
|
2018-03-30 15:14:44 +00:00
|
|
|
RETRY_ONCE:
|
2022-12-14 15:24:22 +00:00
|
|
|
if err := s.agent.RPC(req.Context(), "Health.ChecksInState", &args, &out); err != nil {
|
2014-04-21 19:40:11 +00:00
|
|
|
return nil, err
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
2018-03-30 15:14:44 +00:00
|
|
|
if args.QueryOptions.AllowStale && args.MaxStaleDuration > 0 && args.MaxStaleDuration < out.LastContact {
|
|
|
|
args.AllowStale = false
|
|
|
|
args.MaxStaleDuration = 0
|
|
|
|
goto RETRY_ONCE
|
|
|
|
}
|
|
|
|
out.ConsistencyLevel = args.QueryOptions.ConsistencyLevel()
|
2015-11-15 05:05:37 +00:00
|
|
|
|
|
|
|
// Use empty list instead of nil
|
|
|
|
if out.HealthChecks == nil {
|
|
|
|
out.HealthChecks = make(structs.HealthChecks, 0)
|
|
|
|
}
|
2018-02-07 04:35:55 +00:00
|
|
|
for i, c := range out.HealthChecks {
|
2017-04-28 01:22:07 +00:00
|
|
|
if c.ServiceTags == nil {
|
2018-02-07 04:35:55 +00:00
|
|
|
clone := *c
|
|
|
|
clone.ServiceTags = make([]string, 0)
|
|
|
|
out.HealthChecks[i] = &clone
|
2017-04-28 01:22:07 +00:00
|
|
|
}
|
|
|
|
}
|
2014-04-21 19:40:11 +00:00
|
|
|
return out.HealthChecks, nil
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) HealthNodeChecks(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2014-01-10 23:13:37 +00:00
|
|
|
// Set default DC
|
2014-02-05 22:36:13 +00:00
|
|
|
args := structs.NodeSpecificRequest{}
|
2019-12-10 02:26:41 +00:00
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2014-04-21 19:25:36 +00:00
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
2014-04-21 19:40:11 +00:00
|
|
|
return nil, nil
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Pull out the service name
|
2022-06-01 17:17:14 +00:00
|
|
|
args.Node = strings.TrimPrefix(req.URL.Path, "/v1/health/node/")
|
2014-01-10 23:13:37 +00:00
|
|
|
if args.Node == "" {
|
2022-04-29 17:42:49 +00:00
|
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing node name"}
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make the RPC request
|
2014-02-05 22:36:13 +00:00
|
|
|
var out structs.IndexedHealthChecks
|
2014-04-21 19:40:11 +00:00
|
|
|
defer setMeta(resp, &out.QueryMeta)
|
2018-03-30 15:14:44 +00:00
|
|
|
RETRY_ONCE:
|
2022-12-14 15:24:22 +00:00
|
|
|
if err := s.agent.RPC(req.Context(), "Health.NodeChecks", &args, &out); err != nil {
|
2014-04-21 19:40:11 +00:00
|
|
|
return nil, err
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
2018-03-30 15:14:44 +00:00
|
|
|
if args.QueryOptions.AllowStale && args.MaxStaleDuration > 0 && args.MaxStaleDuration < out.LastContact {
|
|
|
|
args.AllowStale = false
|
|
|
|
args.MaxStaleDuration = 0
|
|
|
|
goto RETRY_ONCE
|
|
|
|
}
|
|
|
|
out.ConsistencyLevel = args.QueryOptions.ConsistencyLevel()
|
2015-11-15 05:05:37 +00:00
|
|
|
|
|
|
|
// Use empty list instead of nil
|
|
|
|
if out.HealthChecks == nil {
|
|
|
|
out.HealthChecks = make(structs.HealthChecks, 0)
|
|
|
|
}
|
2018-02-07 04:35:55 +00:00
|
|
|
for i, c := range out.HealthChecks {
|
2017-04-28 01:22:07 +00:00
|
|
|
if c.ServiceTags == nil {
|
2018-02-07 04:35:55 +00:00
|
|
|
clone := *c
|
|
|
|
clone.ServiceTags = make([]string, 0)
|
|
|
|
out.HealthChecks[i] = &clone
|
2017-04-28 01:22:07 +00:00
|
|
|
}
|
|
|
|
}
|
2014-04-21 19:40:11 +00:00
|
|
|
return out.HealthChecks, nil
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) HealthServiceChecks(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2014-01-10 23:13:37 +00:00
|
|
|
// Set default DC
|
2014-02-05 22:36:13 +00:00
|
|
|
args := structs.ServiceSpecificRequest{}
|
2020-02-10 15:40:44 +00:00
|
|
|
if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil {
|
2019-12-10 02:26:41 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2015-07-27 21:41:46 +00:00
|
|
|
s.parseSource(req, &args.Source)
|
2017-01-14 01:08:43 +00:00
|
|
|
args.NodeMetaFilters = s.parseMetaFilter(req)
|
2014-04-21 19:25:36 +00:00
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
2014-04-21 19:40:11 +00:00
|
|
|
return nil, nil
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Pull out the service name
|
2022-06-01 17:17:14 +00:00
|
|
|
args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/health/checks/")
|
2014-01-10 23:13:37 +00:00
|
|
|
if args.ServiceName == "" {
|
2022-04-29 17:42:49 +00:00
|
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing service name"}
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make the RPC request
|
2014-02-05 22:36:13 +00:00
|
|
|
var out structs.IndexedHealthChecks
|
2014-04-21 19:40:11 +00:00
|
|
|
defer setMeta(resp, &out.QueryMeta)
|
2018-03-30 15:14:44 +00:00
|
|
|
RETRY_ONCE:
|
2022-12-14 15:24:22 +00:00
|
|
|
if err := s.agent.RPC(req.Context(), "Health.ServiceChecks", &args, &out); err != nil {
|
2014-04-21 19:40:11 +00:00
|
|
|
return nil, err
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
2018-03-30 15:14:44 +00:00
|
|
|
if args.QueryOptions.AllowStale && args.MaxStaleDuration > 0 && args.MaxStaleDuration < out.LastContact {
|
|
|
|
args.AllowStale = false
|
|
|
|
args.MaxStaleDuration = 0
|
|
|
|
goto RETRY_ONCE
|
|
|
|
}
|
|
|
|
out.ConsistencyLevel = args.QueryOptions.ConsistencyLevel()
|
2015-11-15 05:05:37 +00:00
|
|
|
|
|
|
|
// Use empty list instead of nil
|
|
|
|
if out.HealthChecks == nil {
|
|
|
|
out.HealthChecks = make(structs.HealthChecks, 0)
|
|
|
|
}
|
2018-02-07 04:35:55 +00:00
|
|
|
for i, c := range out.HealthChecks {
|
2017-04-28 01:22:07 +00:00
|
|
|
if c.ServiceTags == nil {
|
2018-02-07 04:35:55 +00:00
|
|
|
clone := *c
|
|
|
|
clone.ServiceTags = make([]string, 0)
|
|
|
|
out.HealthChecks[i] = &clone
|
2017-04-28 01:22:07 +00:00
|
|
|
}
|
|
|
|
}
|
2014-04-21 19:40:11 +00:00
|
|
|
return out.HealthChecks, nil
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 18:07:15 +00:00
|
|
|
// HealthIngressServiceNodes should return "all the healthy ingress gateway instances
|
|
|
|
// that I can use to access this connect-enabled service without mTLS".
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) HealthIngressServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2020-06-10 18:07:15 +00:00
|
|
|
return s.healthServiceNodes(resp, req, ingressHealth)
|
|
|
|
}
|
|
|
|
|
|
|
|
// HealthConnectServiceNodes should return "all healthy connect-enabled
|
|
|
|
// endpoints (e.g. could be side car proxies or native instances) for this
|
|
|
|
// service so I can connect with mTLS".
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) HealthConnectServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2020-06-10 18:07:15 +00:00
|
|
|
return s.healthServiceNodes(resp, req, connectHealth)
|
2018-03-09 17:52:32 +00:00
|
|
|
}
|
|
|
|
|
2020-06-10 18:07:15 +00:00
|
|
|
// HealthServiceNodes should return "all the healthy instances of this service
|
|
|
|
// registered so I can connect directly to them".
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) HealthServiceNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2020-06-10 18:07:15 +00:00
|
|
|
return s.healthServiceNodes(resp, req, serviceHealth)
|
2018-03-09 17:52:32 +00:00
|
|
|
}
|
|
|
|
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) healthServiceNodes(resp http.ResponseWriter, req *http.Request, healthType string) (interface{}, error) {
|
2014-01-10 23:13:37 +00:00
|
|
|
// Set default DC
|
2020-06-09 19:45:21 +00:00
|
|
|
args := structs.ServiceSpecificRequest{}
|
2020-02-10 15:40:44 +00:00
|
|
|
if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil {
|
2019-12-10 02:26:41 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2015-07-27 21:41:46 +00:00
|
|
|
s.parseSource(req, &args.Source)
|
2017-01-14 01:08:43 +00:00
|
|
|
args.NodeMetaFilters = s.parseMetaFilter(req)
|
2014-04-21 19:25:36 +00:00
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
2014-04-21 19:40:11 +00:00
|
|
|
return nil, nil
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
peering: initial sync (#12842)
- Add endpoints related to peering: read, list, generate token, initiate peering
- Update node/service/check table indexing to account for peers
- Foundational changes for pushing service updates to a peer
- Plumb peer name through Health.ServiceNodes path
see: ENT-1765, ENT-1280, ENT-1283, ENT-1283, ENT-1756, ENT-1739, ENT-1750, ENT-1679,
ENT-1709, ENT-1704, ENT-1690, ENT-1689, ENT-1702, ENT-1701, ENT-1683, ENT-1663,
ENT-1650, ENT-1678, ENT-1628, ENT-1658, ENT-1640, ENT-1637, ENT-1597, ENT-1634,
ENT-1613, ENT-1616, ENT-1617, ENT-1591, ENT-1588, ENT-1596, ENT-1572, ENT-1555
Co-authored-by: R.B. Boyer <rb@hashicorp.com>
Co-authored-by: freddygv <freddy@hashicorp.com>
Co-authored-by: Chris S. Kim <ckim@hashicorp.com>
Co-authored-by: Evan Culver <eculver@hashicorp.com>
Co-authored-by: Nitya Dhanushkodi <nitya@hashicorp.com>
2022-04-21 22:34:40 +00:00
|
|
|
s.parsePeerName(req, &args)
|
|
|
|
|
2018-10-11 11:50:05 +00:00
|
|
|
// Check for tags
|
2014-02-05 22:36:13 +00:00
|
|
|
params := req.URL.Query()
|
2014-01-10 23:13:37 +00:00
|
|
|
if _, ok := params["tag"]; ok {
|
2018-10-11 11:50:05 +00:00
|
|
|
args.ServiceTags = params["tag"]
|
2014-01-10 23:13:37 +00:00
|
|
|
args.TagFilter = true
|
|
|
|
}
|
|
|
|
|
2022-05-25 20:20:17 +00:00
|
|
|
if _, ok := params["merge-central-config"]; ok {
|
|
|
|
args.MergeCentralConfig = true
|
|
|
|
}
|
|
|
|
|
2018-03-09 17:52:32 +00:00
|
|
|
// Determine the prefix
|
2020-06-10 18:07:15 +00:00
|
|
|
var prefix string
|
|
|
|
switch healthType {
|
|
|
|
case connectHealth:
|
2018-03-09 17:52:32 +00:00
|
|
|
prefix = "/v1/health/connect/"
|
2020-06-10 18:07:15 +00:00
|
|
|
args.Connect = true
|
|
|
|
case ingressHealth:
|
|
|
|
prefix = "/v1/health/ingress/"
|
|
|
|
args.Ingress = true
|
|
|
|
default:
|
|
|
|
// serviceHealth is the default type
|
|
|
|
prefix = "/v1/health/service/"
|
2020-06-09 16:44:31 +00:00
|
|
|
}
|
|
|
|
|
2014-01-10 23:13:37 +00:00
|
|
|
// Pull out the service name
|
2022-06-01 17:17:14 +00:00
|
|
|
args.ServiceName = strings.TrimPrefix(req.URL.Path, prefix)
|
2014-01-10 23:13:37 +00:00
|
|
|
if args.ServiceName == "" {
|
2022-04-29 17:42:49 +00:00
|
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing service name"}
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
|
|
|
|
2020-09-30 21:38:13 +00:00
|
|
|
out, md, err := s.agent.rpcClientHealth.ServiceNodes(req.Context(), args)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if args.QueryOptions.UseCache {
|
|
|
|
setCacheMeta(resp, &md)
|
2018-03-30 15:14:44 +00:00
|
|
|
}
|
2021-05-05 18:16:24 +00:00
|
|
|
out.QueryMeta.ConsistencyLevel = args.QueryOptions.ConsistencyLevel()
|
2020-09-30 21:38:13 +00:00
|
|
|
setMeta(resp, &out.QueryMeta)
|
2014-04-21 23:08:26 +00:00
|
|
|
|
2020-09-30 21:38:13 +00:00
|
|
|
// FIXME: argument parsing should be done before performing the rpc
|
2014-04-21 23:08:26 +00:00
|
|
|
// Filter to only passing if specified
|
2020-06-09 16:44:31 +00:00
|
|
|
filter, err := getBoolQueryParam(params, api.HealthPassing)
|
|
|
|
if err != nil {
|
2022-04-29 17:42:49 +00:00
|
|
|
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Invalid value for ?passing"}
|
2020-06-09 16:44:31 +00:00
|
|
|
}
|
2017-06-09 18:55:04 +00:00
|
|
|
|
2020-09-30 21:38:13 +00:00
|
|
|
// FIXME: remove filterNonPassing, replace with nodes.Filter, which is used by DNSServer
|
2020-06-09 16:44:31 +00:00
|
|
|
if filter {
|
|
|
|
out.Nodes = filterNonPassing(out.Nodes)
|
2014-04-21 23:08:26 +00:00
|
|
|
}
|
2015-11-15 05:05:37 +00:00
|
|
|
|
2016-08-15 22:05:02 +00:00
|
|
|
// Translate addresses after filtering so we don't waste effort.
|
2024-02-12 19:27:25 +00:00
|
|
|
s.agent.TranslateAddresses(args.Datacenter, out.Nodes, dnsutil.TranslateAddressAcceptAny)
|
2016-08-15 22:05:02 +00:00
|
|
|
|
2015-11-15 05:05:37 +00:00
|
|
|
// Use empty list instead of nil
|
2017-04-28 01:22:07 +00:00
|
|
|
if out.Nodes == nil {
|
|
|
|
out.Nodes = make(structs.CheckServiceNodes, 0)
|
|
|
|
}
|
2017-04-20 18:42:22 +00:00
|
|
|
for i := range out.Nodes {
|
2015-11-15 05:05:37 +00:00
|
|
|
if out.Nodes[i].Checks == nil {
|
|
|
|
out.Nodes[i].Checks = make(structs.HealthChecks, 0)
|
|
|
|
}
|
2018-02-07 04:35:55 +00:00
|
|
|
for j, c := range out.Nodes[i].Checks {
|
2017-04-28 01:22:07 +00:00
|
|
|
if c.ServiceTags == nil {
|
2018-02-07 04:35:55 +00:00
|
|
|
clone := *c
|
|
|
|
clone.ServiceTags = make([]string, 0)
|
|
|
|
out.Nodes[i].Checks[j] = &clone
|
2017-04-28 01:22:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if out.Nodes[i].Service != nil && out.Nodes[i].Service.Tags == nil {
|
2018-02-07 04:35:55 +00:00
|
|
|
clone := *out.Nodes[i].Service
|
|
|
|
clone.Tags = make([]string, 0)
|
|
|
|
out.Nodes[i].Service = &clone
|
2017-04-28 01:22:07 +00:00
|
|
|
}
|
2015-11-15 05:05:37 +00:00
|
|
|
}
|
2014-04-21 19:40:11 +00:00
|
|
|
return out.Nodes, nil
|
2014-01-10 23:13:37 +00:00
|
|
|
}
|
2014-04-21 23:08:26 +00:00
|
|
|
|
2020-06-09 16:44:31 +00:00
|
|
|
func getBoolQueryParam(params url.Values, key string) (bool, error) {
|
|
|
|
var param bool
|
|
|
|
if _, ok := params[key]; ok {
|
|
|
|
val := params.Get(key)
|
|
|
|
// Orginally a comment declared this check should be removed after Consul
|
|
|
|
// 0.10, to no longer support using ?passing without a value. However, I
|
|
|
|
// think this is a reasonable experience for a user and so am keeping it
|
|
|
|
// here.
|
|
|
|
if val == "" {
|
|
|
|
param = true
|
|
|
|
} else {
|
|
|
|
var err error
|
|
|
|
param, err = strconv.ParseBool(val)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return param, nil
|
|
|
|
}
|
|
|
|
|
2014-04-21 23:08:26 +00:00
|
|
|
// filterNonPassing is used to filter out any nodes that have check that are not passing
|
|
|
|
func filterNonPassing(nodes structs.CheckServiceNodes) structs.CheckServiceNodes {
|
|
|
|
n := len(nodes)
|
2019-04-16 16:29:34 +00:00
|
|
|
|
|
|
|
// Make a copy of the cached nodes rather than operating on the cache directly
|
|
|
|
out := append(nodes[:0:0], nodes...)
|
|
|
|
|
2014-07-05 16:49:10 +00:00
|
|
|
OUTER:
|
2014-04-21 23:08:26 +00:00
|
|
|
for i := 0; i < n; i++ {
|
2019-04-16 16:29:34 +00:00
|
|
|
node := out[i]
|
2014-04-21 23:08:26 +00:00
|
|
|
for _, check := range node.Checks {
|
2017-04-19 23:00:11 +00:00
|
|
|
if check.Status != api.HealthPassing {
|
2019-04-16 16:29:34 +00:00
|
|
|
out[i], out[n-1] = out[n-1], structs.CheckServiceNode{}
|
2014-04-21 23:08:26 +00:00
|
|
|
n--
|
|
|
|
i--
|
2014-07-05 16:49:10 +00:00
|
|
|
continue OUTER
|
2014-04-21 23:08:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-04-16 16:29:34 +00:00
|
|
|
return out[:n]
|
2014-04-21 23:08:26 +00:00
|
|
|
}
|