From 8db9449d74edd5a5307d066653392d4b57ad9e5d Mon Sep 17 00:00:00 2001 From: David Barroso Date: Wed, 25 Jun 2025 13:56:41 +0200 Subject: [PATCH 1/3] feat: added cloud subcommand to develop against a cloud project --- clienv/wf_login.go | 2 +- cmd/dev/cloud.go | 317 +++++++++++++++++++++++++++++++++ cmd/dev/up.go | 9 +- dockercompose/compose_cloud.go | 227 +++++++++++++++++++++++ dockercompose/dockercompose.go | 12 +- dockercompose/graphql.go | 4 +- dockercompose/url.go | 4 +- flake.lock | 5 +- main.go | 1 + 9 files changed, 563 insertions(+), 18 deletions(-) create mode 100644 cmd/dev/cloud.go create mode 100644 dockercompose/compose_cloud.go diff --git a/clienv/wf_login.go b/clienv/wf_login.go index 8e00ba3a4..5ace7d04d 100644 --- a/clienv/wf_login.go +++ b/clienv/wf_login.go @@ -166,7 +166,7 @@ func (ce *CliEnv) loginGithub(ctx context.Context) (credentials.Credentials, err } }() - signinPage := ce.AuthURL() + "/signin/provider/github/?redirectTo=https://local.dashboard.nhost.run:8099/signin" + signinPage := ce.AuthURL() + "/signin/provider/github/?redirectTo=https://local.dashboard.local.nhost.run:8099/signin" ce.Infoln("Opening browser to sign-in") if err := openBrowser(signinPage); err != nil { return credentials.Credentials{}, err diff --git a/cmd/dev/cloud.go b/cmd/dev/cloud.go new file mode 100644 index 000000000..9f6e13493 --- /dev/null +++ b/cmd/dev/cloud.go @@ -0,0 +1,317 @@ +package dev + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + "syscall" + "text/tabwriter" + "time" + + "github.com/nhost/be/services/mimir/model" + "github.com/nhost/cli/clienv" + "github.com/nhost/cli/cmd/config" + "github.com/nhost/cli/cmd/software" + "github.com/nhost/cli/dockercompose" + "github.com/nhost/cli/nhostclient/graphql" + "github.com/nhost/cli/project/env" + "github.com/urfave/cli/v2" +) + +const ( + flagSubdomain = "subdomain" + flagAdminSecret = "admin-secret" + flagPostgresURL = "postgres-url" +) + +func CommandCloud() *cli.Command { //nolint:funlen + 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, + EnvVars: []string{"NHOST_HTTP_PORT"}, + }, + &cli.BoolFlag{ //nolint:exhaustruct + Name: flagDisableTLS, + Usage: "Disable TLS", + Value: false, + EnvVars: []string{"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, + EnvVars: []string{"NHOST_APPLY_SEEDS"}, + }, + &cli.UintFlag{ //nolint:exhaustruct + Name: flagsFunctionsPort, + Usage: "If specified, expose functions on this port. Not recommended", + Value: 0, + }, + &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.30.0", + EnvVars: []string{"NHOST_DASHBOARD_VERSION"}, + }, + &cli.StringFlag{ //nolint:exhaustruct + Name: flagConfigserverImage, + Hidden: true, + Value: "", + EnvVars: []string{"NHOST_CONFIGSERVER_IMAGE"}, + }, + &cli.BoolFlag{ //nolint:exhaustruct + Name: flagDownOnError, + Usage: "Skip confirmation", + EnvVars: []string{"NHOST_YES"}, + }, + &cli.StringFlag{ //nolint:exhaustruct + Name: flagCACertificates, + Usage: "Mounts and everrides path to CA certificates in the containers", + EnvVars: []string{"NHOST_CA_CERTIFICATES"}, + }, + &cli.StringFlag{ //nolint:exhaustruct + Name: flagSubdomain, + Usage: "Project's subdomain to operate on, defaults to linked project", + EnvVars: []string{"NHOST_SUBDOMAIN"}, + }, + &cli.StringFlag{ //nolint:exhaustruct + Name: flagAdminSecret, + Usage: "Admin secret to use for connecting to the GraphQL API from the console", + Required: true, + EnvVars: []string{"NHOST_ADMIN_SECRET"}, + }, + &cli.StringFlag{ //nolint:exhaustruct + Name: flagPostgresURL, + Usage: "Postgres URL", + Required: true, + EnvVars: []string{"NHOST_POSTGRES_URL"}, + }, + }, + } +} + +func commandCloud(cCtx *cli.Context) error { + ce := clienv.FromCLI(cCtx) + + if !clienv.PathExists(ce.Path.NhostToml()) { + return errors.New( //nolint:goerr113 + "no nhost project found, please run `nhost init` or `nhost config pull`", + ) + } + if !clienv.PathExists(ce.Path.Secrets()) { + return errors.New( //nolint:goerr113 + "no secrets found, please run `nhost init` or `nhost config pull`", + ) + } + + proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain)) + if err != nil { + return fmt.Errorf("failed to get app info: %w", err) + } + + configserverImage := cCtx.String(flagConfigserverImage) + if configserverImage == "" { + configserverImage = "nhost/cli:" + cCtx.App.Version + } + + applySeeds := cCtx.Bool(flagApplySeeds) + return Cloud( + cCtx.Context, + ce, + cCtx.App.Version, + cCtx.Uint(flagHTTPPort), + !cCtx.Bool(flagDisableTLS), + applySeeds, + dockercompose.ExposePorts{ + Auth: cCtx.Uint(flagAuthPort), + Storage: cCtx.Uint(flagStoragePort), + Graphql: cCtx.Uint(flagsHasuraPort), + Console: cCtx.Uint(flagsHasuraConsolePort), + Functions: cCtx.Uint(flagsFunctionsPort), + }, + cCtx.String(flagDashboardVersion), + configserverImage, + cCtx.String(flagCACertificates), + cCtx.Bool(flagDownOnError), + proj, + cCtx.String(flagAdminSecret), + cCtx.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, + adminSecret string, + 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() + }() + + var secrets model.Secrets + if err := clienv.UnmarshalFile(ce.Path.Secrets(), &secrets, env.Unmarshal); err != nil { + return fmt.Errorf( + "failed to parse secrets, make sure secret values are between quotes: %w", + err, + ) + } + + cfg, err := config.Validate(ce, "local", secrets) + if err != nil { + return fmt.Errorf("failed to validate config: %w", err) + } + + ctxWithTimeout, cancel := context.WithTimeout(ctx, 5*time.Second) //nolint:mnd + defer cancel() + ce.Infoln("Checking versions...") + if err := software.CheckVersions(ctxWithTimeout, ce, cfg, appVersion); err != nil { + ce.Warnln("Problem verifying recommended versions: %s", err.Error()) + } + + ce.Infoln("Setting up Nhost development environment...") + composeFile, err := dockercompose.CloudComposeFileFromConfig( + cfg, + ce.LocalSubdomain(), + proj.GetSubdomain(), + proj.GetRegion().GetName(), + adminSecret, + postgresURL, + ce.ProjectName(), + httpPort, + useTLS, + ce.Path.NhostFolder(), + ce.Path.DotNhostFolder(), + ce.Path.Root(), + ports, + ce.Branch(), + dashboardVersion, + configserverImage, + clienv.PathExists(ce.Path.Functions()), + 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) + } + + 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(), + *cfg.Hasura.Version, + "metadata", "export", + "--skip-update-check", + "--log-level", "ERROR", + "--endpoint", endpoint, + "--admin-secret", adminSecret, + ); 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, + adminSecret string, + 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, + adminSecret, + postgresURL, + ); err != nil { + return upErr(ce, dc, downOnError, err) //nolint:contextcheck + } + + return nil +} diff --git a/cmd/dev/up.go b/cmd/dev/up.go index 4f0a5a7f5..69c67b815 100644 --- a/cmd/dev/up.go +++ b/cmd/dev/up.go @@ -187,11 +187,12 @@ func migrations( ctx context.Context, ce *clienv.CliEnv, dc *dockercompose.DockerCompose, + endpoint string, applySeeds bool, ) error { if clienv.PathExists(filepath.Join(ce.Path.NhostFolder(), "migrations", "default")) { ce.Infoln("Applying migrations...") - if err := dc.ApplyMigrations(ctx); err != nil { + if err := dc.ApplyMigrations(ctx, endpoint); err != nil { return fmt.Errorf("failed to apply migrations: %w", err) } } else { @@ -200,7 +201,7 @@ func migrations( if clienv.PathExists(filepath.Join(ce.Path.NhostFolder(), "metadata", "version.yaml")) { ce.Infoln("Applying metadata...") - if err := dc.ApplyMetadata(ctx); err != nil { + if err := dc.ApplyMetadata(ctx, endpoint); err != nil { return fmt.Errorf("failed to apply metadata: %w", err) } } else { @@ -210,7 +211,7 @@ func migrations( if applySeeds { if clienv.PathExists(filepath.Join(ce.Path.NhostFolder(), "seeds", "default")) { ce.Infoln("Applying seeds...") - if err := dc.ApplySeeds(ctx); err != nil { + if err := dc.ApplySeeds(ctx, endpoint); err != nil { return fmt.Errorf("failed to apply seeds: %w", err) } } @@ -386,7 +387,7 @@ func up( //nolint:funlen,cyclop return fmt.Errorf("failed to start Nhost development environment: %w", err) } - if err := migrations(ctx, ce, dc, applySeeds); err != nil { + if err := migrations(ctx, ce, dc, "http://graphql:8080", applySeeds); err != nil { return err } diff --git a/dockercompose/compose_cloud.go b/dockercompose/compose_cloud.go new file mode 100644 index 000000000..3aad27df2 --- /dev/null +++ b/dockercompose/compose_cloud.go @@ -0,0 +1,227 @@ +package dockercompose + +import ( + "fmt" + + "github.com/nhost/be/services/mimir/model" +) + +const ( + schemeHTTP = "http" + schemeHTTPS = "https" +) + +func dashboardCloud( + cfg *model.ConfigConfig, + subdomain string, + cloudSubdomain string, + cloudRegion string, + cloudAdminSecret string, + httpPort uint, + useTLS bool, + dashboardVersion string, +) *Service { + dashboard := dashboard(cfg, subdomain, dashboardVersion, httpPort, useTLS) + + dashboard.Environment["NEXT_PUBLIC_NHOST_ADMIN_SECRET"] = cloudAdminSecret + dashboard.Environment["NEXT_PUBLIC_NHOST_AUTH_URL"] = fmt.Sprintf( + "https://%s.auth.%s.nhost.run/v1", cloudSubdomain, cloudRegion, + ) + dashboard.Environment["NEXT_PUBLIC_NHOST_GRAPHQL_URL"] = fmt.Sprintf( + "https://%s.graphql.%s.nhost.run/v1", cloudSubdomain, cloudRegion, + ) + dashboard.Environment["NEXT_PUBLIC_NHOST_STORAGE_URL"] = fmt.Sprintf( + "https://%s.storage.%s.nhost.run/v1", cloudSubdomain, cloudRegion, + ) + dashboard.Environment["NEXT_PUBLIC_NHOST_HASURA_API_URL"] = fmt.Sprintf( + "https://%s.hasura.%s.nhost.run", + cloudSubdomain, cloudRegion, + ) + + return dashboard +} + +func consoleCloud( + cfg *model.ConfigConfig, + subdomain string, + cloudSubdomain string, + cloudRegion string, + cloudAdminSecret string, + clouadPostgresURL string, + httpPort uint, + useTLS bool, + nhostFolder string, + ports ExposePorts, +) (*Service, error) { + console, err := console(cfg, subdomain, httpPort, useTLS, nhostFolder, ports.Console) + if err != nil { + return nil, err + } + + scheme := schemeHTTP + if useTLS { + scheme = schemeHTTPS + } + + console.DependsOn = nil + console.Command = []string{ + "bash", "-c", + fmt.Sprintf(` + hasura-cli \ + console \ + --no-browser \ + --endpoint https://%s.hasura.%s.nhost.run \ + --address 0.0.0.0 \ + --console-port 9695 \ + --api-port %d \ + --api-host %s://%s.hasura.local.nhost.run \ + --console-hge-endpoint https://%s.hasura.%s.nhost.run`, + cloudSubdomain, cloudRegion, httpPort, scheme, subdomain, cloudSubdomain, cloudRegion), + } + + console.Environment["HASURA_GRAPHQL_ADMIN_SECRET"] = cloudAdminSecret + console.Environment["HASURA_GRAPHQL_DATABASE_URL"] = clouadPostgresURL + + return console, nil +} + +func getServicesCloud( //nolint:funlen + cfg *model.ConfigConfig, + subdomain string, + cloudSubdomain string, + cloudRegion string, + cloudAdminSecret string, + clouadPostgresURL string, + projectName string, + httpPort uint, + useTLS bool, + nhostFolder string, + dotNhostFolder string, + rootFolder string, + ports ExposePorts, + branch string, + dashboardVersion string, + configserviceImage string, + startFunctions bool, + runServices ...*RunService, +) (map[string]*Service, error) { + const jwtSecret = "FIXME" + + traefik, err := traefik(subdomain, projectName, httpPort, dotNhostFolder) + if err != nil { + return nil, err + } + + console, err := consoleCloud( + cfg, + subdomain, + cloudSubdomain, + cloudRegion, + cloudAdminSecret, + clouadPostgresURL, + httpPort, + useTLS, + nhostFolder, + ports, + ) + if err != nil { + return nil, fmt.Errorf("failed to create console service: %w", err) + } + + services := map[string]*Service{ + "console": console, + "dashboard": dashboardCloud( + cfg, + subdomain, + cloudSubdomain, + cloudRegion, + cloudAdminSecret, + httpPort, + useTLS, + dashboardVersion, + ), + "traefik": traefik, + "configserver": configserver( + configserviceImage, + rootFolder, + nhostFolder, + useTLS, + runServices...), + } + + if startFunctions { + services["functions"] = functions( + cfg, + subdomain, + httpPort, + useTLS, + rootFolder, + jwtSecret, + ports.Functions, + branch, + ) + } + + return services, nil +} + +func CloudComposeFileFromConfig( + cfg *model.ConfigConfig, + subdomain string, + cloudSubdomain string, + cloudRegion string, + cloudAdminSecret string, + clouadPostgresURL string, + projectName string, + httpPort uint, + useTLS bool, + nhostFolder string, + dotNhostFolder string, + rootFolder string, + ports ExposePorts, + branch string, + dashboardVersion string, + configserverImage string, + startFunctions bool, + caCertificatesPath string, +) (*ComposeFile, error) { + services, err := getServicesCloud( + cfg, + subdomain, + cloudSubdomain, + cloudRegion, + cloudAdminSecret, + clouadPostgresURL, + projectName, + httpPort, + useTLS, + nhostFolder, + dotNhostFolder, + rootFolder, + ports, + branch, + dashboardVersion, + configserverImage, + startFunctions, + ) + if err != nil { + return nil, err + } + + volumes := map[string]struct{}{ + rootNodeModules(branch): {}, + } + + if startFunctions { + volumes[functionsNodeModules(branch)] = struct{}{} + } + + if caCertificatesPath != "" { + mountCACertificates(caCertificatesPath, services) + } + + return &ComposeFile{ + Services: services, + Volumes: volumes, + }, nil +} diff --git a/dockercompose/dockercompose.go b/dockercompose/dockercompose.go index 4ae5cb02c..796dc552d 100644 --- a/dockercompose/dockercompose.go +++ b/dockercompose/dockercompose.go @@ -136,7 +136,7 @@ func (dc *DockerCompose) Wrapper(ctx context.Context, extraArgs ...string) error return nil } -func (dc *DockerCompose) ApplyMetadata(ctx context.Context) error { +func (dc *DockerCompose) ApplyMetadata(ctx context.Context, endpoint string) error { cmd := exec.CommandContext( //nolint:gosec ctx, "docker", "compose", @@ -147,7 +147,7 @@ func (dc *DockerCompose) ApplyMetadata(ctx context.Context) error { "console", "hasura-cli", "metadata", "apply", - "--endpoint", "http://graphql:8080", + "--endpoint", endpoint, "--skip-update-check", ) @@ -193,7 +193,7 @@ func (dc *DockerCompose) ReloadMetadata(ctx context.Context) error { return nil } -func (dc *DockerCompose) ApplyMigrations(ctx context.Context) error { +func (dc *DockerCompose) ApplyMigrations(ctx context.Context, endpoint string) error { cmd := exec.CommandContext( //nolint:gosec ctx, "docker", "compose", @@ -205,7 +205,7 @@ func (dc *DockerCompose) ApplyMigrations(ctx context.Context) error { "hasura-cli", "migrate", "apply", - "--endpoint", "http://graphql:8080", + "--endpoint", endpoint, "--all-databases", "--skip-update-check", ) @@ -229,7 +229,7 @@ func (dc *DockerCompose) ApplyMigrations(ctx context.Context) error { return nil } -func (dc *DockerCompose) ApplySeeds(ctx context.Context) error { +func (dc *DockerCompose) ApplySeeds(ctx context.Context, endpoint string) error { cmd := exec.CommandContext( //nolint:gosec ctx, "docker", "compose", @@ -241,7 +241,7 @@ func (dc *DockerCompose) ApplySeeds(ctx context.Context) error { "hasura-cli", "seed", "apply", - "--endpoint", "http://graphql:8080", + "--endpoint", endpoint, "--all-databases", "--skip-update-check", ) diff --git a/dockercompose/graphql.go b/dockercompose/graphql.go index 9acb2ee73..73f835e07 100644 --- a/dockercompose/graphql.go +++ b/dockercompose/graphql.go @@ -100,9 +100,9 @@ func console( //nolint:funlen ) } - scheme := "http" + scheme := schemeHTTP if useTLS { - scheme = "https" + scheme = schemeHTTPS } envars, err := appconfig.HasuraEnv( diff --git a/dockercompose/url.go b/dockercompose/url.go index 13e213f71..74ad62817 100644 --- a/dockercompose/url.go +++ b/dockercompose/url.go @@ -9,9 +9,9 @@ func URL(host, service string, port uint, useTLS bool) string { return fmt.Sprintf("http://%s.%s.local.nhost.run", host, service) } - protocol := "http" + protocol := schemeHTTP if useTLS { - protocol = "https" + protocol = schemeHTTPS } return fmt.Sprintf("%s://%s.%s.local.nhost.run:%d", protocol, host, service, port) } diff --git a/flake.lock b/flake.lock index 45f82ed16..f4a0f9878 100644 --- a/flake.lock +++ b/flake.lock @@ -62,16 +62,15 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1750076681, + "lastModified": 1750077810, "narHash": "sha256-MsaskaJ+a0O06SZ6eVLfhx+oMcO6R8PFlyBpRwnMn8E=", "owner": "nhost", "repo": "nixops", - "rev": "c5c91aef5a51cb476324478696572dd3bbac2dff", + "rev": "1ed1112322d418277f25f4a13f6b0f2f5928fa6e", "type": "github" }, "original": { "owner": "nhost", - "ref": "arch", "repo": "nixops", "type": "github" } diff --git a/main.go b/main.go index 06e4a643f..5fb29a6df 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,7 @@ func main() { configserver.Command(), deployments.Command(), dev.Command(), + dev.CommandCloud(), dev.CommandUp(), dev.CommandDown(), dev.CommandLogs(), From 211d921d0a453da22e2c74bf2eabfce730be13b6 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Wed, 25 Jun 2025 16:17:05 +0200 Subject: [PATCH 2/3] asd --- cmd/config/apply.go | 99 ++++++++++++++++++++++++ cmd/config/config.go | 1 + cmd/config/validate.go | 39 +++++----- cmd/dev/cloud.go | 34 ++++---- cmd/dev/up.go | 3 + dockercompose/compose_cloud.go | 36 +-------- main.go | 1 - nhostclient/graphql/client_gen.go | 35 +++++++++ nhostclient/graphql/query/config.graphql | 4 + 9 files changed, 180 insertions(+), 72 deletions(-) create mode 100644 cmd/config/apply.go diff --git a/cmd/config/apply.go b/cmd/config/apply.go new file mode 100644 index 000000000..b4acfab21 --- /dev/null +++ b/cmd/config/apply.go @@ -0,0 +1,99 @@ +package config + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/nhost/be/services/mimir/model" + "github.com/nhost/cli/clienv" + "github.com/urfave/cli/v2" +) + +func CommandApply() *cli.Command { + return &cli.Command{ //nolint:exhaustruct + Name: "apply", + Aliases: []string{}, + Usage: "Apply configuration to cloud project", + Action: commandApply, + Flags: []cli.Flag{ + &cli.StringFlag{ //nolint:exhaustruct + Name: flagSubdomain, + Usage: "Subdomain of the Nhost project to apply configuration to. Defaults to linked project", + Required: true, + EnvVars: []string{"NHOST_SUBDOMAIN"}, + }, + &cli.BoolFlag{ //nolint:exhaustruct + Name: flagYes, + Usage: "Skip confirmation", + EnvVars: []string{"NHOST_YES"}, + }, + }, + } +} + +func commandApply(cCtx *cli.Context) error { + ce := clienv.FromCLI(cCtx) + + proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain)) + if err != nil { + return fmt.Errorf("failed to get app info: %w", err) + } + + ce.Infoln("Validating configuration...") + cfg, err := ValidateRemote( + cCtx.Context, + ce, + proj.GetSubdomain(), + proj.GetID(), + ) + if err != nil { + return err + } + + return Apply(cCtx.Context, ce, proj.ID, cfg, cCtx.Bool(flagYes)) +} + +func Apply( + ctx context.Context, + ce *clienv.CliEnv, + appID string, + cfg *model.ConfigConfig, + skipConfirmation bool, +) error { + if !skipConfirmation { + ce.PromptMessage( + "We are going to overwrite the project's configuration. Do you want to proceed? [y/N] ", + ) + resp, err := ce.PromptInput(false) + if err != nil { + return fmt.Errorf("failed to read input: %w", err) + } + if resp != "y" && resp != "Y" { + return errors.New("aborting") //nolint:goerr113 + } + } + + cl, err := ce.GetNhostClient(ctx) + if err != nil { + return fmt.Errorf("failed to get nhost client: %w", err) + } + + b, err := json.Marshal(cfg) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if _, err := cl.ReplaceConfigRawJSON( + ctx, + appID, + string(b), + ); err != nil { + return fmt.Errorf("failed to apply config: %w", err) + } + + ce.Infoln("Configuration applied successfully!") + + return nil +} diff --git a/cmd/config/config.go b/cmd/config/config.go index ec20f68d2..131a985d6 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -12,6 +12,7 @@ func Command() *cli.Command { Subcommands: []*cli.Command{ CommandDefault(), CommandExample(), + CommandApply(), CommandPull(), CommandShow(), CommandValidate(), diff --git a/cmd/config/validate.go b/cmd/config/validate.go index 7c2434895..cfd32c7b9 100644 --- a/cmd/config/validate.go +++ b/cmd/config/validate.go @@ -38,11 +38,18 @@ func commandValidate(cCtx *cli.Context) error { subdomain := cCtx.String(flagSubdomain) if subdomain != "" && subdomain != "local" { - return ValidateRemote( + proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain)) + if err != nil { + return fmt.Errorf("failed to get app info: %w", err) + } + + _, err = ValidateRemote( cCtx.Context, ce, - cCtx.String(flagSubdomain), + proj.GetSubdomain(), + proj.GetID(), ) + return err } var secrets model.Secrets @@ -134,50 +141,46 @@ func ValidateRemote( ctx context.Context, ce *clienv.CliEnv, subdomain string, -) error { + appID string, +) (*model.ConfigConfig, error) { cfg := &model.ConfigConfig{} //nolint:exhaustruct if err := clienv.UnmarshalFile(ce.Path.NhostToml(), cfg, toml.Unmarshal); err != nil { - return fmt.Errorf("failed to parse config: %w", err) + return nil, fmt.Errorf("failed to parse config: %w", err) } schema, err := schema.New() if err != nil { - return fmt.Errorf("failed to create schema: %w", err) - } - - proj, err := ce.GetAppInfo(ctx, subdomain) - if err != nil { - return fmt.Errorf("failed to get app info: %w", err) + return nil, fmt.Errorf("failed to create schema: %w", err) } ce.Infoln("Getting secrets...") cl, err := ce.GetNhostClient(ctx) if err != nil { - return fmt.Errorf("failed to get nhost client: %w", err) + return nil, fmt.Errorf("failed to get nhost client: %w", err) } secretsResp, err := cl.GetSecrets( ctx, - proj.ID, + appID, ) if err != nil { - return fmt.Errorf("failed to get secrets: %w", err) + return nil, fmt.Errorf("failed to get secrets: %w", err) } - if clienv.PathExists(ce.Path.Overlay(proj.GetSubdomain())) { + if clienv.PathExists(ce.Path.Overlay(subdomain)) { var err error - cfg, err = ApplyJSONPatches(*cfg, ce.Path.Overlay(proj.GetSubdomain())) + cfg, err = ApplyJSONPatches(*cfg, ce.Path.Overlay(subdomain)) if err != nil { - return fmt.Errorf("failed to apply json patches: %w", err) + return nil, fmt.Errorf("failed to apply json patches: %w", err) } } secrets := respToSecrets(secretsResp.GetAppSecrets(), false) _, err = appconfig.SecretsResolver[model.ConfigConfig](cfg, secrets, schema.Fill) if err != nil { - return fmt.Errorf("failed to validate config: %w", err) + return nil, fmt.Errorf("failed to validate config: %w", err) } ce.Infoln("Config is valid!") - return nil + return cfg, nil } diff --git a/cmd/dev/cloud.go b/cmd/dev/cloud.go index 9f6e13493..1fd1e4a42 100644 --- a/cmd/dev/cloud.go +++ b/cmd/dev/cloud.go @@ -10,13 +10,11 @@ import ( "text/tabwriter" "time" - "github.com/nhost/be/services/mimir/model" "github.com/nhost/cli/clienv" "github.com/nhost/cli/cmd/config" "github.com/nhost/cli/cmd/software" "github.com/nhost/cli/dockercompose" "github.com/nhost/cli/nhostclient/graphql" - "github.com/nhost/cli/project/env" "github.com/urfave/cli/v2" ) @@ -26,7 +24,7 @@ const ( flagPostgresURL = "postgres-url" ) -func CommandCloud() *cli.Command { //nolint:funlen +func CommandCloud() *cli.Command { return &cli.Command{ //nolint:exhaustruct Name: "cloud", Aliases: []string{}, @@ -51,11 +49,6 @@ func CommandCloud() *cli.Command { //nolint:funlen Value: false, EnvVars: []string{"NHOST_APPLY_SEEDS"}, }, - &cli.UintFlag{ //nolint:exhaustruct - Name: flagsFunctionsPort, - Usage: "If specified, expose functions on this port. Not recommended", - Value: 0, - }, &cli.UintFlag{ //nolint:exhaustruct Name: flagsHasuraConsolePort, Usage: "If specified, expose hasura console on this port. Not recommended", @@ -178,17 +171,15 @@ func cloud( //nolint:funlen cancel() }() - var secrets model.Secrets - if err := clienv.UnmarshalFile(ce.Path.Secrets(), &secrets, env.Unmarshal); err != nil { - return fmt.Errorf( - "failed to parse secrets, make sure secret values are between quotes: %w", - err, - ) - } - - cfg, err := config.Validate(ce, "local", secrets) + ce.Infoln("Validating configuration...") + cfg, err := config.ValidateRemote( + ctx, + ce, + proj.GetSubdomain(), + proj.GetID(), + ) if err != nil { - return fmt.Errorf("failed to validate config: %w", err) + return fmt.Errorf("failed to validate configuration: %w", err) } ctxWithTimeout, cancel := context.WithTimeout(ctx, 5*time.Second) //nolint:mnd @@ -213,10 +204,8 @@ func cloud( //nolint:funlen ce.Path.DotNhostFolder(), ce.Path.Root(), ports, - ce.Branch(), dashboardVersion, configserverImage, - clienv.PathExists(ce.Path.Functions()), caCertificatesPath, ) if err != nil { @@ -231,6 +220,11 @@ func cloud( //nolint:funlen 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(), diff --git a/cmd/dev/up.go b/cmd/dev/up.go index 69c67b815..a5a8cde21 100644 --- a/cmd/dev/up.go +++ b/cmd/dev/up.go @@ -135,6 +135,9 @@ func CommandUp() *cli.Command { //nolint:funlen EnvVars: []string{"NHOST_CA_CERTIFICATES"}, }, }, + Subcommands: []*cli.Command{ + CommandCloud(), + }, } } diff --git a/dockercompose/compose_cloud.go b/dockercompose/compose_cloud.go index 3aad27df2..65433406e 100644 --- a/dockercompose/compose_cloud.go +++ b/dockercompose/compose_cloud.go @@ -85,7 +85,7 @@ func consoleCloud( return console, nil } -func getServicesCloud( //nolint:funlen +func getServicesCloud( cfg *model.ConfigConfig, subdomain string, cloudSubdomain string, @@ -99,14 +99,9 @@ func getServicesCloud( //nolint:funlen dotNhostFolder string, rootFolder string, ports ExposePorts, - branch string, dashboardVersion string, configserviceImage string, - startFunctions bool, - runServices ...*RunService, ) (map[string]*Service, error) { - const jwtSecret = "FIXME" - traefik, err := traefik(subdomain, projectName, httpPort, dotNhostFolder) if err != nil { return nil, err @@ -146,20 +141,7 @@ func getServicesCloud( //nolint:funlen rootFolder, nhostFolder, useTLS, - runServices...), - } - - if startFunctions { - services["functions"] = functions( - cfg, - subdomain, - httpPort, - useTLS, - rootFolder, - jwtSecret, - ports.Functions, - branch, - ) + ), } return services, nil @@ -179,10 +161,8 @@ func CloudComposeFileFromConfig( dotNhostFolder string, rootFolder string, ports ExposePorts, - branch string, dashboardVersion string, configserverImage string, - startFunctions bool, caCertificatesPath string, ) (*ComposeFile, error) { services, err := getServicesCloud( @@ -199,29 +179,19 @@ func CloudComposeFileFromConfig( dotNhostFolder, rootFolder, ports, - branch, dashboardVersion, configserverImage, - startFunctions, ) if err != nil { return nil, err } - volumes := map[string]struct{}{ - rootNodeModules(branch): {}, - } - - if startFunctions { - volumes[functionsNodeModules(branch)] = struct{}{} - } - if caCertificatesPath != "" { mountCACertificates(caCertificatesPath, services) } return &ComposeFile{ Services: services, - Volumes: volumes, + Volumes: nil, }, nil } diff --git a/main.go b/main.go index 5fb29a6df..06e4a643f 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,6 @@ func main() { configserver.Command(), deployments.Command(), dev.Command(), - dev.CommandCloud(), dev.CommandUp(), dev.CommandDown(), dev.CommandLogs(), diff --git a/nhostclient/graphql/client_gen.go b/nhostclient/graphql/client_gen.go index 298002453..a4727d0c2 100644 --- a/nhostclient/graphql/client_gen.go +++ b/nhostclient/graphql/client_gen.go @@ -450,6 +450,17 @@ func (t *GetConfigRawJSON) GetConfigRawJSON() string { return t.ConfigRawJSON } +type ReplaceConfigRawJSON struct { + ReplaceConfigRawJSON string "json:\"replaceConfigRawJSON\" graphql:\"replaceConfigRawJSON\"" +} + +func (t *ReplaceConfigRawJSON) GetReplaceConfigRawJSON() string { + if t == nil { + t = &ReplaceConfigRawJSON{} + } + return t.ReplaceConfigRawJSON +} + type ListDeployments struct { Deployments []*ListDeployments_Deployments "json:\"deployments\" graphql:\"deployments\"" } @@ -690,6 +701,29 @@ func (c *Client) GetConfigRawJSON(ctx context.Context, appID string, interceptor return &res, nil } +const ReplaceConfigRawJSONDocument = `mutation ReplaceConfigRawJSON ($appID: uuid!, $rawJSON: String!) { + replaceConfigRawJSON(appID: $appID, rawJSON: $rawJSON) +} +` + +func (c *Client) ReplaceConfigRawJSON(ctx context.Context, appID string, rawJSON string, interceptors ...clientv2.RequestInterceptor) (*ReplaceConfigRawJSON, error) { + vars := map[string]any{ + "appID": appID, + "rawJSON": rawJSON, + } + + var res ReplaceConfigRawJSON + if err := c.Client.Post(ctx, "ReplaceConfigRawJSON", ReplaceConfigRawJSONDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + + return nil, err + } + + return &res, nil +} + const ListDeploymentsDocument = `query ListDeployments ($appID: uuid!) { deployments(where: {appId:{_eq:$appID}}, order_by: {deploymentStartedAt:desc}, limit: 10) { id @@ -1032,6 +1066,7 @@ var DocumentOperationNames = map[string]string{ GetOrganizationsAndWorkspacesAppsDocument: "GetOrganizationsAndWorkspacesApps", GetHasuraAdminSecretDocument: "GetHasuraAdminSecret", GetConfigRawJSONDocument: "GetConfigRawJSON", + ReplaceConfigRawJSONDocument: "ReplaceConfigRawJSON", ListDeploymentsDocument: "ListDeployments", GetDeploymentLogsDocument: "GetDeploymentLogs", InsertDeploymentDocument: "InsertDeployment", diff --git a/nhostclient/graphql/query/config.graphql b/nhostclient/graphql/query/config.graphql index 28574f6d7..84b7ad2f1 100644 --- a/nhostclient/graphql/query/config.graphql +++ b/nhostclient/graphql/query/config.graphql @@ -1,3 +1,7 @@ query GetConfigRawJSON($appID: uuid!) { configRawJSON(appID: $appID, resolve: false) } + +mutation ReplaceConfigRawJSON($appID: uuid!, $rawJSON: String!) { + replaceConfigRawJSON(appID: $appID, rawJSON: $rawJSON) +} From b8eacfa895c8955d516b02b0c75cb42875b626e9 Mon Sep 17 00:00:00 2001 From: David Barroso Date: Wed, 25 Jun 2025 16:32:35 +0200 Subject: [PATCH 3/3] asd --- cmd/config/apply.go | 2 +- cmd/config/validate.go | 20 ++++++++++---------- cmd/dev/cloud.go | 17 +++-------------- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/cmd/config/apply.go b/cmd/config/apply.go index b4acfab21..88dcf3dcb 100644 --- a/cmd/config/apply.go +++ b/cmd/config/apply.go @@ -42,7 +42,7 @@ func commandApply(cCtx *cli.Context) error { } ce.Infoln("Validating configuration...") - cfg, err := ValidateRemote( + cfg, _, err := ValidateRemote( cCtx.Context, ce, proj.GetSubdomain(), diff --git a/cmd/config/validate.go b/cmd/config/validate.go index cfd32c7b9..46c9047e0 100644 --- a/cmd/config/validate.go +++ b/cmd/config/validate.go @@ -43,7 +43,7 @@ func commandValidate(cCtx *cli.Context) error { return fmt.Errorf("failed to get app info: %w", err) } - _, err = ValidateRemote( + _, _, err = ValidateRemote( cCtx.Context, ce, proj.GetSubdomain(), @@ -142,45 +142,45 @@ func ValidateRemote( ce *clienv.CliEnv, subdomain string, appID string, -) (*model.ConfigConfig, error) { +) (*model.ConfigConfig, *model.ConfigConfig, error) { cfg := &model.ConfigConfig{} //nolint:exhaustruct if err := clienv.UnmarshalFile(ce.Path.NhostToml(), cfg, toml.Unmarshal); err != nil { - return nil, fmt.Errorf("failed to parse config: %w", err) + return nil, nil, fmt.Errorf("failed to parse config: %w", err) } schema, err := schema.New() if err != nil { - return nil, fmt.Errorf("failed to create schema: %w", err) + return nil, nil, fmt.Errorf("failed to create schema: %w", err) } ce.Infoln("Getting secrets...") cl, err := ce.GetNhostClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get nhost client: %w", err) + return nil, nil, fmt.Errorf("failed to get nhost client: %w", err) } secretsResp, err := cl.GetSecrets( ctx, appID, ) if err != nil { - return nil, fmt.Errorf("failed to get secrets: %w", err) + return nil, nil, fmt.Errorf("failed to get secrets: %w", err) } if clienv.PathExists(ce.Path.Overlay(subdomain)) { var err error cfg, err = ApplyJSONPatches(*cfg, ce.Path.Overlay(subdomain)) if err != nil { - return nil, fmt.Errorf("failed to apply json patches: %w", err) + return nil, nil, fmt.Errorf("failed to apply json patches: %w", err) } } secrets := respToSecrets(secretsResp.GetAppSecrets(), false) - _, err = appconfig.SecretsResolver[model.ConfigConfig](cfg, secrets, schema.Fill) + cfgSecrets, err := appconfig.SecretsResolver[model.ConfigConfig](cfg, secrets, schema.Fill) if err != nil { - return nil, fmt.Errorf("failed to validate config: %w", err) + return nil, nil, fmt.Errorf("failed to validate config: %w", err) } ce.Infoln("Config is valid!") - return cfg, nil + return cfg, cfgSecrets, nil } diff --git a/cmd/dev/cloud.go b/cmd/dev/cloud.go index 1fd1e4a42..a25e2d718 100644 --- a/cmd/dev/cloud.go +++ b/cmd/dev/cloud.go @@ -20,7 +20,6 @@ import ( const ( flagSubdomain = "subdomain" - flagAdminSecret = "admin-secret" flagPostgresURL = "postgres-url" ) @@ -81,12 +80,6 @@ func CommandCloud() *cli.Command { Usage: "Project's subdomain to operate on, defaults to linked project", EnvVars: []string{"NHOST_SUBDOMAIN"}, }, - &cli.StringFlag{ //nolint:exhaustruct - Name: flagAdminSecret, - Usage: "Admin secret to use for connecting to the GraphQL API from the console", - Required: true, - EnvVars: []string{"NHOST_ADMIN_SECRET"}, - }, &cli.StringFlag{ //nolint:exhaustruct Name: flagPostgresURL, Usage: "Postgres URL", @@ -141,7 +134,6 @@ func commandCloud(cCtx *cli.Context) error { cCtx.String(flagCACertificates), cCtx.Bool(flagDownOnError), proj, - cCtx.String(flagAdminSecret), cCtx.String(flagPostgresURL), ) } @@ -159,7 +151,6 @@ func cloud( //nolint:funlen configserverImage string, caCertificatesPath string, proj *graphql.AppSummaryFragment, - adminSecret string, postgresURL string, ) error { ctx, cancel := context.WithCancel(ctx) @@ -172,7 +163,7 @@ func cloud( //nolint:funlen }() ce.Infoln("Validating configuration...") - cfg, err := config.ValidateRemote( + cfg, cfgSecrets, err := config.ValidateRemote( ctx, ce, proj.GetSubdomain(), @@ -195,7 +186,7 @@ func cloud( //nolint:funlen ce.LocalSubdomain(), proj.GetSubdomain(), proj.GetRegion().GetName(), - adminSecret, + cfgSecrets.Hasura.GetAdminSecret(), postgresURL, ce.ProjectName(), httpPort, @@ -245,7 +236,7 @@ func cloud( //nolint:funlen "--skip-update-check", "--log-level", "ERROR", "--endpoint", endpoint, - "--admin-secret", adminSecret, + "--admin-secret", cfgSecrets.Hasura.GetAdminSecret(), ); err != nil { return fmt.Errorf("failed to create metadata: %w", err) } @@ -283,7 +274,6 @@ func Cloud( caCertificatesPath string, downOnError bool, proj *graphql.AppSummaryFragment, - adminSecret string, postgresURL string, ) error { dc := dockercompose.New(ce.Path.WorkingDir(), ce.Path.DockerCompose(), ce.ProjectName()) @@ -301,7 +291,6 @@ func Cloud( configserverImage, caCertificatesPath, proj, - adminSecret, postgresURL, ); err != nil { return upErr(ce, dc, downOnError, err) //nolint:contextcheck