initial checkin, work in progress

This commit is contained in:
Cory Bennett
2015-02-10 16:17:13 -08:00
parent 1260624c7c
commit 6936b27ea1
6 changed files with 466 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
package cli
import (
"github.com/op/go-logging"
"net/http"
"net/http/cookiejar"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"net/url"
"time"
"io"
"runtime"
)
var log = logging.MustGetLogger("jira.cli")
type Cli struct {
endpoint *url.URL
opts map[string]string
cookieFile string
ua *http.Client
}
func New(opts map[string]string) *Cli {
homedir := os.Getenv("HOME")
cookieJar, _ := cookiejar.New(nil)
endpoint, _ := opts["endpoint"]
url, _ := url.Parse(endpoint)
cli := &Cli{
endpoint: url,
opts: opts,
cookieFile: fmt.Sprintf("%s/.jira.d/cookies.js", homedir),
ua: &http.Client{Jar: cookieJar},
}
cli.ua.Jar.SetCookies(url, cli.loadCookies())
return cli
}
func (c *Cli) saveCookies(cookies []*http.Cookie) {
// expiry in one week from now
expiry := time.Now().Add(24 * 7 * time.Hour)
for _, cookie := range cookies {
cookie.Expires = expiry
}
if currentCookies := c.loadCookies(); currentCookies != nil {
currentCookiesByName := make(map[string]*http.Cookie)
for _, cookie := range currentCookies {
currentCookiesByName[cookie.Name] = cookie
}
for _, cookie := range cookies {
currentCookiesByName[cookie.Name] = cookie
}
mergedCookies := make([]*http.Cookie, 0, len(currentCookiesByName))
for _, v := range currentCookiesByName {
mergedCookies = append(mergedCookies, v)
}
jsonWrite(c.cookieFile, mergedCookies)
} else {
jsonWrite(c.cookieFile, cookies)
}
}
func (c *Cli) loadCookies() []*http.Cookie {
bytes, err := ioutil.ReadFile(c.cookieFile)
if err != nil && os.IsNotExist(err) {
// dont load cookies if the file does not exist
return nil
}
if err != nil {
log.Error("Failed to open %s: %s", c.cookieFile, err)
os.Exit(1)
}
cookies := make([]*http.Cookie,0)
err = json.Unmarshal(bytes, &cookies)
if err != nil {
log.Error("Failed to parse json from file %s: %s", c.cookieFile, err)
}
log.Debug("Loading Cookies: %s", cookies)
return cookies
}
func (c *Cli) post(uri string, content io.Reader) *http.Response {
req, _ := http.NewRequest("POST", uri, content)
return c.makeRequest(req)
}
func (c *Cli) get(uri string) *http.Response {
req, _ := http.NewRequest("GET", uri, nil)
return c.makeRequest(req)
}
func (c *Cli) makeRequest(req *http.Request) *http.Response {
req.Header.Set("Content-Type", "application/json")
resp, err := c.ua.Do(req)
if err != nil {
fmt.Printf("Error: %s", err)
}
if resp.StatusCode != 200 {
log.Error("response status: %s", resp.Status)
resp.Write(os.Stderr)
}
runtime.SetFinalizer(resp, func(r *http.Response) {
r.Body.Close()
})
if _, ok := resp.Header["Set-Cookie"]; ok {
c.saveCookies(resp.Cookies())
}
return resp
}
+94
View File
@@ -0,0 +1,94 @@
package cli
import (
"net/http"
"encoding/json"
"fmt"
"bytes"
"os"
"code.google.com/p/gopass"
)
func (c *Cli) CmdLogin() {
uri := fmt.Sprintf("%s/rest/auth/1/session", c.endpoint)
resp := c.get(uri)
for ; resp.StatusCode != 200 ; {
req, _ := http.NewRequest("GET", uri, nil)
user, _ := c.opts["user"]
prompt := fmt.Sprintf("Enter Password for %s: ", user)
passwd, _ := gopass.GetPass(prompt);
req.SetBasicAuth(user, passwd)
resp = c.makeRequest(req)
if resp.StatusCode == 403 {
// probably got this, need to redirect the user to login manually
// X-Authentication-Denied-Reason: CAPTCHA_CHALLENGE; login-url=https://jira/login.jsp
if reason := resp.Header.Get("X-Authentication-Denied-Reason"); reason != "" {
log.Error("Authentication Failed: %s", reason)
os.Exit(1)
}
log.Error("Authentication Failead: Unknown")
os.Exit(1)
}
if resp.StatusCode != 200 {
log.Error("Login failed")
}
}
}
func (c *Cli) CmdFields() {
log.Debug("fields called")
resp := c.get(fmt.Sprintf("%s/rest/api/2/field", c.endpoint))
data := jsonDecode(resp.Body)
if templateFile, err := FindClosestParentPath(".jira.d/templates/fields"); err != nil {
runTemplate(default_fields_template, data)
} else {
log.Debug("Using Template: %s", templateFile)
runTemplate(readFile(templateFile), data)
}
}
func (c *Cli) CmdList() {
log.Debug("list called")
if query, ok := c.opts["query"]; !ok {
log.Error("No query argument found, either use --query or set query attribute in .jira file")
os.Exit(1)
} else {
buffer := bytes.NewBuffer(make([]byte, 0, len(query)))
enc := json.NewEncoder(buffer)
enc.Encode(map[string]string{
"jql": query,
"startAt": "0",
"maxResults": "500",
})
resp := c.post(fmt.Sprintf("%s/rest/api/2/search", c.endpoint), buffer)
data := jsonDecode(resp.Body)
if templateFile, err := FindClosestParentPath(".jira.d/templates/list"); err != nil {
runTemplate(default_list_template, data)
} else {
log.Debug("Using Template: %s", templateFile)
runTemplate(readFile(templateFile), data)
}
}
}
func (c *Cli) CmdView(issue string) {
log.Debug("view called")
resp := c.get(fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue))
data := jsonDecode(resp.Body)
if templateFile, err := FindClosestParentPath(".jira.d/templates/view"); err != nil {
runTemplate(default_view_template, data)
} else {
log.Debug("Using Template: %s", templateFile)
runTemplate(readFile(templateFile), data)
}
}
+24
View File
@@ -0,0 +1,24 @@
package cli
const default_fields_template = "{{ . | toJson}}\n"
const default_list_template = "{{ range .issues }}{{ .key | append \":\" | printf \"%-12s\"}} {{ .fields.summary }}\n{{ end }}"
const default_view_template = `issue: {{ .key }}
summary: {{ .fields.summary }}
project: {{ .fields.project.key }}
components: {{ range .fields.components }}{{ .name }} {{end}}
issuetype: {{ .fields.issuetype.name }}
assignee: {{ .fields.assignee.name }}
reporter: {{ .fields.reporter.name }}
watchers: {{ range .fields.customfield_10110 }}{{ .name }} {{end}}
blockers: {{ range .fields.issuelinks }}{{if .outwardIssue}}{{ .outwardIssue.key }}{{end}}{{end}}
depends: {{ range .fields.issuelinks }}{{if .inwardIssue}}{{ .inwardIssue.key }}{{end}}{{end}}
priority: {{ .fields.priority.name }}
description: |
{{ .fields.description | indent 2 }}
comments:
{{ range .fields.comment.comments }} - | # {{.author.name}} at {{.created}}
{{ .body | indent 4}}{{end}}
`
+113
View File
@@ -0,0 +1,113 @@
package cli
import (
"os"
"fmt"
"errors"
"strings"
"encoding/json"
"io/ioutil"
"text/template"
"io"
"github.com/mgutz/ansi"
)
func FindParentPaths(fileName string) []string {
cwd, _ := os.Getwd()
paths := make([]string,0)
var dir string
for _, part := range strings.Split(cwd, string(os.PathSeparator)) {
if dir == "/" {
dir = fmt.Sprintf("/%s", part)
} else {
dir = fmt.Sprintf("%s/%s", dir, part)
}
file := fmt.Sprintf("%s/%s", dir, fileName)
if _, err := os.Stat(file); err == nil {
paths = append(paths, file)
}
}
return paths
}
func FindClosestParentPath(fileName string) (string,error) {
paths := FindParentPaths(fileName)
if len(paths) > 0 {
return paths[len(paths)-1], nil
}
return "", errors.New(fmt.Sprintf("%s not found in parent directory hierarchy", fileName))
}
func readFile(file string) string {
var bytes []byte
var err error
if bytes, err = ioutil.ReadFile(file); err != nil {
log.Error("Failed to read file %s: %s", file, err)
os.Exit(1)
}
return string(bytes)
}
func runTemplate(text string, data interface{}) {
funcs := map[string]interface{}{
"toJson": func(content interface{}) (string, error) {
if bytes, err := json.MarshalIndent(content, "", " "); err != nil {
return "", err
} else {
return string(bytes), nil
}
},
"append": func(more string, content interface{}) (string, error) {
switch value := content.(type) {
case string: return string(append([]byte(content.(string)), []byte(more)...)), nil
case []byte: return string(append(content.([]byte), []byte(more)...)), nil
default: return "", errors.New(fmt.Sprintf("Unknown type: %s", value))
}
},
"indent": func(spaces int, content string) string {
indent := make([]byte, spaces + 1, spaces +1)
indent[0] = '\n'
for i := 1; i < spaces + 1; i += 1 {
indent[i] = ' '
}
return strings.Replace(content, "\n", string(indent), -1)
},
"color": func(color string) string {
return ansi.ColorCode(color)
},
}
if tmpl, err := template.New("template").Funcs(funcs).Parse(text); err != nil {
log.Error("Failed to parse template: %s", err)
os.Exit(1)
} else {
if err := tmpl.Execute(os.Stdout, data); err != nil {
log.Error("Failed to execute template: %s", err)
os.Exit(1)
}
}
}
func jsonDecode(io io.Reader) interface{} {
content, err := ioutil.ReadAll(io)
var data interface{}
err = json.Unmarshal(content, &data)
if err != nil {
log.Error("JSON Parse Error: %s from %s", err, content)
}
return data
}
func jsonWrite(file string, data interface{}) {
fh, err := os.OpenFile(file, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0600)
defer fh.Close()
if err != nil {
log.Error("Failed to open %s: %s", file, err)
os.Exit(1)
}
enc := json.NewEncoder(fh)
enc.Encode(data)
}
+111
View File
@@ -0,0 +1,111 @@
package main
import (
"github.com/docopt/docopt-go"
"github.com/op/go-logging"
"fmt"
"os"
"strings"
"io/ioutil"
"gopkg.in/yaml.v2"
"github.com/Netflix-Skunkworks/go-jira/jira/cli"
)
var log = logging.MustGetLogger("jira")
var format = "%{color}%{time:2006-01-02T15:04:05.000Z07:00} %{level:-5s} [%{shortfile}]%{color:reset} %{message}"
func parseYaml(file string, opts map[string]string) {
if fh, err := ioutil.ReadFile(file); err == nil {
log.Debug("Found Config file: %s", file)
yaml.Unmarshal(fh, &opts)
}
}
func loadConfigs(opts map[string]string) {
paths := cli.FindParentPaths(".jira")
// prepend
paths = append([]string{"/etc/jira-cli.yml"}, paths...)
for _, file := range(paths) {
parseYaml(file, opts)
}
}
func main() {
user := os.Getenv("USER")
usage := fmt.Sprintf(`
Usage:
jira [-v ...] [-u USER] [-e URI] [-t FILE] fields
jira [-v ...] [-u USER] [-e URI] [-t FILE] ls [--query=JQL]
jira [-v ...] [-u USER] [-e URI] [-t FILE] view ISSUE
jira [-v ...] [-u USER] [-e URI] [-t FILE] ISSUE
General Options:
-h --help Show this usage
--version Show this version
-v --verbose Increase output logging
-u --user=USER Username to use for authenticaion (default: %s)
-e --endpoint=URI URI to use for jira (default: https://jira)
-t --template=FILE Template file to use for output
List options:
-q --query=FILE Template to use for output
`, user)
args, _ := docopt.Parse(usage, nil, true, "0.0.1", false, false)
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
logging.SetBackend(
logging.NewBackendFormatter(
logBackend,
logging.MustStringFormatter(format),
),
)
logging.SetLevel(logging.NOTICE, "")
if verbose, ok := args["--verbose"]; ok {
if verbose.(int) > 1 {
logging.SetLevel(logging.DEBUG, "")
} else if verbose.(int) > 0 {
logging.SetLevel(logging.INFO, "")
}
}
log.Info("Args: %v", args)
opts := make(map[string]string)
loadConfigs(opts)
for key,val := range args {
if val != nil && strings.HasPrefix(key, "--") {
opt := key[2:]
switch v := val.(type) {
case string:
opts[opt] = v
}
}
}
if _, ok := opts["endpoint"]; !ok {
opts["endpoint"] = "https://jira"
}
if _, ok := opts["user"]; !ok {
opts["user"] = user
}
c := cli.New(opts)
log.Debug("opts: %s", opts);
c.CmdLogin()
if val, ok := args["fields"]; ok && val.(bool) {
c.CmdFields()
} else if val, ok := args["ls"]; ok && val.(bool) {
c.CmdList()
} else if val, ok := args["ISSUE"]; ok {
c.CmdView(val.(string))
}
os.Exit(0)
}
View File