Initial commit.

This commit is contained in:
Stanislav Nikitin 2024-10-12 13:04:09 +05:00
commit 57937a5845
Signed by: pztrn
GPG Key ID: 1E944A0F0568B550
30 changed files with 816 additions and 0 deletions

59
.drone.yml Normal file
View File

@ -0,0 +1,59 @@
---
kind: pipeline
type: docker
name: "Tests and linting"
trigger:
event:
exclude:
- pull_request
steps:
- name: Linting
image: code.pztrn.name/containers/go-toolbox:v4
pull: if-not-exists
commands:
- task lint
- name: Tests
image: code.pztrn.name/containers/go-toolbox:v4
pull: if-not-exists
commands:
- task test
---
kind: pipeline
type: docker
name: "Build Release"
depends_on:
- "Tests and linting"
trigger:
event:
- tag
steps:
- name: Build Docker image for Featurer
image: code.pztrn.name/containers/mirror/plugins/buildx:1.1.11
settings:
registry: code.pztrn.name
username: drone
password:
from_secret: drone_secret
repo: code.pztrn.name/pztrn/notificator
dockerfile: server/Dockerfile.featurer
auto_tag: true
force_tag: true
- name: Build Docker image for CMS
image: code.pztrn.name/containers/mirror/plugins/buildx:1.1.11
settings:
registry: code.pztrn.name
username: drone
password:
from_secret: drone_secret
repo: code.pztrn.name/pztrn/notificator
dockerfile: server/Dockerfile.CMS
auto_tag: true
force_tag: true

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*DS_Store*
_build

71
.golangci.yml Normal file
View File

@ -0,0 +1,71 @@
---
run:
timeout: 5m
linters:
enable-all: true
disable:
# deprecated
- varnamelen
- testpackage
- containedctx # very strange linter
- gomnd
- execinquery
# meaningfully disabled
- gochecknoglobals
- exhaustruct
- depguard # wtf is this linter
- tparallel # duplicates paralleltest
- unused # not very clever lint for generics
- ireturn # not very clever lint for generics
- interfacebloat # love big interfaces :)
- gci # whines about imports too much.
linters-settings:
cyclop:
skip-tests: true
max-complexity: 20
package-average: 10
forbidigo:
forbid:
- '^(fmt\.Print(|f|ln)|print|println)$'
- '^time\.Now\(\)($|\.F|\.A|\.B|\.L|\.UTC\(\)\.I|,|\))(# Calls of time\.Now() without \.UTC() is prohibited\.)?'
funlen:
ignore-comments: true
lines: 200
statements: 60
gocyclo:
min-complexity: 20
gofumpt:
extra-rules: true
govet:
enable-all: true
lll:
line-length: 120
nestif:
min-complexity: 20
tagliatelle:
case:
use-field-name: true
rules:
json: snake
yaml: camel
issues:
exclude-use-default: false
exclude:
- ST1000 # package comment
- package-comments
exclude-rules:
- path: .+_test\.go
linters:
- gosec
- linters:
- godox
text: "TODO"
# Default error checking pattern "if err := ... ;err != nil {}" multiple times makes go vet to whine.
- linters:
- govet
text: 'declaration of "err" shadows'
max-issues-per-linter: 0
max-same-issues: 0

4
.markdownlint.json Normal file
View File

@ -0,0 +1,4 @@
{
"line-length": false,
"first-line-h1": false
}

9
.yamllint Normal file
View File

@ -0,0 +1,9 @@
---
extends: default
rules:
line-length: disable
braces:
min-spaces-inside: 1
max-spaces-inside: 1

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Featurer
Features toggling server.

58
Taskfile.yml Normal file
View File

@ -0,0 +1,58 @@
---
version: "3"
vars:
BRANCH:
sh: ./scripts/version-data-generator.sh branch
BUILD_DATE:
sh: date '+%Y-%m-%d %H:%M:%S %z'
BUILD_NUMBER:
sh: ./scripts/version-data-generator.sh build_number
COMMIT:
sh: ./scripts/version-data-generator.sh commit
VERSION:
sh: ./scripts/version-data-generator.sh version
BASIC_LDFLAGS: "-X 'go.dev.pztrn.name/featurer/server/internal/application.Version={{ .VERSION }}' -X 'go.dev.pztrn.name/featurer/server/internal/application.Branch={{ .BRANCH }}' -X 'go.dev.pztrn.name/featurer/server/internal/application.Commit={{ .COMMIT }}' -X 'go.dev.pztrn.name/featurer/server/internal/application.Build={{ .BUILD_NUMBER }}' -X 'go.dev.pztrn.name/featurer/server/internal/application.BuildDate={{ .BUILD_DATE }}'"
LDFLAGS: "{{ .BASIC_LDFLAGS }}"
includes:
server: ./server
tasks:
cleanup:
desc: "Cleanup _build directory."
cmds:
- task: server:cmd:cms:cleanup
- task: server:cmd:featurer:cleanup
default:
desc: "Default help."
cmds:
- echo "Run \"task -l\" for list of available tasks."
lint:
desc: "Runs linters."
cmds:
- golangci-lint run ./...
- go vet ./...
pre-commit:
desc: "Runs some tasks before comitting."
cmds:
- task: lint
- task: test
test:
desc: "Run tests."
cmds:
- go test ./...
vars:
desc: "Show vars which will be used for building or running."
silent: true
cmds:
- echo "BRANCH={{ .BRANCH }}"
- echo "BUILD_DATE={{ .BUILD_DATE }}"
- echo "BUILD_NUMBER={{ .BUILD_NUMBER }}"
- echo "COMMIT={{ .COMMIT }}"
- echo "VERSION={{ .VERSION }}"

4
client/go/client.go Normal file
View File

@ -0,0 +1,4 @@
package client
// Client is a Featurer client for Golang application.
type Client struct{}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module go.dev.pztrn.name/featurer
go 1.23

View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
VERSION_PREFIX="v"
# shellcheck disable=SC1083
BUILD_NUMBER=$(git rev-list --count --all)
BRANCH_NAME=$(git branch --show-current)
COMMIT=$(git rev-parse HEAD)
VERSION=$(git tag | sort | grep "${VERSION_PREFIX}" | tail -n 1 | cut -c$((${#VERSION_PREFIX} + 1))-)
if [ "${VERSION}" == "" ]; then
VERSION="0.1.0-dev"
fi
function show_help() {
echo "$0 [param]"
echo ""
echo "Parameters:"
echo ""
echo " branch Show branch."
echo " build_number Show build number (commits count)."
echo " commit Show commit hash."
echo " version Show version (real or '0.1.0-dev' if no version tags present)."
}
case $1 in
branch)
echo "${BRANCH_NAME}"
;;
build_number)
echo "${BUILD_NUMBER}"
;;
commit)
echo "${COMMIT}"
;;
version)
echo "${VERSION}"
;;
*)
show_help
;;
esac

18
server/Dockerfile.CMS Normal file
View File

@ -0,0 +1,18 @@
FROM code.pztrn.name/containers/go-toolbox:v4 AS build
WORKDIR /featurer
COPY . /featurer
RUN --mount=type=cache,target="$GOCACHE" task server:cmd:cms:build
FROM debian:12.6-slim
RUN apt-get update && \
apt-get install -y ca-certificates iputils-ping && \
rm -rf /var/lib/apt/* /var/cache/apt/*
COPY --from=build /featurer/_build/featurer-cms /featurer-cms
COPY --from=build /usr/local/bin/dlv /dlv
COPY server/entrypoint-cms.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -0,0 +1,18 @@
FROM code.pztrn.name/containers/go-toolbox:v4 AS build
WORKDIR /featurer
COPY . /featurer
RUN --mount=type=cache,target="$GOCACHE" task server:cmd:featurer:build
FROM debian:12.6-slim
RUN apt-get update && \
apt-get install -y ca-certificates iputils-ping && \
rm -rf /var/lib/apt/* /var/cache/apt/*
COPY --from=build /featurer/_build/featurer /featurer
COPY --from=build /usr/local/bin/dlv /dlv
COPY server/entrypoint-featurer.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

6
server/Taskfile.yml Normal file
View File

@ -0,0 +1,6 @@
---
version: "3"
includes:
cmd: ./cmd
localdev: ./localdev

6
server/cmd/Taskfile.yml Normal file
View File

@ -0,0 +1,6 @@
---
version: "3"
includes:
cms: ./cms
featurer: ./featurer

View File

@ -0,0 +1,21 @@
---
version: "3"
tasks:
build:
desc: "Builds Featurer's CMS binary."
cmds:
- task: cleanup
- go build -ldflags="{{ .LDFLAGS }}" -tags netgo -o _build/featurer-cms{{exeExt}} ./server/cmd/cms/main.go
sources:
- ./**/*.go
- ./Taskfile.yml
- ./go.mod
generates:
- ./_build/featurer-cms{{exeExt}}
method: none
cleanup:
desc: "Deletes Featurer's CMS binary from local build cache."
cmds:
- rm -f _build/featurer-cms{{exeExt}}

42
server/cmd/cms/main.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"log/slog"
"go.dev.pztrn.name/featurer/server/internal/application"
"go.dev.pztrn.name/featurer/server/internal/services/core/datastore"
)
func main() {
_ = slog.SetLogLoggerLevel(slog.LevelDebug)
app := application.New()
slog.Info(
"Launching Featurer's CMS...",
"version", application.Version,
"build", application.Build,
"branch", application.Branch,
"commit", application.Commit,
"build date", application.BuildDate,
)
// Initializing core services first.
checkError(datastore.Initialize(app))
// Then - features services.
// Start application.
checkError(app.Start())
<-app.GetShutdownDoneChannel()
slog.Info("Featurer's CMS.")
}
func checkError(err error) {
if err == nil {
return
}
panic(err)
}

View File

@ -0,0 +1,21 @@
---
version: "3"
tasks:
build:
desc: "Builds Featurer main binary."
cmds:
- task: cleanup
- go build -ldflags="{{ .LDFLAGS }}" -tags netgo -o _build/featurer{{exeExt}} ./server/cmd/featurer/main.go
sources:
- ./**/*.go
- ./Taskfile.yml
- ./go.mod
generates:
- ./_build/featurer{{exeExt}}
method: none
cleanup:
desc: "Deletes Featurer main binary from local build cache."
cmds:
- rm -f _build/featurer{{exeExt}}

View File

@ -0,0 +1,42 @@
package main
import (
"log/slog"
"go.dev.pztrn.name/featurer/server/internal/application"
"go.dev.pztrn.name/featurer/server/internal/services/core/datastore"
)
func main() {
_ = slog.SetLogLoggerLevel(slog.LevelDebug)
app := application.New()
slog.Info(
"Launching Featurer server...",
"version", application.Version,
"build", application.Build,
"branch", application.Branch,
"commit", application.Commit,
"build date", application.BuildDate,
)
// Initializing core services first.
checkError(datastore.Initialize(app))
// Then - features services.
// Start application.
checkError(app.Start())
<-app.GetShutdownDoneChannel()
slog.Info("Featurer stopped.")
}
func checkError(err error) {
if err == nil {
return
}
panic(err)
}

8
server/entrypoint-cms.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
echo "* Starting Featurer's CMS..."
if [ -n "${FEATURER_DEBUG}" ]; then
exec /dlv --listen=:4000 --headless=true --log=true --accept-multiclient --api-version=2 exec /featurer-cms --continue
else
exec /featurer-cms
fi

8
server/entrypoint-featurer.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
echo "* Starting Featurer..."
if [ -n "${FEATURER_DEBUG}" ]; then
exec /dlv --listen=:4000 --headless=true --log=true --accept-multiclient --api-version=2 exec /featurer --continue
else
exec /featurer
fi

View File

@ -0,0 +1,114 @@
package application
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"sync"
"syscall"
)
var (
// ErrApplication indicates that error appeared somewhere in application's lifecycle controller.
ErrApplication = errors.New("application")
// Version is application's version.
Version string
// Branch is a branch name from which application was built.
Branch string
// Commit is a commit hash from which application was built.
Commit string
// Build is a build number.
Build string
// BuildDate is a date on which application was built.
BuildDate string
)
// Application is an application's lifecycle controlling structure.
type Application struct {
ctx context.Context
shutdownDone chan struct{}
cancelFunc context.CancelFunc
services map[string]Service
dataPath string
servicesMutex sync.RWMutex
}
// New creates new application's lifecycle controlling structure.
func New() *Application {
appl := &Application{}
appl.initialize()
return appl
}
// GetContext returns application's global context.
func (a *Application) GetContext() context.Context {
return a.ctx
}
// GetShutdownDoneChannel returns channel on which lifecycle controller will tell about shutdown completion.
// Should be used only in main()!
func (a *Application) GetShutdownDoneChannel() chan struct{} {
slog.Debug("Returning shutdown completion channel.")
return a.shutdownDone
}
func (a *Application) initialize() {
a.ctx, a.cancelFunc = context.WithCancel(context.Background())
a.ctx, a.cancelFunc = signal.NotifyContext(a.ctx, os.Interrupt, syscall.SIGTERM)
a.services = make(map[string]Service)
a.shutdownDone = make(chan struct{}, 1)
}
// Shutdown stops application and all registered services.
func (a *Application) Shutdown() error {
a.cancelFunc()
a.servicesMutex.RLock()
defer a.servicesMutex.RUnlock()
for _, service := range a.services {
if err := service.Shutdown(); err != nil {
slog.Error("Error appeared when trying to shut down service", "service", service.GetName(), "error", err.Error())
}
}
a.shutdownDone <- struct{}{}
return nil
}
// Start starts application and all registered services.
func (a *Application) Start() error {
go a.signalsListener()
a.servicesMutex.RLock()
defer a.servicesMutex.RUnlock()
// First pass - connecting dependencies.
for _, service := range a.services {
if err := service.ConnectDependencies(); err != nil {
return fmt.Errorf("%w: connecting dependencies for service '%s': %w", ErrApplication, service.GetName(), err)
}
}
// Second pass - launching startup tasks.
for name, service := range a.services {
slog.Debug("Launching startup tasks for service", "service", name)
if err := service.LaunchStartupTasks(); err != nil {
return fmt.Errorf("%w: launching startup tasks for '%s': %w", ErrApplication, service.GetName(), err)
}
}
slog.Debug("Application started.")
return nil
}

View File

@ -0,0 +1,54 @@
package application
import (
"errors"
"fmt"
)
var (
// ErrServiceAlreadyRegistered appears when trying to register a service with already taken name.
ErrServiceAlreadyRegistered = errors.New("service already registered")
// ErrServiceNotRegistered appears when trying to get a registered service with unknown name.
ErrServiceNotRegistered = errors.New("unknown service")
)
// Service это интерфейс, которому должны соответствовать все сервисы, используемые в приложении.
type Service interface {
ConnectDependencies() error
GetName() string
Initialize() error
LaunchStartupTasks() error
Shutdown() error
}
// GetService возвращает сервис по имени, или же ошибку, если сервис не был зарегистрирован ранее.
func (a *Application) GetService(name string) (Service, error) {
a.servicesMutex.RLock()
service, found := a.services[name]
a.servicesMutex.RUnlock()
if !found {
return nil, fmt.Errorf("%w: get service '%s': %w", ErrApplication, name, ErrServiceNotRegistered)
}
return service, nil
}
// RegisterService регистрирует сервис, или возвращает ошибку, если сервис с таким именем уже был
// зарегистрирован ранее.
func (a *Application) RegisterService(service Service) error {
_, found := a.services[service.GetName()]
if found {
return fmt.Errorf("%w: register service '%s': %w", ErrApplication, service.GetName(), ErrServiceAlreadyRegistered)
}
if err := service.Initialize(); err != nil {
return fmt.Errorf("%w: initializing service '%s': %w", ErrApplication, service.GetName(), err)
}
a.servicesMutex.Lock()
a.services[service.GetName()] = service
a.servicesMutex.Unlock()
return nil
}

View File

@ -0,0 +1,24 @@
package application
import (
"log/slog"
"os"
"os/signal"
"syscall"
)
func (a *Application) signalsListener() {
slog.Info("Starting listening for SIGTERM signal...")
listener := make(chan os.Signal, 1)
signal.Notify(listener, syscall.SIGTERM, os.Interrupt)
<-listener
slog.Info("Got SIGTERM, stopping Featurer...")
if err := a.Shutdown(); err != nil {
slog.Error("Something went wrong when trying to shutdown application", "error", err.Error())
slog.Error("!!! APPLICATION CANNOT BE STOPPED NORMALLY, KILL IT MANUALLY !!!")
}
}

View File

@ -0,0 +1,16 @@
package core
import "errors"
// ServiceNameDatastore is a service name for data storage implementation.
const ServiceNameDatastore = "datastore"
var (
// ErrDatastore indicates that error appeared somewhere in data storage service.
ErrDatastore = errors.New("datastore")
// ErrDatastoreServiceIsInvalid appears when data storage implementation isn't conforming to interface.
ErrDatastoreServiceIsInvalid = errors.New("invalid datastore implementation")
)
// DataStore is an interface for data storage implementations.
type DataStore interface{}

View File

@ -0,0 +1,48 @@
package datastore
import (
"fmt"
"log/slog"
"go.dev.pztrn.name/featurer/server/internal/application"
"go.dev.pztrn.name/featurer/server/internal/services/core"
)
type datastore struct {
app *application.Application
}
// Initialize initializes service.
func Initialize(app *application.Application) error {
plan := &datastore{
app: app,
}
if err := app.RegisterService(plan); err != nil {
return fmt.Errorf("%w: %w", core.ErrDatastore, err)
}
return nil
}
func (p *datastore) ConnectDependencies() error {
return nil
}
func (p *datastore) GetName() string {
return core.ServiceNameDatastore
}
func (p *datastore) Initialize() error {
slog.Info("Initializing data storage...")
return nil
}
func (p *datastore) LaunchStartupTasks() error {
return nil
}
func (p *datastore) Shutdown() error {
return nil
}

View File

@ -0,0 +1,19 @@
---
version: 3
includes:
common: ./common
featurer: ./featurer
tasks:
down:
desc: "Removes development environment."
cmds:
- task: featurer:down
- task: common:network-down
up:
desc: "Creates development environment."
cmds:
- task: common:network-up
- task: featurer:up

View File

@ -0,0 +1,15 @@
---
version: "3"
tasks:
network-down:
desc: "Deletes Docker network for development."
dir: "./server/localdev/common"
cmds:
- docker compose -p featurer-network -f network.yaml down
network-up:
desc: "Creates Docker network for development"
dir: "./server/localdev/common"
cmds:
- docker compose -p featurer-network -f network.yaml up -d

View File

@ -0,0 +1,18 @@
---
services:
dummy:
image: busybox
container_name: featurer-dummy
hostname: dummy
networks:
featurer:
ipv4_address: 248.248.0.254
networks:
featurer:
driver: bridge
name: "featurer"
ipam:
config:
- subnet: 248.248.0.0/24
gateway: 248.248.0.1

View File

@ -0,0 +1,47 @@
---
version: "3"
tasks:
build:
desc: "Builds Featurer's container."
dir: "./server/localdev/featurer"
cmds:
- docker compose -p featurer -f docker-compose.yaml build
down:
desc: "Deletes all Featurer's data (down)."
dir: "./server/localdev/featurer"
cmds:
- docker compose -p featurer -f docker-compose.yaml down --volumes
logs:
desc: "Show Featurer logs."
dir: "./server/localdev/featurer"
cmds:
- docker compose -p featurer -f docker-compose.yaml logs -f
restart:
desc: "Restart Featurer."
dir: "./server/localdev/featurer"
cmds:
- docker compose -p featurer -f docker-compose.yaml restart
start:
desc: "Start Featurer."
dir: "./server/localdev/featurer"
cmds:
- docker compose -p featurer -f docker-compose.yaml start
stop:
desc: "Stop Featurer without deleting it's data."
dir: "./server/localdev/featurer"
cmds:
- docker compose -p featurer -f docker-compose.yaml stop
up:
desc: "Start Featurer (up -d)."
dir: "./server/localdev/featurer"
cmds:
- task: :common:network-up
- task: build
- docker compose -p featurer -f docker-compose.yaml up -d

View File

@ -0,0 +1,16 @@
---
services:
featurer:
container_name: "featurer"
build:
context: ../../../
dockerfile: server/Dockerfile.featurer
networks:
featurer:
ipv4_address: 248.248.0.2
cap_add:
- SYS_PTRACE
networks:
featurer:
external: true