Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## v1.1.5 - 04 Jun 2026

* feat(ensure-networks): Add API endpoint and CLI command to trigger it manually without restartng the service (19 minutes ago)
* feat(control-loop/ensure-networks): Add notion of control loop, ensure every 30 minutes (34 minutes ago)
* fix: fix overlay neighbor replay for inactive endpoints

## v1.1.4 - 20 Mar 2026
Expand Down
4 changes: 3 additions & 1 deletion client/sand/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
"net/http"
"time"

"github.com/pkg/errors"

"github.com/Scalingo/sand/api/httpresp"
"github.com/Scalingo/sand/api/params"
"github.com/Scalingo/sand/api/types"
apptls "github.com/Scalingo/sand/utils/tls"
"github.com/pkg/errors"

"crypto/tls"
)
Expand All @@ -20,6 +21,7 @@
NetworksList(context.Context) ([]types.Network, error)
NetworkCreate(context.Context, params.NetworkCreate) (types.Network, error)
NetworkShow(context.Context, string) (types.Network, error)
NodeEnsureNetworkEndpoints(context.Context) error

Check failure on line 24 in client/sand/client.go

View workflow job for this annotation

GitHub Actions / Linter on a PR

interface method NodeEnsureNetworkEndpoints must have named param for type context.Context (inamedparam)
NetworkConnect(context.Context, string, params.NetworkConnect) (net.Conn, error)
NetworkDelete(context.Context, string) error
EndpointCreate(context.Context, params.EndpointCreate) (types.Endpoint, error)
Expand Down
37 changes: 37 additions & 0 deletions client/sand/node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package sand

import (
"context"
"encoding/json"
"fmt"
"net/http"

"github.com/pkg/errors"

"github.com/Scalingo/sand/api/httpresp"
)

func (c *client) NodeEnsureNetworkEndpoints(ctx context.Context) error {
req, err := http.NewRequest("POST", fmt.Sprintf("%s/node/ensure-network-endpoints", c.url), nil)

Check failure on line 15 in client/sand/node.go

View workflow job for this annotation

GitHub Actions / Linter on a PR

string-format: fmt.Sprintf can be replaced with string concatenation (perfsprint)
if err != nil {
return errors.Wrapf(err, "failed to create HTTP request")
}
req = req.WithContext(ctx)
res, err := c.httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "failed to execute POST /node/ensure-network-endpoints")
}
defer res.Body.Close()

if res.StatusCode != http.StatusNoContent {
var reserr httpresp.Error
err := json.NewDecoder(res.Body).Decode(&reserr)
if err != nil {
return errors.Wrapf(err, "failed to decode JSON in errors response: %s", res.Status)
}

return reserr
}

return nil
}
4 changes: 4 additions & 0 deletions cmd/sand-agent-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func main() {
Name: "version",
Action: app.Version,
},
{
Name: "node-ensure-network-endpoints",
Action: app.NodeEnsureNetworkEndpoints,
},
{
Name: "network-create",
Action: app.NetworkCreate,
Expand Down
23 changes: 23 additions & 0 deletions cmd/sand-agent-cli/node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"context"
"fmt"

"github.com/urfave/cli"
)

func (a *App) NodeEnsureNetworkEndpoints(c *cli.Context) error {
client, err := a.sandClient(c)
if err != nil {
return err
}

err = client.NodeEnsureNetworkEndpoints(context.Background())
if err != nil {
return err
}
Comment on lines +12 to +19

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: wrap the errors


fmt.Println("Network endpoints ensure triggered")
return nil
}
91 changes: 23 additions & 68 deletions cmd/sand-agent/sand-agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@ import (
"path/filepath"

"github.com/moby/moby/pkg/reexec"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/Scalingo/go-etcd-lock/v5/lock"
"github.com/Scalingo/go-handlers"
dockeripam "github.com/Scalingo/go-plugins-helpers/ipam"
dockernetwork "github.com/Scalingo/go-plugins-helpers/network"
dockersdk "github.com/Scalingo/go-plugins-helpers/sdk"
"github.com/Scalingo/go-utils/cronsetup"
"github.com/Scalingo/go-utils/graceful"
"github.com/Scalingo/go-utils/logger"
"github.com/Scalingo/go-utils/logger/plugins/rollbarplugin"
"github.com/Scalingo/sand/api/params"
"github.com/Scalingo/sand/api/types"
"github.com/Scalingo/sand/config"
"github.com/Scalingo/sand/endpoint"
Expand All @@ -29,6 +28,7 @@ import (
"github.com/Scalingo/sand/network"
"github.com/Scalingo/sand/network/netmanager"
"github.com/Scalingo/sand/network/overlay"
"github.com/Scalingo/sand/node"
"github.com/Scalingo/sand/store"
apptls "github.com/Scalingo/sand/utils/tls"
"github.com/Scalingo/sand/web"
Expand Down Expand Up @@ -83,11 +83,17 @@ func main() {
endpointRepository := endpoint.NewRepository(c, dataStore, managers)
networkRepository := network.NewRepository(c, dataStore, managers)

err = ensureNetworks(ctx, c, networkRepository, endpointRepository)
err = node.EnsureNetworkEndpoints(ctx, c, networkRepository, endpointRepository)
if err != nil {
log.WithError(err).Error("fail to ensure existing networks")
os.Exit(-1)
}
stopEndpointEnsureCron, err := setupEndpointEnsureCron(ctx, c, networkRepository, endpointRepository)
if err != nil {
log.WithError(err).Error("fail to setup endpoint ensure cron")
os.Exit(-1)
}
defer stopEndpointEnsureCron()

vctrl := web.NewVersionController(c)
nctrl := web.NewNetworksController(c, networkRepository, endpointRepository, ipAllocator)
Expand All @@ -96,6 +102,7 @@ func main() {
sandRouter := handlers.NewRouter(log)
sandRouter.Use(handlers.ErrorMiddleware)
sandRouter.HandleFunc("/version", vctrl.Show).Methods("GET")
sandRouter.HandleFunc("/node/ensure-network-endpoints", nctrl.EnsureNetworkEndpoints).Methods("POST")
sandRouter.HandleFunc("/networks", nctrl.List).Methods("GET")
sandRouter.HandleFunc("/networks", nctrl.Create).Methods("POST")
sandRouter.HandleFunc("/networks/{id}", nctrl.Show).Methods("GET")
Expand Down Expand Up @@ -175,69 +182,17 @@ func main() {
log.Info("All APIs stopped, shutting down..")
}

func ensureNetworks(ctx context.Context, c *config.Config, repo network.Repository, erepo endpoint.Repository) error {
log := logger.Get(ctx)
ctx = logger.ToCtx(ctx, log)

log.Info("Ensure networks on node")

endpoints, err := erepo.List(ctx, map[string]string{"hostname": c.GetPeerHostname()})
if err == store.ErrNotFound {
return nil
}
if err != nil {
return errors.Wrapf(err, "fail to list endpoints of %v", c.GetPeerHostname())
}

for _, endpoint := range endpoints {
log = log.WithField("endpoint_id", endpoint.ID)
ctx = logger.ToCtx(ctx, log)
if !endpoint.Active {
log.Debug("skip inactive endpoint")
continue
}
log = log.WithFields(logrus.Fields{
"network_id": endpoint.NetworkID,
"endpoint_id": endpoint.ID,
"endpoint_netns_path": endpoint.TargetNetnsPath,
})
log.Info("restoring endpoint")

network, ok, err := repo.Exists(ctx, endpoint.NetworkID)
if err != nil {
return errors.Wrapf(err, "fail to get network")
}
if !ok {
log.WithError(errors.Errorf("network not found for %v", endpoint))
continue
}

log.Info("ensuring network")
err = repo.Ensure(ctx, network)
if err != nil {
log.WithError(err).Error("fail to ensure network")
continue
}

endpoint, err = erepo.Activate(ctx, network, endpoint, params.EndpointActivate{
NSHandlePath: endpoint.TargetNetnsPath,
SetAddr: true,
MoveVeth: true,
})
if err != nil {
// if we can't activate the endpoint because the netns path doesn't exist anymore, we
// just deactivate it. Otherwise we raise an error.
if os.IsNotExist(errors.Cause(err)) {
endpoint, err = erepo.Deactivate(ctx, network, endpoint)
if err != nil {
log.WithError(err).Error("fail to deactivate endpoint")
continue
}
} else {
log.WithError(err).Error("fail to ensure endpoint")
continue
}
}
}
return nil
func setupEndpointEnsureCron(ctx context.Context, c *config.Config, repo network.Repository, erepo endpoint.Repository) (func(), error) {
return cronsetup.Setup(ctx, cronsetup.SetupOpts{
WithoutTelemetry: true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is there a reason why you remove the telemtry?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it wasn't installed yet in sand, this will change with my next PR

Jobs: []cronsetup.Job{
{
Name: "ensure network endpoints",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is it safe to have a name with spaces in it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, poor review from my side

Rhythm: "@every " + c.EndpointEnsureInterval.String(),
Func: func(ctx context.Context) error {
return node.EnsureNetworkEndpoints(ctx, c, repo, erepo)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: wrap the error

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I could wrap it, but it doesn't bring a lot of value at this level I'd say, but ok

},
},
},
})
}
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"os"
"time"

etcdutils "github.com/Scalingo/go-utils/etcd"

Expand Down Expand Up @@ -49,6 +50,8 @@ type Config struct {
EnableDockerPlugin bool `envconfig:"ENABLE_DOCKER_PLUGIN"`
DockerPluginHttpPort int `default:"9998"`

EndpointEnsureInterval time.Duration `envconfig:"ENDPOINT_ENSURE_INTERVAL" default:"30m"`

MaxVNI int `envconfig:"MAX_VNI" default:"999_999"`
}

Expand All @@ -60,6 +63,9 @@ func Build() (*Config, error) {
if err != nil {
return nil, errors.Wrapf(err, "fail to build etcd config")
}
if c.EndpointEnsureInterval <= 0 {
return nil, errors.New("endpoint ensure interval must be positive")
}

if c.PublicHostname == "" {
h, err := os.Hostname()
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/Scalingo/go-etcd-lock/v5 v5.1.0
github.com/Scalingo/go-handlers v1.11.1
github.com/Scalingo/go-plugins-helpers v1.4.0
github.com/Scalingo/go-utils/cronsetup v1.6.0
github.com/Scalingo/go-utils/etcd v1.2.2
github.com/Scalingo/go-utils/graceful v1.3.3
github.com/Scalingo/go-utils/logger v1.12.2
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/Scalingo/go-handlers v1.11.1 h1:c1BnZe2EhCebIzfygoOmcl7iZlUdn5OYuG/4x
github.com/Scalingo/go-handlers v1.11.1/go.mod h1:FP+IibVd+yWkSY9B+rzcIi629MMk/7kNfcEjIZUhMq4=
github.com/Scalingo/go-plugins-helpers v1.4.0 h1:TXysHoesEcH5Q4JBCo0prxWLFY0/yp2Q2WFBGa+Curo=
github.com/Scalingo/go-plugins-helpers v1.4.0/go.mod h1:pPivLa4FeG8iB0nff7NwI7gIZ4bMr62u/prYXktoWIo=
github.com/Scalingo/go-utils/cronsetup v1.6.0 h1:mbaZXF1e1tTuZVb6D7nbwxFQVi+fjiEzByZ+vwnUfB8=
github.com/Scalingo/go-utils/cronsetup v1.6.0/go.mod h1:KOVhrti0nmP/szDhNAvdGpivJtXoNwXDfuU8Ywpqhyg=
github.com/Scalingo/go-utils/crypto v1.1.1 h1:+F4JqC/8Lu8b1MDJwdQjT19yzLhShWPk+K93cUv7KVg=
github.com/Scalingo/go-utils/crypto v1.1.1/go.mod h1:29dsjRaXxNI6tUG2ZbNWs4A5pqmwCAztdgfudGQDuhY=
github.com/Scalingo/go-utils/errors/v2 v2.5.1 h1:1tfJW6/ZxTgrRmFTlKQCOtArQquOW0/XdZQzx8wMHoM=
Expand All @@ -21,6 +23,8 @@ github.com/Scalingo/go-utils/graceful v1.3.3 h1:N1Gcw6nnu3Sid2icxE7G25c8JwKjBh4M
github.com/Scalingo/go-utils/graceful v1.3.3/go.mod h1:d22EgSLL6g0uIeqQrsFAQbWqw5Qb5UPpgT0bTVf/e0s=
github.com/Scalingo/go-utils/logger v1.12.2 h1:9vm83/gqjCIy5t+OuNYjkVOUrJtdMy78XNIv8E+OCCU=
github.com/Scalingo/go-utils/logger v1.12.2/go.mod h1:vaeFcI5LMHiRRmMfJbbnblbj3RXRJIzxUcyEjZpMFpg=
github.com/Scalingo/go-utils/otel v0.8.0 h1:iLWJHjjpmYHmzly8XNsA5Kn89GpUaSk6Jie6yPRvtrc=
github.com/Scalingo/go-utils/otel v0.8.0/go.mod h1:ZNIXMGq+HCpUa3SGYZ5i5ivZavlXwVulDm75d6m+sd0=
github.com/Scalingo/go-utils/security v1.2.2 h1:3AkvqQm0b58wsM4mtvh6vlaOEelRJuvI1iLviMTdXX4=
github.com/Scalingo/go-utils/security v1.2.2/go.mod h1:oGgcjK4/PtG3rspMTygnZUl0z2cu/qS+JA+HGpKY0Lk=
github.com/Scalingo/logrus-rollbar v1.4.4 h1:zi+0LJaHlDm9HcArOh8Gmqt0scNFC77wn4c9ePSNof0=
Expand Down Expand Up @@ -60,6 +64,8 @@ github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
Expand Down
88 changes: 88 additions & 0 deletions node/ensure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package node

import (
"context"
"os"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/Scalingo/go-utils/logger"
"github.com/Scalingo/sand/api/params"
"github.com/Scalingo/sand/config"
"github.com/Scalingo/sand/endpoint"
"github.com/Scalingo/sand/network"
"github.com/Scalingo/sand/store"
)

func EnsureNetworkEndpoints(ctx context.Context, c *config.Config, repo network.Repository, erepo endpoint.Repository) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand EnsureNetworkEndpoints() is run by the cron every 30 minutes but also by the endpoint POST /node/ensure-network-endpoints what will happen if they collide and run at the same time ?

log := logger.Get(ctx)
ctx = logger.ToCtx(ctx, log)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ctx = logger.ToCtx(ctx, log)


log.Info("Ensure networks on node")

endpoints, err := erepo.List(ctx, map[string]string{"hostname": c.GetPeerHostname()})
if errors.Cause(err) == store.ErrNotFound {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: should we already switch to go-utils/errors/v3.Is, even though this service does not yet use our own error library?

return nil
}
if err != nil {
return errors.Wrapf(err, "failed to list endpoints of %v", c.GetPeerHostname())
}

for _, endpoint := range endpoints {
endpointLog := log.WithFields(logrus.Fields{
"endpoint_id": endpoint.ID,
})
endpointCtx := logger.ToCtx(ctx, endpointLog)

Check failure on line 36 in node/ensure.go

View workflow job for this annotation

GitHub Actions / Linter on a PR

ineffectual assignment to endpointCtx (ineffassign)
Comment on lines +33 to +36

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
endpointLog := log.WithFields(logrus.Fields{
"endpoint_id": endpoint.ID,
})
endpointCtx := logger.ToCtx(ctx, endpointLog)
ctx, log := logger.WithFieldsToCtx(ctx, logrus.Fields{
"endpoint_id": endpoint.ID,
})


if !endpoint.Active {
endpointLog.Debug("skip inactive endpoint")
continue
}

endpointLog = endpointLog.WithFields(logrus.Fields{
"network_id": endpoint.NetworkID,
"endpoint_netns_path": endpoint.TargetNetnsPath,
})
endpointCtx = logger.ToCtx(ctx, endpointLog)
Comment on lines +43 to +47

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
endpointLog = endpointLog.WithFields(logrus.Fields{
"network_id": endpoint.NetworkID,
"endpoint_netns_path": endpoint.TargetNetnsPath,
})
endpointCtx = logger.ToCtx(ctx, endpointLog)
ctx, log = logger.WithFieldsToCtx(ctx, logrus.Fields{
"network_id": endpoint.NetworkID,
"endpoint_netns_path": endpoint.TargetNetnsPath,
})

endpointLog.Info("restoring endpoint")

network, ok, err := repo.Exists(endpointCtx, endpoint.NetworkID)
if err != nil {
return errors.Wrapf(err, "failed to get network")
}
if !ok {
endpointLog.WithError(errors.Errorf("network not found for %v", endpoint)).Error("skip endpoint")
continue
}

endpointLog.Info("ensuring network")
err = repo.Ensure(endpointCtx, network)
if err != nil {
endpointLog.WithError(err).Error("failed to ensure network")
continue
}

_, err = erepo.Activate(endpointCtx, network, endpoint, params.EndpointActivate{
NSHandlePath: endpoint.TargetNetnsPath,
SetAddr: true,
MoveVeth: true,
})
if err != nil {
// If the netns path no longer exists, deactivate the endpoint. Other
// activation errors should not prevent the remaining endpoints from being ensured.
if os.IsNotExist(errors.Cause(err)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:

A wrapped file-not-found error from erepo.Activate(...) may not be recognized here, so missing endpoint.TargetNetnsPath can skip erepo.Deactivate(...). An attacker who can cause stale netns paths during restore could leave orphaned endpoint network state behind.

More details about this

erepo.Activate(...) returns an error, and this code decides whether to clean up the endpoint by checking os.IsNotExist(errors.Cause(err)). That check only looks at the unwrapped cause, so a wrapped not exist error can be missed and the code will take the failed to ensure endpoint path instead of calling erepo.Deactivate(...).

A plausible abuse case is: 1) an attacker causes endpoint.TargetNetnsPath to point to a namespace path that has already been removed, 2) erepo.Activate(endpointCtx, network, endpoint, ...) returns a wrapped file-not-found error for that path, 3) this os.IsNotExist(errors.Cause(err)) check fails to recognize it, and 4) the stale endpoint is left active because erepo.Deactivate(endpointCtx, network, endpoint) is never called. In a system restoring many endpoints, repeatedly triggering this on selected endpoint.NetworkID / endpoint.TargetNetnsPath values can leave orphaned network state behind, which can keep unexpected connectivity or policy state attached to containers that should have been cleaned up.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
if os.IsNotExist(errors.Cause(err)) {
if errors.Is(errors.Cause(err), fs.ErrNotExist) {
View step-by-step instructions
  1. Replace the os.IsNotExist(...) check with the standard library errors.Is(...) check against the not-exist sentinel error.
    Change os.IsNotExist(errors.Cause(err)) to stderrors.Is(err, fs.ErrNotExist).

  2. Stop unwrapping the error with errors.Cause(...) in this condition.
    errors.Is already walks wrapped errors, so it can match the original not-exist error through errors.Wrap, fmt.Errorf(... %w ...), and similar wrappers.

  3. Add the standard library imports needed for the new check.
    If this file already imports github.com/pkg/errors as errors, import the standard library package with an alias such as stderrors "errors", and add io/fs for fs.ErrNotExist.

  4. Keep the existing deactivate path the same, and only update the condition.
    The resulting condition should look like if stderrors.Is(err, fs.ErrNotExist) { ... }.

  5. Alternatively, if this file does not import github.com/pkg/errors with the name errors, use the standard library package directly as errors.Is(err, fs.ErrNotExist).

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by os-error-is-not-exist.

You can view more details about this finding in the Semgrep AppSec Platform.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second this

_, err = erepo.Deactivate(endpointCtx, network, endpoint)
if err != nil {
endpointLog.WithError(err).Error("failed to deactivate endpoint")
continue
}
} else {
endpointLog.WithError(err).Error("failed to ensure endpoint")
continue
}
}
}

return nil
}
Loading
Loading