mirror of
https://github.com/strongdm/comply
synced 2024-11-24 00:24:54 +00:00
partial jira implementation; TODO: all Find/Read and Link cases.
This commit is contained in:
parent
0f68acae10
commit
10dc0b70e0
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/strongdm/comply/internal/config"
|
"github.com/strongdm/comply/internal/config"
|
||||||
|
"github.com/strongdm/comply/internal/jira"
|
||||||
"github.com/strongdm/comply/internal/plugin/github"
|
"github.com/strongdm/comply/internal/plugin/github"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
@ -55,6 +56,7 @@ func newApp() *cli.App {
|
|||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
github.Register()
|
github.Register()
|
||||||
|
jira.Register()
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/strongdm/comply/internal/config"
|
||||||
"github.com/strongdm/comply/internal/model"
|
"github.com/strongdm/comply/internal/model"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
@ -28,14 +29,22 @@ func procedureAction(c *cli.Context) error {
|
|||||||
|
|
||||||
procedureID := c.Args().First()
|
procedureID := c.Args().First()
|
||||||
|
|
||||||
|
ts, err := config.Config().TicketSystem()
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError("error in ticket system configuration", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := model.GetPlugin(model.TicketSystem(ts))
|
||||||
|
|
||||||
for _, procedure := range procedures {
|
for _, procedure := range procedures {
|
||||||
if procedure.ID == procedureID {
|
if procedure.ID == procedureID {
|
||||||
// TODO: don't hardcode GH
|
err = tp.Create(&model.Ticket{
|
||||||
tp := model.GetPlugin(model.GitHub)
|
|
||||||
tp.Create(&model.Ticket{
|
|
||||||
Name: procedure.Name,
|
Name: procedure.Name,
|
||||||
Body: fmt.Sprintf("%s\n\n\n---\nProcedure-ID: %s", procedure.Body, procedure.ID),
|
Body: fmt.Sprintf("%s\n\n\n---\nProcedure-ID: %s", procedure.Body, procedure.ID),
|
||||||
}, []string{"comply", "comply-procedure"})
|
}, []string{"comply", "comply-procedure"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/strongdm/comply/internal/config"
|
||||||
"github.com/strongdm/comply/internal/model"
|
"github.com/strongdm/comply/internal/model"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
@ -13,8 +14,12 @@ var syncCommand = cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncAction(c *cli.Context) error {
|
func syncAction(c *cli.Context) error {
|
||||||
// TODO: unhardcode plugin
|
ts, err := config.Config().TicketSystem()
|
||||||
tp := model.GetPlugin(model.GitHub)
|
if err != nil {
|
||||||
|
return cli.NewExitError("error in ticket system configuration", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := model.GetPlugin(model.TicketSystem(ts))
|
||||||
tickets, err := tp.FindByTagName("comply")
|
tickets, err := tp.FindByTagName("comply")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -12,6 +13,12 @@ var projectRoot string
|
|||||||
|
|
||||||
var dockerAvailable, pandocAvailable bool
|
var dockerAvailable, pandocAvailable bool
|
||||||
|
|
||||||
|
const (
|
||||||
|
Jira = "jira"
|
||||||
|
GitHub = "github"
|
||||||
|
NoTickets = "none"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// UseDocker invokes pandoc within Docker
|
// UseDocker invokes pandoc within Docker
|
||||||
UseDocker = "docker"
|
UseDocker = "docker"
|
||||||
@ -73,14 +80,14 @@ func Exists() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Config is the parsed contents of ProjectRoot()/config.yml.
|
// Config is the parsed contents of ProjectRoot()/config.yml.
|
||||||
func Config() Project {
|
func Config() *Project {
|
||||||
p := Project{}
|
p := Project{}
|
||||||
cfgBytes, err := ioutil.ReadFile(filepath.Join(ProjectRoot(), "comply.yml"))
|
cfgBytes, err := ioutil.ReadFile(filepath.Join(ProjectRoot(), "comply.yml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("unable to load config.yml: " + err.Error())
|
panic("unable to load config.yml: " + err.Error())
|
||||||
}
|
}
|
||||||
yaml.Unmarshal(cfgBytes, &p)
|
yaml.Unmarshal(cfgBytes, &p)
|
||||||
return p
|
return &p
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectRoot is the fully-qualified path to the root directory.
|
// ProjectRoot is the fully-qualified path to the root directory.
|
||||||
@ -95,3 +102,27 @@ func ProjectRoot() string {
|
|||||||
|
|
||||||
return projectRoot
|
return projectRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TicketSystem indicates the type of the configured ticket system
|
||||||
|
func (p *Project) TicketSystem() (string, error) {
|
||||||
|
if len(p.Tickets) > 1 {
|
||||||
|
return NoTickets, errors.New("multiple ticket systems configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range p.Tickets {
|
||||||
|
switch k {
|
||||||
|
case GitHub:
|
||||||
|
return GitHub, nil
|
||||||
|
case Jira:
|
||||||
|
return Jira, nil
|
||||||
|
case NoTickets:
|
||||||
|
return NoTickets, nil
|
||||||
|
default:
|
||||||
|
// explicit error for this case
|
||||||
|
return "", errors.New("unrecognized ticket system configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no ticket block configured
|
||||||
|
return NoTickets, nil
|
||||||
|
}
|
||||||
|
200
internal/jira/jira.go
Normal file
200
internal/jira/jira.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package jira
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/strongdm/comply/internal/model"
|
||||||
|
|
||||||
|
jira "github.com/andygrunwald/go-jira"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cfgUsername = "username"
|
||||||
|
cfgPassword = "password"
|
||||||
|
cfgURL = "url"
|
||||||
|
cfgProject = "project"
|
||||||
|
)
|
||||||
|
|
||||||
|
var prompts = map[string]string{
|
||||||
|
cfgUsername: "Jira Username",
|
||||||
|
cfgPassword: "Jira Password",
|
||||||
|
cfgURL: "Jira URL",
|
||||||
|
cfgProject: "Jira Project Code",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompts are human-readable configuration element names
|
||||||
|
func (j *jiraPlugin) Prompts() map[string]string {
|
||||||
|
return prompts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register causes the Github plugin to register itself
|
||||||
|
func Register() {
|
||||||
|
model.Register(model.Jira, &jiraPlugin{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type jiraPlugin struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
url string
|
||||||
|
project string
|
||||||
|
|
||||||
|
clientMu sync.Mutex
|
||||||
|
client *jira.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) api() *jira.Client {
|
||||||
|
j.clientMu.Lock()
|
||||||
|
defer j.clientMu.Unlock()
|
||||||
|
|
||||||
|
if j.client == nil {
|
||||||
|
tp := jira.BasicAuthTransport{
|
||||||
|
Username: j.username,
|
||||||
|
Password: j.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _ := jira.NewClient(tp.Client(), j.url)
|
||||||
|
j.client = client
|
||||||
|
}
|
||||||
|
return j.client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) Get(ID string) (*model.Ticket, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) Configured() bool {
|
||||||
|
return j.username != "" && j.password != "" && j.url != "" && j.project != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) Links() model.TicketLinks {
|
||||||
|
links := model.TicketLinks{}
|
||||||
|
links.AuditAll = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url)
|
||||||
|
links.AuditOpen = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url)
|
||||||
|
links.ProcedureAll = fmt.Sprintf("%s/issues?q=is%3Aissue+label%3Acomply+label%3Aprocedure", j.url)
|
||||||
|
links.ProcedureOpen = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aprocedure", j.url)
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) Configure(cfg map[string]interface{}) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if j.username, err = getCfg(cfg, cfgUsername); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if j.password, err = getCfg(cfg, cfgPassword); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if j.url, err = getCfg(cfg, cfgURL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if j.project, err = getCfg(cfg, cfgProject); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCfg(cfg map[string]interface{}, k string) (string, error) {
|
||||||
|
v, ok := cfg[k]
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("Missing key: " + k)
|
||||||
|
}
|
||||||
|
|
||||||
|
vS, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("Malformatted key: " + k)
|
||||||
|
}
|
||||||
|
return vS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) FindOpen() ([]*model.Ticket, error) {
|
||||||
|
return []*model.Ticket{}, nil
|
||||||
|
// issues, _, err := j.api().Issues.ListByRepo(context.Background(), j.username, j.reponame, &github.IssueListByRepoOptions{
|
||||||
|
// State: "open",
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, errors.Wrap(err, "error during FindOpen")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return toTickets(issues), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) FindByTag(name, value string) ([]*model.Ticket, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) FindByTagName(name string) ([]*model.Ticket, error) {
|
||||||
|
return []*model.Ticket{}, nil
|
||||||
|
// issues, _, err := j.api().Issues.ListByRepo(context.Background(), j.username, j.reponame, &github.IssueListByRepoOptions{
|
||||||
|
// State: "all",
|
||||||
|
// Labels: []string{name},
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, errors.Wrap(err, "error during FindOpen")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return toTickets(issues), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) LinkFor(t *model.Ticket) string {
|
||||||
|
// return fmt.Sprintf("https://github.com/strongdm/comply/issues/%s", t.ID)
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jiraPlugin) Create(ticket *model.Ticket, labels []string) error {
|
||||||
|
i := jira.Issue{
|
||||||
|
Fields: &jira.IssueFields{
|
||||||
|
Type: jira.IssueType{
|
||||||
|
Name: "Task",
|
||||||
|
},
|
||||||
|
Project: jira.Project{
|
||||||
|
Key: j.project,
|
||||||
|
},
|
||||||
|
Summary: ticket.Name,
|
||||||
|
Description: ticket.Body,
|
||||||
|
Labels: labels,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := j.api().Issue.Create(&i)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unable to create ticket")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTickets(issues []*jira.Issue) []*model.Ticket {
|
||||||
|
var tickets []*model.Ticket
|
||||||
|
for _, i := range issues {
|
||||||
|
tickets = append(tickets, toTicket(i))
|
||||||
|
}
|
||||||
|
return tickets
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTicket(i *jira.Issue) *model.Ticket {
|
||||||
|
t := &model.Ticket{Attributes: make(map[string]interface{})}
|
||||||
|
t.ID = i.ID
|
||||||
|
t.Name = i.Fields.Description
|
||||||
|
t.Body = i.Fields.Summary
|
||||||
|
createdAt := time.Time(i.Fields.Created)
|
||||||
|
t.CreatedAt = &createdAt
|
||||||
|
t.State = toState(i.Fields.Status)
|
||||||
|
|
||||||
|
for _, l := range i.Fields.Labels {
|
||||||
|
t.SetBool(l)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func toState(status *jira.Status) model.TicketState {
|
||||||
|
switch status.Name {
|
||||||
|
case "Closed":
|
||||||
|
return model.Closed
|
||||||
|
}
|
||||||
|
return model.Open
|
||||||
|
}
|
9
internal/jira/jira_test.go
Normal file
9
internal/jira/jira_test.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package jira
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJira(t *testing.T) {
|
||||||
|
createOne()
|
||||||
|
}
|
@ -17,11 +17,11 @@ type TicketSystem string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Jira from Atlassian.
|
// Jira from Atlassian.
|
||||||
Jira = TicketSystem("jira")
|
Jira = TicketSystem(config.Jira)
|
||||||
// GitHub from GitHub.
|
// GitHub from GitHub.
|
||||||
GitHub = TicketSystem("github")
|
GitHub = TicketSystem(config.GitHub)
|
||||||
// NoTickets indicates no ticketing system integration.
|
// NoTickets indicates no ticketing system integration.
|
||||||
NoTickets = TicketSystem("none")
|
NoTickets = TicketSystem(config.NoTickets)
|
||||||
)
|
)
|
||||||
|
|
||||||
type TicketLinks struct {
|
type TicketLinks struct {
|
||||||
|
@ -135,7 +135,8 @@ func (g *githubPlugin) FindByTagName(name string) ([]*model.Ticket, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *githubPlugin) LinkFor(t *model.Ticket) string {
|
func (g *githubPlugin) LinkFor(t *model.Ticket) string {
|
||||||
return fmt.Sprintf("https://github.com/strongdm/comply/issues/%s", t.ID)
|
// return fmt.Sprintf("https://github.com/strongdm/comply/issues/%s", t.ID)
|
||||||
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *githubPlugin) Create(ticket *model.Ticket, labels []string) error {
|
func (g *githubPlugin) Create(ticket *model.Ticket, labels []string) error {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/strongdm/comply/internal/config"
|
"github.com/strongdm/comply/internal/config"
|
||||||
"github.com/strongdm/comply/internal/model"
|
"github.com/strongdm/comply/internal/model"
|
||||||
)
|
)
|
||||||
@ -93,8 +94,12 @@ func load() (*model.Data, *renderData, error) {
|
|||||||
rd.Name = project.OrganizationName
|
rd.Name = project.OrganizationName
|
||||||
rd.Controls = controls
|
rd.Controls = controls
|
||||||
|
|
||||||
// TODO: unhardcode plugin
|
ts, err := config.Config().TicketSystem()
|
||||||
tp := model.GetPlugin(model.GitHub)
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "error in ticket system configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := model.GetPlugin(model.TicketSystem(ts))
|
||||||
if tp.Configured() {
|
if tp.Configured() {
|
||||||
links := tp.Links()
|
links := tp.Links()
|
||||||
rd.Links = &links
|
rd.Links = &links
|
||||||
|
@ -5,7 +5,9 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/robfig/cron"
|
"github.com/robfig/cron"
|
||||||
|
"github.com/strongdm/comply/internal/config"
|
||||||
"github.com/strongdm/comply/internal/model"
|
"github.com/strongdm/comply/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -68,7 +70,10 @@ func TriggerScheduled() error {
|
|||||||
// in the future, nothing to do
|
// in the future, nothing to do
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
trigger(procedure)
|
err = trigger(procedure)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// don't go back further than 13 months
|
// don't go back further than 13 months
|
||||||
tooOld := time.Now().Add(-1 * time.Hour * 24 * (365 + 30))
|
tooOld := time.Now().Add(-1 * time.Hour * 24 * (365 + 30))
|
||||||
@ -88,7 +93,10 @@ func TriggerScheduled() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// is in the past? then trigger.
|
// is in the past? then trigger.
|
||||||
trigger(procedure)
|
err = trigger(procedure)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
break SEARCH
|
break SEARCH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,13 +105,18 @@ func TriggerScheduled() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func trigger(procedure *model.Procedure) {
|
func trigger(procedure *model.Procedure) error {
|
||||||
fmt.Printf("triggering procedure %s (cron expression: %s)\n", procedure.Name, procedure.Cron)
|
fmt.Printf("triggering procedure %s (cron expression: %s)\n", procedure.Name, procedure.Cron)
|
||||||
|
|
||||||
// TODO: don't hardcode GH
|
ts, err := config.Config().TicketSystem()
|
||||||
tp := model.GetPlugin(model.GitHub)
|
if err != nil {
|
||||||
tp.Create(&model.Ticket{
|
return errors.Wrap(err, "error in ticket system configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := model.GetPlugin(model.TicketSystem(ts))
|
||||||
|
err = tp.Create(&model.Ticket{
|
||||||
Name: procedure.Name,
|
Name: procedure.Name,
|
||||||
Body: fmt.Sprintf("%s\n\n\n---\nProcedure-ID: %s", procedure.Body, procedure.ID),
|
Body: fmt.Sprintf("%s\n\n\n---\nProcedure-ID: %s", procedure.Body, procedure.ID),
|
||||||
}, []string{"comply", "comply-procedure"})
|
}, []string{"comply", "comply-procedure"})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user