Skip to content

Commit

Permalink
Merge pull request #1338 from stgraber/cli
Browse files Browse the repository at this point in the history
Implement `incus webui`
  • Loading branch information
hallyn authored Oct 25, 2024
2 parents 103b0ff + 9ad6fb5 commit 99c8eb4
Show file tree
Hide file tree
Showing 23 changed files with 766 additions and 309 deletions.
4 changes: 3 additions & 1 deletion client/incus_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/oauth2"

"github.com/lxc/incus/v6/shared/util"
)

// ErrOIDCExpired is returned when the token is expired and we can't retry the request ourselves.
Expand Down Expand Up @@ -281,7 +283,7 @@ func (o *oidcClient) authenticate(issuer string, clientID string, audience strin
fmt.Printf("URL: %s\n", u.String())
fmt.Printf("Code: %s\n\n", resp.UserCode)

_ = openBrowser(u.String())
_ = util.OpenBrowser(u.String())

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT)
defer stop()
Expand Down
34 changes: 0 additions & 34 deletions client/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import (
"net"
"net/http"
"net/url"
"os"
"os/exec"
"runtime"
"strings"
"time"

Expand Down Expand Up @@ -253,34 +250,3 @@ type HTTPTransporter interface {
// Transport what this struct wraps
Transport() *http.Transport
}

func openBrowser(url string) error {
var err error

browser := os.Getenv("BROWSER")
if browser != "" {
if browser == "none" {
return nil
}

err = exec.Command(browser, url).Start()
return err
}

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

if err != nil {
return err
}

return nil
}
4 changes: 4 additions & 0 deletions cmd/incus/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ Custom commands can be defined through aliases, use "incus alias" to control tho
warningCmd := cmdWarning{global: &globalCmd}
app.AddCommand(warningCmd.Command())

// webui sub-command
webuiCmd := cmdWebui{global: &globalCmd}
app.AddCommand(webuiCmd.Command())

// Get help command
app.InitDefaultHelpCmd()
var help *cobra.Command
Expand Down
31 changes: 31 additions & 0 deletions cmd/incus/remote_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ type remoteProxyHandler struct {

api10 *api.Server
api10Etag string

token string
}

func (h remoteProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand All @@ -229,6 +231,35 @@ func (h remoteProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
*h.connections += 1
h.mu.Unlock()

// Basic auth.
if h.token != "" {
// Parse query URL.
values, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return
}

token := values.Get("auth_token")
if token != "" {
tokenCookie := http.Cookie{
Name: "auth_token",
Value: token,
Path: "/",
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}

http.SetCookie(w, &tokenCookie)
} else {
cookie, err := r.Cookie("auth_token")
if err != nil || cookie.Value != h.token {
w.WriteHeader(http.StatusUnauthorized)
return
}
}
}

// Handle /1.0 internally (saves a round-trip).
if r.RequestURI == "/1.0" || strings.HasPrefix(r.RequestURI, "/1.0?project=") {
// Parse query URL.
Expand Down
2 changes: 1 addition & 1 deletion cmd/incus/top.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type cmdTop struct {
// Command is a method of the cmdTop structure that returns a new cobra Command for displaying resource usage per instance.
func (c *cmdTop) Command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("top")
cmd.Use = usage("top", i18n.G("[<remote>:]"))
cmd.Short = i18n.G("Display resource usage info per instance")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Displays CPU usage, memory usage, and disk usage per instance`))
Expand Down
24 changes: 24 additions & 0 deletions cmd/incus/webui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"github.com/spf13/cobra"

cli "github.com/lxc/incus/v6/internal/cmd"
"github.com/lxc/incus/v6/internal/i18n"
)

type cmdWebui struct {
global *cmdGlobal
}

// Command is a method of the cmdWebui structure that returns a new cobra Command for displaying resource usage per instance.
func (c *cmdWebui) Command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("webui", i18n.G("[<remote>:]"))
cmd.Short = i18n.G("Open the web interface")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Open the web interface`))

cmd.RunE = c.Run
return cmd
}
132 changes: 132 additions & 0 deletions cmd/incus/webui_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//go:build !windows

package main

import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"sync"

"github.com/google/uuid"
"github.com/spf13/cobra"

"github.com/lxc/incus/v6/internal/i18n"
"github.com/lxc/incus/v6/shared/util"
)

// Run runs the actual command logic.
func (c *cmdWebui) Run(cmd *cobra.Command, args []string) error {
// Quick checks.
exit, err := c.global.CheckArgs(cmd, args, 0, 1)
if exit {
return err
}

// Parse remote
remote := ""
if len(args) > 0 {
remote = args[0]
}

remoteName, _, err := c.global.conf.ParseRemote(remote)
if err != nil {
return err
}

s, err := c.global.conf.GetInstanceServer(remoteName)
if err != nil {
return err
}

// Create localhost socket.
server, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return fmt.Errorf("Unable to setup TCP socket: %w", err)
}

// Get the connection info.
info, err := s.GetConnectionInfo()
if err != nil {
return err
}

uri, err := url.Parse(info.URL)
if err != nil {
return err
}

// Check that the target supports the UI.
req, err := http.NewRequest("GET", fmt.Sprintf("%s/ui/", info.URL), nil)
if err != nil {
return err
}

resp, err := s.DoHTTP(req)
if err != nil {
return err
}

if resp.StatusCode == http.StatusNotFound {
return errors.New(i18n.G("The server doesn't have a web UI installed"))
}

// Enable keep-alive for proxied connections.
httpClient, err := s.GetHTTPClient()
if err != nil {
return err
}

httpTransport, ok := httpClient.Transport.(*http.Transport)
if ok {
httpTransport.DisableKeepAlives = false
}

// Get server info.
api10, api10Etag, err := s.GetServer()
if err != nil {
return err
}

// Generate credentials.
token := uuid.New().String()

// Handle inbound connections.
transport := remoteProxyTransport{
s: s,
baseURL: uri,
}

connections := uint64(0)
transactions := uint64(0)

handler := remoteProxyHandler{
s: s,
transport: transport,
api10: api10,
api10Etag: api10Etag,

mu: &sync.RWMutex{},
connections: &connections,
transactions: &transactions,

token: token,
}

// Print address.
uiURL := fmt.Sprintf("http://%s/ui?auth_token=%s", server.Addr().String(), token)
fmt.Printf(i18n.G("Web server running at: %s")+"\n", uiURL)

// Attempt to automatically open the web browser.
_ = util.OpenBrowser(uiURL)

// Start the server.
err = http.Serve(server, handler)
if err != nil {
return err
}

return nil
}
16 changes: 16 additions & 0 deletions cmd/incus/webui_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build windows

package main

import (
"errors"

"github.com/spf13/cobra"

"github.com/lxc/incus/v6/internal/i18n"
)

// Run runs the actual command logic.
func (c *cmdWebui) Run(cmd *cobra.Command, args []string) error {
return errors.New(i18n.G("This command isn't supported on Windows"))
}
3 changes: 2 additions & 1 deletion cmd/incusd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"errors"
"fmt"
"io/fs"
"net/http"
"net/http/httputil"
Expand Down Expand Up @@ -71,7 +72,7 @@ func restServer(d *Daemon) *http.Server {
mux.UseEncodedPath() // Allow encoded values in path segments.

uiPath := os.Getenv("INCUS_UI")
uiEnabled := uiPath != "" && util.PathExists(uiPath)
uiEnabled := uiPath != "" && util.PathExists(fmt.Sprintf("%s/index.html", uiPath))
if uiEnabled {
uiHttpDir := uiHttpDir{http.Dir(uiPath)}
mux.PathPrefix("/ui/").Handler(http.StripPrefix("/ui/", http.FileServer(uiHttpDir)))
Expand Down
Loading

0 comments on commit 99c8eb4

Please sign in to comment.