未验证 提交 1cc95bfa 编写于 作者: LinuxSuRen's avatar LinuxSuRen 提交者: GitHub

Enhance the searching Jenkins job (#284)

* Add field filter for job search

* Add support to output a common table by reflect way

* Add output and filter as generic method

* Fix the test error of casc cmd

* Keep job path is compitable
上级 7f7f8c08
......@@ -31,4 +31,7 @@ var cascCmd = &cobra.Command{
Use: "casc",
Short: i18n.T("Configuration as Code"),
Long: i18n.T("Configuration as Code"),
Annotations: map[string]string{
since: "v0.0.24",
},
}
package cmd
import (
"github.com/jenkins-zh/jenkins-cli/client"
"net/http"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/spf13/cobra"
......@@ -33,4 +34,7 @@ var cascApplyCmd = &cobra.Command{
getCurrentJenkinsAndClientOrDie(&(jClient.JenkinsCore))
return jClient.Apply()
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
package cmd
import (
"github.com/jenkins-zh/jenkins-cli/client"
"net/http"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/spf13/cobra"
......@@ -38,4 +39,7 @@ var cascExportCmd = &cobra.Command{
}
return
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
......@@ -28,4 +28,7 @@ var cascOpenCmd = &cobra.Command{
jenkins := getCurrentJenkinsFromOptionsOrDie()
return util.Open(fmt.Sprintf("%s/configuration-as-code", jenkins.URL), cascOpenOption.ExecContext)
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
package cmd
import (
"github.com/jenkins-zh/jenkins-cli/client"
"net/http"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/spf13/cobra"
......@@ -33,4 +34,7 @@ var cascReloadCmd = &cobra.Command{
getCurrentJenkinsAndClientOrDie(&(jClient.JenkinsCore))
return jClient.Reload()
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
package cmd
import (
"github.com/jenkins-zh/jenkins-cli/client"
"net/http"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/spf13/cobra"
......@@ -38,4 +39,7 @@ var cascSchemaCmd = &cobra.Command{
}
return
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
package cmd
import (
"io/ioutil"
"os"
"github.com/jenkins-zh/jenkins-cli/client"
......@@ -31,40 +30,41 @@ var _ = Describe("casc command check", func() {
rootURL = "http://localhost:8080/jenkins"
user = "admin"
password = "111e3a2f0231198855dceaff96f20540a9"
config = &Config{
Current: "fake",
JenkinsServers: []JenkinsServer{JenkinsServer{
Name: "fake",
URL: rootURL,
UserName: user,
Token: password,
}},
}
})
AfterEach(func() {
rootCmd.SetArgs([]string{})
os.Remove(rootOptions.ConfigFile)
rootOptions.ConfigFile = ""
config = nil
ctrl.Finish()
})
Context("basic cases", func() {
It("without casc plugin", func() {
data, err := generateSampleConfig()
Expect(err).To(BeNil())
err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664)
Expect(err).To(BeNil())
req, _ := client.PrepareForOneInstalledPlugin(roundTripper, rootURL)
req.SetBasicAuth(user, password)
err = cascOptions.Check()
err := cascOptions.Check()
Expect(err).To(HaveOccurred())
})
It("with casc plugin", func() {
data, err := generateSampleConfig()
Expect(err).To(BeNil())
err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664)
Expect(err).To(BeNil())
req, _ := client.PrepareForOneInstalledPluginWithPluginName(roundTripper, rootURL,
"configuration-as-code")
req.SetBasicAuth(user, password)
err = cascOptions.Check()
err := cascOptions.Check()
Expect(err).NotTo(HaveOccurred())
})
})
......
......@@ -3,14 +3,21 @@ package cmd
import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
"io"
"net/http"
"reflect"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/jenkins-zh/jenkins-cli/util"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
)
const (
since = "since"
)
// CommonOption contains the common options
......@@ -25,7 +32,11 @@ type CommonOption struct {
type OutputOption struct {
Format string
Columns string
WithoutHeaders bool
Filter []string
Writer io.Writer
}
// FormatOutput is the interface of format output
......@@ -43,6 +54,7 @@ const (
)
// Output print the object into byte array
// Deprecated see also OutputV2
func (o *OutputOption) Output(obj interface{}) (data []byte, err error) {
switch o.Format {
case JSONOutputFormat:
......@@ -54,6 +66,96 @@ func (o *OutputOption) Output(obj interface{}) (data []byte, err error) {
return nil, fmt.Errorf("not support format %s", o.Format)
}
// OutputV2 print the data line by line
func (o *OutputOption) OutputV2(obj interface{}) (err error) {
if o.Writer == nil {
err = fmt.Errorf("no writer found")
return
}
if len(o.Columns) == 0 {
err = fmt.Errorf("no columns found")
return
}
obj = o.ListFilter(obj)
var data []byte
switch o.Format {
case JSONOutputFormat:
data, err = json.MarshalIndent(obj, "", " ")
case YAMLOutputFormat:
data, err = yaml.Marshal(obj)
case TableOutputFormat, "":
table := util.CreateTableWithHeader(o.Writer, o.WithoutHeaders)
table.AddHeader(strings.Split(o.Columns, ",")...)
items := reflect.ValueOf(obj)
for i := 0; i < items.Len(); i++ {
table.AddRow(o.GetLine(items.Index(i))...)
}
table.Render()
default:
err = fmt.Errorf("not support format %s", o.Format)
}
if err == nil && len(data) > 0 {
_, err = o.Writer.Write(data)
}
return
}
// ListFilter filter the data list by fields
func (o *OutputOption) ListFilter(obj interface{}) interface{} {
if len(o.Filter) == 0 {
return obj
}
elemType := reflect.TypeOf(obj).Elem()
elemSlice := reflect.MakeSlice(reflect.SliceOf(elemType), 0, 10)
items := reflect.ValueOf(obj)
for i := 0; i < items.Len(); i++ {
item := items.Index(i)
if o.Match(item) {
elemSlice = reflect.Append(elemSlice, item)
}
}
return elemSlice.Interface()
}
// Match filter an item
func (o *OutputOption) Match(item reflect.Value) bool {
if len(o.Filter) == 0 {
return true
}
for _, f := range o.Filter {
arr := strings.Split(f, "=")
if len(arr) < 2 {
continue
}
key := arr[0]
val := arr[1]
if !strings.Contains(util.ReflectFieldValueAsString(item, key), val) {
continue
} else {
return true
}
}
return false
}
// GetLine returns the line of a table
func (o *OutputOption) GetLine(obj reflect.Value) []string {
columns := strings.Split(o.Columns, ",")
values := make([]string, 0)
for _, col := range columns {
values = append(values, util.ReflectFieldValueAsString(obj, col))
}
return values
}
// SetFlag set flag of output format
func (o *OutputOption) SetFlag(cmd *cobra.Command) {
cmd.Flags().StringVarP(&o.Format, "output", "o", TableOutputFormat, "Format the output, supported formats: table, json, yaml")
......
package cmd_test
import (
"bytes"
"github.com/jenkins-zh/jenkins-cli/app/cmd"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"reflect"
)
var _ = Describe("test OutputOption", func() {
var (
outputOption cmd.OutputOption
fakeFoos []FakeFoo
)
BeforeEach(func() {
outputOption = cmd.OutputOption{}
fakeFoos = []FakeFoo{{
Name: "fake",
}, {
Name: "foo-1",
}}
})
Context("ListFilter test", func() {
var (
result interface{}
)
JustBeforeEach(func() {
result = outputOption.ListFilter(fakeFoos)
})
It("without filter", func() {
Expect(result).To(Equal(fakeFoos))
})
Context("with filter", func() {
BeforeEach(func() {
outputOption = cmd.OutputOption{
Filter: []string{"Name=fake"},
}
})
It("should success", func() {
Expect(result).NotTo(Equal(fakeFoos))
})
})
})
Context("OutputV2 test", func() {
var (
err error
)
JustBeforeEach(func() {
err = outputOption.OutputV2(fakeFoos)
})
It("without io writer", func() {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("writer"))
})
Context("without columns", func() {
BeforeEach(func() {
outputOption.Writer = new(bytes.Buffer)
})
It("get no columns error", func() {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("columns"))
})
})
Context("with io writer", func() {
var (
buf *bytes.Buffer
)
BeforeEach(func() {
buf = new(bytes.Buffer)
outputOption.Writer = buf
outputOption.Columns = "Name"
})
It("with the default output format", func() {
Expect(buf.String()).To(Equal(`Name
fake
foo-1
`))
})
Context("with json format", func() {
BeforeEach(func() {
outputOption.Format = cmd.JSONOutputFormat
})
It("should get a json text", func() {
Expect(buf.String()).To(Equal(`[
{
"Name": "fake"
},
{
"Name": "foo-1"
}
]`))
})
})
Context("with yaml format", func() {
BeforeEach(func() {
outputOption.Format = cmd.YAMLOutputFormat
})
It("should get a yaml text", func() {
Expect(buf.String()).To(Equal(`- name: fake
- name: foo-1
`))
})
})
Context("with a unknown format", func() {
BeforeEach(func() {
outputOption.Format = "fake"
})
It("should get an error", func() {
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not support format"))
})
})
})
})
Context("Match test", func() {
var (
result bool
)
JustBeforeEach(func() {
result = outputOption.Match(reflect.ValueOf(fakeFoos[0]))
})
It("without filter", func() {
Expect(result).To(BeTrue())
})
Context("invalid filter", func() {
BeforeEach(func() {
outputOption = cmd.OutputOption{
Filter: []string{"Name"},
}
})
It("not matched", func() {
Expect(result).To(BeFalse())
})
})
})
})
// FakeFoo only for test
type FakeFoo struct {
Name string
}
......@@ -14,4 +14,7 @@ var credentialCmd = &cobra.Command{
Aliases: []string{"secret", "cred"},
Short: i18n.T("Manage the credentials of your Jenkins"),
Long: i18n.T(`Manage the credentials of your Jenkins`),
Annotations: map[string]string{
since: "v0.0.24",
},
}
......@@ -2,10 +2,11 @@ package cmd
import (
"fmt"
"net/http"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/spf13/cobra"
"net/http"
)
// CredentialCreateOption option for credential delete command
......@@ -90,4 +91,7 @@ var credentialCreateCmd = &cobra.Command{
}
return
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
......@@ -2,10 +2,11 @@ package cmd
import (
"fmt"
"net/http"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/spf13/cobra"
"net/http"
)
// CredentialDeleteOption option for credential delete command
......@@ -63,4 +64,7 @@ var credentialDeleteCmd = &cobra.Command{
err = jClient.Delete(credentialDeleteOption.Store, credentialDeleteOption.ID)
return
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
......@@ -3,9 +3,10 @@ package cmd
import (
"bytes"
"fmt"
"github.com/jenkins-zh/jenkins-cli/client"
"net/http"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/jenkins-zh/jenkins-cli/util"
......@@ -51,6 +52,9 @@ var credentialListCmd = &cobra.Command{
}
return
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
// Output render data into byte array as a table format
......
package cmd
import (
"bytes"
"fmt"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/jenkins-zh/jenkins-cli/util"
"net/http"
"github.com/hashicorp/go-version"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/spf13/cobra"
"net/http"
)
// JobSearchOption is the options of job search command
......@@ -35,6 +32,10 @@ func init() {
i18n.T("The name of plugin for search"))
jobSearchCmd.Flags().StringVarP(&jobSearchOption.Type, "type", "", "",
i18n.T("The type of plugin for search"))
jobSearchCmd.Flags().StringVarP(&jobSearchOption.Columns, "columns", "", "Name,DisplayName,Type,URL",
i18n.T("The columns of table"))
jobSearchCmd.Flags().StringArrayVarP(&jobSearchOption.Filter, "filter", "", []string{},
i18n.T("Filter for the list"))
jobSearchOption.SetFlag(jobSearchCmd)
healthCheckRegister.Register(getCmdPath(jobSearchCmd), &jobSearchOption)
......@@ -61,32 +62,13 @@ var jobSearchCmd = &cobra.Command{
var items []client.JenkinsItem
if items, err = jClient.Search(jobSearchOption.Name, jobSearchOption.Type,
jobSearchOption.Start, jobSearchOption.Limit); err == nil {
var data []byte
if data, err = jobSearchOption.Output(items); err == nil {
cmd.Print(string(data))
}
jobSearchOption.Writer = cmd.OutOrStdout()
err = jobSearchOption.OutputV2(items)
}
return
},
}
// Output render data into byte array
func (o *JobSearchOption) Output(obj interface{}) (data []byte, err error) {
if data, err = o.OutputOption.Output(obj); err != nil {
items := obj.([]client.JenkinsItem)
buf := new(bytes.Buffer)
table := util.CreateTableWithHeader(buf, o.WithoutHeaders)
table.AddRow("number", "name", "displayname", "type", "url")
for i, item := range items {
table.AddRow(fmt.Sprintf("%d", i), item.Name, item.DisplayName, item.Type, item.URL)
}
table.Render()
data = buf.Bytes()
err = nil
}
return
}
// Check do the conditions check
func (o *JobSearchOption) Check() (err error) {
opt := PluginOptions{
......
......@@ -58,8 +58,8 @@ var _ = Describe("job search command", func() {
_, err = rootCmd.ExecuteC()
Expect(err).To(BeNil())
Expect(buf.String()).To(Equal(`number name displayname type url
0 fake fake WorkflowJob job/fake/
Expect(buf.String()).To(Equal(`Name DisplayName Type URL
fake fake WorkflowJob job/fake/
`))
})
......@@ -79,8 +79,8 @@ var _ = Describe("job search command", func() {
_, err = rootCmd.ExecuteC()
Expect(err).To(BeNil())
Expect(buf.String()).To(Equal(`number name displayname type url
0 fake fake WorkflowJob job/fake/
Expect(buf.String()).To(Equal(`Name DisplayName Type URL
fake fake WorkflowJob job/fake/
`))
})
})
......@@ -104,9 +104,20 @@ var _ = Describe("job search command check", func() {
rootURL = "http://localhost:8080/jenkins"
user = "admin"
password = "111e3a2f0231198855dceaff96f20540a9"
config = &Config{
Current: "fake",
JenkinsServers: []JenkinsServer{JenkinsServer{
Name: "fake",
URL: rootURL,
UserName: user,
Token: password,
}},
}
})
AfterEach(func() {
config = nil
rootCmd.SetArgs([]string{})
os.Remove(rootOptions.ConfigFile)
rootOptions.ConfigFile = ""
......@@ -115,43 +126,28 @@ var _ = Describe("job search command check", func() {
Context("basic cases", func() {
It("without pipeline-restful-api plugin", func() {
data, err := generateSampleConfig()
Expect(err).To(BeNil())
err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664)
Expect(err).To(BeNil())
req, _ := client.PrepareForOneInstalledPlugin(roundTripper, rootURL)
req.SetBasicAuth(user, password)
err = jobSearchOption.Check()
err := jobSearchOption.Check()
Expect(err).To(HaveOccurred())
})
It("with pipeline-restful-api 0.2+ plugin", func() {
data, err := generateSampleConfig()
Expect(err).To(BeNil())
err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664)
Expect(err).To(BeNil())
req, _ := client.PrepareForOneInstalledPluginWithPluginNameAndVer(roundTripper, rootURL,
"pipeline-restful-api", "1.0")
req.SetBasicAuth(user, password)
err = jobSearchOption.Check()
err := jobSearchOption.Check()
Expect(err).NotTo(HaveOccurred())
})
It("with pipeline-restful-api 0.2- plugin", func() {
data, err := generateSampleConfig()
Expect(err).To(BeNil())
err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664)
Expect(err).To(BeNil())
req, _ := client.PrepareForOneInstalledPluginWithPluginNameAndVer(roundTripper, rootURL,
"pipeline-restful-api", "0.1")
req.SetBasicAuth(user, password)
err = jobSearchOption.Check()
err := jobSearchOption.Check()
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("should be"))
})
......
package cmd
import (
"github.com/jenkins-zh/jenkins-cli/util"
"os"
"github.com/jenkins-zh/jenkins-cli/util"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/spf13/cobra"
......@@ -42,4 +43,7 @@ Plugin tutorial is here https://jenkins.io/doc/developer/tutorial/`),
}
return
},
Annotations: map[string]string{
since: "v0.0.23",
},
}
package cmd
import (
"github.com/jenkins-zh/jenkins-cli/util"
"os"
"github.com/jenkins-zh/jenkins-cli/util"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/spf13/cobra"
......@@ -70,4 +71,7 @@ var pluginReleaseCmd = &cobra.Command{
}
return
},
Annotations: map[string]string{
since: "v0.0.24",
},
}
......@@ -287,7 +287,8 @@ func (q *JobClient) JobInputSubmit(jobName, inputID string, buildID int, abort b
// ParseJobPath leads with slash
func ParseJobPath(jobName string) (path string) {
path = jobName
if jobName == "" || strings.HasPrefix(jobName, "/job") {
if jobName == "" || strings.HasPrefix(jobName, "/job/") ||
strings.HasPrefix(jobName, "job/") {
return
}
......@@ -313,6 +314,15 @@ type JenkinsItem struct {
URL string
Description string
Type string
/** comes from Job */
Buildable bool
Building bool
InQueue bool
/** comes from ParameterizedJob */
Parameterized bool
Disabled bool
}
// Job represents a job
......
package util
import (
"fmt"
"reflect"
)
// ReflectFieldValueAsString returns the value of a field
func ReflectFieldValueAsString(v reflect.Value, field string) string {
return fmt.Sprint(reflect.Indirect(v).FieldByName(field))
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册