Skip to content
Open
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
17 changes: 17 additions & 0 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Once you have configured your local client, you can run the tests by running:
```sh
$ go test --tags=e2e .
```

### HA E2E Tests

The E2E test suite verifies the three correctness guarantees using a real Kubernetes cluster with the HA configuration deployed.
Expand Down Expand Up @@ -144,3 +145,19 @@ API pod tekton-results-api-def456: 12 requests
API pod tekton-results-api-ghi789: 14 requests
```

### Postgres DB layer tests

The `db/` sub-package contains end-to-end tests that exercise
Postgres-specific behavior (error code mapping, schema validation, lister
filter/sort/pagination, jsonb). These run against the deployed API server and
its live Postgres instance — **not** an in-process server.

See [`db/README.md`](db/README.md) for details on environment variables,
standalone runs, and how to add tests for future stories.

```sh
$ go test -v -count=1 -tags=e2e ./test/e2e/db/...
```

The `e2e.sh` script runs these automatically after the main e2e suite; it
handles port-forwarding and credential wiring via `DB_URL`.
108 changes: 108 additions & 0 deletions test/e2e/client/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package client

import (
"fmt"
"os"
"path"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"k8s.io/client-go/transport"
)

// Default e2e environment values. These match the paths used by
// test/e2e/01-install.sh and the kind cluster setup scripts.
const (
DefaultServerName = "tekton-results-api-service.tekton-pipelines.svc.cluster.local"
DefaultServerAddress = "https://localhost:8080"
DefaultCertFileName = "tekton-results-cert.pem"
DefaultCertPath = "/tmp/tekton-results/ssl"
DefaultTokenPath = "/tmp/tekton-results/tokens" //nolint:gosec // Not a credential; directory path for SA token files.

AdminTokenFile = "all-namespaces-admin-access"
ReadTokenFile = "all-namespaces-read-access"
)

// EnvConfig holds the resolved paths and addresses for the e2e test environment.
type EnvConfig struct {
CertFile string
TokenPath string
ServerName string
ServerAddress string
}

// NewEnvConfig reads the standard e2e environment variables and falls back to
// defaults. Both the main e2e suite and the db sub-suite share this to avoid
// duplicating the env-var-to-default resolution logic.
func NewEnvConfig() EnvConfig {
certPath := EnvOrDefault("SSL_CERT_PATH", DefaultCertPath)
certFileName := EnvOrDefault("CERT_FILE_NAME", DefaultCertFileName)
return EnvConfig{
CertFile: path.Join(certPath, certFileName),
TokenPath: EnvOrDefault("SA_TOKEN_PATH", DefaultTokenPath),
ServerName: EnvOrDefault("API_SERVER_NAME", DefaultServerName),
ServerAddress: EnvOrDefault("API_SERVER_ADDR", DefaultServerAddress),
}
}

// TokenFile returns the full path for a given token file name under the
// resolved token directory.
func (c EnvConfig) TokenFile(name string) string {
return path.Join(c.TokenPath, name)
}

// NewGRPCClientFromConfig creates a GRPCClient using the shared EnvConfig and
// the specified token file name.
func NewGRPCClientFromConfig(cfg EnvConfig, tokenFileName string, impersonationConfig *transport.ImpersonationConfig) (GRPCClient, error) {
if impersonationConfig == nil {
impersonationConfig = &transport.ImpersonationConfig{}
}

transportCreds, err := credentials.NewClientTLSFromFile(cfg.CertFile, cfg.ServerName)
if err != nil {
return nil, fmt.Errorf("failed to load TLS credentials from %s: %w", cfg.CertFile, err)
}

opts := []grpc.DialOption{
grpc.WithBlock(), //nolint:staticcheck
grpc.WithTransportCredentials(transportCreds),
grpc.WithDefaultCallOptions(grpc.PerRPCCredentials(&CustomCredentials{
TokenSource: transport.NewCachedFileTokenSource(cfg.TokenFile(tokenFileName)),
ImpersonationConfig: impersonationConfig,
})),
}

return NewGRPCClient(cfg.ServerAddress, opts...)
}

// NewRESTClientFromConfig creates a RESTClient using the shared EnvConfig and
// the specified token file name.
func NewRESTClientFromConfig(cfg EnvConfig, tokenFileName string, impersonationConfig *transport.ImpersonationConfig) (RESTClient, error) {
if impersonationConfig == nil {
impersonationConfig = &transport.ImpersonationConfig{}
}

if _, err := credentials.NewClientTLSFromFile(cfg.CertFile, cfg.ServerName); err != nil {
return nil, fmt.Errorf("failed to verify TLS cert from %s: %w", cfg.CertFile, err)
}

restConfig := &transport.Config{
TLS: transport.TLSConfig{
CAFile: cfg.CertFile,
ServerName: cfg.ServerName,
},
BearerTokenFile: cfg.TokenFile(tokenFileName),
Impersonate: *impersonationConfig,
}

return NewRESTClient(cfg.ServerAddress, WithConfig(restConfig))
}

// EnvOrDefault returns the value of the environment variable named by key,
// or fallback if the variable is empty or unset.
func EnvOrDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
81 changes: 81 additions & 0 deletions test/e2e/db/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Postgres DB Layer E2E Tests

End-to-end tests that validate the Postgres-specific database layer of Tekton
Results. These tests run against the **deployed** API server in the kind e2e
cluster and its **live** Postgres instance—not an in-process server or a
throwaway database.

## What these tests cover

| Area | File | Status |
|---|---|---|
| Error mapping (pgconn.PgError → gRPC codes) | `error_mapping_test.go` | Active |
| Schema validation (column types, PKs, indexes, FKs, jsonb) | `schema_test.go` | Active |
| Lister behavior (filter, sort, pagination via Postgres) | `lister_test.go` | Active |
| Labels (normalized table, selector operators) | `labels_test.go` | Placeholder (Stories 11-13) |
| Metadata columns (text[], GIN indexes) | `migrations_test.go` | Placeholder (Stories 08-10) |
| golang-migrate migrations | `migrations_test.go` | Placeholder (Story 07) |
| Relationships & retention | `relationships_test.go`, `retention_test.go` | Placeholder (Stories 17, 20, 21) |

## Prerequisites

- A running kind cluster with Tekton Results installed (`./test/e2e/00-setup.sh`
and `./test/e2e/01-install.sh`).
- The API server must be accessible at `localhost:8080` (the kind nodePort).
- Postgres must be reachable via `kubectl port-forward` (the `e2e.sh` script
handles this automatically).

## Running

### Via the e2e script (CI path)

```bash
./test/e2e/e2e.sh
```

The script runs the standard e2e tests, then the DB layer tests, then the GCS
logging tests.

### Standalone (after cluster is up)

```bash
# Start port-forward to Postgres
kubectl port-forward svc/tekton-results-postgres-service 15432:5432 -n tekton-pipelines &

# Read the password
PGPASS=$(kubectl get secret tekton-results-postgres -n tekton-pipelines \
-o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)

# Run (single DB_URL)
DB_URL="host=localhost port=15432 user=postgres password=${PGPASS} dbname=tekton-results sslmode=disable" \
go test -v -count=1 -tags=e2e ./test/e2e/db/...
```

## Environment variables

| Variable | Default | Description |
|---|---|---|
| `DB_URL` | (none) | Full Postgres DSN. When set, takes precedence over individual `POSTGRES_*` vars. The `e2e.sh` script constructs this automatically. |
| `POSTGRES_HOST` | `localhost` | Fallback: Postgres host (via port-forward) |
| `POSTGRES_PORT` | `15432` | Fallback: local forwarded port |
| `POSTGRES_USER` | `postgres` | Fallback: DB user |
| `POSTGRES_PASSWORD` | (none) | Fallback: DB password (from cluster secret) |
| `POSTGRES_DB` | `tekton-results` | Fallback: database name (the live API database) |
| `POSTGRES_SSLMODE` | `disable` | Fallback: SSL mode |
| `SSL_CERT_PATH` | `/tmp/tekton-results/ssl` | Path to TLS cert for API |
| `SA_TOKEN_PATH` | `/tmp/tekton-results/tokens` | Path to SA token files |
| `API_SERVER_ADDR` | `https://localhost:8080` | API server address |
| `API_SERVER_NAME` | `tekton-results-api-service.tekton-pipelines.svc.cluster.local` | TLS server name |

## Adding tests for future stories

When your story lands a Postgres-specific feature:

1. Find the matching placeholder file (e.g., `labels_test.go`).
2. Replace the `t.Skip(...)` body with real test logic.
3. If no placeholder exists, create a new `*_test.go` file with the
`//go:build e2e` tag.
4. Use `adminClient` for mutations, `readClient` for queries, and `rawDB`
for direct schema introspection.
5. Clean up test data via the API in `t.Cleanup`—never drop or truncate
tables (it is the live database).
129 changes: 129 additions & 0 deletions test/e2e/db/db_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//go:build e2e
// +build e2e

// Copyright 2026 The Tekton 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 db_test provides end-to-end tests for the Postgres database layer.
//
// Tests exercise the deployed API server (gRPC) in the kind cluster against
// its live Postgres instance, verifying behavior that the SQLite-backed unit
// tests cannot reach:
// - Postgres-specific error code mapping (pgconn.PgError → gRPC status)
// - Lister filter/sort/pagination with real Postgres query execution
// - Schema correctness (column types, indexes, foreign keys, jsonb)
//
// A raw database connection is used only for schema introspection and to
// trigger FK violations that the server's pre-checks would otherwise mask.
//
// Future stories (labels, metadata columns, golang-migrate migrations) add
// their Postgres-specific tests to this package. See the stub files for
// placeholders.
//
// Run:
//
// go test -v -count=1 -tags=e2e ./test/e2e/db/...
package db_test

import (
"context"
"fmt"
"os"
"testing"

"github.com/tektoncd/results/test/e2e/client"
"gorm.io/driver/postgres"
"gorm.io/gorm"

// Register the Postgres error space so pgconn.PgError → gRPC code
// translation is active in the dberrors.Wrap calls used by the FK
// violation test.
_ "github.com/tektoncd/results/pkg/api/server/db/errors/postgres"
)

var (
// adminClient talks to the deployed API server with full CRUD
// permissions. Used for creating/updating/deleting test data.
adminClient client.GRPCClient

// readClient talks to the deployed API server with read-only
// permissions. Used for list/get assertions where we want to confirm
// the read path independently.
readClient client.GRPCClient

// rawDB is a direct Postgres connection to the live tekton-results
// database. Used only for schema introspection (information_schema /
// pg_catalog) and for the FK violation test that must bypass the
// server's pre-check logic.
rawDB *gorm.DB
)

func TestMain(m *testing.M) {
cfg := client.NewEnvConfig()
var err error

adminClient, err = client.NewGRPCClientFromConfig(cfg, client.AdminTokenFile, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create admin gRPC client: %v\n", err)
os.Exit(1)
}

readClient, err = client.NewGRPCClientFromConfig(cfg, client.ReadTokenFile, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create read gRPC client: %v\n", err)
os.Exit(1)
}

dsn := buildDSN()
rawDB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Fprintf(os.Stderr, "failed to connect to postgres: %v\n", err)
os.Exit(1)
}
sqlDB, err := rawDB.DB()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to get underlying sql.DB: %v\n", err)
os.Exit(1)
}
if err := sqlDB.Ping(); err != nil {
fmt.Fprintf(os.Stderr, "failed to ping postgres: %v\n", err)
os.Exit(1)
}

code := m.Run()

sqlDB.Close()
os.Exit(code)
}

func buildDSN() string {
if url := os.Getenv("DB_URL"); url != "" {
return url
}
host := client.EnvOrDefault("POSTGRES_HOST", "localhost")
port := client.EnvOrDefault("POSTGRES_PORT", "15432")
user := client.EnvOrDefault("POSTGRES_USER", "postgres")
pass := client.EnvOrDefault("POSTGRES_PASSWORD", "")
dbname := client.EnvOrDefault("POSTGRES_DB", "tekton-results")
sslmode := client.EnvOrDefault("POSTGRES_SSLMODE", "disable")

return fmt.Sprintf(
"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
host, port, user, pass, dbname, sslmode,
)
}

func defaultCtx() context.Context {
return context.Background()
}
Loading
Loading