From 89dc5df675cc8b857fdddad60c34a712ea761b35 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Tue, 6 Jan 2026 16:11:31 +0100 Subject: [PATCH 1/5] Add LocalManager.host parameter to RunGadget calls to prevent errors with nil container-collection Signed-off-by: Matthias Bertschy --- pkg/containerwatcher/v2/tracers/bpf.go | 5 ++++- pkg/containerwatcher/v2/tracers/capabilities.go | 1 + pkg/containerwatcher/v2/tracers/dns.go | 3 ++- pkg/containerwatcher/v2/tracers/exec.go | 3 ++- pkg/containerwatcher/v2/tracers/exit.go | 5 ++++- pkg/containerwatcher/v2/tracers/fork.go | 5 ++++- pkg/containerwatcher/v2/tracers/hardlink.go | 5 ++++- pkg/containerwatcher/v2/tracers/http.go | 5 ++++- pkg/containerwatcher/v2/tracers/iouring.go | 5 ++++- pkg/containerwatcher/v2/tracers/kmod.go | 5 ++++- pkg/containerwatcher/v2/tracers/network.go | 3 ++- pkg/containerwatcher/v2/tracers/open.go | 3 ++- pkg/containerwatcher/v2/tracers/ptrace.go | 5 ++++- pkg/containerwatcher/v2/tracers/randomx.go | 5 ++++- pkg/containerwatcher/v2/tracers/ssh.go | 5 ++++- pkg/containerwatcher/v2/tracers/symlink.go | 5 ++++- pkg/containerwatcher/v2/tracers/syscall.go | 1 + pkg/containerwatcher/v2/tracers/unshare.go | 5 ++++- 18 files changed, 58 insertions(+), 16 deletions(-) diff --git a/pkg/containerwatcher/v2/tracers/bpf.go b/pkg/containerwatcher/v2/tracers/bpf.go index 87f57b8a3..d74c34503 100644 --- a/pkg/containerwatcher/v2/tracers/bpf.go +++ b/pkg/containerwatcher/v2/tracers/bpf.go @@ -68,7 +68,10 @@ func (bt *BpfTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(bt.ociStore), ) go func() { - err := bt.runtime.RunGadget(bt.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := bt.runtime.RunGadget(bt.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", bt.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/capabilities.go b/pkg/containerwatcher/v2/tracers/capabilities.go index 74bd0b3d7..b00cc1dc5 100644 --- a/pkg/containerwatcher/v2/tracers/capabilities.go +++ b/pkg/containerwatcher/v2/tracers/capabilities.go @@ -70,6 +70,7 @@ func (ct *CapabilitiesTracer) Start(ctx context.Context) error { params := map[string]string{ "operator.oci.ebpf.collect-kstack": "false", "operator.oci.ebpf.unique": "true", + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := ct.runtime.RunGadget(ct.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/dns.go b/pkg/containerwatcher/v2/tracers/dns.go index 0fd3dd0d9..215e2df70 100644 --- a/pkg/containerwatcher/v2/tracers/dns.go +++ b/pkg/containerwatcher/v2/tracers/dns.go @@ -74,7 +74,8 @@ func (dt *DNSTracer) Start(ctx context.Context) error { ) go func() { params := map[string]string{ - "operator.oci.ebpf.paths": "true", // CWD paths in events + "operator.oci.ebpf.paths": "true", // CWD paths in events + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := dt.runtime.RunGadget(dt.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/exec.go b/pkg/containerwatcher/v2/tracers/exec.go index 21a414a90..6c25026ef 100644 --- a/pkg/containerwatcher/v2/tracers/exec.go +++ b/pkg/containerwatcher/v2/tracers/exec.go @@ -69,7 +69,8 @@ func (et *ExecTracer) Start(ctx context.Context) error { ) go func() { params := map[string]string{ - "operator.oci.ebpf.paths": "true", // CWD paths in events + "operator.oci.ebpf.paths": "true", // CWD paths in events + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := et.runtime.RunGadget(et.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/exit.go b/pkg/containerwatcher/v2/tracers/exit.go index 03b0a3a72..cdf9b2dd1 100644 --- a/pkg/containerwatcher/v2/tracers/exit.go +++ b/pkg/containerwatcher/v2/tracers/exit.go @@ -64,7 +64,10 @@ func (et *ExitTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(et.ociStore), ) go func() { - err := et.runtime.RunGadget(et.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := et.runtime.RunGadget(et.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", et.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/fork.go b/pkg/containerwatcher/v2/tracers/fork.go index d31d133d7..d06b159e5 100644 --- a/pkg/containerwatcher/v2/tracers/fork.go +++ b/pkg/containerwatcher/v2/tracers/fork.go @@ -64,7 +64,10 @@ func (ft *ForkTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(ft.ociStore), ) go func() { - err := ft.runtime.RunGadget(ft.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := ft.runtime.RunGadget(ft.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", ft.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/hardlink.go b/pkg/containerwatcher/v2/tracers/hardlink.go index be0558af7..a3dfa85ec 100644 --- a/pkg/containerwatcher/v2/tracers/hardlink.go +++ b/pkg/containerwatcher/v2/tracers/hardlink.go @@ -68,7 +68,10 @@ func (ht *HardlinkTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(ht.ociStore), ) go func() { - err := ht.runtime.RunGadget(ht.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := ht.runtime.RunGadget(ht.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", ht.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/http.go b/pkg/containerwatcher/v2/tracers/http.go index 36f3e5b7e..3577c7e7f 100644 --- a/pkg/containerwatcher/v2/tracers/http.go +++ b/pkg/containerwatcher/v2/tracers/http.go @@ -82,7 +82,10 @@ func (ht *HTTPTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(ht.ociStore), ) go func() { - err := ht.runtime.RunGadget(ht.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := ht.runtime.RunGadget(ht.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", ht.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/iouring.go b/pkg/containerwatcher/v2/tracers/iouring.go index 8977fc396..2f23b27fd 100644 --- a/pkg/containerwatcher/v2/tracers/iouring.go +++ b/pkg/containerwatcher/v2/tracers/iouring.go @@ -82,7 +82,10 @@ func (it *IoUringTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(it.ociStore), ) go func() { - err := it.runtime.RunGadget(it.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := it.runtime.RunGadget(it.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", it.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/kmod.go b/pkg/containerwatcher/v2/tracers/kmod.go index 6d02d0614..6eaa37376 100644 --- a/pkg/containerwatcher/v2/tracers/kmod.go +++ b/pkg/containerwatcher/v2/tracers/kmod.go @@ -68,7 +68,10 @@ func (kt *KmodTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(kt.ociStore), ) go func() { - err := kt.runtime.RunGadget(kt.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := kt.runtime.RunGadget(kt.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", kt.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/network.go b/pkg/containerwatcher/v2/tracers/network.go index c5c36537b..e042203a5 100644 --- a/pkg/containerwatcher/v2/tracers/network.go +++ b/pkg/containerwatcher/v2/tracers/network.go @@ -83,7 +83,8 @@ func (nt *NetworkTracer) Start(ctx context.Context) error { ) go func() { params := map[string]string{ - "operator.oci.annotate": "network:kubenameresolver.enable=true", + "operator.oci.annotate": "network:kubenameresolver.enable=true", + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := nt.runtime.RunGadget(nt.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/open.go b/pkg/containerwatcher/v2/tracers/open.go index 4b5b6e210..be3a97d18 100644 --- a/pkg/containerwatcher/v2/tracers/open.go +++ b/pkg/containerwatcher/v2/tracers/open.go @@ -70,7 +70,8 @@ func (ot *OpenTracer) Start(ctx context.Context) error { ) go func() { params := map[string]string{ - "operator.oci.ebpf.paths": strconv.FormatBool(ot.cfg.EnableFullPathTracing), + "operator.oci.ebpf.paths": strconv.FormatBool(ot.cfg.EnableFullPathTracing), + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := ot.runtime.RunGadget(ot.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/ptrace.go b/pkg/containerwatcher/v2/tracers/ptrace.go index d01625eb1..68201e686 100644 --- a/pkg/containerwatcher/v2/tracers/ptrace.go +++ b/pkg/containerwatcher/v2/tracers/ptrace.go @@ -64,7 +64,10 @@ func (pt *PtraceTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(pt.ociStore), ) go func() { - err := pt.runtime.RunGadget(pt.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := pt.runtime.RunGadget(pt.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", pt.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/randomx.go b/pkg/containerwatcher/v2/tracers/randomx.go index dd6b6bba3..eea8d9a9f 100644 --- a/pkg/containerwatcher/v2/tracers/randomx.go +++ b/pkg/containerwatcher/v2/tracers/randomx.go @@ -65,7 +65,10 @@ func (rt *RandomXTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(rt.ociStore), ) go func() { - err := rt.runtime.RunGadget(rt.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := rt.runtime.RunGadget(rt.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", rt.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/ssh.go b/pkg/containerwatcher/v2/tracers/ssh.go index c29157dc0..c6e9f899e 100644 --- a/pkg/containerwatcher/v2/tracers/ssh.go +++ b/pkg/containerwatcher/v2/tracers/ssh.go @@ -69,7 +69,10 @@ func (st *SSHTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(st.ociStore), ) go func() { - err := st.runtime.RunGadget(st.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := st.runtime.RunGadget(st.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", st.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/symlink.go b/pkg/containerwatcher/v2/tracers/symlink.go index 01aa8cc6e..8e292aeac 100644 --- a/pkg/containerwatcher/v2/tracers/symlink.go +++ b/pkg/containerwatcher/v2/tracers/symlink.go @@ -68,7 +68,10 @@ func (st *SymlinkTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(st.ociStore), ) go func() { - err := st.runtime.RunGadget(st.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := st.runtime.RunGadget(st.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", st.gadgetCtx.Name()), helpers.Error(err)) } diff --git a/pkg/containerwatcher/v2/tracers/syscall.go b/pkg/containerwatcher/v2/tracers/syscall.go index cc2e6d8f9..fbbcb1322 100644 --- a/pkg/containerwatcher/v2/tracers/syscall.go +++ b/pkg/containerwatcher/v2/tracers/syscall.go @@ -68,6 +68,7 @@ func (st *SyscallTracer) Start(ctx context.Context) error { params := map[string]string{ "operator.oci.ebpf.map-fetch-count": "0", "operator.oci.ebpf.map-fetch-interval": "30s", + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager } err := st.runtime.RunGadget(st.gadgetCtx, nil, params) if err != nil { diff --git a/pkg/containerwatcher/v2/tracers/unshare.go b/pkg/containerwatcher/v2/tracers/unshare.go index fdf96ac65..d6dd7e61b 100644 --- a/pkg/containerwatcher/v2/tracers/unshare.go +++ b/pkg/containerwatcher/v2/tracers/unshare.go @@ -68,7 +68,10 @@ func (ut *UnshareTracer) Start(ctx context.Context) error { gadgetcontext.WithOrasReadonlyTarget(ut.ociStore), ) go func() { - err := ut.runtime.RunGadget(ut.gadgetCtx, nil, nil) + params := map[string]string{ + "operator.LocalManager.host": "true", // don't error if container-collection is nil when using local manager + } + err := ut.runtime.RunGadget(ut.gadgetCtx, nil, params) if err != nil { logger.L().Error("Error running gadget", helpers.String("gadget", ut.gadgetCtx.Name()), helpers.Error(err)) } From 8b061b413eac23bc56c5b71bc5bab18f0147c6b6 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Fri, 9 Jan 2026 07:49:02 +0100 Subject: [PATCH 2/5] Add fallback for path retrieval in OpenEventType handling Signed-off-by: Matthias Bertschy --- pkg/utils/datasource_event.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/utils/datasource_event.go b/pkg/utils/datasource_event.go index 143d90ad3..bee5cc037 100644 --- a/pkg/utils/datasource_event.go +++ b/pkg/utils/datasource_event.go @@ -346,6 +346,9 @@ func (e *DatasourceEvent) GetFullPath() string { switch e.EventType { case OpenEventType: path, _ := e.getFieldAccessor("fpath").String(e.Data) + if path == "" { + path, _ = e.getFieldAccessor("fname").String(e.Data) + } return path default: logger.L().Warning("GetFullPath not implemented for event type", helpers.String("eventType", string(e.EventType))) From 9ae9fef11560bdcdc1c44052f5fde38b92326548 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Fri, 9 Jan 2026 07:49:55 +0100 Subject: [PATCH 3/5] Remove container check in OpenTracer callback to allow host event handling Signed-off-by: Matthias Bertschy --- pkg/containerwatcher/v2/tracers/open.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/containerwatcher/v2/tracers/open.go b/pkg/containerwatcher/v2/tracers/open.go index be3a97d18..210dd5b7e 100644 --- a/pkg/containerwatcher/v2/tracers/open.go +++ b/pkg/containerwatcher/v2/tracers/open.go @@ -129,10 +129,6 @@ func (ot *OpenTracer) eventOperator() operators.DataOperator { // callback handles open events from the tracer func (ot *OpenTracer) callback(event utils.OpenEvent) { - if event.GetContainer() == "" { - return - } - errorRaw := event.GetError() if errorRaw > -1 { // Handle the event with syscall enrichment From 5a8735fb4f8957045c2d4da5c3cdfca675d2830b Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Fri, 9 Jan 2026 10:32:13 +0100 Subject: [PATCH 4/5] Add fallback to fetch DigitalOcean metadata if IMDS fails Signed-off-by: Matthias Bertschy --- pkg/cloudmetadata/metadata.go | 86 ++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/pkg/cloudmetadata/metadata.go b/pkg/cloudmetadata/metadata.go index dd26d10d8..3358b4909 100644 --- a/pkg/cloudmetadata/metadata.go +++ b/pkg/cloudmetadata/metadata.go @@ -3,6 +3,10 @@ package cloudmetadata import ( "context" "fmt" + "io" + "net/http" + "strings" + "time" apitypes "github.com/armosec/armoapi-go/armotypes" "github.com/kubescape/go-logger" @@ -54,8 +58,88 @@ func GetCloudMetadataWithIMDS(ctx context.Context) (*apitypes.CloudMetadata, err cMetadata, err := cMetadataClient.GetMetadata(ctx) if err != nil { - return nil, err + // Try DigitalOcean metadata endpoints as a fallback (e.g., droplets) if IMDS didn't work. + if doMeta, derr := fetchDigitalOceanMetadata(ctx); derr == nil && doMeta != nil { + logger.L().Info("retrieved cloud metadata from DigitalOcean metadata service as fallback") + return doMeta, nil + } + // Wrap the underlying error with additional context so logs make it clearer why metadata is missing. + // This helps surface issues like IMDS token endpoint failures (e.g. IMDSv2 token 404), unreachable metadata endpoints, + // or provider-specific metadata problems. + return nil, fmt.Errorf("failed to get cloud metadata from IMDS: %w", err) } return cMetadata, nil } + +// fetchDigitalOceanMetadata attempts to fetch basic metadata from DigitalOcean's metadata service. +// +// It probes the metadata root and queries a few commonly available endpoints. +// It returns a non-nil error if it does not look like DigitalOcean's metadata service or no useful values were found. +func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { + client := &http.Client{ + Timeout: 2 * time.Second, + } + base := "http://169.254.169.254/metadata/v1/" + + // Probe root to see whether the metadata endpoint responds and contains expected entries. + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base, nil) + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("digitalocean metadata root returned status: %d", resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) + bstr := string(body) + // Basic heuristic: the DO metadata root typically lists resources like 'id', 'hostname', 'region' etc. + if !strings.Contains(bstr, "id") && !strings.Contains(bstr, "region") && !strings.Contains(bstr, "hostname") { + return nil, fmt.Errorf("digitalocean metadata root missing expected entries") + } + + // helper to fetch a single textual endpoint and return trimmed result or empty string + get := func(path string) string { + url := base + path + r, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resp2, err2 := client.Do(r) + if err2 != nil || resp2.StatusCode != 200 { + if resp2 != nil { + resp2.Body.Close() + } + return "" + } + defer resp2.Body.Close() + b, _ := io.ReadAll(resp2.Body) + return strings.TrimSpace(string(b)) + } + + id := get("id") + if id == "" { + id = get("droplet_id") + } + hostname := get("hostname") + region := get("region") + instanceType := get("size") + if instanceType == "" { + instanceType = get("type") + } + privateIP := get("interfaces/private/0/ipv4/address") + publicIP := get("interfaces/public/0/ipv4/address") + + // if nothing useful was obtained, return an error so callers can continue trying other fallbacks + if id == "" && hostname == "" && region == "" && privateIP == "" && publicIP == "" && instanceType == "" { + return nil, fmt.Errorf("digitalocean metadata endpoints returned no data") + } + + return &apitypes.CloudMetadata{ + Provider: "digitalocean", + InstanceID: id, + InstanceType: instanceType, + Region: region, + PrivateIP: privateIP, + PublicIP: publicIP, + Hostname: hostname, + }, nil +} From a38a10e1ae2c18905637ad39c458296dbaded9b9 Mon Sep 17 00:00:00 2001 From: Matthias Bertschy Date: Wed, 14 Jan 2026 16:24:47 +0100 Subject: [PATCH 5/5] Add fallback support for fetching metadata from GCP and Azure services Signed-off-by: Matthias Bertschy --- pkg/cloudmetadata/metadata.go | 91 +++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/pkg/cloudmetadata/metadata.go b/pkg/cloudmetadata/metadata.go index 3358b4909..522c7b37e 100644 --- a/pkg/cloudmetadata/metadata.go +++ b/pkg/cloudmetadata/metadata.go @@ -63,6 +63,19 @@ func GetCloudMetadataWithIMDS(ctx context.Context) (*apitypes.CloudMetadata, err logger.L().Info("retrieved cloud metadata from DigitalOcean metadata service as fallback") return doMeta, nil } + + // Try GCP metadata endpoints as a fallback + if gcpMeta, gerr := fetchGCPMetadata(ctx); gerr == nil && gcpMeta != nil { + logger.L().Info("retrieved cloud metadata from GCP metadata service as fallback") + return gcpMeta, nil + } + + // Try Azure metadata endpoints as a fallback + if azureMeta, aerr := fetchAzureMetadata(ctx); aerr == nil && azureMeta != nil { + logger.L().Info("retrieved cloud metadata from Azure metadata service as fallback") + return azureMeta, nil + } + // Wrap the underlying error with additional context so logs make it clearer why metadata is missing. // This helps surface issues like IMDS token endpoint failures (e.g. IMDSv2 token 404), unreachable metadata endpoints, // or provider-specific metadata problems. @@ -143,3 +156,81 @@ func fetchDigitalOceanMetadata(ctx context.Context) (*apitypes.CloudMetadata, er Hostname: hostname, }, nil } + +// fetchGCPMetadata attempts to fetch basic metadata from GCP's metadata service. +func fetchGCPMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { + client := &http.Client{ + Timeout: 2 * time.Second, + } + base := "http://metadata.google.internal/computeMetadata/v1/instance/" + + get := func(path string) string { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, base+path, nil) + req.Header.Set("Metadata-Flavor", "Google") + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + if resp != nil { + resp.Body.Close() + } + return "" + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return strings.TrimSpace(string(b)) + } + + machineType := get("machine-type") + if machineType == "" { + return nil, fmt.Errorf("not a GCP instance") + } + + // GCP returns full path like "projects/12345/machineTypes/n1-standard-1" + parts := strings.Split(machineType, "/") + instanceType := parts[len(parts)-1] + + return &apitypes.CloudMetadata{ + Provider: "gcp", + InstanceID: get("id"), + InstanceType: instanceType, + Zone: get("zone"), + Hostname: get("hostname"), + }, nil +} + +// fetchAzureMetadata attempts to fetch basic metadata from Azure's metadata service. +func fetchAzureMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) { + client := &http.Client{ + Timeout: 2 * time.Second, + } + base := "http://169.254.169.254/metadata/instance/compute/" + + get := func(path string) string { + url := base + path + "?api-version=2021-02-01&format=text" + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + req.Header.Set("Metadata", "true") + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + if resp != nil { + resp.Body.Close() + } + return "" + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return strings.TrimSpace(string(b)) + } + + vmSize := get("vmSize") + if vmSize == "" { + return nil, fmt.Errorf("not an Azure instance") + } + + return &apitypes.CloudMetadata{ + Provider: "azure", + InstanceID: get("vmId"), + InstanceType: vmSize, + Region: get("location"), + Zone: get("zone"), + Hostname: get("name"), + }, nil +}