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

Add support to manage credentials (#266)

* Test pass with listing credentials and delete one

* Add support to create and list credentials

* Add more tests

* Add test cases for credential client

* Add test cases for credential cmds
上级 e0835006
...@@ -66,7 +66,7 @@ type BatchOption struct { ...@@ -66,7 +66,7 @@ type BatchOption struct {
Batch bool Batch bool
} }
// Confirm prompte user if they really want to do this // Confirm promote user if they really want to do this
func (b *BatchOption) Confirm(message string) bool { func (b *BatchOption) Confirm(message string) bool {
if !b.Batch { if !b.Batch {
confirm := false confirm := false
......
package cmd
import (
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(credentialCmd)
}
var credentialCmd = &cobra.Command{
Use: "credential",
Aliases: []string{"secret", "cred"},
Short: i18n.T("Manage the credentials of your Jenkins"),
Long: i18n.T(`Manage the credentials of your Jenkins`),
}
package cmd
import (
"fmt"
"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
type CredentialCreateOption struct {
Description string
ID string
Store string
Username string
Password string
Secret string
Type string
RoundTripper http.RoundTripper
}
var credentialCreateOption CredentialCreateOption
func init() {
credentialCmd.AddCommand(credentialCreateCmd)
credentialCreateCmd.Flags().StringVarP(&credentialCreateOption.Store, "store", "", "system",
i18n.T("The store name of Jenkins credentials"))
credentialCreateCmd.Flags().StringVarP(&credentialCreateOption.Type, "type", "", "basic",
i18n.T("The type of Jenkins credentials which could be: basic, secret"))
credentialCreateCmd.Flags().StringVarP(&credentialCreateOption.ID, "id", "", "",
i18n.T("The ID of Jenkins credentials"))
credentialCreateCmd.Flags().StringVarP(&credentialCreateOption.Username, "username", "", "",
i18n.T("The Username of Jenkins credentials"))
credentialCreateCmd.Flags().StringVarP(&credentialCreateOption.Password, "password", "", "",
i18n.T("The Password of Jenkins credentials"))
credentialCreateCmd.Flags().StringVarP(&credentialCreateOption.Description, "desc", "", "",
i18n.T("The Description of Jenkins credentials"))
credentialCreateCmd.Flags().StringVarP(&credentialCreateOption.Secret, "secret", "", "",
i18n.T("The Secret of Jenkins credentials"))
}
var credentialCreateCmd = &cobra.Command{
Use: "create [store] [id]",
Short: i18n.T("Create a credential from Jenkins"),
Long: i18n.T("Create a credential from Jenkins"),
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
if len(args) >= 1 {
credentialCreateOption.Store = args[0]
}
if credentialCreateOption.Store == "" {
err = fmt.Errorf("the store or id of target credential is empty")
}
return
},
RunE: func(cmd *cobra.Command, args []string) (err error) {
jClient := &client.CredentialsManager{
JenkinsCore: client.JenkinsCore{
RoundTripper: credentialCreateOption.RoundTripper,
Debug: true,
},
}
getCurrentJenkinsAndClient(&(jClient.JenkinsCore))
switch credentialCreateOption.Type {
case "basic":
err = jClient.CreateUsernamePassword(credentialCreateOption.Store, client.UsernamePasswordCredential{
Username: credentialCreateOption.Username,
Password: credentialCreateOption.Password,
Credential: client.Credential{
ID: credentialCreateOption.ID,
Description: credentialCreateOption.Description,
},
})
case "secret":
err = jClient.CreateSecret(credentialCreateOption.Store, client.StringCredentials{
Secret: credentialCreateOption.Secret,
Credential: client.Credential{
ID: credentialCreateOption.ID,
Description: credentialCreateOption.Description,
},
})
default:
err = fmt.Errorf("unknow credential type: %s", credentialCreateOption.Type)
}
return
},
}
package cmd
import (
"bytes"
"github.com/jenkins-zh/jenkins-cli/client"
"io/ioutil"
"os"
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/jenkins-zh/jenkins-cli/mock/mhttp"
)
var _ = Describe("credential create command", func() {
var (
ctrl *gomock.Controller
roundTripper *mhttp.MockRoundTripper
buf *bytes.Buffer
store string
id string
)
BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
roundTripper = mhttp.NewMockRoundTripper(ctrl)
rootCmd.SetArgs([]string{})
buf = new(bytes.Buffer)
rootCmd.SetOutput(buf)
rootOptions.Jenkins = ""
rootOptions.ConfigFile = "test.yaml"
credentialCreateOption.RoundTripper = roundTripper
store = "system"
id = "fake-id"
})
AfterEach(func() {
rootCmd.SetArgs([]string{})
os.Remove(rootOptions.ConfigFile)
rootOptions.ConfigFile = ""
ctrl.Finish()
})
Context("basic cases", func() {
var (
err error
)
BeforeEach(func() {
var data []byte
data, err = generateSampleConfig()
Expect(err).To(BeNil())
err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664)
Expect(err).To(BeNil())
})
It("lack of the necessary parameters", func() {
rootCmd.SetArgs([]string{"credential", "create", "--store="})
_, err = rootCmd.ExecuteC()
Expect(err).To(HaveOccurred())
})
It("unknown type", func() {
rootCmd.SetArgs([]string{"credential", "create", "--type", "fake-type", "--store", store})
_, err = rootCmd.ExecuteC()
Expect(err).To(HaveOccurred())
})
It("should success with user name and password", func() {
credential := client.UsernamePasswordCredential{}
client.PrepareForCreateUsernamePasswordCredential(roundTripper, "http://localhost:8080/jenkins",
"admin", "111e3a2f0231198855dceaff96f20540a9", store, credential)
rootCmd.SetArgs([]string{"credential", "create", "--type", "basic", store, id})
_, err = rootCmd.ExecuteC()
Expect(err).NotTo(HaveOccurred())
})
It("should success with secret", func() {
credential := client.StringCredentials{}
client.PrepareForCreateSecretCredential(roundTripper, "http://localhost:8080/jenkins",
"admin", "111e3a2f0231198855dceaff96f20540a9", store, credential)
rootCmd.SetArgs([]string{"credential", "create", "--type", "secret", store, id})
_, err = rootCmd.ExecuteC()
Expect(err).NotTo(HaveOccurred())
})
})
})
package cmd
import (
"fmt"
"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
type CredentialDeleteOption struct {
BatchOption
ID string
Store string
RoundTripper http.RoundTripper
}
var credentialDeleteOption CredentialDeleteOption
func init() {
credentialCmd.AddCommand(credentialDeleteCmd)
credentialDeleteCmd.Flags().StringVarP(&credentialDeleteOption.Store, "store", "", "system",
i18n.T("The store name of Jenkins credentials"))
credentialDeleteCmd.Flags().StringVarP(&credentialDeleteOption.ID, "id", "", "",
i18n.T("The ID of Jenkins credentials"))
credentialDeleteOption.SetFlag(credentialDeleteCmd)
}
var credentialDeleteCmd = &cobra.Command{
Use: "delete [store] [id]",
Aliases: []string{"remove", "del"},
Short: i18n.T("Delete a credential from Jenkins"),
Long: i18n.T("Delete a credential from Jenkins"),
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
if len(args) >= 1 {
credentialDeleteOption.Store = args[0]
}
if len(args) >= 2 {
credentialDeleteOption.ID = args[1]
}
if credentialDeleteOption.Store == "" || credentialDeleteOption.ID == "" {
err = fmt.Errorf("the store or id of target credential is empty")
}
return
},
RunE: func(cmd *cobra.Command, args []string) (err error) {
if !credentialDeleteOption.Confirm(fmt.Sprintf("Are you sure to delete credential %s", credentialDeleteOption.ID)) {
return
}
jClient := &client.CredentialsManager{
JenkinsCore: client.JenkinsCore{
RoundTripper: credentialDeleteOption.RoundTripper,
},
}
getCurrentJenkinsAndClient(&(jClient.JenkinsCore))
err = jClient.Delete(credentialDeleteOption.Store, credentialDeleteOption.ID)
return
},
}
package cmd
import (
"bytes"
"io/ioutil"
"os"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/jenkins-zh/jenkins-cli/mock/mhttp"
)
var _ = Describe("credential delete command", func() {
var (
ctrl *gomock.Controller
roundTripper *mhttp.MockRoundTripper
buf *bytes.Buffer
store string
id string
)
BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
roundTripper = mhttp.NewMockRoundTripper(ctrl)
rootCmd.SetArgs([]string{})
buf = new(bytes.Buffer)
rootCmd.SetOutput(buf)
rootOptions.Jenkins = ""
rootOptions.ConfigFile = "test.yaml"
credentialDeleteOption.RoundTripper = roundTripper
store = "system"
id = "fake-id"
})
AfterEach(func() {
rootCmd.SetArgs([]string{})
os.Remove(rootOptions.ConfigFile)
rootOptions.ConfigFile = ""
ctrl.Finish()
})
Context("basic cases", func() {
var (
err error
)
BeforeEach(func() {
var data []byte
data, err = generateSampleConfig()
Expect(err).To(BeNil())
err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664)
Expect(err).To(BeNil())
})
It("lack of the necessary parameters", func() {
rootCmd.SetArgs([]string{"credential", "delete"})
_, err = rootCmd.ExecuteC()
Expect(err).To(HaveOccurred())
})
It("should success", func() {
client.PrepareForDeleteCredential(roundTripper, "http://localhost:8080/jenkins",
"admin", "111e3a2f0231198855dceaff96f20540a9", store, id)
rootCmd.SetArgs([]string{"credential", "delete", store, id, "-b"})
_, err = rootCmd.ExecuteC()
Expect(err).To(BeNil())
})
})
})
package cmd
import (
"bytes"
"fmt"
"github.com/jenkins-zh/jenkins-cli/client"
"net/http"
"github.com/jenkins-zh/jenkins-cli/app/i18n"
"github.com/jenkins-zh/jenkins-cli/util"
"github.com/spf13/cobra"
)
// CredentialListOption option for credential list command
type CredentialListOption struct {
OutputOption
Store string
RoundTripper http.RoundTripper
}
var credentialListOption CredentialListOption
func init() {
credentialCmd.AddCommand(credentialListCmd)
credentialListCmd.Flags().StringVarP(&credentialListOption.Store, "store", "", "system",
i18n.T("The store name of Jenkins credentials"))
credentialListOption.SetFlag(credentialListCmd)
}
var credentialListCmd = &cobra.Command{
Use: "list",
Short: i18n.T("List all credentials of Jenkins"),
Long: i18n.T("List all credentials of Jenkins"),
RunE: func(cmd *cobra.Command, _ []string) (err error) {
jClient := &client.CredentialsManager{
JenkinsCore: client.JenkinsCore{
RoundTripper: credentialListOption.RoundTripper,
},
}
getCurrentJenkinsAndClient(&(jClient.JenkinsCore))
var credentialList client.CredentialList
var data []byte
if credentialList, err = jClient.GetList(credentialListOption.Store); err == nil {
if data, err = credentialListOption.Output(credentialList); err == nil {
cmd.Print(string(data))
}
}
return
},
}
// Output render data into byte array as a table format
func (o *CredentialListOption) Output(obj interface{}) (data []byte, err error) {
if data, err = o.OutputOption.Output(obj); err != nil && o.Format == TableOutputFormat {
credentialList := obj.(client.CredentialList)
buf := new(bytes.Buffer)
table := util.CreateTableWithHeader(buf, o.WithoutHeaders)
table.AddHeader("number", "displayName", "id", "type", "description")
for i, cred := range credentialList.Credentials {
table.AddRow(fmt.Sprintf("%d", i), cred.DisplayName, cred.ID, cred.TypeName, cred.Description)
}
table.Render()
err = nil
data = buf.Bytes()
}
return
}
package cmd
import (
"bytes"
"io/ioutil"
"os"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/jenkins-zh/jenkins-cli/mock/mhttp"
)
var _ = Describe("credential list command", func() {
var (
ctrl *gomock.Controller
roundTripper *mhttp.MockRoundTripper
buf *bytes.Buffer
store string
)
BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
roundTripper = mhttp.NewMockRoundTripper(ctrl)
rootCmd.SetArgs([]string{})
buf = new(bytes.Buffer)
rootCmd.SetOutput(buf)
rootOptions.Jenkins = ""
rootOptions.ConfigFile = "test.yaml"
credentialListOption.RoundTripper = roundTripper
store = "system"
})
AfterEach(func() {
rootCmd.SetArgs([]string{})
os.Remove(rootOptions.ConfigFile)
rootOptions.ConfigFile = ""
ctrl.Finish()
})
Context("basic cases", func() {
var (
err error
)
BeforeEach(func() {
var data []byte
data, err = generateSampleConfig()
Expect(err).To(BeNil())
err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664)
Expect(err).To(BeNil())
})
It("should success", func() {
client.PrepareForGetCredentialList(roundTripper, "http://localhost:8080/jenkins",
"admin", "111e3a2f0231198855dceaff96f20540a9", store)
rootCmd.SetArgs([]string{"credential", "list"})
_, err = rootCmd.ExecuteC()
Expect(err).To(BeNil())
Expect(buf.String()).To(Equal(`number displayName id type description
0 displayName 19c27487-acca-4a39-9889-9ddd500388f3 Username with password
`))
})
})
})
package client
import (
"encoding/json"
"fmt"
"net/url"
"strings"
"github.com/jenkins-zh/jenkins-cli/util"
)
// CredentialsManager hold the info of credentials client
type CredentialsManager struct {
JenkinsCore
}
// GetList returns the credential list
func (c *CredentialsManager) GetList(store string) (credentialList CredentialList, err error) {
api := fmt.Sprintf("/credentials/store/%s/domain/_/api/json?pretty=true&depth=1", store)
err = c.RequestWithData("GET", api, nil, nil, 200, &credentialList)
return
}
// Delete removes a credential by id from a store
func (c *CredentialsManager) Delete(store, id string) (err error) {
api := fmt.Sprintf("/credentials/store/%s/domain/_/credential/%s/doDelete", store, id)
_, err = c.RequestWithoutData("POST", api, nil, nil, 200)
return
}
// Create create a credential in Jenkins
func (c *CredentialsManager) Create(store, credential string) (err error) {
api := fmt.Sprintf("/credentials/store/%s/domain/_/createCredentials", store)
formData := url.Values{}
formData.Add("json", fmt.Sprintf(`{"credentials": %s}`, credential))
payload := strings.NewReader(formData.Encode())
_, err = c.RequestWithoutData("POST", api,
map[string]string{util.ContentType: util.ApplicationForm}, payload, 200)
return
}
// CreateUsernamePassword create username and password credential in Jenkins
func (c *CredentialsManager) CreateUsernamePassword(store string, cred UsernamePasswordCredential) (err error) {
var payload []byte
cred.Class = "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
if payload, err = json.Marshal(cred); err == nil {
err = c.Create(store, string(payload))
}
return
}
// CreateSecret create token credential in Jenkins
func (c *CredentialsManager) CreateSecret(store string, cred StringCredentials) (err error) {
var payload []byte
cred.Class = "org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl"
cred.Scope = "GLOBAL"
if payload, err = json.Marshal(cred); err == nil {
err = c.Create(store, string(payload))
}
return
}
// Credential of Jenkins
type Credential struct {
Description string `json:"description"`
DisplayName string
Fingerprint interface{}
FullName string
ID string `json:"id"`
TypeName string
Class string `json:"$class"`
Scope string `json:"scope"`
}
// UsernamePasswordCredential hold the username and password
type UsernamePasswordCredential struct {
Credential `json:",inline"`
Username string `json:"username"`
Password string `json:"password"`
}
// StringCredentials hold a token
type StringCredentials struct {
Credential `json:",inline"`
Secret string `json:"secret"`
}
// CredentialList contains many credentials
type CredentialList struct {
Description string
DisplayName string
FullDisplayName string
FullName string
Global bool
URLName string
Credentials []Credential
}
package client_test
import (
"github.com/golang/mock/gomock"
"github.com/jenkins-zh/jenkins-cli/client"
"github.com/jenkins-zh/jenkins-cli/mock/mhttp"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("job test", func() {
var (
ctrl *gomock.Controller
credentialsManager client.CredentialsManager
roundTripper *mhttp.MockRoundTripper
store string
)
BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
credentialsManager = client.CredentialsManager{}
roundTripper = mhttp.NewMockRoundTripper(ctrl)
credentialsManager.RoundTripper = roundTripper
credentialsManager.URL = "http://localhost"
store = "system"
})
AfterEach(func() {
ctrl.Finish()
})
Context("GetList", func() {
It("should success", func() {
client.PrepareForGetCredentialList(roundTripper, credentialsManager.URL, "", "", store)
list, err := credentialsManager.GetList(store)
Expect(err).NotTo(HaveOccurred())
Expect(list).NotTo(BeNil())
Expect(len(list.Credentials)).To(Equal(1))
})
})
Context("Delete", func() {
var (
id = "fake-id"
)
It("should success", func() {
client.PrepareForDeleteCredential(roundTripper, credentialsManager.URL, "", "", store, id)
err := credentialsManager.Delete(store, id)
Expect(err).NotTo(HaveOccurred())
})
})
Context("CreateUsernamePassword", func() {
It("should success", func() {
cred := client.UsernamePasswordCredential{}
client.PrepareForCreateUsernamePasswordCredential(roundTripper, credentialsManager.URL,
"", "", store, cred)
err := credentialsManager.CreateUsernamePassword(store, cred)
Expect(err).NotTo(HaveOccurred())
})
})
Context("CreateSecret", func() {
It("should success", func() {
cred := client.StringCredentials{}
client.PrepareForCreateSecretCredential(roundTripper, credentialsManager.URL,
"", "", store, cred)
err := credentialsManager.CreateSecret(store, cred)
Expect(err).NotTo(HaveOccurred())
})
})
})
package client
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/jenkins-zh/jenkins-cli/mock/mhttp"
"github.com/jenkins-zh/jenkins-cli/util"
)
// PrepareForGetCredentialList only for test
func PrepareForGetCredentialList(roundTripper *mhttp.MockRoundTripper, rootURL, user, password, store string) {
api := fmt.Sprintf("%s/credentials/store/%s/domain/_/api/json?pretty=true&depth=1", rootURL, store)
request, _ := http.NewRequest("GET", api, nil)
response := &http.Response{
StatusCode: 200,
Request: request,
Body: ioutil.NopCloser(bytes.NewBufferString(PrepareForCredentialListJSON())),
}
roundTripper.EXPECT().
RoundTrip(request).Return(response, nil)
if user != "" && password != "" {
request.SetBasicAuth(user, password)
}
}
// PrepareForDeleteCredential only for test
func PrepareForDeleteCredential(roundTripper *mhttp.MockRoundTripper, rootURL, user, password, store, id string) {
api := fmt.Sprintf("%s/credentials/store/%s/domain/_/credential/%s/doDelete", rootURL, store, id)
request, _ := http.NewRequest("POST", api, nil)
PrepareCommonPost(request, "", roundTripper, user, password, rootURL)
}
// PrepareForCreateCredential only for test
func PrepareForCreateCredential(roundTripper *mhttp.MockRoundTripper, rootURL, user, password, store, credential string) {
api := fmt.Sprintf("%s/credentials/store/%s/domain/_/createCredentials", rootURL, store)
formData := url.Values{}
formData.Add("json", fmt.Sprintf(`{"credentials": %s}`, credential))
payload := strings.NewReader(formData.Encode())
request, _ := http.NewRequest("POST", api, payload)
request.Header.Add(util.ContentType, util.ApplicationForm)
PrepareCommonPost(request, "", roundTripper, user, password, rootURL)
}
// PrepareForCreateUsernamePasswordCredential only for test
func PrepareForCreateUsernamePasswordCredential(roundTripper *mhttp.MockRoundTripper, rootURL, user, password,
store string, cred UsernamePasswordCredential) {
cred.Class = "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
if payload, err := json.Marshal(cred); err == nil {
PrepareForCreateCredential(roundTripper, rootURL, user, password, store, string(payload))
}
}
// PrepareForCreateSecretCredential only for test
func PrepareForCreateSecretCredential(roundTripper *mhttp.MockRoundTripper, rootURL, user, password,
store string, cred StringCredentials) {
cred.Class = "org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl"
cred.Scope = "GLOBAL"
if payload, err := json.Marshal(cred); err == nil {
PrepareForCreateCredential(roundTripper, rootURL, user, password, store, string(payload))
}
}
// PrepareForCredentialListJSON only for test
func PrepareForCredentialListJSON() string {
return `{
"_class" : "com.cloudbees.plugins.credentials.CredentialsStoreAction$DomainWrapper",
"credentials" : [
{
"description" : "",
"displayName" : "displayName",
"fingerprint" : {
},
"fullName" : "system/_/19c27487-acca-4a39-9889-9ddd500388f3",
"id" : "19c27487-acca-4a39-9889-9ddd500388f3",
"typeName" : "Username with password"
}
],
"description" : "Credentials that should be available irrespective of domain specification to requirements matching.",
"displayName" : "全局凭据 (unrestricted)",
"fullDisplayName" : "系统 » 全局凭据 (unrestricted)",
"fullName" : "system/_",
"global" : true,
"urlName" : "_"
}`
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册