From 84c4b150f3914a9e081e3a0188a021be9978783e Mon Sep 17 00:00:00 2001 From: Pratik Panda Date: Wed, 18 Sep 2024 23:07:15 +0530 Subject: [PATCH] OSD-25183: Update Dynatrace related commands for osdctl (#608) * Update dynatrace commands to get HCP Ns without login * Refactor and fix logs to fetch from MC * Update context command for Dynatrace Logs * Add logs URL to context command * Update the logs command * Remove unsued variable and consts Remove unsued const * Update how the error message is parsed --- cmd/cluster/context.go | 68 +++++++--- cmd/cluster/dynatrace/cluster.go | 148 ++++++++-------------- cmd/cluster/dynatrace/hcpGatherLogsCmd.go | 45 ++++--- cmd/cluster/dynatrace/logsCmd.go | 48 +++---- cmd/cluster/dynatrace/requests.go | 2 +- cmd/cluster/dynatrace/rootCmd.go | 5 - cmd/cluster/dynatrace/urlCmd.go | 4 +- cmd/cluster/dynatrace/vault.go | 6 +- go.mod | 14 +- go.sum | 32 ++--- pkg/utils/ocm.go | 95 +++++++++++++- 11 files changed, 263 insertions(+), 204 deletions(-) diff --git a/cmd/cluster/context.go b/cmd/cluster/context.go index c7bb50a3..33f15361 100644 --- a/cmd/cluster/context.go +++ b/cmd/cluster/context.go @@ -1,6 +1,7 @@ package cluster import ( + "encoding/base64" "encoding/json" "fmt" "os" @@ -72,8 +73,9 @@ type contextData struct { // Current OCM environment (e.g., "production" or "stage") OCMEnv string - // Dynatrace Environment URL - DyntraceEnvURL string + // Dynatrace Environment URL and Logs URL + DyntraceEnvURL string + DyntraceLogsURL string // limited Support Status LimitedSupportReasons []*cmv1.LimitedSupportReason @@ -239,7 +241,7 @@ func (o *contextOptions) printLongOutput(data *contextData) { fmt.Println() // Print Dynatrace URL - printDynatraceEnvURL(data) + printDynatraceResources(data) } func (o *contextOptions) printShortOutput(data *contextData) { @@ -395,26 +397,28 @@ func (o *contextOptions) generateContextData() (*contextData, []error) { } } - GetDynatraceURL := func() { + GetDynatraceDetails := func() { var clusterID string = o.clusterID defer wg.Done() defer utils.StartDelayTracker(o.verbose, "Dynatrace URL").End() - clusterID, _, err := dynatrace.GetManagementCluster(ocmClient, o.cluster) + hcpCluster, err := dynatrace.FetchClusterDetails(clusterID) if err != nil { - errors = append(errors, err) - data.DyntraceEnvURL = err.Error() + if err == dynatrace.ErrUnsupportedCluster { + data.DyntraceEnvURL = dynatrace.ErrUnsupportedCluster.Error() + } else { + errors = append(errors, fmt.Errorf("failed to acquire cluster details %v", err)) + data.DyntraceEnvURL = "Failed to fetch Dynatrace URL" + } return - } - data.DyntraceEnvURL, err = dynatrace.GetDynatraceURLFromLabel(ocmClient, clusterID) - if err != nil { - errors = append(errors, fmt.Errorf("error The Dynatrace Environemnt URL could not be determined from Label. Using fallback method%s", err)) - // FallBack method to determine via Cluster Login - data.DyntraceEnvURL, err = dynatrace.GetDynatraceURLFromManagementCluster(clusterID) + } else { + query, err := dynatrace.GetQuery(hcpCluster) if err != nil { - errors = append(errors, fmt.Errorf("error The Dynatrace Environemnt URL could not be determined %s", err)) - data.DyntraceEnvURL = "the Dynatrace Environemnt URL could not be determined. \nPlease refer the SOP to determine the correct Dyntrace Tenant URL- https://github.com/openshift/ops-sop/tree/master/dynatrace#what-environments-are-there" + errors = append(errors, fmt.Errorf("failed to build query for Dynatrace %v", err)) } + queryTxt := query.Build() + data.DyntraceEnvURL = hcpCluster.DynatraceURL + data.DyntraceLogsURL = dynatrace.GetLinkToWebConsole(hcpCluster.DynatraceURL, 10, base64.StdEncoding.EncodeToString([]byte(queryTxt))) } } @@ -450,7 +454,7 @@ func (o *contextOptions) generateContextData() (*contextData, []error) { GetJiraIssues, GetSupportExceptions, GetPagerDutyAlerts, - GetDynatraceURL, + GetDynatraceDetails, ) if o.output == longOutputConfigValue { @@ -696,10 +700,36 @@ func skippableEvent(eventName string) bool { return false } -func printDynatraceEnvURL(data *contextData) { - var name string = "Dynatrace Environment URL" +func printDynatraceResources(data *contextData) { + var name string = "Dynatrace Details" fmt.Println(delimiter + name) - fmt.Println(data.DyntraceEnvURL) + + links := map[string]string{ + "Dynatrace Tenant URL": data.DyntraceEnvURL, + "Logs App URL": data.DyntraceLogsURL, + } + + // Sort, so it's always a predictable order + var keys []string + for k := range links { + keys = append(keys, k) + } + sort.Strings(keys) + + table := printer.NewTablePrinter(os.Stdout, 20, 1, 3, ' ') + for _, link := range keys { + url := strings.TrimSpace(links[link]) + if url == dynatrace.ErrUnsupportedCluster.Error() { + fmt.Println(dynatrace.ErrUnsupportedCluster.Error()) + break + } else if url != "" { + table.AddRow([]string{link, url}) + } + } + + if err := table.Flush(); err != nil { + fmt.Fprintf(os.Stderr, "Error printing %s: %v\n", name, err) + } } func (data *contextData) printClusterHeader() { diff --git a/cmd/cluster/dynatrace/cluster.go b/cmd/cluster/dynatrace/cluster.go index 7bc0516e..2c18a5d1 100644 --- a/cmd/cluster/dynatrace/cluster.go +++ b/cmd/cluster/dynatrace/cluster.go @@ -4,120 +4,88 @@ import ( "context" "fmt" "net/url" + "strings" "github.com/Dynatrace/dynatrace-operator/src/api/v1beta1" "github.com/Dynatrace/dynatrace-operator/src/api/v1beta1/dynakube" - sdk "github.com/openshift-online/ocm-sdk-go" - v1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" "github.com/openshift/osdctl/pkg/k8s" ocmutils "github.com/openshift/osdctl/pkg/utils" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" ) -func fetchClusterDetails(clusterKey string) (clusterID string, mcName string, dynatraceURL string, error error) { +type HCPCluster struct { + name string + internalID string + managementClusterID string + klusterletNS string + hostedNS string + hcpNamespace string + managementClusterName string + DynatraceURL string +} + +var ErrUnsupportedCluster = fmt.Errorf("Not an HCP or MC Cluster") + +func FetchClusterDetails(clusterKey string) (hcpCluster HCPCluster, error error) { + hcpCluster = HCPCluster{} if err := ocmutils.IsValidClusterKey(clusterKey); err != nil { - return "", "", "", err + return hcpCluster, err } connection, err := ocmutils.CreateConnection() if err != nil { - return "", "", "", err + return HCPCluster{}, err } defer connection.Close() cluster, err := ocmutils.GetCluster(connection, clusterKey) if err != nil { - return "", "", "", err - } - - mgmtClusterID, mgmtClusterName, err := GetManagementCluster(connection, cluster) - if err != nil { - return "", "", "", err - } - - url, err := GetDynatraceURLFromLabel(connection, mgmtClusterID) - if err != nil { - return "", "", "", fmt.Errorf("the Dynatrace Environemnt URL could not be determined. \nPlease refer the SOP to determine the correct Dyntrace Tenant URL- https://github.com/openshift/ops-sop/tree/master/dynatrace#what-environments-are-there \n\nError Details - %s", err) - } - return cluster.ID(), mgmtClusterName, url, nil -} - -func GetManagementCluster(connection *sdk.Connection, cluster *v1.Cluster) (id string, name string, error error) { - clusterID := cluster.ID() - clusterName := cluster.Name() - if cluster.Hypershift().Enabled() { - ManagementCluster, err := ocmutils.GetManagementCluster(clusterID) - if err != nil { - return "", "", fmt.Errorf("error retreiving Management Cluster for given HCP") + return HCPCluster{}, err + } + + if !cluster.Hypershift().Enabled() { + isMC, err := ocmutils.IsManagementCluster(cluster.ID()) + if !isMC || err != nil { + // if the cluster is not a HCP or MC, then return an error + return HCPCluster{}, ErrUnsupportedCluster + } else { + // if the cluster is not a HCP but a MC, then return a just relevant info for HCPCluster Object + hcpCluster.managementClusterID = cluster.ID() + hcpCluster.managementClusterName = cluster.Name() + url, err := ocmutils.GetDynatraceURLFromLabel(hcpCluster.managementClusterID) + if err != nil { + return HCPCluster{}, fmt.Errorf("the Dynatrace Environemnt URL could not be determined. \nPlease refer the SOP to determine the correct Dyntrace Tenant URL- https://github.com/openshift/ops-sop/tree/master/dynatrace#what-environments-are-there \n\nError Details - %s", err) + } + hcpCluster.DynatraceURL = url + return hcpCluster, nil } - clusterID = ManagementCluster.ID() - clusterName = ManagementCluster.Name() } - isMC, err := isManagementCluster(connection, clusterID) + mgmtCluster, err := ocmutils.GetManagementCluster(cluster.ID()) if err != nil { - return "", "", fmt.Errorf("could not verify if the cluster is HCP/Management Cluster %v", err) + return HCPCluster{}, fmt.Errorf("error retreiving Management Cluster for given HCP %s", err) } - if !isMC && !cluster.Hypershift().Enabled() { - return "", "", fmt.Errorf("cluster is not a HCP/Management Cluster") - } - return clusterID, clusterName, nil -} - -// Sanity Check for MC Cluster -func isManagementCluster(connection *sdk.Connection, clusterID string) (isMC bool, err error) { - collection := connection.ClustersMgmt().V1().Clusters() - // Get the labels externally available for the cluster - resource := collection.Cluster(clusterID).ExternalConfiguration().Labels() - // Send the request to retrieve the list of external cluster labels: - response, err := resource.List().Send() + hcpCluster.hcpNamespace, err = ocmutils.GetHCPNamespace(clusterKey) if err != nil { - return false, fmt.Errorf("can't retrieve cluster labels: %v", err) - } - - labels, ok := response.GetItems() - if !ok { - return false, nil + return HCPCluster{}, fmt.Errorf("error retreiving HCP Namespace for given cluster") } + hcpCluster.klusterletNS = fmt.Sprintf("klusterlet-%s", cluster.ID()) + hcpCluster.hostedNS = strings.SplitAfter(hcpCluster.hcpNamespace, cluster.ID())[0] - for _, label := range labels.Slice() { - if l, ok := label.GetKey(); ok { - // If the label is found as the key, we know its an Managemnt Cluster - if l == HypershiftClusterTypeLabel { - return true, nil - } - } - } - return false, nil -} - -func GetDynatraceURLFromLabel(connection *sdk.Connection, clusterID string) (url string, err error) { - subscription, err := ocmutils.GetSubscription(connection, clusterID) + url, err := ocmutils.GetDynatraceURLFromLabel(mgmtCluster.ID()) if err != nil { - return "", err + return HCPCluster{}, fmt.Errorf("the Dynatrace Environemnt URL could not be determined. \nPlease refer the SOP to determine the correct Dyntrace Tenant URL- https://github.com/openshift/ops-sop/tree/master/dynatrace#what-environments-are-there \n\nError Details - %s", err) } - subscriptionLabels, err := connection.AccountsMgmt().V1().Subscriptions().Subscription(subscription.ID()).Labels().List().Send() - labels, ok := subscriptionLabels.GetItems() - if !ok { - return "", err - } + hcpCluster.DynatraceURL = url + hcpCluster.internalID = cluster.ID() + hcpCluster.managementClusterID = mgmtCluster.ID() + hcpCluster.name = cluster.Name() - for _, label := range labels.Slice() { - if key, ok := label.GetKey(); ok { - if key == DynatraceTenantKeyLabel { - if value, ok := label.GetValue(); ok { - url := fmt.Sprintf("https://%s.apps.dynatrace.com/", value) - return url, nil - } - } - } - } - return "", fmt.Errorf("DT Tenant Not Found") + hcpCluster.managementClusterName = mgmtCluster.Name() + + return hcpCluster, nil } func GetDynatraceURLFromManagementCluster(clusterID string) (string, error) { @@ -151,17 +119,3 @@ func GetDynatraceURLFromManagementCluster(clusterID string) (string, error) { DTURL := fmt.Sprintf("%s://%s", DTApiURL.Scheme, DTApiURL.Host) return DTURL, nil } - -func GetHCPNamespacesFromInternalID(clientset *kubernetes.Clientset, clusterID string) (klusterletNS string, shortNS string, hcpNS string, error error) { - labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{"api.openshift.com/id": clusterID}} - nsList, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{LabelSelector: labels.Set(labelSelector.MatchLabels).String()}) - if err != nil { - return "", "", "", fmt.Errorf("failed to determine HCP namespace %v", err) - } - if len(nsList.Items) != 1 { - return "", "", "", fmt.Errorf("failed to determine HCP namespace, matchiing namespaces %v", len(nsList.Items)) - } - - ns := nsList.Items[0] - return fmt.Sprintf("klusterlet-%s", clusterID), ns.Name, fmt.Sprintf("%s-%s", ns.Name, ns.Labels["api.openshift.com/name"]), nil -} diff --git a/cmd/cluster/dynatrace/hcpGatherLogsCmd.go b/cmd/cluster/dynatrace/hcpGatherLogsCmd.go index ad394e08..89d6911e 100644 --- a/cmd/cluster/dynatrace/hcpGatherLogsCmd.go +++ b/cmd/cluster/dynatrace/hcpGatherLogsCmd.go @@ -53,35 +53,26 @@ func gatherLogs(clusterID string) (error error) { return fmt.Errorf("failed to acquire access token %v", err) } - clusterInternalID, managementClusterName, DTURL, err := fetchClusterDetails(clusterID) - if err != nil { - return err - } - managementClusterInternalID, _, _, err := fetchClusterDetails(managementClusterName) + hcpCluster, err := FetchClusterDetails(clusterID) if err != nil { return err } - _, _, clientset, err := common.GetKubeConfigAndClient(managementClusterInternalID, "", "") + _, _, clientset, err := common.GetKubeConfigAndClient(hcpCluster.managementClusterID, "", "") if err != nil { - return fmt.Errorf("failed to retrieve Kubernetes configuration and client for cluster with ID %s: %w", managementClusterInternalID, err) + return fmt.Errorf("failed to retrieve Kubernetes configuration and client for cluster with ID %s: %w", hcpCluster.managementClusterID, err) } - klusterletNS, shortNS, hcpNS, err := GetHCPNamespacesFromInternalID(clientset, clusterInternalID) - if err != nil { - return err - } - - fmt.Println(fmt.Sprintf("Using HCP Namespace %v", hcpNS)) + fmt.Printf("Using HCP Namespace %v\n", hcpCluster.hcpNamespace) - gatherNamespaces := []string{hcpNS, klusterletNS, shortNS, "hypershift", "cert-manager", "redhat-cert-manager-operator"} - gatherDir, err := setupGatherDir(hcpNS) + gatherNamespaces := []string{hcpCluster.hcpNamespace, hcpCluster.klusterletNS, hcpCluster.hostedNS, "hypershift", "cert-manager", "redhat-cert-manager-operator"} + gatherDir, err := setupGatherDir(hcpCluster.hcpNamespace) if err != nil { return err } for _, gatherNS := range gatherNamespaces { - fmt.Println(fmt.Sprintf("Gathering for %s", gatherNS)) + fmt.Printf("Gathering for %s\n", gatherNS) pods, err := getPodsForNamespace(clientset, gatherNS) if err != nil { @@ -93,7 +84,7 @@ func gatherLogs(clusterID string) (error error) { return err } - err = dumpPodLogs(pods, nsDir, gatherNS, managementClusterName, DTURL, accessToken, since, tail, sortOrder) + err = dumpPodLogs(pods, nsDir, gatherNS, hcpCluster.managementClusterName, hcpCluster.DynatraceURL, accessToken, since, tail, sortOrder) if err != nil { return err } @@ -103,7 +94,7 @@ func gatherLogs(clusterID string) (error error) { return err } - err = dumpEvents(deployments, nsDir, gatherNS, managementClusterName, DTURL, accessToken, since, tail, sortOrder) + err = dumpEvents(deployments, nsDir, gatherNS, hcpCluster.managementClusterName, hcpCluster.DynatraceURL, accessToken, since, tail, sortOrder) if err != nil { return err } @@ -116,7 +107,7 @@ func gatherLogs(clusterID string) (error error) { func dumpEvents(deploys *appsv1.DeploymentList, parentDir string, targetNS string, managementClusterName string, DTURL string, accessToken string, since int, tail int, sortOrder string) error { totalDeployments := len(deploys.Items) for k, d := range deploys.Items { - fmt.Println(fmt.Sprintf("[%d/%d] Deployment events for %s", k+1, totalDeployments, d.Name)) + fmt.Printf("[%d/%d] Deployment events for %s\n", k+1, totalDeployments, d.Name) eventQuery, err := getEventQuery(d.Name, targetNS, since, tail, sortOrder, managementClusterName) if err != nil { @@ -133,6 +124,9 @@ func dumpEvents(deploys *appsv1.DeploymentList, parentDir string, targetNS strin deploymentYamlPath := filepath.Join(eventsDirPath, deploymentYamlFileName) deploymentYaml, err := yaml.Marshal(d) + if err != nil { + return fmt.Errorf("failed to marshal YAML: %v", err) + } f, err := os.OpenFile(deploymentYamlPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0655) if err != nil { return err @@ -147,6 +141,10 @@ func dumpEvents(deploys *appsv1.DeploymentList, parentDir string, targetNS strin } eventsRequestToken, err := getDTQueryExecution(DTURL, accessToken, eventQuery.finalQuery) + if err != nil { + log.Print("failed to get request token", err) + continue + } err = getEvents(DTURL, accessToken, eventsRequestToken, f) f.Close() if err != nil { @@ -161,7 +159,7 @@ func dumpEvents(deploys *appsv1.DeploymentList, parentDir string, targetNS strin func dumpPodLogs(pods *corev1.PodList, parentDir string, targetNS string, managementClusterName string, DTURL string, accessToken string, since int, tail int, sortOrder string) error { totalPods := len(pods.Items) for k, p := range pods.Items { - fmt.Println(fmt.Sprintf("[%d/%d] Pod logs for %s", k+1, totalPods, p.Name)) + fmt.Printf("[%d/%d] Pod logs for %s\n", k+1, totalPods, p.Name) podLogsQuery, err := getPodQuery(p.Name, targetNS, since, tail, sortOrder, managementClusterName) if err != nil { @@ -178,6 +176,9 @@ func dumpPodLogs(pods *corev1.PodList, parentDir string, targetNS string, manage podYamlFilePath := filepath.Join(podDirPath, podYamlFileName) podYaml, err := yaml.Marshal(p) + if err != nil { + return fmt.Errorf("failed to marshal YAML: %v", err) + } f, err := os.OpenFile(podYamlFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0655) if err != nil { return err @@ -192,6 +193,10 @@ func dumpPodLogs(pods *corev1.PodList, parentDir string, targetNS string, manage } podLogsRequestToken, err := getDTQueryExecution(DTURL, accessToken, podLogsQuery.finalQuery) + if err != nil { + log.Print("failed to get request token", err) + continue + } err = getLogs(DTURL, accessToken, podLogsRequestToken, f) f.Close() if err != nil { diff --git a/cmd/cluster/dynatrace/logsCmd.go b/cmd/cluster/dynatrace/logsCmd.go index cbfb9400..1a257465 100644 --- a/cmd/cluster/dynatrace/logsCmd.go +++ b/cmd/cluster/dynatrace/logsCmd.go @@ -4,18 +4,15 @@ import ( "encoding/base64" "fmt" - "github.com/openshift/osdctl/cmd/common" "github.com/spf13/cobra" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) var ( dryRun bool - hcp bool tail int since int contains string - cluster string sortOrder string namespaceList []string nodeList []string @@ -50,7 +47,6 @@ func NewCmdLogs() *cobra.Command { logsCmd.Flags().IntVar(&since, "since", 1, "Number of hours (integer) since which to search (defaults to 1 hour)") logsCmd.Flags().StringVar(&contains, "contains", "", "Include logs which contain a phrase") logsCmd.Flags().StringVar(&sortOrder, "sort", "desc", "Sort the results by timestamp in either ascending or descending order. Accepted values are 'asc' and 'desc'") - logsCmd.Flags().BoolVar(&hcp, "hcp", false, "Set true to Include the HCP Namespace") logsCmd.Flags().StringSliceVar(&namespaceList, "namespace", []string{}, "Namespace(s) (comma-separated)") logsCmd.Flags().StringSliceVar(&nodeList, "node", []string{}, "Node name(s) (comma-separated)") logsCmd.Flags().StringSliceVar(&podList, "pod", []string{}, "Pod name(s) (comma-separated)") @@ -60,27 +56,27 @@ func NewCmdLogs() *cobra.Command { return logsCmd } -func getLinkToWebConsole(dtURL string, since int, base64Url string) string { - return fmt.Sprintf("\nLink to Web Console - \n%sui/apps/dynatrace.classic.logs.events/ui/logs-events?gtf=-%dh&gf=all&sortDirection=desc&advancedQueryMode=true&isDefaultQuery=false&visualizationType=table#%s\n\n", dtURL, since, base64Url) +func GetLinkToWebConsole(dtURL string, since int, base64Url string) string { + return fmt.Sprintf("%sui/apps/dynatrace.logs/?gtf=-%dh&gf=all&sortDirection=desc&advancedQueryMode=true&isDefaultQuery=false&visualizationType=table#%s\n\n", dtURL, since, base64Url) } func main(clusterID string) error { + var hcpCluster HCPCluster if since <= 0 { return fmt.Errorf("invalid time duration") } - - clusterInternalID, mgmtClusterName, DTURL, err := fetchClusterDetails(clusterID) + hcpCluster, err := FetchClusterDetails(clusterID) if err != nil { return fmt.Errorf("failed to acquire cluster details %v", err) } - query, err := getQuery(clusterInternalID, mgmtClusterName) + query, err := GetQuery(hcpCluster) if err != nil { return fmt.Errorf("failed to build query for Dynatrace %v", err) } fmt.Println(query.Build()) - fmt.Println(getLinkToWebConsole(DTURL, since, base64.StdEncoding.EncodeToString([]byte(query.finalQuery)))) + fmt.Println("\nLink to Web Console - \n", GetLinkToWebConsole(hcpCluster.DynatraceURL, since, base64.StdEncoding.EncodeToString([]byte(query.finalQuery)))) if dryRun { return nil @@ -91,8 +87,11 @@ func main(clusterID string) error { return fmt.Errorf("failed to acquire access token %v", err) } - requestToken, err := getDTQueryExecution(DTURL, accessToken, query.finalQuery) - err = getLogs(DTURL, accessToken, requestToken, nil) + requestToken, err := getDTQueryExecution(hcpCluster.DynatraceURL, accessToken, query.finalQuery) + if err != nil { + return fmt.Errorf("failed to get vault token %v", err) + } + err = getLogs(hcpCluster.DynatraceURL, accessToken, requestToken, nil) if err != nil { return fmt.Errorf("failed to get logs %v", err) } @@ -100,26 +99,15 @@ func main(clusterID string) error { return nil } -func getQuery(clusterID string, mgmtClusterName string) (query DTQuery, error error) { +func GetQuery(hcpCluster HCPCluster) (query DTQuery, error error) { q := DTQuery{} - q.InitLogs(since).Cluster(mgmtClusterName) + q.InitLogs(since).Cluster(hcpCluster.managementClusterName) - if len(namespaceList) > 0 || hcp { - if hcp { - managementClusterInternalID, _, _, err := fetchClusterDetails(mgmtClusterName) - if err != nil { - return q, err - } - _, _, clientset, err := common.GetKubeConfigAndClient(managementClusterInternalID, "", "") - if err != nil { - return q, fmt.Errorf("failed to retrieve Kubernetes configuration and client for cluster with ID %s: %w", managementClusterInternalID, err) - } - _, _, hcpNS, err := GetHCPNamespacesFromInternalID(clientset, clusterID) - if err != nil { - return q, err - } - namespaceList = append(namespaceList, hcpNS) - } + if hcpCluster.hcpNamespace != "" { + namespaceList = append(namespaceList, hcpCluster.hcpNamespace) + } + + if len(namespaceList) > 0 { q.Namespaces(namespaceList) } diff --git a/cmd/cluster/dynatrace/requests.go b/cmd/cluster/dynatrace/requests.go index 21d7b857..01c8f353 100644 --- a/cmd/cluster/dynatrace/requests.go +++ b/cmd/cluster/dynatrace/requests.go @@ -96,7 +96,7 @@ func getAccessToken() (string, error) { return "", err } - err = setupVaultToken(vaultAddr, vaultPath) + err = setupVaultToken(vaultAddr) if err != nil { return "", err } diff --git a/cmd/cluster/dynatrace/rootCmd.go b/cmd/cluster/dynatrace/rootCmd.go index 07f333e3..baf6b4de 100644 --- a/cmd/cluster/dynatrace/rootCmd.go +++ b/cmd/cluster/dynatrace/rootCmd.go @@ -4,11 +4,6 @@ import ( "github.com/spf13/cobra" ) -const ( - DynatraceTenantKeyLabel string = "sre-capabilities.dtp.tenant" - HypershiftClusterTypeLabel string = "ext-hypershift.openshift.io/cluster-type" -) - func NewCmdDynatrace() *cobra.Command { dtCmd := &cobra.Command{ Use: "dynatrace", diff --git a/cmd/cluster/dynatrace/urlCmd.go b/cmd/cluster/dynatrace/urlCmd.go index 785cac78..86ca3893 100644 --- a/cmd/cluster/dynatrace/urlCmd.go +++ b/cmd/cluster/dynatrace/urlCmd.go @@ -14,11 +14,11 @@ func newCmdURL() *cobra.Command { Args: cobra.ExactArgs(1), DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { - _, _, dtURL, err := fetchClusterDetails(args[0]) + hcpCluster, err := FetchClusterDetails(args[0]) if err != nil { cmdutil.CheckErr(err) } - fmt.Println("Dynatrace Environment URL - ", dtURL) + fmt.Println("Dynatrace Environment URL - ", hcpCluster.DynatraceURL) }, } return urlCmd diff --git a/cmd/cluster/dynatrace/vault.go b/cmd/cluster/dynatrace/vault.go index d59d9873..043dc0f7 100644 --- a/cmd/cluster/dynatrace/vault.go +++ b/cmd/cluster/dynatrace/vault.go @@ -13,10 +13,10 @@ type response struct { } `json:"data"` } -func setupVaultToken(vaultAddr, vaultPath string) error { +func setupVaultToken(vaultAddr string) error { err := os.Setenv("VAULT_ADDR", vaultAddr) if err != nil { - return fmt.Errorf("Error setting environment variable: %v", err) + return fmt.Errorf("error setting environment variable: %v", err) } tokenCheckCmd := exec.Command("vault", "token", "lookup") @@ -30,7 +30,7 @@ func setupVaultToken(vaultAddr, vaultPath string) error { loginCmd.Stdout = nil loginCmd.Stderr = nil if err = loginCmd.Run(); err != nil { - return fmt.Errorf("Error running 'vault login': %v", err) + return fmt.Errorf("error running 'vault login': %v", err) } fmt.Println("Acquired vault token") diff --git a/go.mod b/go.mod index 344a5b8f..d2e2eaaf 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.33.1 github.com/openshift-online/ocm-cli v0.1.74 - github.com/openshift-online/ocm-sdk-go v0.1.436 + github.com/openshift-online/ocm-sdk-go v0.1.440 github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e github.com/openshift/aws-account-operator/api v0.0.0-20230322125717-5b5a00a3e99f github.com/openshift/backplane-cli v0.1.33 @@ -49,7 +49,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.4.0 - golang.org/x/sync v0.6.0 + golang.org/x/sync v0.7.0 golang.org/x/term v0.22.0 google.golang.org/api v0.171.0 google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 @@ -135,7 +135,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.3 // indirect - github.com/gorilla/css v1.0.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect @@ -157,7 +157,7 @@ require ( github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/microcosm-cc/bluemonday v1.0.23 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -204,12 +204,12 @@ require ( go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.22.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 255ec05b..35f9a54c 100644 --- a/go.sum +++ b/go.sum @@ -292,8 +292,8 @@ github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/ github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -390,8 +390,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfr github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= -github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -447,8 +447,8 @@ github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/openshift-online/ocm-cli v0.1.74 h1:5X6ocWV5S/oFAkviCwM6T7qrI63Rxbqdizm+V+RVoXM= github.com/openshift-online/ocm-cli v0.1.74/go.mod h1:nL8zLsW2MMVkGij0ANvjmskwANY6yDcX/jOmZrn1Nfc= -github.com/openshift-online/ocm-sdk-go v0.1.436 h1:ajW7VzheCv1uk8NE3hTTpwvllk1+WP62IslXz5567ks= -github.com/openshift-online/ocm-sdk-go v0.1.436/go.mod h1:CiAu2jwl3ITKOxkeV0Qnhzv4gs35AmpIzVABQLtcI2Y= +github.com/openshift-online/ocm-sdk-go v0.1.440 h1:qHVF8iZ0V3DPPuZq2LF7pwQKVsm0W0QxVDoXxDS7oyw= +github.com/openshift-online/ocm-sdk-go v0.1.440/go.mod h1:CiAu2jwl3ITKOxkeV0Qnhzv4gs35AmpIzVABQLtcI2Y= github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e h1:cxgCNo/R769CO23AK5TCh45H9SMUGZ8RukiF2/Qif3o= github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= github.com/openshift/aws-account-operator/api v0.0.0-20230322125717-5b5a00a3e99f h1:sfv+SJc6S9r2m+c8/7N9RwzHIXQEVkXDl1odNcizwis= @@ -592,8 +592,8 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= @@ -627,8 +627,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= @@ -640,8 +640,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -692,8 +692,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -711,8 +711,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/utils/ocm.go b/pkg/utils/ocm.go index 52f3431b..d6461567 100644 --- a/pkg/utils/ocm.go +++ b/pkg/utils/ocm.go @@ -19,10 +19,12 @@ import ( const ClusterServiceClusterSearch = "id = '%s' or name = '%s' or external_id = '%s'" const ( - productionURL = "https://api.openshift.com" - stagingURL = "https://api.stage.openshift.com" - integrationURL = "https://api.integration.openshift.com" - productionGovURL = "https://api-admin.openshiftusgov.com" + productionURL = "https://api.openshift.com" + stagingURL = "https://api.stage.openshift.com" + integrationURL = "https://api.integration.openshift.com" + productionGovURL = "https://api-admin.openshiftusgov.com" + HypershiftClusterTypeLabel = "ext-hypershift.openshift.io/cluster-type" + DynatraceTenantKeyLabel = "sre-capabilities.dtp.tenant" ) var urlAliases = map[string]string{ @@ -454,6 +456,91 @@ func GetManagementCluster(clusterId string) (*cmv1.Cluster, error) { return nil, fmt.Errorf("no management cluster found for %s", clusterId) } +// Sanity Check for MC Cluster +func IsManagementCluster(clusterID string) (isMC bool, err error) { + conn, err := CreateConnection() + if err != nil { + return false, err + } + defer conn.Close() + collection := conn.ClustersMgmt().V1().Clusters() + // Get the labels externally available for the cluster + resource := collection.Cluster(clusterID).ExternalConfiguration().Labels() + // Send the request to retrieve the list of external cluster labels: + response, err := resource.List().Send() + if err != nil { + return false, fmt.Errorf("can't retrieve cluster labels: %v", err) + } + + labels, ok := response.GetItems() + if !ok { + return false, nil + } + + for _, label := range labels.Slice() { + if l, ok := label.GetKey(); ok { + // If the label is found as the key, we know its an Managemnt Cluster + if l == HypershiftClusterTypeLabel { + return true, nil + } + } + } + return false, nil +} + +func GetHCPNamespace(clusterId string) (namespace string, err error) { + conn, err := CreateConnection() + if err != nil { + return "", err + } + defer conn.Close() + + hypershiftResp, err := conn.ClustersMgmt().V1().Clusters(). + Cluster(clusterId). + Hypershift(). + Get(). + Send() + if err != nil { + return "", err + } + + if namespace, ok := hypershiftResp.Body().GetHCPNamespace(); ok { + return namespace, nil + } + + return "", fmt.Errorf("no hcp namespace found for %s", clusterId) +} + +func GetDynatraceURLFromLabel(clusterID string) (url string, err error) { + conn, err := CreateConnection() + if err != nil { + return "", err + } + defer conn.Close() + subscription, err := GetSubscription(conn, clusterID) + if err != nil { + return "", err + } + + subscriptionLabels, err := conn.AccountsMgmt().V1().Subscriptions().Subscription(subscription.ID()).Labels().List().Send() + labels, ok := subscriptionLabels.GetItems() + if !ok { + return "", err + } + + for _, label := range labels.Slice() { + if key, ok := label.GetKey(); ok { + if key == DynatraceTenantKeyLabel { + if value, ok := label.GetValue(); ok { + url := fmt.Sprintf("https://%s.apps.dynatrace.com/", value) + return url, nil + } + } + } + } + return "", fmt.Errorf("DT Tenant Not Found") +} + func SendRequest(request *sdk.Request) (*sdk.Response, error) { response, err := request.Send() if err != nil {