mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-09-18 10:07:47 +00:00
Cli setup command (#3384)
Co-authored-by: Robert Kaussow <xoxys@rknet.org>
This commit is contained in:
88
cli/setup/setup.go
Normal file
88
cli/setup/setup.go
Normal file
@@ -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
cli/setup/token_fetcher.go
Normal file
117
cli/setup/token_fetcher.go
Normal file
@@ -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
|
||||
}
|
79
cli/setup/ui/ask.go
Normal file
79
cli/setup/ui/ask.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type askModel struct {
|
||||
prompt string
|
||||
required bool
|
||||
textInput textinput.Model
|
||||
err error
|
||||
}
|
||||
|
||||
func (m askModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m askModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyEnter:
|
||||
if !m.required || (m.required && strings.TrimSpace(m.textInput.Value()) != "") {
|
||||
return m, tea.Quit
|
||||
}
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
return m, tea.Quit
|
||||
}
|
||||
default:
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m askModel) View() string {
|
||||
return fmt.Sprintf(
|
||||
"%s\n\n%s\n\n%s",
|
||||
m.prompt,
|
||||
m.textInput.View(),
|
||||
"(esc to quit)",
|
||||
) + "\n"
|
||||
}
|
||||
|
||||
func Ask(prompt, placeholder string, required bool) (string, error) {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = placeholder
|
||||
ti.Focus()
|
||||
ti.CharLimit = 156
|
||||
ti.Width = 40
|
||||
|
||||
p := tea.NewProgram(askModel{
|
||||
prompt: prompt,
|
||||
textInput: ti,
|
||||
required: required,
|
||||
err: nil,
|
||||
})
|
||||
|
||||
_m, err := p.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
m, ok := _m.(askModel)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unexpected model: %T", _m)
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(m.textInput.Value())
|
||||
|
||||
return text, nil
|
||||
}
|
71
cli/setup/ui/confirm.go
Normal file
71
cli/setup/ui/confirm.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type confirmModel struct {
|
||||
confirmed bool
|
||||
prompt string
|
||||
err error
|
||||
}
|
||||
|
||||
func (m confirmModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m confirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if msg.Runes != nil {
|
||||
switch msg.Runes[0] {
|
||||
case 'y':
|
||||
m.confirmed = true
|
||||
return m, tea.Quit
|
||||
case 'n':
|
||||
m.confirmed = false
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
return m, tea.Quit
|
||||
}
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m confirmModel) View() string {
|
||||
return fmt.Sprintf(
|
||||
"%s y / n (esc to quit)",
|
||||
m.prompt,
|
||||
) + "\n"
|
||||
}
|
||||
|
||||
func Confirm(prompt string) (bool, error) {
|
||||
p := tea.NewProgram(confirmModel{
|
||||
prompt: prompt,
|
||||
err: nil,
|
||||
})
|
||||
|
||||
_m, err := p.Run()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
m, ok := _m.(confirmModel)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unexpected model: %T", _m)
|
||||
}
|
||||
|
||||
return m.confirmed, nil
|
||||
}
|
Reference in New Issue
Block a user