-
-
Notifications
You must be signed in to change notification settings - Fork 385
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Robert Kaussow <[email protected]>
- Loading branch information
Showing
17 changed files
with
664 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.