Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @oschwartz10612 @miloschwartz
25 changes: 25 additions & 0 deletions cmd/apply/blueprint/blueprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/fosrl/cli/internal/api"
Expand Down Expand Up @@ -86,6 +87,8 @@ func applyBlueprintMain(cmd *cobra.Command, opts BlueprintCmdOpts) error {
return err
}

blueprintContents = interpolateBlueprint(blueprintContents)

_, err = api.ApplyBlueprint(account.OrgID, opts.Name, string(blueprintContents))
if err != nil {
logger.Error("Error: failed to apply blueprint: %v", err)
Expand All @@ -96,3 +99,25 @@ func applyBlueprintMain(cmd *cobra.Command, opts BlueprintCmdOpts) error {

return nil
}

// interpolateBlueprint finds all {{...}} tokens in the raw blueprint bytes and
// replaces recognised schemes with their resolved values. Currently supported:
//
// - env.<VAR> – replaced with the value of the named environment variable
//
// Any token that does not match a supported scheme is left as-is so that
// future schemes are preserved rather than silently dropped.
func interpolateBlueprint(data []byte) []byte {
re := regexp.MustCompile(`\{\{([^}]+)\}\}`)
return re.ReplaceAllFunc(data, func(match []byte) []byte {
inner := strings.TrimSpace(string(match[2 : len(match)-2]))

if strings.HasPrefix(inner, "env.") {
varName := strings.TrimPrefix(inner, "env.")
return []byte(os.Getenv(varName))
}

// unrecognised scheme – leave the token untouched
return match
})
}
6 changes: 4 additions & 2 deletions cmd/authdaemon/authdaemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ func AuthDaemonCmd() *cobra.Command {
Short: "Start the auth daemon",
Long: "Start the auth daemon for remote SSH authentication",
PreRunE: func(c *cobra.Command, args []string) error {
if opts.PreSharedKey == "" {
opts.PreSharedKey = os.Getenv("AUTH_DAEMON_PRE_SHARED_KEY")
}
if opts.PreSharedKey == "" {
return errPresharedKeyRequired
}
Expand All @@ -53,8 +56,7 @@ func AuthDaemonCmd() *cobra.Command {
},
}

cmd.Flags().StringVar(&opts.PreSharedKey, "pre-shared-key", "", "Preshared key required for all requests to the auth daemon (required)")
cmd.MarkFlagRequired("pre-shared-key")
cmd.Flags().StringVar(&opts.PreSharedKey, "pre-shared-key", "", "Preshared key required for all requests to the auth daemon (required). Can also be set via AUTH_DAEMON_PRE_SHARED_KEY env var")
cmd.Flags().IntVar(&opts.Port, "port", defaultPort, "TCP listen port for the HTTPS server")
cmd.Flags().StringVar(&opts.PrincipalsFile, "principals-file", defaultPrincipalsPath, "Path to the principals file")
cmd.Flags().StringVar(&opts.CACertPath, "ca-cert-path", defaultCACertPath, "Path to the CA certificate file")
Expand Down
46 changes: 46 additions & 0 deletions cmd/list/aliases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package list

import (
"fmt"

"github.com/fosrl/cli/internal/api"
"github.com/fosrl/cli/internal/config"
"github.com/fosrl/cli/internal/utils"
"github.com/spf13/cobra"
)

const aliasesPageSize = 1000

func aliasesCmd() *cobra.Command {
return &cobra.Command{
Use: "aliases",
Short: "Print every private host alias you can reach in the current organization",
Long: `Lists all private site aliases you have access to in your selected organization—one name per line.`,
RunE: func(cmd *cobra.Command, args []string) error {
apiClient := api.FromContext(cmd.Context())
accountStore := config.AccountStoreFromContext(cmd.Context())

orgID, err := utils.ResolveOrgID(accountStore, "")
if err != nil {
return err
}

for page := 1; ; page++ {
data, err := apiClient.ListUserResourceAliases(orgID, page, aliasesPageSize)
if err != nil {
return fmt.Errorf("list user resource aliases: %w", err)
}
for _, a := range data.Aliases {
fmt.Println(a)
}
if len(data.Aliases) == 0 {
break
}
if len(data.Aliases) < aliasesPageSize {
break
}
}
return nil
},
}
}
13 changes: 13 additions & 0 deletions cmd/list/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package list

import "github.com/spf13/cobra"

// ListCmd is the parent `list` command for listing server-side items.
func ListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List resources and other items from the server",
}
cmd.AddCommand(aliasesCmd())
return cmd
}
6 changes: 4 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/fosrl/cli/cmd/auth/logout"
"github.com/fosrl/cli/cmd/authdaemon"
"github.com/fosrl/cli/cmd/down"
"github.com/fosrl/cli/cmd/list"
"github.com/fosrl/cli/cmd/logs"
selectcmd "github.com/fosrl/cli/cmd/select"
"github.com/fosrl/cli/cmd/ssh"
Expand Down Expand Up @@ -49,7 +50,8 @@ func RootCommand(initResources bool) (*cobra.Command, error) {
}
cmd.AddCommand(apply.ApplyCommand())
cmd.AddCommand(selectcmd.SelectCmd())

cmd.AddCommand(list.ListCmd())

// Platform-specific commands - nil on unsupported platforms
if upCmd := up.UpCmd(); upCmd != nil {
cmd.AddCommand(upCmd)
Expand All @@ -63,7 +65,7 @@ func RootCommand(initResources bool) (*cobra.Command, error) {
if statusCmd := status.StatusCmd(); statusCmd != nil {
cmd.AddCommand(statusCmd)
}

cmd.AddCommand(ssh.SSHCmd())
cmd.AddCommand(update.UpdateCmd())
cmd.AddCommand(version.VersionCmd())
Expand Down
45 changes: 19 additions & 26 deletions cmd/ssh/jit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"time"

"github.com/fosrl/cli/internal/api"
"github.com/fosrl/cli/internal/config"
"github.com/fosrl/cli/internal/sshkeys"
)

Expand All @@ -29,44 +28,38 @@ func GenerateAndSignKey(client *api.Client, orgID string, resourceID string) (pr
if err != nil {
return "", "", "", nil, fmt.Errorf("SSH error: %w", err)
}
messageID := initResp.MessageID
if messageID == 0 {

// Collect all message IDs to poll (support both single and multiple).
var messageIDs []int64
if len(initResp.MessageIDs) > 0 {
messageIDs = initResp.MessageIDs
} else if initResp.MessageID != 0 {
messageIDs = []int64{initResp.MessageID}
} else {
return "", "", "", nil, fmt.Errorf("SSH error: API did not return a message ID")
}

time.Sleep(pollInitialDelay)

interval := pollStartInterval
for i := 0; i <= pollBackoffSteps; i++ {
msg, pollErr := client.GetRoundTripMessage(messageID)
if pollErr != nil {
return "", "", "", nil, fmt.Errorf("SSH error: poll: %w", pollErr)
}
if msg.Complete {
if msg.Error != nil && *msg.Error != "" {
return "", "", "", nil, fmt.Errorf("SSH error: %s", *msg.Error)
for _, messageID := range messageIDs {
msg, pollErr := client.GetRoundTripMessage(messageID)
if pollErr != nil {
return "", "", "", nil, fmt.Errorf("SSH error: poll: %w", pollErr)
}
if msg.Complete {
if msg.Error != nil && *msg.Error != "" {
return "", "", "", nil, fmt.Errorf("SSH error: %s", *msg.Error)
}
return privPEM, pubKey, initResp.Certificate, initResp, nil
}
return privPEM, pubKey, initResp.Certificate, initResp, nil
}
if i < pollBackoffSteps {
time.Sleep(interval)
interval *= 2
}
}
return "", "", "", nil, fmt.Errorf("SSH error: timed out waiting for round-trip message")
}

// ResolveOrgID returns orgID from the flag or the active account. Returns empty string and nil error if both are empty.
func ResolveOrgID(accountStore *config.AccountStore, flagOrgID string) (string, error) {
if flagOrgID != "" {
return flagOrgID, nil
}
active, err := accountStore.ActiveAccount()
if err != nil || active == nil {
return "", errOrgRequired
}
if active.OrgID == "" {
return "", errOrgRequired
}
return active.OrgID, nil
return "", "", "", nil, fmt.Errorf("SSH error: timed out waiting for round-trip message")
}
2 changes: 1 addition & 1 deletion cmd/ssh/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func SignCmd() *cobra.Command {
apiClient := api.FromContext(c.Context())
accountStore := config.AccountStoreFromContext(c.Context())

orgID, err := ResolveOrgID(accountStore, "")
orgID, err := utils.ResolveOrgID(accountStore, "")
if err != nil {
logger.Error("%v", err)
os.Exit(1)
Expand Down
64 changes: 47 additions & 17 deletions cmd/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import (
"errors"
"os"
"runtime"
"time"

"github.com/fosrl/cli/internal/api"
"github.com/fosrl/cli/internal/config"
"github.com/fosrl/cli/internal/logger"
"github.com/fosrl/cli/internal/olm"
"github.com/fosrl/cli/internal/utils"
"github.com/spf13/cobra"
)

var (
errHostnameRequired = errors.New("API did not return a hostname for the connection")
errResourceIDRequired = errors.New("Resource (alias or identifier) is required; example: pangolin ssh my-server.internal")
errOrgRequired = errors.New("Organization is required")
errNoClientRunning = errors.New("No client is currently running. Start the client first with `pangolin up`")
errNoClientRunningWindows = errors.New("No client is currently running. Start the client first in the system tray")
)
Expand All @@ -39,27 +40,22 @@ func SSHCmd() *cobra.Command {
return nil
},
Run: func(c *cobra.Command, args []string) {
if runtime.GOOS != "windows" {
client := olm.NewClient("")
if !client.IsRunning() {
logger.Error("%v", errNoClientRunning)
os.Exit(1)
}
} else {
// check if the named pipe exists by trying to open it. If it doesn't exist, the client is not running.
pipePath := `\\.\pipe\pangolin-olm`
pipeFile, err := os.Open(pipePath)
if err != nil {
logger.Error("%v", errNoClientRunningWindows)
os.Exit(1)
}
pipeFile.Close()
client := olm.NewClient("")
if !client.IsRunning() {
logger.Error("%v", errNoClientRunning)
os.Exit(1)
}

apiClient := api.FromContext(c.Context())
accountStore := config.AccountStoreFromContext(c.Context())

orgID, err := ResolveOrgID(accountStore, "")
// init a jit connection to the site if we need to because we might not be connected
_, err := client.JITConnectByResourceID(opts.ResourceID)
if err != nil {
logger.Warning("%v", err) // we pass through this warning for backward compatibility with older olm api servers
}

orgID, err := utils.ResolveOrgID(accountStore, "")
if err != nil {
logger.Error("%v", err)
os.Exit(1)
Expand All @@ -74,6 +70,40 @@ func SSHCmd() *cobra.Command {
logger.Error("%v", errHostnameRequired)
os.Exit(1)
}

// logger.Info("signData: %+v", signData)

siteIDs := []int{}
if signData.SiteID != 0 {
siteIDs = append(siteIDs, signData.SiteID)
}
for _, id := range signData.SiteIDs {
if id != 0 {
siteIDs = append(siteIDs, id)
}
}

if len(siteIDs) > 0 { // older versions of the server did not send back the site id so we need to check for backward compatibility
for _, siteID := range siteIDs {
deadline := time.Now().Add(15 * time.Second)
connected := false
for time.Now().Before(deadline) {
status, err := client.GetStatus()
if err == nil {
if peer, ok := status.PeerStatuses[siteID]; ok && peer.Connected {
connected = true
// logger.Info("site is connected")
break
}
}
time.Sleep(500 * time.Millisecond)
}
if !connected {
logger.Error("site %d is not connected; timed out waiting for connection", siteID)
os.Exit(1)
}
}
}

passThrough := args[1:]
runOpts := RunOpts{
Expand Down
17 changes: 15 additions & 2 deletions cmd/status/status_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,23 @@
package status

import (
"github.com/fosrl/cli/cmd/status/client"
"github.com/spf13/cobra"
)

// StatusCmd returns nil on Windows as this command is not supported.
func StatusCmd() *cobra.Command {
return nil
// If no subcommand is specified, run the `client`
// subcommand by default.
cmd := client.ClientStatusCmd()

cmd.Use = "status"
cmd.Short = "Status commands"
cmd.Long = `View status information.

If ran with no subcommand, 'client' is passed.
`

cmd.AddCommand(client.ClientStatusCmd())

return cmd
}
15 changes: 12 additions & 3 deletions cmd/update/update_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ func UpdateCmd() *cobra.Command {
func updateMain() error {
logger.Info("Updating Pangolin CLI...")

// Execute: curl -fsSL https://pangolin.net/get-cli.sh | bash
updateCmd := exec.Command("sh", "-c", "curl -fsSL https://static.pangolin.net/get-cli.sh | bash")
// Get the path of the current running binary
execPath, err := os.Executable()
if err != nil {
logger.Error("Failed to get current executable path: %v", err)
return err
}

logger.Debug("Current executable path: %s", execPath)

// Execute: curl -fsSL https://static.pangolin.net/get-cli.sh | bash -s -- --path /current/binary/path
updateCmd := exec.Command("sh", "-c", "curl -fsSL https://static.pangolin.net/get-cli.sh | bash -s -- --path "+execPath)
updateCmd.Stdin = os.Stdin
updateCmd.Stdout = os.Stdout
updateCmd.Stderr = os.Stderr
Expand All @@ -42,4 +51,4 @@ func updateMain() error {
logger.Success("Pangolin CLI updated successfully!")

return nil
}
}
Loading
Loading