Merge branch 'ind-bootstrap' into protocol
# Conflicts: # version.go
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
indra
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
@@ -26,9 +26,93 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
timeout = 120 * time.Second
|
||||
defaultBuildingTimeout = 800 * time.Second
|
||||
defaultRepositoryName = "indralabs"
|
||||
defaultBuildContainer = "golang:1.19.4"
|
||||
)
|
||||
|
||||
func strPtr(str string) *string { return &str }
|
||||
|
||||
var buildConfigurations = []docker.BuildConfiguration{
|
||||
//docker.BuildConfiguration{
|
||||
// Name: defaultRepositoryName + "/" + "scratch",
|
||||
// ContextFilePath: "/tmp/scratch.tar",
|
||||
// BuildOpts: types.ImageBuildOptions{
|
||||
// Dockerfile: "docker/scratch/Dockerfile",
|
||||
// Tags: []string{
|
||||
// indra.SemVer,
|
||||
// "latest",
|
||||
// },
|
||||
// BuildArgs: map[string]*string{
|
||||
// "base_image": strPtr("busybox"),
|
||||
// },
|
||||
// SuppressOutput: false,
|
||||
// Remove: true,
|
||||
// ForceRemove: true,
|
||||
// PullParent: true,
|
||||
// },
|
||||
//},
|
||||
docker.BuildConfiguration{
|
||||
Name: defaultRepositoryName + "/" + "btcd",
|
||||
ContextFilePath: "/tmp/btcd.tar",
|
||||
BuildOpts: types.ImageBuildOptions{
|
||||
Dockerfile: "docker/btcd/Dockerfile",
|
||||
Tags: []string{
|
||||
"v0.23.4",
|
||||
"latest",
|
||||
},
|
||||
BuildArgs: map[string]*string{
|
||||
"base_image": strPtr(defaultBuildContainer),
|
||||
"target_image": strPtr("indralabs/scratch:latest"),
|
||||
// This argument is the tag fetched by git
|
||||
// It MUST be updated alongside the tag above
|
||||
"git_repository": strPtr("github.com/btcsuite/btcd"),
|
||||
"git_tag": strPtr("v0.23.4"),
|
||||
},
|
||||
SuppressOutput: false,
|
||||
Remove: true,
|
||||
ForceRemove: true,
|
||||
PullParent: true,
|
||||
},
|
||||
},
|
||||
//docker.BuildConfiguration{
|
||||
// Name: defaultRepositoryName + "/" + "lnd",
|
||||
// ContextFilePath: "/tmp/lnd.tar",
|
||||
// BuildOpts: types.ImageBuildOptions{
|
||||
// Dockerfile: "docker/lnd/Dockerfile",
|
||||
// Tags: []string{
|
||||
// "v0.15.5-beta",
|
||||
// "latest",
|
||||
// },
|
||||
// BuildArgs: map[string]*string{
|
||||
// // This argument is the tag fetched by git
|
||||
// // It MUST be updated alongside the tag above
|
||||
// "git_tag": strPtr("v0.15.5-beta"),
|
||||
// },
|
||||
// SuppressOutput: false,
|
||||
// Remove: true,
|
||||
// ForceRemove: true,
|
||||
// PullParent: true,
|
||||
// },
|
||||
//},
|
||||
//docker.BuildConfiguration{
|
||||
// Name: defaultRepositoryName + "/" + "indra",
|
||||
// ContextFilePath: "/tmp/indra-" + indra.SemVer + ".tar",
|
||||
// BuildOpts: types.ImageBuildOptions{
|
||||
// Dockerfile: "docker/indra/Dockerfile",
|
||||
// Tags: []string{
|
||||
// indra.SemVer,
|
||||
// "latest",
|
||||
// },
|
||||
// BuildArgs: map[string]*string{},
|
||||
// SuppressOutput: false,
|
||||
// Remove: true,
|
||||
// ForceRemove: true,
|
||||
// PullParent: true,
|
||||
// },
|
||||
//},
|
||||
}
|
||||
|
||||
var commands = &cmds.Command{
|
||||
Name: "release",
|
||||
Description: "Builds the indra docker image and pushes it to a list of docker repositories.",
|
||||
@@ -61,7 +145,7 @@ var commands = &cmds.Command{
|
||||
}
|
||||
|
||||
// Set a Timeout for 120 seconds
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultBuildingTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Setup a new instance of the docker client
|
||||
@@ -75,15 +159,14 @@ var commands = &cmds.Command{
|
||||
|
||||
defer cli.Close()
|
||||
|
||||
// Get ready to submit a build
|
||||
|
||||
var builder = docker.NewBuilder(ctx, cli)
|
||||
// Get ready to submit the builds
|
||||
var builder = docker.NewBuilder(ctx, cli, buildConfigurations)
|
||||
|
||||
if err = builder.Build(); check(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = builder.Push(types.ImagePushOptions{}); check(err) {
|
||||
if err = builder.Push(); check(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -118,9 +118,7 @@ var commands = &cmds.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
log.I.Ln("starting the server.")
|
||||
|
||||
if srv.Serve(); check(err) {
|
||||
if err = srv.Serve(); check(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
58
docker/btcd/Dockerfile
Normal file
58
docker/btcd/Dockerfile
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
ARG base_image="golang"
|
||||
ARG target_image="indralabs/scratch"
|
||||
|
||||
# ---
|
||||
# Build Process
|
||||
# ---
|
||||
|
||||
FROM ${base_image} AS builder
|
||||
|
||||
# Get the repo and build
|
||||
ARG git_repository="github.com/indra-labs/btcd"
|
||||
ARG git_tag="master"
|
||||
|
||||
# Install dependencies and build the binaries.
|
||||
RUN git clone "https://"${git_repository} /go/src/${git_repository}
|
||||
|
||||
WORKDIR $GOPATH/src/${git_repository}
|
||||
|
||||
RUN git checkout ${git_tag}
|
||||
|
||||
# Source/Target release defaults
|
||||
ARG ARCH=amd64
|
||||
ARG GOARCH=amd64
|
||||
|
||||
ENV GO111MODULE=on GOOS=linux
|
||||
|
||||
WORKDIR $GOPATH/src/${git_repository}
|
||||
|
||||
RUN cp sample-btcd.conf /tmp/btcd.conf
|
||||
|
||||
RUN set -ex \
|
||||
&& CGO_ENABLED=0 go build --ldflags '-w -s' -o /tmp/bin/btcd . \
|
||||
&& CGO_ENABLED=0 go build --ldflags '-w -s' -o /tmp/bin/ ./cmd/...
|
||||
|
||||
# ---
|
||||
# Target Configuration
|
||||
# ---
|
||||
|
||||
FROM indralabs/scratch:latest
|
||||
|
||||
## Migrate the binaries and storage folder
|
||||
|
||||
COPY --from=builder /tmp/btcd.conf /etc/btcd/btcd.conf
|
||||
COPY --from=builder /tmp/bin /bin
|
||||
|
||||
# Enable the btcd user
|
||||
USER btcd:btcd
|
||||
|
||||
# Set the data volumes
|
||||
#VOLUME ["/etc/btcd"]
|
||||
#VOLUME ["/var/btcd"]
|
||||
|
||||
# :8333 btcd peer-to-peer port
|
||||
# :8334 btcd RPC port
|
||||
EXPOSE 8333 8334
|
||||
|
||||
ENTRYPOINT ["/bin/btcd", "--configfile=/etc/btcd/btcd.conf", "--datadir=/var/btcd", "--logdir=/var/btcd", "--rpckey=/etc/btcd/keys/rpc.key", "--rpccert=/etc/btcd/keys/rpc.cert", "--listen=0.0.0.0:8333", "--rpclisten=0.0.0.0:8334"]
|
||||
72
docker/lnd/Dockerfile
Normal file
72
docker/lnd/Dockerfile
Normal file
@@ -0,0 +1,72 @@
|
||||
FROM golang:1.19.4 AS builder
|
||||
|
||||
# User/Group definition
|
||||
ENV USER=lnd GROUP=lnd UID=9735 GID=9735
|
||||
|
||||
## Create a user/group for indra, to be migrated to the target container
|
||||
RUN addgroup ${GROUP} --gid ${GID} \
|
||||
&& adduser \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--home "/var/lnd" \
|
||||
--shell "/sbin/nologin" \
|
||||
#--no-create-home \
|
||||
--uid "${UID}" \
|
||||
--gid "${GID}" \
|
||||
"${USER}" \
|
||||
&& mkdir -pv /var/lnd/.lnd && chown -R lnd:lnd /var/lnd
|
||||
|
||||
# Pass a tag, branch or a commit using build-arg. This allows a docker
|
||||
# image to be built from a specified Git state. The default image
|
||||
# will use the Git tip of master by default.
|
||||
ARG git_tag="master"
|
||||
|
||||
# Install dependencies and build the binaries.
|
||||
RUN git clone "https://github.com/lightningnetwork/lnd" /go/src/github.com/lightningnetwork/lnd \
|
||||
&& cd /go/src/github.com/lightningnetwork/lnd \
|
||||
&& git checkout ${git_tag}
|
||||
|
||||
# Source/Target release defaults
|
||||
ARG ARCH=amd64
|
||||
ARG GOARCH=amd64
|
||||
|
||||
ENV GO111MODULE=on GOOS=linux
|
||||
|
||||
WORKDIR $GOPATH/src/github.com/lightningnetwork/lnd
|
||||
|
||||
RUN set -ex \
|
||||
&& cp /go/src/github.com/lightningnetwork/lnd/sample-lnd.conf /var/lnd/.lnd/lnd.conf \
|
||||
&& chown -R lnd:lnd /var/lnd \
|
||||
&& CGO_ENABLED=0 go build --ldflags '-w -s' -o /bin/lnd ./cmd/lnd/. \
|
||||
&& CGO_ENABLED=0 go build --ldflags '-w -s' -o /bin/lncli ./cmd/lncli/.
|
||||
|
||||
# ---
|
||||
# Configure and Build the target container
|
||||
# ---
|
||||
|
||||
FROM scratch
|
||||
|
||||
# Migrate User/Group to target
|
||||
COPY --from=builder /etc/passwd /etc/passwd
|
||||
COPY --from=builder /etc/group /etc/group
|
||||
|
||||
# Migrate the binaries and storage folder
|
||||
COPY --from=builder /bin /bin
|
||||
COPY --from=builder --chown=lnd:lnd /var/lnd /var/lnd
|
||||
|
||||
# Enable the indra user
|
||||
USER lnd:lnd
|
||||
|
||||
# ENV defaults
|
||||
# ENV IND_LOGFILEPATH=""
|
||||
|
||||
# Set the data volume
|
||||
#VOLUME ["/var/lnd"]
|
||||
|
||||
# :9735 lnd peer-to-peer port
|
||||
# :10009 lnd RPC port
|
||||
EXPOSE 9735 10009
|
||||
|
||||
ENTRYPOINT ["/bin/lnd"]
|
||||
|
||||
# docker run -it --rm --entrypoint="/bin/lncli" -v ~/rpc.cert:/var/lnd/.lnd/rpc.cert indralabs/lnd --skipverify -s <host>:<port> -u <username> -P <password> <function>
|
||||
134
docker/scratch/Dockerfile
Normal file
134
docker/scratch/Dockerfile
Normal file
@@ -0,0 +1,134 @@
|
||||
|
||||
ARG base_image=busybox
|
||||
|
||||
FROM ${base_image} as base
|
||||
|
||||
RUN set -ex && echo "creating root filesystem" \
|
||||
&& mkdir -pv /tmp/root-fs \
|
||||
&& mkdir -pv /tmp/root-fs/etc \
|
||||
&& mkdir -pv /tmp/root-fs/var \
|
||||
&& mkdir -pv /tmp/root-fs/bin
|
||||
|
||||
RUN set -ex && echo "checking root filesystem" \
|
||||
&& ls -hal /tmp/root-fs \
|
||||
&& ls -hal /tmp/root-fs/etc \
|
||||
&& ls -hal /tmp/root-fs/var \
|
||||
&& ls -hal /tmp/root-fs/bin
|
||||
|
||||
##
|
||||
## Users and Groups
|
||||
##
|
||||
|
||||
RUN set -ex && echo "adding users and groups" \
|
||||
&& echo "btcd:*:::::::" >> /etc/shadow \
|
||||
&& echo "btcd:x:8333:" >> /etc/group \
|
||||
&& echo "btcd:x:8333:8333:btcd:/var/btcd:/sbin/false" >> /etc/passwd \
|
||||
&& echo "lnd:*:::::::" >> /etc/shadow \
|
||||
&& echo "lnd:x:9735:" >> /etc/group \
|
||||
&& echo "lnd:x:9735:9735:lnd:/var/lnd:/sbin/false" >> /etc/passwd \
|
||||
&& echo "indra:*:::::::" >> /etc/shadow \
|
||||
&& echo "indra:x:8337:" >> /etc/group \
|
||||
&& echo "indra:x:8337:8337:indra:/var/indra:/sbin/false" >> /etc/passwd
|
||||
|
||||
RUN set -ex && echo "checking users and groups" \
|
||||
&& cat /etc/shadow \
|
||||
&& cat /etc/group \
|
||||
&& cat /etc/passwd
|
||||
|
||||
RUN set -ex && echo "copying users and groups to root filesystem" \
|
||||
&& cp -p /etc/shadow /tmp/root-fs/etc/shadow \
|
||||
&& cp -p /etc/group /tmp/root-fs/etc/group \
|
||||
&& cp -p /etc/passwd /tmp/root-fs/etc/passwd
|
||||
|
||||
# DEBUG
|
||||
RUN set -ex && echo "checking users and groups to root filesystem" \
|
||||
&& ls -hal /tmp/root-fs/etc \
|
||||
&& cat /tmp/root-fs/etc/shadow \
|
||||
&& cat /tmp/root-fs/etc/passwd \
|
||||
&& cat /tmp/root-fs/etc/group
|
||||
|
||||
##
|
||||
## Configuration and Data directories
|
||||
##
|
||||
|
||||
RUN set -ex && echo "adding and permissioning /etc directories" \
|
||||
&& mkdir -pv /etc/btcd && chmod 755 /etc/btcd \
|
||||
&& mkdir -pv /etc/btcd/keys && chmod 750 /etc/btcd/keys && chown btcd:btcd /etc/btcd/keys \
|
||||
&& mkdir -pv /etc/lnd && chmod 755 /etc/lnd \
|
||||
&& mkdir -pv /etc/lnd/keys && chmod 750 /etc/lnd/keys && chown lnd:lnd /etc/lnd/keys \
|
||||
&& mkdir -pv /etc/indra && chmod 755 /etc/indra
|
||||
|
||||
RUN set -ex && echo "copying /etc directories to root filesystem" \
|
||||
&& cp -rp /etc/btcd /tmp/root-fs/etc/btcd \
|
||||
&& cp -rp /etc/lnd /tmp/root-fs/etc/lnd \
|
||||
&& cp -rp /etc/indra /tmp/root-fs/etc/indra
|
||||
|
||||
# DEBUG
|
||||
RUN set -ex && echo "checking /etc directories on root filesystem" \
|
||||
&& ls -hal /tmp/root-fs/etc \
|
||||
&& ls -hal /tmp/root-fs/etc/btcd \
|
||||
&& ls -hal /tmp/root-fs/etc/btcd/keys \
|
||||
&& ls -hal /tmp/root-fs/etc/lnd \
|
||||
&& ls -hal /tmp/root-fs/etc/lnd/keys \
|
||||
&& ls -hal /tmp/root-fs/etc/indra
|
||||
|
||||
RUN set -ex && echo "adding and permissioning /var directories" \
|
||||
&& mkdir -pv /var/btcd && chmod 750 /var/btcd && chown btcd:btcd /var/btcd \
|
||||
&& mkdir -pv /var/btcd/.btcd && chmod 750 /var/btcd/.btcd && chown btcd:btcd /var/btcd/.btcd \
|
||||
&& mkdir -pv /var/lnd && chmod 750 /var/lnd && chown lnd:lnd /var/lnd \
|
||||
&& mkdir -pv /var/lnd/.lnd && chmod 750 /var/lnd/.lnd && chown lnd:lnd /var/lnd/.lnd \
|
||||
&& mkdir -pv /var/indra && chmod 750 /var/indra && chown indra:indra /var/indra
|
||||
|
||||
RUN set -ex && echo "copying /var directories to root filesystem" \
|
||||
&& cp -rp /var/btcd /tmp/root-fs/var/btcd \
|
||||
&& cp -rp /var/lnd /tmp/root-fs/var/lnd \
|
||||
&& cp -rp /var/indra /tmp/root-fs/var/indra
|
||||
|
||||
# DEBUG
|
||||
RUN set -ex && echo "checking /var directories on root filesystem" \
|
||||
&& ls -hal /tmp/root-fs/var \
|
||||
&& ls -hal /tmp/root-fs/var/btcd \
|
||||
&& ls -hal /tmp/root-fs/var/btcd/.btcd \
|
||||
&& ls -hal /tmp/root-fs/var/lnd \
|
||||
&& ls -hal /tmp/root-fs/var/lnd/.lnd \
|
||||
&& ls -hal /tmp/root-fs/var/indra
|
||||
|
||||
WORKDIR /tmp/root-fs
|
||||
|
||||
RUN set -ex && echo "building root-fs tarball" \
|
||||
&& tar -cvzf /tmp/root-fs.tgz . \
|
||||
&& rm -rf /tmp/root-fs \
|
||||
&& ls -hal /tmp
|
||||
|
||||
#RUN set -ex && tar -xzvf /tmp/root-fs.tgz \
|
||||
# && ls -hal /tmp \
|
||||
# && ls -hal /tmp/root-fs \
|
||||
# && ls -hal /tmp/root-fs/etc \
|
||||
# && ls -hal /tmp/root-fs/etc/btcd \
|
||||
|
||||
##
|
||||
## Base Image
|
||||
##
|
||||
|
||||
#
|
||||
# Note: We CANNOT use the scratch container to build the our scratch image.
|
||||
#
|
||||
# When using the COPY command between container, docker does not preserve permissions.
|
||||
# Instead, we will opt for generating a root-fs on the build image and extracting it as a tarball.
|
||||
#
|
||||
|
||||
#FROM scratch
|
||||
#
|
||||
## Migrate over users and groups
|
||||
#COPY --from=base /etc/passwd /etc/passwd
|
||||
#COPY --from=base /etc/group /etc/group
|
||||
#
|
||||
## Configuration
|
||||
#COPY --from=base /etc/btcd /etc/btcd
|
||||
#COPY --from=base /etc/lnd /etc/lnd
|
||||
#COPY --from=base /etc/indra /etc/indra
|
||||
#
|
||||
### Data
|
||||
#COPY --from=base --chown=btcd:btcd /var/btcd /var/btcd
|
||||
#COPY --from=base --chown=lnd:lnd /var/lnd /var/lnd
|
||||
#COPY --from=base --chown=indra:indra /var/indra /var/indra
|
||||
9
docker/scratch/build.sh
Executable file
9
docker/scratch/build.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker build -t indralabs/scratch-builder .
|
||||
|
||||
docker run --rm -it --volume=${PWD}/tmp:/output indralabs/scratch-builder cp /tmp/root-fs.tgz /output
|
||||
|
||||
docker image import tmp/root-fs.tgz indralabs/scratch
|
||||
|
||||
docker push indralabs/scratch:latest
|
||||
1
docker/scratch/tmp/.gitignore
vendored
Normal file
1
docker/scratch/tmp/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
root-fs.tgz
|
||||
0
docker/scratch/tmp/.gitkeep
Normal file
0
docker/scratch/tmp/.gitkeep
Normal file
@@ -1,5 +1,25 @@
|
||||
version: '3'
|
||||
services:
|
||||
btcd:
|
||||
sysctls:
|
||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||
image: indralabs/btcd:latest
|
||||
container_name: indra-btcd
|
||||
volumes:
|
||||
- btcd_config:/etc/btcd:ro
|
||||
- btcd_data:/var/btcd
|
||||
networks:
|
||||
indranet:
|
||||
ipv4_address: 172.16.238.254
|
||||
expose:
|
||||
- 8333
|
||||
- 8334
|
||||
command:
|
||||
- "--listen=0.0.0.0:8333"
|
||||
- "--rpclisten=0.0.0.0:8334"
|
||||
- "--rpcuser=simnet"
|
||||
- "--rpcpass=simnet"
|
||||
- "--simnet"
|
||||
seed0:
|
||||
sysctls:
|
||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||
@@ -17,6 +37,8 @@ services:
|
||||
environment:
|
||||
INDRA_SERVE_KEY: "66T7j5JnhsjDTqVvV8zEM2rTUobu66tocizfqArVEnP1"
|
||||
INDRA_SERVE_LISTEN: "/ip4/0.0.0.0/tcp/62134,/ip6/::/tcp/62134"
|
||||
command:
|
||||
- "serve"
|
||||
seed1:
|
||||
sysctls:
|
||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||
@@ -36,6 +58,8 @@ services:
|
||||
environment:
|
||||
INDRA_SERVE_KEY: "66T7j5JnhsjDTqVvV8zEM2rTUobu66tocizfqArVEnP2"
|
||||
INDRA_SERVE_LISTEN: "/ip4/0.0.0.0/tcp/62134,/ip6/::/tcp/62134"
|
||||
command:
|
||||
- "serve"
|
||||
seed2:
|
||||
sysctls:
|
||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||
@@ -56,6 +80,8 @@ services:
|
||||
environment:
|
||||
INDRA_SERVE_KEY: "66T7j5JnhsjDTqVvV8zEM2rTUobu66tocizfqArVEnP3"
|
||||
INDRA_SERVE_LISTEN: "/ip4/0.0.0.0/tcp/62134,/ip6/::/tcp/62134"
|
||||
command:
|
||||
- "serve"
|
||||
peer0:
|
||||
sysctls:
|
||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||
@@ -76,6 +102,8 @@ services:
|
||||
environment:
|
||||
#INDRA_SERVE_SEED: "/dns4/seed0/tcp/62134/p2p/16Uiu2HAm2LgowPNBM47dR6gSJmEeQaqCZ6u4WPhTCSWkxyNrfAxo"
|
||||
INDRA_SERVE_LISTEN: "/ip4/0.0.0.0/tcp/62134,/ip6/::/tcp/62134"
|
||||
command:
|
||||
- "serve"
|
||||
peer1:
|
||||
sysctls:
|
||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||
@@ -96,6 +124,8 @@ services:
|
||||
environment:
|
||||
#INDRA_SERVE_SEED: "/dns4/seed0/tcp/62134/p2p/16Uiu2HAm2LgowPNBM47dR6gSJmEeQaqCZ6u4WPhTCSWkxyNrfAxo"
|
||||
INDRA_SERVE_LISTEN: "/ip4/0.0.0.0/tcp/62134,/ip6/::/tcp/62134"
|
||||
command:
|
||||
- "serve"
|
||||
peer2:
|
||||
sysctls:
|
||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||
@@ -116,7 +146,11 @@ services:
|
||||
environment:
|
||||
#INDRA_SERVE_SEED: "/dns4/seed0/tcp/62134/p2p/16Uiu2HAm2LgowPNBM47dR6gSJmEeQaqCZ6u4WPhTCSWkxyNrfAxo"
|
||||
INDRA_SERVE_LISTEN: "/ip4/127.0.0.1/tcp/62134,/ip6/::1/tcp/62134"
|
||||
command:
|
||||
- "serve"
|
||||
volumes:
|
||||
btcd_config:
|
||||
btcd_data:
|
||||
seed0_gopath:
|
||||
seed1_gopath:
|
||||
seed2_gopath:
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
go mod tidy
|
||||
|
||||
IPFS_LOGGING=info go run ./cmd/indra/. -lcl serve
|
||||
IPFS_LOGGING=info go run ./cmd/indra/. $@
|
||||
@@ -26,19 +26,6 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
buildRepositoryName = "indralabs/indra"
|
||||
buildContextFilePath = "/tmp/indra-" + indra.SemVer + ".tar"
|
||||
buildOpts = types.ImageBuildOptions{
|
||||
Dockerfile: "docker/indra/Dockerfile",
|
||||
Tags: []string{
|
||||
buildRepositoryName + ":" + indra.SemVer,
|
||||
buildRepositoryName + ":" + "latest",
|
||||
},
|
||||
SuppressOutput: false,
|
||||
Remove: true,
|
||||
ForceRemove: true,
|
||||
PullParent: true,
|
||||
}
|
||||
isRelease = false
|
||||
isPushable = false
|
||||
)
|
||||
@@ -53,17 +40,21 @@ func SetPush() {
|
||||
|
||||
type Builder struct {
|
||||
*client.Client
|
||||
ctx context.Context
|
||||
ctx context.Context
|
||||
configs []BuildConfiguration
|
||||
}
|
||||
|
||||
func (cli *Builder) Build() (err error) {
|
||||
func (self *Builder) build(buildConfig BuildConfiguration) (err error) {
|
||||
|
||||
log.I.Ln("building", buildOpts.Tags[0], "from", buildOpts.Dockerfile)
|
||||
// We need the absolute path for build tags to be valid
|
||||
buildConfig.BuildOpts.Tags = buildConfig.FixTagPrefix()
|
||||
|
||||
log.I.Ln("building", buildConfig.BuildOpts.Tags[0], "from", buildConfig.BuildOpts.Dockerfile)
|
||||
|
||||
// If we're building a release, we should also tag stable.
|
||||
|
||||
if isRelease {
|
||||
buildOpts.Tags = append(buildOpts.Tags, buildRepositoryName+":"+"stable")
|
||||
buildConfig.BuildOpts.Tags = append(buildConfig.BuildOpts.Tags, "stable")
|
||||
}
|
||||
|
||||
// Generate a tar file for docker's release context. It will contain the root of the repository's path.
|
||||
@@ -83,7 +74,7 @@ func (cli *Builder) Build() (err error) {
|
||||
|
||||
var response types.ImageBuildResponse
|
||||
|
||||
if response, err = cli.ImageBuild(cli.ctx, tar, buildOpts); check(err) {
|
||||
if response, err = self.ImageBuild(self.ctx, tar, buildConfig.BuildOpts); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,7 +92,7 @@ func (cli *Builder) Build() (err error) {
|
||||
|
||||
log.I.Ln("pruning release container(s)...")
|
||||
|
||||
if _, err = cli.ImagesPrune(cli.ctx, filters.NewArgs()); check(err) {
|
||||
if _, err = self.ImagesPrune(self.ctx, filters.NewArgs()); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -111,7 +102,19 @@ func (cli *Builder) Build() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (cli *Builder) Push(opts types.ImagePushOptions) (err error) {
|
||||
func (self *Builder) Build() (err error) {
|
||||
|
||||
for _, buildConfig := range self.configs {
|
||||
|
||||
if err = self.build(buildConfig); check(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Builder) push(buildConfig BuildConfiguration) (err error) {
|
||||
|
||||
if !isPushable {
|
||||
return nil
|
||||
@@ -148,15 +151,15 @@ func (cli *Builder) Push(opts types.ImagePushOptions) (err error) {
|
||||
|
||||
authConfigBytes, _ := json.Marshal(auth)
|
||||
|
||||
opts.RegistryAuth = base64.URLEncoding.EncodeToString(authConfigBytes)
|
||||
buildConfig.PushOpts.RegistryAuth = base64.URLEncoding.EncodeToString(authConfigBytes)
|
||||
|
||||
// Pushes each tag to the docker repository.
|
||||
|
||||
for _, tag := range buildOpts.Tags {
|
||||
for _, tag := range buildConfig.FixTagPrefix() {
|
||||
|
||||
log.I.Ln("pushing", tag)
|
||||
|
||||
if pushResponse, err = cli.ImagePush(cli.ctx, tag, opts); check(err) {
|
||||
if pushResponse, err = self.ImagePush(self.ctx, tag, buildConfig.PushOpts); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -175,10 +178,23 @@ func (cli *Builder) Push(opts types.ImagePushOptions) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBuilder(ctx context.Context, cli *client.Client) (builder *Builder) {
|
||||
func (self *Builder) Push() (err error) {
|
||||
|
||||
for _, buildConfig := range self.configs {
|
||||
|
||||
if err = self.push(buildConfig); check(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBuilder(ctx context.Context, cli *client.Client, buildConfigs []BuildConfiguration) (builder *Builder) {
|
||||
|
||||
return &Builder{
|
||||
cli,
|
||||
ctx,
|
||||
buildConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
24
pkg/docker/config.go
Normal file
24
pkg/docker/config.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
type BuildConfiguration struct {
|
||||
Name string
|
||||
ContextFilePath string
|
||||
BuildOpts types.ImageBuildOptions
|
||||
PushOpts types.ImagePushOptions
|
||||
}
|
||||
|
||||
func (self *BuildConfiguration) FixTagPrefix() []string {
|
||||
|
||||
var fullTags = []string{}
|
||||
|
||||
for _, tag := range self.BuildOpts.Tags {
|
||||
|
||||
fullTags = append(fullTags, self.Name+":"+tag)
|
||||
}
|
||||
|
||||
return fullTags
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/indra-labs/indra"
|
||||
@@ -49,14 +50,20 @@ var interruptCallbackSources []string
|
||||
// responds to custom shutdown signals as required
|
||||
func Listener() {
|
||||
invokeCallbacks := func() {
|
||||
log.D.Ln(
|
||||
"running interrupt callbacks",
|
||||
len(interruptCallbacks),
|
||||
strings.Repeat(" ", 48),
|
||||
interruptCallbackSources,
|
||||
)
|
||||
// run handlers in LIFO order.
|
||||
for i := range interruptCallbacks {
|
||||
idx := len(interruptCallbacks) - 1 - i
|
||||
log.I.Ln("running callback", idx,
|
||||
log.D.Ln("running callback", idx,
|
||||
interruptCallbackSources[idx])
|
||||
interruptCallbacks[idx]()
|
||||
}
|
||||
log.I.Ln("interrupt handlers finished")
|
||||
log.D.Ln("interrupt handlers finished")
|
||||
close(HandlersDone)
|
||||
if Restart {
|
||||
var file string
|
||||
@@ -103,8 +110,7 @@ out:
|
||||
case sig := <-ch:
|
||||
// if !requested {
|
||||
// L.Printf("\r>>> received signal (%s)\n", sig)
|
||||
fmt.Print("\r")
|
||||
log.I.Ln("received interrupt signal", sig)
|
||||
log.I.Ln("received signal", sig)
|
||||
requested.Store(true)
|
||||
invokeCallbacks()
|
||||
// pprof.Lookup("goroutine").WriteTo(os.Stderr, 2)
|
||||
@@ -137,7 +143,7 @@ func AddHandler(handler func()) {
|
||||
// all other callbacks and exits if not already done.
|
||||
_, loc, line, _ := runtime.Caller(1)
|
||||
msg := fmt.Sprintf("%s:%d", loc, line)
|
||||
log.I.Ln("handler added by:", msg)
|
||||
log.D.Ln("handler added by:", msg)
|
||||
if ch == nil {
|
||||
ch = make(chan os.Signal)
|
||||
signal.Notify(ch, signals...)
|
||||
|
||||
96
pkg/p2p/introducer/bootstrap.go
Normal file
96
pkg/p2p/introducer/bootstrap.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package introducer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/indra-labs/indra"
|
||||
log2 "github.com/indra-labs/indra/pkg/log"
|
||||
dht "github.com/libp2p/go-libp2p-kad-dht"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/libp2p/go-libp2p/core/protocol"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
log = log2.GetLogger(indra.PathBase)
|
||||
check = log.E.Chk
|
||||
)
|
||||
|
||||
var (
|
||||
name = "[introducer.bootstrap]"
|
||||
protocolPrefix protocol.ID = "/indra"
|
||||
)
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
m sync.Mutex
|
||||
c context.Context
|
||||
h host.Host = nil
|
||||
|
||||
kadht *dht.IpfsDHT
|
||||
bootstrapPeers []peer.AddrInfo
|
||||
)
|
||||
|
||||
func Bootstrap(ctx context.Context, host host.Host, seeds []multiaddr.Multiaddr) (err error) {
|
||||
|
||||
log.I.Ln("starting [introducer.bootstrap]")
|
||||
|
||||
// Guarding against multiple instantiations
|
||||
if !m.TryLock() {
|
||||
return errors.New("[introducer.bootstrap] service is in use.")
|
||||
}
|
||||
|
||||
c = ctx
|
||||
h = host
|
||||
|
||||
log.I.Ln("using seeds:")
|
||||
|
||||
var bootstrapPeer *peer.AddrInfo
|
||||
|
||||
for _, seed := range seeds {
|
||||
|
||||
log.I.Ln("-", seed.String())
|
||||
|
||||
if bootstrapPeer, err = peer.AddrInfoFromP2pAddr(seed); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
// We can skip ourselves
|
||||
if bootstrapPeer.ID == host.ID() {
|
||||
continue
|
||||
}
|
||||
|
||||
bootstrapPeers = append(bootstrapPeers, *bootstrapPeer)
|
||||
}
|
||||
|
||||
var options = []dht.Option{
|
||||
dht.Mode(dht.ModeServer),
|
||||
dht.ProtocolPrefix(protocolPrefix),
|
||||
dht.BootstrapPeers(bootstrapPeers...),
|
||||
dht.DisableValues(),
|
||||
dht.DisableProviders(),
|
||||
//dht.Validator(),
|
||||
}
|
||||
|
||||
if kadht, err = dht.New(ctx, h, options...); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = kadht.Bootstrap(ctx); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
log.I.Ln("[introducer.bootstrap] is ready")
|
||||
|
||||
select {
|
||||
case <-c.Done():
|
||||
|
||||
log.I.Ln("shutting down [introducer.bootstrap]")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/indra-labs/indra"
|
||||
log2 "github.com/indra-labs/indra/pkg/log"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
)
|
||||
|
||||
var (
|
||||
log = log2.GetLogger(indra.PathBase)
|
||||
check = log.E.Chk
|
||||
)
|
||||
|
||||
var (
|
||||
hostStatusTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func SetTimeout(key string, timeout time.Duration) {
|
||||
hostStatusTimeout = timeout
|
||||
}
|
||||
|
||||
func HostStatus(ctx context.Context, host host.Host) {
|
||||
|
||||
for {
|
||||
|
||||
time.Sleep(hostStatusTimeout)
|
||||
|
||||
select {
|
||||
|
||||
case <-ctx.Done():
|
||||
|
||||
log.I.Ln("shutting down metrics.hoststatus")
|
||||
|
||||
return
|
||||
|
||||
default:
|
||||
|
||||
}
|
||||
|
||||
log.I.Ln("---- host status ----")
|
||||
log.I.Ln("-- peers:", len(host.Network().Peers()))
|
||||
log.I.Ln("-- connections:", len(host.Network().Conns()))
|
||||
log.I.Ln("---- ---- ------ ----")
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package seed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/indra-labs/indra"
|
||||
log2 "github.com/indra-labs/indra/pkg/log"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
var (
|
||||
log = log2.GetLogger(indra.PathBase)
|
||||
check = log.E.Chk
|
||||
)
|
||||
|
||||
var (
|
||||
defaultConnectionAttempts uint = 3
|
||||
defaultConnectionAttemptInterval = 10 * time.Second
|
||||
|
||||
defaultConnectionsMax uint = 32
|
||||
defaultConnectionsToSatisfy uint = 5
|
||||
)
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
m sync.Mutex
|
||||
c context.Context
|
||||
h host.Host = nil
|
||||
|
||||
failedChan = make(chan error)
|
||||
)
|
||||
|
||||
func connection_attempt(peer *peer.AddrInfo, attempts_left uint) {
|
||||
|
||||
if attempts_left == 0 {
|
||||
|
||||
wg.Done()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.Connect(c, *peer); err != nil {
|
||||
|
||||
log.I.Ln("connection attempt failed:", peer.ID)
|
||||
|
||||
select {
|
||||
case <-time.After(defaultConnectionAttemptInterval):
|
||||
connection_attempt(peer, attempts_left-1)
|
||||
case <-c.Done():
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.I.Ln("seed connection established:", peer.String())
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
func Bootstrap(ctx context.Context, host host.Host, seeds []multiaddr.Multiaddr) (err error) {
|
||||
|
||||
log.I.Ln("[seed.bootstrap] starting")
|
||||
|
||||
// Guarding against multiple instantiations
|
||||
if !m.TryLock() {
|
||||
return errors.New("bootstrapping service is in use.")
|
||||
}
|
||||
|
||||
c = ctx
|
||||
h = host
|
||||
|
||||
log.I.Ln("attempting peering with seeds...")
|
||||
|
||||
var peerInfo *peer.AddrInfo
|
||||
|
||||
for _, peerAddr := range seeds {
|
||||
|
||||
log.I.Ln("-", peerAddr.String())
|
||||
|
||||
if peerInfo, err = peer.AddrInfoFromP2pAddr(peerAddr); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
// We can skip ourselves
|
||||
if peerInfo.ID == host.ID() {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
log.I.Ln("attempting connection", peerInfo.ID)
|
||||
|
||||
go connection_attempt(peerInfo, defaultConnectionAttempts)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
log.I.Ln("finished seed bootstrapping")
|
||||
|
||||
return
|
||||
}
|
||||
60
pkg/server/metrics/host.go
Normal file
60
pkg/server/metrics/host.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/indra-labs/indra"
|
||||
log2 "github.com/indra-labs/indra/pkg/log"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
)
|
||||
|
||||
var (
|
||||
log = log2.GetLogger(indra.PathBase)
|
||||
check = log.E.Chk
|
||||
)
|
||||
|
||||
var (
|
||||
hostStatusInterval = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
mutex sync.Mutex
|
||||
)
|
||||
|
||||
func SetInterval(timeout time.Duration) {
|
||||
hostStatusInterval = timeout
|
||||
}
|
||||
|
||||
func HostStatus(ctx context.Context, host host.Host) {
|
||||
|
||||
log.I.Ln("starting [metrics.hoststatus]")
|
||||
|
||||
// Guarding against multiple instantiations
|
||||
if !mutex.TryLock() {
|
||||
return
|
||||
}
|
||||
|
||||
log.I.Ln("[metrics.hoststatus] is ready")
|
||||
|
||||
for {
|
||||
|
||||
select {
|
||||
|
||||
case <-time.After(hostStatusInterval):
|
||||
|
||||
log.I.Ln()
|
||||
log.I.Ln("---- host status ----")
|
||||
log.I.Ln("-- peers:", len(host.Network().Peers()))
|
||||
log.I.Ln("-- connections:", len(host.Network().Conns()))
|
||||
log.I.Ln("---- ---- ------ ----")
|
||||
|
||||
case <-ctx.Done():
|
||||
|
||||
log.I.Ln("shutting down [metrics.hoststatus]")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"github.com/indra-labs/indra/pkg/cfg"
|
||||
"github.com/indra-labs/indra/pkg/interrupt"
|
||||
log2 "github.com/indra-labs/indra/pkg/log"
|
||||
"github.com/indra-labs/indra/pkg/p2p/metrics"
|
||||
"github.com/indra-labs/indra/pkg/p2p/seed"
|
||||
"github.com/indra-labs/indra/pkg/p2p/introducer"
|
||||
"github.com/indra-labs/indra/pkg/server/metrics"
|
||||
"github.com/libp2p/go-libp2p"
|
||||
dht "github.com/libp2p/go-libp2p-kad-dht"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,7 +31,6 @@ type Server struct {
|
||||
params *cfg.Params
|
||||
|
||||
host host.Host
|
||||
dht *dht.IpfsDHT
|
||||
}
|
||||
|
||||
func (srv *Server) Restart() (err error) {
|
||||
@@ -43,25 +42,21 @@ func (srv *Server) Restart() (err error) {
|
||||
|
||||
func (srv *Server) Shutdown() (err error) {
|
||||
|
||||
//log.I.Ln("shutting down the dht...")
|
||||
//
|
||||
//if srv.dht.Close(); check(err) {
|
||||
// return
|
||||
//}
|
||||
|
||||
log.I.Ln("shutting down the p2p host...")
|
||||
log.I.Ln("shutting down [p2p.host]")
|
||||
|
||||
if srv.host.Close(); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
log.I.Ln("shutdown complete.")
|
||||
log.I.Ln("shutdown complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *Server) Serve() (err error) {
|
||||
|
||||
log.I.Ln("starting the server")
|
||||
|
||||
// Here we create a context with cancel and add it to the interrupt handler
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
@@ -70,54 +65,23 @@ func (srv *Server) Serve() (err error) {
|
||||
|
||||
interrupt.AddHandler(cancel)
|
||||
|
||||
// Introduce your node to the network
|
||||
go introducer.Bootstrap(ctx, srv.host, srv.config.SeedAddresses)
|
||||
|
||||
// Get some basic metrics for the host
|
||||
//metrics.Init()
|
||||
//metrics.Set('indra.host.status.reporting.interval', 30 * time.Second)
|
||||
//metrics.Enable('indra.host.status')
|
||||
metrics.SetInterval(30 * time.Second)
|
||||
|
||||
go metrics.HostStatus(ctx, srv.host)
|
||||
|
||||
// Run the bootstrapping service on the peer.
|
||||
if err = seed.Bootstrap(ctx, srv.host, srv.config.SeedAddresses); check(err) {
|
||||
return
|
||||
}
|
||||
|
||||
//log.I.Ln("bootstrapping the DHT")
|
||||
|
||||
// Bootstrap the DHT. In the default configuration, this spawns a Background
|
||||
// thread that will refresh the peer table every five minutes.
|
||||
//if err = srv.dht.Bootstrap(srv.Context); check(err) {
|
||||
// return err
|
||||
//}
|
||||
|
||||
log.I.Ln("successfully connected")
|
||||
|
||||
//var pingService *ping.PingService
|
||||
//
|
||||
//if pingService = ping.NewPingService(srv.host); check(err) {
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//go func() {
|
||||
//
|
||||
// log.I.Ln("attempting ping")
|
||||
//
|
||||
// for {
|
||||
//
|
||||
// for _, peer := range srv.host.Peerstore().Peers() {
|
||||
//
|
||||
// select {
|
||||
// case result := <- pingService.Ping(context.Background(), peer):
|
||||
// log.I.Ln("ping", peer.String(), "-", result.RTT)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// time.Sleep(10 * time.Second)
|
||||
// }
|
||||
//
|
||||
//}()
|
||||
|
||||
select {
|
||||
|
||||
case <-ctx.Done():
|
||||
|
||||
log.I.Ln("shutting down server")
|
||||
|
||||
srv.Shutdown()
|
||||
}
|
||||
|
||||
@@ -151,13 +115,5 @@ func New(params *cfg.Params, config *Config) (srv *Server, err error) {
|
||||
|
||||
config.SeedAddresses = append(config.SeedAddresses, seedAddresses...)
|
||||
|
||||
// Start a DHT, for use in peer discovery. We can't just make a new DHT
|
||||
// client because we want each peer to maintain its own local copy of the
|
||||
// DHT, so that the bootstrapping node of the DHT can go down without
|
||||
// inhibiting future peer discovery.
|
||||
//if s.dht, err = dht.New(s.Context, s.host); check(err) {
|
||||
// return nil, err
|
||||
//}
|
||||
|
||||
return &s, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user