Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/eng 2763 show pro auth status #892

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
94 changes: 88 additions & 6 deletions cmd/pro/list.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package pro

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"

"github.com/loft-sh/devpod/cmd/flags"
"github.com/loft-sh/devpod/pkg/binaries"
"github.com/loft-sh/devpod/pkg/config"
"github.com/loft-sh/devpod/pkg/provider"
"github.com/loft-sh/devpod/pkg/workspace"
Expand All @@ -21,6 +27,7 @@ type ListCmd struct {
flags.GlobalFlags

Output string
Login bool
}

// NewListCmd creates a new command
Expand All @@ -39,6 +46,7 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command {
}

listCmd.Flags().StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain")
listCmd.Flags().BoolVar(&cmd.Login, "login", false, "Check if the user is logged into the pro instance")
return listCmd
}

Expand All @@ -57,24 +65,45 @@ func (cmd *ListCmd) Run(ctx context.Context) error {
if cmd.Output == "plain" {
tableEntries := [][]string{}
for _, proInstance := range proInstances {
tableEntries = append(tableEntries, []string{
entry := []string{
proInstance.Host,
proInstance.Provider,
time.Since(proInstance.CreationTimestamp.Time).Round(1 * time.Second).String(),
})
}
if cmd.Login {
err = checkLogin(ctx, devPodConfig, proInstance)
entry = append(entry, fmt.Sprintf("%t", err == nil))
}

tableEntries = append(tableEntries, entry)
}
sort.SliceStable(tableEntries, func(i, j int) bool {
return tableEntries[i][0] < tableEntries[j][0]
})

table.PrintTable(log.Default, []string{
tableHeaders := []string{
"Host",
"Provider",
"Age",
}, tableEntries)
}
if cmd.Login {
tableHeaders = append(tableHeaders, "Authenticated")
}

table.PrintTable(log.Default, tableHeaders, tableEntries)
} else if cmd.Output == "json" {
tableEntries := []*provider.ProInstance{}
tableEntries = append(tableEntries, proInstances...)
tableEntries := []*proTableEntry{}
for _, proInstance := range proInstances {
entry := &proTableEntry{ProInstance: proInstance}
if cmd.Login {
err = checkLogin(ctx, devPodConfig, proInstance)
isAuthenticated := err == nil
entry.Authenticated = &isAuthenticated
}

tableEntries = append(tableEntries, entry)
}

sort.SliceStable(tableEntries, func(i, j int) bool {
return tableEntries[i].Host < tableEntries[j].Host
})
Expand All @@ -89,3 +118,56 @@ func (cmd *ListCmd) Run(ctx context.Context) error {

return nil
}

type proTableEntry struct {
*provider.ProInstance

Authenticated *bool `json:"authenticated,omitempty"`
}

func checkLogin(ctx context.Context, devPodConfig *config.Config, proInstance *provider.ProInstance) error {
providerConfig, err := provider.LoadProviderConfig(devPodConfig.DefaultContext, proInstance.Provider)
if err != nil {
return err
}

providerBinaries, err := binaries.GetBinaries(devPodConfig.DefaultContext, providerConfig)
if err != nil {
return fmt.Errorf("get provider binaries: %w", err)
} else if providerBinaries[LOFT_PROVIDER_BINARY] == "" {
return fmt.Errorf("provider is missing %s binary", LOFT_PROVIDER_BINARY)
}

providerDir, err := provider.GetProviderDir(devPodConfig.DefaultContext, providerConfig.Name)
if err != nil {
return err
}

args := []string{
"login",
"--log-output=raw",
}

extraEnv := []string{
"LOFT_SKIP_VERSION_CHECK=true",
"LOFT_CONFIG=" + filepath.Join(providerDir, "loft-config.json"),
}

stdout := &bytes.Buffer{}

// start the command
loginCmd := exec.CommandContext(ctx, providerBinaries[LOFT_PROVIDER_BINARY], args...)
loginCmd.Env = os.Environ()
loginCmd.Env = append(loginCmd.Env, extraEnv...)
loginCmd.Stdout = stdout
err = loginCmd.Run()
if err != nil {
return fmt.Errorf("run login command: %w", err)
}

if stdout.Len() > 0 && strings.Contains(stdout.String(), "Not logged in") {
return fmt.Errorf("not logged into %s", proInstance.Host)
}

return nil
}
1 change: 1 addition & 0 deletions desktop/src/client/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ export const DEVPOD_FLAG_TIMEOUT = "--timeout"
export const DEVPOD_FLAG_DEVCONTAINER_PATH = "--devcontainer-path"
export const DEVPOD_FLAG_WORKSPACE_ID = "--workspace-id"
export const DEVPOD_FLAG_WORKSPACE_UID = "--workspace-uid"
export const DEVPOD_FLAG_LOGIN = "--login"

export const DEVPOD_UI_ENV_VAR = "DEVPOD_UI"
6 changes: 3 additions & 3 deletions desktop/src/client/pro/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Result, ResultError } from "../../lib"
import { TImportWorkspaceConfig, TProID, TProInstance } from "../../types"
import { TImportWorkspaceConfig, TListProInstancesConfig, TProID, TProInstance } from "../../types"
import { TDebuggable, TStreamEventListenerFn } from "../types"
import { ProCommands } from "./proCommands"

Expand All @@ -19,8 +19,8 @@ export class ProClient implements TDebuggable {
return ProCommands.Login(host, providerName, accessKey, listener)
}

public async listAll(): Promise<Result<readonly TProInstance[]>> {
return ProCommands.ListProInstances()
public async listAll(config: TListProInstancesConfig): Promise<Result<readonly TProInstance[]>> {
return ProCommands.ListProInstances(config)
}

public async remove(id: TProID) {
Expand Down
9 changes: 7 additions & 2 deletions desktop/src/client/pro/proCommands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Result, ResultError, Return, getErrorFromChildProcess } from "@/lib"
import { TImportWorkspaceConfig, TProID, TProInstance } from "@/types"
import { TImportWorkspaceConfig, TListProInstancesConfig, TProID, TProInstance } from "@/types"
import { Command, isOk, serializeRawOptions, toFlagArg } from "../command"
import {
DEVPOD_COMMAND_DELETE,
Expand All @@ -11,6 +11,7 @@ import {
DEVPOD_FLAG_DEBUG,
DEVPOD_FLAG_JSON_LOG_OUTPUT,
DEVPOD_FLAG_JSON_OUTPUT,
DEVPOD_FLAG_LOGIN,
DEVPOD_FLAG_PROVIDER,
DEVPOD_FLAG_USE,
DEVPOD_FLAG_WORKSPACE_ID,
Expand Down Expand Up @@ -62,11 +63,15 @@ export class ProCommands {
}
}

static async ListProInstances(): Promise<Result<readonly TProInstance[]>> {
static async ListProInstances(
config: TListProInstancesConfig
): Promise<Result<readonly TProInstance[]>> {
const maybeLoginFlag = config?.authenticate ? [DEVPOD_FLAG_LOGIN] : []
const result = await ProCommands.newCommand([
DEVPOD_COMMAND_PRO,
DEVPOD_COMMAND_LIST,
DEVPOD_FLAG_JSON_OUTPUT,
...maybeLoginFlag,
]).run()
if (result.err) {
return result
Expand Down
67 changes: 49 additions & 18 deletions desktop/src/components/Layout/Pro.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { client } from "@/client"
import { useProInstances, useSettings, useWorkspaces } from "@/contexts"
import { Briefcase, DevPodProBadge, Plus } from "@/icons"
import { Briefcase, CheckCircle, DevPodProBadge, ExclamationTriangle, Plus } from "@/icons"
import { exists } from "@/lib"
import { TProID, TProInstance } from "@/types"
import { useLoginProModal } from "@/views/ProInstances/useLoginProModal"
import { useLoginProModal, useReLoginProModal } from "@/views/ProInstances/useLoginProModal"
import { useDeleteProviderModal } from "@/views/Providers/useDeleteProviderModal"
import { ChevronDownIcon, CloseIcon } from "@chakra-ui/icons"
import {
Expand Down Expand Up @@ -35,6 +35,7 @@ import { IconTag } from "../Tag"
export function Pro() {
const [[proInstances]] = useProInstances()
const { modal: loginProModal, handleOpenLogin: handleConnectClicked } = useLoginProModal()
const { modal: reLoginProModal, handleOpenLogin: handleReLoginClicked } = useReLoginProModal()
const [isDeleting, setIsDeleting] = useState(false)

const backgroundColor = useColorModeValue("white", "gray.900")
Expand Down Expand Up @@ -101,24 +102,30 @@ export function Pro() {
</ButtonGroup>
</VStack>
) : (
proInstances.map(
(proInstance) =>
proInstance.host && (
<ProInstaceRow
key={proInstance.host}
{...proInstance}
host={proInstance.host}
onIsDeletingChanged={setIsDeleting}
/>
)
)
proInstances.map((proInstance) => {
const host = proInstance.host
if (!host) {
return null
}

return (
<ProInstanceRow
key={host}
{...proInstance}
host={host}
onIsDeletingChanged={setIsDeleting}
onLoginClicked={() => handleReLoginClicked({ host })}
/>
)
})
)}
</Box>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
{loginProModal}
{reLoginProModal}
</>
) : (
<Button
Expand All @@ -131,12 +138,18 @@ export function Pro() {
}

type TProInstaceRowProps = Omit<TProInstance, "host"> &
Readonly<{ host: TProID; onIsDeletingChanged: (isDeleting: boolean) => void }>
function ProInstaceRow({
Readonly<{
host: TProID
onIsDeletingChanged: (isDeleting: boolean) => void
onLoginClicked?: VoidFunction
}>
function ProInstanceRow({
host,
creationTimestamp,
onIsDeletingChanged,
provider,
authenticated,
onLoginClicked,
}: TProInstaceRowProps) {
const [, { disconnect }] = useProInstances()
const workspaces = useWorkspaces()
Expand All @@ -157,15 +170,33 @@ function ProInstaceRow({
)
useEffect(() => {
onIsDeletingChanged(isOpen)
// `onIsDeletingChanged` is expectd to be a stable reference
// `onIsDeletingChanged` is expected to be a stable reference
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen])

return (
<>
<HStack width="full" padding="2" justifyContent="space-between">
<VStack align="start" spacing="0" fontSize="sm">
<Text fontWeight="bold">{host}</Text>
<HStack>
<Text fontWeight="bold">{host}</Text>
{exists(authenticated) && (
<IconTag
variant="ghost"
icon={
authenticated ? (
<CheckCircle color={"green.300"} />
) : (
<ExclamationTriangle color="orange.300" />
)
}
label=""
paddingInlineStart="0"
infoText={authenticated ? "Authenticated" : "Not Authenticated"}
{...(authenticated ? {} : { onClick: onLoginClicked, cursor: "pointer" })}
/>
)}
</HStack>
<HStack>
<IconTag
variant="ghost"
Expand All @@ -178,7 +209,7 @@ function ProInstaceRow({
<IconTag
variant="ghost"
icon={<Icon as={HiClock} />}
label={dayjs(new Date(creationTimestamp)).fromNow()}
label={dayjs(new Date(creationTimestamp)).format("MMM D, YY")}
infoText={`Created ${dayjs(new Date(creationTimestamp)).fromNow()}`}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/contexts/DevPodContext/DevPodProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function DevPodProvider({ children }: Readonly<{ children?: ReactNode }>)

const proInstancesQuery = useQuery({
queryKey: QueryKeys.PRO_INSTANCES,
queryFn: async () => (await client.pro.listAll()).unwrap(),
queryFn: async () => (await client.pro.listAll({ authenticate: true })).unwrap(),
refetchInterval: REFETCH_INTERVAL_MS,
})

Expand Down
7 changes: 7 additions & 0 deletions desktop/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export type TProInstance = Readonly<{
host: TMaybe<string>
provider: TMaybe<string>
creationTimestamp: TMaybe<string>
authenticated: TMaybe<boolean>
}>
export type TProInstances = readonly TProInstance[]
export type TProInstanceManager = Readonly<{
Expand All @@ -211,6 +212,12 @@ export type TProInstanceLoginConfig = Readonly<{
accessKey?: string
streamListener?: TStreamEventListenerFn
}>
export type TListProInstancesConfig = Readonly<
| {
authenticate?: boolean
}
| undefined
>
//#endregion

export type TDevcontainerSetup = Readonly<{
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/views/ProInstances/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { useLoginProModal } from "./useLoginProModal"
export { useLoginProModal, useReLoginProModal } from "./useLoginProModal"
Loading
Loading