diff --git a/api/httpserver/handle_tickets.go b/api/httpserver/handle_tickets.go index 128d178..620dee4 100644 --- a/api/httpserver/handle_tickets.go +++ b/api/httpserver/handle_tickets.go @@ -20,7 +20,7 @@ type Ticket struct { func (s *Server) GetTickets(c *gin.Context) { ctx := c.Request.Context() - tickets, err := s.ticketService.FetchTicketsFromDB(ctx) + tickets, err := s.ticketService.FetchTicketsFromJira(ctx) if err != nil { c.JSON(http.StatusInternalServerError, c.Error(err)) diff --git a/background/assetrefresher/refresh.go b/background/assetrefresher/refresh.go index cd5c9c0..d23ce32 100644 --- a/background/assetrefresher/refresh.go +++ b/background/assetrefresher/refresh.go @@ -3,6 +3,7 @@ package assetrefresher import ( "context" "fossa/pkg/logging" + "fossa/service/asset" "fossa/service/ticket" "time" ) @@ -12,26 +13,26 @@ const updateInterval = 10 * time.Second type Refresher struct { tkr *time.Ticker ticketService *ticket.Service + assetService *asset.Service logger *logging.Logger } -func New(ticketService *ticket.Service, logger *logging.Logger) *Refresher { +func New(ticketService *ticket.Service, assetService *asset.Service, logger *logging.Logger) *Refresher { return &Refresher{ tkr: time.NewTicker(updateInterval), ticketService: ticketService, + assetService: assetService, logger: logger, } } func (r *Refresher) Run(ctx context.Context) { - r.logger.Info("Asset refresher started") + r.logger.Debug("Asset refresher started") for range r.tkr.C { - r.logger.Info("Asset refresher tick") - - err := r.ticketService.GenerateTexts(ctx) + err := r.generateAssets(ctx) if err != nil { - r.logger.Error("Error generating texts: %v", err) + r.logger.Error("generate texts: %v", err) } } } @@ -39,3 +40,32 @@ func (r *Refresher) Run(ctx context.Context) { func (r *Refresher) Stop() { r.tkr.Stop() } + +func (r *Refresher) generateAssets(ctx context.Context) error { + r.logger.Debug("Asset refresher triggered") + + /* + TODO: + - calculate hash on json/yaml in Jira + - compare hash with cached value, proceed if hash changed + */ + + tickets, err := r.ticketService.FetchTicketsFromJira(ctx) + if err != nil { + return err + } + + for _, t := range tickets { + if t.TemplateVariables == nil { + continue + } + + _, err := r.assetService.GenerateAssetsForTicket(ctx, t.TemplateVariables) + if err != nil { + r.logger.Error("generate assets %s: %v", t.ID, err) + continue + } + } + + return nil +} diff --git a/go.mod b/go.mod index 8ab1660..2c59abd 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/squirrel v1.5.4 github.com/andygrunwald/go-jira v1.17.0 github.com/gin-gonic/gin v1.9.1 + github.com/goccy/go-yaml v1.19.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/ncruces/go-sqlite3 v0.20.1 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index c5d6b35..816a629 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= +github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= diff --git a/main.go b/main.go index a8b4d27..6dccfd6 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "fossa/pkg/jiraclient" "fossa/pkg/logging" "fossa/pkg/sqlite" + "fossa/service/asset" "fossa/service/template" "fossa/service/ticket" "log" @@ -50,6 +51,7 @@ func main() { templatesService := template.NewService(templatesRepository) ticketsService := ticket.NewService(ticketsRepository, templatesService, jiraClient) + assetService := asset.NewService(nil, templatesService) httpServer := httpserver.New(cfg.HTTPServer, ticketsService) @@ -59,6 +61,7 @@ func main() { backgroundAssetRefresher := assetrefresher.New( ticketsService, + assetService, logger, ) diff --git a/pkg/jiraclient/client.go b/pkg/jiraclient/client.go index 1fc1362..6758453 100644 --- a/pkg/jiraclient/client.go +++ b/pkg/jiraclient/client.go @@ -40,7 +40,7 @@ func New(config Config) (*Client, error) { return &Client{client: client}, nil } -func (c *Client) FetchTicketsFromJira(ctx context.Context) ([]ticket.Ticket, error) { +func (c *Client) FetchTickets(ctx context.Context) ([]ticket.Ticket, error) { fmt.Println("Fetching tickets from Jira") options := &jira.SearchOptionsV2{ @@ -74,3 +74,21 @@ func (c *Client) FetchTicketsFromJira(ctx context.Context) ([]ticket.Ticket, err return tt, nil } + +func (c *Client) FetchTicketDetails(ctx context.Context, ticketID string) (*ticket.Ticket, error) { + issue, resp, err := c.client.Issue.GetWithContext(ctx, ticketID, nil) + if err != nil { + return nil, errors.Wrap(err, "fetch issue") + } + if resp.StatusCode != 200 { + return nil, errors.Errorf("non-200 response: %d", resp.StatusCode) + } + + t := &ticket.Ticket{ + ID: issue.Key, + Title: issue.Fields.Summary, + Description: issue.Fields.Description, + } + + return t, nil +} diff --git a/repository/assetrepo/sql.go b/repository/assetrepo/sql.go new file mode 100644 index 0000000..0e094c6 --- /dev/null +++ b/repository/assetrepo/sql.go @@ -0,0 +1,66 @@ +package assetrepo + +import ( + "context" + "database/sql" + "fmt" + + "fossa/service/asset" + + sq "github.com/Masterminds/squirrel" + "github.com/pkg/errors" +) + +const ( + assetsTable = "assets" +) + +type SQLite struct { + database *sql.DB +} + +func NewSQLite(database *sql.DB) *SQLite { + return &SQLite{ + database: database, + } +} + +func (s *SQLite) FetchAssetsByTicketID(ctx context.Context, ticketID string) ([]asset.Asset, error) { + query, values, _ := sq. + Select( + "id", + "job_type", + "step", + "contents", + ). + From(assetsTable). + OrderByClause(fmt.Sprintf("%s %s", "job_type", "ASC")). + PlaceholderFormat(sq.Dollar). + ToSql() + + results := []asset.Asset{} + + rows, err := s.database.QueryContext(ctx, query, values...) + if err != nil { + return nil, errors.Wrap(err, "send query") + } + + defer rows.Close() + + for rows.Next() { + var t asset.Asset + + if err := rows.Scan( + &t.ID, + &t.JobType, + &t.Step, + &t.Content, + ); err != nil { + return nil, errors.Wrap(err, "iterate rows") + } + + results = append(results, t) + } + + return results, nil +} diff --git a/repository/templaterepo/sql.go b/repository/templaterepo/sql.go index 1dea097..411d98c 100644 --- a/repository/templaterepo/sql.go +++ b/repository/templaterepo/sql.go @@ -3,12 +3,8 @@ package templaterepo import ( "context" "database/sql" - "fmt" "fossa/service/template" - - sq "github.com/Masterminds/squirrel" - "github.com/pkg/errors" ) const ( @@ -25,42 +21,53 @@ func NewSQLite(database *sql.DB) *SQLite { } } -func (s *SQLite) FetchTemplatesByName(ctx context.Context, name string) ([]template.Template, error) { - query, values, _ := sq. - Select( - "id", - "name", - "step", - "contents", - ). - From(templatesTable). - OrderByClause(fmt.Sprintf("%s %s", "name", "ASC")). - PlaceholderFormat(sq.Dollar). - ToSql() +func (s *SQLite) FetchTemplatesByJobType(ctx context.Context, jobType string) ([]template.Template, error) { + // query, values, _ := sq. + // Select( + // "id", + // "job_type", + // "step", + // "contents", + // ). + // From(templatesTable). + // OrderByClause(fmt.Sprintf("%s %s", "job_type", "ASC")). + // PlaceholderFormat(sq.Dollar). + // ToSql() results := []template.Template{} - rows, err := s.database.QueryContext(ctx, query, values...) - if err != nil { - return nil, errors.Wrap(err, "cannot perform select query") - } + // rows, err := s.database.QueryContext(ctx, query, values...) + // if err != nil { + // return nil, errors.Wrap(err, "cannot perform select query") + // } + + // defer rows.Close() - defer rows.Close() + // for rows.Next() { + // var t template.Template - for rows.Next() { - var t template.Template + // if err := rows.Scan( + // &t.ID, + // &t.JobType, + // &t.Step, + // &t.Content, + // ); err != nil { + // return nil, errors.Wrap(err, "cannot perform select query") + // } - if err := rows.Scan( - &t.ID, - &t.Name, - &t.Step, - &t.Content, - ); err != nil { - return nil, errors.Wrap(err, "cannot perform select query") - } + // results = append(results, t) + // } - results = append(results, t) + // TODO: remove mock data + + t := template.Template{ + ID: "1", + JobType: "install_optics_and_connect_two_cenic_devices", + Step: "installation", + Content: "example-content:\n A device: {{ device_a_port }}", } + results = append(results, t) + return results, nil } diff --git a/repository/ticketrepo/sql.go b/repository/ticketrepo/sql.go index 10f46b5..ceadea1 100644 --- a/repository/ticketrepo/sql.go +++ b/repository/ticketrepo/sql.go @@ -62,5 +62,14 @@ func (s *SQLite) FetchTickets(ctx context.Context) ([]ticket.Ticket, error) { results = append(results, t) } + // TODO: remove mock data + t := ticket.Ticket{ + ID: "NET-1000", + Title: "Install Optics and Connect Two CENIC Devices", + Description: "Please install the optics and connect the two CENIC devices as per the instructions.\n\nFor automation:\n device_a_port: ge-1/0/1\n device_b_port: ge-1/0/2\n interface_speed: 10G", + } + + results = append(results, t) + return results, nil } diff --git a/service/asset/domain.go b/service/asset/domain.go new file mode 100644 index 0000000..ea7b950 --- /dev/null +++ b/service/asset/domain.go @@ -0,0 +1,12 @@ +package asset + +import "fossa/pkg/history" + +// business domain entities +type Asset struct { + ID string + JobType string + Step string + Content string + history.HistoryData +} diff --git a/service/asset/int_assetrepo.go b/service/asset/int_assetrepo.go new file mode 100644 index 0000000..49f2013 --- /dev/null +++ b/service/asset/int_assetrepo.go @@ -0,0 +1,9 @@ +package asset + +import ( + "context" +) + +type AssetRepository interface { + FetchAssetsByTicketID(ctx context.Context, ticketID string) ([]Asset, error) +} diff --git a/service/asset/service.go b/service/asset/service.go new file mode 100644 index 0000000..a7abaeb --- /dev/null +++ b/service/asset/service.go @@ -0,0 +1,29 @@ +package asset + +import ( + "context" + "fossa/service/template" +) + +type ticketAssets map[string]Asset // map key: step + +type Service struct { + repository AssetRepository + templateService *template.Service +} + +func NewService(repository AssetRepository, templateService *template.Service) *Service { + return &Service{ + repository: repository, + templateService: templateService} +} + +func (s *Service) GenerateAssetsForTicket(ctx context.Context, vars map[string]string) (map[string]Asset, error) { + res := make(map[string]Asset) + + // for _, step := range vars { + + // } + + return res, nil +} diff --git a/service/assetgen/service.go b/service/assetgen/service.go deleted file mode 100644 index fa23b71..0000000 --- a/service/assetgen/service.go +++ /dev/null @@ -1,9 +0,0 @@ -package assetgen - -type ticketAssets map[string]string -type Service struct { -} - -func NewService() *Service { - return &Service{} -} diff --git a/service/template/domain.go b/service/template/domain.go new file mode 100644 index 0000000..53a0822 --- /dev/null +++ b/service/template/domain.go @@ -0,0 +1,14 @@ +package template + +import ( + "fossa/pkg/history" +) + +// business domain entities +type Template struct { + ID string + JobType string + Step string + Content string + history.HistoryData +} diff --git a/service/template/int_ticketrepo.go b/service/template/int_ticketrepo.go index 478f534..6986736 100644 --- a/service/template/int_ticketrepo.go +++ b/service/template/int_ticketrepo.go @@ -5,5 +5,5 @@ import ( ) type TemplateRepository interface { - FetchTemplatesByName(ctx context.Context, name string) ([]Template, error) + FetchTemplatesByJobType(ctx context.Context, jobType string) ([]Template, error) } diff --git a/service/template/service.go b/service/template/service.go index 20acc87..b152d20 100644 --- a/service/template/service.go +++ b/service/template/service.go @@ -2,22 +2,10 @@ package template import ( "context" - "fossa/pkg/history" "github.com/pkg/errors" ) -// business domain entities -type Template struct { - ID string - Name string - Step string - Content string - history.HistoryData -} - -// end of entities - type Service struct { repository TemplateRepository } @@ -31,7 +19,7 @@ func NewService( } func (s *Service) FetchTemplates(ctx context.Context, name string) ([]Template, error) { - templates, err := s.repository.FetchTemplatesByName(ctx, name) + templates, err := s.repository.FetchTemplatesByJobType(ctx, name) if err != nil { return nil, errors.Wrap(err, "can't get templates") } diff --git a/service/ticket/domain.go b/service/ticket/domain.go new file mode 100644 index 0000000..4d79af9 --- /dev/null +++ b/service/ticket/domain.go @@ -0,0 +1,17 @@ +package ticket + +import ( + "fossa/pkg/history" +) + +// business domain entities +type Ticket struct { + ID string + Title string + Priority string + Description string + Assignee string + history.HistoryData + + TemplateVariables map[string]string +} diff --git a/service/ticket/int_jira.go b/service/ticket/int_jira.go index cf8333f..62fd54c 100644 --- a/service/ticket/int_jira.go +++ b/service/ticket/int_jira.go @@ -5,5 +5,6 @@ import ( ) type JiraClient interface { - FetchTicketsFromJira(ctx context.Context) ([]Ticket, error) + FetchTickets(ctx context.Context) ([]Ticket, error) + FetchTicketDetails(ctx context.Context, ticketID string) (*Ticket, error) } diff --git a/service/ticket/int_ticketrepo.go b/service/ticket/int_ticketrepo.go index 04c6bef..7da276a 100644 --- a/service/ticket/int_ticketrepo.go +++ b/service/ticket/int_ticketrepo.go @@ -1,9 +1,5 @@ package ticket -import ( - "context" -) - type TicketRepository interface { - FetchTickets(ctx context.Context) ([]Ticket, error) + // FetchTickets(ctx context.Context) ([]Ticket, error) } diff --git a/service/ticket/service.go b/service/ticket/service.go index bf3731c..f95e9ee 100644 --- a/service/ticket/service.go +++ b/service/ticket/service.go @@ -2,14 +2,16 @@ package ticket import ( "context" - "encoding/json" - "fmt" - "fossa/pkg/history" + "fossa/pkg/logging" "fossa/service/template" + "strings" + "github.com/goccy/go-yaml" "github.com/pkg/errors" ) +const delimiterStart = "\nFor automation:" + type Service struct { repository TicketRepository templateService *template.Service @@ -28,49 +30,66 @@ func NewService( } } -type Ticket struct { - ID string - Title string - Priority string - Description json.RawMessage - history.HistoryData -} - -func (s *Service) FetchTicketsFromDB(ctx context.Context) ([]Ticket, error) { - /* - - fetch tickets with filter - - calculate hash on json/yaml - - compare hash with cached value, proceed if not changed - - - */ - settings, err := s.repository.FetchTickets(ctx) - if err != nil { - return nil, errors.Wrap(err, "can't get tickets") - } +// func (s *Service) FetchTicketsFromDB(ctx context.Context) ([]Ticket, error) { +// settings, err := s.repository.FetchTickets(ctx) +// if err != nil { +// return nil, errors.Wrap(err, "can't get tickets") +// } - return settings, nil -} +// return settings, nil +// } func (s *Service) FetchTicketsFromJira(ctx context.Context) ([]Ticket, error) { - tickets, err := s.jiraClient.FetchTicketsFromJira(ctx) + logger := logging.UnpackContext(ctx) + + tickets, err := s.jiraClient.FetchTickets(ctx) if err != nil { return nil, errors.Wrap(err, "can't fetch tickets from Jira") } - return tickets, nil + for _, t := range tickets { + details, err := s.jiraClient.FetchTicketDetails(ctx, t.ID) + if err != nil { + return nil, errors.Wrap(err, "fetch ticket details") + } + + vars, err := s.parseTicket(ctx, details) + if err != nil { + return nil, errors.Wrap(err, "parse ticket") + } + + if len(vars) == 0 { + logger.Debug("Skipping ticket %s as it does not contain template variables", t.ID) + + continue + } + logger.Debug("Parsed template variables for ticket %s: %+v", t.ID, vars) + + t.TemplateVariables = vars + } + + return tickets, nil } -func (s *Service) GenerateTexts(ctx context.Context) error { - tickets, err := s.jiraClient.FetchTicketsFromJira(ctx) - if err != nil { - return errors.Wrap(err, "can't fetch tickets from Jira") +func (s *Service) parseTicket(ctx context.Context, ticket *Ticket) (map[string]string, error) { + des := strings.ToLower(ticket.Description) + + vars := make(map[string]string, 0) + + if !strings.Contains(des, delimiterStart) { + return vars, nil } - // allAssets := make(map[string]ticketAssets) + logger := logging.UnpackContext(ctx) + logger.Debug("found automation directive in ticket %s!", ticket.ID) - for _, t := range tickets { - fmt.Printf("Processing ticket: %s\n", t.ID) + yml := strings.Split(des, delimiterStart)[1] + + err := yaml.Unmarshal([]byte(yml), &vars) + if err != nil { + return nil, errors.Wrap(err, "parse description") } - return nil + + return vars, nil }