internal/system/clouddiscovery/cloud_discovery.go (597 lines of code) (raw):

/* Copyright 2023 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package clouddiscovery contains functions related to discovering SAP System cloud resources. package clouddiscovery import ( "context" "fmt" "net" "regexp" "strings" "time" timestamppb "google.golang.org/protobuf/types/known/timestamppb" "golang.org/x/exp/slices" "google.golang.org/api/compute/v1" "google.golang.org/api/file/v1" ipb "github.com/GoogleCloudPlatform/sapagent/protos/instanceinfo" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log" spb "github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos/system" ) const ( zonesURIPart = "zones" projectsURIPart = "projects" regionsURIPart = "regions" instancesURIPart = "instances" addressesURIPart = "addresses" disksURIPart = "disks" forwardingRulesURIPart = "forwardingRules" backendServicesURIPart = "backendServices" subnetworksURIPart = "subnetworks" networksURIPart = "networks" instanceGroupsURIPart = "instanceGroups" filestoresURIPart = "filestores" healthChecksURIPart = "healthChecks" locationsURIPart = "locations" ) var ( addressRegex = regexp.MustCompile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$") uriRegex = regexp.MustCompile("https?://(www|beta)\\.googleapis\\.com(\\/[a-zA-Z0-9\\-]+)+") ) type gceInterface interface { GetInstance(project, zone, instance string) (*compute.Instance, error) GetInstanceByIP(project, ip string) (*compute.Instance, error) GetDisk(project, zone, name string) (*compute.Disk, error) GetAddress(project, location, name string) (*compute.Address, error) GetAddressByIP(project, region, subnetwork, ip string) (*compute.Address, error) GetForwardingRule(project, location, name string) (*compute.ForwardingRule, error) GetRegionalBackendService(project, region, name string) (*compute.BackendService, error) GetInstanceGroup(project, zone, name string) (*compute.InstanceGroup, error) ListInstanceGroupInstances(project, zone, name string) (*compute.InstanceGroupsListInstances, error) GetFilestore(project, location, name string) (*file.Instance, error) GetFilestoreByIP(project, location, ip string) (*file.ListInstancesResponse, error) GetURIForIP(project, ip, region, subnetwok string) (string, error) GetHealthCheck(projectID, name string) (*compute.HealthCheck, error) GetNetwork(name, project string) (*compute.Network, error) GetSubnetwork(name, project, region string) (*compute.Subnetwork, error) } // ExtractFromURI attempts to extract the value of a field from a URI. This expects the format of the string to be "/<field>/<value>". func ExtractFromURI(uri, field string) string { parts := strings.Split(uri, "/") for i, s := range parts { if s == field && i+1 < len(parts) { return parts[i+1] } } return "" } func regionFromZone(zone string) string { parts := strings.Split(zone, "-") if len(parts) == 3 { return parts[0] + "-" + parts[1] } return "" } func getResourceKind(uri string) string { parts := strings.Split(uri, "/") if len(parts) < 2 { return "" } return parts[len(parts)-2] } // CloudDiscovery provides methods to discover a set of resources, and ones related to those. type CloudDiscovery struct { GceService gceInterface HostResolver func(string) ([]string, error) discoveryFunctions map[string]func(context.Context, string) (*spb.SapDiscovery_Resource, []toDiscover, error) resourceCache map[string]cacheEntry networks map[string]*CloudNetwork } // CloudSubnetwork represents a Cloud Subnetwork. type CloudSubnetwork struct { name, region string ipRange *net.IPNet network *CloudNetwork } // CloudNetwork represents a Cloud Network. type CloudNetwork struct { name, project string subnets []*CloudSubnetwork } type toDiscover struct { name string region string network string parent *spb.SapDiscovery_Resource } type cacheEntry struct { res *spb.SapDiscovery_Resource related []toDiscover } func (d *CloudDiscovery) configureDiscoveryFunctions() { d.discoveryFunctions = make(map[string]func(context.Context, string) (*spb.SapDiscovery_Resource, []toDiscover, error)) d.discoveryFunctions[instancesURIPart] = d.discoverInstance d.discoveryFunctions[addressesURIPart] = d.discoverAddress d.discoveryFunctions[disksURIPart] = d.discoverDisk d.discoveryFunctions[forwardingRulesURIPart] = d.discoverForwardingRule d.discoveryFunctions[backendServicesURIPart] = d.discoverBackendService d.discoveryFunctions[instanceGroupsURIPart] = d.discoverInstanceGroup d.discoveryFunctions[filestoresURIPart] = d.discoverFilestore d.discoveryFunctions[healthChecksURIPart] = d.discoverHealthCheck d.discoveryFunctions[subnetworksURIPart] = d.discoverSubnetwork d.discoveryFunctions[networksURIPart] = d.discoverNetwork } // DiscoverComputeResources attempts to gather information about the provided hosts and any additional // resources that are identified as related from the cloud descriptions. func (d *CloudDiscovery) DiscoverComputeResources(ctx context.Context, parentResource *spb.SapDiscovery_Resource, parentNetwork string, hostList []string, cp *ipb.CloudProperties) []*spb.SapDiscovery_Resource { log.CtxLogger(ctx).Debugw("DiscoverComputeResources called", "parent", parentResource, "hostList", hostList) var res []*spb.SapDiscovery_Resource var uris []string var discoverQueue []toDiscover var region string if cp.GetZone() != "" { region = regionFromZone(cp.GetZone()) } for _, h := range hostList { discoverQueue = append(discoverQueue, toDiscover{ name: h, region: region, network: parentNetwork, parent: parentResource, }) } for len(discoverQueue) > 0 { var h toDiscover h, discoverQueue = discoverQueue[0], discoverQueue[1:] if h.name == "" { continue } if slices.Contains(uris, h.name) { log.CtxLogger(ctx).Debugw("Already discovered", "h", h.name) // Already discovered, ignore continue } r, dis, err := d.discoverResource(ctx, h, cp.GetProjectId()) if err != nil { log.CtxLogger(ctx).Infow("discoverResource error", "err", err, "h", h.name) continue } // If the parent is not an instance, and this resource is, then move the instance properties // virtual hostname from the parent to this resource. if h.parent != nil && r.ResourceKind == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE && h.parent.ResourceKind != spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE && h.parent.GetInstanceProperties().GetVirtualHostname() != "" { if r.InstanceProperties == nil { r.InstanceProperties = &spb.SapDiscovery_Resource_InstanceProperties{} } r.InstanceProperties.VirtualHostname = h.parent.GetInstanceProperties().GetVirtualHostname() h.parent.InstanceProperties = nil } if h.name != r.ResourceUri { // Only apply a virtual hostn ame if the name being discovered is not a URI or IP address. log.CtxLogger(ctx).Debugw("Checking virtual hostname", "h", h.name) if !addressRegex.MatchString(h.name) && !uriRegex.MatchString(h.name) { if r.InstanceProperties == nil { r.InstanceProperties = &spb.SapDiscovery_Resource_InstanceProperties{} } log.CtxLogger(ctx).Debugw("Setting virtual hostname on resource", "h", h.name, "r", r) r.InstanceProperties.VirtualHostname = h.name } } log.CtxLogger(ctx).Debugw("Adding to queue", "dis", dis, "h", h.name) discoverQueue = append(discoverQueue, dis...) res = append(res, r) uris = append(uris, h.name) if h.name != r.ResourceUri { uris = append(uris, r.ResourceUri) } } return res } func (d *CloudDiscovery) discoverResource(ctx context.Context, host toDiscover, project string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverResource", "name", host.name, "parent", host.parent.GetResourceUri()) if d.resourceCache == nil { d.resourceCache = make(map[string]cacheEntry) } now := time.Now() // Check cache for this hostname if c, ok := d.resourceCache[host.name]; ok { if now.Sub(c.res.UpdateTime.AsTime()) < (10 * time.Minute) { log.CtxLogger(ctx).Debugw("discoverResource cache hit", "name", host.name, "now", now, "res", c.res, "related", c.related) return c.res, c.related, nil } } // h may be a resource URI, a hostname, or an IP address uri := host.name var addr string addrs, _ := d.HostResolver(host.name) log.CtxLogger(ctx).Debugw("discoverResource addresses", "addrs", addrs) if len(addrs) > 0 { uri = "" addr = addrs[0] // Check cache for this address if c, ok := d.resourceCache[addr]; ok { // Cache did not hit for the hostname, add it d.resourceCache[host.name] = c if now.Sub(c.res.UpdateTime.AsTime()) < (10 * time.Minute) { log.CtxLogger(ctx).Debugw("discoverResource cache hit", "name", host.name, "now", now, "res", c.res, "related", c.related) return c.res, c.related, nil } } if host.parent != nil { project = ExtractFromURI(host.parent.ResourceUri, projectsURIPart) } var err error if host.network != "" { log.CtxLogger(ctx).Debugw("discoverResource host network", "network", host.network) ip := net.ParseIP(addr) // Find the right subnetwork that the address might belong to cloudNet := d.networks[host.network] if cloudNet == nil { log.CtxLogger(ctx).Debugw("discoverResource host network not found, discovering", "network", host.network) d.discoverNetwork(ctx, host.network) cloudNet = d.networks[host.network] } log.CtxLogger(ctx).Debugw("discoverResource host network subnets", "subnets", cloudNet.subnets) for _, s := range cloudNet.subnets { log.CtxLogger(ctx).Debugw("discoverResource host network subnets ipRange", "ipRange", s.ipRange) if s.ipRange.Contains(ip) { log.CtxLogger(ctx).Debugw("discoverResource host network subnets ipRange contains", "ipRange", s.ipRange, "ip", addr, "subnet", s.name) uri, err = d.GceService.GetURIForIP(project, addr, host.region, s.name) if err != nil { log.CtxLogger(ctx).Infow("discoverResource URI in network error", "err", err, "addr", addr, "host", host.name) } break } } } if uri == "" { uri, err = d.GceService.GetURIForIP(project, addr, host.region, "") } if err != nil { log.CtxLogger(ctx).Infow("discoverResource URI error", "err", err, "addr", addr, "host", host.name) return nil, nil, err } log.CtxLogger(ctx).Debugw("discoverResource uri for ip", "uri", uri) // Check cache for this URI if c, ok := d.resourceCache[uri]; ok { // Cache did not hit for the hostname or address, add it d.resourceCache[host.name] = c d.resourceCache[addr] = c if now.Sub(c.res.UpdateTime.AsTime()) < (10 * time.Minute) { return c.res, c.related, nil } } } log.CtxLogger(ctx).Debugw("discoverResource host did not resolve", "uri", uri) res, toAdd, err := d.discoverResourceForURI(ctx, uri) if res == nil || err != nil { return nil, nil, err } if uri != host.name && res.ResourceKind == spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE { res.InstanceProperties.VirtualHostname = host.name } if host.parent != nil { if !slices.Contains(host.parent.RelatedResources, res.ResourceUri) { host.parent.RelatedResources = append(host.parent.RelatedResources, res.ResourceUri) } if !slices.Contains(res.RelatedResources, host.parent.ResourceUri) { res.RelatedResources = append(res.RelatedResources, host.parent.ResourceUri) } } c := cacheEntry{res, toAdd} d.resourceCache[host.name] = c d.resourceCache[res.ResourceUri] = c if host.name != uri { d.resourceCache[uri] = c } if addr != "" && addr != host.name { d.resourceCache[addr] = c } return res, toAdd, err } func (d *CloudDiscovery) discoverResourceForURI(ctx context.Context, uri string) (*spb.SapDiscovery_Resource, []toDiscover, error) { if d.discoveryFunctions == nil { d.configureDiscoveryFunctions() } resourceKind := getResourceKind(uri) if resourceKind == "" { return nil, nil, fmt.Errorf("Undetected resource kind for URI %q", uri) } log.CtxLogger(ctx).Debugw("discoverResourceForURI", "uri", uri, "resourceKind", resourceKind) f, ok := d.discoveryFunctions[resourceKind] if !ok { return nil, nil, fmt.Errorf("Unsupported resource URI: %q", uri) } return f(ctx, uri) } func (d *CloudDiscovery) discoverAddress(ctx context.Context, addressURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverAddress", "addressURI", addressURI) project := ExtractFromURI(addressURI, projectsURIPart) region := ExtractFromURI(addressURI, regionsURIPart) ca, err := d.GceService.GetAddress(project, region, ExtractFromURI(addressURI, addressesURIPart)) if err != nil { return nil, nil, err } ar := &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_COMPUTE, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_ADDRESS, ResourceUri: ca.SelfLink, UpdateTime: timestamppb.Now(), } toAdd := []toDiscover{{ name: ca.Subnetwork, region: region, network: ca.Network, parent: ar, }, { name: ca.Network, region: region, network: ca.Network, parent: ar, }} for _, u := range ca.Users { toAdd = append(toAdd, toDiscover{ name: u, region: region, network: ca.Network, parent: ar, }) } return ar, toAdd, nil } func (d *CloudDiscovery) discoverInstance(ctx context.Context, instanceURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverInstance", "instanceURI", instanceURI) project := ExtractFromURI(instanceURI, projectsURIPart) zone := ExtractFromURI(instanceURI, zonesURIPart) region := regionFromZone(zone) instanceName := ExtractFromURI(instanceURI, instancesURIPart) ci, err := d.GceService.GetInstance(project, zone, instanceName) if err != nil { return nil, nil, err } ir := &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_COMPUTE, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE, ResourceUri: ci.SelfLink, UpdateTime: timestamppb.Now(), InstanceProperties: &spb.SapDiscovery_Resource_InstanceProperties{ InstanceNumber: ci.Id, }, } toAdd := []toDiscover{} for _, disk := range ci.Disks { ir.InstanceProperties.DiskDeviceNames = append(ir.InstanceProperties.DiskDeviceNames, &spb.SapDiscovery_Resource_InstanceProperties_DiskDeviceName{ Source: disk.Source, DeviceName: disk.DeviceName, }) toAdd = append(toAdd, toDiscover{ name: disk.Source, region: region, parent: ir, }) } for _, net := range ci.NetworkInterfaces { toAdd = append(toAdd, toDiscover{ name: net.Network, region: region, network: net.Network, parent: ir, }, toDiscover{ name: net.Subnetwork, region: region, network: net.Network, parent: ir, }, toDiscover{ name: net.NetworkIP, region: region, network: net.Network, parent: ir, }) } return ir, toAdd, nil } func (d *CloudDiscovery) discoverDisk(ctx context.Context, diskURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverDisk", "diskURI", diskURI) diskName := ExtractFromURI(diskURI, disksURIPart) diskZone := ExtractFromURI(diskURI, zonesURIPart) projectID := ExtractFromURI(diskURI, projectsURIPart) cd, err := d.GceService.GetDisk(projectID, diskZone, diskName) if err != nil { return nil, nil, err } return &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_COMPUTE, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_DISK, ResourceUri: cd.SelfLink, UpdateTime: timestamppb.Now(), }, nil, nil } func (d *CloudDiscovery) discoverForwardingRule(ctx context.Context, fwrURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverForwardingRule", "fwrURI", fwrURI) project := ExtractFromURI(fwrURI, projectsURIPart) region := ExtractFromURI(fwrURI, regionsURIPart) fwrName := ExtractFromURI(fwrURI, forwardingRulesURIPart) fwr, err := d.GceService.GetForwardingRule(project, region, fwrName) if err != nil { return nil, nil, err } fr := &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_COMPUTE, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_FORWARDING_RULE, ResourceUri: fwr.SelfLink, UpdateTime: timestamppb.Now(), } toAdd := []toDiscover{{ name: fwr.BackendService, region: region, network: fwr.Network, parent: fr, }, { name: fwr.Network, region: region, network: fwr.Network, parent: fr, }, { name: fwr.Subnetwork, region: region, network: fwr.Network, parent: fr, }} return fr, toAdd, nil } func (d *CloudDiscovery) discoverInstanceGroup(ctx context.Context, groupURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("Discovering instance group", "groupURI", groupURI) project := ExtractFromURI(groupURI, projectsURIPart) zone := ExtractFromURI(groupURI, zonesURIPart) name := ExtractFromURI(groupURI, instanceGroupsURIPart) ig, err := d.GceService.GetInstanceGroup(project, zone, name) if err != nil { return nil, nil, err } igr := &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_COMPUTE, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_INSTANCE_GROUP, ResourceUri: ig.SelfLink, UpdateTime: timestamppb.Now(), } instances, err := d.discoverInstanceGroupInstances(ctx, groupURI) if err != nil { return nil, nil, err } region := regionFromZone(zone) toAdd := []toDiscover{} for _, inst := range instances { toAdd = append(toAdd, toDiscover{ name: inst, region: region, network: ig.Network, parent: igr, }) igr.RelatedResources = append(igr.RelatedResources, inst) } return igr, toAdd, nil } func (d *CloudDiscovery) discoverInstanceGroupInstances(ctx context.Context, groupURI string) ([]string, error) { log.CtxLogger(ctx).Debugw("discoverInstanceGroupInstances", "groupURI", groupURI) project := ExtractFromURI(groupURI, projectsURIPart) zone := ExtractFromURI(groupURI, zonesURIPart) name := ExtractFromURI(groupURI, instanceGroupsURIPart) list, err := d.GceService.ListInstanceGroupInstances(project, zone, name) if err != nil { return nil, err } var instances []string for _, i := range list.Items { instances = append(instances, i.Instance) } return instances, nil } func (d *CloudDiscovery) discoverFilestore(ctx context.Context, filestoreURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverFilestore", "filestoreURI", filestoreURI) project := ExtractFromURI(filestoreURI, projectsURIPart) location := ExtractFromURI(filestoreURI, locationsURIPart) name := ExtractFromURI(filestoreURI, filestoresURIPart) f, err := d.GceService.GetFilestore(project, location, name) if err != nil { return nil, nil, err } return &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_STORAGE, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_FILESTORE, ResourceUri: f.Name, UpdateTime: timestamppb.Now(), }, nil, nil } func (d *CloudDiscovery) discoverHealthCheck(ctx context.Context, healthCheckURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverHealthCheck", "healthCheckURI", healthCheckURI) project := ExtractFromURI(healthCheckURI, projectsURIPart) name := ExtractFromURI(healthCheckURI, healthChecksURIPart) hc, err := d.GceService.GetHealthCheck(project, name) if err != nil { return nil, nil, err } return &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_COMPUTE, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_HEALTH_CHECK, ResourceUri: hc.SelfLink, UpdateTime: timestamppb.Now(), }, nil, nil } func (d *CloudDiscovery) discoverBackendService(ctx context.Context, backendServiceURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverBackendService", "backendServiceURI", backendServiceURI) project := ExtractFromURI(backendServiceURI, projectsURIPart) region := ExtractFromURI(backendServiceURI, regionsURIPart) name := ExtractFromURI(backendServiceURI, backendServicesURIPart) bes, err := d.GceService.GetRegionalBackendService(project, region, name) if err != nil { return nil, nil, err } bsr := &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_COMPUTE, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_BACKEND_SERVICE, ResourceUri: bes.SelfLink, UpdateTime: timestamppb.Now(), } toAdd := []toDiscover{} for _, gs := range bes.Backends { if gs.Group != "" { toAdd = append(toAdd, toDiscover{ name: gs.Group, region: region, parent: bsr, network: bes.Network, }) } } for _, hc := range bes.HealthChecks { toAdd = append(toAdd, toDiscover{ name: hc, region: region, parent: bsr, network: bes.Network, }) } return bsr, toAdd, nil } func (d *CloudDiscovery) discoverNetwork(ctx context.Context, networkURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverNetwork", "networkURI", networkURI) if d.networks == nil { d.networks = make(map[string]*CloudNetwork) } cloudNet, ok := d.networks[networkURI] if !ok { cloudNet = &CloudNetwork{ name: ExtractFromURI(networkURI, networksURIPart), project: ExtractFromURI(networkURI, projectsURIPart), } } cn, err := d.GceService.GetNetwork(ExtractFromURI(networkURI, networksURIPart), ExtractFromURI(networkURI, projectsURIPart)) if err != nil { return nil, nil, err } nr := &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_NETWORK, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_NETWORK, ResourceUri: cn.SelfLink, UpdateTime: timestamppb.Now(), } cloudNet.subnets = []*CloudSubnetwork{} for _, s := range cn.Subnetworks { log.CtxLogger(ctx).Debugw("discoverNetwork subnetwork", "subnetwork", s) cs, err := d.GceService.GetSubnetwork(ExtractFromURI(s, subnetworksURIPart), ExtractFromURI(s, projectsURIPart), ExtractFromURI(s, regionsURIPart)) if err != nil { log.CtxLogger(ctx).Infow("discoverNetwork subnetwork error", "err", err, "subnetwork", s) continue } log.CtxLogger(ctx).Debugw("discoverNetwork subnetwork", "subnetwork", cs) _, ipRange, err := net.ParseCIDR(cs.IpCidrRange) if err != nil { log.CtxLogger(ctx).Infow("discoverNetwork ipRange error", "err", err, "ipRange", cs.IpCidrRange) continue } cloudNet.subnets = append(cloudNet.subnets, &CloudSubnetwork{ name: cs.Name, region: cs.Region, ipRange: ipRange, network: cloudNet, }) } d.networks[networkURI] = cloudNet log.CtxLogger(ctx).Debugw("discoverNetwork result", "nr", nr, "cloudNet", cloudNet) return nr, nil, nil } func (d *CloudDiscovery) discoverSubnetwork(ctx context.Context, subnetworkURI string) (*spb.SapDiscovery_Resource, []toDiscover, error) { log.CtxLogger(ctx).Debugw("discoverSubnetwork", "subnetworkURI", subnetworkURI) return &spb.SapDiscovery_Resource{ ResourceType: spb.SapDiscovery_Resource_RESOURCE_TYPE_NETWORK, ResourceKind: spb.SapDiscovery_Resource_RESOURCE_KIND_SUBNETWORK, ResourceUri: subnetworkURI, UpdateTime: timestamppb.Now(), }, nil, nil }