Repository: VictoriaMetrics/VictoriaMetrics
Pull request #4162 (2023-04-20 11:24:48)

vmui: fix bug where tenant list was not displayed
The list of tenants will be displayed if either of the following conditions is met:
- the `useTenantID` flag is set to `true`.
- the `serverUrl` contains tenantID, for example, `/select/<tenantID>/,` and there is more than one tenant in the response.

#4160

codecov[bot]: ## [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4162?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) Report
Patch and project coverage have no change.
> Comparison is base [(`de61a73`)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/de61a73c6358e8891581223b050fd422d9046738?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 61.68% compared to head [(`a07b149`)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4162?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 61.68%.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #4162   +/-   ##
=======================================
  Coverage   61.68%   61.68%           
=======================================
  Files         323      323           
  Lines       58340    58340           
=======================================
+ Hits        35987    35988    +1     
  Misses      20386    20386           
+ Partials     1967     1966    -1     
```


[see 6 files with indirect coverage changes](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4162/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4162?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).


Patch:
diff --git a/app/vmauth/auth_config.go b/app/vmauth/auth_config.go
index 2310d5035..3a4737c16 100644
--- a/app/vmauth/auth_config.go
+++ b/app/vmauth/auth_config.go
@@ -289,11 +289,11 @@ func initAuthConfig() {
 	// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240
 	sighupCh := procutil.NewSighupChan()
 
-	m, err := readAuthConfig(*authConfigPath)
+	err := loadAuthConfig()
 	if err != nil {
 		logger.Fatalf("cannot load auth config from `-auth.config=%s`: %s", *authConfigPath, err)
 	}
-	authConfig.Store(m)
+
 	stopCh = make(chan struct{})
 	authConfigWG.Add(1)
 	go func() {
@@ -324,44 +324,60 @@ func authConfigReloader(sighupCh <-chan os.Signal) {
 			procutil.SelfSIGHUP()
 		case <-sighupCh:
 			logger.Infof("SIGHUP received; loading -auth.config=%q", *authConfigPath)
-			m, err := readAuthConfig(*authConfigPath)
+			err := loadAuthConfig()
 			if err != nil {
 				logger.Errorf("failed to load -auth.config=%q; using the last successfully loaded config; error: %s", *authConfigPath, err)
 				continue
 			}
-			authConfig.Store(m)
 			logger.Infof("Successfully reloaded -auth.config=%q", *authConfigPath)
 		}
 	}
 }
 
-var authConfig atomic.Value
+var authConfig atomic.Pointer[AuthConfig]
+var authUsers atomic.Pointer[map[string]*UserInfo]
 var authConfigWG sync.WaitGroup
 var stopCh chan struct{}
 
-func readAuthConfig(path string) (map[string]*UserInfo, error) {
-	data, err := fs.ReadFileOrHTTP(path)
+func loadAuthConfig() error {
+	ac, err := readAuthConfig(*authConfigPath)
 	if err != nil {
-		return nil, err
+		return fmt.Errorf("failed to load `-auth.config=%q`: %s", *authConfigPath, err)
 	}
-	m, err := parseAuthConfig(data)
+
+	m, err := parseAuthConfigUsers(ac)
 	if err != nil {
-		return nil, fmt.Errorf("cannot parse %q: %w", path, err)
+		return fmt.Errorf("failed to parse users from `-auth.config=%q`: %s", *authConfigPath, err)
 	}
-	logger.Infof("Loaded information about %d users from %q", len(m), path)
-	return m, nil
+	logger.Infof("Loaded information about %d users from %q", len(m), *authConfigPath)
+
+	authConfig.Store(ac)
+	authUsers.Store(&m)
+
+	return nil
 }
 
-func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
-	var err error
-	data, err = envtemplate.ReplaceBytes(data)
+func readAuthConfig(path string) (*AuthConfig, error) {
+	data, err := fs.ReadFileOrHTTP(path)
+	if err != nil {
+		return nil, err
+	}
+	return parseAuthConfig(data)
+}
+
+func parseAuthConfig(data []byte) (*AuthConfig, error) {
+	data, err := envtemplate.ReplaceBytes(data)
 	if err != nil {
 		return nil, fmt.Errorf("cannot expand environment vars: %w", err)
 	}
 	var ac AuthConfig
-	if err := yaml.UnmarshalStrict(data, &ac); err != nil {
+	if err = yaml.UnmarshalStrict(data, &ac); err != nil {
 		return nil, fmt.Errorf("cannot unmarshal AuthConfig data: %w", err)
 	}
+	return &ac, nil
+}
+
+func parseAuthConfigUsers(ac *AuthConfig) (map[string]*UserInfo, error) {
 	uis := ac.Users
 	if len(uis) == 0 {
 		return nil, fmt.Errorf("`users` section cannot be empty in AuthConfig")
diff --git a/app/vmauth/main.go b/app/vmauth/main.go
index 4b400c063..7281527c8 100644
--- a/app/vmauth/main.go
+++ b/app/vmauth/main.go
@@ -94,7 +94,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
 		authToken = strings.Replace(authToken, "Token", "Bearer", 1)
 	}
 
-	ac := authConfig.Load().(map[string]*UserInfo)
+	ac := *authUsers.Load()
 	ui := ac[authToken]
 	if ui == nil {
 		invalidAuthTokenRequests.Inc()
diff --git a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration.tsx b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration.tsx
index ec57de90e..5f32aa2d3 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration.tsx
+++ b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration.tsx
@@ -4,13 +4,13 @@ import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
 import { ArrowDownIcon, StorageIcon } from "../../../Main/Icons";
 import Button from "../../../Main/Button/Button";
 import "./style.scss";
-import { replaceTenantId } from "../../../../utils/default-server-url";
 import classNames from "classnames";
 import Popper from "../../../Main/Popper/Popper";
 import { getAppModeEnable } from "../../../../utils/app-mode";
 import Tooltip from "../../../Main/Tooltip/Tooltip";
 import useDeviceDetect from "../../../../hooks/useDeviceDetect";
 import TextField from "../../../Main/TextField/TextField";
+import { getTenantIdFromUrl, replaceTenantId } from "../../../../utils/tenants";
 
 const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
   const appModeEnable = getAppModeEnable();
@@ -35,15 +35,7 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
     }
   }, [search, accountIds]);
 
-  const getTenantIdFromUrl = (url: string) => {
-    const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
-    return (url.match(regexp) || [])[2];
-  };
-
-  const showTenantSelector = useMemo(() => {
-    const id = true; //getTenantIdFromUrl(serverUrl);
-    return accountIds.length > 1 && id;
-  }, [accountIds, serverUrl]);
+  const showTenantSelector = useMemo(() => accountIds.length > 1, [accountIds]);
 
   const toggleOpenOptions = () => {
     setOpenOptions(prev => !prev);
diff --git a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds.ts b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds.ts
index 994ad370f..a2ac67faf 100644
--- a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds.ts
+++ b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds.ts
@@ -2,10 +2,12 @@ import { useAppState } from "../../../../../state/common/StateContext";
 import { useEffect, useMemo, useState } from "preact/compat";
 import { ErrorTypes } from "../../../../../types";
 import { getAccountIds } from "../../../../../api/accountId";
-import { getAppModeParams } from "../../../../../utils/app-mode";
+import { getAppModeEnable, getAppModeParams } from "../../../../../utils/app-mode";
+import { getTenantIdFromUrl } from "../../../../../utils/tenants";
 
 export const useFetchAccountIds = () => {
   const { useTenantID } = getAppModeParams();
+  const appModeEnable = getAppModeEnable();
   const { serverUrl } = useAppState();
 
   const [isLoading, setIsLoading] = useState(false);
@@ -13,9 +15,11 @@ export const useFetchAccountIds = () => {
   const [accountIds, setAccountIds] = useState<string[]>([]);
 
   const fetchUrl = useMemo(() => getAccountIds(serverUrl), [serverUrl]);
+  const isServerUrlWithTenant = useMemo(() => !!getTenantIdFromUrl(serverUrl), [serverUrl]);
+  const preventFetch = appModeEnable ? !useTenantID : !isServerUrlWithTenant;
 
   useEffect(() => {
-    if (!useTenantID) return;
+    if (preventFetch) return;
     const fetchData = async () => {
       setIsLoading(true);
       try {
diff --git a/app/vmui/packages/vmui/src/utils/tenants.ts b/app/vmui/packages/vmui/src/utils/tenants.ts
new file mode 100644
index 000000000..89133d165
--- /dev/null
+++ b/app/vmui/packages/vmui/src/utils/tenants.ts
@@ -0,0 +1,9 @@
+const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
+
+export const replaceTenantId = (serverUrl: string, tenantId: string) => {
+  return serverUrl.replace(regexp, `$1${tenantId}/$4`);
+};
+
+export const getTenantIdFromUrl = (url: string): string => {
+  return url.match(regexp)?.[2] || "";
+};
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 06366daf6..3369446d3 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -27,6 +27,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
 * BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl.html): fix performance issue when migrating data from VictoriaMetrics according to [these docs](https://docs.victoriametrics.com/vmctl.html#migrating-data-from-victoriametrics). Add the ability to speed up the data migration via `--vm-native-disable-retries` command-line flag. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4092).
 * BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): fix a panic when the duration in the query contains uppercase `M` suffix. Such a suffix isn't allowed to use in durations, since it clashes with `a million` suffix, e.g. it isn't clear whether `rate(metric[5M])` means rate over 5 minutes, 5 months or 5 million seconds. See [this](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3589) and [this](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4120) issues.
 * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): retry failed read request on the closed connection one more time. This improves rules execution reliability when connection between vmalert and datasource closes unexpectedly.
+* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix the display of the tenant selector. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4160).
 
 ## [v1.90.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.90.0)
 


Test patch:
diff --git a/app/vmauth/auth_config_test.go b/app/vmauth/auth_config_test.go
index 6771513ef..8c6664059 100644
--- a/app/vmauth/auth_config_test.go
+++ b/app/vmauth/auth_config_test.go
@@ -13,7 +13,11 @@ import (
 func TestParseAuthConfigFailure(t *testing.T) {
 	f := func(s string) {
 		t.Helper()
-		_, err := parseAuthConfig([]byte(s))
+		ac, err := parseAuthConfig([]byte(s))
+		if err != nil {
+			return
+		}
+		_, err = parseAuthConfigUsers(ac)
 		if err == nil {
 			t.Fatalf("expecting non-nil error")
 		}
@@ -202,7 +206,11 @@ users:
 func TestParseAuthConfigSuccess(t *testing.T) {
 	f := func(s string, expectedAuthConfig map[string]*UserInfo) {
 		t.Helper()
-		m, err := parseAuthConfig([]byte(s))
+		ac, err := parseAuthConfig([]byte(s))
+		if err != nil {
+			t.Fatalf("unexpected error: %s", err)
+		}
+		m, err := parseAuthConfigUsers(ac)
 		if err != nil {
 			t.Fatalf("unexpected error: %s", err)
 		}