Skip to content

Commit

Permalink
chore(misc/loop): Setup the portal loop infra (#1400)
Browse files Browse the repository at this point in the history
Portal loop infrastructure code.

Co-authored-by: Morgan <git@howl.moe>
Co-authored-by: Morgan <morgan@morganbaz.com>
Co-authored-by: deelawn <dboltz03@gmail.com>
Co-authored-by: Guilhem Fanton <8671905+gfanton@users.noreply.github.com>
Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
Co-authored-by: Miloš Živković <milos.zivkovic@tendermint.com>
Co-authored-by: Lee ByeongJun <lbj199874@gmail.com>
Co-authored-by: Kazaï <149690535+kazai777@users.noreply.github.com>
Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
  • Loading branch information
10 people authored Mar 19, 2024
1 parent 173d5a2 commit b93032a
Show file tree
Hide file tree
Showing 22 changed files with 1,476 additions and 94 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/portal-loop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: portal-loop

on:
push:
paths:
- misc/loop
- .github/workflows/portal-loop.yml
branches:
- "master"
- "ops/portal-loop"
tags:
- "v*"

permissions:
contents: read
packages: write

jobs:
portal-loop:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata portalloopd
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/portalloopd
tags: |
type=raw,value=latest
type=semver,pattern=v{{version}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: ./misc/loop
target: portalloopd
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
3 changes: 3 additions & 0 deletions misc/loop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/portalloopd
/backups
/traefik/letsencrypt
19 changes: 19 additions & 0 deletions misc/loop/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM golang:alpine AS builder

COPY . /go/src/github.com/gnolang/gno/misc/loop

WORKDIR /go/src/github.com/gnolang/gno/misc/loop

RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
go build -o /build/portalloopd ./cmd

# Final image for portalloopd
FROM docker AS portalloopd

RUN apk add bash curl jq

COPY --from=builder /build/portalloopd /usr/bin/portalloopd

ENTRYPOINT [ "/usr/bin/portalloopd" ]
CMD [ "serve" ]
52 changes: 15 additions & 37 deletions misc/loop/Makefile
Original file line number Diff line number Diff line change
@@ -1,44 +1,22 @@
# The startup delay (waits until the node is "ready")
DELAY ?= 10 # seconds
# The temporary backup file for transactions
BACKUP_FILE ?= $(abspath ./txs_backup.log)
# The entire txs history across all iterations
HISTORY_OUTPUT := $(abspath ./txs_history.log)
all: docker.start

# The gnoland binary
gnoland_bin := go run github.com/gnolang/gno/gno.land/cmd/gnoland
# The tx archive binary
tx_bin := go run github.com/gnolang/tx-archive/cmd
docker.start: # Start the portal loop
docker compose up -d

# The relative gno.land directory
gnoland_dir := $(abspath ../../gno.land)
docker.stop: # Stop the portal loop
docker compose down
docker rm -f $(docker ps -aq --filter "label=the-portal-loop")

all: loop
docker.build: # (re)Build snapshotter image
docker compose build

start.gnoland:
cd $(gnoland_dir) && $(gnoland_bin) start -skip-failing-genesis-txs -genesis-txs-file $(HISTORY_OUTPUT)
clean.gnoland:
make -C $(gnoland_dir) fclean
.PHONY: start.gnoland clean.gnoland
docker.pull: # Pull new images to update versions
docker compose pull

# Starts the backup service
# and backs up transactions into a file
# that is wiped on every loop
tx.backup:
sleep $(DELAY)
$(tx_bin) backup -legacy -watch -overwrite -output-path "$(BACKUP_FILE)"
.PHONY: tx.backup
portalloopd.bash: # Get a bash command inside of the portalloopd container
docker compose exec portalloopd bash

# Saves the history from previous iterations into
# a temporary transactions log
save.history:
@test -e $(BACKUP_FILE) || (echo "No existing backup file not found: '$(BACKUP_FILE)'"; exit 1)
cat $(BACKUP_FILE) >> $(HISTORY_OUTPUT)
.PHONY: save.history
switch: portalloopd.switch

loop: clean.gnoland
# backup history, if needed
$(MAKE) save.history || true
# run our dev loop
./run_loop.sh
.PHONY: loop
portalloopd.switch: # Force switch the portal loop with latest image
docker compose exec portalloopd switch
42 changes: 42 additions & 0 deletions misc/loop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# The portal loop :infinity:

## What is it?

It's a Gnoland node that aim to run with always the latest version of gno and never loose transactions history.

For more information, see issue on github [gnolang/gno#1239](https://github.com/gnolang/gno/issues/1239)


## How to use

Start the loop with:

```sh
$ docker compose up -d

# or using the Makefile

$ make
```

The [`portalloopd`](./cmd/portalloopd) binary is starting inside of the docker container `portalloopd`

This script is doing:

- Setup the current portal-loop in read only mode
- Pull the latest version of [ghcr.io/gnolang/gno]()
- Backup the txs using [gnolang/tx-archive](https://github.com/gnolang/tx-archive)
- Start a new docker container with the backups files
- Changing the proxy (traefik) to redirect to the new portal loop
- Unlock read only mode
- Stop the previous loop

### Makefile helper

You can find a [Makefile](./Makefile) to help you interact with the portal loop

- Force switch of the portal loop with a new version

```bash
make portalloopd.switch
```
Empty file added misc/loop/backups/.keep
Empty file.
85 changes: 85 additions & 0 deletions misc/loop/cmd/cmd_backup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"context"
"flag"
"os"

"github.com/docker/docker/client"
"github.com/gnolang/gno/tm2/pkg/commands"
)

type backupCfg struct {
rpcAddr string
traefikGnoFile string
backupDir string
hostPWD string
}

func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) {
if os.Getenv("HOST_PWD") == "" {
os.Setenv("HOST_PWD", os.Getenv("PWD"))
}

if os.Getenv("BACKUP_DIR") == "" {
os.Setenv("BACKUP_DIR", "./backups")
}

if os.Getenv("RPC_URL") == "" {
os.Setenv("RPC_URL", "http://rpc.portal.gno.local:26657")
}

if os.Getenv("PROM_ADDR") == "" {
os.Setenv("PROM_ADDR", ":9090")
}

if os.Getenv("TRAEFIK_GNO_FILE") == "" {
os.Setenv("TRAEFIK_GNO_FILE", "./traefik/gno.yml")
}

fs.StringVar(&c.rpcAddr, "rpc", os.Getenv("RPC_URL"), "tendermint rpc url")
fs.StringVar(&c.traefikGnoFile, "traefik-gno-file", os.Getenv("TRAEFIK_GNO_FILE"), "traefik gno file")
fs.StringVar(&c.backupDir, "backup-dir", os.Getenv("BACKUP_DIR"), "backup directory")
fs.StringVar(&c.hostPWD, "pwd", os.Getenv("HOST_PWD"), "host pwd (for docker usage)")
}

func newBackupCmd(io commands.IO) *commands.Command {
cfg := &backupCfg{}

return commands.NewCommand(
commands.Metadata{
Name: "backup",
ShortUsage: "backup [flags]",
},
cfg,
func(ctx context.Context, _ []string) error {
return execBackup(ctx, cfg)
},
)
}

func execBackup(ctx context.Context, cfg *backupCfg) error {
dockerClient, err := client.NewEnvClient()
if err != nil {
return err
}

portalLoop := &snapshotter{}

portalLoop, err = NewSnapshotter(dockerClient, config{
backupDir: cfg.backupDir,
rpcAddr: cfg.rpcAddr,
hostPWD: cfg.hostPWD,
traefikGnoFile: cfg.traefikGnoFile,
})
if err != nil {
return err
}

err = StartPortalLoop(ctx, portalLoop, false)
if err != nil {
return err
}

return portalLoop.backupTXs(ctx, portalLoop.url)
}
116 changes: 116 additions & 0 deletions misc/loop/cmd/cmd_serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package main

import (
"context"
"flag"
"net/http"
"os"
"time"

"github.com/docker/docker/client"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
)

type serveCfg struct {
rpcAddr string
traefikGnoFile string
backupDir string
hostPWD string
}

type serveService struct {
cfg serveCfg

// TODO(albttx): put getter on it with RMutex
portalLoop *snapshotter

portalLoopURL string
}

func (c *serveCfg) RegisterFlags(fs *flag.FlagSet) {
if os.Getenv("HOST_PWD") == "" {
os.Setenv("HOST_PWD", os.Getenv("PWD"))
}

if os.Getenv("BACKUP_DIR") == "" {
os.Setenv("BACKUP_DIR", "./backups")
}

if os.Getenv("RPC_URL") == "" {
os.Setenv("RPC_URL", "http://rpc.portal.gno.local:26657")
}

if os.Getenv("PROM_ADDR") == "" {
os.Setenv("PROM_ADDR", ":9090")
}

if os.Getenv("TRAEFIK_GNO_FILE") == "" {
os.Setenv("TRAEFIK_GNO_FILE", "./traefik/gno.yml")
}

fs.StringVar(&c.rpcAddr, "rpc", os.Getenv("RPC_URL"), "tendermint rpc url")
fs.StringVar(&c.traefikGnoFile, "traefik-gno-file", os.Getenv("TRAEFIK_GNO_FILE"), "traefik gno file")
fs.StringVar(&c.backupDir, "backup-dir", os.Getenv("BACKUP_DIR"), "backup directory")
fs.StringVar(&c.hostPWD, "pwd", os.Getenv("HOST_PWD"), "host pwd (for docker usage)")
}

func newServeCmd(io commands.IO) *commands.Command {
cfg := &serveCfg{}

return commands.NewCommand(
commands.Metadata{
Name: "serve",
ShortUsage: "serve [flags]",
},
cfg,
func(ctx context.Context, args []string) error {
return execServe(ctx, cfg, args)
},
)
}

func execServe(ctx context.Context, cfg *serveCfg, args []string) error {
dockerClient, err := client.NewEnvClient()
if err != nil {
return err
}

portalLoop := &snapshotter{}

// Serve monitoring
go func() {
s := &monitoringService{
portalLoop: portalLoop,
}

for portalLoop.url == "" {
time.Sleep(time.Second * 1)
}

go s.recordMetrics()

http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(os.Getenv("PROM_ADDR"), nil)
}()

// the loop
for {
portalLoop, err = NewSnapshotter(dockerClient, config{
backupDir: cfg.backupDir,
rpcAddr: cfg.rpcAddr,
hostPWD: cfg.hostPWD,
traefikGnoFile: cfg.traefikGnoFile,
})
if err != nil {
return err
}

err = StartPortalLoop(ctx, portalLoop, false)
if err != nil {
logrus.WithError(err).Error()
}
time.Sleep(time.Second * 120)
}
}
Loading

0 comments on commit b93032a

Please sign in to comment.