feat: add gitea to the docker stack

This commit is contained in:
Dan Anglin 2021-09-01 22:04:40 +01:00
parent fdf2db9e4e
commit 378585875c
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
12 changed files with 334 additions and 103 deletions

View file

@ -26,8 +26,7 @@ type DockerNetworkConfig struct {
Driver string `json:"driver"`
}
// DockerSharedVolumeConfig contains configuration
// for creating the shared volume.
// DockerSharedVolumeConfig contains configuration for creating the shared volume.
type DockerSharedVolumeConfig struct {
Name string `json:"name"`
}
@ -35,6 +34,7 @@ type DockerSharedVolumeConfig struct {
// Services contains a list of services and their configuration.
type ServicesConfig struct {
Traefik TraefikConfig `json:"traefik"`
Gitea GiteaConfig `json:"gitea"`
}
// TraefikConfig contains configuration for the Traefik container.
@ -48,8 +48,27 @@ type TraefikConfig struct {
Version string `json:"version"`
}
// NewConfig creates a new Config value from a given
// JSON file.
// GiteaConfig contains configuration for the Gitea container.
type GiteaConfig struct {
AppName string `json:"appName"`
BaseUri string `json:"baseUri"`
ContainerIp string `json:"containerIp"`
DataDirectory string `json:"dataDirectory"`
Domain string `json:"domain"`
GroupId int
HttpPort int `json:"httpPort"`
InternalToken string `json:"internalToken"`
LogLevel string `json:"logLevel"`
RootUrl string `json:"rootUrl"`
RunMode string `json:"runMode"`
SecretKey string `json:"secretKey"`
SshDomain string `json:"sshDomain"`
SshPort int `json:"sshPort"`
UserId int
Version string `json:"version"`
}
// NewConfig creates a new Config value from a given JSON file.
func NewConfig(file string) (Config, error) {
var c Config
var err error

View file

@ -31,11 +31,29 @@ func TestValidConfig(t *testing.T) {
Traefik: TraefikConfig{
CheckNewVersion: true,
ContainerIp: "172.17.1.2",
Domain: "forge.localhost",
Domain: "forge.test.local",
LogLevel: "info",
SendAnonymousUsage: false,
Version: "v2.4.9",
},
Gitea: GiteaConfig{
AppName: "A git hosting platform",
BaseUri: "git",
ContainerIp: "172.17.1.3",
DataDirectory: "/helix/data/gitea",
Domain: "forge.test.local",
GroupId: 0,
HttpPort: 3000,
InternalToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE2MjY0ODQxNjV9.Lp2v7vluALZtAng1jte5-SvF69iAUoh9pFBxf-IJ1a0",
LogLevel: "info",
RootUrl: "https://forge.test.local/git",
RunMode: "test",
SecretKey: "gBFbTiV4GTwzonAyyHNKghc9lmWvaTmFqZs5u0h14Qgx5yp1OKlrZKgw1e5LfCiE",
SshDomain: "forge.test.local",
SshPort: 2222,
UserId: 0,
Version: "1.14.4",
},
},
},
}

View file

@ -13,11 +13,27 @@
"services": {
"traefik": {
"checkNewVersion": true,
"sendAnonymousUsage": false,
"version": "v2.4.9",
"containerIp": "172.17.1.2",
"domain": "forge.test.local",
"logLevel": "info",
"domain": "forge.localhost"
"sendAnonymousUsage": false,
"version": "v2.4.9"
},
"gitea": {
"appName": "A git hosting platform",
"baseUri": "git",
"containerIp": "172.17.1.3",
"dataDirectory": "/helix/data/gitea",
"domain": "forge.test.local",
"httpPort": 3000,
"internalToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE2MjY0ODQxNjV9.Lp2v7vluALZtAng1jte5-SvF69iAUoh9pFBxf-IJ1a0",
"logLevel": "info",
"rootUrl": "https://forge.test.local/git",
"runMode": "test",
"secretKey": "gBFbTiV4GTwzonAyyHNKghc9lmWvaTmFqZs5u0h14Qgx5yp1OKlrZKgw1e5LfCiE",
"sshDomain": "forge.test.local",
"sshPort": 2222,
"version": "1.14.4"
}
}
}

View file

@ -36,6 +36,7 @@ type DockerVolume struct {
MountPath pulumi.StringInput
}
// CreateContainer creates and runs a new container.
func CreateContainer(ctx *pulumi.Context, c DockerContainerInput) error {
// all containers will mount the host's timezone and localtime files
// to ensure the correct time is synced.

View file

@ -2,7 +2,7 @@ package stacks
import (
"context"
_ "embed"
"embed"
"fmt"
"os"
"path/filepath"
@ -23,17 +23,8 @@ type DockerStack struct {
Stack auto.Stack
}
//go:embed templates/traefik/Dockerfile.tmpl
var templateTraefikDockerfile string
//go:embed templates/traefik/traefik.yaml.tmpl
var templateTraefikStaticConfig string
//go:embed templates/traefik/dynamic_dashboard.yaml.tmpl
var templateTraefikDynamicDashboardConfig string
//go:embed templates/traefik/entrypoint.sh.tmpl
var templateTraefikEntrypoint string
//go:embed templates/*
var templates embed.FS
// newContainerStack creates the ContainerStack value.
func newDockerStack(ctx context.Context, project, stack string, c config.Config) (*DockerStack, error) {
@ -52,7 +43,7 @@ func newDockerStack(ctx context.Context, project, stack string, c config.Config)
return &d, nil
}
// Preview the proposed changes to the container stack.
// Preview the proposed changes to the docker stack.
func (c *DockerStack) Preview(ctx context.Context) error {
streamer := optpreview.ProgressStreams(os.Stdout)
_, err := c.Stack.Preview(ctx, streamer)
@ -62,7 +53,7 @@ func (c *DockerStack) Preview(ctx context.Context) error {
return nil
}
// Update the container stack.
// Update the docker stack.
func (c *DockerStack) Update(ctx context.Context) error {
streamer := optup.ProgressStreams(os.Stdout)
_, err := c.Stack.Up(ctx, streamer)
@ -72,7 +63,7 @@ func (c *DockerStack) Update(ctx context.Context) error {
return nil
}
// Destroy the container stack.
// Destroy the docker stack.
func (c *DockerStack) Destroy(ctx context.Context) error {
streamer := optdestroy.ProgressStreams(os.Stdout)
_, err := c.Stack.Destroy(ctx, streamer)
@ -82,11 +73,16 @@ func (c *DockerStack) Destroy(ctx context.Context) error {
return nil
}
// deployDockerStack returns a Pulumi run function
// that is used to deploy the docker stack.
// deployDockerStack returns a Pulumi run function that is used to deploy the docker stack.
func deployDockerStack(project string, dockerConfig config.DockerConfig, services config.ServicesConfig) pulumi.RunFunc {
groupID := 2239
services.Traefik.GroupId = groupID
sharedVolumeMountPath := "/helix/shared"
groupId := 2239
services.Traefik.GroupId = groupId
services.Gitea.GroupId = groupId
services.Gitea.UserId = 2000
return func(ctx *pulumi.Context) error {
// TODO: Create the provider when we start playing with remote hosts
@ -114,43 +110,27 @@ func deployDockerStack(project string, dockerConfig config.DockerConfig, service
return err
}
base_cache, err := os.UserCacheDir()
baseCache, err := os.UserCacheDir()
if err != nil {
return fmt.Errorf("unable to get the base cache directory...\n%w", err)
}
// Create the Traefik service
traefikContextDir := filepath.Join(base_cache, "helix", project, "traefik")
projectCacheRoot := filepath.Join(baseCache, "helix", project)
if err := os.MkdirAll(traefikContextDir, 0700); err != nil {
return fmt.Errorf("unable to make the cache directory for traefik...\n%w", err)
// Traefik service.
if err = renderTemplates(services.Traefik, "traefik", projectCacheRoot); err != nil {
return err
}
if err := generateFile(services.Traefik, templateTraefikDockerfile, "traefikDocker", filepath.Join(traefikContextDir, "Dockerfile")); err != nil {
return fmt.Errorf("unable to generate the Traefik Dockerfile from template...\n%w", err)
}
if err := generateFile(services.Traefik, templateTraefikStaticConfig, "traefikStaticConf", filepath.Join(traefikContextDir, "traefik.yml")); err != nil {
return fmt.Errorf("unable to generate the Traefik static configuration from template...\n%w", err)
}
if err := generateFile(services.Traefik, templateTraefikDynamicDashboardConfig, "traefikDashboardConf", filepath.Join(traefikContextDir, "dynamic_dashboard.yaml")); err != nil {
return fmt.Errorf("unable to generate the Traefik dashboard configuration from template...\n%w", err)
}
if err := generateFile(services.Traefik, templateTraefikEntrypoint, "traefikEntrypoint", filepath.Join(traefikContextDir, "entrypoint.sh")); err != nil {
return fmt.Errorf("unable to generate the Traefik entrypoint script from template...\n%w", err)
}
c := docker.DockerImageInput{
BuildContext: pulumi.String(traefikContextDir),
Dockerfile: pulumi.String(filepath.Join(traefikContextDir, "Dockerfile")),
traefikImageInput := docker.DockerImageInput{
BuildContext: pulumi.String(filepath.Join(projectCacheRoot, "traefik")),
Dockerfile: pulumi.String(filepath.Join(projectCacheRoot, "traefik", "Dockerfile")),
ImageName: pulumi.String("helix-traefik"),
ImageTag: pulumi.String(services.Traefik.Version),
UniqueLabel: "traefik-image",
}
traefikImage, err := docker.CreateImage(ctx, c)
traefikImage, err := docker.CreateImage(ctx, traefikImageInput)
if err != nil {
return err
}
@ -164,7 +144,7 @@ func deployDockerStack(project string, dockerConfig config.DockerConfig, service
DockerVolumes: []docker.DockerVolume{
{
Name: sharedVolume.Name,
MountPath: pulumi.String("/helix/shared"),
MountPath: pulumi.String(sharedVolumeMountPath),
},
},
}
@ -173,6 +153,48 @@ func deployDockerStack(project string, dockerConfig config.DockerConfig, service
return err
}
// Gitea service
if err = renderTemplates(services.Gitea, "gitea", projectCacheRoot); err != nil {
return err
}
giteaImageInput := docker.DockerImageInput{
BuildContext: pulumi.String(filepath.Join(projectCacheRoot, "gitea")),
Dockerfile: pulumi.String(filepath.Join(projectCacheRoot, "gitea", "Dockerfile")),
ImageName: pulumi.String("helix-gitea"),
ImageTag: pulumi.String(services.Gitea.Version),
UniqueLabel: "gitea-image",
}
giteaImage, err := docker.CreateImage(ctx, giteaImageInput)
if err != nil {
return err
}
giteaContainerInput := docker.DockerContainerInput{
Image: giteaImage.ImageName,
Ipv4Address: pulumi.String(services.Gitea.ContainerIp),
Name: pulumi.String("helix-gitea"),
Network: network.Name,
DockerVolumes: []docker.DockerVolume{
{
Name: sharedVolume.Name,
MountPath: pulumi.String(sharedVolumeMountPath),
},
},
HostPathVolumes: []docker.HostPathVolume{
{
HostPath: pulumi.String(services.Gitea.DataDirectory),
MountPath: pulumi.String("/helix/gitea/data"),
},
},
UniqueLabel: "gitea-container",
}
if err = docker.CreateContainer(ctx, giteaContainerInput); err != nil {
return err
}
return nil
}
}

View file

@ -3,21 +3,57 @@ package stacks
import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
)
// generateFile renders a given template to a given filepath.
func generateFile(data interface{}, templateString, templateName, path string) error {
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("unable to create the file '%s'...\n%v", path, err)
// renderTemplates renders all template files (.tmpl) within a given directory in the
// embedded 'templates' filesystem.
func renderTemplates(data interface{}, service, projectCacheRoot string) error {
// create the context directory
contextDir := filepath.Join(projectCacheRoot, service)
if err := os.MkdirAll(contextDir, 0700); err != nil {
return fmt.Errorf("unable to make the cache directory for %s...\n%w", service, err)
}
defer file.Close()
tmpl := template.Must(template.New(templateName).Parse(templateString))
// read the service's template directory to get the list of the template files.
templateDir := "templates/" + service
if err = tmpl.Execute(file, data); err != nil {
return fmt.Errorf("unable to execute the template at '%s'...\n%v", path, err)
fsDir, err := templates.ReadDir(templateDir)
if err != nil {
return err
}
// render each template file (.tmpl) to the context directory.
for _, f := range fsDir {
filename := f.Name()
content, err := templates.ReadFile(fmt.Sprintf("%s/%s", templateDir, filename))
if err != nil {
return err
}
outputFilename := strings.TrimSuffix(filename, ".tmpl")
outputPath := filepath.Join(contextDir, outputFilename)
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("unable to create the file '%s'...\n%v", outputPath, err)
}
defer file.Close()
if !strings.HasSuffix(filename, ".tmpl") {
fmt.Fprint(file, string(content))
return nil
}
tmpl := template.Must(template.New(filename).Parse(string(content)))
if err = tmpl.Execute(file, data); err != nil {
return fmt.Errorf("unable to execute the template at '%s'...\n%v", outputFilename, err)
}
}
return nil

View file

@ -1,15 +1,55 @@
{{/* vim: set ft=dockerfile : */}}
# This is a helix flavoured Dockerfile for Gitea which is inspired from
# the official Dockerfile.rootless from https://github.com/go-gitea/gitea/
ARG GITEA_VERSION={{ .Version }}
FROM alpine:3.14.0 AS verifier
FROM gitea/gitea:${GITEA_VERSION}
RUN apk --no-cache add gnupg curl && \
curl -L https://dl.gitea.io/gitea/{{ .Version }}/gitea-{{ .Version }}-linux-amd64 -o /gitea && \
curl -L https://dl.gitea.io/gitea/{{ .Version }}/gitea-{{ .Version }}-linux-amd64.asc -o /gitea.asc && \
gpg --keyserver keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2 && \
gpg --verify /gitea.asc /gitea
ENV USER_UID=1000 \
USER_GID=1000 \
GITEA_CUSTOM=/helix/gitea/custom
ADD files app.ini /helix/gitea/custom/app.ini
FROM alpine:3.14.0
RUN chown -R ${USER_ID}:${USER_GID} /helix
RUN apk --no-cache add \
bash \
ca-certificates \
gettext \
git \
curl \
gnupg
EXPOSE {{ .App.HttpPort }} {{ .App.SshPort }}
RUN addgroup -S -g {{ .GroupId }} git && \
adduser -S -H -D -h /helix/gitea/data/home -s /bin/bash -u {{ .UserId }} -G git git && \
mkdir -p /helix/gitea/data /helix/gitea/tmp && \
chown git:git /helix/gitea/data && chmod 0700 /helix/gitea/data && \
chown git:git /helix/gitea/tmp && chmod 0700 /helix/gitea/tmp
COPY --from=verifier --chown=root:root /gitea /usr/local/bin/gitea
ADD app.ini /helix/gitea/config/
ADD entrypoint.sh /usr/local/bin/entrypoint.sh
ADD --chown={{ .UserId }}:{{ .GroupId }} dynamic_git.yaml /helix/gitea/tmp/
RUN chown -R git:git /helix/gitea/config/app.ini && \
chmod 0400 /helix/gitea/config/app.ini && \
chmod a+x /usr/local/bin/gitea && \
chmod a+x /usr/local/bin/entrypoint.sh
ENV GITEA_WORK_DIR=/helix/gitea/data \
GITEA_CUSTOM=/helix/gitea/data/custom \
GITEA_APP_INI=/helix/gitea/config/app.ini \
GITEA_BIN=/usr/local/bin/gitea \
HOME=/helix/gitea/data/home
USER {{ .UserId }}:{{ .GroupId }}
WORKDIR /helix/gitea/data
VOLUME ["/helix/gitea/data"]
EXPOSE {{ .HttpPort }} {{ .SshPort }}
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD []

View file

@ -1,16 +1,16 @@
{{/* vim: set ft=dosini : */}}
APP_NAME = {{ .App.Name }}
RUN_MODE = {{ .App.RunMode }}
APP_NAME = {{ .AppName }}
RUN_USER = git
RUN_MODE = {{ .RunMode }}
[repository]
ROOT = /data/gitea/repositories
ROOT = /helix/gitea/data/git/repositories
DEFAULT_BRANCH = main
[repository.local]
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
LOCAL_COPY_PATH = /helix/gitea/tmp/local-repo
[repository.upload]
TEMP_PATH = /data/gitea/uploads
TEMP_PATH = /helix/gitea/tmp/uploads
[repository.signing]
; Gitea will sign initial commits only if the user has a public key.
@ -20,17 +20,18 @@ INITIAL_COMMIT = pubkey
DEFAULT_THEME = arc-green
[server]
APP_DATA_PATH = /data/gitea
DOMAIN = {{ .App.Domain }}
HTTP_ADDR = {{ .Container.Ip }}
HTTP_PORT = {{ .App.HttpPort }}
ROOT_URL = {{ .App.RootUrl }}
DISABLE_SSH = false
SSH_DOMAIN = {{ .App.SshDomain }}
SSH_PORT = {{ .App.SshPort }}
SSH_LISTEN_PORT = {{ .App.SshPort }}
LFS_START_SERVER = false
LFS_CONTENT_PATH = /data/gitea/lfs
APP_DATA_PATH = /helix/gitea/data/git
DOMAIN = {{ .Domain }}
HTTP_ADDR = {{ .ContainerIp }}
HTTP_PORT = {{ .HttpPort }}
ROOT_URL = {{ .RootUrl }}
DISABLE_SSH = false
SSH_DOMAIN = {{ .SshDomain }}
SSH_PORT = {{ .SshPort }}
SSH_LISTEN_PORT = {{ .SshPort }}
BUILTIN_SSH_SERVER_USER = git
LFS_START_SERVER = false
LFS_CONTENT_PATH = /helix/gitea/data/git/lfs
[ssh.minimum_key_sizes]
ED25519 = 256
@ -40,16 +41,20 @@ DSA = -1
[database]
DB_TYPE = sqlite3
PATH = /data/gitea/database/gitea.db
PATH = /helix/gitea/data/database/gitea.db
HOST = localhost:3306
NAME = gitea
USER = gitea
PASSWD =
[indexer]
ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve
ISSUE_INDEXER_PATH = /helix/gitea/data/indexers/issues.bleve
[session]
PROVIDER_CONFIG = /data/gitea/sessions
PROVIDER_CONFIG = /helix/gitea/data/sessions
[queue]
DATADIR = /data/gitea/queues
DATADIR = /helix/gitea/data/queues
[admin]
DISABLE_REGULAR_ORG_CREATION = true
@ -57,27 +62,30 @@ DEFAULT_EMAIL_NOTIFICATION = disabled
[security]
INSTALL_LOCK = true
SECRET_KEY = {{ .App.SecretKey }}
SECRET_KEY = {{ .SecretKey }}
INTERNAL_TOKEN = {{ .InternalToken }}
LOGIN_REMEMBER_DAYS = 1
MIN_PASSWORD_LENGTH = 12
PASSWORD_COMPLEXITY = lower,upper,digit
[service]
DISABLED_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
DISABLE_REGISTRATION = true
[service.explore]
REQUIRE_SIGNIN_VIEW = false
[picture]
AVATAR_UPLOAD_PATH = /data/gitea/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars
AVATAR_UPLOAD_PATH = /helix/gitea/data/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = /helix/gitea/data/repo-avatars
[attachment]
ENABLED = true
PATH = /data/gitea/attachments
PATH = /helix/gitea/data/attachments
[log]
ROOT_PATH = /data/gitea/log
ROOT_PATH = /helix/gitea/data/log
MODE = console
LEVEL = {{ .App.LogLevel }}
LEVEL = {{ .LogLevel }}
[log.console]
STDERR = false
@ -90,3 +98,6 @@ NAMES = English
SHOW_FOOTER_BRANDING = true
SHOW_FOOTER_VERSION = false
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false
[oauth2]
ENABLE = false

View file

@ -0,0 +1,34 @@
---
http:
routers:
gitea:
entryPoints:
- "https"
rule: "Host(`{{ .Domain }}`) && PathPrefix(`/{{ .BaseUri }}`)"
service: "git"
middlewares:
- "git-strip-prefix"
tls: {}
middlewares:
git-strip-prefix:
stripPrefix:
prefixes:
- "/{{ .BaseUri }}"
services:
git:
loadBalancer:
servers:
- url: "http://{{ .ContainerIp }}:{{ .HttpPort }}/"
tcp:
routers:
gitSSH:
entryPoints:
- "ssh"
rule: "HostSNI(`*`)"
service: "gitSSH"
services:
gitSSH:
loadBalancer:
servers:
- address: "{{ .ContainerIp }}:{{ .SshPort }}"

View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
TRAEFIK_CONFIG_SRC="/helix/gitea/tmp/dynamic_git.yaml"
TRAEFIK_CONFIG_DEST="/helix/shared/traefik/dynamic/dynamic_git.yaml"
# Create the home directory.
if ! [ -d ${HOME} ]; then
mkdir -p ${HOME}
chmod 0700 ${HOME}
fi
# Create the custom directory.
if ! [ -d ${GITEA_CUSTOM} ]; then
mkdir -p ${GITEA_CUSTOM}
chmod 0500 ${GITEA_CUSTOM}
fi
# Move the dynamic Traefik config to the shared volume.
if [ -f ${TRAEFIK_CONFIG_SRC} ]; then
mv ${TRAEFIK_CONFIG_SRC} ${TRAEFIK_CONFIG_DEST}
fi
if [ $# -gt 0 ]; then
exec "$@"
else
exec ${GITEA_BIN} -c ${GITEA_APP_INI} web
fi

View file

@ -1,6 +1,6 @@
FROM traefik:{{ .Version }}
ADD traefik.yml /helix/traefik/
ADD traefik.yaml /helix/traefik/
ADD entrypoint.sh /
@ -10,4 +10,4 @@ RUN chmod +x /entrypoint.sh
EXPOSE 22 80 443
CMD ["--configfile=/helix/traefik/traefik.yml"]
CMD ["--configfile=/helix/traefik/traefik.yaml"]

View file

@ -1,13 +1,18 @@
#!/bin/sh
set -e
DASHBOARD_CONFIG_SRC="/tmp/dynamic_dashboard.yaml"
DASHBOARD_CONFIG_DEST="/helix/shared/traefik/dynamic/dashboard.yaml"
# Create the dynamic config directory in the shared volume.
mkdir -p /helix/shared/traefik/dynamic
chgrp {{ .GroupId }} /helix/shared/traefik/dynamic
chmod a-rwx,u+rwx,g+rwx /helix/shared/traefik/dynamic
# Move the dashboard config to the new directory.
mv /tmp/dynamic_dashboard.yaml /helix/shared/traefik/dynamic/dashboard.yaml
if [ -f ${DASHBOARD_CONFIG_SRC} ]; then
mv ${DASHBOARD_CONFIG_SRC} ${DASHBOARD_CONFIG_DEST}
fi
# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then