From 6ba5e3ec5b917cffa2815f06850ca20e353da469 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Tue, 14 Oct 2025 22:11:03 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=BD=D0=B0=20justfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 8 --- Dockerfile | 10 +++- Makefile | 69 ------------------------ api/userservice/user.pb.go | 4 +- api/userservice/user_grpc.pb.go | 2 +- cmd/main.go | 14 ++--- configs/dev.yaml | 3 ++ configs/prod.yaml | 3 ++ internal/infisical/infisical.go | 78 --------------------------- internal/userservice/config/config.go | 62 +++++++-------------- justfile | 28 ++++++++++ tools/common.just | 37 +++++++++++++ tools/load_envs.sh | 73 +++++++++++++++++++++++++ 13 files changed, 178 insertions(+), 213 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Makefile create mode 100644 configs/dev.yaml create mode 100644 configs/prod.yaml delete mode 100644 internal/infisical/infisical.go create mode 100644 justfile create mode 100644 tools/common.just create mode 100644 tools/load_envs.sh diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 015f29b..0000000 --- a/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -.git -.gitignore -README.md -Dockerfile -.dockerignore -**/*.test -**/*.swp -**/*.swo \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 59ff2c8..110e375 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,10 +12,16 @@ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main main.go FROM alpine:latest -RUN apk --no-cache add ca-certificates +RUN apk --no-cache add ca-certificates bash curl jq yq WORKDIR /root/ COPY --from=builder /app/cmd/main . -CMD ["./main"] \ No newline at end of file +COPY configs ./configs +COPY tools ./tools +RUN chmod +x ./tools/load_envs.sh + +ENV ENV=prod + +CMD ["./tools/load_envs.sh", "./main"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 36c4535..0000000 --- a/Makefile +++ /dev/null @@ -1,69 +0,0 @@ -PROTO_TAG ?= v0.0.11 -PROTO_NAME := user.proto -PROTO_PATH := github.com/esclient/protos -PROTO_MODULE := $(shell go list -m) - -TMP_DIR := .proto -OUT_DIR := api/userservice - -ENV_FILE := .env -DOCKER_PORT := 50125 - -.PHONY: clean fetch-proto gen-stubs update run stop rebuild logs - -ifeq ($(OS),Windows_NT) -MKDIR = powershell -Command "New-Item -ItemType Directory -Force -Path" -RM = powershell -NoProfile -Command "Remove-Item -Path '$(TMP_DIR)' -Recurse -Force" -DOWN = powershell -Command "Invoke-WebRequest -Uri" -DOWN_OUT = -OutFile -else -MKDIR = mkdir -p -RM = rm -rf $(TMP_DIR) -DOWN = wget -DOWN_OUT = -O -endif - - -clean: - $(RM) - -fetch-proto: - $(MKDIR) "$(TMP_DIR)" - $(DOWN) "https://raw.githubusercontent.com/esclient/protos/$(PROTO_TAG)/$(PROTO_NAME)" $(DOWN_OUT) "$(TMP_DIR)/$(PROTO_NAME)" - -gen-stubs: fetch-proto - $(MKDIR) "$(OUT_DIR)" - protoc \ - --proto_path="$(TMP_DIR)" \ - --go_out="$(OUT_DIR)" \ - --go_opt=module=$(PROTO_MODULE) \ - --go-grpc_out="$(OUT_DIR)" \ - --go-grpc_opt=module=$(PROTO_MODULE) \ - "$(TMP_DIR)/$(PROTO_NAME)" - -update: gen-stubs clean - - -# --- Docker commands --- -run: build - docker run -d \ - --name user-service \ - --env-file $(ENV_FILE) \ - -p ${DOCKER_PORT}:${DOCKER_PORT} \ - --restart unless-stopped \ - user-service - docker logs -f user-service - -build: - docker build -t user-service . - -stop: - -docker stop user-service - -docker rm user-service - -rebuild: stop - docker build -t user-service . - $(MAKE) run - -logs: - docker logs -f user-service diff --git a/api/userservice/user.pb.go b/api/userservice/user.pb.go index 13f6edb..a3fc2d0 100644 --- a/api/userservice/user.pb.go +++ b/api/userservice/user.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 -// protoc v6.31.1 +// protoc-gen-go v1.36.10 +// protoc v6.32.1 // source: user.proto package user_service diff --git a/api/userservice/user_grpc.pb.go b/api/userservice/user_grpc.pb.go index ff238d9..6664e1a 100644 --- a/api/userservice/user_grpc.pb.go +++ b/api/userservice/user_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v6.31.1 +// - protoc v6.32.1 // source: user.proto package user_service diff --git a/cmd/main.go b/cmd/main.go index b1fd4b8..f8b9290 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -15,30 +15,24 @@ import ( "github.com/esclient/user-service/internal/userservice/handler" repo "github.com/esclient/user-service/internal/userservice/repository" "github.com/esclient/user-service/internal/userservice/service" - - infisical "github.com/esclient/user-service/internal/infisical" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - cfg := config.LoadConfig() - - infisicalClient := infisical.NewClient(infisical.InfisicalURL, cfg.InfisicalSecretKey) - - secretDBURL, err := infisicalClient.GetSecret(ctx, cfg.InfisicalProjectId, cfg.InfisicalEnv, infisical.SecretPathUserService, "DB_URL") + cfg, err := config.LoadConfig() if err != nil { log.Fatal(err) } - databaseConn, err := repo.NewDatabaseConnection(ctx, secretDBURL.Value) + databaseConn, err := repo.NewDatabaseConnection(ctx, cfg.DatabaseURL) if err != nil { log.Fatal(err) } repository := repo.NewPostgresUserRepository(databaseConn) - + userService := service.NewUserService(repository) userHandler := handler.NewUserHandler(userService) @@ -58,4 +52,4 @@ func main() { if err := grpcServer.Serve(listener); err != nil { log.Fatalf("failed to serve: %v", err) } -} \ No newline at end of file +} diff --git a/configs/dev.yaml b/configs/dev.yaml new file mode 100644 index 0000000..0d5e04f --- /dev/null +++ b/configs/dev.yaml @@ -0,0 +1,3 @@ +DATABASE_URL: "postgresql://esclient_devuser:KAFEzSGK45!8M43L@pg4.sweb.ru:5433/esclient_devuser" +HOST: "0.0.0.0" +PORT: "7001" diff --git a/configs/prod.yaml b/configs/prod.yaml new file mode 100644 index 0000000..e0317b3 --- /dev/null +++ b/configs/prod.yaml @@ -0,0 +1,3 @@ +DATABASE_URL: "{vault:user-service/prod#database_url}" +HOST: "{vault:user-service/prod#host}" +PORT: "{vault:user-service/prod#port}" diff --git a/internal/infisical/infisical.go b/internal/infisical/infisical.go deleted file mode 100644 index afa5886..0000000 --- a/internal/infisical/infisical.go +++ /dev/null @@ -1,78 +0,0 @@ -package infisical - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" -) - -const ( - InfisicalURL = "https://us.infisical.com" - SecretPathUserService = "/user-service" -) - -type RetrievedSecret struct { - Key string `json:"secretKey"` - Value string `json:"secretValue"` -} - -type Client struct { - BaseURL string - AuthToken string - http *http.Client -} - -func NewClient(baseURL string, authToken string) *Client { - return &Client{ - BaseURL: baseURL, - AuthToken: authToken, - http: &http.Client{}, - } -} - -func (c *Client) GetSecretsV4(ctx context.Context, projectId string, environment string, secretsPath string) ([]RetrievedSecret, error) { - url := fmt.Sprintf("%s/api/v4/secrets?projectId=%s&environment=%s&secretPath=%s", - c.BaseURL, projectId, environment, secretsPath) - - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", "Bearer "+c.AuthToken) - - resp, err := c.http.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("infisical v4 error: %s", string(body)) - } - - var data struct { - Secrets []RetrievedSecret `json:"secrets"` - } - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - return nil, err - } - - return data.Secrets, nil -} - -// Функция взятия конкретного ключа, а не списка -func (c *Client) GetSecret(ctx context.Context, projectId string, environment string, secretsPath string, key string) (*RetrievedSecret, error) { - secrets, err := c.GetSecretsV4(ctx, projectId, environment, secretsPath) - if err != nil { - return nil, err - } - for _, s := range secrets { - if s.Key == key { - return &s, nil - } - } - return nil, fmt.Errorf("secret %s not found", key) -} diff --git a/internal/userservice/config/config.go b/internal/userservice/config/config.go index 4301f79..a2f764b 100644 --- a/internal/userservice/config/config.go +++ b/internal/userservice/config/config.go @@ -1,59 +1,35 @@ package config import ( - "log" + "errors" "os" - - "github.com/joho/godotenv" - "github.com/spf13/viper" ) type Config struct { - Host string `mapstructure:"HOST"` - Port string `mapstructure:"PORT"` - - InfisicalSecretKey string `mapstructure:"INFISICAL_SECRET_KEY"` - InfisicalProjectId string `mapstructure:"INFISICAL_PROJECT_ID"` - InfisicalEnv string `mapstructure:"INFISICAL_ENV"` + Host string `mapstructure:"HOST"` + Port string `mapstructure:"PORT"` + DatabaseURL string `mapstructure:"DATABASE_URL"` } -func LoadConfig() *Config { - if _, err := os.Stat(".env"); err == nil { - godotenv.Load() - } - - viper.AutomaticEnv() - if err := viper.BindEnv("HOST"); err != nil { - log.Fatalf("viper.BindEnv HOST error: %v", err) - } - - if err := viper.BindEnv("PORT"); err != nil { - log.Fatalf("viper.BindEnv PORT error: %v", err) +func LoadConfig() (*Config, error) { + host, exists := os.LookupEnv("HOST") + if !exists { + return nil, errors.New("HOST environment variable is required") } - if err := viper.BindEnv("INFISICAL_SECRET_KEY"); err != nil { - log.Fatalf("viper.BindEnv INFISICAL_SECRET_KEY error: %v", err) + port, exists := os.LookupEnv("PORT") + if !exists { + return nil, errors.New("PORT environment variable is required") } - if err := viper.BindEnv("INFISICAL_PROJECT_ID"); err != nil { - log.Fatalf("viper.BindEnv INFISICAL_PROJECT_ID error: %v", err) + databaseURL, exists := os.LookupEnv("DATABASE_URL") + if !exists { + return nil, errors.New("DATABASE_URL environment variable is required") } - if err := viper.BindEnv("INFISICAL_ENV"); err != nil { - log.Fatalf("viper.BindEnv INFISICAL_ENV error: %v", err) - } - - host := viper.GetString("HOST") - port := viper.GetString("PORT") - infisicalSecretKey := viper.GetString("INFISICAL_SECRET_KEY") - infisicalProjectId := viper.GetString("INFISICAL_PROJECT_ID") - infisicalEnv := viper.GetString("INFISICAL_ENV") - return &Config{ - Host: host, - Port: port, - InfisicalSecretKey: infisicalSecretKey, - InfisicalProjectId: infisicalProjectId, - InfisicalEnv: infisicalEnv, - } -} \ No newline at end of file + Host: host, + Port: port, + DatabaseURL: databaseURL, + }, nil +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..c7d9cfc --- /dev/null +++ b/justfile @@ -0,0 +1,28 @@ +set windows-shell := ["sh", "-c"] +set dotenv-load := true + +COMMON_JUST_URL := 'https://raw.githubusercontent.com/esclient/tools/refs/heads/main/go/common.just' +LOAD_ENVS_URL := 'https://raw.githubusercontent.com/esclient/tools/refs/heads/main/load_envs.sh' + +PROTO_TAG := 'v0.0.11' +PROTO_NAME := 'user.proto' +TMP_DIR := '.proto' +OUT_DIR := 'api/userservice' +PROTO_MODULE := `go list -m` +GO_MODULE_GO_OPT := '--go_opt=module=' + PROTO_MODULE +GO_MODULE_GRPC_OPT := '--go-grpc_opt=module=' + PROTO_MODULE + +MKDIR_TOOLS := 'mkdir -p tools' + +FETCH_COMMON_JUST := 'curl -fsSL ' + COMMON_JUST_URL + ' -o tools/common.just' +FETCH_LOAD_ENVS := 'curl -fsSL ' + LOAD_ENVS_URL + ' -o tools/load_envs.sh' + +import? 'tools/common.just' + +default: + @just --list + +fetch-tools: + {{MKDIR_TOOLS}} + {{FETCH_COMMON_JUST}} + {{FETCH_LOAD_ENVS}} diff --git a/tools/common.just b/tools/common.just new file mode 100644 index 0000000..66af451 --- /dev/null +++ b/tools/common.just @@ -0,0 +1,37 @@ +MKDIR := 'mkdir -p' +RM := 'rm -rf ' + TMP_DIR +DOWN := 'curl -fsSL' +DOWN_OUT := '-o' + +clean: + {{RM}} + +fetch-proto: + {{MKDIR}} "{{TMP_DIR}}" + {{DOWN}} "https://raw.githubusercontent.com/esclient/protos/{{PROTO_TAG}}/{{PROTO_NAME}}" {{DOWN_OUT}} "{{TMP_DIR}}/{{PROTO_NAME}}" + +run: + ENV=dev ./tools/load_envs.sh go run ./cmd + +gen-stubs: fetch-proto + {{MKDIR}} "{{OUT_DIR}}" + protoc \ + --proto_path="{{TMP_DIR}}" \ + --go_out="{{OUT_DIR}}" \ + {{GO_MODULE_GO_OPT}} \ + --go-grpc_out="{{OUT_DIR}}" \ + {{GO_MODULE_GRPC_OPT}} \ + "{{TMP_DIR}}/{{PROTO_NAME}}" + +update: gen-stubs clean + +format: + # placeholder + +lint: + # placeholder + +test: + # placeholder + +prepare: format lint test diff --git a/tools/load_envs.sh b/tools/load_envs.sh new file mode 100644 index 0000000..cbc4a4b --- /dev/null +++ b/tools/load_envs.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -e + +if [ "$ENV" = "prod" ] && [ -f .env ]; then + export $(grep -v '^#' .env | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed 's/"//g' | sed "s/'//g" | xargs) +fi + +CONFIG="configs/${ENV}.yaml" +[ ! -f "$CONFIG" ] && echo "ERROR: Config not found: $CONFIG" >&2 && exit 1 + +eval "$(yq eval 'to_entries | .[] | "export " + .key + "='\''" + (.value | tostring) + "'\''"' "$CONFIG")" + +if [ "$ENV" = "dev" ]; then + exec "$@" +fi + +VAULT_REFS_COUNT=$(env | grep -c '{vault:' || true) + +if [ "$VAULT_REFS_COUNT" -eq 0 ]; then + exec "$@" +fi + +if [ -z "$VAULT_ADDR" ] || [ -z "$VAULT_TOKEN" ]; then + echo "ERROR: Config contains vault references but VAULT_ADDR or VAULT_TOKEN is not set" >&2 + exit 1 +fi + +for var in $(env | grep '{vault:' | cut -d= -f1); do + value="${!var}" + + while [[ "$value" =~ \{vault:([^}]+)\} ]]; do + ref="${BASH_REMATCH[1]}" + path="${ref%#*}" + key="${ref##*#}" + + vault_mount="${VAULT_MOUNT_POINT:-secret}" + vault_url="${VAULT_ADDR}/v1/${vault_mount}/data/${path}" + + set +e + vault_response=$(curl -s -S -w "\n%{http_code}" \ + --header "X-Vault-Token: $VAULT_TOKEN" \ + "$vault_url" 2>&1) + curl_exit_code=$? + set -e + + if [ $curl_exit_code -ne 0 ]; then + echo "ERROR: Failed to fetch secret from Vault: $vault_url" >&2 + echo "Curl response: $vault_response" >&2 + exit 1 + fi + + http_code=$(echo "$vault_response" | tail -n1) + vault_response=$(echo "$vault_response" | sed '$d') + + if [ "$http_code" != "200" ]; then + echo "ERROR: Vault API returned HTTP $http_code for $vault_url" >&2 + exit 1 + fi + + secret=$(echo "$vault_response" | jq -r ".data.data.$key // .data.$key") + + if [ "$secret" = "null" ] || [ -z "$secret" ]; then + echo "ERROR: Secret key '$key' not found at path '$path'" >&2 + exit 1 + fi + + value="${value/\{vault:$ref\}/$secret}" + done + + export "$var=$value" +done + +exec "$@"