Skip to content

Commit 64d2167

Browse files
authored
[EXP-704] ecs: v1 metadata is not required for daemon mode on Managed Instances (#54174)
### What does this PR do? Makes the ECS Metadata v1 introspection endpoint a best-effort dependency for ECS **Managed Instances** daemon mode instead of a hard startup requirement, and adds a task-metadata fallback for ECS cluster metadata. Two changes: 1. `comp/core/workloadmeta/collectors/internal/ecs/daemon_parser.go` — `initializeDaemonMode` no longer returns early when `ecsmeta.V1()` or `GetInstance` fails. When v1 is unusable and the launch type is Managed Instances with a working v4 client, it routes to `parseTasksFromV4TasksEndpoint`. EC2 still fails, because both of its daemon parsers read the task list from `metaV1.GetTasks` — but it now fails with a retriable error so the collector is retried rather than dropped. 2. `pkg/util/ecs/ecs.go` — `newECSMeta` falls back from `getECSInstanceMetadata` (v1) to `getECSTaskMetadata` (v3/v4) on error, instead of only using task metadata on Fargate. ### Motivation On Managed Instances, `initializeDaemonMode` called `ecsmeta.V1()` unconditionally before daemon mode could start, but the parser that actually runs there never consumes what v1 provides: - **ClusterName / ClusterARN** — derived from each v4 task's own `Cluster` field in `util.ParseV4Task` - **ContainerInstanceARN** — never set by `ParseV4Task` for any launch type, so it is already empty on this path - **instance.Version** — only drives parser selection, and Managed Instances already has a fallback that keys off `ECS_CONTAINER_METADATA_URI_V4` So a v1 failure blocked startup entirely, producing zero `ECSTask` entities and no `task_arn`, `task_family`, `task_version`, `ecs_cluster_name` or `ecs_container_name` tags on `container.*` metrics for the whole host — even though the v4 `/tasks` path works fine. The second change fixes `GetClusterMeta()` which separate v1 call site that the collector fix does not touch, so the orchestrator ECS check stayed gated off: ``` Failed to get ECS meta: temporary failure in ecsutil-meta-v1 ... Orchestrator ECS check is missing required information, region: , awsAccountID: , clusterName: , clusterID: ``` All four required values are available from the v4 task payload, and `initClusterID` is a pure `md5(account/region/cluster)` derivation, so the cluster ID is identical either way. This also unblocks the container lifecycle check's cluster ID and the flare's ECS section. ### Describe how you validated your changes Unit tests (run in the Linux dev container, since the `docker` build tag is excluded on macOS by `DARWIN_EXCLUDED_TAGS`): - `TestInitializeDaemonModeV1Unavailable` — Managed Instances starts and selects the v4 `/tasks` parser both when the v1 client fails to initialize and when `GetInstance` fails; EC2 fails in both cases; Managed Instances also fails when task collection is disabled or no v4 client is available. Asserts `taskCollectionParser` is never nil when `Start` succeeds, and that every error return satisfies `retry.IsErrWillRetry`. - `TestInitializeDaemonModeV1Available` — a reachable v1 endpoint still populates `clusterName`/`containerInstanceARN` and selects the per-task v4 parser, unchanged for EC2. - `TestNewECSMetaTaskMetadataFallback` — instance metadata used when available; falls back to task metadata on failure and produces the same cluster ID; surfaces the original v1 error when both fail. ``` dda inv test --targets=./comp/core/workloadmeta/collectors/internal/ecs # 69 tests pass dda inv test --targets=./pkg/util/ecs # 47 tests pass dda inv test --targets=./pkg/collector/corechecks/orchestrator/ecs,./pkg/collector/corechecks/containerlifecycle,./comp/core/tagger/collectors # 177 tests pass dda inv linter.go --targets=./pkg/util/ecs,./comp/core/workloadmeta/collectors/internal/ecs # 0 issues ``` ### Additional Notes **The daemon-mode startup error has to be a `*retry.Error`.** `workloadmeta.startCandidates` keeps a collector in its candidate set only when `retry.IsErrWillRetry(err)` is true, and `retry.IsRetryError` type-asserts to `*retry.Error` instead of unwrapping. So wrapping with `fmt.Errorf("%w")` makes the error look non-retriable, and the collector is deleted from the candidate set — ECS task collection would then stay off until the Agent restarts. `initializeDaemonMode` returns a `*retry.Error` with `FailWillRetry`, keeping the original error as `LogicError` for context. This also makes the `GetInstance`-failure path retriable, where it previously returned `nil` and left a nil task parser that would be invoked on the first `Pull`. `TestInitializeDaemonModeV1Unavailable` asserts the retriable contract on every error path so it cannot regress silently. **Launch types other than MI daemon are unaffected.** `metaV1` is referenced in exactly four places in the collector package: `daemon_parser.go` (init) and `v1parser.go` / `v4parser.go` (`GetTasks`). Sidecar mode uses `metaV2` and `metaV4` and never touches v1, so Fargate (always sidecar) and MI sidecar are untouched. EC2 daemon keeps its existing v1 requirement. Co-authored-by: steve.zou <steve.zou@datadoghq.com>
1 parent d38e9d0 commit 64d2167

7 files changed

Lines changed: 379 additions & 22 deletions

File tree

comp/core/workloadmeta/collectors/internal/ecs/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ go_library(
2929
"//pkg/util/ecs/metadata/v3or4",
3030
"//pkg/util/fargate",
3131
"//pkg/util/log",
32+
"//pkg/util/retry",
3233
"@com_github_patrickmn_go_cache//:go-cache",
3334
"@io_k8s_client_go//util/workqueue",
3435
"@org_golang_x_time//rate",
@@ -62,6 +63,7 @@ dd_agent_go_test(
6263
"//pkg/util/ecs/metadata/v1:metadata",
6364
"//pkg/util/ecs/metadata/v2:metadata",
6465
"//pkg/util/ecs/metadata/v3or4",
66+
"//pkg/util/retry",
6567
"@com_github_patrickmn_go_cache//:go-cache",
6668
"@com_github_stretchr_testify//assert",
6769
"@com_github_stretchr_testify//require",

comp/core/workloadmeta/collectors/internal/ecs/daemon_parser.go

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package ecs
1010

1111
import (
1212
"context"
13+
"fmt"
1314
"os"
1415
"time"
1516

@@ -18,14 +19,26 @@ import (
1819
ecsmeta "github.com/DataDog/datadog-agent/pkg/util/ecs/metadata"
1920
"github.com/DataDog/datadog-agent/pkg/util/ecs/metadata/v3or4"
2021
"github.com/DataDog/datadog-agent/pkg/util/log"
22+
"github.com/DataDog/datadog-agent/pkg/util/retry"
2123

2224
"github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors/util"
2325
)
2426

27+
// declare these as vars not const to ease testing: the underlying helpers memoize
28+
// global state and perform network I/O, which unit tests cannot rely on.
29+
//
30+
// These are process-wide mutable globals, so tests that swap them must restore the
31+
// originals and must not call t.Parallel().
32+
var (
33+
ecsMetaV1 = ecsmeta.V1
34+
ecsMetaV4FromCurrentTask = ecsmeta.V4FromCurrentTask
35+
ecsHasEC2ResourceTags = ecsutil.HasEC2ResourceTags
36+
)
37+
2538
// initializeDaemonMode sets up the collector for daemon deployment mode.
2639
//
2740
// In daemon mode, the agent runs as a daemon on an ECS instance and monitors all tasks on that instance.
28-
// This mode requires V1 metadata API access and uses different parsing strategies based on configuration:
41+
// The parsing strategy depends on configuration and on which metadata endpoints are reachable:
2942
//
3043
// - V1 parsing: Lists all tasks on the instance (basic info)
3144
// See: v1parser.go - parseTasksFromV1Endpoint()
@@ -36,15 +49,13 @@ import (
3649
// - V4 /tasks parsing: Fetches all host tasks in a single call from the daemon container's v4 endpoint.
3750
// Used on ECS Managed Instances where the /tasks endpoint is available.
3851
// See: daemon_parser.go - parseTasksFromV4TasksEndpoint()
52+
//
53+
// V1 is required by the first two strategies, which read the task list from it. It is not
54+
// required by the third: on ECS Managed Instances the v4 /tasks payload carries everything
55+
// the parser needs, and the v1 introspection endpoint is not guaranteed to be reachable
56+
// from the daemon container. Daemon mode therefore treats v1 as best-effort and only fails
57+
// when no v1-independent strategy is available.
3958
func (c *collector) initializeDaemonMode(ctx context.Context) error {
40-
var err error
41-
42-
// Daemon mode requires v1 API access
43-
c.metaV1, err = ecsmeta.V1()
44-
if err != nil {
45-
return err
46-
}
47-
4859
// This only exists to allow overriding for testing
4960
c.metaV3or4 = func(metaURI, metaVersion string) v3or4.Client {
5061
return v3or4.NewClient(metaURI, metaVersion, v3or4.WithTryOption(
@@ -56,25 +67,54 @@ func (c *collector) initializeDaemonMode(ctx context.Context) error {
5667

5768
// Attempt to initialize a v4 client for the daemon agent's own container.
5869
// This enables the /tasks endpoint on ECS Managed Instances.
59-
if v4Client, err := ecsmeta.V4FromCurrentTask(); err == nil {
70+
if v4Client, err := ecsMetaV4FromCurrentTask(); err == nil {
6071
c.metaV4 = v4Client
6172
} else {
6273
log.Debugf("ECS daemon: failed to initialize v4 client for current task (may not be available): %v", err)
6374
}
6475

65-
c.hasResourceTags = ecsutil.HasEC2ResourceTags()
76+
c.hasResourceTags = ecsHasEC2ResourceTags()
6677
c.collectResourceTags = c.config.GetBool("ecs_collect_resource_tags_ec2")
6778

68-
instance, err := c.metaV1.GetInstance(ctx)
69-
if err == nil {
70-
c.clusterName = instance.Cluster
71-
c.containerInstanceARN = instance.ContainerInstanceARN
72-
c.setTaskCollectionParserForDaemon(instance.Version)
79+
var v1Err error
80+
c.metaV1, v1Err = ecsMetaV1()
81+
if v1Err != nil {
82+
log.Warnf("ECS daemon: metadata v1 client unavailable: %v", v1Err)
83+
// Guard against a non-nil interface wrapping a nil client.
84+
c.metaV1 = nil
7385
} else {
74-
log.Warnf("cannot determine ECS cluster name: %s", err)
86+
instance, err := c.metaV1.GetInstance(ctx)
87+
if err != nil {
88+
v1Err = err
89+
log.Warnf("cannot determine ECS cluster name: %s", err)
90+
} else {
91+
c.clusterName = instance.Cluster
92+
c.containerInstanceARN = instance.ContainerInstanceARN
93+
c.setTaskCollectionParserForDaemon(instance.Version)
94+
return nil
95+
}
96+
}
97+
98+
// No usable v1 instance metadata. The v4 /tasks endpoint on Managed Instances derives
99+
// cluster identity from each task, so it can run without v1. Select it explicitly
100+
// rather than going through setTaskCollectionParserForDaemon, which would otherwise
101+
// pick a v1-backed parser and dereference the nil client on the first Pull.
102+
if c.taskCollectionEnabled && c.metaV4 != nil &&
103+
c.actualLaunchType == workloadmeta.ECSLaunchTypeManagedInstances {
104+
log.Infof("ECS daemon: metadata v1 unavailable, using metadata v4 /tasks endpoint for managed instances")
105+
c.taskCollectionParser = c.parseTasksFromV4TasksEndpoint
106+
return nil
75107
}
76108

77-
return nil
109+
// Returning a retriable error leaves the collector in workloadmeta's candidate set, so
110+
// Start is retried and picks up the cluster name once the endpoint recovers. This must
111+
// be a *retry.Error: workloadmeta gates on retry.IsErrWillRetry, which type-asserts
112+
// rather than unwrapping, so wrapping with %w here would drop the collector for good.
113+
return &retry.Error{
114+
LogicError: fmt.Errorf("ECS daemon mode requires metadata v1: %w", v1Err),
115+
RessourceName: componentName,
116+
RetryStatus: retry.FailWillRetry,
117+
}
78118
}
79119

80120
// setTaskCollectionParserForDaemon sets up the appropriate task parser for daemon deployment mode.

comp/core/workloadmeta/collectors/internal/ecs/daemon_parser_test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
package ecs
99

1010
import (
11+
"context"
12+
"errors"
1113
"os"
1214
"reflect"
1315
"runtime"
@@ -16,8 +18,11 @@ import (
1618
"github.com/stretchr/testify/assert"
1719
"github.com/stretchr/testify/require"
1820

21+
"github.com/DataDog/datadog-agent/comp/core/config"
1922
workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def"
23+
v1 "github.com/DataDog/datadog-agent/pkg/util/ecs/metadata/v1"
2024
"github.com/DataDog/datadog-agent/pkg/util/ecs/metadata/v3or4"
25+
"github.com/DataDog/datadog-agent/pkg/util/retry"
2126
)
2227

2328
// taskParserName returns the name of the function backing the task parser for assertion.
@@ -166,3 +171,167 @@ func TestSetTaskCollectionParserForDaemon(t *testing.T) {
166171
})
167172
}
168173
}
174+
175+
// TestInitializeDaemonModeV1Unavailable covers daemon-mode startup when the metadata v1
176+
// introspection endpoint cannot be used. ECS Managed Instances must still start by routing
177+
// to the v4 /tasks endpoint, while EC2 must keep failing because every EC2 daemon parser
178+
// reads the task list from v1.
179+
func TestInitializeDaemonModeV1Unavailable(t *testing.T) {
180+
getInstanceErr := errors.New("connection refused")
181+
182+
tests := []struct {
183+
name string
184+
launchType workloadmeta.ECSLaunchType
185+
taskCollectionEnabled bool
186+
hasMetaV4 bool
187+
// v1Client is returned by the ecsMetaV1 seam; nil means v1 client init fails.
188+
v1Client *fakev1EcsClient
189+
expectErr bool
190+
expectParserSuffix string
191+
}{
192+
{
193+
name: "managed instances without v1 client uses v4 /tasks",
194+
launchType: workloadmeta.ECSLaunchTypeManagedInstances,
195+
taskCollectionEnabled: true,
196+
hasMetaV4: true,
197+
v1Client: nil,
198+
expectParserSuffix: "parseTasksFromV4TasksEndpoint",
199+
},
200+
{
201+
name: "managed instances with failing GetInstance uses v4 /tasks",
202+
launchType: workloadmeta.ECSLaunchTypeManagedInstances,
203+
taskCollectionEnabled: true,
204+
hasMetaV4: true,
205+
v1Client: &fakev1EcsClient{
206+
mockGetInstance: func(context.Context) (*v1.Instance, error) { return nil, getInstanceErr },
207+
},
208+
expectParserSuffix: "parseTasksFromV4TasksEndpoint",
209+
},
210+
{
211+
name: "managed instances without v1 and without metaV4 fails",
212+
launchType: workloadmeta.ECSLaunchTypeManagedInstances,
213+
taskCollectionEnabled: true,
214+
hasMetaV4: false,
215+
v1Client: nil,
216+
expectErr: true,
217+
},
218+
{
219+
name: "managed instances without v1 and task collection disabled fails",
220+
launchType: workloadmeta.ECSLaunchTypeManagedInstances,
221+
taskCollectionEnabled: false,
222+
hasMetaV4: true,
223+
v1Client: nil,
224+
expectErr: true,
225+
},
226+
{
227+
name: "ec2 without v1 client fails",
228+
launchType: workloadmeta.ECSLaunchTypeEC2,
229+
taskCollectionEnabled: true,
230+
hasMetaV4: true,
231+
v1Client: nil,
232+
expectErr: true,
233+
},
234+
{
235+
name: "ec2 with failing GetInstance fails instead of leaving a nil parser",
236+
launchType: workloadmeta.ECSLaunchTypeEC2,
237+
taskCollectionEnabled: true,
238+
hasMetaV4: true,
239+
v1Client: &fakev1EcsClient{
240+
mockGetInstance: func(context.Context) (*v1.Instance, error) { return nil, getInstanceErr },
241+
},
242+
expectErr: true,
243+
},
244+
}
245+
246+
for _, tt := range tests {
247+
t.Run(tt.name, func(t *testing.T) {
248+
// The v4 /tasks route additionally requires the v4 env var to be present.
249+
t.Setenv(v3or4.DefaultMetadataURIv4EnvVariable, "http://169.254.170.2/v4")
250+
251+
restore := stubDaemonMetadataSeams(t, tt.v1Client, tt.hasMetaV4)
252+
defer restore()
253+
254+
c := &collector{
255+
config: config.NewMockWithOverrides(t, map[string]interface{}{}),
256+
taskCollectionEnabled: tt.taskCollectionEnabled,
257+
actualLaunchType: tt.launchType,
258+
}
259+
260+
err := c.initializeDaemonMode(context.Background())
261+
262+
if tt.expectErr {
263+
require.Error(t, err)
264+
// workloadmeta only keeps a collector in its candidate set when
265+
// retry.IsErrWillRetry matches, and that helper type-asserts instead of
266+
// unwrapping. A non-retriable error here would drop the ECS collector
267+
// permanently instead of retrying Start.
268+
assert.True(t, retry.IsErrWillRetry(err),
269+
"startup error must be retriable, got %T: %v", err, err)
270+
return
271+
}
272+
273+
require.NoError(t, err)
274+
require.NotNil(t, c.taskCollectionParser, "a nil parser would panic on the first Pull")
275+
assert.Contains(t, taskParserName(c.taskCollectionParser), tt.expectParserSuffix)
276+
})
277+
}
278+
279+
}
280+
281+
// TestInitializeDaemonModeV1Available verifies that a reachable v1 endpoint still drives
282+
// parser selection and populates the instance-derived fields, unchanged for EC2.
283+
func TestInitializeDaemonModeV1Available(t *testing.T) {
284+
t.Setenv(v3or4.DefaultMetadataURIv4EnvVariable, "http://169.254.170.2/v4")
285+
286+
v1Client := &fakev1EcsClient{
287+
mockGetInstance: func(context.Context) (*v1.Instance, error) {
288+
return &v1.Instance{
289+
Cluster: "my-cluster",
290+
ContainerInstanceARN: "arn:aws:ecs:us-east-1:123456789012:container-instance/my-cluster/abc123",
291+
Version: "Amazon ECS Agent - v1.54.0 (abc1234)",
292+
}, nil
293+
},
294+
}
295+
296+
restore := stubDaemonMetadataSeams(t, v1Client, false)
297+
defer restore()
298+
299+
c := &collector{
300+
config: config.NewMockWithOverrides(t, map[string]interface{}{}),
301+
taskCollectionEnabled: true,
302+
actualLaunchType: workloadmeta.ECSLaunchTypeEC2,
303+
}
304+
305+
require.NoError(t, c.initializeDaemonMode(context.Background()))
306+
assert.Equal(t, "my-cluster", c.clusterName)
307+
assert.Equal(t, "arn:aws:ecs:us-east-1:123456789012:container-instance/my-cluster/abc123", c.containerInstanceARN)
308+
require.NotNil(t, c.taskCollectionParser)
309+
assert.Contains(t, taskParserName(c.taskCollectionParser), "parseTasksFromV4Endpoint")
310+
}
311+
312+
// stubDaemonMetadataSeams replaces the metadata helpers that initializeDaemonMode calls so
313+
// tests do not depend on memoized global clients or network I/O. A nil v1Client makes the
314+
// v1 client initialization fail.
315+
func stubDaemonMetadataSeams(t *testing.T, v1Client *fakev1EcsClient, hasMetaV4 bool) func() {
316+
t.Helper()
317+
318+
origV1, origV4, origTags := ecsMetaV1, ecsMetaV4FromCurrentTask, ecsHasEC2ResourceTags
319+
320+
ecsMetaV1 = func() (v1.Client, error) {
321+
if v1Client == nil {
322+
return nil, errors.New("temporary failure in ecsutil-meta-v1")
323+
}
324+
return v1Client, nil
325+
}
326+
ecsMetaV4FromCurrentTask = func() (v3or4.Client, error) {
327+
if !hasMetaV4 {
328+
return nil, errors.New("v4 metadata endpoint not available")
329+
}
330+
return &fakev3or4EcsClient{}, nil
331+
}
332+
ecsHasEC2ResourceTags = func() bool { return false }
333+
334+
return func() {
335+
ecsMetaV1, ecsMetaV4FromCurrentTask, ecsHasEC2ResourceTags = origV1, origV4, origTags
336+
}
337+
}

comp/core/workloadmeta/collectors/internal/ecs/ecs_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,18 @@ func (store *fakeWorkloadmetaStore) GetContainer(id string) (*workloadmeta.Conta
4545
}
4646

4747
type fakev1EcsClient struct {
48-
mockGetTasks func(context.Context) ([]v1.Task, error)
48+
mockGetTasks func(context.Context) ([]v1.Task, error)
49+
mockGetInstance func(context.Context) (*v1.Instance, error)
4950
}
5051

5152
func (c *fakev1EcsClient) GetTasks(ctx context.Context) ([]v1.Task, error) {
5253
return c.mockGetTasks(ctx)
5354
}
5455

55-
func (c *fakev1EcsClient) GetInstance(_ context.Context) (*v1.Instance, error) {
56+
func (c *fakev1EcsClient) GetInstance(ctx context.Context) (*v1.Instance, error) {
57+
if c.mockGetInstance != nil {
58+
return c.mockGetInstance(ctx)
59+
}
5660
return nil, errors.New("unimplemented")
5761
}
5862

pkg/util/ecs/ecs.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ import (
2121
"github.com/DataDog/datadog-agent/pkg/config/setup/constants"
2222
"github.com/DataDog/datadog-agent/pkg/util/cache"
2323
"github.com/DataDog/datadog-agent/pkg/util/ecs/metadata"
24+
"github.com/DataDog/datadog-agent/pkg/util/log"
25+
)
26+
27+
// declare these as vars not const to ease testing
28+
var (
29+
fetchECSInstanceMetadata = getECSInstanceMetadata
30+
fetchECSTaskMetadata = getECSTaskMetadata
2431
)
2532

2633
// MetaECS stores ECS cluster metadata
@@ -80,9 +87,35 @@ func newECSMeta(ctx context.Context) (*MetaECS, error) {
8087

8188
if env.IsFeaturePresent(env.ECSFargate) {
8289
// There is no instance metadata endpoint on ECS Fargate
83-
awsAccountID, region, cluster, version, err = getECSTaskMetadata(ctx)
90+
awsAccountID, region, cluster, version, err = fetchECSTaskMetadata(ctx)
8491
} else {
85-
awsAccountID, region, cluster, version, err = getECSInstanceMetadata(ctx)
92+
awsAccountID, region, cluster, version, err = fetchECSInstanceMetadata(ctx)
93+
if err != nil {
94+
// The v1 introspection endpoint is not guaranteed to be reachable outside of
95+
// ECS EC2 (notably on ECS Managed Instances). The agent's own task metadata
96+
// carries the same cluster identity — the agent task belongs to the cluster it
97+
// runs on, and region/account come from its own task ARN — so fall back to it.
98+
// This applies to every non-Fargate launch type, not just Managed Instances: on
99+
// EC2 it only takes effect when v1 already failed, where the previous behaviour
100+
// was to give up entirely. A non-containerised agent has no task metadata
101+
// endpoint, so the fallback fails and the original v1 error is returned.
102+
//
103+
// Note the returned version is the task revision here rather than the ECS agent
104+
// version. MetaECS.ECSAgentVersion is only round-tripped through the cache key
105+
// (toCacheValue/fromCacheValue) and never used for behaviour, and the Fargate
106+
// branch above already populates it the same way.
107+
log.Debugf("could not get ECS instance metadata, falling back to task metadata: %s", err)
108+
109+
var fallbackErr error
110+
awsAccountID, region, cluster, version, fallbackErr = fetchECSTaskMetadata(ctx)
111+
if fallbackErr != nil {
112+
log.Debugf("could not get ECS task metadata either: %s", fallbackErr)
113+
// Surface the original instance metadata error, which is the more
114+
// meaningful one outside of Managed Instances.
115+
return nil, err
116+
}
117+
err = nil
118+
}
86119
}
87120

88121
if err != nil {

0 commit comments

Comments
 (0)