Cli setup command (#3384)

Co-authored-by: Robert Kaussow <xoxys@rknet.org>
This commit is contained in:
Anbraten 2024-03-13 11:08:22 +01:00 committed by GitHub
parent 1026f95f7e
commit 03c891eb93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 665 additions and 43 deletions

View file

@ -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",

View file

@ -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"
)
@ -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
}
@ -49,7 +50,7 @@ func Before(c *cli.Context) error {
}
}()
return nil
return config.Load(c)
}
func After(_ *cli.Context) error {

View file

@ -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)
}

View file

@ -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
cli/setup/setup.go Normal file
View 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
View 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
View 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
View 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
}

View file

@ -29,6 +29,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/cli/registry"
"go.woodpecker-ci.org/woodpecker/v2/cli/repo"
"go.woodpecker-ci.org/woodpecker/v2/cli/secret"
"go.woodpecker-ci.org/woodpecker/v2/cli/setup"
"go.woodpecker-ci.org/woodpecker/v2/cli/update"
"go.woodpecker-ci.org/woodpecker/v2/cli/user"
"go.woodpecker-ci.org/woodpecker/v2/version"
@ -58,6 +59,7 @@ func newApp() *cli.App {
lint.Command,
loglevel.Command,
cron.Command,
setup.Command,
update.Command,
}

15
go.mod
View file

@ -7,10 +7,13 @@ require (
codeberg.org/6543/go-yaml2json v1.0.0
codeberg.org/6543/xyaml v1.1.0
github.com/6543/logfile-open v1.2.1
github.com/adrg/xdg v0.4.0
github.com/alessio/shellescape v1.4.2
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/caddyserver/certmagic v0.20.0
github.com/cenkalti/backoff/v4 v4.2.1
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/distribution/reference v0.5.0
github.com/docker/cli v24.0.9+incompatible
github.com/docker/docker v24.0.9+incompatible
@ -50,6 +53,7 @@ require (
github.com/urfave/cli/v2 v2.27.1
github.com/xanzy/go-gitlab v0.97.0
github.com/xeipuuv/gojsonschema v1.2.0
github.com/zalando/go-keyring v0.2.3
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.19.0
golang.org/x/net v0.21.0
@ -72,13 +76,17 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
@ -97,6 +105,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
@ -120,6 +129,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mholt/acmez v1.2.0 // indirect
@ -127,6 +137,9 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
@ -135,7 +148,7 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect

37
go.sum
View file

@ -20,8 +20,12 @@ github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6Xge
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -38,6 +42,12 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
@ -49,6 +59,8 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@ -57,6 +69,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -81,8 +95,9 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/expr-lang/expr v1.16.1 h1:Na8CUcMdyGbnNpShY7kzcHCU7WqxuL+hnxgHZ4vaz/A=
@ -138,6 +153,8 @@ github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@ -299,6 +316,9 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
@ -325,6 +345,12 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@ -364,9 +390,10 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -438,6 +465,8 @@ github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsr
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms=
github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
@ -537,11 +566,13 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

1
web/components.d.ts vendored
View file

@ -110,6 +110,7 @@ declare module 'vue' {
Tabs: typeof import('./src/components/layout/scaffold/Tabs.vue')['default']
TextField: typeof import('./src/components/form/TextField.vue')['default']
UserAPITab: typeof import('./src/components/user/UserAPITab.vue')['default']
UserCLIAndAPITab: typeof import('./src/components/user/UserCLIAndAPITab.vue')['default']
UserGeneralTab: typeof import('./src/components/user/UserGeneralTab.vue')['default']
UserSecretsTab: typeof import('./src/components/user/UserSecretsTab.vue')['default']
Warning: typeof import('./src/components/atomic/Warning.vue')['default']

View file

@ -486,15 +486,13 @@
"pr_warning": "Please be careful with this option as a bad actor can submit a malicious pull request that exposes your secrets."
}
},
"api": {
"api": "API",
"desc": "Personal Access Token and API usage",
"cli_and_api": {
"cli_and_api": "CLI & API",
"desc": "Personal Access Token, CLI and API usage",
"token": "Personal Access Token",
"shell_setup": "Shell setup",
"api_usage": "Example API Usage",
"cli_usage": "Example CLI Usage",
"dl_cli": "Download CLI",
"shell_setup_before": "do shell setup steps before",
"download_cli": "Download CLI",
"reset_token": "Reset token",
"swagger_ui": "Swagger UI"
}
@ -508,5 +506,12 @@
"running_version": "You are running Woodpecker {0}",
"update_woodpecker": "Please update your Woodpecker instance to {0}",
"global_level_secret": "global secret",
"org_level_secret": "organization secret"
"org_level_secret": "organization secret",
"login_to_cli": "Login to CLI",
"login_to_cli_description": "By continuing you will be logged in to the CLI.",
"abort": "Abort",
"cli_login_success": "Login to CLI successful",
"cli_login_failed": "Login to CLI failed",
"cli_login_denied": "Login to CLI denied",
"return_to_cli": "You can now close this tab and return to the CLI."
}

View file

@ -1,43 +1,38 @@
<template>
<Settings :title="$t('user.settings.api.api')" :desc="$t('user.settings.api.desc')">
<InputField :label="$t('user.settings.api.token')">
<Settings :title="$t('user.settings.cli_and_api.cli_and_api')" :desc="$t('user.settings.cli_and_api.desc')">
<InputField :label="$t('user.settings.cli_and_api.cli_usage')">
<template #titleActions>
<Button class="ml-auto" :text="$t('user.settings.api.reset_token')" @click="resetToken" />
<a :href="cliDownload" target="_blank" class="ml-4 text-wp-link-100 hover:text-wp-link-200">{{
$t('user.settings.cli_and_api.download_cli')
}}</a>
</template>
<pre class="code-box">{{ usageWithCli }}</pre>
</InputField>
<InputField :label="$t('user.settings.cli_and_api.token')">
<template #titleActions>
<Button class="ml-auto" :text="$t('user.settings.cli_and_api.reset_token')" @click="resetToken" />
</template>
<pre class="code-box">{{ token }}</pre>
</InputField>
<InputField :label="$t('user.settings.api.shell_setup')">
<pre class="code-box">{{ usageWithShell }}</pre>
</InputField>
<InputField :label="$t('user.settings.api.api_usage')">
<InputField :label="$t('user.settings.cli_and_api.api_usage')">
<template #titleActions>
<a
v-if="enableSwagger"
:href="`${address}/swagger/index.html`"
target="_blank"
class="ml-4 text-wp-link-100 hover:text-wp-link-200"
>{{ $t('user.settings.api.swagger_ui') }}</a
>{{ $t('user.settings.cli_and_api.swagger_ui') }}</a
>
</template>
<pre class="code-box">{{ usageWithCurl }}</pre>
</InputField>
<InputField :label="$t('user.settings.api.cli_usage')">
<template #titleActions>
<a :href="cliDownload" target="_blank" class="ml-4 text-wp-link-100 hover:text-wp-link-200">{{
$t('user.settings.api.dl_cli')
}}</a>
</template>
<pre class="code-box">{{ usageWithCli }}</pre>
</InputField>
</Settings>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import InputField from '~/components/form/InputField.vue';
@ -45,7 +40,6 @@ import Settings from '~/components/layout/Settings.vue';
import useApiClient from '~/compositions/useApiClient';
import useConfig from '~/compositions/useConfig';
const { t } = useI18n();
const { rootPath, enableSwagger } = useConfig();
const apiClient = useApiClient();
@ -57,17 +51,15 @@ onMounted(async () => {
const address = `${window.location.protocol}//${window.location.host}${rootPath}`; // port is included in location.host
const usageWithShell = computed(() => {
const usageWithCurl = computed(() => {
let usage = `export WOODPECKER_SERVER="${address}"\n`;
usage += `export WOODPECKER_TOKEN="${token.value}"\n`;
usage += `\n`;
usage += `# curl -i \${WOODPECKER_SERVER}/api/user -H "Authorization: Bearer \${WOODPECKER_TOKEN}"`;
return usage;
});
const usageWithCurl = `# ${t(
'user.settings.api.shell_setup_before',
)}\ncurl -i \${WOODPECKER_SERVER}/api/user -H "Authorization: Bearer \${WOODPECKER_TOKEN}"`;
const usageWithCli = `# ${t('user.settings.api.shell_setup_before')}\nwoodpecker info`;
const usageWithCli = `# woodpecker setup --server-url ${address}`;
const cliDownload = 'https://github.com/woodpecker-ci/woodpecker/releases';

View file

@ -166,6 +166,11 @@ const routes: RouteRecordRaw[] = [
meta: { blank: true },
props: true,
},
{
path: `${rootPath}/cli/auth`,
component: () => import('~/views/cli/Auth.vue'),
meta: { authentication: 'required' },
},
// TODO: deprecated routes => remove after some time
{

View file

@ -8,8 +8,8 @@
<Tab id="secrets" :title="$t('user.settings.secrets.secrets')">
<UserSecretsTab />
</Tab>
<Tab id="api" :title="$t('user.settings.api.api')">
<UserAPITab />
<Tab id="cli-and-api" :title="$t('user.settings.cli_and_api.cli_and_api')">
<UserCLIAndAPITab />
</Tab>
</Scaffold>
</template>
@ -17,7 +17,7 @@
<script lang="ts" setup>
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import UserAPITab from '~/components/user/UserAPITab.vue';
import UserCLIAndAPITab from '~/components/user/UserCLIAndAPITab.vue';
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
import UserSecretsTab from '~/components/user/UserSecretsTab.vue';
import useConfig from '~/compositions/useConfig';

View file

@ -0,0 +1,80 @@
<template>
<div class="flex flex-col gap-4 m-auto">
<div class="text-center text-wp-text-100">
<img src="../../assets/logo.svg" alt="CLI" class="w-32 m-auto mb-8" />
<template v-if="state === 'confirm'">
<h1 class="text-4xl font-bold">{{ $t('login_to_cli') }}</h1>
<p class="text-2xl">{{ $t('login_to_cli_description') }}</p>
</template>
<template v-else-if="state === 'success'">
<h1 class="text-4xl font-bold">{{ $t('cli_login_success') }}</h1>
<p class="text-2xl">{{ $t('return_to_cli') }}</p>
</template>
<template v-else-if="state === 'failed'">
<h1 class="text-4xl font-bold mt-4">{{ $t('cli_login_failed') }}</h1>
<p class="text-2xl">{{ $t('return_to_cli') }}</p>
</template>
<template v-else-if="state === 'denied'">
<h1 class="text-4xl font-bold mt-4">{{ $t('cli_login_denied') }}</h1>
<p class="text-2xl">{{ $t('return_to_cli') }}</p>
</template>
</div>
<div v-if="state === 'confirm'" class="flex gap-4 justify-center">
<Button :text="$t('login_to_cli')" color="green" @click="sendToken(false)" />
<Button :text="$t('abort')" color="red" @click="abortLogin" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import Button from '~/components/atomic/Button.vue';
import useApiClient from '~/compositions/useApiClient';
const apiClient = useApiClient();
const route = useRoute();
const { t } = useI18n();
const state = ref<'confirm' | 'success' | 'failed' | 'denied'>('confirm');
async function sendToken(abort = false) {
const port = route.query.port as string;
if (!port) {
throw new Error('Unexpected: port not found');
}
const address = `http://localhost:${port}`;
const token = abort ? '' : await apiClient.getToken();
const resp = await fetch(`${address}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
});
if (abort) {
state.value = 'denied';
window.close();
return;
}
const data = (await resp.json()) as { ok: string };
if (data.ok === 'true') {
state.value = 'success';
} else {
state.value = 'failed';
// eslint-disable-next-line no-alert
alert(t('cli_login_failed'));
}
}
async function abortLogin() {
await sendToken(true);
}
</script>