From f3094032da1371e37879dce1baeccc123220342a Mon Sep 17 00:00:00 2001 From: Yasuo Honda Date: Thu, 4 Jun 2026 16:48:00 +0900 Subject: [PATCH 1/5] cluster: don't fail SELinux status check when getenforce is unavailable #2496 added a getenforce-based SELinux status probe in addition to the config-file check. On hosts without the SELinux userspace tools (most Debian/Ubuntu systems, including the integration-test containers), getenforce is missing, so CheckSELinuxStatus failed. With --apply that failed check triggers the SELinux fix, which runs sed -i 's/.../' /etc/selinux/config && setenforce 0 but /etc/selinux/config does not exist on those hosts, so the command errors with 'sed: can't read /etc/selinux/config: No such file or directory' and the whole check/apply run aborts. This breaks the cluster and dm integration tests on master. Treat a getenforce execution error as 'SELinux disabled' (the host has no SELinux), matching the lenient pre-#2496 behavior, instead of a failure. Enforcing still fails and Permissive still warns. The configuration is still validated independently by CheckSELinuxConf. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/cluster/operation/check.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/cluster/operation/check.go b/pkg/cluster/operation/check.go index f0e69e111e..5928d0b410 100644 --- a/pkg/cluster/operation/check.go +++ b/pkg/cluster/operation/check.go @@ -608,9 +608,15 @@ func CheckSELinuxStatus(ctx context.Context, e ctxt.Executor, sudo bool) *CheckR Command: "getenforce", Sudo: sudo, }) - stdout, stderr, err := m.Execute(ctx, e) + stdout, _, err := m.Execute(ctx, e) if err != nil { - result.Err = fmt.Errorf("%w %s", err, stderr) + // getenforce is unavailable (e.g. SELinux userspace tools are not + // installed, as on most Debian/Ubuntu hosts), which means SELinux is + // not enforcing on this host. Treat it as disabled rather than a + // failure, so we don't trigger a fix that edits a non-existent + // /etc/selinux/config. The configuration is still checked separately + // by CheckSELinuxConf. + result.Msg = "getenforce not available, assuming SELinux is disabled" return result } out := strings.Trim(string(stdout), "\n") From e976f74f3bc5f4077c74793277edf712bec5af37 Mon Sep 17 00:00:00 2001 From: Yasuo Honda Date: Thu, 4 Jun 2026 16:58:20 +0900 Subject: [PATCH 2/5] cluster: skip grubby in THP fix when it is not installed #2498 added a persistent THP disable via grubby --update-kernel=ALL --args="transparent_hugepage=never" to the THP check auto-fix. grubby is a RHEL-family tool and is not present on Debian/Ubuntu (including the integration-test containers), so with --apply the fix aborts with 'grubby: command not found' (exit 127) and the whole check/apply run fails. Guard the grubby invocation with 'command -v grubby' so the persistent kernel argument is still set where grubby exists, but is skipped on hosts without it. The runtime THP setting (echo never > .../enabled) still applies everywhere. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/cluster/manager/check.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cluster/manager/check.go b/pkg/cluster/manager/check.go index 0559aa8f90..99addd799e 100644 --- a/pkg/cluster/manager/check.go +++ b/pkg/cluster/manager/check.go @@ -686,7 +686,10 @@ func fixFailedChecks(host string, res *operator.CheckResult, t *task.Builder, sy case operator.CheckNameTHP: t.Shell(host, fmt.Sprintf( - `if [ -d %[1]s ]; then echo never > %[1]s/enabled; fi && %s`, + // grubby only exists on RHEL-family distros; skip the persistent + // kernel argument when it's not available (e.g. Debian/Ubuntu) + // instead of failing the whole apply. + `if [ -d %[1]s ]; then echo never > %[1]s/enabled; fi && if command -v grubby >/dev/null 2>&1; then %s; fi`, "/sys/kernel/mm/transparent_hugepage", `grubby --update-kernel=ALL --args="transparent_hugepage=never"`, ), From 1c62a2000460573071afe8aa00a0bea6f0183cd4 Mon Sep 17 00:00:00 2001 From: Yasuo Honda Date: Thu, 4 Jun 2026 14:38:48 +0900 Subject: [PATCH 3/5] fix: auto-bootstrap root.json and create bin dir on first run When tiup is installed via mise (https://mise.jdx.dev, a polyglot tool version manager similar to asdf) with: mise install github:pingcap/tiup mise use -g github:pingcap/tiup the install.sh script is not executed, which means: 1. ~/.tiup/bin/ directory is not created 2. root.json is not downloaded from the mirror This leaves the installation incomplete and requires the user to manually run 'tiup mirror set' before any tiup command works. This patch fixes both issues: - ResetMirror now ensures the bin directory exists before writing root.json - InitEnv automatically bootstraps root.json from the default mirror if it's missing, so no manual 'tiup mirror set' is needed In the future, we would like to register tiup in the mise registry so that users can simply run: mise install tiup mise use tiup This fix is a prerequisite for that, as it removes the need for any post-install initialization steps. --- pkg/environment/env.go | 25 ++++++++++++++++++++----- pkg/localdata/profile.go | 5 +++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pkg/environment/env.go b/pkg/environment/env.go index 8ea388cc3e..352f959a18 100644 --- a/pkg/environment/env.go +++ b/pkg/environment/env.go @@ -14,6 +14,7 @@ package environment import ( + "errors" "fmt" "os" "path/filepath" @@ -21,7 +22,7 @@ import ( "strings" "time" - "github.com/pingcap/errors" + perrs "github.com/pingcap/errors" "github.com/pingcap/tiup/pkg/localdata" "github.com/pingcap/tiup/pkg/repository" "github.com/pingcap/tiup/pkg/repository/v1manifest" @@ -144,7 +145,21 @@ func InitEnv(options repository.Options, mOpt repository.MirrorOptions) (*Enviro var local v1manifest.LocalManifests local, err = v1manifest.NewManifests(profile) if err != nil { - return nil, errors.Annotatef(err, "initial repository from mirror(%s) failed", mirrorAddr) + if errors.Is(err, v1manifest.ErrLoadManifest) { + // Only bootstrap root.json if the file is actually missing. + // If the file exists but is corrupted, preserve the original error. + rootPath := profile.Path("bin", "root.json") + if _, statErr := os.Stat(rootPath); os.IsNotExist(statErr) { + // Use the configured mirrorAddr so that custom/test mirrors are respected. + if err := profile.ResetMirror(mirrorAddr, ""); err != nil { + return nil, perrs.Annotatef(err, "initial repository from mirror(%s) failed", mirrorAddr) + } + local, err = v1manifest.NewManifests(profile) + } + } + if err != nil { + return nil, perrs.Annotatef(err, "initial repository from mirror(%s) failed", mirrorAddr) + } } v1repo = repository.NewV1Repo(mirror, options, local) @@ -222,7 +237,7 @@ func (env *Environment) SelectInstalledVersion(component string, ver utils.Versi versions := []string{} for _, v := range installed { vi, err := env.v1Repo.LocalComponentVersion(component, v, true) - if errors.Cause(err) == repository.ErrUnknownVersion { + if perrs.Cause(err) == repository.ErrUnknownVersion { continue } if err != nil { @@ -238,9 +253,9 @@ func (env *Environment) SelectInstalledVersion(component string, ver utils.Versi return semver.Compare(versions[i], versions[j]) > 0 }) - errInstallFirst := errors.Annotatef(ErrInstallFirst, "use `tiup install %s` to install component `%s` first", component, component) + errInstallFirst := perrs.Annotatef(ErrInstallFirst, "use `tiup install %s` to install component `%s` first", component, component) if !ver.IsEmpty() { - errInstallFirst = errors.Annotatef(ErrInstallFirst, "use `tiup install %s:%s` to install specified version", component, ver.String()) + errInstallFirst = perrs.Annotatef(ErrInstallFirst, "use `tiup install %s:%s` to install specified version", component, ver.String()) } if ver.IsEmpty() || string(ver) == utils.NightlyVersionAlias { diff --git a/pkg/localdata/profile.go b/pkg/localdata/profile.go index 1712536fb5..0d29550c01 100644 --- a/pkg/localdata/profile.go +++ b/pkg/localdata/profile.go @@ -224,6 +224,11 @@ func (p *Profile) VersionIsInstalled(component, version string) (bool, error) { // ResetMirror reset root.json and cleanup manifests directory func (p *Profile) ResetMirror(addr, root string) error { + // Ensure bin directory exists + if err := os.MkdirAll(p.Path("bin"), 0755); err != nil { + return err + } + // Calculating root.json path shaWriter := sha256.New() if _, err := io.Copy(shaWriter, strings.NewReader(addr)); err != nil { From c9a6c2b78c33267e8968be504fc05a4a8ffc1e03 Mon Sep 17 00:00:00 2001 From: Yasuo Honda Date: Thu, 4 Jun 2026 16:46:43 +0900 Subject: [PATCH 4/5] env: keep ErrInstallFirst as a pingcap error and clarify bootstrap comment After aliasing the pingcap/errors import to perrs (to free the name for the stdlib errors used by errors.Is), the package-level ErrInstallFirst sentinel silently switched to stdlib errors.New. Use perrs.New to preserve the prior behavior. Also reword the bootstrap comment: a present-but-corrupt root.json never yields ErrLoadManifest, so the os.Stat guard actually covers the exists-but-unreadable case. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/environment/env.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/environment/env.go b/pkg/environment/env.go index 352f959a18..c88150823a 100644 --- a/pkg/environment/env.go +++ b/pkg/environment/env.go @@ -33,7 +33,7 @@ import ( var ( // ErrInstallFirst indicates that a component/version is not installed - ErrInstallFirst = errors.New("component not installed") + ErrInstallFirst = perrs.New("component not installed") ) // EnvList is the canonical allowlist of environment variables TiUP will print or expose. @@ -146,8 +146,9 @@ func InitEnv(options repository.Options, mOpt repository.MirrorOptions) (*Enviro local, err = v1manifest.NewManifests(profile) if err != nil { if errors.Is(err, v1manifest.ErrLoadManifest) { - // Only bootstrap root.json if the file is actually missing. - // If the file exists but is corrupted, preserve the original error. + // Only bootstrap root.json when the file is actually missing. + // If it exists but can't be read (e.g. permissions), preserve the + // original error rather than overwriting it. rootPath := profile.Path("bin", "root.json") if _, statErr := os.Stat(rootPath); os.IsNotExist(statErr) { // Use the configured mirrorAddr so that custom/test mirrors are respected. From c4ac960f700ca554f29d7bba1b839628318cbb9a Mon Sep 17 00:00:00 2001 From: Yasuo Honda Date: Thu, 11 Jun 2026 17:57:39 +0900 Subject: [PATCH 5/5] ci: trigger re-run