Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix:(jenkins api token) fix the api token for new Jenkins version #1555

Merged
merged 5 commits into from Sep 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion Gopkg.toml
Expand Up @@ -185,7 +185,6 @@ required = [
non-go = false
go-tests = false


[[constraint]]
name = "github.com/andygrunwald/go-gerrit"
version = "0.5.2"
Expand Down
4 changes: 4 additions & 0 deletions pkg/jenkins/utils.go
Expand Up @@ -99,6 +99,10 @@ func JenkinsTokenURL(url string) string {
return tokenUrl
}

func JenkinsNewTokenURL(url string) string {
return util.UrlJoin(url, "/me/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken")
}

func EditUserAuth(url string, configService *jenkauth.AuthConfigService, config *jenkauth.AuthConfig, auth *jenkauth.UserAuth, tokenUrl string, batchMode bool, in terminal.FileReader, out terminal.FileWriter, outErr io.Writer) (jenkauth.UserAuth, error) {

log.Infof("\nTo be able to connect to the Jenkins server we need a username and API Token\n\n")
Expand Down
185 changes: 142 additions & 43 deletions pkg/jx/cmd/create_jenkins_token.go
Expand Up @@ -3,25 +3,35 @@ package cmd
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
golog "log"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/chromedp"
"github.com/chromedp/chromedp/runner"
"github.com/hpcloud/tail"
"github.com/jenkins-x/jx/pkg/auth"
"github.com/jenkins-x/jx/pkg/jenkins"
"github.com/jenkins-x/jx/pkg/jx/cmd/templates"
"github.com/jenkins-x/jx/pkg/kube"
"github.com/jenkins-x/jx/pkg/log"
"github.com/jenkins-x/jx/pkg/util"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"gopkg.in/AlecAivazis/survey.v1/terminal"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const JenkinsCookieName = "JSESSIONID"

var (
create_jenkins_user_long = templates.LongDesc(`
Creates a new user and API Token for the current Jenkins Server
Expand Down Expand Up @@ -138,12 +148,14 @@ func (o *CreateJenkinsUserOptions) Run() error {

tokenUrl := jenkins.JenkinsTokenURL(server.URL)
if o.Verbose {
log.Infof("using url %s\n", tokenUrl)
log.Infof("Using url %s\n", tokenUrl)
}
if userAuth.IsInvalid() && o.Password != "" && o.UseBrowser {
err := o.tryFindAPITokenFromBrowser(tokenUrl, userAuth)
newTokenUrl := jenkins.JenkinsNewTokenURL(server.URL)
err = o.tryFindAPITokenFromBrowser(tokenUrl, newTokenUrl, userAuth)
if err != nil {
log.Warnf("unable to automatically find API token with chromedp using URL %s\n", tokenUrl)
log.Warnf("Unable to automatically find API token with chromedp using URL %s\n", tokenUrl)
log.Warnf("Error: %v\n", err)
}
}

Expand Down Expand Up @@ -187,36 +199,43 @@ func (o *CreateJenkinsUserOptions) Run() error {
}

// lets try use the users browser to find the API token
func (o *CreateJenkinsUserOptions) tryFindAPITokenFromBrowser(tokenUrl string, userAuth *auth.UserAuth) error {
var ctxt context.Context
func (o *CreateJenkinsUserOptions) tryFindAPITokenFromBrowser(tokenUrl string, newTokenUrl string, userAuth *auth.UserAuth) error {
var ctx context.Context
var cancel context.CancelFunc
if o.Timeout != "" {
duration, err := time.ParseDuration(o.Timeout)
if err != nil {
return err
return errors.Wrap(err, "parsing the timeout value")
}
ctxt, cancel = context.WithTimeout(context.Background(), duration)
ctx, cancel = context.WithTimeout(context.Background(), duration)
} else {
ctxt, cancel = context.WithCancel(context.Background())
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel()

c, err := o.createChromeClient(ctxt)
userDataDir, err := ioutil.TempDir("/tmp", "jx-login-chrome-userdata-dir")
if err != nil {
return err
return errors.Wrap(err, "creating the chrome user data dir")
}
defer os.RemoveAll(userDataDir)
netLogFile := filepath.Join(userDataDir, "net-logs.json")

c, err := o.createChromeClientWithNetLog(ctx, userDataDir, netLogFile)
if err != nil {
return errors.Wrap(err, "creating the chrome client")
}

err = c.Run(ctxt, chromedp.Tasks{
err = c.Run(ctx, chromedp.Tasks{
chromedp.Navigate(tokenUrl),
})
if err != nil {
return err
return errors.Wrapf(err, "navigating to token URL '%s'", tokenUrl)
}

nodeSlice := []*cdp.Node{}
err = c.Run(ctxt, chromedp.Nodes("//input", &nodeSlice))
err = c.Run(ctx, chromedp.Nodes("//input", &nodeSlice))
if err != nil {
return err
return errors.Wrap(err, "serching the login form")
}

login := false
Expand All @@ -230,57 +249,115 @@ func (o *CreateJenkinsUserOptions) tryFindAPITokenFromBrowser(tokenUrl string, u
}

if login {
// disable screenshots to try and reduce errors when running headless
//o.captureScreenshot(ctxt, c, "screenshot-jenkins-login.png", "main-panel", chromedp.ByID)

log.Infoln("logging in")
err = c.Run(ctxt, chromedp.Tasks{
log.Infoln("Logging in...")
err = c.Run(ctx, chromedp.Tasks{
chromedp.WaitVisible(userNameInputName, chromedp.ByID),
chromedp.SendKeys(userNameInputName, userAuth.Username, chromedp.ByID),
chromedp.SendKeys(passwordInputSelector, o.Password+"\n"),
})
if err != nil {
return err
return errors.Wrap(err, "filling up the login form")
}
}

// disable screenshots to try and reduce errors when running headless
//o.captureScreenshot(ctxt, c, "screenshot-jenkins-api-token.png", "main-panel", chromedp.ByID)

getAPITokenButtonSelector := "//button[normalize-space(text())='Show API Token...']"
nodeSlice = []*cdp.Node{}

log.Infoln("Getting the API Token...")
err = c.Run(ctxt, chromedp.Tasks{
chromedp.Sleep(2 * time.Second),
chromedp.WaitVisible(getAPITokenButtonSelector),
chromedp.Click(getAPITokenButtonSelector),
//chromedp.WaitVisible("apiToken", chromedp.ByID),
chromedp.Nodes("apiToken", &nodeSlice, chromedp.ByID),
})
t, err := tail.TailFile(netLogFile, tail.Config{
Follow: true,
Logger: golog.New(ioutil.Discard, "", golog.LstdFlags)})
if err != nil {
return err
return errors.Wrap(err, "reading the netlog file")
}
token := ""
for _, node := range nodeSlice {
text := node.AttributeValue("value")
if text != "" && token == "" {
token = text

cookie := ""
for line := range t.Lines {
if strings.Contains(line.Text, JenkinsCookieName) &&
strings.Contains(line.Text, "GET /me/configure") {
cookie = o.extractJenkinsCookie(line.Text)
break
}
}
log.Infoln("Found API Token")

if cookie == "" {
return errors.New("No Jenkins cookie found")
}

token, err := o.generateNewApiToken(newTokenUrl, cookie)
if err != nil {
return errors.Wrap(err, "generating the API token")
}

if token != "" {
userAuth.ApiToken = token
}

err = c.Shutdown(ctxt)
err = c.Shutdown(ctx)
if err != nil {
return err
return errors.Wrap(err, "shutting down the chrome client")
}

return nil
}

func (o *CommonOptions) generateNewApiToken(newTokenUrl string, cookie string) (string, error) {
client := http.Client{}
req, err := http.NewRequest(http.MethodPost, newTokenUrl, nil)
if err != nil {
return "", errors.Wrap(err, "building request to generate the API token")
}
parts := strings.Split(cookie, "=")
if len(parts) != 2 {
return "", errors.Wrap(err, "building jenkins cookie")
}
jenkinsCookie := http.Cookie{Name: parts[0], Value: parts[1]}
req.AddCookie(&jenkinsCookie)
resp, err := client.Do(req)
if err != nil {
return "", errors.Wrap(err, "execute generate API token request")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "reading API token from response body")
}

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("generate API token status code: %d, error: %s", resp.StatusCode, string(body))
}

type TokenData struct {
TokenName string `json:"tokenName"`
TokenUuid string `json:"tokenUuid"`
TokenValue string `json:"tokenValue"`
}

type TokenResponse struct {
Status string `json:"status"`
Data TokenData `json:"data"`
}
tokenResponse := &TokenResponse{}
if err := json.Unmarshal(body, tokenResponse); err != nil {
return "", errors.Wrap(err, "parsing the API token from response")
}
return tokenResponse.Data.TokenValue, nil
}

func (o *CommonOptions) extractJenkinsCookie(text string) string {
start := strings.Index(text, JenkinsCookieName)
if start < 0 {
return ""
}
end := -1
for i, ch := range text[start:] {
if ch == '"' {
end = start + i
break
}
}
if end < 0 {
return ""
}
return text[start:end]
}

// lets try use the users browser to find the API token
func (o *CommonOptions) createChromeClient(ctxt context.Context) (*chromedp.CDP, error) {
if o.Headless {
Expand All @@ -296,6 +373,28 @@ func (o *CommonOptions) createChromeClient(ctxt context.Context) (*chromedp.CDP,
return chromedp.New(ctxt)
}

func (o *CommonOptions) createChromeClientWithNetLog(ctx context.Context, userDataDir string, netLogFile string) (*chromedp.CDP, error) {
options := func(m map[string]interface{}) error {
if o.Headless {
m["remote-debugging-port"] = 9222
m["no-sandbox"] = true
m["headless"] = true
}
m["user-data-dir"] = userDataDir
m["log-net-log"] = netLogFile
m["net-log-capture-mode"] = "IncludeCookiesAndCredentials"
m["v"] = 1
return nil
}

logger := func(string, ...interface{}) {
return
}
return chromedp.New(ctx,
chromedp.WithRunnerOptions(runner.CommandLineOption(options)),
chromedp.WithLog(logger))
}

func (o *CommonOptions) captureScreenshot(ctxt context.Context, c *chromedp.CDP, screenshotFile string, selector interface{}, options ...chromedp.QueryOption) error {
log.Infoln("Creating a screenshot...")

Expand Down
2 changes: 1 addition & 1 deletion pkg/jx/cmd/install.go
Expand Up @@ -875,7 +875,7 @@ func (options *InstallOptions) logAdminPassword() {
********************************************************

`
log.Infof(astrix, fmt.Sprintf("Your admin password is: %s", util.ColorInfo(options.AdminSecretsService.Flags.DefaultAdminPassword)))
log.Infof(astrix+"\n", fmt.Sprintf("Your admin password is: %s", util.ColorInfo(options.AdminSecretsService.Flags.DefaultAdminPassword)))
}

// LoadVersionFromCloudEnvironmentsDir loads a version from the cloud environments directory
Expand Down