Initial commit.
This commit is contained in:
53
internal/config/config.go
Normal file
53
internal/config/config.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.dev.pztrn.name/periodicator/internal/gitlab"
|
||||
"go.dev.pztrn.name/periodicator/internal/tasks"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Config is a global configuration structure.
|
||||
type Config struct {
|
||||
Gitlab gitlab.Config `yaml:"gitlab"`
|
||||
Tasks []tasks.Config `yaml:"tasks"`
|
||||
}
|
||||
|
||||
// Parse tries to parse configuration and returns filled structure.
|
||||
func Parse() *Config {
|
||||
configPath, found := os.LookupEnv("GPT_CONFIG")
|
||||
if !found {
|
||||
panic("No configuration file path provided in 'GPT_CONFIG' environment variable!")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(configPath, "~") {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic("Failed to get user's home directory: " + err.Error())
|
||||
}
|
||||
|
||||
configPath = strings.Replace(configPath, "~", homeDir, 1)
|
||||
}
|
||||
|
||||
configPath, absErr := filepath.Abs(configPath)
|
||||
if absErr != nil {
|
||||
panic("Failed to get absolute path for '" + configPath + "': " + absErr.Error())
|
||||
}
|
||||
|
||||
data, readErr := os.ReadFile(configPath)
|
||||
if readErr != nil {
|
||||
panic("Failed to read configuration file data: " + readErr.Error())
|
||||
}
|
||||
|
||||
// nolint:exhaustivestruct
|
||||
c := &Config{}
|
||||
|
||||
if err := yaml.Unmarshal(data, c); err != nil {
|
||||
panic("Failed to unmarshal YAML data: " + err.Error())
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
11
internal/gitlab/config.go
Normal file
11
internal/gitlab/config.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package gitlab
|
||||
|
||||
// Config is a Gitlab client configuration structure that embedded in common
|
||||
// Config struct.
|
||||
// nolint:tagliatelle
|
||||
type Config struct {
|
||||
// BaseURL is a base part of URL for Gitlab.
|
||||
BaseURL string `yaml:"base_url"`
|
||||
// Token is a personal token to use for interacting with Gitlab API.
|
||||
Token string `yaml:"token"`
|
||||
}
|
83
internal/gitlab/gitlab.go
Normal file
83
internal/gitlab/gitlab.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
)
|
||||
|
||||
// Client is a Gitlab's client controlling structure.
|
||||
type Client struct {
|
||||
client *gitlab.Client
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewGitlabClient creates new Gitlab's client controlling structure.
|
||||
func NewGitlabClient(cfg *Config) *Client {
|
||||
// nolint:exhaustivestruct
|
||||
c := &Client{
|
||||
config: cfg,
|
||||
}
|
||||
c.initialize()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// CreateIssue creates issue in designated project (by ID) using passed options.
|
||||
// Returns error if something went wrong.
|
||||
func (c *Client) CreateIssue(projectID int, options *gitlab.CreateIssueOptions) error {
|
||||
_, _, err := c.client.Issues.CreateIssue(projectID, options)
|
||||
|
||||
// ToDo: fix it!
|
||||
// nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
// GetClient returns underlying Gitlab's client.
|
||||
func (c *Client) GetClient() *gitlab.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
// GetIssuesByTitle returns list of issues that matches passed title in specific
|
||||
// project.
|
||||
func (c *Client) GetIssuesByTitle(projectID int, title string) ([]*gitlab.Issue, error) {
|
||||
// nolint:exhaustivestruct
|
||||
issues, resp, err := c.client.Issues.ListProjectIssues(projectID, &gitlab.ListProjectIssuesOptions{
|
||||
ListOptions: gitlab.ListOptions{
|
||||
PerPage: 1000,
|
||||
Page: 1,
|
||||
},
|
||||
Search: &title,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Failed to execute HTTP request to Gitlab: " + err.Error())
|
||||
|
||||
// ToDo: fix it!
|
||||
// nolint:wrapcheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Println("Got status: " + resp.Status)
|
||||
|
||||
// ToDo: fix it!
|
||||
// nolint:goerr113
|
||||
return nil, errors.New("not HTTP 200 from Gitlab")
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (c *Client) initialize() {
|
||||
client, err := gitlab.NewClient(
|
||||
c.config.Token,
|
||||
gitlab.WithBaseURL(c.config.BaseURL),
|
||||
)
|
||||
if err != nil {
|
||||
panic("Failed to create Gitlab Client: " + err.Error())
|
||||
}
|
||||
|
||||
c.client = client
|
||||
}
|
127
internal/tasks/base.go
Normal file
127
internal/tasks/base.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron"
|
||||
g "github.com/xanzy/go-gitlab"
|
||||
"go.dev.pztrn.name/periodicator/internal/gitlab"
|
||||
)
|
||||
|
||||
// BaseTask is a base task structure.
|
||||
type BaseTask struct {
|
||||
client *gitlab.Client
|
||||
|
||||
projectID int
|
||||
title string
|
||||
body string
|
||||
tags []string
|
||||
|
||||
executionStartTimestamp time.Time
|
||||
cron string
|
||||
dueIn time.Duration
|
||||
}
|
||||
|
||||
func (b *BaseTask) checkIfOpenedTaskExists(issues []*g.Issue) bool {
|
||||
b.log("Checking if opened task already exists...")
|
||||
|
||||
var foundAndNotClosed bool
|
||||
|
||||
for _, issue := range issues {
|
||||
if issue.Title == b.title && issue.ClosedAt == nil {
|
||||
foundAndNotClosed = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return foundAndNotClosed
|
||||
}
|
||||
|
||||
func (b *BaseTask) log(message string) {
|
||||
log.Println("Task '" + b.title + "': " + message)
|
||||
}
|
||||
|
||||
// Run executes task procedure.
|
||||
func (b *BaseTask) Run() {
|
||||
// Get similar tasks.
|
||||
issues, err := b.client.GetIssuesByTitle(b.projectID, b.title)
|
||||
if err != nil {
|
||||
b.log("Error while getting issues from Gitlab: " + err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have opened task. We should not create another task until already
|
||||
// created task is closed.
|
||||
|
||||
if b.checkIfOpenedTaskExists(issues) {
|
||||
b.log("Found already existing task that isn't closed, won't create new task")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
b.log("No still opened tasks found, checking if we should create new one...")
|
||||
|
||||
// Get latest task creation timestamp from Gitlab.
|
||||
lastTaskCreationTS := b.executionStartTimestamp
|
||||
|
||||
for _, issue := range issues {
|
||||
if issue.ClosedAt != nil && issue.CreatedAt.After(lastTaskCreationTS) {
|
||||
lastTaskCreationTS = *issue.CreatedAt
|
||||
}
|
||||
}
|
||||
|
||||
b.log("Last task creation timestamp: " + lastTaskCreationTS.String())
|
||||
|
||||
// Set up cron job parser.
|
||||
cp := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
|
||||
schedule, err := cp.Parse(b.cron)
|
||||
if err != nil {
|
||||
b.log("Failed to parse cron string: " + err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Figure out next task creation and deadline timestamps.
|
||||
nextCreationTS := schedule.Next(lastTaskCreationTS)
|
||||
|
||||
// Check if task should be created and create if so.
|
||||
if nextCreationTS.Before(time.Now()) {
|
||||
// Deadlines should be calculated until first one will appear AFTER today.
|
||||
nextDeadlineTSAssumed := nextCreationTS.Add(b.dueIn)
|
||||
|
||||
var nextDeadlineTS g.ISOTime
|
||||
|
||||
for {
|
||||
if nextDeadlineTSAssumed.After(time.Now()) {
|
||||
nextDeadlineTS = g.ISOTime(nextDeadlineTSAssumed)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
nextDeadlineTSAssumed = nextDeadlineTSAssumed.Add(b.dueIn)
|
||||
}
|
||||
|
||||
b.log("Found no opened tasks and task should be created, doing so. Task deadline: " + nextDeadlineTS.String())
|
||||
|
||||
// nolint:exhaustivestruct
|
||||
err := b.client.CreateIssue(b.projectID, &g.CreateIssueOptions{
|
||||
Title: &b.title,
|
||||
Description: &b.body,
|
||||
Labels: b.tags,
|
||||
DueDate: &nextDeadlineTS,
|
||||
})
|
||||
if err != nil {
|
||||
b.log("Failed to create task: " + err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
b.log("Next task creation in future (" + nextCreationTS.String() + "), skipping for now")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
43
internal/tasks/config.go
Normal file
43
internal/tasks/config.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package tasks
|
||||
|
||||
import "time"
|
||||
|
||||
// Config is a task's configuration as should be defined in configuration file.
|
||||
// nolint:tagliatelle
|
||||
type Config struct {
|
||||
ProjectID int `yaml:"project_id"`
|
||||
Title string `yaml:"title"`
|
||||
Body string `yaml:"body"`
|
||||
Tags []string `yaml:"tags"`
|
||||
ExecutionStart TaskStartTime `yaml:"execution_start"`
|
||||
Cron string `yaml:"cron"`
|
||||
DueIn time.Duration `yaml:"due_in"`
|
||||
}
|
||||
|
||||
// TaskStartTime holds task's start time for next creation timestamp calculation.
|
||||
type TaskStartTime struct {
|
||||
ts time.Time
|
||||
}
|
||||
|
||||
func (tts *TaskStartTime) GetTimestamp() time.Time {
|
||||
return tts.ts
|
||||
}
|
||||
|
||||
func (tts *TaskStartTime) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var timeData string
|
||||
|
||||
if err := unmarshal(&timeData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02 15:04:05", timeData)
|
||||
if err != nil {
|
||||
// ToDo: fix it!
|
||||
// nolint:wrapcheck
|
||||
return err
|
||||
}
|
||||
|
||||
tts.ts = t
|
||||
|
||||
return nil
|
||||
}
|
23
internal/tasks/tasks.go
Normal file
23
internal/tasks/tasks.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"go.dev.pztrn.name/periodicator/internal/gitlab"
|
||||
)
|
||||
|
||||
// Process processes passed tasks.
|
||||
func Process(client *gitlab.Client, tasks []Config) {
|
||||
for _, task := range tasks {
|
||||
t := &BaseTask{
|
||||
client: client,
|
||||
projectID: task.ProjectID,
|
||||
title: task.Title,
|
||||
body: task.Body,
|
||||
tags: task.Tags,
|
||||
executionStartTimestamp: task.ExecutionStart.GetTimestamp(),
|
||||
cron: task.Cron,
|
||||
dueIn: task.DueIn,
|
||||
}
|
||||
|
||||
t.Run()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user