Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions images/compatibility/gitea/gitea/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM gitea/gitea:1.26.2
1 change: 1 addition & 0 deletions images/compatibility/gitea/postgres/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM postgres:16.4
1 change: 1 addition & 0 deletions images/compatibility/nextcloud/mariadb/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM mariadb:12.3.2
1 change: 1 addition & 0 deletions images/compatibility/nextcloud/nextcloud/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM nextcloud:34.0.0
1 change: 1 addition & 0 deletions images/compatibility/wordpress/mysql/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM mysql:9.7.1
1 change: 1 addition & 0 deletions images/compatibility/wordpress/wordpress/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM wordpress:7.0.0-php8.4-apache
41 changes: 41 additions & 0 deletions test/compatibility/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
load("//tools:defs.bzl", "go_library")
load(":defs.bzl", "compatibility_test")

package(
default_applicable_licenses = ["//:license"],
default_visibility = ["//:sandbox"],
licenses = ["notice"],
)

go_library(
name = "compatibility",
testonly = True,
srcs = ["compatibility.go"],
)

compatibility_test(
name = "gitea",
srcs = ["gitea/gitea_test.go"],
deps = [
"//pkg/test/dockerutil",
"//test/compatibility",
],
)

compatibility_test(
name = "nextcloud",
srcs = ["nextcloud/nextcloud_test.go"],
deps = [
"//pkg/test/dockerutil",
"//test/compatibility",
],
)

compatibility_test(
name = "wordpress",
srcs = ["wordpress/wordpress_test.go"],
deps = [
"//pkg/test/dockerutil",
"//test/compatibility",
],
)
33 changes: 33 additions & 0 deletions test/compatibility/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Application Compatibility Tests

This directory contains compatibility tests used to automate the process of
determining third-party application compatibility with gVisor over time.

To reduce iteration time, these tests are *not* a part of our CI/CD pipeline.

## Application versions

The images used for each stack are in `images/compatibility/<application>`.
The application version under test is determined by the Dockerfile present in
these directories.

## Running

First load the required images (built from `images/compatibility/...`):

```
make load-compatibility_gitea
# or load every image at once. this can take a while:
make load-compatibility
```

Then run the native baseline and the gVisor target. The Docker daemon must be
running locally, and gVisor must be present as the `runsc` runtime:

```
make test TARGETS="//test/compatibility:gitea_native //test/compatibility:gitea_runsc"
```

## Website

Soon the results from these tests will be listed on [gvisor.dev](https://gvisor.dev).
156 changes: 156 additions & 0 deletions test/compatibility/compatibility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package compatibility provides shared helpers for gVisor application
// compatibility tests.
package compatibility

import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
)

// WriteConfigFile writes content to a file named name in a fresh temporary
// directory and returns the file's absolute path.
func WriteConfigFile(t *testing.T, name, content string) string {
t.Helper()
dir, err := os.MkdirTemp("", "compat-config")
if err != nil {
t.Fatalf("failed to create temp config dir: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
p := filepath.Join(dir, name)
if err := os.WriteFile(p, []byte(content), 0644); err != nil {
t.Fatalf("failed to write config file %s: %v", name, err)
}
return p
}

var httpClient = &http.Client{Timeout: 30 * time.Second}

// Poll repeatedly calls cond until it returns nil, failing the test (via
// t.Fatalf) if it has not succeeded within timeout. It waits interval between
// attempts.
//
// This is the preferred way to wait for a stack to become ready: poll for a
// readiness signal (a health endpoint, an accepted connection, etc).
//
// desc is a short human-readable description of what is being waited for, used in
// the timeout message (e.g. "gitea API to be ready").
func Poll(ctx context.Context, t *testing.T, desc string, timeout, interval time.Duration, cond func() error) {
t.Helper()
deadline := time.Now().Add(timeout)
var lastErr error
for {
if lastErr = cond(); lastErr == nil {
return
}
if time.Now().After(deadline) {
t.Fatalf("timed out after %v waiting for %s: %v", timeout, desc, lastErr)
}
select {
case <-ctx.Done():
t.Fatalf("context cancelled while waiting for %s: %v (last error: %v)", desc, ctx.Err(), lastErr)
case <-time.After(interval):
}
}
}

// Request describes an HTTP request issued by a compatibility test.
type Request struct {
Method string // defaults to GET.
URL string
Body string
ContentType string
Host string // overrides the Host header if non-empty.
Username string // enables HTTP basic auth if non-empty.
Password string
Headers map[string]string
// Timeout overrides the default client timeout for this request.
// Zero uses the default from httpClient.
Timeout time.Duration
}

func (r Request) method() string {
if r.Method == "" {
return http.MethodGet
}
return r.Method
}

// Do issues the request and returns the response status code and body. A
// transport-level failure is returned as an error; an HTTP error status is
// not an error.
func (r Request) Do() (int, string, error) {
var body io.Reader
if r.Body != "" {
body = strings.NewReader(r.Body)
}
req, err := http.NewRequest(r.method(), r.URL, body)
if err != nil {
return 0, "", err
}
if r.ContentType != "" {
req.Header.Set("Content-Type", r.ContentType)
}
for k, v := range r.Headers {
req.Header.Set(k, v)
}
if r.Host != "" {
req.Host = r.Host
}
if r.Username != "" {
req.SetBasicAuth(r.Username, r.Password)
}
client := httpClient
if r.Timeout > 0 {
client = &http.Client{Timeout: r.Timeout}
}
resp, err := client.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, "", fmt.Errorf("reading body of %s %s: %w", r.method(), r.URL, err)
}
return resp.StatusCode, string(respBody), nil
}

// DoOrFatal issues the request, failing the test on a transport error or if the
// status code is not wantStatus, and returns the response body.
func (r Request) DoOrFatal(t *testing.T, wantStatus int) string {
t.Helper()
status, body, err := r.Do()
if err != nil {
t.Fatalf("%s %s failed: %v", r.method(), r.URL, err)
}
if status != wantStatus {
t.Fatalf("%s %s: got status %d, want %d; body: %s", r.method(), r.URL, status, wantStatus, body)
}
return body
}

// Get is a convenience wrapper for a plain GET.
func Get(url string) (int, string, error) {
return Request{URL: url}.Do()
}
46 changes: 46 additions & 0 deletions test/compatibility/defs.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Defines the compatibility_test macro for application compatibility tests."""

load("//tools:defs.bzl", "go_test")

# Runtimes under which compatibility tests are run, as (target suffix, runtime)
# pairs. "native" is the unsandboxed baseline (runc), matching the naming used by
# the syscall tests; "runsc" is gVisor.
_RUNTIMES = [
("native", "runc"),
("runsc", "runsc"),
]

def compatibility_test(name, srcs, deps = [], data = [], tags = [], size = "large", **kwargs):
"""compatibility_test generates one go_test target per runtime.

For <name> it generates:
<name>_native: runs the test under runc (as a baseline).
<name>_runsc: runs the test under runsc.

Both targets share the same sources and differ only in the "--runtime"
argument passed to the test binary (which dockerutil.MakeContainer reads).

Args:
name: base name; per-runtime targets append "_native"/"_runsc".
srcs: test sources.
deps: test dependencies.
data: runtime data dependencies.
tags: extra tags (in addition to "local" and "manual").
size: test size (default "large").
**kwargs: forwarded to go_test (e.g. visibility).
"""
for suffix, runtime in _RUNTIMES:
target_data = list(data)
if runtime != "runc":
# runsc is needed to invalidate the bazel cache on any code change.
target_data = target_data + ["//runsc"]
go_test(
name = name + "_" + suffix,
srcs = srcs,
size = size,
args = ["--runtime=" + runtime],
data = target_data,
tags = tags + ["local", "manual"],
deps = deps,
**kwargs
)
Loading
Loading