Files
zeekayandHanzo Dev cd4e9a11bb Rename the environment contract to ZT_
Every variable the code reads or sets is now ZT_* with the same suffix,
with no aliases for the old names. User-facing text, default CSR fields
and error messages follow, as do the identifiers on the lines that
changed.

Three names keep their spelling because another program owns them: the
C tunneler reads ZITI_LOG and ZITI_TIME_FORMAT, and the Go SDK reads
ZITI_TRACE_ENABLED.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 00:57:09 -07:00

350 lines
9.6 KiB
Go

package getzt
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/blang/semver"
"github.com/go-resty/resty/v2"
"github.com/hanzozt/foundation/v2/versions"
c "github.com/hanzozt/zt/v2/zt/constants"
"github.com/michaelquigley/pfxlog"
"github.com/pkg/errors"
)
// GitHubReleasesData is used to parse the '/releases/latest' response from GitHub
type GitHubReleasesData struct {
Version string `json:"tag_name"`
SemVer semver.Version
Assets []struct {
BrowserDownloadURL string `json:"browser_download_url"`
}
}
func (self *GitHubReleasesData) GetDownloadUrl(appName string, targetOS, targetArch string) (string, error) {
arches := []string{targetArch}
if strings.ToLower(targetArch) == "amd64" {
arches = append(arches, "x86_64")
}
for _, asset := range self.Assets {
if !strings.HasSuffix(asset.BrowserDownloadURL, ".zip") &&
!strings.HasSuffix(asset.BrowserDownloadURL, ".tar.gz") {
continue
}
ok := false
for _, arch := range arches {
if strings.Contains(strings.ToLower(asset.BrowserDownloadURL), arch) {
ok = true
}
}
ok = ok && strings.Contains(strings.ToLower(asset.BrowserDownloadURL), targetOS)
if ok {
return asset.BrowserDownloadURL, nil
}
}
return "", errors.Errorf("no download URL found for os/arch %s/%s for '%s'", targetOS, targetArch, appName)
}
func NewClient() *resty.Client {
// Use a 2-second timeout with a retry count of 5
return resty.
New().
SetTimeout(2 * time.Second).
SetRetryCount(5).
SetRedirectPolicy(resty.FlexibleRedirectPolicy(15))
}
func getRequest(verbose bool) *resty.Request {
request := NewClient().
SetDebug(verbose).
R()
if githubToken := os.Getenv("GITHUB_TOKEN"); githubToken != "" {
request = request.SetHeader("Authorization", fmt.Sprintf("Bearer %s", githubToken))
}
return request
}
func GetLatestGitHubReleaseVersion(org, ztApp string, verbose bool) (semver.Version, error) {
var result semver.Version
release, err := GetHighestVersionGitHubReleaseInfo(org, ztApp, verbose)
if release != nil {
result = release.SemVer
}
return result, err
}
func GetHighestVersionGitHubReleaseInfo(org, appName string, verbose bool) (*GitHubReleasesData, error) {
resp, err := getRequest(verbose).
SetQueryParams(map[string]string{}).
SetHeader("Accept", "application/vnd.github.v3+json").
SetResult([]*GitHubReleasesData{}).
Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", org, appName))
if err != nil {
return nil, errors.Wrapf(err, "unable to get latest version for '%s'", appName)
}
if resp.StatusCode() == http.StatusNotFound {
return nil, errors.Errorf("unable to get latest version for '%s'; Not Found (invalid URL)", appName)
}
if resp.StatusCode() != http.StatusOK {
return nil, errors.Errorf("unable to get latest version for '%s'; return status=%s", appName, resp.Status())
}
result := *resp.Result().(*[]*GitHubReleasesData)
return GetHighestVersionRelease(appName, result)
}
func GetHighestVersionRelease(appName string, releases []*GitHubReleasesData) (*GitHubReleasesData, error) {
for _, release := range releases {
v, err := semver.ParseTolerant(release.Version)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse version %v for '%v'", release.Version, appName)
}
release.SemVer = v
}
sort.Slice(releases, func(i, j int) bool {
return releases[i].SemVer.GT(releases[j].SemVer) // sort in reverse order
})
if len(releases) == 0 {
return nil, errors.Errorf("no releases found for '%v'", appName)
}
return releases[0], nil
}
func GetLatestGitHubReleaseAsset(org, appName string, appGitHub string, version string, verbose bool) (*GitHubReleasesData, error) {
if version != "latest" {
if appName == "zt-prox-c" {
version = strings.TrimPrefix(version, "v")
}
if appName == "zt" || appName == "zt-edge-tunnel" || appName == "zrok" || appName == "caddy" {
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
}
}
if version != "latest " {
version = "tags/" + version
}
ctx, cancelF := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelF()
resp, err := getRequest(verbose).
SetContext(ctx).
SetQueryParams(map[string]string{}).
SetHeader("Accept", "application/vnd.github.v3+json").
SetResult(&GitHubReleasesData{}).
Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/%s", org, appGitHub, version))
if err != nil {
return nil, fmt.Errorf("unable to get latest version for '%s'; %s", appName, err)
}
if resp.StatusCode() == http.StatusNotFound {
return nil, fmt.Errorf("unable to get latest version for '%s'; Not Found", appName)
}
if resp.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("unable to get latest version for '%s'; %s", appName, resp.Status())
}
result := resp.Result().(*GitHubReleasesData)
return result, nil
}
// DownloadGitHubReleaseAsset will download a file from the given GitHUb release area
func DownloadGitHubReleaseAsset(fullUrl string, filepath string) (err error) {
resp, err := resty.
New().
SetTimeout(time.Minute).
SetRetryCount(2).
SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)).
R().
SetOutput(filepath).
Get(fullUrl)
if err != nil {
return fmt.Errorf("unable to download '%s', %s", fullUrl, err)
}
if resp.IsError() {
return fmt.Errorf("unable to download file, error HTTP status code [%d] returned for url [%s]", resp.StatusCode(), fullUrl)
}
return nil
}
func FindVersionAndInstallGitHubRelease(org, app string, ztAppGitHub string, targetOS, targetArch string, binDir string, version string, verbose bool) error {
releaseVersion := version
if version != "" && version != "latest" {
if _, err := semver.Make(strings.TrimPrefix(version, "v")); err != nil {
return err
}
} else {
version = "latest"
v, err := GetLatestGitHubReleaseVersion(org, app, verbose)
if err != nil {
return err
}
releaseVersion = v.String()
}
release, err := GetLatestGitHubReleaseAsset(org, app, ztAppGitHub, releaseVersion, verbose)
if err != nil {
return err
}
return InstallGitHubRelease(app, release, binDir, targetOS, targetArch, version)
}
func InstallGitHubRelease(ztApp string, release *GitHubReleasesData, binDir string, targetOS, targetArch, version string) error {
releaseUrl, err := release.GetDownloadUrl(ztApp, targetOS, targetArch)
if err != nil {
return err
}
parsedUrl, err := url.Parse(releaseUrl)
if err != nil {
return err
}
fileName := filepath.Base(parsedUrl.Path)
fullPath := filepath.Join(binDir, fileName)
err = DownloadGitHubReleaseAsset(releaseUrl, fullPath)
if err != nil {
return err
}
defer func() {
err = os.Remove(fullPath)
if err != nil {
pfxlog.Logger().WithError(err).Errorf("failed to removed downloaded release archive %s", fullPath)
}
}()
if strings.HasSuffix(fileName, ".zip") {
if ztApp == c.ZT_EDGE_TUNNEL {
count := 0
ztFileName := "zt-edge-tunnel-" + version
err = Unzip(fullPath, binDir, func(path string) (string, bool) {
if path == "zt-edge-tunnel" {
count++
return ztFileName, true
}
return "", false
})
if err != nil {
return err
}
if count != 1 {
return errors.Errorf("didn't find zt-edge-tunnel executable in release archive. count: %v", count)
}
pfxlog.Logger().Infof("Successfully installed '%s' version '%s' to %s", ztApp, release.Version, filepath.Join(binDir, ztFileName))
return nil
} else {
return errors.Errorf("unsupported application '%s'", ztApp)
}
} else if strings.HasSuffix(fileName, ".tar.gz") {
if ztApp == c.ZT {
count := 0
ztFileName := "zt-" + version
expectedPath := "zt"
if version != "latest" {
semVer, err := versions.ParseSemVer(version)
if err != nil {
return err
}
pathChangedVersion := versions.MustParseSemVer("0.29.0")
if semVer.CompareTo(pathChangedVersion) < 0 {
expectedPath = "zt/zt"
}
}
err = UnTarGz(fullPath, binDir, func(path string) (string, bool) {
if path == expectedPath {
count++
return ztFileName, true
}
return "", false
})
if err != nil {
return err
}
if count != 1 {
return errors.Errorf("didn't find zt executable in release archive. count: %v", count)
}
pfxlog.Logger().Infof("Successfully installed '%s' version '%s' to %s", ztApp, release.Version, filepath.Join(binDir, ztFileName))
return nil
} else if ztApp == c.ZROK {
count := 0
ztFileName := "zrok-" + version
expectedPath := "zrok"
err = UnTarGz(fullPath, binDir, func(path string) (string, bool) {
if path == expectedPath {
count++
return ztFileName, true
}
return "", false
})
if err != nil {
return err
}
if count != 1 {
return errors.Errorf("didn't find zrok executable in release archive. count: %v", count)
}
pfxlog.Logger().Infof("Successfully installed '%s' version '%s' to %s", ztApp, release.Version, filepath.Join(binDir, ztFileName))
return nil
} else if ztApp == c.Caddy {
count := 0
ztFileName := "caddy-" + version
expectedPath := "caddy"
err = UnTarGz(fullPath, binDir, func(path string) (string, bool) {
if path == expectedPath {
count++
return ztFileName, true
}
return "", false
})
if err != nil {
return err
}
if count != 1 {
return errors.Errorf("didn't find caddy executable in release archive. count: %v", count)
}
pfxlog.Logger().Infof("Successfully installed '%s' version '%s' to %s", ztApp, release.Version, filepath.Join(binDir, ztFileName))
return nil
} else {
return errors.Errorf("unsupported application '%s'", ztApp)
}
} else {
return errors.Errorf("unsupported release file type '%s'", fileName)
}
}