Skip to content

Commit

Permalink
Cli setup command (#3384)
Browse files Browse the repository at this point in the history
Co-authored-by: Robert Kaussow <[email protected]>
  • Loading branch information
anbraten and xoxys authored Mar 13, 2024
1 parent 1026f95 commit 03c891e
Show file tree
Hide file tree
Showing 17 changed files with 664 additions and 42 deletions.
6 changes: 6 additions & 0 deletions cli/common/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import (
)

var GlobalFlags = append([]cli.Flag{
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CONFIG"},
Name: "config",
Aliases: []string{"c"},
Usage: "path to config file",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_TOKEN"},
Name: "token",
Expand Down
5 changes: 3 additions & 2 deletions cli/common/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"

"go.woodpecker-ci.org/woodpecker/v2/cli/internal/config"
"go.woodpecker-ci.org/woodpecker/v2/cli/update"
)

Expand All @@ -17,7 +18,7 @@ var (
)

func Before(c *cli.Context) error {
if err := SetupGlobalLogger(c); err != nil {
if err := setupGlobalLogger(c); err != nil {
return err
}

Expand Down Expand Up @@ -49,7 +50,7 @@ func Before(c *cli.Context) error {
}
}()

return nil
return config.Load(c)
}

func After(_ *cli.Context) error {
Expand Down
2 changes: 1 addition & 1 deletion cli/common/zerologger.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/shared/logger"
)

func SetupGlobalLogger(c *cli.Context) error {
func setupGlobalLogger(c *cli.Context) error {
return logger.SetupGlobalLogger(c, false)
}
131 changes: 131 additions & 0 deletions cli/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package config

import (
"encoding/json"
"errors"
"os"

"github.com/adrg/xdg"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"github.com/zalando/go-keyring"
)

type Config struct {
ServerURL string `json:"server_url"`
Token string `json:"-"`
LogLevel string `json:"log_level"`
}

func Load(c *cli.Context) error {
// If the command is setup, we don't need to load the config
if firstArg := c.Args().First(); firstArg == "setup" {
return nil
}

config, err := Get(c, c.String("config"))
if err != nil {
return err
}

if config == nil && !c.IsSet("server-url") && !c.IsSet("token") {
log.Info().Msg("The woodpecker-cli is not yet set up. Please run `woodpecker-cli setup`")
return errors.New("woodpecker-cli is not setup")
}

if !c.IsSet("server") {
err = c.Set("server", config.ServerURL)
if err != nil {
return err
}
}

if !c.IsSet("token") {
err = c.Set("token", config.Token)
if err != nil {
return err
}
}

if !c.IsSet("log-level") {
err = c.Set("log-level", config.LogLevel)
if err != nil {
return err
}
}

return nil
}

func getConfigPath(configPath string) (string, error) {
if configPath != "" {
return configPath, nil
}

configPath, err := xdg.ConfigFile("woodpecker/config.json")
if err != nil {
return "", err
}

return configPath, nil
}

func Get(ctx *cli.Context, _configPath string) (*Config, error) {
configPath, err := getConfigPath(_configPath)
if err != nil {
return nil, err
}

content, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
log.Debug().Err(err).Msg("Failed to read the config file")
return nil, err
} else if err != nil && os.IsNotExist(err) {
log.Debug().Msg("The config file does not exist")
return nil, nil
}

c := &Config{}
err = json.Unmarshal(content, c)
if err != nil {
return nil, err
}

// load token from keyring
service := ctx.App.Name
secret, err := keyring.Get(service, c.ServerURL)
if err != nil && !errors.Is(err, keyring.ErrNotFound) {
return nil, err
}
if err == nil {
c.Token = secret
}

return c, nil
}

func Save(ctx *cli.Context, _configPath string, c *Config) error {
config, err := json.Marshal(c)
if err != nil {
return err
}

configPath, err := getConfigPath(_configPath)
if err != nil {
return err
}

// save token to keyring
service := ctx.App.Name
err = keyring.Set(service, c.ServerURL, c.Token)
if err != nil {
return err
}

err = os.WriteFile(configPath, config, 0o600)
if err != nil {
return err
}

return nil
}
88 changes: 88 additions & 0 deletions cli/setup/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package setup

import (
"errors"
"strings"

"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"

"go.woodpecker-ci.org/woodpecker/v2/cli/internal/config"
"go.woodpecker-ci.org/woodpecker/v2/cli/setup/ui"
)

// Command exports the setup command.
var Command = &cli.Command{
Name: "setup",
Usage: "setup the woodpecker-cli for the first time",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "server-url",
Usage: "The URL of the woodpecker server",
},
&cli.StringFlag{
Name: "token",
Usage: "The token to authenticate with the woodpecker server",
},
},
Action: setup,
}

func setup(c *cli.Context) error {
_config, err := config.Get(c, c.String("config"))
if err != nil {
return err
} else if _config != nil {
setupAgain, err := ui.Confirm("The woodpecker-cli was already configured. Do you want to configure it again?")
if err != nil {
return err
}

if !setupAgain {
log.Info().Msg("Configuration skipped")
return nil
}
}

serverURL := c.String("server-url")

if serverURL == "" {
serverURL, err = ui.Ask("Enter the URL of the woodpecker server", "https://ci.woodpecker-ci.org", true)
if err != nil {
return err
}

if serverURL == "" {
return errors.New("server URL cannot be empty")
}
}

if !strings.Contains(serverURL, "://") {
serverURL = "https://" + serverURL
}

token := c.String("token")
if token == "" {
token, err = receiveTokenFromUI(c.Context, serverURL)
if err != nil {
return err
}

if token == "" {
return errors.New("no token received from the UI")
}
}

err = config.Save(c, c.String("config"), &config.Config{
ServerURL: serverURL,
Token: token,
LogLevel: "info",
})
if err != nil {
return err
}

log.Info().Msg("The woodpecker-cli has been successfully setup")

return nil
}
117 changes: 117 additions & 0 deletions cli/setup/token_fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package setup

import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"os/exec"
"runtime"
"time"

"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)

func receiveTokenFromUI(c context.Context, serverURL string) (string, error) {
port := randomPort()

tokenReceived := make(chan string)

srv := &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", port)}
srv.Handler = setupRouter(tokenReceived)

go func() {
log.Debug().Msgf("Listening for token response on :%d", port)
_ = srv.ListenAndServe()
}()

defer func() {
log.Debug().Msg("Shutting down server")
_ = srv.Shutdown(c)
}()

err := openBrowser(fmt.Sprintf("%s/cli/auth?port=%d", serverURL, port))
if err != nil {
return "", err
}

// wait for token to be received or timeout
select {
case token := <-tokenReceived:
return token, nil
case <-c.Done():
return "", c.Err()
case <-time.After(5 * time.Minute):
return "", errors.New("timed out waiting for token")
}
}

func setupRouter(tokenReceived chan string) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
e := gin.New()
e.UseRawPath = true
e.Use(gin.Recovery())

e.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")

if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}

c.Next()
})

e.POST("/token", func(c *gin.Context) {
data := struct {
Token string `json:"token"`
}{}

err := c.BindJSON(&data)
if err != nil {
log.Debug().Err(err).Msg("Failed to bind JSON")
c.JSON(400, gin.H{
"error": "invalid request",
})
return
}

tokenReceived <- data.Token

c.JSON(200, gin.H{
"ok": "true",
})
})

return e
}

func openBrowser(url string) error {
var err error

log.Debug().Msgf("Opening browser with URL: %s", url)

switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}

func randomPort() int {
s1 := rand.NewSource(time.Now().UnixNano())
r1 := rand.New(s1)
return r1.Intn(10000) + 20000
}
Loading

0 comments on commit 03c891e

Please sign in to comment.