| package e2e |
| |
| import ( |
| "context" |
| "crypto/sha256" |
| "encoding/hex" |
| "errors" |
| "fmt" |
| "io" |
| "net/http" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "time" |
| ) |
| |
| const ( |
| maxOryArchiveDownloadSize int64 = 512 << 20 |
| |
| kratosVersion = "v26.2.0" |
| hydraVersion = "v26.2.0" |
| playwrightVersion = "v0.6100.0" |
| playwrightCLIVersion = "1.61.1" |
| chromiumRevision = "1228" |
| chromiumVersion = "149.0.7827.55" |
| ffmpegRevision = "1011" |
| ) |
| |
| type releaseArtifact struct { |
| Service string |
| Version string |
| Archive string |
| SHA256 string |
| URL string |
| } |
| |
| type platformArtifacts struct { |
| Kratos releaseArtifact |
| Hydra releaseArtifact |
| } |
| |
| // Hashes are from the official release checksum manifests: |
| // https://github.com/ory/kratos/releases/download/v26.2.0/checksums.txt |
| // https://github.com/ory/hydra/releases/download/v26.2.0/checksums.txt |
| var artifactPlatforms = map[string]platformArtifacts{ |
| "linux/amd64": platform("kratos_26.2.0-linux_sqlite_64bit.tar.gz", "c80113f2c861b2fbd80290697d1380000111e297716018617423cca7f2e7e668", "hydra_26.2.0-linux_sqlite_64bit.tar.gz", "28baf98c6d5da617c905d5ca77826fe863db9f9edab8f72664c69a56bde56b5a"), |
| "linux/arm64": platform("kratos_26.2.0-linux_sqlite_arm64.tar.gz", "fd2cedebc4034ad1aef726f64665052c809fb05f86968e9c8a36372d7578e865", "hydra_26.2.0-linux_sqlite_arm64.tar.gz", "18e73ac632d2081b76fbceb03130803a63bdb9b8e2ecc3f06f4a6e6820cabe26"), |
| "darwin/amd64": platform("kratos_26.2.0-macOS_sqlite_64bit.tar.gz", "0ac76d0ad85fe452e711a2aafe56b49b0f5d1a57200f4464dcb48e7267c94062", "hydra_26.2.0-macOS_sqlite_64bit.tar.gz", "ede453fa15c82cfb23e61b138a0b4d9558127a26ebf51eead97d0c58a7eff422"), |
| "darwin/arm64": platform("kratos_26.2.0-macOS_sqlite_arm64.tar.gz", "e198d84bde6e4b7c68714cdad00c8800edd88c2311adde8b08ea5469cea263ec", "hydra_26.2.0-macOS_sqlite_arm64.tar.gz", "d45f26be758196e66e98dff06a9eaefcd21d719123f74f43a0f1fabcfb71da60"), |
| } |
| |
| func platform(kratosArchive, kratosHash, hydraArchive, hydraHash string) platformArtifacts { |
| return platformArtifacts{ |
| Kratos: releaseArtifact{"kratos", kratosVersion, kratosArchive, kratosHash, "https://github.com/ory/kratos/releases/download/" + kratosVersion + "/" + kratosArchive}, |
| Hydra: releaseArtifact{"hydra", hydraVersion, hydraArchive, hydraHash, "https://github.com/ory/hydra/releases/download/" + hydraVersion + "/" + hydraArchive}, |
| } |
| } |
| |
| func artifactsFor(goos, goarch string) (platformArtifacts, error) { |
| p, ok := artifactPlatforms[goos+"/"+goarch] |
| if !ok { |
| return platformArtifacts{}, fmt.Errorf("unsupported E2E platform %s/%s (supported: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64)", goos, goarch) |
| } |
| return p, nil |
| } |
| |
| func repositoryDir() (string, error) { |
| _, source, _, ok := runtime.Caller(0) |
| if !ok { |
| return "", errors.New("locate e2e source file") |
| } |
| dir := filepath.Dir(filepath.Dir(source)) |
| if _, err := os.Stat(filepath.Join(dir, "go.mod")); err != nil { |
| return "", fmt.Errorf("locate auth-ui repository: %w", err) |
| } |
| return dir, nil |
| } |
| |
| func cacheRoot(repo string) string { |
| if root := os.Getenv("AUTH_UI_E2E_CACHE_DIR"); root != "" { |
| return root |
| } |
| return filepath.Join(repo, "e2e", "cache") |
| } |
| |
| func archiveCachePath(root string, artifact releaseArtifact) string { |
| return filepath.Join(root, "ory", artifact.Service, artifact.Version, artifact.Archive) |
| } |
| |
| func ensureArchive(ctx context.Context, client *http.Client, root string, artifact releaseArtifact, offline bool) (string, error) { |
| return ensureArchiveWithLimit(ctx, client, root, artifact, offline, maxOryArchiveDownloadSize) |
| } |
| |
| func ensureArchiveWithLimit(ctx context.Context, client *http.Client, root string, artifact releaseArtifact, offline bool, maxSize int64) (string, error) { |
| path := archiveCachePath(root, artifact) |
| valid, actual, err := validSHA256(path, artifact.SHA256) |
| if err != nil && !errors.Is(err, os.ErrNotExist) { |
| return "", err |
| } |
| if valid { |
| return path, nil |
| } |
| if offline { |
| return "", fmt.Errorf("offline E2E cache is missing or invalid: %s (expected sha256 %s, got %s); populate it with an online E2E run", path, artifact.SHA256, printableHash(actual)) |
| } |
| if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| return "", err |
| } |
| unlock, err := acquireCacheLock(ctx, path+".lock") |
| if err != nil { |
| return "", fmt.Errorf("lock archive cache %s: %w", path, err) |
| } |
| defer unlock() |
| |
| // Another harness process may have published a valid archive while this |
| // caller waited for the lock. Revalidate before removing the invalid path. |
| valid, actual, err = validSHA256(path, artifact.SHA256) |
| if err != nil && !errors.Is(err, os.ErrNotExist) { |
| return "", err |
| } |
| if valid { |
| return path, nil |
| } |
| if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { |
| return "", fmt.Errorf("remove invalid cache file: %w", err) |
| } |
| |
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, artifact.URL, nil) |
| if err != nil { |
| return "", err |
| } |
| resp, err := client.Do(req) |
| if err != nil { |
| return "", fmt.Errorf("download %s: %w", artifact.Archive, err) |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode != http.StatusOK { |
| _, _ = io.CopyN(io.Discard, resp.Body, 4096) |
| return "", fmt.Errorf("download %s: unexpected HTTP status %s", artifact.Archive, resp.Status) |
| } |
| if resp.ContentLength > maxSize { |
| return "", fmt.Errorf("download %s exceeds maximum archive size %d bytes (content length %d)", artifact.Archive, maxSize, resp.ContentLength) |
| } |
| |
| tmp, err := os.CreateTemp(filepath.Dir(path), "."+artifact.Archive+"-*") |
| if err != nil { |
| return "", err |
| } |
| tmpName := tmp.Name() |
| published := false |
| defer func() { |
| _ = tmp.Close() |
| if !published { |
| _ = os.Remove(tmpName) |
| } |
| }() |
| h := sha256.New() |
| written, err := io.Copy(io.MultiWriter(tmp, h), io.LimitReader(resp.Body, maxSize+1)) |
| if err != nil { |
| return "", fmt.Errorf("download %s body: %w", artifact.Archive, err) |
| } |
| if written > maxSize { |
| return "", fmt.Errorf("download %s exceeds maximum archive size %d bytes", artifact.Archive, maxSize) |
| } |
| actual = hex.EncodeToString(h.Sum(nil)) |
| if !strings.EqualFold(actual, artifact.SHA256) { |
| return "", fmt.Errorf("download %s checksum mismatch: expected %s, got %s", artifact.Archive, artifact.SHA256, actual) |
| } |
| if err := tmp.Sync(); err != nil { |
| return "", err |
| } |
| if err := tmp.Close(); err != nil { |
| return "", err |
| } |
| if err := os.Rename(tmpName, path); err != nil { |
| // A concurrent writer may have published the same valid archive. |
| if valid, _, checkErr := validSHA256(path, artifact.SHA256); checkErr == nil && valid { |
| return path, nil |
| } |
| return "", fmt.Errorf("publish %s: %w", path, err) |
| } |
| published = true |
| if valid, actual, err := validSHA256(path, artifact.SHA256); err != nil || !valid { |
| _ = os.Remove(path) |
| return "", fmt.Errorf("validate published archive %s: expected %s, got %s: %v", path, artifact.SHA256, actual, err) |
| } |
| return path, nil |
| } |
| |
| func validSHA256(path, expected string) (bool, string, error) { |
| f, err := os.Open(path) |
| if err != nil { |
| return false, "", err |
| } |
| defer f.Close() |
| h := sha256.New() |
| if _, err := io.Copy(h, f); err != nil { |
| return false, "", err |
| } |
| actual := hex.EncodeToString(h.Sum(nil)) |
| return strings.EqualFold(actual, expected), actual, nil |
| } |
| |
| func printableHash(hash string) string { |
| if hash == "" { |
| return "missing" |
| } |
| return hash |
| } |
| |
| func downloadClient() *http.Client { return &http.Client{Timeout: 2 * time.Minute} } |