diff --git a/action/repo/add.go b/action/repo/add.go index 56c7f067..9763d37e 100644 --- a/action/repo/add.go +++ b/action/repo/add.go @@ -57,6 +57,17 @@ func (c *Config) Add(client *vela.Client) error { return err } + err = c.Install(client, repo) + if err != nil { + return err + } + + // client.Install + + // todo: this just redirects to the auth callback + // we need this to redirect to the server + // the server then sees whether or not it was a cli auth flow and it + // handle the output based off the provided configuration switch c.Output { case output.DriverDump: diff --git a/action/repo/install.go b/action/repo/install.go new file mode 100644 index 00000000..d35fa8c7 --- /dev/null +++ b/action/repo/install.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 + +package repo + +import ( + "fmt" + "strconv" + + "github.com/cli/browser" + "github.com/sirupsen/logrus" + + "github.com/go-vela/sdk-go/vela" + api "github.com/go-vela/server/api/types" +) + +// Install executes the repo app installation process, which should redirect to the SCM web flow. +func (c *Config) Install(client *vela.Client, repo *api.Repo) error { + logrus.Debug("executing app install for repo configuration") + + // start the local server + err := c.StartServer() + if err != nil { + return err + } + + // request the install URL from the server + installHTMLURL, _, err := client.Repo.InstallHTMLURL(repo.GetOrg(), repo.GetName()) + if err != nil { + return err + } + + // attach contextual information like cli type and local server port + *installHTMLURL = fmt.Sprintf( + "%s&type=%s&port=%s", + *installHTMLURL, + "cli", strconv.Itoa(c.server.Port()), + ) + + // launch the login process in the browser + err = browser.OpenURL(*installHTMLURL) + if err != nil { + return err + } + + // capture result from local server + err = c.WaitForResult(client) + if err != nil { + return err + } + + return nil +} + +// WaitForResult will wait for the callback and handle the response. +func (c *Config) WaitForResult(client *vela.Client) error { + logrus.Debug("waiting for app installation server callback") + + // waiting for local server to receive the redirect + _, err := c.server.WaitForResult() + if err != nil { + return err + } + + return nil +} + +// StartServer starts a local server as part of the +// auth flow. It will handle the callback. +func (c *Config) StartServer() error { + logrus.Debug("starting local server") + + // set up the local server to capture the redirect from auth + server, err := bindLocalServer() + if err != nil { + return err + } + + logrus.Debug("local server is bound") + + // store on struct + c.server = server + + // start the server up + go func() { + _ = c.server.Serve() + }() + + logrus.Debug("local server started") + + return nil +} diff --git a/action/repo/local_server.go b/action/repo/local_server.go new file mode 100644 index 00000000..2e877815 --- /dev/null +++ b/action/repo/local_server.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +// mostly taken from https://github.com/cli/oauth/tree/v0.8.0/webapp + +package repo + +import ( + "fmt" + "io" + "net" + "net/http" +) + +type CodeResponse struct { + Code string + State string +} + +type localServer struct { + CallbackPath string + WriteSuccessHTML func(w io.Writer) + + resultChan chan (CodeResponse) + listener net.Listener +} + +// bindLocalServer initializes a LocalServer that will listen on a randomly available TCP port. +func bindLocalServer() (*localServer, error) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + return nil, err + } + + return &localServer{ + listener: listener, + resultChan: make(chan CodeResponse, 1), + }, nil +} + +func (s *localServer) Port() int { + return s.listener.Addr().(*net.TCPAddr).Port +} + +func (s *localServer) Close() error { + return s.listener.Close() +} + +func (s *localServer) Serve() error { + //nolint:gosec // TODO: add a way to timeout the local server + return http.Serve(s.listener, s) +} + +func (s *localServer) WaitForResult() (CodeResponse, error) { + return <-s.resultChan, nil +} + +// ServeHTTP implements http.Handler. +func (s *localServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if s.CallbackPath != "" && r.URL.Path != s.CallbackPath { + w.WriteHeader(http.StatusNotFound) + return + } + + defer func() { + _ = s.Close() + }() + + params := r.URL.Query() + s.resultChan <- CodeResponse{ + Code: params.Get("code"), + State: params.Get("state"), + } + + w.Header().Add("content-type", "text/html") + + if s.WriteSuccessHTML != nil { + s.WriteSuccessHTML(w) + } else { + defaultSuccessHTML(w) + } +} + +func defaultSuccessHTML(w io.Writer) { + fmt.Fprint(w, authSuccess) +} diff --git a/action/repo/repo.go b/action/repo/repo.go index 80bfbff2..0da53822 100644 --- a/action/repo/repo.go +++ b/action/repo/repo.go @@ -27,4 +27,6 @@ type Config struct { PerPage int Output string Color output.ColorOptions + + server *localServer } diff --git a/action/repo/success_page.go b/action/repo/success_page.go new file mode 100644 index 00000000..3f9393be --- /dev/null +++ b/action/repo/success_page.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +package repo + +// authSuccess provides the HTML for rendering +// a message in the browser after successfully +// completing the oauth workflow. +const authSuccess = ` + + +Success: Vela CLI + + + + +
+

Successfully authenticated with Vela!

+

You may now close this tab and return to the terminal.

+
+ +` diff --git a/go.mod b/go.mod index e9470cb5..ada46f3c 100644 --- a/go.mod +++ b/go.mod @@ -160,3 +160,11 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/go-vela/types => ../types + +replace github.com/go-vela/server => ../server + +replace github.com/go-vela/worker => ../worker + +replace github.com/go-vela/sdk-go => ../sdk-go diff --git a/go.sum b/go.sum index 1b2816b0..201ac795 100644 --- a/go.sum +++ b/go.sum @@ -139,14 +139,6 @@ github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27 github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-vela/sdk-go v0.25.1 h1:aEWH88BjLmV5s3mWPbi7OeGY5u8IQ9xQNF8CWH3e5HU= -github.com/go-vela/sdk-go v0.25.1/go.mod h1:98yxwcqGQidNke/QsbG8WeYJG56AImc9o9dcKiXBQ5k= -github.com/go-vela/server v0.25.1 h1:KM3g5ZD3N6SnttnkfOyJjS2utbL6baKx0mGSJLCEf0c= -github.com/go-vela/server v0.25.1/go.mod h1:QZ9troVMUpDCAdUxxAquHqahkeSwp9Lmx6a47ao0gGs= -github.com/go-vela/types v0.25.1 h1:DCPHv1+ouqldjfsjfcVTcm/Yyd0OwazIfxYyR+GwpMo= -github.com/go-vela/types v0.25.1/go.mod h1:5+MHUI9ZSY2Uz1cTJa64FWUv8jKzzUUq96UQTokGJzs= -github.com/go-vela/worker v0.25.1 h1:vpD82MEZUBgloNiXUa0KtZzxz9+V5+vOusiFdQbE6lM= -github.com/go-vela/worker v0.25.1/go.mod h1:tZ4AEUc4kf5umXFZ3/9piQhqe4E8QkmxjrhoJ2/9rt8= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=