Files
Paul Lorenz e8b42b89ae Add OIDC auth fablab test and supporting test infrastructure
- adds an oidc-auth-test fablab model that exercises OIDC authentication end to end, with an event-forwarder component, an oidc-test-client, a ziti-prox-c component, and OIDC event/gossip/traffic validations
- moves zitirest out of the public API into separate test-only shims: an integration-test shim (tests/restclient.go) and a fablab shim (zititest/zitirest), and repoints upgrade-test at the fablab shim
- adds region-isolation network partitioning and packet capture on all interfaces to the chaos toolkit
- queues oidc-test-client results reporting on a single sender goroutine with a bounded write deadline, so a stalled results circuit can't block the traffic loops
- counts results events dropped when that queue fills and reports them to the collector as errors, so a reporting outage fails validation instead of looking like a clean run
- bounds the event forwarder's event and keepalive writes, and checks for shutdown on every send attempt, so an unreadable destination can't park the forward loop holding its lock
- logs read errors on collector connections instead of ending collection for that client silently
- bounds the oidc-auth-test debug server's header and body reads
- matches ziti-prox-c process matching to the version-agnostic binary name, so a version change still finds the running process
- points upgrade-test's per-service terminator count at the shared validations helper
- adds a fablab design summary doc
2026-08-28 22:00:01 -04:00

398 lines
11 KiB
Go

package getziti
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/michaelquigley/pfxlog"
"github.com/openziti/foundation/v2/versions"
c "github.com/openziti/ziti/v2/ziti/constants"
"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, zitiApp string, verbose bool) (semver.Version, error) {
var result semver.Version
release, err := GetHighestVersionGitHubReleaseInfo(org, zitiApp, 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 == c.ZitiProxC {
version = strings.TrimPrefix(version, "v")
}
if appName == "ziti" || appName == "ziti-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
}
// FindVersionAndInstallGitHubRelease downloads and installs an executable from a GitHub release.
// app is the executable name, which may differ from zitiAppGitHub, the repository the releases are
// published under, as with ziti-prox-c releasing out of ziti-sdk-c. An empty or "latest" version
// resolves to the highest version released from that repository.
func FindVersionAndInstallGitHubRelease(org, app string, zitiAppGitHub 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, zitiAppGitHub, verbose)
if err != nil {
return err
}
releaseVersion = v.String()
}
release, err := GetLatestGitHubReleaseAsset(org, app, zitiAppGitHub, releaseVersion, verbose)
if err != nil {
return err
}
return InstallGitHubRelease(app, release, binDir, targetOS, targetArch, version)
}
func InstallGitHubRelease(zitiApp string, release *GitHubReleasesData, binDir string, targetOS, targetArch, version string) error {
releaseUrl, err := release.GetDownloadUrl(zitiApp, 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 zitiApp == c.ZitiEdgeTunnel {
count := 0
zitiFileName := "ziti-edge-tunnel-" + version
err = Unzip(fullPath, binDir, func(path string) (string, bool) {
if path == "ziti-edge-tunnel" {
count++
return zitiFileName, true
}
return "", false
})
if err != nil {
return err
}
if count != 1 {
return errors.Errorf("didn't find ziti-edge-tunnel executable in release archive. count: %v", count)
}
pfxlog.Logger().Infof("Successfully installed '%s' version '%s' to %s", zitiApp, release.Version, filepath.Join(binDir, zitiFileName))
return nil
} else if zitiApp == c.ZitiProxC {
count := 0
zitiFileName := "ziti-prox-c-" + version
err = Unzip(fullPath, binDir, func(path string) (string, bool) {
if path == "ziti-prox-c" {
count++
return zitiFileName, true
}
return "", false
})
if err != nil {
return err
}
if count != 1 {
return errors.Errorf("didn't find ziti-prox-c executable in release archive. count: %v", count)
}
pfxlog.Logger().Infof("Successfully installed '%s' version '%s' to %s", zitiApp, release.Version, filepath.Join(binDir, zitiFileName))
return nil
} else {
return errors.Errorf("unsupported application '%s'", zitiApp)
}
} else if strings.HasSuffix(fileName, ".tar.gz") {
if zitiApp == c.ZITI {
count := 0
zitiFileName := "ziti-" + version
expectedPath := "ziti"
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 = "ziti/ziti"
}
}
err = UnTarGz(fullPath, binDir, func(path string) (string, bool) {
if path == expectedPath {
count++
return zitiFileName, true
}
return "", false
})
if err != nil {
return err
}
if count != 1 {
return errors.Errorf("didn't find ziti executable in release archive. count: %v", count)
}
pfxlog.Logger().Infof("Successfully installed '%s' version '%s' to %s", zitiApp, release.Version, filepath.Join(binDir, zitiFileName))
return nil
} else if zitiApp == c.ZROK {
count := 0
zitiFileName := "zrok-" + version
expectedPath := "zrok"
err = UnTarGz(fullPath, binDir, func(path string) (string, bool) {
if path == expectedPath {
count++
return zitiFileName, 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", zitiApp, release.Version, filepath.Join(binDir, zitiFileName))
return nil
} else if zitiApp == c.Caddy {
count := 0
zitiFileName := "caddy-" + version
expectedPath := "caddy"
err = UnTarGz(fullPath, binDir, func(path string) (string, bool) {
if path == expectedPath {
count++
return zitiFileName, 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", zitiApp, release.Version, filepath.Join(binDir, zitiFileName))
return nil
} else if zitiApp == c.ZitiProxC {
count := 0
zitiFileName := "ziti-prox-c-" + version
expectedPath := "ziti-prox-c"
err = UnTarGz(fullPath, binDir, func(path string) (string, bool) {
if path == expectedPath {
count++
return zitiFileName, true
}
return "", false
})
if err != nil {
return err
}
if count != 1 {
return errors.Errorf("didn't find ziti-prox-c executable in release archive. count: %v", count)
}
pfxlog.Logger().Infof("Successfully installed '%s' version '%s' to %s", zitiApp, release.Version, filepath.Join(binDir, zitiFileName))
return nil
} else {
return errors.Errorf("unsupported application '%s'", zitiApp)
}
} else {
return errors.Errorf("unsupported release file type '%s'", fileName)
}
}