Skip to content

Commit 6f68717

Browse files
s04claudemdelapenya
authored
feat: add Forgejo module (#3556)
* feat: add Forgejo module Add a new testcontainers module for Forgejo, a self-hosted Git forge. The module provides: - Container startup with SQLite and INSTALL_LOCK pre-configured - Automatic admin user creation via PostReadies lifecycle hook - ConnectionString() and SSHConnectionString() helper methods - WithAdminCredentials() option for custom admin setup - WithConfig() option for arbitrary Forgejo configuration via FORGEJO__section__key environment variables - Health check via /api/healthz endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback on Forgejo module - Extract extractAdminCredentials helper to deduplicate env parsing - Use closure variables in PostReadies hook to avoid redundant Inspect call - Add nil-map guards in WithAdminCredentials and WithConfig - Use http.NewRequestWithContext instead of http.Get in tests - Assert SSH connection string format in TestForgejoSSHEndpoint - Add language specifier to markdown code block (MD040) * fix: add input validation to WithAdminCredentials and WithConfig - Validate that username, password, and email are non-empty in WithAdminCredentials to fail fast before container startup - Validate that section and key don't contain "__" in WithConfig to prevent ambiguous env var names that break Forgejo's config parsing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make Forgejo container credential fields private Make AdminUsername and AdminPassword private with getter methods, matching the convention used by other modules (postgres, mysql, mssql). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: validate non-empty section and key in WithConfig Reject empty section or key before constructing the env var name, preventing malformed keys like FORGEJO____key. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use defaultHTTPPort constant in wait strategy Replace hardcoded "3000" with defaultHTTPPort to stay consistent with the exposed ports declaration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump Go version * fix: lint --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: mdelapenya <mdelapenya@gmail.com>
1 parent c4e345e commit 6f68717

10 files changed

Lines changed: 620 additions & 0 deletions

File tree

.github/dependabot.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ updates:
3535
- /modules/dynamodb
3636
- /modules/elasticsearch
3737
- /modules/etcd
38+
- /modules/forgejo
3839
- /modules/gcloud
3940
- /modules/grafana-lgtm
4041
- /modules/inbucket

.vscode/.testcontainers-go.code-workspace

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@
8989
"name": "module / etcd",
9090
"path": "../modules/etcd"
9191
},
92+
{
93+
"name": "module / forgejo",
94+
"path": "../modules/forgejo"
95+
},
9296
{
9397
"name": "module / gcloud",
9498
"path": "../modules/gcloud"

docs/modules/forgejo.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Forgejo
2+
3+
Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
4+
5+
## Introduction
6+
7+
The Testcontainers module for [Forgejo](https://forgejo.org/), a self-hosted Git forge. Forgejo is a community-driven fork of Gitea, providing a lightweight code hosting solution.
8+
9+
## Adding this module to your project dependencies
10+
11+
Please run the following command to add the Forgejo module to your Go dependencies:
12+
13+
```sh
14+
go get github.com/testcontainers/testcontainers-go/modules/forgejo
15+
```
16+
17+
## Usage example
18+
19+
<!--codeinclude-->
20+
[Creating a Forgejo container](../../modules/forgejo/examples_test.go) inside_block:runForgejoContainer
21+
<!--/codeinclude-->
22+
23+
## Module Reference
24+
25+
### Run function
26+
27+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
28+
29+
The Forgejo module exposes one entrypoint function to create the Forgejo container, and this function receives three parameters:
30+
31+
```golang
32+
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error)
33+
```
34+
35+
- `context.Context`, the Go context.
36+
- `string`, the Docker image to use.
37+
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.
38+
39+
#### Image
40+
41+
Use the second argument in the `Run` function to set a valid Docker image.
42+
In example: `Run(context.Background(), "codeberg.org/forgejo/forgejo:11")`.
43+
44+
### Container Options
45+
46+
When starting the Forgejo container, you can pass options in a variadic way to configure it.
47+
48+
#### Admin Credentials
49+
50+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
51+
52+
Use `WithAdminCredentials(username, password, email)` to set the admin user credentials. An admin user is automatically created when the container starts. Default credentials are `forgejo-admin` / `forgejo-admin`.
53+
54+
#### Configuration via Environment
55+
56+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
57+
58+
Use `WithConfig(section, key, value)` to set Forgejo configuration values using the `FORGEJO__section__key` environment variable format. See the [Forgejo Configuration Cheat Sheet](https://forgejo.org/docs/latest/admin/config-cheat-sheet/) for available options.
59+
60+
{% include "../features/common_functional_options_list.md" %}
61+
62+
### Container Methods
63+
64+
The Forgejo container exposes the following methods:
65+
66+
#### ConnectionString
67+
68+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
69+
70+
The `ConnectionString` method returns the HTTP URL for the Forgejo instance (e.g. `http://localhost:12345`).
71+
72+
#### SSHConnectionString
73+
74+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
75+
76+
The `SSHConnectionString` method returns the SSH endpoint for Git operations.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ nav:
9292
- modules/dynamodb.md
9393
- modules/elasticsearch.md
9494
- modules/etcd.md
95+
- modules/forgejo.md
9596
- modules/gcloud.md
9697
- modules/grafana-lgtm.md
9798
- modules/inbucket.md

modules/forgejo/Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include ../../commons-test.mk
2+
3+
.PHONY: test
4+
test:
5+
$(MAKE) test-forgejo

modules/forgejo/examples_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package forgejo_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
8+
"github.com/testcontainers/testcontainers-go"
9+
"github.com/testcontainers/testcontainers-go/modules/forgejo"
10+
)
11+
12+
func ExampleRun() {
13+
// runForgejoContainer {
14+
ctx := context.Background()
15+
16+
forgejoContainer, err := forgejo.Run(ctx, "codeberg.org/forgejo/forgejo:11")
17+
defer func() {
18+
if err := testcontainers.TerminateContainer(forgejoContainer); err != nil {
19+
log.Printf("failed to terminate container: %s", err)
20+
}
21+
}()
22+
if err != nil {
23+
log.Printf("failed to start container: %s", err)
24+
return
25+
}
26+
// }
27+
28+
state, err := forgejoContainer.State(ctx)
29+
if err != nil {
30+
log.Printf("failed to get container state: %s", err)
31+
return
32+
}
33+
34+
fmt.Println(state.Running)
35+
36+
// Output:
37+
// true
38+
}

modules/forgejo/forgejo.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package forgejo
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
"github.com/testcontainers/testcontainers-go"
11+
"github.com/testcontainers/testcontainers-go/exec"
12+
"github.com/testcontainers/testcontainers-go/wait"
13+
)
14+
15+
const (
16+
defaultHTTPPort = "3000/tcp"
17+
defaultSSHPort = "22/tcp"
18+
defaultUser = "forgejo-admin"
19+
defaultPassword = "forgejo-admin"
20+
defaultEmail = "admin@forgejo.local"
21+
)
22+
23+
// Container represents the Forgejo container type used in the module
24+
type Container struct {
25+
testcontainers.Container
26+
adminUsername string
27+
adminPassword string
28+
}
29+
30+
// AdminUsername returns the admin username for the Forgejo instance.
31+
func (c *Container) AdminUsername() string {
32+
return c.adminUsername
33+
}
34+
35+
// AdminPassword returns the admin password for the Forgejo instance.
36+
func (c *Container) AdminPassword() string {
37+
return c.adminPassword
38+
}
39+
40+
// extractAdminCredentials parses FORGEJO_ADMIN_* env vars from the container
41+
// environment, falling back to the default values for any that are not set.
42+
func extractAdminCredentials(env []string) (username, password, email string) {
43+
username, password, email = defaultUser, defaultPassword, defaultEmail
44+
for _, e := range env {
45+
if v, ok := strings.CutPrefix(e, "FORGEJO_ADMIN_USERNAME="); ok {
46+
username = v
47+
}
48+
if v, ok := strings.CutPrefix(e, "FORGEJO_ADMIN_PASSWORD="); ok {
49+
password = v
50+
}
51+
if v, ok := strings.CutPrefix(e, "FORGEJO_ADMIN_EMAIL="); ok {
52+
email = v
53+
}
54+
}
55+
return
56+
}
57+
58+
// Run creates an instance of the Forgejo container type
59+
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
60+
// Closure variables populated by the PostReadies hook so we can avoid
61+
// a second container.Inspect call after Run returns.
62+
var adminUser, adminPass string
63+
64+
moduleOpts := make([]testcontainers.ContainerCustomizer, 0, 4+len(opts))
65+
moduleOpts = append(moduleOpts,
66+
testcontainers.WithExposedPorts(defaultHTTPPort, defaultSSHPort),
67+
testcontainers.WithWaitStrategy(
68+
wait.ForHTTP("/api/healthz").WithPort(defaultHTTPPort),
69+
),
70+
// Use SQLite for simplicity in tests (no external DB needed).
71+
// INSTALL_LOCK skips the install wizard so the instance is ready to use.
72+
testcontainers.WithEnv(map[string]string{
73+
"FORGEJO__database__DB_TYPE": "sqlite3",
74+
"FORGEJO__security__INSTALL_LOCK": "true",
75+
"FORGEJO_ADMIN_USERNAME": defaultUser,
76+
"FORGEJO_ADMIN_PASSWORD": defaultPassword,
77+
"FORGEJO_ADMIN_EMAIL": defaultEmail,
78+
}),
79+
)
80+
81+
moduleOpts = append(moduleOpts, opts...)
82+
83+
// Add lifecycle hook to create admin user after container is ready.
84+
// The hook reads credentials from container env vars so that user-provided
85+
// options (which override the defaults above) are respected.
86+
// The command runs as the "git" user because Forgejo refuses to run CLI
87+
// commands as root.
88+
adminHook := testcontainers.ContainerLifecycleHooks{
89+
PostReadies: []testcontainers.ContainerHook{
90+
func(ctx context.Context, container testcontainers.Container) error {
91+
inspect, err := container.Inspect(ctx)
92+
if err != nil {
93+
return fmt.Errorf("inspect forgejo: %w", err)
94+
}
95+
96+
username, password, email := extractAdminCredentials(inspect.Config.Env)
97+
98+
// Store credentials in closure for Run to use later.
99+
adminUser = username
100+
adminPass = password
101+
102+
code, output, err := container.Exec(ctx, []string{
103+
"forgejo", "admin", "user", "create",
104+
"--username", username,
105+
"--password", password,
106+
"--email", email,
107+
"--admin",
108+
"--must-change-password=false",
109+
}, exec.WithUser("git"))
110+
if err != nil {
111+
return fmt.Errorf("create admin user: %w", err)
112+
}
113+
if code != 0 {
114+
data, _ := io.ReadAll(output)
115+
return fmt.Errorf("create admin user: exit code %d: %s", code, string(data))
116+
}
117+
return nil
118+
},
119+
},
120+
}
121+
122+
moduleOpts = append(moduleOpts, testcontainers.WithAdditionalLifecycleHooks(adminHook))
123+
124+
ctr, err := testcontainers.Run(ctx, img, moduleOpts...)
125+
var c *Container
126+
if ctr != nil {
127+
c = &Container{Container: ctr}
128+
}
129+
130+
if err != nil {
131+
return c, fmt.Errorf("run forgejo: %w", err)
132+
}
133+
134+
// Credentials were populated by the PostReadies hook above.
135+
c.adminUsername = adminUser
136+
c.adminPassword = adminPass
137+
138+
return c, nil
139+
}
140+
141+
// ConnectionString returns the HTTP URL for the Forgejo instance
142+
func (c *Container) ConnectionString(ctx context.Context) (string, error) {
143+
return c.PortEndpoint(ctx, defaultHTTPPort, "http")
144+
}
145+
146+
// SSHConnectionString returns the SSH endpoint for Git operations
147+
func (c *Container) SSHConnectionString(ctx context.Context) (string, error) {
148+
return c.PortEndpoint(ctx, defaultSSHPort, "")
149+
}
150+
151+
// WithAdminCredentials sets the admin username, password, and email for the Forgejo instance.
152+
// These credentials are used to create an admin user after the container is ready.
153+
func WithAdminCredentials(username, password, email string) testcontainers.CustomizeRequestOption {
154+
return func(req *testcontainers.GenericContainerRequest) error {
155+
if username == "" || password == "" || email == "" {
156+
return errors.New("WithAdminCredentials: username, password, and email must not be empty")
157+
}
158+
if req.Env == nil {
159+
req.Env = make(map[string]string)
160+
}
161+
req.Env["FORGEJO_ADMIN_USERNAME"] = username
162+
req.Env["FORGEJO_ADMIN_PASSWORD"] = password
163+
req.Env["FORGEJO_ADMIN_EMAIL"] = email
164+
return nil
165+
}
166+
}
167+
168+
// WithConfig sets a Forgejo configuration value using the FORGEJO__section__key
169+
// environment variable format.
170+
// See https://forgejo.org/docs/latest/admin/config-cheat-sheet/ for available options.
171+
func WithConfig(section, key, value string) testcontainers.CustomizeRequestOption {
172+
return func(req *testcontainers.GenericContainerRequest) error {
173+
if section == "" || key == "" {
174+
return fmt.Errorf("WithConfig: section and key must not be empty (got section=%q, key=%q)", section, key)
175+
}
176+
if strings.Contains(section, "__") || strings.Contains(key, "__") {
177+
return fmt.Errorf("WithConfig: section and key must not contain \"__\" (got section=%q, key=%q)", section, key)
178+
}
179+
if req.Env == nil {
180+
req.Env = make(map[string]string)
181+
}
182+
envKey := fmt.Sprintf("FORGEJO__%s__%s", section, key)
183+
req.Env[envKey] = value
184+
return nil
185+
}
186+
}

0 commit comments

Comments
 (0)