ref: ef379bb73ac216792ac1601ec6589cbe09ccfeba
parent: 0c729fb8280bf2bc2dd12952873bd82f20247c69
author: Kenny Levinsen <w@kl.wtf>
date: Wed Jun 8 18:31:33 EDT 2016
Refactor
--- /dev/null
+++ b/client.go
@@ -1,0 +1,207 @@
+package main
+
+import (
+ "bytes"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ "github.com/mrjones/oauth"
+)
+
+type Client struct {
+ *http.Client
+
+ user, pass string
+ jiraURL *url.URL
+ cookies []*http.Cookie
+ alwaysLogin, usingOAuth bool
+
+ maxIssueListing int
+}
+
+type RPCError struct {
+ Status string
+ Body []byte
+ Description string
+}
+
+func (rpc *RPCError) Error() string {
+ return fmt.Sprintf("RPCError: %s: status %d, %s", rpc.Description, rpc.Status, rpc.Body)
+}
+
+func (c *Client) RPC(method, path string, body, target interface{}) error {
+ u, err := c.jiraURL.Parse(path)
+ if err != nil {
+ return err
+ }
+
+ var b io.Reader
+ if body != nil {
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return err
+ }
+ b = bytes.NewReader(buf)
+
+ }
+
+ req, err := http.NewRequest(method, u.String(), b)
+ if err != nil {
+ return err
+ }
+
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+ req.Header.Set("X-Atlassian-Token", "nocheck")
+
+ if c.alwaysLogin && !c.usingOAuth {
+ if err := c.AcquireSessionCookie(c.user, c.pass); err != nil {
+ return err
+ }
+ }
+
+ for _, cookie := range c.cookies {
+ req.AddCookie(cookie)
+ }
+
+ resp, err := c.Client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ respBody, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ resp.Body.Close()
+
+ if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
+ err = &RPCError{
+ Description: "request failed",
+ Status: resp.Status,
+ Body: respBody,
+ }
+ return err
+ }
+
+ if target != nil {
+ if err := json.Unmarshal(respBody, target); err != nil {
+ return err
+ }
+ }
+
+ return nil
+
+}
+
+func (c *Client) AcquireSessionCookie(username, password string) error {
+ url, err := c.jiraURL.Parse("/rest/auth/1/session")
+ if err != nil {
+ return err
+ }
+
+ body := struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ }{username, password}
+ b, err := json.Marshal(body)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("POST", url.String(), bytes.NewReader(b))
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := c.Client.Do(req)
+ if _, err := ioutil.ReadAll(resp.Body); err != nil {
+ return err
+ }
+ resp.Body.Close()
+ c.cookies = resp.Cookies()
+
+ if err != nil {
+ return fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
+ }
+ if resp != nil && resp.StatusCode != 200 {
+ return fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+func (c *Client) login() error {
+ if c.usingOAuth {
+ return nil
+ }
+ if err := c.AcquireSessionCookie(c.user, c.pass); err != nil {
+ return fmt.Errorf("Could not authenticate to JIRA: %v\n", err)
+ }
+ return nil
+}
+
+func (c *Client) oauth(consumerKey, privateKeyFile string) error {
+ pvf, err := ioutil.ReadFile(privateKeyFile)
+ if err != nil {
+ return err
+ }
+
+ block, _ := pem.Decode(pvf)
+ privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+ if err != nil {
+ return err
+ }
+
+ url1, _ := c.jiraURL.Parse("/plugins/servlet/oauth/request-token")
+ url2, _ := c.jiraURL.Parse("/plugins/servlet/oauth/authorize")
+ url3, _ := c.jiraURL.Parse("/plugins/servlet/oauth/access-token")
+
+ t := oauth.NewRSAConsumer(
+ consumerKey,
+ privateKey,
+ oauth.ServiceProvider{
+ RequestTokenUrl: url1.String(),
+ AuthorizeTokenUrl: url2.String(),
+ AccessTokenUrl: url3.String(),
+ HttpMethod: "POST",
+ },
+ )
+
+ t.HttpClient = &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+
+ requestToken, url, err := t.GetRequestTokenAndUrl("oob")
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("OAuth token requested. Please to go the following URL:\n\t%s\n\nEnter verification code: ", url)
+ var verificationCode string
+ fmt.Scanln(&verificationCode)
+ accessToken, err := t.AuthorizeToken(requestToken, verificationCode)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("OAuth token authorized.\n")
+
+ client, err := t.MakeHttpClient(accessToken)
+ if err != nil {
+ return err
+ }
+
+ c.Client = client
+ return nil
+}
--- /dev/null
+++ b/files.go
@@ -1,0 +1,189 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+
+ "github.com/joushou/qp"
+ "github.com/joushou/qptools/fileserver/trees"
+)
+
+type jiraWalker interface {
+ Walk(jc *Client, name string) (trees.File, error)
+}
+
+type jiraLister interface {
+ List(jc *Client) ([]qp.Stat, error)
+}
+
+type jiraRemover interface {
+ Remove(jc *Client, name string) error
+}
+
+// JiraDir is a convenience wrapper for dynamic directory hooks.
+type JiraDir struct {
+ thing interface{}
+ client *Client
+ *trees.SyntheticDir
+}
+
+func (jd *JiraDir) Walk(user, name string) (trees.File, error) {
+
+ if f, ok := jd.thing.(jiraWalker); ok {
+ return f.Walk(jd.client, name)
+ }
+ if f, ok := jd.thing.(trees.Dir); ok {
+ return f.Walk(user, name)
+ }
+
+ return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) List(user string) ([]qp.Stat, error) {
+ if f, ok := jd.thing.(jiraLister); ok {
+ return f.List(jd.client)
+ }
+ if f, ok := jd.thing.(trees.Lister); ok {
+ return f.List(user)
+ }
+
+ return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Remove(user, name string) error {
+ if f, ok := jd.thing.(jiraRemover); ok {
+ return f.Remove(jd.client, name)
+ }
+ if f, ok := jd.thing.(trees.Dir); ok {
+ return f.Remove(user, name)
+ }
+
+ return trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Create(user, name string, perms qp.FileMode) (trees.File, error) {
+ return nil, trees.ErrPermissionDenied
+}
+
+func (jd *JiraDir) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+ if !jd.CanOpen(user, mode) {
+ return nil, errors.New("access denied")
+ }
+
+ jd.Lock()
+ defer jd.Unlock()
+ jd.Atime = time.Now()
+ jd.Opens++
+ return &trees.ListHandle{
+ Dir: jd,
+ User: user,
+ }, nil
+}
+
+func NewJiraDir(name string, perm qp.FileMode, user, group string, jc *Client, thing interface{}) (*JiraDir, error) {
+ switch thing.(type) {
+ case trees.Dir, jiraWalker, jiraLister, jiraRemover:
+ default:
+ return nil, fmt.Errorf("unsupported type: %T", thing)
+ }
+
+ return &JiraDir{
+ thing: thing,
+ client: jc,
+ SyntheticDir: trees.NewSyntheticDir(name, perm, user, group),
+ }, nil
+}
+
+type CloseSaverHandle struct {
+ onClose func() error
+ trees.ReadWriteAtCloser
+}
+
+func (csh *CloseSaverHandle) Close() error {
+ err := csh.ReadWriteAtCloser.Close()
+ if err != nil {
+ return err
+ }
+
+ if csh.onClose != nil {
+ return csh.onClose()
+ }
+
+ return nil
+}
+
+// CloseSaver calls a callback on save if the file was opened for writing.
+type CloseSaver struct {
+ onClose func() error
+ trees.File
+}
+
+func (cs *CloseSaver) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+ hndl, err := cs.File.Open(user, mode)
+ if err != nil {
+ return nil, err
+ }
+
+ var closer func() error
+
+ switch mode & 3 {
+ case qp.OWRITE, qp.ORDWR:
+ closer = cs.onClose
+ }
+
+ return &CloseSaverHandle{
+ ReadWriteAtCloser: hndl,
+ onClose: closer,
+ }, nil
+}
+
+func NewCloseSaver(file trees.File, onClose func() error) trees.File {
+ return &CloseSaver{
+ onClose: onClose,
+ File: file,
+ }
+}
+
+// CommandFile calls commands on write.
+type CommandFile struct {
+ cmds map[string]func([]string) error
+ *trees.SyntheticFile
+}
+
+func (cf *CommandFile) Close() error { return nil }
+func (cf *CommandFile) ReadAt(p []byte, offset int64) (int, error) {
+ return 0, errors.New("cannot read from command file")
+}
+
+func (cf *CommandFile) WriteAt(p []byte, offset int64) (int, error) {
+ args := strings.Split(strings.Trim(string(p), " \n"), " ")
+ cmd := args[0]
+ args = args[1:]
+
+ if f, exists := cf.cmds[cmd]; exists {
+ err := f(args)
+ if err != nil {
+ log.Printf("Command %s failed: %v", cmd, err)
+ }
+ return len(p), err
+ }
+ return len(p), errors.New("no such command")
+}
+
+func (cf *CommandFile) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
+ if !cf.CanOpen(user, mode) {
+ return nil, trees.ErrPermissionDenied
+ }
+
+ return cf, nil
+}
+
+func NewCommandFile(name string, perms qp.FileMode, user, group string, cmds map[string]func([]string) error) *CommandFile {
+ return &CommandFile{
+ cmds: cmds,
+ SyntheticFile: trees.NewSyntheticFile(name, perms, user, group),
+ }
+}
--- a/jira.go
+++ b/jira.go
@@ -15,18 +15,6 @@
"github.com/joushou/qptools/fileserver/trees"
)
-type jiraWalker interface {
- Walk(jc *Client, name string) (trees.File, error)
-}
-
-type jiraLister interface {
- List(jc *Client) ([]qp.Stat, error)
-}
-
-type jiraRemover interface {
- Remove(jc *Client, name string) error
-}
-
type CommentView struct {
project string
issueNo string
@@ -98,7 +86,8 @@
}
func (iw *IssueView) normalFiles() (files, dirs []string) {
- files = []string{"assignee", "creator", "ctl", "description", "type", "key", "reporter", "status", "summary", "labels", "transitions", "priority", "resolution", "raw", "progress", "links", "components"}
+ files = []string{"assignee", "creator", "ctl", "description", "type", "key", "reporter", "status",
+ "summary", "labels", "transition", "priority", "resolution", "raw", "progress", "links", "components"}
dirs = []string{"comments"}
return
}
@@ -277,7 +266,7 @@
}
sf.SetContent([]byte(s))
}
- case "transitions":
+ case "transition":
trs, err := GetTransitionsForIssue(jc, issue.Key)
if err != nil {
log.Printf("Could not get transitions for issue %s: %v", issue.Key, err)
@@ -371,7 +360,7 @@
}
return nil
- case "transitions":
+ case "transition":
sf.Lock()
str := string(sf.Content)
sf.Unlock()
@@ -478,7 +467,7 @@
}
func (sw *SearchView) search(jc *Client) error {
- keys, err := GetKeysForSearch(jc, sw.query, 250)
+ keys, err := GetKeysForSearch(jc, sw.query, jc.maxIssueListing)
if err != nil {
return err
}
@@ -562,7 +551,7 @@
}
func (pw *ProjectView) List(jc *Client) ([]qp.Stat, error) {
- keys, err := GetKeysForNIssues(jc, pw.project, 250)
+ keys, err := GetKeysForNIssues(jc, pw.project, jc.maxIssueListing)
if err != nil {
log.Printf("Could not generate issue list: %v", err)
return nil, err
@@ -639,8 +628,28 @@
jw.searchLock.Unlock()
return nil
},
- "relogin": func(args []string) error {
+ "pass-login": func(args []string) error {
+ if len(args) == 2 {
+ jc.user = args[0]
+ jc.pass = args[1]
+ }
return jc.login()
+ },
+ "set": func(args []string) error {
+ if len(args) != 2 {
+ return errors.New("invalid arguments")
+ }
+ switch args[0] {
+ case "max-issues":
+ mi, err := strconv.ParseInt(args[1], 10, 64)
+ if err != nil {
+ return err
+ }
+ jc.maxIssueListing = int(mi)
+ return nil
+ default:
+ return errors.New("unknown variable")
+ }
},
}
return NewCommandFile("ctl", 0777, "jira", "jira", cmds), nil
--- a/main.go
+++ b/main.go
@@ -14,11 +14,14 @@
)
var (
- usingOAuth = flag.Bool("oauth", false, "use OAuth 1.0 for authorization")
- ckey = flag.String("ckey", "", "consumer key for OAuth")
- pkey = flag.String("pkey", "", "private key file for OAuth")
- pass = flag.Bool("pass", false, "use password for authorization")
- jiraURLStr = flag.String("url", "", "jira URL")
+ usingOAuth = flag.Bool("oauth", false, "use OAuth 1.0 for authorization")
+ ckey = flag.String("ckey", "", "consumer key for OAuth")
+ pkey = flag.String("pkey", "", "private key file for OAuth")
+ pass = flag.Bool("pass", false, "use password for authorization")
+ jiraURLStr = flag.String("url", "", "jira URL")
+ loginInt = flag.Int("loginint", 5, "login interval in minutes - 0 disables automatic relogin (password auth only)")
+ alwaysLogin = flag.Bool("alwayslogin", false, "log in on all requests (password auth only)")
+ maxIssues = flag.Int("maxissues", 100, "max issue listing")
)
func main() {
@@ -30,31 +33,39 @@
return
}
- client := &Client{Client: &http.Client{}, usingOAuth: *usingOAuth, jiraURL: jiraURL}
+ client := &Client{
+ Client: &http.Client{},
+ alwaysLogin: *alwaysLogin,
+ usingOAuth: *usingOAuth,
+ jiraURL: jiraURL,
+ maxIssueListing: *maxIssues,
+ }
switch {
case *pass:
- var user string
+ var username string
fmt.Printf("Username: ")
- _, err = fmt.Scanln(&user)
+ _, err = fmt.Scanln(&username)
if err == nil {
fmt.Printf("Password: ")
- pass, err := gopass.GetPasswdMasked()
+ password, err := gopass.GetPasswdMasked()
if err != nil {
- fmt.Printf("Could not read password: %v", err)
+ fmt.Printf("Could not read password: %v\n", err)
return
}
- client.user = user
- client.pass = string(pass)
+ client.user = username
+ client.pass = string(password)
client.login()
- go func() {
- t := time.NewTicker(5 * time.Minute)
- for range t.C {
- client.login()
- }
- }()
+ if *loginInt > 0 {
+ go func() {
+ t := time.NewTicker(time.Duration(*loginInt) * time.Minute)
+ for range t.C {
+ client.login()
+ }
+ }()
+ }
} else {
fmt.Printf("Continuing without authentication.\n")
}
@@ -63,11 +74,13 @@
fmt.Printf("Could not complete oauth handshake: %v\n", err)
return
}
+ default:
+ fmt.Printf("Continuing without authentication\n")
}
root, err := NewJiraDir("", 0555|qp.DMDIR, "jira", "jira", client, &JiraView{})
if err != nil {
- fmt.Printf("Could not create JIRA view")
+ fmt.Printf("Could not create JIRA view\n")
return
}
--- a/utils.go
+++ b/utils.go
@@ -1,26 +1,12 @@
package main
import (
- "bytes"
- "crypto/tls"
- "crypto/x509"
- "encoding/json"
- "encoding/pem"
- "errors"
"fmt"
- "io"
- "io/ioutil"
- "log"
- "net/http"
- "net/http/httputil"
"net/url"
"strings"
- "time"
"github.com/andygrunwald/go-jira"
"github.com/joushou/qp"
- "github.com/joushou/qptools/fileserver/trees"
- "github.com/mrjones/oauth"
)
type SearchResult struct {
@@ -28,28 +14,17 @@
}
func GetProjects(jc *Client) ([]jira.Project, error) {
- req, err := jc.NewRequest("GET", "/rest/api/2/project", nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
- }
-
var projects []jira.Project
- if _, err := jc.Do(req, &projects); err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
+ if err := jc.RPC("GET", "/rest/api/2/project", nil, &projects); err != nil {
+ return nil, fmt.Errorf("could not query projects: %v", err)
}
-
return projects, nil
}
func GetTypesForProject(jc *Client, project string) ([]string, error) {
- req, err := jc.NewRequest("GET", "/rest/api/2/issuetype", nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
- }
-
var types []jira.IssueType
- if _, err := jc.Do(req, &types); err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
+ if err := jc.RPC("GET", "/rest/api/2/issuetype", nil, &types); err != nil {
+ return nil, fmt.Errorf("could not query issue types: %v", err)
}
ss := make([]string, len(types))
@@ -61,16 +36,10 @@
}
func GetKeysForSearch(jc *Client, query string, max int) ([]string, error) {
- cmd := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=%s", max, url.QueryEscape(query))
-
- req, err := jc.NewRequest("GET", cmd, nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
- }
-
var s SearchResult
- if _, err := jc.Do(req, &s); err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=%s", max, url.QueryEscape(query))
+ if err := jc.RPC("GET", url, nil, &s); err != nil {
+ return nil, fmt.Errorf("could not execute search: %v", err)
}
ss := make([]string, len(s.Issues))
@@ -82,16 +51,10 @@
}
func GetKeysForNIssues(jc *Client, project string, max int) ([]string, error) {
- cmd := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=project=%s", max, project)
-
- req, err := jc.NewRequest("GET", cmd, nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
- }
-
var s SearchResult
- if _, err := jc.Do(req, &s); err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/search?fields=key&maxResults=%d&jql=project=%s", max, project)
+ if err := jc.RPC("GET", url, nil, &s); err != nil {
+ return nil, fmt.Errorf("could not execute search: %v", err)
}
ss := make([]string, len(s.Issues))
@@ -107,14 +70,10 @@
}
func GetIssue(jc *Client, key string) (*jira.Issue, error) {
- req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s", key), nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
- }
-
var i jira.Issue
- if _, err = jc.Do(req, &i); err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s", key)
+ if err := jc.RPC("GET", url, nil, &i); err != nil {
+ return nil, fmt.Errorf("could not query issue: %v", err)
}
return &i, nil
}
@@ -125,39 +84,26 @@
}
func CreateIssue(jc *Client, issue *jira.Issue) (string, error) {
- req, err := jc.NewRequest("POST", "/rest/api/2/issue", issue)
- if err != nil {
- return "", fmt.Errorf("could not query JIRA: %v", err)
- }
-
var cir CreateIssueResult
- if _, err = jc.Do(req, &cir); err != nil {
- return "", fmt.Errorf("could not query JIRA: %v", err)
+ if err := jc.RPC("POST", "/rest/api/2/issue", issue, &cir); err != nil {
+ return "", fmt.Errorf("could not create issue: %v", err)
}
return cir.Key, nil
}
func DeleteIssue(jc *Client, issue string) error {
- req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issue/%s", issue), nil)
- if err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s", issue)
+ if err := jc.RPC("DELETE", url, nil, nil); err != nil {
+ return fmt.Errorf("could not delete issue: %v", err)
}
-
- if _, err = jc.Do(req, nil); err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
- }
return nil
}
func DeleteIssueLink(jc *Client, issueLinkID string) error {
- req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issueLink/%s", issueLinkID), nil)
- if err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issueLink/%s", issueLinkID)
+ if err := jc.RPC("DELETE", url, nil, nil); err != nil {
+ return fmt.Errorf("could not delete issue link: %v", err)
}
-
- if _, err = jc.Do(req, nil); err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
- }
return nil
}
@@ -174,14 +120,9 @@
},
}
- req, err := jc.NewRequest("POST", "/rest/api/2/issueLink", issueLink)
- if err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
+ if err := jc.RPC("POST", "/rest/api/2/issueLink", issueLink, nil); err != nil {
+ return fmt.Errorf("could not create issue link: %v", err)
}
-
- if _, err = jc.Do(req, nil); err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
- }
return nil
}
@@ -196,16 +137,11 @@
}
func GetTransitionsForIssue(jc *Client, issue string) ([]Transition, error) {
- req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue), nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
- }
-
var tr TransitionResult
- if _, err = jc.Do(req, &tr); err != nil {
- return nil, fmt.Errorf("could no query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue)
+ if err := jc.RPC("GET", url, nil, &tr); err != nil {
+ return nil, fmt.Errorf("could not get transitions: %v", err)
}
-
return tr.Transitions, nil
}
@@ -232,16 +168,10 @@
"id": id,
},
}
-
- req, err := jc.NewRequest("POST", fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue), post)
- if err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s/transitions", issue)
+ if err := jc.RPC("POST", url, post, nil); err != nil {
+ return fmt.Errorf("could not transition issue: %v", err)
}
-
- if _, err = jc.Do(req, nil); err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
- }
-
return nil
}
@@ -251,7 +181,7 @@
field = "issuetype"
}
- cmd := fmt.Sprintf("/rest/api/2/issue/%s", issue)
+ url := fmt.Sprintf("/rest/api/2/issue/%s", issue)
method := "PUT"
var value interface{}
@@ -296,18 +226,10 @@
default:
fields[field] = value
}
- req, err := jc.NewRequest(method, cmd, post)
- if err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
- }
- b, _ := httputil.DumpRequestOut(req, true)
- log.Printf("Set field body: \n%s\n", b)
-
- if _, err = jc.Do(req, nil); err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
+ if err := jc.RPC(method, url, post, nil); err != nil {
+ return fmt.Errorf("could not set field for issue: %v", err)
}
-
return nil
}
@@ -316,14 +238,10 @@
}
func GetCommentsForIssue(jc *Client, issue string) ([]string, error) {
- req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/comment?maxResults=1000", issue), nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
- }
-
var cr CommentResult
- if _, err := jc.Do(req, &cr); err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s/comment?maxResults=1000", issue)
+ if err := jc.RPC("GET", url, nil, &cr); err != nil {
+ return nil, fmt.Errorf("could not get comments: %v", err)
}
var ss []string
@@ -335,16 +253,11 @@
}
func GetComment(jc *Client, issue, id string) (*jira.Comment, error) {
- req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
- }
-
var c jira.Comment
- if _, err := jc.Do(req, &c); err != nil {
- return nil, fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id)
+ if err := jc.RPC("GET", url, nil, &c); err != nil {
+ return nil, fmt.Errorf("could not get comment: %v", err)
}
-
return &c, nil
}
@@ -352,16 +265,10 @@
c := jira.Comment{
Body: body,
}
-
- req, err := jc.NewRequest("PUT", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), c)
- if err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id)
+ if err := jc.RPC("PUT", url, c, nil); err != nil {
+ return fmt.Errorf("could not set comment: %v", err)
}
-
- if _, err = jc.Do(req, nil); err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
- }
-
return nil
}
@@ -369,48 +276,21 @@
c := jira.Comment{
Body: body,
}
-
- req, err := jc.NewRequest("POST", fmt.Sprintf("/rest/api/2/issue/%s/comment/", issue), c)
- if err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s/comment/", issue)
+ if err := jc.RPC("POST", url, c, nil); err != nil {
+ return fmt.Errorf("could not add comment: %v", err)
}
-
- if _, err = jc.Do(req, nil); err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
- }
-
return nil
}
func RemoveComment(jc *Client, issue, id string) error {
- req, err := jc.NewRequest("DELETE", fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id), nil)
- if err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
+ url := fmt.Sprintf("/rest/api/2/issue/%s/comment/%s", issue, id)
+ if err := jc.RPC("DELETE", url, nil, nil); err != nil {
+ return fmt.Errorf("could not delete comment: %v", err)
}
-
- if _, err = jc.Do(req, nil); err != nil {
- return fmt.Errorf("could not query JIRA: %v", err)
- }
-
return nil
}
-type CanOpenAndLister interface {
- CanOpen(string, qp.OpenMode) bool
- trees.Lister
-}
-
-func OpenList(l CanOpenAndLister, user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
- if !l.CanOpen(user, mode) {
- return nil, errors.New("permission denied")
- }
-
- return &trees.ListHandle{
- Dir: l,
- User: user,
- }, nil
-}
-
func StringsToStats(strs []string, Perm qp.FileMode, user, group string) []qp.Stat {
var stats []qp.Stat
for _, str := range strs {
@@ -437,325 +317,4 @@
}
return false
-}
-
-type CloseSaverHandle struct {
- onClose func() error
- trees.ReadWriteAtCloser
-}
-
-func (csh *CloseSaverHandle) Close() error {
- err := csh.ReadWriteAtCloser.Close()
- if err != nil {
- return err
- }
-
- if csh.onClose != nil {
- return csh.onClose()
- }
-
- return nil
-}
-
-type CloseSaver struct {
- onClose func() error
- trees.File
-}
-
-func (cs *CloseSaver) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
- hndl, err := cs.File.Open(user, mode)
- if err != nil {
- return nil, err
- }
-
- var closer func() error
-
- switch mode & 3 {
- case qp.OWRITE, qp.ORDWR:
- closer = cs.onClose
- }
-
- return &CloseSaverHandle{
- ReadWriteAtCloser: hndl,
- onClose: closer,
- }, nil
-}
-
-func NewCloseSaver(file trees.File, onClose func() error) trees.File {
- return &CloseSaver{
- onClose: onClose,
- File: file,
- }
-}
-
-type CommandFile struct {
- cmds map[string]func([]string) error
- *trees.SyntheticFile
-}
-
-func (cf *CommandFile) Close() error { return nil }
-func (cf *CommandFile) ReadAt(p []byte, offset int64) (int, error) {
- return 0, errors.New("cannot read from command file")
-}
-
-func (cf *CommandFile) WriteAt(p []byte, offset int64) (int, error) {
- args := strings.Split(strings.Trim(string(p), " \n"), " ")
- cmd := args[0]
- args = args[1:]
-
- if f, exists := cf.cmds[cmd]; exists {
- err := f(args)
- if err != nil {
- log.Printf("Command %s failed: %v", cmd, err)
- }
- return len(p), err
- }
- return len(p), errors.New("no such command")
-}
-
-func (cf *CommandFile) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
- if !cf.CanOpen(user, mode) {
- return nil, trees.ErrPermissionDenied
- }
-
- return cf, nil
-}
-
-func NewCommandFile(name string, perms qp.FileMode, user, group string, cmds map[string]func([]string) error) *CommandFile {
- return &CommandFile{
- cmds: cmds,
- SyntheticFile: trees.NewSyntheticFile(name, perms, user, group),
- }
-}
-
-type JiraDir struct {
- thing interface{}
- client *Client
- *trees.SyntheticDir
-}
-
-func (jd *JiraDir) Walk(user, name string) (trees.File, error) {
-
- if f, ok := jd.thing.(jiraWalker); ok {
- return f.Walk(jd.client, name)
- }
- if f, ok := jd.thing.(trees.Dir); ok {
- return f.Walk(user, name)
- }
-
- return nil, trees.ErrPermissionDenied
-}
-
-func (jd *JiraDir) List(user string) ([]qp.Stat, error) {
- if f, ok := jd.thing.(jiraLister); ok {
- return f.List(jd.client)
- }
- if f, ok := jd.thing.(trees.Lister); ok {
- return f.List(user)
- }
-
- return nil, trees.ErrPermissionDenied
-}
-
-func (jd *JiraDir) Remove(user, name string) error {
- if f, ok := jd.thing.(jiraRemover); ok {
- return f.Remove(jd.client, name)
- }
- if f, ok := jd.thing.(trees.Dir); ok {
- return f.Remove(user, name)
- }
-
- return trees.ErrPermissionDenied
-}
-
-func (jd *JiraDir) Create(user, name string, perms qp.FileMode) (trees.File, error) {
- return nil, trees.ErrPermissionDenied
-}
-
-func (jd *JiraDir) Open(user string, mode qp.OpenMode) (trees.ReadWriteAtCloser, error) {
- if !jd.CanOpen(user, mode) {
- return nil, errors.New("access denied")
- }
-
- jd.Lock()
- defer jd.Unlock()
- jd.Atime = time.Now()
- jd.Opens++
- return &trees.ListHandle{
- Dir: jd,
- User: user,
- }, nil
-}
-
-func NewJiraDir(name string, perm qp.FileMode, user, group string, jc *Client, thing interface{}) (*JiraDir, error) {
- switch thing.(type) {
- case trees.Dir, jiraWalker, jiraLister, jiraRemover:
- default:
- return nil, fmt.Errorf("unsupported type: %T", thing)
- }
-
- return &JiraDir{
- thing: thing,
- client: jc,
- SyntheticDir: trees.NewSyntheticDir(name, perm, user, group),
- }, nil
-}
-
-type Client struct {
- user, pass string
- jiraURL *url.URL
- cookies []*http.Cookie
- usingOAuth bool
- *http.Client
-}
-
-func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
- rel, err := url.Parse(urlStr)
- if err != nil {
- return nil, err
- }
-
- u := c.jiraURL.ResolveReference(rel)
-
- var buf io.ReadWriter
- if body != nil {
- buf = new(bytes.Buffer)
- err := json.NewEncoder(buf).Encode(body)
- if err != nil {
- return nil, err
- }
- }
-
- req, err := http.NewRequest(method, u.String(), buf)
- if err != nil {
- return nil, err
- }
-
- req.Header.Set("Content-Type", "application/json")
-
- // Set session cookie if there is one
- for _, cookie := range c.cookies {
- req.AddCookie(cookie)
- }
-
- return req, nil
-}
-
-func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
- resp, err := c.Client.Do(req)
- if err != nil {
- return nil, err
- }
-
- respBody, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return resp, err
- }
-
- if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
- return resp, fmt.Errorf("Error (status code %d): %s", resp.StatusCode, respBody)
- }
-
- if v != nil {
- // Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
- defer resp.Body.Close()
- err = json.Unmarshal(respBody, v)
- }
-
- return resp, err
-}
-
-func (c *Client) AcquireSessionCookie(username, password string) (bool, error) {
- apiEndpoint := "rest/auth/1/session"
- body := struct {
- Username string `json:"username"`
- Password string `json:"password"`
- }{
- username,
- password,
- }
-
- req, err := c.NewRequest("POST", apiEndpoint, body)
- if err != nil {
- return false, err
- }
-
- resp, err := c.Do(req, nil)
- c.cookies = resp.Cookies()
-
- if err != nil {
- return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
- }
- if resp != nil && resp.StatusCode != 200 {
- return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
- }
-
- return true, nil
-}
-
-func (c *Client) login() error {
- if c.usingOAuth {
- return nil
- }
- res, err := c.AcquireSessionCookie(c.user, c.pass)
- if err != nil || res == false {
- return fmt.Errorf("Could not authenticate to JIRA: %v\n", err)
- }
- return nil
-}
-
-func (c *Client) oauth(consumerKey, privateKeyFile string) error {
- pvf, err := ioutil.ReadFile(privateKeyFile)
- if err != nil {
- return err
- }
-
- block, _ := pem.Decode(pvf)
- privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
- if err != nil {
- return err
- }
-
- url1, _ := c.jiraURL.Parse("/plugins/servlet/oauth/request-token")
- url2, _ := c.jiraURL.Parse("/plugins/servlet/oauth/authorize")
- url3, _ := c.jiraURL.Parse("/plugins/servlet/oauth/access-token")
-
- t := oauth.NewRSAConsumer(
- consumerKey,
- privateKey,
- oauth.ServiceProvider{
- RequestTokenUrl: url1.String(),
- AuthorizeTokenUrl: url2.String(),
- AccessTokenUrl: url3.String(),
- HttpMethod: "POST",
- },
- )
-
- t.HttpClient = &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
- },
- }
-
- requestToken, url, err := t.GetRequestTokenAndUrl("oob")
- if err != nil {
- return err
- }
-
- fmt.Printf("OAuth token requested. Please to go the following URL:\n\t%s\n\nEnter verification code: ", url)
- var verificationCode string
- fmt.Scanln(&verificationCode)
- accessToken, err := t.AuthorizeToken(requestToken, verificationCode)
- if err != nil {
- return err
- }
- fmt.Printf("OAuth token authorized.\n")
-
- client, err := t.MakeHttpClient(accessToken)
- if err != nil {
- return err
- }
-
- c.Client = client
- return nil
}
--- a/workflow.go
+++ b/workflow.go
@@ -12,24 +12,16 @@
}
func BuildWorkflow1(jc *Client, project, issueTypeNo string) (*WorkflowGraph, error) {
- req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA 1: %v", err)
- }
-
var t thing
- if _, err = jc.Do(req, &t); err != nil {
- return nil, fmt.Errorf("could not query JIRA 2: %v", err)
+ u := fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo)
+ if err := jc.RPC("GET", u, nil, &t); err != nil {
+ return nil, fmt.Errorf("could not query workflow for issue: %v", err)
}
- req, err = jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/workflow?workflowName=%s", url.QueryEscape(t.Name)), nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA 3: %v", err)
- }
-
var wr WorkflowResponse1
- if _, err = jc.Do(req, &wr); err != nil {
- return nil, fmt.Errorf("could not query JIRA 4: %v", err)
+ u = fmt.Sprintf("/rest/projectconfig/latest/workflow?workflowName=%s", url.QueryEscape(t.Name))
+ if err := jc.RPC("GET", u, nil, &wr); err != nil {
+ return nil, fmt.Errorf("could not query workflow graph: %v", err)
}
var wg WorkflowGraph
@@ -38,26 +30,16 @@
}
func BuildWorkflow2(jc *Client, project, issueTypeNo string) (*WorkflowGraph, error) {
- req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA 1: %v", err)
- }
-
var t thing
- if _, err = jc.Do(req, &t); err != nil {
- return nil, fmt.Errorf("could not query JIRA 2: %v", err)
+ u := fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo)
+ if err := jc.RPC("GET", u, nil, &t); err != nil {
+ return nil, fmt.Errorf("could not query workflow for issue: %v", err)
}
- req, err = jc.NewRequest("GET", fmt.Sprintf("/rest/workflowDesigner/latest/workflows?name=%s", url.QueryEscape(t.Name)), nil)
- if err != nil {
- return nil, fmt.Errorf("could not query JIRA 3: %v", err)
- }
-
- req.Header.Set("X-Atlassian-Token", "nocheck")
-
var wr WorkflowResponse2
- if _, err = jc.Do(req, &wr); err != nil {
- return nil, fmt.Errorf("could not query JIRA 4: %v", err)
+ u = fmt.Sprintf("/rest/workflowDesigner/latest/workflows?name=%s", url.QueryEscape(t.Name))
+ if err := jc.RPC("GET", u, nil, &wr); err != nil {
+ return nil, fmt.Errorf("could not query workflow graph: %v", err)
}
var wg WorkflowGraph