From a00e81e7f227b73bf4d7fdae11acee216b10f1d5 Mon Sep 17 00:00:00 2001 From: "d.pikaliuk" Date: Fri, 12 Jan 2024 07:30:00 +0100 Subject: [PATCH] feat: added ci/cd --- .github/workflows/ci-cd.yml | 35 +++++++++++++++++++++++ cicd/dockerfiles/insta-bot.Dockerfile | 30 ++++++++++++++++++++ cmd/app/config.go | 40 ++++++--------------------- cmd/app/entry.go | 19 ++++++------- cmd/app/main.go | 4 +-- docker-compose.yml | 26 +++++++++++++++-- go.mod | 5 +++- go.sum | 3 ++ pkg/factory/telegram-bot.go | 8 ++++-- pkg/run/run.go | 5 +--- 10 files changed, 120 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/ci-cd.yml create mode 100644 cicd/dockerfiles/insta-bot.Dockerfile diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..396792e --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,35 @@ +name: CI/CD Pipeline + +on: + push: + tags: + - 'v1.*.*' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build and Push Docker image + uses: docker/build-push-action@v2 + with: + file: ./cicd/dockerfiles/insta-bot.Dockerfile + context: . + push: true + tags: haski007/insta-bot:${{ github.ref_name }} + + - name: Run Docker Compose + run: | + docker-compose -f docker-compose.yml up --build -d diff --git a/cicd/dockerfiles/insta-bot.Dockerfile b/cicd/dockerfiles/insta-bot.Dockerfile new file mode 100644 index 0000000..6385206 --- /dev/null +++ b/cicd/dockerfiles/insta-bot.Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.21 as builder + +# Set the working dixrectory in the container +WORKDIR /app + +# Copy the Go modules and sum files +COPY go.mod go.sum ./ + +# Download Go module dependencies +RUN go mod download + +# Copy the rest of the source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o insta-bot ./cmd/app + +# Use a small image to run the application +FROM alpine:latest +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Copy the pre-built binary file from the previous stage +COPY --from=builder /app/insta-bot . +COPY --from=builder /app/token.json . + + +# Command to run the executable +CMD ["./insta-bot"] diff --git a/cmd/app/config.go b/cmd/app/config.go index a3971f9..158f3cd 100644 --- a/cmd/app/config.go +++ b/cmd/app/config.go @@ -1,12 +1,9 @@ package main import ( - "fmt" - "os" "time" "github.com/haski007/insta-bot/pkg/factory" - "gopkg.in/yaml.v2" ) type Config struct { @@ -22,43 +19,22 @@ type Config struct { } type GoogleConfig struct { - CredentialsPath string `yaml:"credentials_path"` + Credentials string `yaml:"credentials_path" env:"GOOGLE_CREDENTIALS"` } type OpenAIConfig struct { - ApiKey string `yaml:"api_key"` - GPTModelForConv string `yaml:"gpt_model_for_conv"` - GPTModelForHistory string `yaml:"gpt_model_for_history"` + ApiKey string `yaml:"api_key" env:"OPENAI_API_KEY"` + GPTModelForConv string `yaml:"gpt_model_for_conv" env:"OPENAI_GPT_MODEL_FOR_CONV"` + GPTModelForHistory string `yaml:"gpt_model_for_history" env:"OPENAI_GPT_MODEL_FOR_HISTORY"` } type RedisClient struct { - Addr string `yaml:"addr"` - Pass string `yaml:"pass"` - ConversationTTLMin time.Duration `yaml:"conversation_ttl_min"` - HistoryMessagesTTLHours time.Duration `yaml:"history_messages_ttl_hours"` + Addr string `yaml:"addr" env:"REDIS_ADDR"` + Pass string `yaml:"pass" env:"REDIS_PASS"` + ConversationTTL time.Duration `yaml:"conversation_ttl_min" env:"REDIS_CONVERSATION_TTL"` + HistoryMessagesTTL time.Duration `yaml:"history_messages_ttl_hours" env:"REDIS_HISTORY_MESSAGES_TTL"` } type YouTubeConfig struct { MaxQuality int `yaml:"max_quality"` } - -func Load(configFile string, cfg interface{}) error { - fileData, err := os.ReadFile(configFile) - if err != nil { - return fmt.Errorf("can't read config file: %w", err) - } - - fileData = []byte(os.ExpandEnv(string(fileData))) - - if err = yaml.Unmarshal(fileData, cfg); err != nil { - return fmt.Errorf("can't unmarshal config data: %w", err) - } - - if v, ok := cfg.(interface{ Validate() error }); ok { - if err = v.Validate(); err != nil { - return fmt.Errorf("invalid config: %w", err) - } - } - - return nil -} diff --git a/cmd/app/entry.go b/cmd/app/entry.go index d94f680..724ca84 100644 --- a/cmd/app/entry.go +++ b/cmd/app/entry.go @@ -1,14 +1,13 @@ package main import ( + "bytes" "context" "errors" "fmt" "net/http" - "os" "os/signal" "syscall" - "time" "github.com/go-redis/redis" "github.com/haski007/insta-bot/internal/bot/listener" @@ -20,6 +19,7 @@ import ( "github.com/haski007/insta-bot/pkg/run" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sashabaranov/go-openai" + "github.com/sethvargo/go-envconfig" "github.com/sirupsen/logrus" "golang.org/x/oauth2/google" "golang.org/x/sync/errgroup" @@ -38,15 +38,12 @@ func Run(ctx context.Context, args run.Args) error { log.SetFormatter(&logrus.JSONFormatter{}) var cfg Config - if err := Load(args.ConfigFile, &cfg); err != nil { - return fmt.Errorf("load config %s err: %w", args.ConfigFile, err) + if err := envconfig.Process(ctx, &cfg); err != nil { + return fmt.Errorf("load config from env err: %w", err) } // ---> Google AUTH - b, err := os.ReadFile(cfg.Clients.Google.CredentialsPath) - if err != nil { - log.Fatalf("Unable to read client secret file: %v", err) - } + b := bytes.NewBufferString(cfg.Clients.Google.Credentials).Bytes() // If modifying these scopes, delete your previously saved token.json. config, err := google.ConfigFromJSON(b, @@ -78,7 +75,7 @@ func Run(ctx context.Context, args run.Args) error { } u := tgbotapi.NewUpdate(0) - u.Timeout = cfg.TelegramBot.UpdatesTimeoutSec + u.Timeout = int(cfg.TelegramBot.UpdatesTimeout.Seconds()) chUpdates := botApi.GetUpdatesChan(u) apiCli := instapi.New() @@ -91,8 +88,8 @@ func Run(ctx context.Context, args run.Args) error { redisStorage, err := redisWrapper.NewClient( redCC, - time.Minute*cfg.Clients.Redis.ConversationTTLMin, - time.Hour*cfg.Clients.Redis.HistoryMessagesTTLHours, + cfg.Clients.Redis.ConversationTTL, + cfg.Clients.Redis.HistoryMessagesTTL, ) if err != nil { return fmt.Errorf("connect to redis err: %w", err) diff --git a/cmd/app/main.go b/cmd/app/main.go index ec26315..3b36a66 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -25,7 +25,7 @@ func main() { } if err := app.Run(os.Args); err != nil { - logrus.Panicln(err) + logrus.WithError(err).Fatalln("app run") } } @@ -50,7 +50,7 @@ func flags() []cli.Flag { &cli.StringFlag{ Name: "config", Aliases: []string{"c"}, - Required: true, + Required: false, EnvVars: []string{"CONFIG_PATH"}, }, } diff --git a/docker-compose.yml b/docker-compose.yml index b76edab..845f1a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,33 @@ version: "3" services: + insta-bot: + build: + context: . + dockerfile: cicd/dockerfiles/insta-bot.Dockerfile + environment: + REDIS_ADDR: "redis:6379" + REDIS_PASS: "${REDIS_PASS}" + REDIS_CONVERSATION_TTL: "${REDIS_CONVERSATION_TTL}" + REDIS_HISTORY_MESSAGES_TTL: "${REDIS_HISTORY_MESSAGES_TTL}" + GOOGLE_CREDENTIALS: "${GOOGLE_CREDENTIALS}" + OPENAI_API_KEY: "${OPENAI_API_KEY}" + OPENAI_GPT_MODEL_FOR_CONV: "${OPENAI_GPT_MODEL_FOR_CONV}" + OPENAI_GPT_MODEL_FOR_HISTORY: "${OPENAI_GPT_MODEL_FOR_HISTORY}" + BOT_TOKEN: "${BOT_TOKEN}" + BOT_CREATOR_USER_ID: "${BOT_CREATOR_USER_ID}" + BOT_UPDATES_TIMEOUT: "${BOT_UPDATES_TIMEOUT}" + CAPTION_CHARS_LIMIT: "${CAPTION_CHARS_LIMIT}" + ports: + - "8080:8080" + depends_on: + - redis + redis: image: redis:alpine - command: redis-server --requirepass zakEfron1 + command: redis-server --requirepass "${REDIS_PASS}" environment: - REDIS_REPLICATION_MODE=master - - REDIS_PASSWORD=zakEfron1 + - REDIS_PASSWORD=${REDIS_PASS} ports: - "6381:6379" volumes: diff --git a/go.mod b/go.mod index e18d375..ddb9455 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/haski007/insta-bot -go 1.18 +go 1.21 + +toolchain go1.21.5 require ( github.com/PuerkitoBio/goquery v1.8.0 @@ -48,6 +50,7 @@ require ( github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sethvargo/go-envconfig v1.0.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/net v0.4.0 // indirect diff --git a/go.sum b/go.sum index 8b5e0c0..e7eeb5f 100644 --- a/go.sum +++ b/go.sum @@ -151,6 +151,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -261,6 +262,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sashabaranov/go-openai v1.16.1 h1:R7c8jhXeQEtHkD6MoXQfkgzz/7JI6jjFKl+DXawaejQ= github.com/sashabaranov/go-openai v1.16.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sethvargo/go-envconfig v1.0.0 h1:1C66wzy4QrROf5ew4KdVw942CQDa55qmlYmw9FZxZdU= +github.com/sethvargo/go-envconfig v1.0.0/go.mod h1:Lzc75ghUn5ucmcRGIdGQ33DKJrcjk4kihFYgSTBmjIc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= diff --git a/pkg/factory/telegram-bot.go b/pkg/factory/telegram-bot.go index dcf7d10..8d25669 100644 --- a/pkg/factory/telegram-bot.go +++ b/pkg/factory/telegram-bot.go @@ -1,7 +1,9 @@ package factory +import "time" + type TelegramBotCfg struct { - Token string `json:"token" yaml:"token"` - CreatorUserID int64 `json:"creator_user_id" yaml:"creator_user_id"` - UpdatesTimeoutSec int `json:"updates_timeout_sec" yaml:"updates_timeout_sec"` + Token string `json:"token" yaml:"token" env:"BOT_TOKEN"` + CreatorUserID int64 `json:"creator_user_id" yaml:"creator_user_id" env:"BOT_CREATOR_USER_ID"` + UpdatesTimeout time.Duration `json:"updates_timeout" yaml:"updates_timeout" env:"BOT_UPDATES_TIMEOUT"` } diff --git a/pkg/run/run.go b/pkg/run/run.go index 02e4954..0850fc6 100644 --- a/pkg/run/run.go +++ b/pkg/run/run.go @@ -14,10 +14,7 @@ type Args struct { } func (a Args) Validate() error { - return validation.ValidateStruct(&a, - validation.Field(&a.ConfigFile, validation.Required), - validation.Field(&a.MetricsAddr, validation.Required), - ) + return validation.ValidateStruct(&a) } func LogLevel(level string) logrus.Level {