Files
nhost/cli/cmd/dev/cloud.go
2025-11-13 10:07:25 +01:00

314 lines
7.7 KiB
Go

package dev
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"syscall"
"text/tabwriter"
"time"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/cmd/config"
"github.com/nhost/nhost/cli/cmd/software"
"github.com/nhost/nhost/cli/dockercompose"
"github.com/nhost/nhost/cli/nhostclient/graphql"
"github.com/urfave/cli/v3"
)
const (
flagSubdomain = "subdomain"
flagPostgresURL = "postgres-url"
)
func CommandCloud() *cli.Command {
return &cli.Command{ //nolint:exhaustruct
Name: "cloud",
Aliases: []string{},
Usage: "Start local development environment connected to an Nhost Cloud project (BETA)",
Action: commandCloud,
Flags: []cli.Flag{
&cli.UintFlag{ //nolint:exhaustruct
Name: flagHTTPPort,
Usage: "HTTP port to listen on",
Value: defaultHTTPPort,
Sources: cli.EnvVars("NHOST_HTTP_PORT"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagDisableTLS,
Usage: "Disable TLS",
Value: false,
Sources: cli.EnvVars("NHOST_DISABLE_TLS"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagApplySeeds,
Usage: "Apply seeds. If the .nhost folder does not exist, seeds will be applied regardless of this flag",
Value: false,
Sources: cli.EnvVars("NHOST_APPLY_SEEDS"),
},
&cli.UintFlag{ //nolint:exhaustruct
Name: flagsHasuraConsolePort,
Usage: "If specified, expose hasura console on this port. Not recommended",
Value: 0,
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagDashboardVersion,
Usage: "Dashboard version to use",
Value: "nhost/dashboard:2.42.0",
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagConfigserverImage,
Hidden: true,
Value: "",
Sources: cli.EnvVars("NHOST_CONFIGSERVER_IMAGE"),
},
&cli.BoolFlag{ //nolint:exhaustruct
Name: flagDownOnError,
Usage: "Skip confirmation",
Sources: cli.EnvVars("NHOST_YES"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagCACertificates,
Usage: "Mounts and everrides path to CA certificates in the containers",
Sources: cli.EnvVars("NHOST_CA_CERTIFICATES"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagSubdomain,
Usage: "Project's subdomain to operate on, defaults to linked project",
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
},
&cli.StringFlag{ //nolint:exhaustruct
Name: flagPostgresURL,
Usage: "Postgres URL",
Required: true,
Sources: cli.EnvVars("NHOST_POSTGRES_URL"),
},
},
}
}
func commandCloud(ctx context.Context, cmd *cli.Command) error {
ce := clienv.FromCLI(cmd)
if !clienv.PathExists(ce.Path.NhostToml()) {
return errors.New( //nolint:err113
"no nhost project found, please run `nhost init` or `nhost config pull`",
)
}
if !clienv.PathExists(ce.Path.Secrets()) {
return errors.New( //nolint:err113
"no secrets found, please run `nhost init` or `nhost config pull`",
)
}
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
if err != nil {
return fmt.Errorf("failed to get app info: %w", err)
}
configserverImage := cmd.String(flagConfigserverImage)
if configserverImage == "" {
configserverImage = "nhost/cli:" + cmd.Root().Version
}
applySeeds := cmd.Bool(flagApplySeeds)
return Cloud(
ctx,
ce,
cmd.Root().Version,
cmd.Uint(flagHTTPPort),
!cmd.Bool(flagDisableTLS),
applySeeds,
dockercompose.ExposePorts{
Auth: cmd.Uint(flagAuthPort),
Storage: cmd.Uint(flagStoragePort),
Graphql: cmd.Uint(flagsHasuraPort),
Console: cmd.Uint(flagsHasuraConsolePort),
Functions: cmd.Uint(flagsFunctionsPort),
},
cmd.String(flagDashboardVersion),
configserverImage,
cmd.String(flagCACertificates),
cmd.Bool(flagDownOnError),
proj,
cmd.String(flagPostgresURL),
)
}
func cloud( //nolint:funlen
ctx context.Context,
ce *clienv.CliEnv,
appVersion string,
dc *dockercompose.DockerCompose,
httpPort uint,
useTLS bool,
applySeeds bool,
ports dockercompose.ExposePorts,
dashboardVersion string,
configserverImage string,
caCertificatesPath string,
proj *graphql.AppSummaryFragment,
postgresURL string,
) error {
ctx, cancel := context.WithCancel(ctx)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
go func() {
<-sigChan
cancel()
}()
ce.Infoln("Validating configuration...")
cfg, cfgSecrets, err := config.ValidateRemote(
ctx,
ce,
proj.GetSubdomain(),
proj.GetID(),
)
if err != nil {
return fmt.Errorf("failed to validate configuration: %w", err)
}
ctxWithTimeout, cancel := context.WithTimeout(ctx, 5*time.Second) //nolint:mnd
defer cancel()
ce.Infoln("Checking versions...")
if err := software.CheckVersions(ctxWithTimeout, ce, cfgSecrets, appVersion); err != nil {
ce.Warnln("Problem verifying recommended versions: %s", err.Error())
}
ce.Infoln("Setting up Nhost development environment...")
composeFile, err := dockercompose.CloudComposeFileFromConfig(
cfgSecrets,
ce.LocalSubdomain(),
proj.GetSubdomain(),
proj.GetRegion().GetName(),
cfgSecrets.Hasura.GetAdminSecret(),
postgresURL,
ce.ProjectName(),
httpPort,
useTLS,
ce.Path.NhostFolder(),
ce.Path.DotNhostFolder(),
ce.Path.Root(),
ports,
dashboardVersion,
configserverImage,
caCertificatesPath,
)
if err != nil {
return fmt.Errorf("failed to generate docker-compose.yaml: %w", err)
}
if err := dc.WriteComposeFile(composeFile); err != nil {
return fmt.Errorf("failed to write docker-compose.yaml: %w", err)
}
ce.Infoln("Starting Nhost development environment...")
if err = dc.Start(ctx); err != nil {
return fmt.Errorf("failed to start Nhost development environment: %w", err)
}
ce.Infoln("Applying configuration to Nhost Cloud project...")
if err = config.Apply(ctx, ce, proj.GetID(), cfg, true); err != nil {
return fmt.Errorf("failed to apply configuration: %w", err)
}
endpoint := fmt.Sprintf(
"https://%s.hasura.%s.nhost.run",
proj.GetSubdomain(), proj.GetRegion().GetName(),
)
if err := migrations(ctx, ce, dc, endpoint, applySeeds); err != nil {
return err
}
docker := dockercompose.NewDocker()
ce.Infoln("Downloading metadata...")
if err := docker.HasuraWrapper(
ctx,
ce.LocalSubdomain(),
ce.Path.NhostFolder(),
*cfgSecrets.Hasura.Version,
"metadata", "export",
"--skip-update-check",
"--log-level", "ERROR",
"--endpoint", endpoint,
"--admin-secret", cfgSecrets.Hasura.GetAdminSecret(),
); err != nil {
return fmt.Errorf("failed to create metadata: %w", err)
}
ce.Infoln("Nhost development environment started.")
printCloudInfo(ce.LocalSubdomain(), httpPort, useTLS)
return nil
}
func printCloudInfo(
subdomain string,
httpPort uint,
useTLS bool,
) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) //nolint:mnd
fmt.Fprintf(w, "URLs:\n")
fmt.Fprintf(w, "- Console:\t\t%s\n", dockercompose.URL(
subdomain, "hasura", httpPort, useTLS))
fmt.Fprintf(w, "- Dashboard:\t\t%s\n", dockercompose.URL(
subdomain, "dashboard", httpPort, useTLS))
w.Flush()
}
func Cloud(
ctx context.Context,
ce *clienv.CliEnv,
appVersion string,
httpPort uint,
useTLS bool,
applySeeds bool,
ports dockercompose.ExposePorts,
dashboardVersion string,
configserverImage string,
caCertificatesPath string,
downOnError bool,
proj *graphql.AppSummaryFragment,
postgresURL string,
) error {
dc := dockercompose.New(ce.Path.WorkingDir(), ce.Path.DockerCompose(), ce.ProjectName())
if err := cloud(
ctx,
ce,
appVersion,
dc,
httpPort,
useTLS,
applySeeds,
ports,
dashboardVersion,
configserverImage,
caCertificatesPath,
proj,
postgresURL,
); err != nil {
return upErr(ce, dc, downOnError, err) //nolint:contextcheck
}
return nil
}