Skip to content
Open

Vtadmin2 #20946

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/common/scripts/vtadmin-down.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@

source "$(dirname "${BASH_SOURCE[0]:-$0}")/../env.sh"

stop_process "vtadmin2" "$VTDATAROOT/tmp/vtadmin2.pid"
stop_process "vtadmin-web" "$VTDATAROOT/tmp/vtadmin-web.pid"
stop_process "vtadmin-api" "$VTDATAROOT/tmp/vtadmin-api.pid"
58 changes: 58 additions & 0 deletions examples/common/scripts/vtadmin2-up.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/bash

# Copyright 2026 The Vitess Authors.
#
# 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
#
# http://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.

function output() {
echo -e "$@"
}

script_dir="$(dirname "${BASH_SOURCE[0]:-$0}")"
source "${script_dir}/../env.sh"

cluster_name="local"
log_dir="${VTDATAROOT}/tmp"
vtadmin2_port=14202

case_insensitive_hostname=$(echo "$hostname" | tr '[:upper:]' '[:lower:]')

output "\n\033[1;32mStarting vtadmin2 on http://${case_insensitive_hostname}:${vtadmin2_port}\033[0m"

vtadmin2 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ship vtadmin2 before starting it

This script is now started by examples/local/101_initial_cluster.sh by default, but it invokes a standalone vtadmin2 binary. I checked the install and release copy lists (Makefile install/install-local and tools/make-release-packages.sh), and they still copy/package vtadmin but not vtadmin2, so users running the packaged examples from an install/release tree will fail here with vtadmin2: command not found unless they know to set SKIP_VTADMIN2.

Useful? React with 👍 / 👎.

--addr "${case_insensitive_hostname}:${vtadmin2_port}" \
--logtostderr \
--alsologtostderr \
--rbac \
--rbac-config="${script_dir}/../vtadmin/rbac.yaml" \
--cluster "id=${cluster_name},name=${cluster_name},discovery=staticfile,discovery-staticfile-path=${script_dir}/../vtadmin/discovery.json,tablet-fqdn-tmpl=http://{{ .Tablet.Hostname }}:15{{ .Tablet.Alias.Uid }},schema-cache-default-expiration=1m" \
> "${log_dir}/vtadmin2.out" 2>&1 &

vtadmin2_pid=$!
echo ${vtadmin2_pid} > "${log_dir}/vtadmin2.pid"

for _ in {0..100}; do
if curl -s "http://${case_insensitive_hostname}:${vtadmin2_port}/clusters" | grep -q "${cluster_name}"; then
break
fi
sleep 0.1
done

curl -s "http://${case_insensitive_hostname}:${vtadmin2_port}/clusters" | grep -q "${cluster_name}" || fail "vtadmin2 failed to discover the running example Vitess cluster."

echo "\
vtadmin2 is running!
- Browser: http://${case_insensitive_hostname}:${vtadmin2_port}
- Logs: ${log_dir}/vtadmin2.out
- PID: ${vtadmin2_pid}
"
6 changes: 6 additions & 0 deletions examples/local/101_initial_cluster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ else
../common/scripts/vtadmin-up.sh
fi

# start vtadmin2
if [[ -n ${SKIP_VTADMIN2} ]]; then
echo -e "\nSkipping VTAdmin2! If this is not what you want then please unset the SKIP_VTADMIN2 env variable in your shell."
else
../common/scripts/vtadmin2-up.sh
fi
86 changes: 83 additions & 3 deletions go/cmd/vtadmin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ limitations under the License.
package main

import (
"context"
"errors"
"flag"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"syscall"
"time"

"github.com/spf13/cobra"
Expand All @@ -38,6 +43,7 @@ import (
vtadminhttp "vitess.io/vitess/go/vt/vtadmin/http"
"vitess.io/vitess/go/vt/vtadmin/http/debug"
"vitess.io/vitess/go/vt/vtadmin/rbac"
"vitess.io/vitess/go/vt/vtadmin/vtadmin2"
"vitess.io/vitess/go/vt/vtctl/grpcclientcommon"
"vitess.io/vitess/go/vt/vtenv"
)
Expand All @@ -56,11 +62,21 @@ var (

cacheRefreshKey string

// Server-rendered UI (vtadmin2) options.
ui string
uiAddr string
uiReadOnly bool
uiDebugJSON bool

traceCloser io.Closer = &noopCloser{}

rootCmd = &cobra.Command{
Use: "vtadmin",
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := validateUIOptions(ui, enableDynamicClusters); err != nil {
return err
}

_flag.TrickGlog()

if err := log.Init(cmd.Flags()); err != nil {
Expand Down Expand Up @@ -107,6 +123,16 @@ func startTracing(cmd *cobra.Command) {
traceCloser = trace.StartTracing("vtadmin")
}

func validateUIOptions(ui string, enableDynamicClusters bool) error {
if ui != "react" && ui != "vtadmin2" {
return fmt.Errorf("invalid --ui value %q: want react or vtadmin2", ui)
}
if ui == "vtadmin2" && enableDynamicClusters {
return errors.New("--enable-dynamic-clusters is not supported with --ui=vtadmin2")
}
return nil
}

func run(cmd *cobra.Command, args []string) {
bootSpan, ctx := trace.NewSpan(cmd.Context(), "vtadmin.boot")
defer bootSpan.Finish()
Expand Down Expand Up @@ -164,6 +190,53 @@ func run(cmd *cobra.Command, args []string) {
RBAC: rbacConfig,
EnableDynamicClusters: enableDynamicClusters,
})

// The vtadmin2 server reuses the same API implementation (and its RBAC
// enforcement) and runs alongside the JSON API on its own address, so both
// UIs can be served during the migration.
if ui == "vtadmin2" {
uiServer, err := vtadmin2.NewServer(s, vtadmin2.Options{
Addr: uiAddr,
ReadOnly: uiReadOnly,
DocumentTitle: "VTAdmin",
EnableDebugJSON: uiDebugJSON,
Authenticator: rbacConfig.GetAuthenticator(),
})
if err != nil {
fatal(err)
}

httpServer := vtadmin2.NewHTTPServer(uiAddr, uiServer)

uiErr := make(chan error, 1)
shutdownBase := context.WithoutCancel(cmd.Context())
shutdownUI := func() {
shutdownCtx, cancel := context.WithTimeout(shutdownBase, 5*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("vtadmin2 UI shutdown failed", slog.Any("error", err))
}
}
servenv.OnTermSync(shutdownUI)

go func() {
slog.Info("vtadmin2 UI listening", slog.String("addr", uiAddr))
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
uiErr <- err
}
}()

defer shutdownUI()

go func() {
if err := <-uiErr; err != nil {
slog.Error("vtadmin2 UI server failed", slog.Any("error", err))
if signalErr := syscall.Kill(syscall.Getpid(), syscall.SIGTERM); signalErr != nil {
slog.Error("failed to signal vtadmin shutdown", slog.Any("error", signalErr))
}
}
}()
}
bootSpan.Finish()

if err := s.ListenAndServe(); err != nil {
Expand All @@ -183,6 +256,12 @@ func registerFlags() {
rootCmd.Flags().Var(&defaultClusterConfig, "cluster-defaults", "default options for all clusters")
rootCmd.Flags().BoolVar(&enableDynamicClusters, "enable-dynamic-clusters", false, "whether to enable dynamic clusters that are set by request header cookies or gRPC metadata")

// Server-rendered UI flags
rootCmd.Flags().StringVar(&ui, "ui", "react", "admin UI to serve: react (default SPA) or vtadmin2 (server-rendered UI)")
rootCmd.Flags().StringVar(&uiAddr, "ui-addr", ":15001", "address for the vtadmin2 UI to listen on (used with --ui=vtadmin2)")
rootCmd.Flags().BoolVar(&uiReadOnly, "ui-read-only", false, "run the vtadmin2 UI in read-only mode (used with --ui=vtadmin2)")
rootCmd.Flags().BoolVar(&uiDebugJSON, "ui-debug-json", false, "enable ?format=json page data output in the vtadmin2 UI (used with --ui=vtadmin2)")

// Tracing flags
trace.RegisterFlags(rootCmd.Flags()) // defined in go/vt/trace
utils.SetFlagBoolVar(rootCmd.Flags(), &opts.EnableTracing, "grpc-tracing", false, "whether to enable tracing on the gRPC server")
Expand All @@ -201,7 +280,8 @@ func registerFlags() {
"HTTP endpoint to expose prometheus metrics on. Omit to disable scraping metrics. "+
"Using a path used by VTAdmin's http API is unsupported and causes undefined behavior.")
rootCmd.Flags().StringSliceVar(&httpOpts.CORSOrigins, "http-origin", []string{}, "repeated, comma-separated flag of allowed CORS origins. omit to disable CORS")
rootCmd.Flags().StringVar(&httpOpts.ExperimentalOptions.TabletURLTmpl,
rootCmd.Flags().StringVar(
&httpOpts.ExperimentalOptions.TabletURLTmpl,
"http-tablet-url-tmpl",
"https://{{ .Tablet.Hostname }}:80",
"[EXPERIMENTAL] Go template string to generate a reachable http(s) "+
Expand All @@ -211,8 +291,8 @@ func registerFlags() {

// RBAC flags
rootCmd.Flags().StringVar(&rbacConfigPath, "rbac-config", "", "path to an RBAC config file. must be set if passing --rbac")
rootCmd.Flags().BoolVar(&enableRBAC, "rbac", false, "whether to enable RBAC. must be set if not passing --rbac")
rootCmd.Flags().BoolVar(&disableRBAC, "no-rbac", false, "whether to disable RBAC. must be set if not passing --no-rbac")
rootCmd.Flags().BoolVar(&enableRBAC, "rbac", false, "whether to enable RBAC. must be set if not passing --no-rbac")
rootCmd.Flags().BoolVar(&disableRBAC, "no-rbac", false, "whether to disable RBAC. must be set if not passing --rbac")

// Global cache flags (N.B. there are also cluster-specific cache flags)
cacheRefreshHelp := "instructs a request to ignore any cached data (if applicable) and refresh the cache;" +
Expand Down
7 changes: 7 additions & 0 deletions go/cmd/vtadmin/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import (
"github.com/stretchr/testify/require"
)

func TestValidateUIOptions(t *testing.T) {
assert.NoError(t, validateUIOptions("react", false))
assert.NoError(t, validateUIOptions("vtadmin2", false))
assert.ErrorContains(t, validateUIOptions("unknown", false), "invalid --ui value")
assert.ErrorContains(t, validateUIOptions("vtadmin2", true), "not supported")
}

func TestMainFlagRegistration(t *testing.T) {
registerFlags()

Expand Down
Loading
Loading