diff --git a/README.md b/README.md index 61f7100..ed3106b 100644 --- a/README.md +++ b/README.md @@ -64,23 +64,32 @@ In order to always skip folders, you may set the environment variable `JENKINS_S ### Jobs Management ``` -$ butler jobs export --server localhost:8080 --username admin --password admin --skip-folder +$ butler jobs export --server localhost:8080 --skip-folder ``` ``` -$ butler jobs import --server localhost:8080 --username admin --password admin +$ butler jobs import --server localhost:8080 ``` ### Plugins Management ``` -$ butler plugins export --server localhost:8080 --username admin --password admin +$ butler plugins export --server localhost:8080 ``` ``` -$ butler plugins import --server localhost:8080 --username admin --password admin +$ butler plugins import --server localhost:8080 ``` +### Credentials Management + +``` +$ butler credentials decrypt --server localhost:8080 --folder foo/bar > decryptedCredentials.json +``` + +``` +$ cat decryptedCredentials.json | butler credentials apply --server localhost:8080 --folder bar/foo +``` ## Tutorials * [Butler CLI: Import/Export Jenkins Plugins & Jobs](http://www.blog.labouardy.com/butler-cli-import-export-jenkins-plugins-jobs/) diff --git a/credentials.go b/credentials.go new file mode 100644 index 0000000..24a2cbc --- /dev/null +++ b/credentials.go @@ -0,0 +1,32 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" +) + +func DecryptFolderCredentials(url string, folderName string, username string, password string) error { + folder, _ := GetFolder(url, folderName, username, password) + credentials := folder.GetCredentials() + script := GetDecryptScriptForCredentials(credentials) + response := ExecuteGroovyScriptOnJenkins(script, url, username, password) + fmt.Println(response) + return nil +} + +func ApplyFolderCredentials(url string, folderName string, username string, password string) error { + var credentials Credentials + + err := json.NewDecoder(os.Stdin).Decode(&credentials) + if err != nil { + log.Fatal(err) + panic(err) + } + script := GetApplyScriptForCredentials(credentials, folderName) + response := ExecuteGroovyScriptOnJenkins(script, url, username, password) + fmt.Println(response) + + return nil +} diff --git a/folder.go b/folder.go index b3af929..39e00c2 100644 --- a/folder.go +++ b/folder.go @@ -1,6 +1,47 @@ package main -import "strings" +import ( + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +type JenkinsFolder struct { + Properties struct { + CredentialProperty struct { + DomainCredentials struct { + Class string `xml:"class,attr"` + Entry struct { + Credentials Credentials `xml:"java.util.concurrent.CopyOnWriteArrayList"` + } `xml:"entry"` + } `xml:"domainCredentialsMap"` + } `xml:"com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider_-FolderCredentialsProperty"` + } `xml:"properties"` +} + +type Credentials struct { + UsernamePassword []UsernamePasswordCredential `xml:"com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl" json:"userpass"` + SecretFile []SecretFileCredential `xml:"org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl" json:"secretfile"` +} + +type UsernamePasswordCredential struct { + Plugin string `xml:"plugin,attr" json:"plugin"` + ID string `xml:"id" json:"id"` + Description string `xml:"description" json:"description"` + Username string `xml:"username" json:"username"` + Password string `xml:"password" json:"password"` +} + +type SecretFileCredential struct { + Plugin string `xml:"plugin,attr"` + ID string `xml:"id" json:"id"` + Description string `xml:"description" json:"description"` + FileName string `xml:"fileName" json:"fileName"` + SecretBytes string `xml:"secretBytes" json:"secretBytes"` +} func GetFolderURL(url string, folderName string) string { if folderName == "" { @@ -15,3 +56,42 @@ func GetFolderURL(url string, folderName string) string { path := strings.Replace(folderName, "/", "/job/", -1) return url + path } + +func parseJenkinsFolder(xmlInput []byte) JenkinsFolder { + var folder JenkinsFolder + xmlInput = []byte(strings.Trim(string(xmlInput), "")) + xml.Unmarshal(xmlInput, &folder) + return folder +} + +func (folder *JenkinsFolder) GetCredentials() Credentials { + return folder.Properties.CredentialProperty.DomainCredentials.Entry.Credentials +} + +func GetFolder(url string, folderName string, username string, password string) (JenkinsFolder, error) { + url = fmt.Sprintf("%s/config.xml", GetFolderURL(url, folderName)) + + client := &http.Client{} + req, err := http.NewRequest("GET", url, nil) + req.SetBasicAuth(username, password) + if err != nil { + return JenkinsFolder{}, err + } + + resp, err := client.Do(req) + if err != nil { + return JenkinsFolder{}, err + } + defer resp.Body.Close() + + if resp.StatusCode == 401 { + return JenkinsFolder{}, errors.New("Unauthorized 401") + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return JenkinsFolder{}, err + } + + return parseJenkinsFolder(data), nil +} diff --git a/folder_test.go b/folder_test.go index 09c80a7..cc332ba 100644 --- a/folder_test.go +++ b/folder_test.go @@ -1,6 +1,10 @@ package main -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestGetFolderURL(t *testing.T) { type args struct { @@ -69,3 +73,61 @@ func TestGetFolderURL(t *testing.T) { }) } } + +func TestParseJenkinsFolder(t *testing.T) { + assert := assert.New(t) + xml := ` + + + Ansible + + + + + + + + + + test + Test + Test + {AQAAABAAAAAQ7EzV5N/fXZEKM9HyG+1T66P67iqU+tptVCNuvNX1TM0=} + + + deploy-key-file + blub + accessKeys.csv + {bEtRRJ+hCoQHgEAmcGhAOlKFx6J5tVuKmwdBVSgdq4zkktsLwG1zHO6swI3mQ5z9UhbgRRHDf2W8oSHlfmno8+KHWKWKyNmQUL5cv6/8n5JnmvsMGx+DT4KJL2XDVl33nuNbDpkcJEDGBWqb2hA47iRtW6h4mxlbNja5E12eUMs=} + + + + + + + + + + + all + false + false + + + + all + + + + + false + + + +` + + got := parseJenkinsFolder([]byte(xml)) + + assert.NotNil(got, "Should not be nil.") + assert.Equal(got.GetCredentials().UsernamePassword[0].ID, "test") +} diff --git a/groovy_scripts.go b/groovy_scripts.go new file mode 100644 index 0000000..6903318 --- /dev/null +++ b/groovy_scripts.go @@ -0,0 +1,125 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +func GetDecryptScriptForCredentials(credentials Credentials) string { + marshalledCredentials, _ := json.Marshal(credentials) + return strings.Replace(decryptScriptTemplate, "<>", string(marshalledCredentials), 1) +} + +const decryptScriptTemplate = `import groovy.json.JsonSlurperClassic +import groovy.json.JsonOutput + + +def json = """<>""" + +def data = new JsonSlurperClassic().parseText(json) +data.userpass.each { + it.password = hudson.util.Secret.fromString(it.password).getPlainText() +} + +data.secretfile.each { + it.secretBytes = new String(com.cloudbees.plugins.credentials.SecretBytes.fromString(it.secretBytes).getPlainData(), "ASCII") +} + +println JsonOutput.toJson(data)` + +func GetApplyScriptForCredentials(credentials Credentials, folderPath string) string { + marshalledCredentials, _ := json.Marshal(credentials) + templated := strings.Replace(createOrUpdateCredentialsTemplate, "<>", string(marshalledCredentials), 1) + templated = strings.Replace(templated, "<>", folderPath, 1) + return templated +} + +const createOrUpdateCredentialsTemplate = `import com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider.FolderCredentialsProperty +import com.cloudbees.hudson.plugins.folder.AbstractFolder +import com.cloudbees.hudson.plugins.folder.Folder +import jenkins.model.* +import com.cloudbees.plugins.credentials.* +import com.cloudbees.plugins.credentials.common.* +import com.cloudbees.plugins.credentials.domains.* +import com.cloudbees.plugins.credentials.impl.* +import org.jenkinsci.plugins.plaincredentials.* +import org.jenkinsci.plugins.plaincredentials.impl.* +import org.apache.commons.fileupload.FileItem +import groovy.json.JsonSlurperClassic +import groovy.json.JsonOutput + +def createOrUpdateCredential(credentialStore, newCredential, existingCredentials) { + def existingCredential = existingCredentials.find{c -> c.getId() == newCredential.getId()} + if (existingCredential) + credentialStore.updateCredentials(Domain.global(), existingCredential, newCredential) + else + credentialStore.addCredentials(Domain.global(), newCredential) +} + +def json = """<>""" +String folderPath = "<>" +def data = new JsonSlurperClassic().parseText(json) + +Jenkins.instance.getAllItems(Folder.class) + .findAll{it.fullName.equals(folderPath)} + .each{ + AbstractFolder folderAbs = AbstractFolder.class.cast(it) + println "We're at ${folderAbs.fullName}" + FolderCredentialsProperty property = folderAbs.getProperties().get(FolderCredentialsProperty.class) + if(property == null){ + property = new FolderCredentialsProperty() + folderAbs.addProperty(property) + } + + def store = property.getStore() + def existingCredentials = property.getCredentials() + data.userpass.each { + Credentials c = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, it.id, it.description, it.username, it.password) + createOrUpdateCredential(store, c, existingCredentials) + } + data.secretfile.each { + def rawSecretFile = it + fileItem = [ getName: { return rawSecretFile.fileName}, get: { return rawSecretFile.secretBytes.getBytes() } ] as FileItem + secretFile = new FileCredentialsImpl( + CredentialsScope.GLOBAL, + rawSecretFile.id, + rawSecretFile.description, + fileItem, // Don't use FileItem + null, + "") + createOrUpdateCredential(store, secretFile, existingCredentials) + } + println existingCredentials.toString() +}` + +func ExecuteGroovyScriptOnJenkins(script string, rawUrl string, username string, password string) string { + apiURL := fmt.Sprintf("%s/scriptText", rawUrl) + data := url.Values{} + data.Set("script", script) + body := strings.NewReader(data.Encode()) + req, err := http.NewRequest("POST", apiURL, body) + if err != nil { + panic(err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + crumb, err := GetCrumb(rawUrl, username, password) + if err != nil { + fmt.Errorf("No crumb issueing possible: %v", err) + } else { + req.Header.Set(crumb[0], crumb[1]) + } + + req.SetBasicAuth(username, password) + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + responseBody, _ := ioutil.ReadAll(resp.Body) + return string(responseBody) +} diff --git a/main.go b/main.go index e11252e..ee96e41 100644 --- a/main.go +++ b/main.go @@ -123,6 +123,98 @@ func main() { }, }, }, + { + Name: "credentials", + Usage: "Jenkins Credential Management", + Subcommands: []cli.Command{ + { + Name: "decrypt", + Usage: "Decrypt credentials of Jenkins folder", + Aliases: []string{"d"}, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "server", + Usage: "Jenkins url", + }, + cli.StringFlag{ + Name: "folder, f", + Usage: "Jenkins Folder", + }, + cli.StringFlag{ + Name: "username, u", + Usage: "Jenkins username", + EnvVar: "JENKINS_USER", + }, + cli.StringFlag{ + Name: "password, p", + Usage: "Jenkins password", + EnvVar: "JENKINS_PASSWORD", + }, + }, + Action: func(c *cli.Context) error { + var url = getSanitizedUrl(c.String("server")) + var username = c.String("username") + var password = c.String("password") + var folder = c.String("folder") + + if url == "" || folder == "" { + cli.ShowSubcommandHelp(c) + return nil + } + + err := DecryptFolderCredentials(url, folder, username, password) + if err != nil { + return cli.NewExitError(err.Error(), 1) + } + + return nil + }, + }, + { + Name: "apply", + Usage: "Apply (from STDIN) credentials of Jenkins folder", + Aliases: []string{"a"}, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "server", + Usage: "Jenkins url", + }, + cli.StringFlag{ + Name: "folder, f", + Usage: "Jenkins Folder", + }, + cli.StringFlag{ + Name: "username, u", + Usage: "Jenkins username", + EnvVar: "JENKINS_USER", + }, + cli.StringFlag{ + Name: "password, p", + Usage: "Jenkins password", + EnvVar: "JENKINS_PASSWORD", + }, + }, + Action: func(c *cli.Context) error { + var url = getSanitizedUrl(c.String("server")) + var username = c.String("username") + var password = c.String("password") + var folder = c.String("folder") + + if url == "" || folder == "" { + cli.ShowSubcommandHelp(c) + return nil + } + + err := ApplyFolderCredentials(url, folder, username, password) + if err != nil { + return cli.NewExitError(err.Error(), 1) + } + + return nil + }, + }, + }, + }, { Name: "plugins", Usage: "Jenkins Plugins Management",