Skip to content
32 changes: 26 additions & 6 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,32 @@ jobs:
path: releases/

- name: Generate sha256 checksum and gpg signatures for release artifacts
uses: livepeer/action-gh-checksum-and-gpg-sign@latest
with:
artifacts-dir: releases
release-name: ${{ (github.ref_type == 'tag' && github.ref_name) || github.event.pull_request.head.sha || github.sha }}
gpg-key: ${{ secrets.CI_GPG_SIGNING_KEY }}
gpg-key-passphrase: ${{ secrets.CI_GPG_SIGNING_PASSPHRASE }}
env:
RELEASE_NAME: ${{ (github.ref_type == 'tag' && github.ref_name) || github.event.pull_request.head.sha || github.sha }}
CI_GPG_SIGNING_KEY: ${{ secrets.CI_GPG_SIGNING_KEY }}
CI_GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.CI_GPG_SIGNING_PASSPHRASE }}
run: |
cd releases
sha256sum -- * > "${RELEASE_NAME}_checksums.txt"

if [[ -z "${CI_GPG_SIGNING_KEY}" ]] || ! command -v gpg >/dev/null; then
exit 0
fi

printf '%s\n' "${CI_GPG_SIGNING_KEY}" | gpg --batch --import
for file in *; do
if [[ "${file}" == *.txt ]]; then
continue
fi
gpg \
--batch \
--no-tty \
--passphrase "${CI_GPG_SIGNING_KEY_PASSPHRASE}" \
--pinentry-mode loopback \
--output "${file}.sig" \
--detach-sign \
"${file}"
done

- name: Generate branch manifest
id: branch-manifest
Expand Down
45 changes: 43 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,37 @@ jobs:
with:
config: config.toml

- name: Set up reviewdog
uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0
with:
reviewdog_version: v0.21.0

- name: Install misspell
env:
MISSPELL_VERSION: 0.8.0
MISSPELL_SHA256: 0a889624797be3eed241eeef4dd2b982adadedd6556723fd00b9c3f721417b51
run: |
archive="${RUNNER_TEMP}/misspell.tar.gz"
curl -fL --retry 3 \
"https://github.com/golangci/misspell/releases/download/v${MISSPELL_VERSION}/misspell_${MISSPELL_VERSION}_linux_amd64.tar.gz" \
-o "${archive}"
echo "${MISSPELL_SHA256} ${archive}" | sha256sum --check
tar -xzf "${archive}" --strip-components=1 -C "${RUNNER_TEMP}" \
"misspell_${MISSPELL_VERSION}_linux_amd64/misspell"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"

- name: misspell
uses: reviewdog/action-misspell@v1
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
find . -type f -print0 \
| xargs -0 misspell \
| reviewdog -efm="%f:%l:%c: %m" \
-filter-mode=added \
-name=misspell \
-reporter=github-pr-check \
-level=error \
-fail-level=none

- name: Install FFMPEG
uses: FedericoCarboni/setup-ffmpeg@v2
Expand All @@ -63,9 +92,21 @@ jobs:
- name: Build Docker Box image
run: make box

- name: Install cloudflared
env:
CLOUDFLARED_VERSION: 2026.7.3
CLOUDFLARED_SHA256: 9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17
run: |
curl -fL --retry 3 \
"https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64" \
-o "${RUNNER_TEMP}/cloudflared"
echo "${CLOUDFLARED_SHA256} ${RUNNER_TEMP}/cloudflared" | sha256sum --check
chmod +x "${RUNNER_TEMP}/cloudflared"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"

- name: Run E2E tests
run:
go test $(go list ./... | grep 'test/e2e') --timeout 15m --image
go test $(go list ./... | grep 'test/e2e') --timeout 30m -v --image
catalyst-e2e-test

- name: Upload coverage reports
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ RUN curl -L -O https://binaries.cockroachdb.com/cockroach-v23.1.5.linux-$TARGETA
&& rm -rf cockroach-v23.1.5.linux-$TARGETARCH.tgz cockroach-v23.1.5.linux-$TARGETARCH \
&& cockroach --version

RUN curl -o /usr/bin/minio https://dl.min.io/server/minio/release/linux-$TARGETARCH/minio \
&& curl -o /usr/bin/mc https://dl.min.io/client/mc/release/linux-$TARGETARCH/mc \
RUN curl -fL -o /usr/bin/minio https://dl.min.io/server/minio/release/linux-$TARGETARCH/minio \
&& curl -fL -o /usr/bin/mc https://dl.min.io/client/mc/release/linux-$TARGETARCH/mc \
&& chmod +x /usr/bin/minio /usr/bin/mc \
&& minio --version \
&& mc --version
Expand Down
4 changes: 2 additions & 2 deletions manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ box:
strategy:
download: bucket
project: catalyst-api
commit: 7c549ec156e81fbad359738e088e1d3fca559b80
commit: 56aff2d85fe064e610dc15d5793c23199267fb5b
release: main
srcFilenames:
darwin-amd64: livepeer-catalyst-api-darwin-amd64.tar.gz
Expand All @@ -49,7 +49,7 @@ box:
strategy:
download: bucket
project: go-livepeer
commit: 48c5d860fec714677f465a38c8a6d52a9f384154
commit: 38eb47d12ab1d2d874fc4c7c061aa1900b7c0bad
binary: livepeer
release: master
archivePath: livepeer
Expand Down
16 changes: 12 additions & 4 deletions scripts/livepeer-nginx
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,28 @@ http {
proxy_pass http://127.0.0.1:3080;
}

location /os-vod/ {
location /os-vod {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host \$http_host;
}

location /os-catalyst-vod/ {
location /os-catalyst-vod {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host \$http_host;
}

location /os-private/ {
location /os-private {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host \$http_host;
}

location /os-recordings/ {
location /os-recordings {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host \$http_host;
}

location /task-runner {
proxy_pass http://127.0.0.1:3060;
}

location / {
Expand Down
166 changes: 154 additions & 12 deletions test/e2e/box_record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@ package e2e
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"sync"
"testing"
"time"

"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"golang.org/x/sync/errgroup"
)

func TestBoxRecording(t *testing.T) {
Expand All @@ -29,22 +35,31 @@ func TestBoxRecording(t *testing.T) {
defer network.Remove(ctx)

boxName := randomString("box-")
publicURL := startQuickTunnel(t, "http://127.0.0.1:8888")

// when
box := startBoxWithEnv(ctx, t, boxName, network.name)
box := startBoxWithEnv(ctx, t, boxName, network.name, publicURL)
defer box.Terminate(ctx)
waitForBoxMinio(t, publicURL)
configureBoxObjectStores(t, publicURL)

eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
return startRecordTester(ctx, false)
})
eg.Go(func() error {
return startRecordTester(ctx, true)
})
require.NoError(t, eg.Wait())
for _, mode := range []struct {
name string
copyOnly bool
}{
{name: "copy-only", copyOnly: true},
{name: "transcoded", copyOnly: false},
} {
t.Run(mode.name, func(t *testing.T) {
if err := startRecordTester(ctx, mode.copyOnly); err != nil {
dumpContainerLogs(ctx, t, box.Container)
require.NoError(t, err)
}
})
}
}

func startBoxWithEnv(ctx context.Context, t *testing.T, hostname, network string) *catalystContainer {
func startBoxWithEnv(ctx context.Context, t *testing.T, hostname, network, publicURL string) *catalystContainer {
req := testcontainers.ContainerRequest{
Image: "livepeer/in-a-box",
Hostname: hostname,
Expand All @@ -54,7 +69,23 @@ func startBoxWithEnv(ctx context.Context, t *testing.T, hostname, network string
ShmSize: 1000000000,
WaitingFor: wait.NewLogStrategy("API server listening").WithStartupTimeout(3 * time.Minute),
Env: map[string]string{
"LP_API_FRONTEND": "false",
"LP_API_FRONTEND": "false",
"E2E_PUBLIC_URL": publicURL,
"E2E_ORCHESTRATOR_URL": fmt.Sprintf("https://%s:8936", hostname),
},
Cmd: []string{
"bash",
"-ceu",
`sed -i \
-e "s|\"api-server\": \"http://127.0.0.1:3004\"|\"api-server\": \"${E2E_PUBLIC_URL}\"|" \
-e "s|\"own-base-url\": \"http://127.0.0.1:3060/task-runner\"|\"own-base-url\": \"${E2E_PUBLIC_URL}/task-runner\"|" \
-e "s|\"orchAddr\": \"127.0.0.1:8936\"|\"orchAddr\": \"${E2E_ORCHESTRATOR_URL}\"|g" \
-e "s|\"serviceAddr\": \"127.0.0.1:8936\"|\"httpAddr\": \"https://0.0.0.0:8936\", \"serviceAddr\": \"${E2E_ORCHESTRATOR_URL}\"|" \
/etc/livepeer/full-stack.json
grep -Fq "\"api-server\": \"${E2E_PUBLIC_URL}\"" /etc/livepeer/full-stack.json
grep -Fq "\"own-base-url\": \"${E2E_PUBLIC_URL}/task-runner\"" /etc/livepeer/full-stack.json
grep -Fq "\"serviceAddr\": \"${E2E_ORCHESTRATOR_URL}\"" /etc/livepeer/full-stack.json
exec /usr/local/bin/catalyst -- /usr/local/bin/MistController -c /etc/livepeer/full-stack.json`,
},
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
Expand Down Expand Up @@ -97,6 +128,117 @@ func startBoxWithEnv(ctx context.Context, t *testing.T, hostname, network string
return catalyst
}

const boxAPIToken = "f61b3cdb-d173-4a7a-a0d3-547b871a56f9"

var boxObjectStores = map[string]string{
"917a2f18-f7a8-4ae3-a849-6efd4aac8e59": "os-vod",
"517873a4-487c-40ad-872f-027f4bc6bd98": "os-catalyst-vod",
"cab9266f-5583-4532-9630-7be10d92affe": "os-private",
"0926e4ba-b726-4386-92ee-5c4583f62f0a": "os-recordings",
}

func waitForBoxMinio(t *testing.T, publicURL string) {
t.Helper()

u, err := url.Parse(publicURL)
require.NoError(t, err)
cli, err := minio.New(u.Host, &minio.Options{
Creds: credentials.NewStaticV4("admin", "password", ""),
Secure: true,
Region: region,
BucketLookup: minio.BucketLookupPath,
})
require.NoError(t, err)

deadline := time.Now().Add(time.Minute)
for {
requestCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
exists, err := cli.BucketExists(requestCtx, "os-recordings")
cancel()
if err == nil && exists {
return
}
if err == nil {
err = fmt.Errorf("recordings bucket does not exist")
}
if time.Now().After(deadline) {
t.Fatalf("in-a-box MinIO tunnel did not become ready: %v", err)
}
time.Sleep(time.Second)
}
}

func configureBoxObjectStores(t *testing.T, publicURL string) {
t.Helper()

client := &http.Client{Timeout: 10 * time.Second}
for id, bucket := range boxObjectStores {
deadline := time.Now().Add(time.Minute)
for {
err := patchBoxObjectStore(client, publicURL, id, bucket)
if err == nil {
break
}
if time.Now().After(deadline) {
t.Fatalf("could not configure object store %s: %v", bucket, err)
}
time.Sleep(time.Second)
}
}
}

func patchBoxObjectStore(client *http.Client, publicURL, id, bucket string) error {
storeURL, err := boxObjectStoreURL(publicURL, bucket)
if err != nil {
return err
}
payload, err := json.Marshal(map[string]string{
"url": storeURL,
"publicUrl": strings.TrimRight(publicURL, "/") + "/" + bucket,
})
if err != nil {
return err
}

req, err := http.NewRequest(http.MethodPatch, strings.TrimRight(publicURL, "/")+"/api/object-store/"+id, bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+boxAPIToken)
req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("unexpected status %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return nil
}

func boxObjectStoreURL(publicURL, bucket string) (string, error) {
u, err := url.Parse(publicURL)
if err != nil {
return "", err
}
if u.Scheme != "https" || u.Hostname() == "" {
return "", fmt.Errorf("invalid tunnel URL %q", publicURL)
}
u.Scheme = "s3+https"
u.User = url.UserPassword("admin", "password")
u.Path = "/" + bucket
return u.String(), nil
}

func TestBoxObjectStoreURL(t *testing.T) {
got, err := boxObjectStoreURL("https://example.trycloudflare.com", "os-recordings")
require.NoError(t, err)
require.Equal(t, "s3+https://admin:password@example.trycloudflare.com/os-recordings", got)
}

func startRecordTester(ctx context.Context, recordingCopyOnly bool) error {
startTime := time.Now()
fmt.Printf("starting record tester copyOnly=%v\n", recordingCopyOnly)
Expand Down
Loading
Loading