├── .deepsource.toml ├── .direnv └── preflight-example ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── flyctl-bug-report.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── auto-release.yml │ ├── build.yml │ ├── checks.yml │ ├── ci-dev.yml │ ├── ci.yml │ ├── preflight.yml │ ├── preflight_cleanup.yml │ ├── release.yml │ ├── test.yml │ └── test_install.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.2.yml ├── .goreleaser.yml ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.dev ├── Dockerfile.mcp ├── LICENSE ├── Makefile ├── README.md ├── agent ├── agent.go ├── client.go ├── client_unix.go ├── client_windows.go ├── cmd_unix.go ├── cmd_windows.go ├── context.go ├── errors.go ├── internal │ └── proto │ │ └── proto.go ├── server │ ├── server.go │ ├── server_unix.go │ ├── server_windows.go │ └── session.go └── start.go ├── aur └── PKGBUILD ├── building.md ├── cmd └── audit │ ├── audit.go │ ├── lint.go │ ├── list.go │ ├── output.go │ └── stats.go ├── debug_unix.go ├── debug_windows.go ├── deps └── wintun │ ├── LICENSE.txt │ ├── README.md │ ├── bin │ ├── .gitkeep │ ├── amd64 │ │ └── wintun.dll │ ├── arm │ │ └── wintun.dll │ ├── arm64 │ │ └── wintun.dll │ └── x86 │ │ └── wintun.dll │ └── include │ └── wintun.h ├── doc └── main.go ├── flyctl ├── config.go └── flyctl.go ├── flypg ├── cmd.go ├── http.go ├── launcher.go ├── pg.go ├── tigris.go └── types.go ├── genqlient.yaml ├── go.mod ├── go.sum ├── gql ├── conversions.go ├── generated.go ├── genqclient.graphql ├── inputs.go ├── schema.graphql └── types.go ├── helpers ├── clone.go ├── clone_test.go ├── config.go ├── config_test.go ├── duration.go ├── fs.go ├── rand.go ├── stdin.go ├── tablehelper.go └── units.go ├── installers ├── install.ps1 └── install.sh ├── internal ├── appconfig │ ├── config.go │ ├── config_test.go │ ├── context.go │ ├── context_test.go │ ├── definition.go │ ├── definition_test.go │ ├── file.go │ ├── flyctl.config.lock │ ├── from_machine_set.go │ ├── from_machine_set_test.go │ ├── machines.go │ ├── machines_test.go │ ├── patches.go │ ├── platformversion.go │ ├── platformversion_test.go │ ├── processgroups.go │ ├── processgroups_test.go │ ├── remote.go │ ├── serde.go │ ├── serde_test.go │ ├── service.go │ ├── setters.go │ ├── setters_test.go │ ├── testdata │ │ ├── always-invalid-v2.toml │ │ ├── app-name.toml │ │ ├── build-with-args.toml │ │ ├── build.toml │ │ ├── docker.toml │ │ ├── env-list.toml │ │ ├── experimental-alt.toml │ │ ├── format-quirks.toml │ │ ├── full-reference.toml │ │ ├── image.toml │ │ ├── mounts-array.toml │ │ ├── old-format.toml │ │ ├── old-pg-checks.toml │ │ ├── old-processes.toml │ │ ├── processes-multi.toml │ │ ├── processes-multiwithapp.toml │ │ ├── processes-none.toml │ │ ├── processes-one.toml │ │ ├── services-emptysection.toml │ │ ├── services-multi.toml │ │ ├── services-ports.toml │ │ ├── setters-deploy.toml │ │ ├── setters-experimental.toml │ │ ├── setters-httpservice.toml │ │ ├── setters-processes.toml │ │ ├── setters-service.toml │ │ ├── tomachine-compute-nodefault.toml │ │ ├── tomachine-compute.toml │ │ ├── tomachine-default-for-new-apps.toml │ │ ├── tomachine-experimental.toml │ │ ├── tomachine-hostdedicationid.toml │ │ ├── tomachine-machinechecks.toml │ │ ├── tomachine-mounts.toml │ │ ├── tomachine-processgroups.toml │ │ ├── tomachine-services.toml │ │ ├── tomachine.toml │ │ ├── validate-groups.toml │ │ ├── validate-mounts.toml │ │ └── validate-services.toml │ ├── toplevelcheck.go │ ├── validation.go │ └── validation_test.go ├── build │ └── imgsrc │ │ ├── archive.go │ │ ├── archive_test.go │ │ ├── buildkit.go │ │ ├── buildpacks_builder.go │ │ ├── builtin_builder.go │ │ ├── builtins │ │ ├── builtinsupport.go │ │ └── defaultbuiltins.go │ │ ├── depot.go │ │ ├── depot_test.go │ │ ├── docker.go │ │ ├── docker_test.go │ │ ├── dockerfile_builder.go │ │ ├── ensure_builder.go │ │ ├── ensure_builder_test.go │ │ ├── errors.go │ │ ├── local_image_resolver.go │ │ ├── nixpacks_builder.go │ │ ├── remote_image_resolver.go │ │ ├── resolver.go │ │ ├── resolver_test.go │ │ └── testdata │ │ └── dockerfile_app │ │ ├── Dockerfile │ │ └── index.html ├── buildinfo │ ├── buildinfo.go │ ├── env.go │ ├── env_dev.go │ └── env_production.go ├── cache │ ├── cache.go │ ├── context.go │ └── context_test.go ├── cli │ ├── cli.go │ └── cli_windows.go ├── cli_test │ └── cli_test.go ├── cmdfmt │ ├── logging.go │ └── messages.go ├── cmdutil │ ├── flags.go │ ├── preparers │ │ └── preparers.go │ ├── strings.go │ └── terminal.go ├── command │ ├── agent │ │ ├── agent.go │ │ ├── connect.go │ │ ├── establish.go │ │ ├── instances.go │ │ ├── ping.go │ │ ├── probe.go │ │ ├── resolve.go │ │ ├── restart.go │ │ ├── run.go │ │ ├── start.go │ │ └── stop.go │ ├── apps │ │ ├── apps.go │ │ ├── create.go │ │ ├── destroy.go │ │ ├── errors.go │ │ ├── list.go │ │ ├── move.go │ │ ├── open.go │ │ ├── releases.go │ │ ├── restart.go │ │ ├── resume.go │ │ └── suspend.go │ ├── auth │ │ ├── auth.go │ │ ├── docker.go │ │ ├── login.go │ │ ├── logout.go │ │ ├── signup.go │ │ ├── token.go │ │ ├── webauth │ │ │ └── webauth.go │ │ └── whoami.go │ ├── certificates │ │ └── root.go │ ├── checks │ │ ├── checks.go │ │ └── list.go │ ├── command.go │ ├── command_run.go │ ├── config │ │ ├── config.go │ │ ├── env.go │ │ ├── save.go │ │ ├── show.go │ │ └── validate.go │ ├── console │ │ └── console.go │ ├── consul │ │ ├── attach.go │ │ ├── consul.go │ │ └── detach.go │ ├── context.go │ ├── context_test.go │ ├── create │ │ └── create.go │ ├── curl │ │ └── curl.go │ ├── dashboard │ │ └── root.go │ ├── deploy │ │ ├── common_secrets.go │ │ ├── common_secrets_test.go │ │ ├── deploy.go │ │ ├── deploy_build.go │ │ ├── deploy_build_test.go │ │ ├── deploy_first.go │ │ ├── deploy_test.go │ │ ├── flyctl.cache.lock │ │ ├── flyctl.config.lock │ │ ├── machinebasedtest.go │ │ ├── machines.go │ │ ├── machines_deploymachinesapp.go │ │ ├── machines_deploymachinesapp_test.go │ │ ├── machines_launchinput.go │ │ ├── machines_launchinput_test.go │ │ ├── machines_releasecommand.go │ │ ├── machines_test.go │ │ ├── manifest.go │ │ ├── manifest_test.go │ │ ├── mock_client_test.go │ │ ├── plan.go │ │ ├── plan_test.go │ │ ├── statics │ │ │ ├── addon.go │ │ │ ├── deployer.go │ │ │ ├── filesystem.go │ │ │ ├── move.go │ │ │ └── util.go │ │ ├── strategy_bluegreen.go │ │ ├── strategy_bluegreen_test.go │ │ ├── testdata │ │ │ └── basic │ │ │ │ ├── Dockerfile │ │ │ │ └── fly.toml │ │ └── web_client.go │ ├── destroy │ │ └── destroy.go │ ├── dig │ │ └── dig.go │ ├── dnsrecords │ │ └── root.go │ ├── docs │ │ └── docs.go │ ├── doctor │ │ ├── app_checks.go │ │ ├── diag │ │ │ └── diag.go │ │ ├── doctor.go │ │ └── wireguard.go │ ├── domains │ │ └── root.go │ ├── errors │ │ └── errors.go │ ├── extensions │ │ ├── arcjet │ │ │ ├── arcjet.go │ │ │ ├── create.go │ │ │ ├── dashboard.go │ │ │ ├── destroy.go │ │ │ ├── list.go │ │ │ └── status.go │ │ ├── core │ │ │ └── core.go │ │ ├── enveloop │ │ │ ├── create.go │ │ │ ├── dashboard.go │ │ │ ├── destroy.go │ │ │ ├── enveloop.go │ │ │ ├── list.go │ │ │ └── status.go │ │ ├── extensions.go │ │ ├── fly_mysql │ │ │ ├── create.go │ │ │ ├── destroy.go │ │ │ ├── fly_mysql.go │ │ │ ├── list.go │ │ │ ├── status.go │ │ │ └── update.go │ │ ├── kubernetes │ │ │ ├── auth.go │ │ │ ├── create.go │ │ │ ├── destroy.go │ │ │ ├── kubeconfig.go │ │ │ ├── kubernetes.go │ │ │ └── list.go │ │ ├── sentry │ │ │ ├── create.go │ │ │ ├── dashboard.go │ │ │ ├── destroy.go │ │ │ ├── list.go │ │ │ └── sentry.go │ │ ├── supabase │ │ │ ├── dashboard.go │ │ │ ├── destroy.go │ │ │ ├── list.go │ │ │ ├── status.go │ │ │ └── supabase.go │ │ ├── tigris │ │ │ ├── create.go │ │ │ ├── dashboard.go │ │ │ ├── destroy.go │ │ │ ├── list.go │ │ │ ├── status.go │ │ │ ├── tigris.go │ │ │ └── update.go │ │ ├── vector │ │ │ ├── create.go │ │ │ ├── dashboard.go │ │ │ ├── destroy.go │ │ │ ├── list.go │ │ │ ├── status.go │ │ │ └── vector.go │ │ └── wafris │ │ │ ├── create.go │ │ │ ├── dashboard.go │ │ │ ├── destroy.go │ │ │ └── wafris.go │ ├── history │ │ └── history.go │ ├── image │ │ ├── image.go │ │ ├── show.go │ │ ├── update.go │ │ └── update_machines.go │ ├── incidents │ │ ├── hosts │ │ │ ├── hosts.go │ │ │ └── list.go │ │ └── incidents.go │ ├── info │ │ └── info.go │ ├── ips │ │ ├── allocate.go │ │ ├── ips.go │ │ ├── list.go │ │ ├── private.go │ │ ├── release.go │ │ └── render.go │ ├── jobs │ │ ├── jobs.go │ │ └── open.go │ ├── launch │ │ ├── cmd.go │ │ ├── deploy.go │ │ ├── describe_plan.go │ │ ├── dockerfiles.go │ │ ├── launch.go │ │ ├── launch_databases.go │ │ ├── launch_extensions.go │ │ ├── launch_frameworks.go │ │ ├── plan │ │ │ ├── context.go │ │ │ ├── github_actions.go │ │ │ ├── object_storage.go │ │ │ ├── plan.go │ │ │ ├── postgres.go │ │ │ └── redis.go │ │ ├── plan_builder.go │ │ ├── plan_commands.go │ │ ├── sourceinfo.go │ │ ├── state.go │ │ └── webui.go │ ├── lfsc │ │ ├── clusters.go │ │ ├── clusters_create.go │ │ ├── clusters_destroy.go │ │ ├── clusters_list.go │ │ ├── export.go │ │ ├── import.go │ │ ├── lfsc.go │ │ ├── regions.go │ │ ├── restore.go │ │ └── status.go │ ├── logs │ │ └── logs.go │ ├── machine │ │ ├── clone.go │ │ ├── cordon.go │ │ ├── destroy.go │ │ ├── egress_ip.go │ │ ├── exec.go │ │ ├── kill.go │ │ ├── leases.go │ │ ├── lifecycle_hooks.go │ │ ├── list.go │ │ ├── machine.go │ │ ├── place.go │ │ ├── proxy.go │ │ ├── restart.go │ │ ├── run.go │ │ ├── select.go │ │ ├── start.go │ │ ├── status.go │ │ ├── stop.go │ │ ├── suspend.go │ │ ├── uncordon.go │ │ └── update.go │ ├── mcp │ │ ├── config.go │ │ ├── destroy.go │ │ ├── launch.go │ │ ├── list.go │ │ ├── logs.go │ │ ├── mcp.go │ │ ├── proxy.go │ │ ├── proxy │ │ │ ├── passthru.go │ │ │ ├── relay.go │ │ │ └── types.go │ │ ├── server.go │ │ ├── server │ │ │ ├── apps.go │ │ │ ├── logs.go │ │ │ ├── machine.go │ │ │ ├── orgs.go │ │ │ ├── platform.go │ │ │ ├── status.go │ │ │ ├── types.go │ │ │ └── volumes.go │ │ ├── volume.go │ │ └── wrap.go │ ├── metrics │ │ └── metrics.go │ ├── move │ │ └── move.go │ ├── mpg │ │ ├── attach.go │ │ ├── connect.go │ │ ├── create.go │ │ ├── list.go │ │ ├── mpg.go │ │ ├── proxy.go │ │ └── status.go │ ├── mysql │ │ └── mysql.go │ ├── open │ │ └── open.go │ ├── options.go │ ├── orgs │ │ ├── create.go │ │ ├── delete.go │ │ ├── invite.go │ │ ├── list.go │ │ ├── orgs.go │ │ ├── remove.go │ │ └── show.go │ ├── ping │ │ └── ping.go │ ├── platform │ │ ├── platform.go │ │ ├── regions.go │ │ ├── status.go │ │ └── vmsizes.go │ ├── postgres │ │ ├── add_flycast.go │ │ ├── attach.go │ │ ├── backup.go │ │ ├── barman.go │ │ ├── config.go │ │ ├── config_show.go │ │ ├── config_update.go │ │ ├── connect.go │ │ ├── create.go │ │ ├── db.go │ │ ├── detach.go │ │ ├── events.go │ │ ├── failover.go │ │ ├── import.go │ │ ├── list.go │ │ ├── postgres.go │ │ ├── postgres_test.go │ │ ├── renew_certs.go │ │ ├── restart.go │ │ └── users.go │ ├── proxy │ │ └── proxy.go │ ├── redis │ │ ├── attach.go │ │ ├── connect.go │ │ ├── create.go │ │ ├── dashboard.go │ │ ├── destroy.go │ │ ├── list.go │ │ ├── plans.go │ │ ├── proxy.go │ │ ├── redis.go │ │ ├── reset_password.go │ │ ├── status.go │ │ └── update.go │ ├── regions │ │ ├── machines.go │ │ └── root.go │ ├── registry │ │ ├── args.go │ │ ├── auth.go │ │ ├── command.go │ │ ├── files.go │ │ ├── filter.go │ │ ├── sbom.go │ │ ├── scantron.go │ │ ├── vulns.go │ │ └── vulnsummary.go │ ├── releases │ │ └── releases.go │ ├── restart │ │ └── restart.go │ ├── resume │ │ └── resume.go │ ├── root │ │ └── root.go │ ├── scale │ │ ├── count.go │ │ ├── count_machines.go │ │ ├── count_machines_test.go │ │ ├── machine_defaults.go │ │ ├── machines.go │ │ ├── memory.go │ │ ├── scale.go │ │ ├── show.go │ │ ├── show_machines.go │ │ ├── show_machines_test.go │ │ └── vm.go │ ├── secrets │ │ ├── deploy.go │ │ ├── import.go │ │ ├── key_delete.go │ │ ├── key_set.go │ │ ├── keys.go │ │ ├── keys_common.go │ │ ├── keys_list.go │ │ ├── list.go │ │ ├── parser.go │ │ ├── parser_test.go │ │ ├── secrets.go │ │ ├── set.go │ │ └── unset.go │ ├── services │ │ ├── list.go │ │ └── services.go │ ├── settings │ │ ├── analytics.go │ │ ├── autoupdate.go │ │ ├── settings.go │ │ └── synthetics.go │ ├── ssh │ │ ├── connect.go │ │ ├── console.go │ │ ├── console_stub.go │ │ ├── console_windows.go │ │ ├── issue.go │ │ ├── log.go │ │ ├── sftp.go │ │ ├── ssh.go │ │ └── ssh_terminal.go │ ├── status │ │ ├── machines.go │ │ ├── machines_test.go │ │ └── status.go │ ├── storage │ │ └── storage.go │ ├── suspend │ │ └── suspend.go │ ├── synthetics │ │ └── synthetics.go │ ├── tokens │ │ ├── 3p.go │ │ ├── attenuate.go │ │ ├── create.go │ │ ├── debug.go │ │ ├── list.go │ │ ├── revoke.go │ │ └── tokens.go │ ├── version │ │ ├── save_install.go │ │ ├── upgrade.go │ │ └── version.go │ ├── volumes │ │ ├── create.go │ │ ├── destroy.go │ │ ├── extend.go │ │ ├── fork.go │ │ ├── list.go │ │ ├── lsvd │ │ │ ├── lsvd.go │ │ │ └── setup.go │ │ ├── show.go │ │ ├── snapshots │ │ │ ├── create.go │ │ │ ├── list.go │ │ │ └── snapshots.go │ │ ├── update.go │ │ └── volumes.go │ └── wireguard │ │ ├── others.go │ │ ├── root.go │ │ ├── tokens.go │ │ └── wireguard.go ├── command_context │ └── context.go ├── config │ ├── config.go │ ├── context.go │ ├── context_test.go │ ├── file.go │ ├── machine.go │ ├── tokens.go │ └── tokens_test.go ├── ctrlc │ ├── context.go │ └── ctrlc.go ├── env │ └── env.go ├── filemu │ └── filemu.go ├── flag │ ├── completion │ │ ├── completion_preparers.go │ │ └── completions.go │ ├── context.go │ ├── flag.go │ ├── flagctx │ │ └── helpers.go │ ├── flagnames │ │ └── constants.go │ └── machines.go ├── flapsutil │ ├── flaps_client.go │ └── flapsutil.go ├── flyerr │ └── flyerr.go ├── flyutil │ ├── client.go │ └── flyutil.go ├── format │ └── format.go ├── future │ └── future.go ├── haikunator │ ├── haikunator.go │ └── haikunator_test.go ├── httptracing │ └── har.go ├── incidents │ ├── hosts.go │ ├── incidents.go │ └── statuspage.go ├── inmem │ ├── client.go │ ├── flaps_client.go │ └── server.go ├── instrument │ └── call.go ├── launchdarkly │ └── launchdarkly.go ├── logger │ ├── context.go │ ├── context_test.go │ ├── file_logger.go │ ├── global_logfile.go │ ├── logfile │ │ ├── fs.go │ │ └── name.go │ ├── logger.go │ ├── split_logger.go │ └── writer_logger.go ├── machine │ ├── config.go │ ├── ephemeral.go │ ├── leasable_machine.go │ ├── lease.go │ ├── machine_set.go │ ├── machine_set_test.go │ ├── query.go │ ├── restart.go │ ├── update.go │ └── wait.go ├── metrics │ ├── api.go │ ├── command.go │ ├── db.go │ ├── helpers.go │ ├── synthetics │ │ ├── agent.go │ │ ├── prober.go │ │ ├── signals_unix.go │ │ ├── signals_windows.go │ │ ├── synthetics.go │ │ ├── token.go │ │ └── ws.go │ └── token.go ├── mock │ ├── client.go │ └── flaps_client.go ├── oci │ └── image.go ├── prompt │ ├── prompt.go │ └── prompt_test.go ├── release │ └── meta.go ├── render │ ├── checks.go │ ├── logs.go │ └── render.go ├── sentry │ └── sentry.go ├── set │ ├── set.go │ └── set_test.go ├── sort │ └── sort.go ├── spinner │ └── spinner.go ├── state │ ├── state.go │ └── state_test.go ├── statuslogger │ ├── context.go │ ├── create.go │ ├── interactiveline.go │ ├── interactivelogger.go │ ├── interface.go │ ├── noninteractive.go │ └── shared.go ├── task │ ├── task.go │ └── task_test.go ├── tracing │ ├── tracing.go │ └── transport.go ├── uiex │ ├── client.go │ └── managed_postgres.go ├── uiexutil │ ├── client.go │ └── uiexutil.go ├── update │ ├── memoize.go │ └── update.go ├── version │ ├── version.go │ └── version_test.go ├── watch │ └── watch.go └── wireguard │ └── wg.go ├── iostreams ├── color.go ├── color_test.go ├── context.go ├── context_test.go └── iostreams.go ├── ip └── ip.go ├── logs ├── entry.go ├── logs.go ├── nats.go └── polling.go ├── main.go ├── proxy ├── connect.go └── server.go ├── scanner ├── bridgetown.go ├── deno.go ├── django.go ├── dockerfile.go ├── dockerfile_test.go ├── dotnet.go ├── elixir.go ├── flask.go ├── github.go ├── gitignore.go ├── go.go ├── helpers.go ├── jsFramework.go ├── laravel.go ├── lucky.go ├── nextjs.go ├── node.go ├── nuxtjs.go ├── phoenix.go ├── python.go ├── rails.go ├── redwood.go ├── ruby.go ├── rust.go ├── scanner.go ├── static.go └── templates │ ├── bridgetown │ └── Dockerfile │ ├── deno │ ├── .dockerignore │ ├── Dockerfile │ └── example.ts │ ├── django │ ├── .dockerignore │ └── Dockerfile │ ├── dotnet │ ├── .dockerignore │ └── Dockerfile │ ├── flask │ └── Dockerfile │ ├── github │ └── .github │ │ └── workflows │ │ └── fly-deploy.yml │ ├── go │ └── Dockerfile │ ├── laravel │ ├── .dockerignore │ ├── .fly │ │ ├── entrypoint.sh │ │ └── scripts │ │ │ ├── .gitkeep │ │ │ └── caches.sh │ └── Dockerfile │ ├── lucky │ ├── .dockerignore │ └── Dockerfile │ ├── nextjs │ ├── .dockerignore │ └── Dockerfile │ ├── node │ ├── .dockerignore │ ├── Dockerfile │ └── docker-entrypoint │ ├── nuxtjs │ ├── .dockerignore │ └── Dockerfile │ ├── phoenix │ ├── .dockerignore │ └── Dockerfile │ ├── python-docker │ ├── .dockerignore │ └── Dockerfile │ ├── python │ ├── .dockerignore │ └── Procfile │ ├── redwood │ ├── .dockerignore │ ├── .fly │ │ ├── migrate.sh │ │ ├── release.sh │ │ └── start.sh │ └── Dockerfile │ ├── ruby │ └── Dockerfile │ ├── rust │ ├── .dockerignore │ └── Dockerfile │ └── static │ ├── .dockerignore │ └── Dockerfile ├── scripts ├── build-dfly ├── bump_version.sh ├── changelog.sh ├── clean-up-preflight-apps │ └── main.go ├── delete_preflight_apps.sh ├── dev_release.sh ├── dfly ├── force_bump_version.sh ├── generate_docs.sh ├── preflight.sh ├── publish_docs.sh ├── semver ├── version.sh └── yank_version.sh ├── shell.nix ├── ssh ├── client.go ├── io.go ├── key.go ├── terminal_unix.go └── terminal_windows.go ├── terminal └── logging.go ├── test └── preflight │ ├── apps_v2_integration_test.go │ ├── fixtures │ ├── deploy-node │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── fly.toml │ │ ├── index.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── somefile │ ├── example-buildpack │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── app.rb │ │ ├── config.ru │ │ ├── fly.toml │ │ └── release.sh │ ├── example │ │ ├── Dockerfile │ │ ├── fly.toml │ │ ├── index.html │ │ └── release.sh │ └── launch-laravel │ │ ├── .gitignore │ │ └── Dockerfile │ ├── fly_console_test.go │ ├── fly_deploy_test.go │ ├── fly_launch_test.go │ ├── fly_logs_test.go │ ├── fly_machine_test.go │ ├── fly_postgres_test.go │ ├── fly_scale_test.go │ ├── fly_tokens_test.go │ ├── fly_volume_test.go │ └── testlib │ ├── helpers.go │ ├── result.go │ └── test_env.go ├── tools.go ├── tools ├── distribute │ ├── bundle │ │ ├── archive.go │ │ └── meta.go │ ├── flypkgs │ │ ├── api.go │ │ ├── errors.go │ │ ├── releases.go │ │ └── types.go │ └── main.go └── version │ ├── main.go │ └── relmeta │ ├── relmeta.go │ ├── relmeta_test.go │ └── repo.go ├── wg ├── signals_unix.go ├── signals_windows.go ├── state.go ├── tunnel.go ├── wg.go └── ws.go └── winbuild.ps1 /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "go" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | import_path = "github.com/superfly/flyctl" 9 | -------------------------------------------------------------------------------- /.direnv/preflight-example: -------------------------------------------------------------------------------- 1 | export FLY_PREFLIGHT_TEST_FLY_ORG="flyctl-test-YOURNAME" 2 | export FLY_PREFLIGHT_TEST_FLY_REGIONS="region1 region2" 3 | export FLY_PREFLIGHT_TEST_ACCESS_TOKEN="Your auth token goes here" 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Fly.io Community Forum 4 | url: https://community.fly.io/ 5 | about: We monitor our community forum for all other discussions. If you have feature requests, need help, or just want to talk about what you're building, post here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/flyctl-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Flyctl Bug Report 3 | about: Report bugs in the Fly CLI 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Please only report specific issues with `flyctl` behavior*. Anything like a support request for your application should go to https://community.fly.io. More people watch that space and can help you faster! 11 | 12 | **Describe the bug** 13 | Briefly, describe what broke and provide the following details: 14 | 15 | * Operating system 16 | * `fly version` 17 | 18 | ** Paste your `fly.toml` 19 | 20 | ```toml 21 | # paste your config file here 22 | ``` 23 | 24 | ** Command output: ** 25 | 26 | ```text 27 | # paste command output here 28 | ``` 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | labels: 6 | - "dependencies" 7 | - "go" 8 | schedule: 9 | interval: "daily" 10 | groups: 11 | tracing: 12 | patterns: 13 | - "go.opentelemetry.io/*" 14 | golangx: 15 | patterns: 16 | - "golang.org/x/*" 17 | aws-sdk: 18 | patterns: 19 | - "github.com/aws/aws-sdk-go-v2/*" 20 | 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | labels: 24 | - "dependencies" 25 | - "actions" 26 | schedule: 27 | interval: "daily" 28 | groups: 29 | artifacts: 30 | patterns: 31 | - "action/upload-artifact" 32 | - "action/download-artifact" 33 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Change Summary 2 | 3 | What and Why: 4 | 5 | How: 6 | 7 | Related to: 8 | 9 | --- 10 | 11 | ### Documentation 12 | 13 | - [ ] Fresh Produce 14 | - [ ] In superfly/docs, or asked for help from docs team 15 | - [ ] n/a 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | schedule: 4 | - cron: '21 */2 * * *' 5 | push: 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} 10 | 11 | jobs: 12 | test_build: 13 | runs-on: ubuntu-latest-m 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version-file: "go.mod" 21 | check-latest: true 22 | - name: "Place wintun.dll" 23 | run: cp deps/wintun/bin/amd64/wintun.dll ./ 24 | - name: build 25 | uses: goreleaser/goreleaser-action@v5 26 | env: 27 | BUILD_ENV: "development" 28 | with: 29 | version: latest 30 | args: build --clean --snapshot --verbose 31 | - name: Upload flyctl for preflight 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: flyctl 35 | path: dist/default_linux_amd64_v1/flyctl 36 | overwrite: true 37 | 38 | preflight: 39 | needs: test_build 40 | uses: ./.github/workflows/preflight.yml 41 | secrets: inherit 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | test: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [ ubuntu-latest, macos-latest, windows-latest ] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version-file: "go.mod" 19 | check-latest: true 20 | - name: Get go version 21 | id: go-version 22 | run: echo "name=version::$(go env GOVERSION)" >> $GITHUB_OUTPUT 23 | - name: go mod download 24 | run: go mod download 25 | - name: go mod verify 26 | run: go mod verify 27 | - name: generate command strings 28 | run: go generate ./... && git diff --exit-code 29 | if: runner.os != 'Windows' 30 | - name: "Place wintun.dll" 31 | run: cp deps/wintun/bin/amd64/wintun.dll ./ 32 | - name: Run tests 33 | run: make test 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ ubuntu-latest, macos-latest, windows-latest ] 12 | test: 13 | - workdir: "." 14 | target: "test" 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version-file: "${{ matrix.test.workdir }}/go.mod" 23 | - name: Run Tests (${{ matrix.test.target }}) 24 | run: make ${{ matrix.test.target }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | /bin 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | dist/ 19 | 20 | # Local environment settings 21 | .envrc 22 | 23 | tmp/ 24 | 25 | # generated docs 26 | out 27 | #docstrings/gen.go 28 | /.idea/* 29 | /.idea/.gitignore 30 | /.idea/dictionaries/dj.xml 31 | /.idea/flyctl.iml 32 | /.idea/misc.xml 33 | /.idea/modules.xml 34 | /.idea/vcs.xml 35 | .vscode/launch.json 36 | .DS_Store 37 | 38 | .direnv/ 39 | /ci-preflight-test-results.njson 40 | .tool-versions 41 | 42 | # generated release meta 43 | release.json 44 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | 12 | - repo: https://github.com/tekwizely/pre-commit-golang 13 | rev: v1.0.0-rc.1 14 | hooks: 15 | - id: go-mod-tidy 16 | 17 | # NOTE: This pre-commit hook is ignored when running on Github Workflow 18 | # because goalngci-lint github action is much more useful than the pre-commit action. 19 | # The trick is to run github action only for "manual" hook stage 20 | - repo: https://github.com/golangci/golangci-lint 21 | rev: v1.54.2 22 | hooks: 23 | - id: golangci-lint 24 | stages: [pre-commit] 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "grammarly.userWords": [ 3 | "dj", 4 | "flyctl", 5 | "helloruby", 6 | "microVM", 7 | "toc", 8 | "winbuild" 9 | ], 10 | "editor.formatOnSave": true, 11 | "go.buildFlags": [ 12 | "-tags", 13 | "integration" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as build 2 | RUN apk --no-cache add ca-certificates 3 | 4 | RUN mkdir /newtmp && chown 1777 /newtmp 5 | 6 | FROM scratch 7 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 8 | COPY --from=build /newtmp /tmp 9 | 10 | COPY flyctl / 11 | 12 | ENTRYPOINT ["/flyctl"] 13 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as build 2 | RUN apk --no-cache add ca-certificates 3 | 4 | WORKDIR /build 5 | 6 | COPY go.mod go.sum ./ 7 | 8 | RUN CGO_ENABLED=0 go mod download 9 | 10 | COPY . . 11 | RUN CGO_ENABLED=0 go build -o /flyctl -ldflags="-X 'github.com/superfly/flyctl/internal/buildinfo.buildDate=NOW_RFC3339'" . 12 | 13 | RUN mkdir /newtmp && chown 1777 /newtmp 14 | 15 | FROM scratch 16 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 17 | COPY --from=build /newtmp /tmp 18 | COPY --from=build /flyctl / 19 | 20 | ENTRYPOINT ["/flyctl"] 21 | -------------------------------------------------------------------------------- /Dockerfile.mcp: -------------------------------------------------------------------------------- 1 | FROM node:slim 2 | COPY flyctl /usr/bin 3 | COPY --from=ghcr.io/astral-sh/uv:debian /usr/local/bin/uv* /usr/local/bin 4 | EXPOSE 8080 5 | ENTRYPOINT [ "/usr/bin/flyctl", "mcp", "wrap", "--" ] 6 | -------------------------------------------------------------------------------- /agent/agent.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/superfly/flyctl/helpers" 7 | ) 8 | 9 | // TODO: deprecate 10 | func PathToSocket() string { 11 | dir, err := helpers.GetConfigDirectory() 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | return filepath.Join(dir, "fly-agent.sock") 17 | } 18 | 19 | type Instances struct { 20 | Labels []string 21 | Addresses []string 22 | } 23 | -------------------------------------------------------------------------------- /agent/client_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package agent 4 | 5 | import ( 6 | "context" 7 | "net" 8 | ) 9 | 10 | func (c *Client) dialContext(ctx context.Context) (conn net.Conn, err error) { 11 | return c.dialer.DialContext(ctx, c.network, c.address) 12 | } 13 | -------------------------------------------------------------------------------- /agent/client_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package agent 4 | 5 | import ( 6 | "context" 7 | "net" 8 | 9 | "github.com/Microsoft/go-winio" 10 | ) 11 | 12 | func (c *Client) dialContext(ctx context.Context) (conn net.Conn, err error) { 13 | if UseUnixSockets() { 14 | return c.dialer.DialContext(ctx, c.network, c.address) 15 | } 16 | 17 | pipe, err := PipeName() 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return winio.DialPipeContext(ctx, pipe) 23 | } 24 | -------------------------------------------------------------------------------- /agent/cmd_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package agent 4 | 5 | import ( 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | func SetSysProcAttributes(cmd *exec.Cmd) { 11 | cmd.SysProcAttr = &syscall.SysProcAttr{ 12 | Setpgid: true, 13 | Pgid: 0, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /agent/cmd_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package agent 4 | 5 | import ( 6 | "fmt" 7 | "os/exec" 8 | "os/user" 9 | "syscall" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | func SetSysProcAttributes(cmd *exec.Cmd) { 15 | cmd.SysProcAttr = &syscall.SysProcAttr{ 16 | HideWindow: true, 17 | CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP, 18 | } 19 | } 20 | 21 | // Use UNIX sockets since 10.0.17063 22 | // https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ 23 | func UseUnixSockets() bool { 24 | maj, _, patch := windows.RtlGetNtVersionNumbers() 25 | if maj > 10 || maj == 10 && patch >= 17063 { 26 | return true 27 | } 28 | 29 | return false 30 | } 31 | 32 | func PipeName() (string, error) { 33 | user, err := user.Current() 34 | if err != nil { 35 | return "", fmt.Errorf("can't query current username: %w", err) 36 | } 37 | 38 | return `\\.\pipe\fly-agent-` + user.Username, nil 39 | } 40 | -------------------------------------------------------------------------------- /agent/context.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type contextKey struct{} 8 | 9 | func DialerWithContext(ctx context.Context, dialer Dialer) context.Context { 10 | return context.WithValue(ctx, contextKey{}, dialer) 11 | } 12 | 13 | func DialerFromContext(ctx context.Context) Dialer { 14 | return ctx.Value(contextKey{}).(Dialer) 15 | } 16 | -------------------------------------------------------------------------------- /agent/errors.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNoSuchHost = errors.New("host was not found in DNS") 7 | ErrTunnelUnavailable = errors.New("tunnel unavailable") 8 | ) 9 | -------------------------------------------------------------------------------- /agent/internal/proto/proto.go: -------------------------------------------------------------------------------- 1 | // Package proto implements the agent's protocol. 2 | package proto 3 | 4 | import ( 5 | "encoding/binary" 6 | "io" 7 | ) 8 | 9 | func Read(r io.Reader) (data []byte, err error) { 10 | var b [2]byte 11 | if _, err = io.ReadFull(r, b[:]); err == nil { 12 | l := binary.LittleEndian.Uint16(b[:]) 13 | 14 | data = make([]byte, l) 15 | _, err = io.ReadFull(r, data) 16 | } 17 | 18 | return 19 | } 20 | 21 | func Write(w io.Writer, verb string, args ...string) (err error) { 22 | size := len(verb) + len(args) 23 | for _, arg := range args { 24 | size += len(arg) 25 | } 26 | 27 | var b [2]byte 28 | binary.LittleEndian.PutUint16(b[:], uint16(size)) 29 | 30 | if _, err = w.Write(b[:]); err != nil { 31 | return 32 | } 33 | 34 | if _, err = io.WriteString(w, verb); err != nil { 35 | return 36 | } 37 | 38 | for _, arg := range args { 39 | if _, err = io.WriteString(w, " "); err != nil { 40 | break 41 | } 42 | if _, err = io.WriteString(w, arg); err != nil { 43 | break 44 | } 45 | } 46 | 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /agent/server/server_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package server 4 | 5 | import ( 6 | "errors" 7 | "io/fs" 8 | "net" 9 | "os" 10 | ) 11 | 12 | func removeSocket(path string) (err error) { 13 | var stat os.FileInfo 14 | switch stat, err = os.Lstat(path); { 15 | case errors.Is(err, fs.ErrNotExist): 16 | err = nil 17 | case err != nil: 18 | break 19 | case stat.Mode()&os.ModeSocket == 0: 20 | err = errors.New("not a socket") 21 | default: 22 | if err = os.Remove(path); errors.Is(err, fs.ErrNotExist) { 23 | err = nil 24 | } 25 | } 26 | 27 | return 28 | } 29 | 30 | func bindSocket(socket string) (net.Listener, error) { 31 | return bindUnixSocket(socket) 32 | } 33 | -------------------------------------------------------------------------------- /aur/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Jerome Gravel-Niquet 2 | 3 | pkgname="flyctl-bin" 4 | pkgver="${version}" 5 | pkgrel="1" 6 | pkgdesc="Command line tools for fly.io services" 7 | arch=("x86_64") 8 | url="https://fly.io" 9 | license=("Apache") 10 | depends=() 11 | provides=("flyctl") 12 | conflicts=("flyctl") 13 | source=("$pkgname-$pkgver.tgz::https://github.com/superfly/flyctl/releases/download/v${pkgver}/flyctl_${pkgver}_Linux_x86_64.tar.gz") 14 | sha256sums=('${sha256sum}') 15 | 16 | package() { 17 | mkdir -p "$pkgdir/usr/bin" 18 | ln -s flyctl "$pkgdir/usr/bin/fly" 19 | install -m755 flyctl "$pkgdir/usr/bin" 20 | } 21 | -------------------------------------------------------------------------------- /cmd/audit/audit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func main() { 10 | rootCmd := &cobra.Command{ 11 | Use: "audit", 12 | Short: "Tool for auditing flyctl commands", 13 | } 14 | 15 | rootCmd.AddCommand(newListCmd()) 16 | rootCmd.AddCommand(newStatsCmd()) 17 | rootCmd.AddCommand(newLintCmd()) 18 | 19 | if err := rootCmd.Execute(); err != nil { 20 | log.Fatalln(err) 21 | } 22 | } 23 | 24 | type walkFn func(cmd *cobra.Command, depth int) 25 | 26 | func walkInternal(cmd *cobra.Command, maxDepth int, depth int, fn walkFn) { 27 | if maxDepth > -1 && depth > maxDepth { 28 | return 29 | } 30 | 31 | fn(cmd, depth) 32 | 33 | for _, childCmd := range cmd.Commands() { 34 | walkInternal(childCmd, maxDepth, depth+1, fn) 35 | } 36 | } 37 | 38 | func walk(cmd *cobra.Command, fn walkFn) { 39 | walkInternal(cmd, -1, 0, fn) 40 | } 41 | 42 | func walkMaxDepth(cmd *cobra.Command, maxDepth int, fn walkFn) { 43 | walkInternal(cmd, maxDepth, 0, fn) 44 | } 45 | -------------------------------------------------------------------------------- /debug_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "os/signal" 9 | "runtime/pprof" 10 | 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | // handleDebugSignal handles SIGUSR2 and dumps debug information. 15 | func handleDebugSignal(ctx context.Context) { 16 | sigCh := make(chan os.Signal, 1) 17 | signal.Notify(sigCh, unix.SIGUSR2) 18 | 19 | for { 20 | select { 21 | case <-sigCh: 22 | pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) 23 | case <-ctx.Done(): 24 | return 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /debug_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package main 4 | 5 | import "context" 6 | 7 | // handleDebugSignal is no-op on Windows 8 | func handleDebugSignal(_ context.Context) { 9 | } 10 | -------------------------------------------------------------------------------- /deps/wintun/bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/deps/wintun/bin/.gitkeep -------------------------------------------------------------------------------- /deps/wintun/bin/amd64/wintun.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/deps/wintun/bin/amd64/wintun.dll -------------------------------------------------------------------------------- /deps/wintun/bin/arm/wintun.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/deps/wintun/bin/arm/wintun.dll -------------------------------------------------------------------------------- /deps/wintun/bin/arm64/wintun.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/deps/wintun/bin/arm64/wintun.dll -------------------------------------------------------------------------------- /deps/wintun/bin/x86/wintun.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/deps/wintun/bin/x86/wintun.dll -------------------------------------------------------------------------------- /genqlient.yaml: -------------------------------------------------------------------------------- 1 | # Default genqlient config; for full documentation see: 2 | # https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml 3 | schema: gql/schema.graphql 4 | operations: 5 | - gql/genqclient.graphql 6 | - agent/**.go 7 | - internal/appconfig/*.go 8 | - internal/command/**/*.go 9 | - internal/command/**/**/*.go 10 | - internal/build/imgsrc/*.go 11 | - scripts/clean-up-preflight-apps/*.go 12 | bindings: 13 | JSON: 14 | type: interface{} 15 | BigInt: 16 | type: int64 17 | ISO8601DateTime: 18 | type: time.Time 19 | generated: gql/generated.go 20 | -------------------------------------------------------------------------------- /gql/conversions.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | fly "github.com/superfly/fly-go" 5 | ) 6 | 7 | // AppForFlaps converts the genqclient AppFragment to an AppCompact suitable for flaps, which only needs two fields 8 | func ToAppCompact(app AppData) *fly.AppCompact { 9 | return &fly.AppCompact{ 10 | Name: app.Name, 11 | Deployed: app.Deployed, 12 | PlatformVersion: string(app.PlatformVersion), 13 | Organization: &fly.OrganizationBasic{ 14 | Slug: app.Organization.Slug, 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gql/inputs.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | func DefaultCreateAppInput() CreateAppInput { 4 | return CreateAppInput{ 5 | Runtime: "FIRECRACKER", 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /gql/types.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | // Alias unwieldy types from GraphQL generated code 4 | type AddOn = CreateAddOnCreateAddOnCreateAddOnPayloadAddOn 5 | type AddOnOptions map[string]interface{} 6 | type LimitedAccessTokenOptions map[string]interface{} 7 | type AddOnEnvironment map[string]interface{} 8 | -------------------------------------------------------------------------------- /helpers/config.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // GetConfigDirectory will return where config and state files should be 9 | // stored, either respecting `FLY_CONFIG_DIR` or defaulting to the user's home 10 | // directory at `~/.fly`. 11 | func GetConfigDirectory() (string, error) { 12 | if value, isSet := os.LookupEnv("FLY_CONFIG_DIR"); isSet { 13 | return value, nil 14 | } 15 | homeDir, err := os.UserHomeDir() 16 | if err != nil { 17 | return "", err 18 | } 19 | return filepath.Join(homeDir, ".fly"), nil 20 | } 21 | -------------------------------------------------------------------------------- /helpers/config_test.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetConfigDirectoryWithEnv(t *testing.T) { 12 | var previousEnv string 13 | if v, isSet := os.LookupEnv("FLY_CONFIG_DIR"); isSet { 14 | previousEnv = v 15 | } 16 | 17 | os.Setenv("FLY_CONFIG_DIR", "/var/db/flyctl") 18 | 19 | value, err := GetConfigDirectory() 20 | assert.NoError(t, err) 21 | assert.Equal(t, "/var/db/flyctl", value) 22 | 23 | if previousEnv != "" { 24 | os.Setenv("FLY_CONFIG_DIR", previousEnv) 25 | } 26 | } 27 | 28 | func TestGetConfigDirectoryDefault(t *testing.T) { 29 | var previousEnv string 30 | if v, isSet := os.LookupEnv("FLY_CONFIG_DIR"); isSet { 31 | previousEnv = v 32 | } 33 | 34 | os.Unsetenv("FLY_CONFIG_DIR") 35 | 36 | value, err := GetConfigDirectory() 37 | assert.NoError(t, err) 38 | 39 | homeDir, err := os.UserHomeDir() 40 | assert.NoError(t, err) 41 | 42 | assert.Equal(t, filepath.Join(homeDir, ".fly"), value) 43 | 44 | if previousEnv != "" { 45 | os.Setenv("FLY_CONFIG_DIR", previousEnv) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /helpers/duration.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | func Duration(d time.Duration, dicimal int) time.Duration { 9 | shift := int(math.Pow10(dicimal)) 10 | 11 | units := []time.Duration{time.Second, time.Millisecond, time.Microsecond, time.Nanosecond} 12 | for _, u := range units { 13 | if d > u { 14 | div := u / time.Duration(shift) 15 | if div == 0 { 16 | break 17 | } 18 | d = d / div * div 19 | break 20 | } 21 | } 22 | 23 | return d 24 | } 25 | -------------------------------------------------------------------------------- /helpers/fs.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | ) 8 | 9 | func FileExists(path string) bool { 10 | info, err := os.Stat(path) 11 | if err != nil { 12 | return false 13 | } 14 | return !info.IsDir() 15 | } 16 | 17 | func DirectoryExists(path string) bool { 18 | info, err := os.Stat(path) 19 | if err != nil { 20 | return false 21 | } 22 | return info.IsDir() 23 | } 24 | 25 | func PathRelativeToCWD(path string) string { 26 | cwd, err := os.Getwd() 27 | if err != nil { 28 | return path 29 | } 30 | path, err = filepath.Rel(cwd, path) 31 | if err != nil { 32 | return path 33 | } 34 | return path 35 | } 36 | 37 | func MkdirAll(pathname string) error { 38 | if path.Ext(pathname) != "" { 39 | pathname = filepath.Dir(pathname) 40 | } 41 | 42 | // TODO: this should probably be 0755 43 | return os.MkdirAll(pathname, 0o777) 44 | } 45 | -------------------------------------------------------------------------------- /helpers/stdin.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func IsTerminal() bool { 12 | if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 { 13 | return true 14 | } 15 | 16 | return false 17 | } 18 | 19 | func ReadStdin(maxLength int) (string, error) { 20 | reader := bufio.NewReader(os.Stdin) 21 | var output []rune 22 | 23 | bytesRead := 0 24 | 25 | for { 26 | input, size, err := reader.ReadRune() 27 | if err != nil && err == io.EOF { 28 | break 29 | } else if err != nil { 30 | return "", err 31 | } 32 | bytesRead += size 33 | if bytesRead > maxLength { 34 | return "", fmt.Errorf("Input exceeded max length of %d bytes", maxLength) 35 | } 36 | output = append(output, input) 37 | } 38 | 39 | return strings.TrimSpace(string(output)), nil 40 | } 41 | 42 | // HasPipedStdin returns if stdin has piped input 43 | func HasPipedStdin() bool { 44 | stat, _ := os.Stdin.Stat() 45 | return (stat.Mode() & os.ModeCharDevice) == 0 46 | } 47 | -------------------------------------------------------------------------------- /helpers/tablehelper.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "io" 5 | 6 | tablewriter "github.com/olekukonko/tablewriter" 7 | ) 8 | 9 | func MakeSimpleTable(out io.Writer, headings []string) (table *tablewriter.Table) { 10 | newtable := tablewriter.NewWriter(out) 11 | // Future code to turn headers bold 12 | // headercolors := []tablewriter.Colors{} 13 | // for range headings { 14 | // headercolors = append(headercolors, tablewriter.Colors{tablewriter.Bold}) 15 | // } 16 | newtable.SetHeader(headings) 17 | newtable.SetHeaderLine(true) 18 | newtable.SetBorder(false) 19 | newtable.SetAutoFormatHeaders(true) 20 | newtable.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 21 | newtable.SetAlignment(tablewriter.ALIGN_LEFT) 22 | newtable.SetTablePadding(" ") 23 | newtable.SetCenterSeparator("*") 24 | newtable.SetRowSeparator("-") 25 | newtable.SetAutoWrapText(false) 26 | return newtable 27 | } 28 | -------------------------------------------------------------------------------- /helpers/units.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func ParseSize(s string, parser func(string) (int64, error), quotient int) (int, error) { 9 | size, err := strconv.Atoi(s) 10 | if err != nil { 11 | sizeBytes, err := parser(s) 12 | if err != nil { 13 | return 0, fmt.Errorf("invalid size: %w", err) 14 | } 15 | 16 | size = int(sizeBytes) / quotient 17 | } 18 | 19 | return size, nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/appconfig/context_test.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestConfigFromContextReturnsNil(t *testing.T) { 11 | assert.Nil(t, ConfigFromContext(context.Background())) 12 | } 13 | 14 | func TestConfigFromContext(t *testing.T) { 15 | exp := new(Config) 16 | 17 | ctx := WithConfig(context.Background(), exp) 18 | assert.Same(t, exp, ConfigFromContext(ctx)) 19 | } 20 | 21 | func TestNameFromContextReturnsEmptyString(t *testing.T) { 22 | assert.Equal(t, "", NameFromContext(context.Background())) 23 | } 24 | 25 | func TestNameFromContext(t *testing.T) { 26 | const exp = "123" 27 | 28 | ctx := WithName(context.Background(), exp) 29 | assert.Equal(t, exp, NameFromContext(ctx)) 30 | } 31 | -------------------------------------------------------------------------------- /internal/appconfig/definition.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | import ( 4 | "github.com/pelletier/go-toml/v2" 5 | fly "github.com/superfly/fly-go" 6 | ) 7 | 8 | func (c *Config) ToDefinition() (*fly.Definition, error) { 9 | var err error 10 | buf, err := toml.Marshal(c) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | definition := &fly.Definition{} 16 | if err := toml.Unmarshal(buf, definition); err != nil { 17 | return nil, err 18 | } 19 | return definition, nil 20 | } 21 | 22 | func FromDefinition(definition *fly.Definition) (*Config, error) { 23 | buf, err := toml.Marshal(*definition) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return unmarshalTOML(buf) 28 | } 29 | -------------------------------------------------------------------------------- /internal/appconfig/file.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | ) 8 | 9 | func ResolveConfigFileFromPath(p string) (string, error) { 10 | p, err := filepath.Abs(p) 11 | if err != nil { 12 | return "", err 13 | } 14 | 15 | // Is this a bare directory path? Stat the path 16 | pd, err := os.Stat(p) 17 | if err != nil { 18 | if os.IsNotExist(err) { 19 | return p, nil 20 | } 21 | return "", err 22 | } 23 | 24 | // Ok, something exists. Is it a file - yes? return the path 25 | if pd.IsDir() { 26 | return path.Join(p, DefaultConfigFileName), nil 27 | } 28 | 29 | return p, nil 30 | } 31 | 32 | func ConfigFileExistsAtPath(p string) (bool, error) { 33 | p, err := ResolveConfigFileFromPath(p) 34 | if err != nil { 35 | return false, err 36 | } 37 | _, err = os.Stat(p) 38 | return !os.IsNotExist(err), nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/appconfig/flyctl.config.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/internal/appconfig/flyctl.config.lock -------------------------------------------------------------------------------- /internal/appconfig/from_machine_set_test.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestQuotePosixWords(t *testing.T) { 10 | type test struct { 11 | input []string 12 | expected []string 13 | } 14 | tests := []test{ 15 | {input: []string{ 16 | "nginx", "-g", "daemon off;", 17 | }, expected: []string{ 18 | "nginx", "-g", "'daemon off;'", 19 | }}, 20 | {input: []string{ 21 | "", 22 | }, expected: []string{ 23 | "", 24 | }}, 25 | {input: []string{ 26 | "/app", 27 | }, expected: []string{ 28 | "/app", 29 | }}, 30 | {input: []string{ 31 | "bundle", "exec", "rake", "db:migrate", 32 | }, expected: []string{ 33 | "bundle", "exec", "rake", "db:migrate", 34 | }}, 35 | {input: []string{ 36 | "/release_cmd.sh", "foo", "bar", "baz", "123", "--six=seven", 37 | }, expected: []string{ 38 | "/release_cmd.sh", "foo", "bar", "baz", "123", "'--six=seven'", 39 | }}, 40 | {input: []string{ 41 | "echo", "hi there", 42 | }, expected: []string{ 43 | "echo", `"hi there"`, 44 | }}, 45 | } 46 | for _, tc := range tests { 47 | result := quotePosixWords(tc.input) 48 | require.EqualValues(t, tc.expected, result) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/appconfig/platformversion.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | // SetMachinesPlatform informs the TOML marshaller that this config is for the machines platform 4 | func (c *Config) SetMachinesPlatform() error { 5 | if c.v2UnmarshalError != nil { 6 | return c.v2UnmarshalError 7 | } 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /internal/appconfig/platformversion_test.go: -------------------------------------------------------------------------------- 1 | package appconfig 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSetMachinesPlatform(t *testing.T) { 11 | cfg := NewConfig() 12 | assert.NoError(t, cfg.SetMachinesPlatform()) 13 | 14 | cfg.v2UnmarshalError = fmt.Errorf("Failed to parse fly.toml") 15 | assert.Error(t, cfg.SetMachinesPlatform()) 16 | } 17 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/always-invalid-v2.toml: -------------------------------------------------------------------------------- 1 | app = "unsupported-format" 2 | 3 | [build] 4 | builder = "dockerfile" 5 | image = "foo/fighter" 6 | builtin = "whatisthis" 7 | dockerfile = "Dockerfile" 8 | ignorefile = ".gitignore" 9 | build-target = "target" 10 | buildpacks = ["packme", "well"] 11 | 12 | [build.settings] 13 | foo = "bar" 14 | other = 2 15 | 16 | [build.args] 17 | param1 = "value1" 18 | param2 = "value2" 19 | 20 | [[services]] 21 | internal_port = "8080" 22 | # Single numerical concurrency is not valid 23 | # but we are testing here that this file can't be parsed 24 | concurrency = 20 25 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/app-name.toml: -------------------------------------------------------------------------------- 1 | app = "test-app" 2 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/build-with-args.toml: -------------------------------------------------------------------------------- 1 | app = "build-with-args" 2 | 3 | [build] 4 | builder = "builder/name" 5 | [build.args] 6 | A = "B" 7 | C = "D" 8 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/build.toml: -------------------------------------------------------------------------------- 1 | app = "build" 2 | 3 | [build] 4 | builder = "builder/name" 5 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/docker.toml: -------------------------------------------------------------------------------- 1 | app = "image" 2 | 3 | [build] 4 | dockerfile = "./Dockerfile" 5 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/env-list.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [[env]] 4 | FOO = "BAR" 5 | TWO = 2 6 | 7 | [[env]] 8 | TRUE = true 9 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/experimental-alt.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [experimental] 4 | cmd = "cmd" 5 | exec = "exec" 6 | entrypoint = "entrypoint" 7 | kill_timeout = 3 8 | metrics_port = 9000 9 | metrics_path = "/foo" 10 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/format-quirks.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [vm] 4 | memory = 512 5 | 6 | [mount] 7 | source = "data" 8 | destination = "/data" 9 | initial_size = 200 10 | snapshot_retention = 10 11 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/image.toml: -------------------------------------------------------------------------------- 1 | app = "image" 2 | 3 | [build] 4 | image = "image/name" 5 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/mounts-array.toml: -------------------------------------------------------------------------------- 1 | # Seen at https://community.fly.io/t/problem-exposing-postgresql-database-to-external-world/10869/7 2 | app = "foo" 3 | 4 | [[mounts]] 5 | source = "pg_data" 6 | destination = "/data" 7 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/old-pg-checks.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [checks.pg] 4 | type = "http" 5 | port = 5500 6 | path = "/flycheck/pg" 7 | headers = [] 8 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/old-processes.toml: -------------------------------------------------------------------------------- 1 | [[processes]] 2 | name = "web" 3 | command = "./web" 4 | 5 | [[processes]] 6 | name = "worker" 7 | command = "./worker" 8 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/processes-multi.toml: -------------------------------------------------------------------------------- 1 | # Use this file to test processconfig.go 2 | app = "foo" 3 | 4 | [processes] 5 | zzz = "/zzzz" 6 | bar = "/app/bar" 7 | foo = "/app/foo" 8 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/processes-multiwithapp.toml: -------------------------------------------------------------------------------- 1 | # Use this file to test processconfig.go 2 | app = "foo" 3 | 4 | # "app" is the default process because it is present 5 | [processes] 6 | aaa = "/run1" 7 | ass = "/run2" 8 | app = "/run2" 9 | bbb = "/run3" 10 | abc = "/run1" 11 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/processes-none.toml: -------------------------------------------------------------------------------- 1 | # Use this file to test processconfig.go 2 | app = "foo" 3 | 4 | [processes] 5 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/processes-one.toml: -------------------------------------------------------------------------------- 1 | # Use this file to test processconfig.go 2 | app = "foo" 3 | 4 | [processes] 5 | web = "/app/server" 6 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/services-emptysection.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | # A service without any key must be ignored 4 | [[services]] 5 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/services-multi.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [[services]] 4 | internal_port = 8081 5 | protocol = "tcp" 6 | 7 | [services.concurrency] 8 | type = "requests" 9 | hard_limit = 22 10 | soft_limit = 13 11 | 12 | [[services]] 13 | internal_port = 9999 14 | protocol = "tcp" 15 | 16 | [services.concurrency] 17 | type = "connections" 18 | hard_limit = 10 19 | soft_limit = 8 20 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/services-ports.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [[services]] 4 | internal_port = 8080 5 | protocol = "tcp" 6 | 7 | [[services.ports]] 8 | port = 80 9 | 10 | [services.ports.tls_options] 11 | alpn = ["h2", "http/1.1"] 12 | versions = ["TLSv1.2", "TLSv1.3"] 13 | 14 | # https://community.fly.io/t/new-feature-basic-http-response-header-modification/3594 15 | [services.ports.http_options] 16 | compress = true 17 | 18 | [services.ports.http_options.response.headers] 19 | fly-request-id = false 20 | fly-wasnt-here = "yes, it was" 21 | multi-valued = ["value1", "value2"] 22 | 23 | [[services.ports]] 24 | port = 82 25 | handlers = ["proxy_proto"] 26 | 27 | [services.ports.proxy_proto_options] 28 | version = "v2" 29 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/setters-deploy.toml: -------------------------------------------------------------------------------- 1 | # Use this file to test setters.go 2 | app = "setters" 3 | 4 | [deploy] 5 | strategy = "immediate" 6 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/setters-experimental.toml: -------------------------------------------------------------------------------- 1 | # Use this file to test setters.go 2 | app = "setters" 3 | 4 | [experimental] 5 | exec = "/exec" 6 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/setters-httpservice.toml: -------------------------------------------------------------------------------- 1 | app = "httpservice-setters" 2 | 3 | [http_service] 4 | internal_port = 9999 5 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/setters-processes.toml: -------------------------------------------------------------------------------- 1 | # Use this file to test setters.go 2 | app = "setters" 3 | 4 | [processes] 5 | foo = "bar" 6 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/setters-service.toml: -------------------------------------------------------------------------------- 1 | # Use this file to test setters.go 2 | 3 | app = "setters" 4 | 5 | [[services]] 6 | protocol = "tcp" 7 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-compute-nodefault.toml: -------------------------------------------------------------------------------- 1 | app = "no-default-compute" 2 | 3 | [processes] 4 | app = "" 5 | woo = "" 6 | bar = "" 7 | 8 | [[vm]] 9 | size = "performance-4x" 10 | processes = ["bar"] 11 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-compute.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [processes] 4 | app = "" 5 | worker = "/worker" 6 | whisper = "/whisperme" 7 | isolated = "/must-run-alone" 8 | 9 | # A section that applies to a specific group 10 | [[vm]] 11 | memory = "64gb" 12 | gpu_kind = "a100-pcie-40gb" 13 | processes = ["whisper"] 14 | 15 | # [[compute]] is an alias for [[vm]] 16 | [[compute]] 17 | host_dedication_id = "lookma-iamsolo" 18 | processes = ["isolated"] 19 | 20 | # A section without processes set must apply to all process groups 21 | [[vm]] 22 | size = "shared-cpu-2x" 23 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-default-for-new-apps.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | primary_region = "ord" 3 | 4 | [http_service] 5 | internal_port = 8080 6 | force_https = true 7 | 8 | [checks] 9 | [checks.alive] 10 | type = "tcp" 11 | interval = "15s" 12 | timeout = "2s" 13 | grace_period = "5s" 14 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-experimental.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [experimental] 4 | cmd = ["/call", "me"] 5 | entrypoint = ["/IgoFirst"] 6 | exec = ["ignore", "others"] 7 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-hostdedicationid.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | host_dedication_id = "toplevel" 3 | 4 | [processes] 5 | front = "" 6 | back = "" 7 | other = "" 8 | 9 | [[vm]] 10 | size = "shared-cpu-4x" 11 | processes = ["other"] 12 | 13 | [[vm]] 14 | host_dedication_id = "specific" 15 | processes = ["back"] 16 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-machinechecks.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | primary_region = "mia" 3 | swap_size_mb = 512 4 | 5 | [processes] 6 | greeting = "echo 'Hello'" 7 | salutation = "echo 'Whatsssup'" 8 | farewell = "echo 'bye :('" 9 | 10 | [http_service] 11 | internal_port = 8080 12 | force_https = true 13 | processes = ["greeting", "salutation"] 14 | 15 | [[http_service.machine_checks]] 16 | image = "curlimages/curl" 17 | command = ["curl", "https://fly.io"] 18 | entrypoint = ["/bin/sh"] 19 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-mounts.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | 3 | [processes] 4 | app = "" 5 | back = "back" 6 | hola = "hi!" 7 | 8 | [[mounts]] 9 | source = "data" 10 | destination = "/data" 11 | 12 | [[mounts]] 13 | source = "trash" 14 | destination = "/trash" 15 | processes = ["back"] 16 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-processgroups.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | primary_region = "ord" 3 | 4 | [processes] 5 | app = "run-nginx" 6 | vpn = "run-tailscale" 7 | foo = "keep me alive" 8 | 9 | [http_service] 10 | internal_port = 8080 11 | processes = ["app"] 12 | 13 | [[services]] 14 | internal_port = 9999 15 | protocol = "udp" 16 | processes = ["vpn"] 17 | 18 | [[services]] 19 | internal_port = 1111 20 | protocol = "tcp" 21 | processes = ["vpn", "app", "foo"] 22 | 23 | [checks.listening] 24 | port = 8080 25 | type = "tcp" 26 | processes = ["app", "foo"] 27 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine-services.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | primary_region = "scl" 3 | 4 | [http_service] 5 | internal_port = 8080 6 | force_https = true 7 | auto_start_machines = true 8 | auto_stop_machines = "stop" 9 | 10 | [[services]] 11 | protocol = "tcp" 12 | internal_port = 1000 13 | auto_start_machines = true 14 | auto_stop_machines = "stop" 15 | 16 | [[services]] 17 | protocol = "tcp" 18 | internal_port = 1001 19 | auto_start_machines = false 20 | auto_stop_machines = "off" 21 | 22 | [[services]] 23 | protocol = "tcp" 24 | internal_port = 1002 25 | auto_start_machines = false 26 | 27 | [[services]] 28 | protocol = "tcp" 29 | internal_port = 1003 30 | auto_stop_machines = "stop" 31 | 32 | [[services]] 33 | protocol = "tcp" 34 | internal_port = 1004 35 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/tomachine.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | primary_region = "mia" 3 | kill_signal = "SIGTERM" 4 | kill_timeout = "10s" 5 | swap_size_mb = 512 6 | 7 | [deploy] 8 | release_command = "migrate-db" 9 | release_command_timeout = "3m" 10 | [deploy.release_command_vm] 11 | size = "performance-2x" 12 | memory = "4G" 13 | 14 | [[restart]] 15 | policy = "always" 16 | 17 | [env] 18 | FOO = "BAR" 19 | 20 | [metrics] 21 | port = 9999 22 | path = "/metrics" 23 | 24 | [http_service] 25 | internal_port = 8080 26 | force_https = true 27 | 28 | [[http_service.machine_checks]] 29 | command = ["curl", "https://fly.io"] 30 | image = "curlimages/curl" 31 | entrypoint = ["/bin/sh"] 32 | 33 | [checks.listening] 34 | port = 8080 35 | type = "tcp" 36 | 37 | [checks.status] 38 | port = 8080 39 | type = "http" 40 | interval = "10s" 41 | timeout = "1s" 42 | path = "/status" 43 | 44 | [mounts] 45 | source = "data" 46 | destination = "/data" 47 | 48 | [[statics]] 49 | guest_path = "/guest/path" 50 | url_prefix = "/url/prefix" 51 | tigris_bucket = "example-bucket" 52 | index_document = "index.html" 53 | 54 | [experimental] 55 | machine_config = '{"dns": {"nameservers": ["1.2.3.4"]}}' 56 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/validate-groups.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | primary_region = "ord" 3 | 4 | [processes] 5 | app = "run-nginx" 6 | vpn = "run-tailscale" 7 | foo = "keep me alive" 8 | 9 | [http_service] 10 | internal_port = 8080 11 | processes = ["app"] 12 | 13 | [[services]] 14 | internal_port = 9999 15 | protocol = "udp" 16 | processes = ["vpn"] 17 | 18 | [[services.ports]] 19 | port = 80 20 | 21 | [[services]] 22 | internal_port = 1111 23 | protocol = "tcp" 24 | processes = ["vpn", "app", "foo"] 25 | 26 | [[services.ports]] 27 | port = 81 28 | 29 | [checks.listening] 30 | port = 8080 31 | type = "tcp" 32 | processes = ["app", "foo"] 33 | 34 | [[mounts]] 35 | source = "foo" 36 | destination = "bar" 37 | processes = ["app"] 38 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/validate-mounts.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | primary_region = "ord" 3 | 4 | [processes] 5 | app = "" 6 | vpn = "" 7 | foo = "" 8 | 9 | [[mounts]] 10 | source = "data" 11 | destination = "/data" 12 | initial_size = "15Mb" 13 | processes = ["vpn"] 14 | 15 | [[mounts]] 16 | source = "data" 17 | destination = "/data" 18 | initial_size = "unparseable" 19 | processes = ["foo"] 20 | 21 | [[mounts]] 22 | source = "foo" 23 | destination = "bar" 24 | processes = ["app"] 25 | 26 | [[mounts]] 27 | source = "data" 28 | destination = "/data" 29 | processes = ["app"] 30 | -------------------------------------------------------------------------------- /internal/appconfig/testdata/validate-services.toml: -------------------------------------------------------------------------------- 1 | app = "foo" 2 | primary_region = "ord" 3 | 4 | [processes] 5 | app = "" 6 | foo = "" 7 | success = "" 8 | 9 | # Check: 10 | # * missing [[services.ports]] 11 | # * no processes specified 12 | [[services]] 13 | internal_port = 8080 14 | 15 | [[services]] 16 | internal_port = 8080 17 | processes = ["success"] 18 | 19 | [[services.ports]] 20 | port = 80 21 | -------------------------------------------------------------------------------- /internal/build/imgsrc/depot_test.go: -------------------------------------------------------------------------------- 1 | package imgsrc 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/superfly/fly-go" 9 | "github.com/superfly/flyctl/internal/flyutil" 10 | "github.com/superfly/flyctl/iostreams" 11 | ) 12 | 13 | func TestInitBuilder(t *testing.T) { 14 | ctx := context.Background() 15 | ctx = flyutil.NewContextWithClient(ctx, flyutil.NewClientFromOptions(ctx, fly.ClientOptions{BaseURL: "invalid://localhost"})) 16 | ios, _, _, _ := iostreams.Test() 17 | build := newBuild("build1", false) 18 | 19 | // The invocation below doesn't test things much, but it may be better than nothing. 20 | _, _, err := initBuilder(ctx, build, "app1", ios, DepotBuilderScopeOrganization) 21 | require.ErrorContains(t, err, `unsupported protocol scheme "invalid"`) 22 | } 23 | -------------------------------------------------------------------------------- /internal/build/imgsrc/errors.go: -------------------------------------------------------------------------------- 1 | package imgsrc 2 | 3 | import "fmt" 4 | 5 | type RegistryUnauthorizedError struct { 6 | Tag string 7 | } 8 | 9 | func (err *RegistryUnauthorizedError) Error() string { 10 | return fmt.Sprintf("you are not authorized to push \"%s\"", err.Tag) 11 | } 12 | -------------------------------------------------------------------------------- /internal/build/imgsrc/testdata/dockerfile_app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | RUN sed -i 's/ listen 80;/ listen 8080;/g' /etc/nginx/conf.d/default.conf 4 | 5 | ARG SOURCE_DIR=. 6 | ARG APP_DIR=/usr/share/nginx/html 7 | ARG ENTRY_FILE=index.html 8 | 9 | COPY ${SOURCE_DIR} ${APP_DIR} 10 | 11 | ENV NGINX_PORT=8080 12 | -------------------------------------------------------------------------------- /internal/build/imgsrc/testdata/dockerfile_app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | hello! 4 | 5 | 6 | -------------------------------------------------------------------------------- /internal/buildinfo/env.go: -------------------------------------------------------------------------------- 1 | package buildinfo 2 | 3 | func Environment() string { 4 | return environment 5 | } 6 | 7 | func IsDev() bool { 8 | return environment == "development" 9 | } 10 | 11 | func IsRelease() bool { 12 | return !IsDev() 13 | } 14 | -------------------------------------------------------------------------------- /internal/buildinfo/env_dev.go: -------------------------------------------------------------------------------- 1 | //go:build !production 2 | 3 | package buildinfo 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/superfly/flyctl/internal/version" 9 | ) 10 | 11 | var ( 12 | buildDate = "" 13 | environment = "development" 14 | ) 15 | 16 | func loadBuildTime() (err error) { 17 | // Makefile sets proper values for buildDate but bare `go run .` doesn't 18 | if buildDate == "" { 19 | buildDate = time.Now().Format(time.RFC3339) 20 | } 21 | cachedBuildTime, err = time.Parse(time.RFC3339, buildDate) 22 | return 23 | } 24 | 25 | func loadVersion() error { 26 | // Makefile sets proper values for branchName but bare `go run .` doesn't 27 | if branchName == "" { 28 | branchName = "dev" 29 | } 30 | cachedVersion = version.New(cachedBuildTime, branchName, int(cachedBuildTime.Unix())) 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/buildinfo/env_production.go: -------------------------------------------------------------------------------- 1 | //go:build production 2 | 3 | package buildinfo 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/superfly/flyctl/internal/version" 9 | ) 10 | 11 | var ( 12 | buildDate = "" 13 | buildVersion = "" 14 | environment = "production" 15 | ) 16 | 17 | func loadBuildTime() (err error) { 18 | cachedBuildTime, err = time.Parse(time.RFC3339, buildDate) 19 | return 20 | } 21 | 22 | func loadVersion() (err error) { 23 | cachedVersion, err = version.Parse(buildVersion) 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /internal/cache/context.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "context" 4 | 5 | type contextKey struct{} 6 | 7 | // NewContext derives a context that carries c from ctx. 8 | func NewContext(ctx context.Context, c Cache) context.Context { 9 | return context.WithValue(ctx, contextKey{}, c) 10 | } 11 | 12 | // FromContext returns the Cache ctx carries. It panics in case ctx carries 13 | // no Cache. 14 | func FromContext(ctx context.Context) Cache { 15 | return ctx.Value(contextKey{}).(Cache) 16 | } 17 | -------------------------------------------------------------------------------- /internal/cache/context_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFromContextPanics(t *testing.T) { 11 | assert.Panics(t, func() { _ = FromContext(context.Background()) }) 12 | } 13 | 14 | func TestNewContext(t *testing.T) { 15 | exp := new(cache) 16 | 17 | ctx := NewContext(context.Background(), exp) 18 | assert.Same(t, exp, FromContext(ctx)) 19 | } 20 | -------------------------------------------------------------------------------- /internal/cli/cli_windows.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "golang.org/x/sys/windows" 4 | 5 | func init() { 6 | kernel32 := windows.NewLazySystemDLL("kernel32.dll") 7 | 8 | setConsoleCP := kernel32.NewProc("SetConsoleCP") 9 | // Set console input codepage to UTF-8 10 | // https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers#:~:text=Unicode%20(UTF%2D7)-,65001,-utf%2D8 11 | setConsoleCP.Call(uintptr(65001)) 12 | 13 | setConsoleOutputCP := kernel32.NewProc("SetConsoleOutputCP") 14 | // Set console ouput codepage to UTF-8 15 | setConsoleOutputCP.Call(uintptr(65001)) 16 | } 17 | -------------------------------------------------------------------------------- /internal/cli_test/cli_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/superfly/flyctl/iostreams" 12 | 13 | "github.com/superfly/flyctl/internal/cli" 14 | ) 15 | 16 | func TestVersion(t *testing.T) { 17 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 18 | defer cancel() 19 | 20 | stdout, stderr, code := capture(ctx, t, "version") 21 | assert.Equal(t, 0, code) 22 | 23 | assert.Equal(t, 0, code) 24 | assert.NotEmpty(t, stdout) 25 | assert.Empty(t, stderr) 26 | } 27 | 28 | func capture(ctx context.Context, t *testing.T, args ...string) (stdout, stderr string, code int) { 29 | t.Helper() 30 | 31 | var o1, o2 strings.Builder 32 | 33 | io := iostreams.IOStreams{ 34 | In: nil, 35 | Out: &o1, 36 | ErrOut: &o2, 37 | } 38 | 39 | code = cli.Run(ctx, &io, args...) 40 | stdout = o1.String() 41 | stderr = o2.String() 42 | 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /internal/cmdfmt/logging.go: -------------------------------------------------------------------------------- 1 | package cmdfmt 2 | 3 | const lineFeed = '\n' 4 | 5 | // AppendMissingLineFeed adds \n to a line unless already present 6 | func AppendMissingLineFeed(msg string) string { 7 | buff := []byte(msg) 8 | if len(buff) == 0 || buff[len(buff)-1] != lineFeed { 9 | buff = append(buff, lineFeed) 10 | } 11 | return string(buff) 12 | } 13 | -------------------------------------------------------------------------------- /internal/cmdfmt/messages.go: -------------------------------------------------------------------------------- 1 | package cmdfmt 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/logrusorgru/aurora" 8 | ) 9 | 10 | // extract message printing from ctx until we find a better way to do this 11 | // TODO: deprecate this package in favor of render.TextBlock 12 | func PrintBegin(w io.Writer, args ...interface{}) { 13 | fmt.Fprintln(w, aurora.Green("==> "+fmt.Sprint(args...))) 14 | } 15 | 16 | func PrintDone(w io.Writer, args ...interface{}) { 17 | fmt.Fprintln(w, aurora.Gray(20, "--> "+fmt.Sprint(args...))) 18 | } 19 | -------------------------------------------------------------------------------- /internal/cmdutil/flags.go: -------------------------------------------------------------------------------- 1 | package cmdutil 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ParseKVStringsToMap converts a slice of NAME=VALUE strings into a map[string]string 9 | func ParseKVStringsToMap(args []string) (map[string]string, error) { 10 | out := make(map[string]string, len(args)) 11 | 12 | for _, arg := range args { 13 | parts := strings.SplitN(arg, "=", 2) 14 | if len(parts) != 2 { 15 | return nil, fmt.Errorf("'%s': must be in the format NAME=VALUE", arg) 16 | } 17 | out[parts[0]] = parts[1] 18 | } 19 | 20 | return out, nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/cmdutil/strings.go: -------------------------------------------------------------------------------- 1 | package cmdutil 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" 8 | 9 | var re = regexp.MustCompile(ansi) 10 | 11 | func StripANSI(str string) string { 12 | return re.ReplaceAllString(str, "") 13 | } 14 | -------------------------------------------------------------------------------- /internal/cmdutil/terminal.go: -------------------------------------------------------------------------------- 1 | package cmdutil 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mattn/go-isatty" 7 | ) 8 | 9 | func IsTerminal(f *os.File) bool { 10 | return isatty.IsTerminal(f.Fd()) || IsCygwinTerminal(f) 11 | } 12 | 13 | func IsCygwinTerminal(f *os.File) bool { 14 | return isatty.IsCygwinTerminal(f.Fd()) 15 | } 16 | -------------------------------------------------------------------------------- /internal/command/agent/connect.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/superfly/flyctl/agent" 11 | "github.com/superfly/flyctl/iostreams" 12 | 13 | "github.com/superfly/flyctl/internal/command" 14 | "github.com/superfly/flyctl/internal/flag" 15 | ) 16 | 17 | func newConnect() (cmd *cobra.Command) { 18 | const ( 19 | short = "Connect" 20 | long = short + "\n" 21 | usage = "connect " 22 | ) 23 | 24 | cmd = command.New(usage, short, long, runConnect, 25 | command.RequireSession, 26 | ) 27 | 28 | cmd.Args = cobra.ExactArgs(2) 29 | 30 | return 31 | } 32 | 33 | func runConnect(ctx context.Context) (err error) { 34 | var client *agent.Client 35 | if client, err = establish(ctx); err != nil { 36 | return 37 | } 38 | 39 | var dialer agent.Dialer 40 | if dialer, err = client.Dialer(ctx, flag.FirstArg(ctx), ""); err != nil { 41 | return 42 | } 43 | 44 | var conn net.Conn 45 | if conn, err = dialer.DialContext(ctx, "tcp", flag.Args(ctx)[1]); err != nil { 46 | return 47 | } 48 | defer conn.Close() 49 | 50 | out := iostreams.FromContext(ctx).Out 51 | _, err = io.Copy(out, conn) 52 | 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /internal/command/agent/establish.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/agent" 9 | "github.com/superfly/flyctl/iostreams" 10 | 11 | "github.com/superfly/flyctl/internal/command" 12 | "github.com/superfly/flyctl/internal/flag" 13 | "github.com/superfly/flyctl/internal/render" 14 | ) 15 | 16 | func newEstablish() (cmd *cobra.Command) { 17 | const ( 18 | short = "Establish" 19 | long = short + "\n" 20 | usage = "establish " 21 | ) 22 | 23 | cmd = command.New(usage, short, long, runEstablish) 24 | 25 | cmd.Args = cobra.ExactArgs(1) 26 | 27 | return 28 | } 29 | 30 | func runEstablish(ctx context.Context) (err error) { 31 | var client *agent.Client 32 | if client, err = establish(ctx); err != nil { 33 | return 34 | } 35 | 36 | var res *agent.EstablishResponse 37 | if res, err = client.Establish(ctx, flag.FirstArg(ctx), ""); err == nil { 38 | out := iostreams.FromContext(ctx).Out 39 | err = render.JSON(out, res) 40 | } 41 | 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /internal/command/agent/probe.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/superfly/flyctl/agent" 10 | "github.com/superfly/flyctl/iostreams" 11 | 12 | "github.com/superfly/flyctl/internal/command" 13 | "github.com/superfly/flyctl/internal/flag" 14 | ) 15 | 16 | func newProbe() (cmd *cobra.Command) { 17 | const ( 18 | short = "Probe tunnel for org" 19 | long = short + "\n" 20 | usage = "probe " 21 | ) 22 | 23 | cmd = command.New(usage, short, long, runProbe, 24 | command.RequireSession, 25 | ) 26 | 27 | cmd.Args = cobra.ExactArgs(1) 28 | 29 | return 30 | } 31 | 32 | func runProbe(ctx context.Context) (err error) { 33 | var client *agent.Client 34 | if client, err = establish(ctx); err != nil { 35 | return 36 | } 37 | 38 | if err = client.Probe(ctx, flag.FirstArg(ctx), ""); err != nil { 39 | return 40 | } 41 | 42 | out := iostreams.FromContext(ctx).Out 43 | _, err = fmt.Fprintln(out, "tunnel is up") 44 | 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /internal/command/agent/restart.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/internal/command" 9 | ) 10 | 11 | func newRestart() (cmd *cobra.Command) { 12 | const ( 13 | short = "Restart the Fly agent" 14 | long = short + "\n" 15 | ) 16 | 17 | cmd = command.New("restart", short, long, runRestart, 18 | command.RequireSession, 19 | ) 20 | 21 | cmd.Args = cobra.NoArgs 22 | 23 | return 24 | } 25 | 26 | func runRestart(ctx context.Context) error { 27 | if client, err := dial(ctx); err == nil { 28 | _ = client.Kill(ctx) 29 | } 30 | 31 | _, err := establish(ctx) 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /internal/command/agent/start.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/internal/command" 9 | ) 10 | 11 | func newStart() (cmd *cobra.Command) { 12 | const ( 13 | short = "Start the Fly agent" 14 | long = short + "\n" 15 | ) 16 | 17 | cmd = command.New("start", short, long, runStart, 18 | command.RequireSession, 19 | ) 20 | 21 | cmd.Args = cobra.NoArgs 22 | 23 | return 24 | } 25 | 26 | func runStart(ctx context.Context) error { 27 | if _, err := dial(ctx); err == nil { 28 | return errDupInstance 29 | } 30 | 31 | _, err := establish(ctx) 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /internal/command/agent/stop.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/superfly/flyctl/agent" 10 | 11 | "github.com/superfly/flyctl/internal/command" 12 | ) 13 | 14 | func newStop() (cmd *cobra.Command) { 15 | const ( 16 | short = "Stop the Fly agent" 17 | long = short + "\n" 18 | ) 19 | 20 | cmd = command.New("stop", short, long, runStop) 21 | 22 | cmd.Args = cobra.NoArgs 23 | 24 | return 25 | } 26 | 27 | func runStop(ctx context.Context) (err error) { 28 | var client *agent.Client 29 | if client, err = dial(ctx); err != nil { 30 | return 31 | } 32 | 33 | if err = client.Kill(ctx); err != nil { 34 | err = fmt.Errorf("failed stopping agent: %w", err) 35 | } 36 | 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /internal/command/apps/errors.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/internal/command" 10 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 11 | "github.com/superfly/flyctl/internal/flag" 12 | ) 13 | 14 | func newErrors() (cmd *cobra.Command) { 15 | const ( 16 | long = `View application errors on Sentry.io` 17 | short = long 18 | usage = "errors" 19 | ) 20 | 21 | cmd = command.New(usage, short, long, RunDashboard, command.RequireSession, command.RequireAppName) 22 | cmd.Args = cobra.NoArgs 23 | flag.Add(cmd, 24 | flag.App(), 25 | flag.AppConfig(), 26 | ) 27 | return cmd 28 | } 29 | 30 | func RunDashboard(ctx context.Context) error { 31 | extension, _, err := extensions_core.Discover(ctx, gql.AddOnTypeSentry) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeSentry) 37 | } 38 | -------------------------------------------------------------------------------- /internal/command/apps/resume.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func newResume() *cobra.Command { 10 | resume := command.New("resume ", "", "", nil) 11 | resume.Hidden = true 12 | resume.Deprecated = "use `fly scale count` instead" 13 | return resume 14 | } 15 | -------------------------------------------------------------------------------- /internal/command/apps/suspend.go: -------------------------------------------------------------------------------- 1 | package apps 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func newSuspend() *cobra.Command { 10 | suspend := command.New("suspend ", "", "", nil) 11 | suspend.Hidden = true 12 | suspend.Deprecated = "use `fly scale count` instead" 13 | return suspend 14 | } 15 | -------------------------------------------------------------------------------- /internal/command/auth/auth.go: -------------------------------------------------------------------------------- 1 | // Package auth implements the auth command chain. 2 | package auth 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/superfly/flyctl/internal/command" 8 | ) 9 | 10 | // New initializes and returns a new apps Command. 11 | func New() *cobra.Command { 12 | const ( 13 | long = `Authenticate with Fly (and logout if you need to). 14 | If you do not have an account, start with the AUTH SIGNUP command. 15 | If you do have an account, begin with the AUTH LOGIN subcommand. 16 | ` 17 | short = "Manage authentication" 18 | ) 19 | 20 | auth := command.New("auth", short, long, nil) 21 | 22 | auth.AddCommand( 23 | newWhoAmI(), 24 | newToken(), 25 | newLogin(), 26 | newDocker(), 27 | newLogout(), 28 | newSignup(), 29 | ) 30 | 31 | return auth 32 | } 33 | -------------------------------------------------------------------------------- /internal/command/auth/signup.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/superfly/flyctl/internal/command/auth/webauth" 8 | 9 | "github.com/superfly/flyctl/internal/command" 10 | ) 11 | 12 | func newSignup() *cobra.Command { 13 | const ( 14 | long = `Creates a new fly account. The command opens the browser 15 | and sends the user to a form to provide appropriate credentials. 16 | ` 17 | short = "Create a new fly account" 18 | ) 19 | 20 | return command.New("signup", short, long, runSignup) 21 | } 22 | 23 | func runSignup(ctx context.Context) error { 24 | token, err := webauth.RunWebLogin(ctx, true) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if err := webauth.SaveToken(ctx, token); err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/command/checks/checks.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | "github.com/superfly/flyctl/internal/flag" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | commonFlags := flag.Set{flag.App(), flag.AppConfig()} 11 | 12 | cmd := command.New("checks", "Manage health checks", "", nil) 13 | flag.Add(cmd, commonFlags) 14 | 15 | // fly checks list 16 | listCmd := command.New("list", "List health checks", "", runAppCheckList, command.RequireSession, command.RequireAppName) 17 | listCmd.Aliases = []string{"ls"} 18 | flag.Add(listCmd, commonFlags, 19 | flag.String{Name: "check-name", Description: "Filter checks by name"}, 20 | ) 21 | flag.Add(listCmd, flag.JSONOutput()) 22 | cmd.AddCommand(listCmd) 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /internal/command/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | // New initializes and returns a new platform Command. 10 | func New() (cmd *cobra.Command) { 11 | const ( 12 | short = "Manage an app's configuration" 13 | long = `The CONFIG commands allow you to work with an application's configuration.` 14 | ) 15 | cmd = command.New("config", short, long, nil) 16 | 17 | cmd.AddCommand( 18 | newShow(), 19 | newSave(), 20 | newValidate(), 21 | newEnv(), 22 | ) 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /internal/command/consul/consul.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() *cobra.Command { 9 | const ( 10 | short = "Enable and manage Consul clusters" 11 | long = "Enable and manage Consul clusters" 12 | ) 13 | cmd := command.New("consul", short, long, nil) 14 | cmd.AddCommand( 15 | newAttach(), 16 | newDetach(), 17 | ) 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /internal/command/context.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/superfly/flyctl/internal/command_context" 8 | ) 9 | 10 | // NewContext derives a context that carries cmd from ctx. 11 | func NewContext(ctx context.Context, cmd *cobra.Command) context.Context { 12 | // uses command_context.go so there isn't a dependency cycle with flaps 13 | return command_context.NewContext(ctx, cmd) 14 | } 15 | 16 | // FromContext returns the Command ctx carries. It panics in case ctx carries 17 | // no Command. 18 | func FromContext(ctx context.Context) *cobra.Command { 19 | return command_context.FromContext(ctx) 20 | } 21 | -------------------------------------------------------------------------------- /internal/command/context_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestFromContextPanics(t *testing.T) { 12 | assert.Panics(t, func() { _ = FromContext(context.Background()) }) 13 | } 14 | 15 | func TestNewContext(t *testing.T) { 16 | exp := new(cobra.Command) 17 | 18 | ctx := NewContext(context.Background(), exp) 19 | assert.Equal(t, exp, FromContext(ctx)) 20 | } 21 | -------------------------------------------------------------------------------- /internal/command/create/create.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/flag" 6 | ) 7 | 8 | // TODO: deprecate & remove 9 | func New() (cmd *cobra.Command) { 10 | cmd = &cobra.Command{ 11 | Use: "create", 12 | Hidden: true, 13 | Deprecated: "use `fly apps create` instead", 14 | } 15 | 16 | flag.Add(cmd, 17 | flag.String{ 18 | Name: "name", 19 | Description: "The app name to use", 20 | }, 21 | flag.Bool{ 22 | Name: "generate-name", 23 | Description: "Generate an app name", 24 | }, 25 | flag.String{ 26 | Name: "network", 27 | Description: "Specify custom network id", 28 | }, 29 | flag.Bool{ 30 | Name: "machines", 31 | Description: "Use the machines platform", 32 | }, 33 | flag.Org(), 34 | ) 35 | 36 | return cmd 37 | } 38 | -------------------------------------------------------------------------------- /internal/command/deploy/common_secrets.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import "strings" 4 | 5 | // some common secrets 6 | var commonSecretSubstrings = []string{"KEY", "PRIVATE", "DATABASE_URL", "PASSWORD", "SECRET"} 7 | 8 | func containsCommonSecretSubstring(s string) bool { 9 | // Allowlist for strings which contain a substring but are not secrets. 10 | switch s { 11 | case "AWS_ACCESS_KEY_ID", "TIGRIS_ACCESS_KEY_ID": 12 | return false 13 | } 14 | 15 | for _, substr := range commonSecretSubstrings { 16 | if strings.Contains(s, substr) { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | -------------------------------------------------------------------------------- /internal/command/deploy/common_secrets_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestContainsCommonSecretSubstring(t *testing.T) { 10 | assert.True(t, containsCommonSecretSubstring("THIRDPARTY_SERVICE_SECRET")) 11 | } 12 | -------------------------------------------------------------------------------- /internal/command/deploy/deploy_build_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "github.com/superfly/flyctl/internal/appconfig" 12 | "github.com/superfly/flyctl/internal/state" 13 | ) 14 | 15 | func TestMultipleDockerfile(t *testing.T) { 16 | dir := t.TempDir() 17 | 18 | dockerfile, err := os.Create(filepath.Join(dir, "Dockerfile")) 19 | require.NoError(t, err) 20 | defer dockerfile.Close() // skipcq: GO-S2307 21 | 22 | flyToml, err := os.Create(filepath.Join(dir, "fly.production.toml")) 23 | require.NoError(t, err) 24 | defer flyToml.Close() // skipcq: GO-S2307 25 | 26 | cfg, err := appconfig.LoadConfig(flyToml.Name()) 27 | require.NoError(t, err) 28 | cfg.Build = &appconfig.Build{ 29 | Dockerfile: "Dockerfile.from-fly-toml", 30 | } 31 | 32 | ctx := state.WithWorkingDirectory(context.Background(), dir) 33 | err = multipleDockerfile(ctx, &appconfig.Config{}) 34 | 35 | assert.NoError(t, err) 36 | 37 | err = multipleDockerfile(ctx, cfg) 38 | assert.ErrorContains(t, err, "fly.production.toml") 39 | } 40 | -------------------------------------------------------------------------------- /internal/command/deploy/flyctl.cache.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/internal/command/deploy/flyctl.cache.lock -------------------------------------------------------------------------------- /internal/command/deploy/flyctl.config.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/internal/command/deploy/flyctl.config.lock -------------------------------------------------------------------------------- /internal/command/deploy/manifest_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestManifest(t *testing.T) { 11 | m := NewManifest("app1", nil, MachineDeploymentArgs{}) 12 | var buf bytes.Buffer 13 | m.Encode(&buf) 14 | assert.Contains(t, buf.String(), "{") 15 | } 16 | -------------------------------------------------------------------------------- /internal/command/deploy/testdata/basic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | CMD run.sh 3 | -------------------------------------------------------------------------------- /internal/command/deploy/testdata/basic/fly.toml: -------------------------------------------------------------------------------- 1 | app = "test-basic" 2 | primary_region = "ord" 3 | -------------------------------------------------------------------------------- /internal/command/deploy/web_client.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | fly "github.com/superfly/fly-go" 8 | "github.com/superfly/flyctl/logs" 9 | ) 10 | 11 | // webClient is a subset of web API that is needed for the deploy package. 12 | type webClient interface { 13 | AddCertificate(ctx context.Context, appName, hostname string) (*fly.AppCertificate, *fly.HostnameCheck, error) 14 | AllocateIPAddress(ctx context.Context, appName string, addrType string, region string, org *fly.Organization, network string) (*fly.IPAddress, error) 15 | GetIPAddresses(ctx context.Context, appName string) ([]fly.IPAddress, error) 16 | AllocateSharedIPAddress(ctx context.Context, appName string) (net.IP, error) 17 | 18 | LatestImage(ctx context.Context, appName string) (string, error) 19 | 20 | CreateRelease(ctx context.Context, input fly.CreateReleaseInput) (*fly.CreateReleaseResponse, error) 21 | UpdateRelease(ctx context.Context, input fly.UpdateReleaseInput) (*fly.UpdateReleaseResponse, error) 22 | 23 | GetApp(ctx context.Context, appName string) (*fly.App, error) 24 | GetOrganizationBySlug(ctx context.Context, slug string) (*fly.Organization, error) 25 | 26 | logs.WebClient 27 | blueGreenWebClient 28 | } 29 | -------------------------------------------------------------------------------- /internal/command/destroy/destroy.go: -------------------------------------------------------------------------------- 1 | package destroy 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | "github.com/superfly/flyctl/internal/command/apps" 8 | "github.com/superfly/flyctl/internal/flag" 9 | ) 10 | 11 | // TODO: deprecate & remove 12 | func New() *cobra.Command { 13 | const ( 14 | long = `The DESTROY command will remove an application 15 | from the Fly platform. 16 | ` 17 | short = "Permanently destroys an app" 18 | usage = "destroy " 19 | ) 20 | 21 | destroy := command.New(usage, short, long, apps.RunDestroy, 22 | command.RequireSession) 23 | destroy.Hidden = true 24 | destroy.Deprecated = "use `fly apps destroy` instead" 25 | 26 | destroy.Args = cobra.ExactArgs(1) 27 | 28 | flag.Add(destroy, 29 | flag.Yes(), 30 | ) 31 | 32 | return destroy 33 | } 34 | -------------------------------------------------------------------------------- /internal/command/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs implements the docs command chain. 2 | package docs 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/superfly/flyctl/internal/command" 10 | "github.com/superfly/flyctl/iostreams" 11 | 12 | "github.com/skratchdot/open-golang/open" 13 | ) 14 | 15 | func New() (cmd *cobra.Command) { 16 | const ( 17 | long = `View Fly documentation on the Fly.io website. This command will open a 18 | browser to view the content. 19 | ` 20 | short = "View Fly documentation" 21 | ) 22 | 23 | cmd = command.New("docs", short, long, run) 24 | 25 | cmd.Args = cobra.NoArgs 26 | 27 | return 28 | } 29 | 30 | func run(ctx context.Context) error { 31 | const url = "https://fly.io/docs/" 32 | 33 | out := iostreams.FromContext(ctx).ErrOut 34 | fmt.Fprintf(out, "opening %s ...\n", url) 35 | 36 | if err := open.Run(url); err != nil { 37 | return fmt.Errorf("failed opening %s: %w", url, err) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/command/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | sentry_ext "github.com/superfly/flyctl/internal/command/extensions/sentry" 6 | ) 7 | 8 | func New() (cmd *cobra.Command) { 9 | return sentry_ext.Dashboard() 10 | } 11 | -------------------------------------------------------------------------------- /internal/command/extensions/arcjet/arcjet.go: -------------------------------------------------------------------------------- 1 | package arcjet 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func New() (cmd *cobra.Command) { 10 | 11 | const ( 12 | short = "Provision and manage Arcjet" 13 | long = short + "\n" 14 | ) 15 | 16 | cmd = command.New("arcjet", short, long, nil) 17 | cmd.AddCommand(create()) 18 | cmd.AddCommand(dashboard()) 19 | cmd.AddCommand(list()) 20 | cmd.AddCommand(destroy()) 21 | cmd.AddCommand(status()) 22 | 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /internal/command/extensions/arcjet/dashboard.go: -------------------------------------------------------------------------------- 1 | package arcjet 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/internal/command" 10 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 11 | "github.com/superfly/flyctl/internal/flag" 12 | ) 13 | 14 | func dashboard() (cmd *cobra.Command) { 15 | const ( 16 | long = `Visit the Arcjet dashboard` 17 | 18 | short = long 19 | usage = "dashboard [site_name]" 20 | ) 21 | 22 | cmd = command.New(usage, short, long, runDashboard, command.RequireSession, command.LoadAppNameIfPresent) 23 | 24 | flag.Add(cmd, 25 | flag.App(), 26 | flag.AppConfig(), 27 | flag.Org(), 28 | extensions_core.SharedFlags, 29 | ) 30 | cmd.Args = cobra.MaximumNArgs(1) 31 | return cmd 32 | } 33 | 34 | func runDashboard(ctx context.Context) (err error) { 35 | 36 | org := flag.GetOrg(ctx) 37 | 38 | if org != "" { 39 | return extensions_core.OpenOrgDashboard(ctx, org, "arcjet") 40 | } 41 | 42 | extension, _, err := extensions_core.Discover(ctx, gql.AddOnTypeArcjet) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | 48 | err = extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeArcjet) 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /internal/command/extensions/arcjet/status.go: -------------------------------------------------------------------------------- 1 | package arcjet 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/internal/command" 10 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 11 | "github.com/superfly/flyctl/internal/flag" 12 | ) 13 | 14 | func status() *cobra.Command { 15 | const ( 16 | short = "Show details about an Arcjet site setup" 17 | long = short + "\n" 18 | 19 | usage = "status [name]" 20 | ) 21 | 22 | cmd := command.New(usage, short, long, runStatus, 23 | command.RequireSession, command.LoadAppNameIfPresent, 24 | ) 25 | 26 | cmd.Args = cobra.MaximumNArgs(1) 27 | 28 | flag.Add(cmd, 29 | flag.App(), 30 | flag.AppConfig(), 31 | extensions_core.SharedFlags, 32 | ) 33 | 34 | return cmd 35 | } 36 | 37 | func runStatus(ctx context.Context) (err error) { 38 | return extensions_core.Status(ctx, gql.AddOnTypeArcjet) 39 | } 40 | -------------------------------------------------------------------------------- /internal/command/extensions/enveloop/dashboard.go: -------------------------------------------------------------------------------- 1 | package enveloop 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/superfly/flyctl/gql" 8 | "github.com/superfly/flyctl/internal/command" 9 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 10 | "github.com/superfly/flyctl/internal/flag" 11 | ) 12 | 13 | func dashboard() (cmd *cobra.Command) { 14 | const ( 15 | long = `Open the Enveloop dashboard via your web browser` 16 | 17 | short = long 18 | usage = "dashboard" 19 | ) 20 | 21 | cmd = command.New(usage, short, long, runDashboard, command.RequireSession, command.LoadAppNameIfPresent) 22 | 23 | flag.Add(cmd, 24 | flag.App(), 25 | flag.AppConfig(), 26 | flag.Org(), 27 | extensions_core.SharedFlags, 28 | ) 29 | cmd.Args = cobra.NoArgs 30 | return cmd 31 | } 32 | 33 | func runDashboard(ctx context.Context) (err error) { 34 | if org := flag.GetOrg(ctx); org != "" { 35 | return extensions_core.OpenOrgDashboard(ctx, org, "enveloop") 36 | } 37 | 38 | extension, _, err := extensions_core.Discover(ctx, gql.AddOnTypeEnveloop) 39 | if err != nil { 40 | return err 41 | } 42 | return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeEnveloop) 43 | } 44 | -------------------------------------------------------------------------------- /internal/command/extensions/enveloop/enveloop.go: -------------------------------------------------------------------------------- 1 | package enveloop 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | "github.com/superfly/flyctl/internal/flag" 7 | ) 8 | 9 | func New() (cmd *cobra.Command) { 10 | const ( 11 | short = "Provision and manage Enveloop projects" 12 | long = short + "\n" 13 | ) 14 | 15 | cmd = command.New("enveloop", short, long, nil) 16 | cmd.AddCommand(create(), list(), dashboard(), destroy(), status()) 17 | 18 | return cmd 19 | } 20 | 21 | var SharedFlags = flag.Set{} 22 | -------------------------------------------------------------------------------- /internal/command/extensions/kubernetes/kubernetes.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() (cmd *cobra.Command) { 9 | 10 | const ( 11 | short = "Provision and manage Kubernetes clusters" 12 | long = short + "\n" 13 | ) 14 | 15 | cmd = command.New("kubernetes", short, long, nil) 16 | cmd.Aliases = []string{"k8s"} 17 | cmd.AddCommand(create(), destroy(), list(), kubectlToken(), saveKubeconfig()) 18 | cmd.Hidden = false 19 | return cmd 20 | } 21 | -------------------------------------------------------------------------------- /internal/command/extensions/kubernetes/list.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/iostreams" 10 | 11 | "github.com/superfly/flyctl/internal/command" 12 | "github.com/superfly/flyctl/internal/flyutil" 13 | "github.com/superfly/flyctl/internal/render" 14 | ) 15 | 16 | func list() (cmd *cobra.Command) { 17 | const ( 18 | long = `List your Kubernetes clusters` 19 | short = long 20 | usage = "list" 21 | ) 22 | 23 | cmd = command.New(usage, short, long, runList, command.RequireSession) 24 | cmd.Aliases = []string{"ls"} 25 | 26 | return cmd 27 | } 28 | 29 | func runList(ctx context.Context) (err error) { 30 | var ( 31 | out = iostreams.FromContext(ctx).Out 32 | client = flyutil.ClientFromContext(ctx).GenqClient() 33 | ) 34 | 35 | response, err := gql.ListAddOns(ctx, client, "kubernetes") 36 | 37 | var rows [][]string 38 | 39 | for _, addon := range response.AddOns.Nodes { 40 | rows = append(rows, []string{ 41 | addon.Name, 42 | addon.Organization.Slug, 43 | addon.PrimaryRegion, 44 | }) 45 | } 46 | 47 | _ = render.Table(out, "", rows, "Name", "Org", "Primary Region") 48 | 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /internal/command/extensions/sentry/create.go: -------------------------------------------------------------------------------- 1 | package sentry_ext 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/superfly/flyctl/gql" 8 | "github.com/superfly/flyctl/internal/appconfig" 9 | "github.com/superfly/flyctl/internal/command" 10 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 11 | "github.com/superfly/flyctl/internal/command/secrets" 12 | "github.com/superfly/flyctl/internal/flag" 13 | ) 14 | 15 | func create() (cmd *cobra.Command) { 16 | 17 | const ( 18 | short = "Provision a Sentry project for a Fly.io app" 19 | long = short + "\n" 20 | ) 21 | 22 | cmd = command.New("create", short, long, runSentryCreate, command.RequireSession, command.RequireAppName) 23 | flag.Add(cmd, 24 | flag.App(), 25 | flag.AppConfig(), 26 | extensions_core.SharedFlags, 27 | ) 28 | return cmd 29 | } 30 | 31 | func runSentryCreate(ctx context.Context) (err error) { 32 | appName := appconfig.NameFromContext(ctx) 33 | 34 | extension, err := extensions_core.ProvisionExtension(ctx, extensions_core.ExtensionParams{ 35 | AppName: appName, 36 | Provider: "sentry", 37 | }) 38 | if extension.SetsSecrets { 39 | err = secrets.DeploySecrets(ctx, gql.ToAppCompact(*extension.App), false, false) 40 | } 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /internal/command/extensions/sentry/dashboard.go: -------------------------------------------------------------------------------- 1 | package sentry_ext 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/internal/command" 10 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 11 | "github.com/superfly/flyctl/internal/flag" 12 | ) 13 | 14 | func Dashboard() (cmd *cobra.Command) { 15 | const ( 16 | long = `View application errors in the Sentry dashboard` 17 | 18 | short = long 19 | usage = "dashboard" 20 | ) 21 | 22 | cmd = command.New(usage, short, long, RunDashboard, command.RequireSession, command.RequireAppName) 23 | 24 | flag.Add(cmd, 25 | flag.App(), 26 | flag.AppConfig(), 27 | extensions_core.SharedFlags, 28 | ) 29 | cmd.Aliases = []string{"errors"} 30 | cmd.Args = cobra.NoArgs 31 | return cmd 32 | } 33 | 34 | func RunDashboard(ctx context.Context) (err error) { 35 | 36 | extension, _, err := extensions_core.Discover(ctx, gql.AddOnTypeSentry) 37 | 38 | if err != nil { 39 | return err 40 | } 41 | 42 | err = extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeSentry) 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /internal/command/extensions/sentry/sentry.go: -------------------------------------------------------------------------------- 1 | package sentry_ext 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() (cmd *cobra.Command) { 9 | 10 | const ( 11 | short = "Setup a Sentry project for this app" 12 | long = short + "\n" 13 | ) 14 | 15 | cmd = command.New("sentry", short, long, nil) 16 | cmd.AddCommand(create(), Dashboard(), destroy(), list()) 17 | 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /internal/command/extensions/supabase/dashboard.go: -------------------------------------------------------------------------------- 1 | package supabase 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/internal/command" 10 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 11 | "github.com/superfly/flyctl/internal/flag" 12 | ) 13 | 14 | func dashboard() (cmd *cobra.Command) { 15 | const ( 16 | long = `Visit the Supabase database dashboard` 17 | 18 | short = long 19 | usage = "dashboard [database_name]" 20 | ) 21 | 22 | cmd = command.New(usage, short, long, runDashboard, command.RequireSession, command.LoadAppNameIfPresent) 23 | 24 | flag.Add(cmd, 25 | flag.App(), 26 | flag.AppConfig(), 27 | flag.Org(), 28 | extensions_core.SharedFlags, 29 | ) 30 | cmd.Args = cobra.MaximumNArgs(1) 31 | return cmd 32 | } 33 | 34 | func runDashboard(ctx context.Context) error { 35 | org := flag.GetOrg(ctx) 36 | 37 | if org != "" { 38 | return extensions_core.OpenOrgDashboard(ctx, org, "supabase") 39 | } 40 | 41 | extension, _, err := extensions_core.Discover(ctx, gql.AddOnTypeSupabase) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeSupabase) 47 | } 48 | -------------------------------------------------------------------------------- /internal/command/extensions/supabase/status.go: -------------------------------------------------------------------------------- 1 | package supabase 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/superfly/flyctl/gql" 8 | "github.com/superfly/flyctl/internal/command" 9 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 10 | "github.com/superfly/flyctl/internal/flag" 11 | ) 12 | 13 | func status() *cobra.Command { 14 | const ( 15 | short = "Show details about a Supabase Postgres database" 16 | long = short + "\n" 17 | 18 | usage = "status [name]" 19 | ) 20 | 21 | cmd := command.New(usage, short, long, runStatus, 22 | command.RequireSession, command.LoadAppNameIfPresent, 23 | ) 24 | 25 | cmd.Args = cobra.MaximumNArgs(1) 26 | 27 | flag.Add(cmd, 28 | flag.App(), 29 | flag.AppConfig(), 30 | extensions_core.SharedFlags, 31 | ) 32 | 33 | return cmd 34 | } 35 | 36 | func runStatus(ctx context.Context) (err error) { 37 | return extensions_core.Status(ctx, gql.AddOnTypeSupabase) 38 | } 39 | -------------------------------------------------------------------------------- /internal/command/extensions/supabase/supabase.go: -------------------------------------------------------------------------------- 1 | package supabase 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() (cmd *cobra.Command) { 9 | 10 | const ( 11 | short = "Provision and manage Supabase Postgres databases" 12 | long = short + "\n" 13 | ) 14 | 15 | cmd = command.New("supabase", short, long, nil) 16 | cmd.AddCommand(destroy(), dashboard(), list(), status()) 17 | 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /internal/command/extensions/tigris/dashboard.go: -------------------------------------------------------------------------------- 1 | package tigris 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/internal/command" 10 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 11 | "github.com/superfly/flyctl/internal/flag" 12 | ) 13 | 14 | func dashboard() (cmd *cobra.Command) { 15 | const ( 16 | long = `Visit the Tigris dashboard` 17 | 18 | short = long 19 | usage = "dashboard [bucket_name]" 20 | ) 21 | 22 | cmd = command.New(usage, short, long, runDashboard, command.RequireSession, command.LoadAppNameIfPresent) 23 | 24 | flag.Add(cmd, 25 | flag.App(), 26 | flag.AppConfig(), 27 | flag.Org(), 28 | extensions_core.SharedFlags, 29 | ) 30 | cmd.Args = cobra.MaximumNArgs(1) 31 | return cmd 32 | } 33 | 34 | func runDashboard(ctx context.Context) (err error) { 35 | 36 | org := flag.GetOrg(ctx) 37 | 38 | if org != "" { 39 | return extensions_core.OpenOrgDashboard(ctx, org, "tigris") 40 | } 41 | 42 | extension, _, err := extensions_core.Discover(ctx, gql.AddOnTypeTigris) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | 48 | err = extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeTigris) 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /internal/command/extensions/vector/dashboard.go: -------------------------------------------------------------------------------- 1 | package vector 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/superfly/flyctl/gql" 8 | "github.com/superfly/flyctl/internal/command" 9 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 10 | "github.com/superfly/flyctl/internal/flag" 11 | ) 12 | 13 | func dashboard() (cmd *cobra.Command) { 14 | const ( 15 | long = `Visit the Upstash Vector dashboard on the Upstash web console` 16 | 17 | short = long 18 | usage = "dashboard" 19 | ) 20 | 21 | cmd = command.New(usage, short, long, runDashboard, command.RequireSession, command.LoadAppNameIfPresent) 22 | 23 | flag.Add(cmd, 24 | flag.App(), 25 | flag.AppConfig(), 26 | flag.Org(), 27 | extensions_core.SharedFlags, 28 | ) 29 | cmd.Args = cobra.NoArgs 30 | return cmd 31 | } 32 | 33 | func runDashboard(ctx context.Context) (err error) { 34 | if org := flag.GetOrg(ctx); org != "" { 35 | return extensions_core.OpenOrgDashboard(ctx, org, "upstash_vector") 36 | } 37 | 38 | extension, _, err := extensions_core.Discover(ctx, gql.AddOnTypeUpstashVector) 39 | if err != nil { 40 | return err 41 | } 42 | return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeUpstashVector) 43 | } 44 | -------------------------------------------------------------------------------- /internal/command/extensions/wafris/dashboard.go: -------------------------------------------------------------------------------- 1 | package wafris 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/internal/command" 10 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 11 | "github.com/superfly/flyctl/internal/flag" 12 | ) 13 | 14 | func dashboard() (cmd *cobra.Command) { 15 | const ( 16 | long = `Visit the Wafris dashboard` 17 | 18 | short = long 19 | usage = "dashboard [firewall_name]" 20 | ) 21 | 22 | cmd = command.New(usage, short, long, runDashboard, command.RequireSession, command.LoadAppNameIfPresent) 23 | 24 | flag.Add(cmd, 25 | flag.App(), 26 | flag.AppConfig(), 27 | flag.Org(), 28 | extensions_core.SharedFlags, 29 | ) 30 | cmd.Args = cobra.MaximumNArgs(1) 31 | return cmd 32 | } 33 | 34 | func runDashboard(ctx context.Context) (err error) { 35 | org := flag.GetOrg(ctx) 36 | 37 | if org != "" { 38 | return extensions_core.OpenOrgDashboard(ctx, org, "wafris") 39 | } 40 | 41 | extension, _, err := extensions_core.Discover(ctx, gql.AddOnTypeWafris) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return extensions_core.OpenDashboard(ctx, extension.Name, gql.AddOnTypeWafris) 47 | } 48 | -------------------------------------------------------------------------------- /internal/command/extensions/wafris/wafris.go: -------------------------------------------------------------------------------- 1 | package wafris 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func New() (cmd *cobra.Command) { 10 | 11 | const ( 12 | short = "Provision and manage Wafris WAFs (Web Application Firewalls)" 13 | long = short + "\n" 14 | ) 15 | 16 | cmd = command.New("wafris", short, long, nil) 17 | cmd.Aliases = []string{"waf"} 18 | cmd.AddCommand(create(), destroy(), dashboard()) 19 | 20 | return cmd 21 | } 22 | -------------------------------------------------------------------------------- /internal/command/history/history.go: -------------------------------------------------------------------------------- 1 | // Package history implements the history command chain. 2 | package history 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/superfly/flyctl/internal/command" 8 | "github.com/superfly/flyctl/internal/flag" 9 | ) 10 | 11 | func New() (cmd *cobra.Command) { 12 | const ( 13 | long = `List the history of changes in the application. Includes autoscaling 14 | events and their results. 15 | ` 16 | short = "List an app's change history" 17 | ) 18 | 19 | cmd = command.New("history", short, long, nil, 20 | command.RequireSession, 21 | command.RequireAppName, 22 | ) 23 | cmd.Deprecated = "Use `flyctl apps releases` instead" 24 | cmd.Hidden = true 25 | 26 | cmd.Args = cobra.NoArgs 27 | 28 | flag.Add(cmd, 29 | flag.App(), 30 | flag.AppConfig(), 31 | flag.JSONOutput(), 32 | ) 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /internal/command/image/image.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | const ( 11 | short = "Manage app image" 12 | long = short + "\n" 13 | 14 | usage = "image" 15 | ) 16 | 17 | cmd := command.New(usage, short, long, nil) 18 | 19 | cmd.Args = cobra.NoArgs 20 | 21 | cmd.Aliases = []string{"img"} 22 | 23 | cmd.AddCommand( 24 | newShow(), 25 | newUpdate(), 26 | ) 27 | 28 | return cmd 29 | } 30 | -------------------------------------------------------------------------------- /internal/command/incidents/hosts/hosts.go: -------------------------------------------------------------------------------- 1 | package hosts 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() (cmd *cobra.Command) { 9 | 10 | const ( 11 | short = "Show hosts' incidents" 12 | long = "Show hosts' incidents affecting applications" 13 | ) 14 | 15 | cmd = command.New("hosts", short, long, nil) 16 | cmd.AddCommand(list()) 17 | 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /internal/command/info/info.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | import ( 4 | "github.com/superfly/flyctl/internal/command" 5 | "github.com/superfly/flyctl/internal/flag" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func New() *cobra.Command { 11 | const ( 12 | long = `Shows information about the application.` 13 | short = `Shows information about the application` 14 | ) 15 | 16 | cmd := command.New("info", short, long, nil) 17 | cmd.Hidden = true 18 | cmd.Deprecated = "Replaced by 'status', 'ips list', and 'services list'" 19 | 20 | flag.Add(cmd, 21 | flag.App(), 22 | flag.AppConfig(), 23 | ) 24 | 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /internal/command/ips/ips.go: -------------------------------------------------------------------------------- 1 | package ips 2 | 3 | import ( 4 | "github.com/superfly/flyctl/internal/command" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | const ( 11 | long = `Commands for managing IP addresses associated with an application` 12 | short = `Manage IP addresses for apps` 13 | ) 14 | 15 | cmd := command.New("ips", short, long, nil) 16 | cmd.Aliases = []string{"ip"} 17 | cmd.AddCommand( 18 | newList(), 19 | newAllocatev4(), 20 | newAllocatev6(), 21 | newPrivate(), 22 | newRelease(), 23 | ) 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /internal/command/jobs/jobs.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/superfly/flyctl/internal/command" 9 | "github.com/superfly/flyctl/iostreams" 10 | ) 11 | 12 | func New() *cobra.Command { 13 | const ( 14 | short = "Show jobs at Fly.io" 15 | 16 | long = `Show jobs at Fly.io, including maybe ones you should apply to` 17 | ) 18 | 19 | cmd := command.New("jobs", short, long, run) 20 | cmd.AddCommand(NewOpen()) 21 | return cmd 22 | } 23 | func run(ctx context.Context) (err error) { 24 | out := iostreams.FromContext(ctx).Out 25 | _, err = fmt.Fprintln(out, `Want to work on super fun problems with (arguably) likeable people? Then you’ve come to the right place. 26 | 27 | The tl;dr is that we build on Rust, Go, Ruby, and Elixir, on Linux. If you're comfortable with any of those, we probably have interesting roles for you. 28 | 29 | We've got roles on our API backend, defining our developer experience; on our Elixir frontend; in security engineering; on infrastructure; and, of course, on the platform itself. 30 | 31 | Check out https://fly.io/jobs to see our open roles. Or run: fly jobs open`) 32 | 33 | if err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/command/jobs/open.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/skratchdot/open-golang/open" 8 | "github.com/spf13/cobra" 9 | "github.com/superfly/flyctl/internal/command" 10 | ) 11 | 12 | const jobsUrl = "https://fly.io/jobs/" 13 | 14 | func NewOpen() *cobra.Command { 15 | return command.New( 16 | "open", 17 | "Open fly.io/jobs", 18 | "Open browser to https://fly.io/jobs/", 19 | func(ctx context.Context) error { 20 | if err := open.Run(jobsUrl); err != nil { 21 | return fmt.Errorf("failed opening %s: %v", jobsUrl, err) 22 | } 23 | return nil 24 | }, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /internal/command/launch/launch_extensions.go: -------------------------------------------------------------------------------- 1 | package launch 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/superfly/flyctl/gql" 7 | extensions_core "github.com/superfly/flyctl/internal/command/extensions/core" 8 | "github.com/superfly/flyctl/internal/command/secrets" 9 | ) 10 | 11 | func (state *launchState) launchSentry(ctx context.Context, app_name string) error { 12 | if state.Plan.Sentry { 13 | extension, err := extensions_core.ProvisionExtension(ctx, extensions_core.ExtensionParams{ 14 | AppName: app_name, 15 | Provider: "sentry", 16 | }) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | if extension.SetsSecrets { 22 | if err = secrets.DeploySecrets(ctx, gql.ToAppCompact(*extension.App), false, false); err != nil { 23 | return err 24 | } 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/command/launch/plan/context.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import "context" 4 | 5 | type contextKey string 6 | 7 | const PlanStepKey contextKey = "planStep" 8 | 9 | func GetPlanStep(ctx context.Context) string { 10 | step := ctx.Value(PlanStepKey) 11 | if step == nil { 12 | return "" 13 | } else { 14 | return step.(string) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/command/launch/plan/github_actions.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | type GitHubActionsPlan struct { 4 | Deploy bool `json:"deploy"` 5 | Review bool `json:"review"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/command/launch/plan/object_storage.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | type ObjectStoragePlan struct { 4 | TigrisObjectStorage *TigrisObjectStoragePlan `json:"tigris_object_storage"` 5 | } 6 | 7 | func (p *ObjectStoragePlan) Provider() any { 8 | if p == nil { 9 | return nil 10 | } 11 | if p.TigrisObjectStorage != nil { 12 | return p.TigrisObjectStorage 13 | } 14 | return nil 15 | } 16 | 17 | func DefaultObjectStorage(plan *LaunchPlan) ObjectStoragePlan { 18 | return ObjectStoragePlan{ 19 | TigrisObjectStorage: &TigrisObjectStoragePlan{}, 20 | } 21 | } 22 | 23 | type TigrisObjectStoragePlan struct { 24 | Name string `json:"name"` 25 | Public bool `json:"public"` 26 | Accelerate bool `json:"accelerate"` 27 | WebsiteDomainName string `json:"website_domain_name"` 28 | } 29 | -------------------------------------------------------------------------------- /internal/command/launch/plan/redis.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | type RedisPlan struct { 4 | UpstashRedis *UpstashRedisPlan `json:"upstash_redis"` 5 | } 6 | 7 | func (p *RedisPlan) Provider() any { 8 | if p == nil { 9 | return nil 10 | } 11 | if p.UpstashRedis != nil { 12 | return p.UpstashRedis 13 | } 14 | return nil 15 | } 16 | 17 | func DefaultRedis(plan *LaunchPlan) RedisPlan { 18 | return RedisPlan{ 19 | UpstashRedis: &UpstashRedisPlan{ 20 | Eviction: false, 21 | }, 22 | } 23 | } 24 | 25 | type UpstashRedisPlan struct { 26 | Eviction bool `json:"eviction"` 27 | ReadReplicas []string `json:"read_replicas"` 28 | } 29 | -------------------------------------------------------------------------------- /internal/command/lfsc/clusters.go: -------------------------------------------------------------------------------- 1 | package lfsc 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func newClusters() *cobra.Command { 10 | const ( 11 | long = `"Commands for managing LiteFS Cloud clusters" 12 | ` 13 | short = "Manage LiteFS Cloud clusters" 14 | usage = "clusters " 15 | ) 16 | 17 | cmd := command.New(usage, short, long, nil, 18 | command.RequireSession, 19 | ) 20 | 21 | cmd.AddCommand( 22 | newClustersCreate(), 23 | newClustersDestroy(), 24 | newClustersList(), 25 | ) 26 | 27 | return cmd 28 | } 29 | -------------------------------------------------------------------------------- /internal/command/machine/lifecycle_hooks.go: -------------------------------------------------------------------------------- 1 | package machine 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | fly "github.com/superfly/fly-go" 8 | "github.com/superfly/flyctl/flypg" 9 | "github.com/superfly/flyctl/internal/command/postgres" 10 | "github.com/superfly/flyctl/iostreams" 11 | ) 12 | 13 | func runOnDeletionHook(ctx context.Context, app *fly.AppCompact, machine *fly.Machine) { 14 | var ( 15 | io = iostreams.FromContext(ctx) 16 | labels = machine.ImageRef.Labels 17 | ) 18 | 19 | if labels["fly.pg-manager"] == flypg.ReplicationManager { 20 | fmt.Fprintf(io.Out, "unregistering postgres member '%s' from the cluster... ", machine.PrivateIP) 21 | if err := postgres.UnregisterMember(ctx, app, machine); err != nil { 22 | fmt.Fprintln(io.Out, "(failed)") 23 | fmt.Fprintf(io.Out, "failed to unregister postgres member: %v\n", err) 24 | return 25 | } 26 | fmt.Fprintln(io.Out, "(success)") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/command/machine/machine.go: -------------------------------------------------------------------------------- 1 | package machine 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() *cobra.Command { 9 | const ( 10 | short = "Manage Fly Machines." 11 | long = short + ` Fly Machines are super-fast, lightweight VMs that can be created, 12 | and then quickly started and stopped as needed with flyctl commands or with the 13 | Machines REST fly.` 14 | usage = "machine " 15 | ) 16 | 17 | cmd := command.New(usage, short, long, nil) 18 | 19 | cmd.Args = cobra.NoArgs 20 | 21 | cmd.Aliases = []string{"machines", "m"} 22 | 23 | cmd.AddCommand( 24 | newKill(), 25 | newList(), 26 | newDestroy(), 27 | newRun(), 28 | newCreate(), 29 | newStart(), 30 | newStop(), 31 | newStatus(), 32 | newProxy(), 33 | newClone(), 34 | newUpdate(), 35 | newRestart(), 36 | newLeases(), 37 | newMachineExec(), 38 | newMachineCordon(), 39 | newMachineUncordon(), 40 | newSuspend(), 41 | newEgressIp(), 42 | newPlace(), 43 | ) 44 | 45 | return cmd 46 | } 47 | -------------------------------------------------------------------------------- /internal/command/mcp/mcp.go: -------------------------------------------------------------------------------- 1 | package mcp 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/apex/log" 10 | "github.com/spf13/cobra" 11 | "github.com/superfly/flyctl/internal/command" 12 | ) 13 | 14 | func New() *cobra.Command { 15 | const ( 16 | short = `flyctl Model Context Protocol.` 17 | 18 | long = short + "\n" 19 | ) 20 | 21 | cmd := command.New("mcp", short, long, nil) 22 | // cmd.Hidden = true 23 | 24 | cmd.AddCommand( 25 | NewProxy(), 26 | NewInspect(), 27 | newServer(), 28 | NewWrap(), 29 | 30 | NewAdd(), 31 | NewRemove(), 32 | 33 | NewLaunch(), 34 | NewDestroy(), 35 | 36 | newVolume(), 37 | newList(), 38 | newLogs(), 39 | ) 40 | 41 | return cmd 42 | } 43 | 44 | func flyctl(args ...string) error { 45 | executable, err := os.Executable() 46 | if err != nil { 47 | return fmt.Errorf("failed to find executable: %w", err) 48 | } 49 | 50 | log.Debugf("Running:", executable, strings.Join(args, " ")) 51 | 52 | cmd := exec.Command(executable, args...) 53 | cmd.Env = os.Environ() 54 | cmd.Stdout = os.Stdout 55 | cmd.Stderr = os.Stderr 56 | cmd.Stdin = os.Stdin 57 | return cmd.Run() 58 | } 59 | -------------------------------------------------------------------------------- /internal/command/mcp/proxy/types.go: -------------------------------------------------------------------------------- 1 | package mcpProxy 2 | 3 | type ProxyInfo struct { 4 | Url string 5 | BearerToken string 6 | User string 7 | Password string 8 | Instance string 9 | Mode string // "passthru" or "sse" or "stream" 10 | Timeout int // Timeout in seconds for the request 11 | Ping bool 12 | } 13 | -------------------------------------------------------------------------------- /internal/command/mcp/server/logs.go: -------------------------------------------------------------------------------- 1 | package mcpServer 2 | 3 | var LogCommands = []FlyCommand{ 4 | { 5 | ToolName: "fly-logs", 6 | ToolDescription: "Get logs for a Fly.io app or specific machine", 7 | ToolArgs: map[string]FlyArg{ 8 | "app": { 9 | Description: "Name of the app", 10 | Required: true, 11 | Type: "string", 12 | }, 13 | "machine": { 14 | Description: "Specific machine ID", 15 | Required: false, 16 | Type: "string", 17 | }, 18 | "region": { 19 | Description: "Region to get logs from", 20 | Required: false, 21 | Type: "string", 22 | }, 23 | }, 24 | Builder: func(args map[string]string) ([]string, error) { 25 | cmdArgs := []string{"logs", "--no-tail"} 26 | 27 | if app, ok := args["app"]; ok { 28 | cmdArgs = append(cmdArgs, "-a", app) 29 | } 30 | 31 | if machine, ok := args["machine"]; ok { 32 | cmdArgs = append(cmdArgs, "--machine", machine) 33 | } 34 | 35 | if region, ok := args["region"]; ok { 36 | cmdArgs = append(cmdArgs, "--region", region) 37 | } 38 | 39 | return cmdArgs, nil 40 | }, 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /internal/command/mcp/server/platform.go: -------------------------------------------------------------------------------- 1 | package mcpServer 2 | 3 | import "github.com/superfly/flyctl/internal/command/platform" 4 | 5 | var PlatformCommands = []FlyCommand{ 6 | { 7 | ToolName: "fly-platform-regions", 8 | ToolDescription: platform.RegionsCommandDesc, 9 | ToolArgs: map[string]FlyArg{}, 10 | 11 | Builder: func(args map[string]string) ([]string, error) { 12 | cmdArgs := []string{"platform", "regions", "--json"} 13 | return cmdArgs, nil 14 | }, 15 | }, 16 | 17 | { 18 | ToolName: "fly-platform-status", 19 | ToolDescription: "Get the status of Fly's platform", 20 | ToolArgs: map[string]FlyArg{}, 21 | 22 | Builder: func(args map[string]string) ([]string, error) { 23 | cmdArgs := []string{"platform", "status", "--json"} 24 | return cmdArgs, nil 25 | }, 26 | }, 27 | 28 | { 29 | ToolName: "fly-platform-vm-sizes", 30 | ToolDescription: "Get a list of VM sizes available for Fly apps", 31 | ToolArgs: map[string]FlyArg{}, 32 | 33 | Builder: func(args map[string]string) ([]string, error) { 34 | cmdArgs := []string{"platform", "vm-sizes", "--json"} 35 | return cmdArgs, nil 36 | }, 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /internal/command/mcp/server/status.go: -------------------------------------------------------------------------------- 1 | package mcpServer 2 | 3 | var StatusCommands = []FlyCommand{ 4 | { 5 | ToolName: "fly-status", 6 | ToolDescription: "Get status of a Fly.io app", 7 | ToolArgs: map[string]FlyArg{ 8 | "app": { 9 | Description: "Name of the app", 10 | Required: true, 11 | Type: "string", 12 | }, 13 | }, 14 | Builder: func(args map[string]string) ([]string, error) { 15 | cmdArgs := []string{"status", "--json"} 16 | 17 | if app, ok := args["app"]; ok { 18 | cmdArgs = append(cmdArgs, "-a", app) 19 | } 20 | 21 | return cmdArgs, nil 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /internal/command/move/move.go: -------------------------------------------------------------------------------- 1 | package move 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | "github.com/superfly/flyctl/internal/command/apps" 8 | "github.com/superfly/flyctl/internal/flag" 9 | ) 10 | 11 | // TODO: deprecate & remove 12 | func New() *cobra.Command { 13 | const ( 14 | long = `The MOVE command will move an application to another 15 | organization the current user belongs to. 16 | ` 17 | short = "Move an app to another organization" 18 | usage = "move " 19 | ) 20 | 21 | move := command.New(usage, short, long, apps.RunMove, 22 | command.RequireSession) 23 | move.Hidden = true 24 | move.Deprecated = "use `fly apps move` instead" 25 | 26 | move.Args = cobra.ExactArgs(1) 27 | 28 | flag.Add(move, 29 | flag.Yes(), 30 | flag.Org(), 31 | ) 32 | 33 | return move 34 | } 35 | -------------------------------------------------------------------------------- /internal/command/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command/extensions/fly_mysql" 6 | ) 7 | 8 | func New() (cmd *cobra.Command) { 9 | return fly_mysql.New() 10 | } 11 | -------------------------------------------------------------------------------- /internal/command/open/open.go: -------------------------------------------------------------------------------- 1 | package open 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command/apps" 7 | ) 8 | 9 | // TODO: deprecate 10 | func New() *cobra.Command { 11 | cmd := apps.NewOpen() 12 | cmd.Deprecated = "use `fly apps open` instead" 13 | cmd.Hidden = true 14 | return cmd 15 | } 16 | -------------------------------------------------------------------------------- /internal/command/options.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func AnnotateCommand(cmd *cobra.Command, key, value string) { 8 | if cmd.Annotations == nil { 9 | cmd.Annotations = map[string]string{} 10 | } 11 | 12 | cmd.Annotations[key] = value 13 | } 14 | 15 | func TagV1Command(cmd *cobra.Command) { 16 | AnnotateCommand(cmd, "apps_v1", "1") 17 | } 18 | 19 | func IsAppsV1Command(cmd *cobra.Command) bool { 20 | _, ok := cmd.Annotations["apps_v1"] 21 | return ok 22 | } 23 | -------------------------------------------------------------------------------- /internal/command/platform/platform.go: -------------------------------------------------------------------------------- 1 | // Package platform implements the platform command chain. 2 | package platform 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/superfly/flyctl/internal/command" 8 | ) 9 | 10 | // New initializes and returns a new platform Command. 11 | func New() (cmd *cobra.Command) { 12 | const ( 13 | long = `The PLATFORM commands are for users looking for information 14 | about the Fly platform. 15 | ` 16 | short = "Fly platform information" 17 | ) 18 | 19 | cmd = command.New("platform", short, long, nil) 20 | 21 | cmd.AddCommand( 22 | newRegions(), 23 | newStatus(), 24 | newVMSizes(), 25 | ) 26 | 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /internal/command/postgres/config.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | // pgSettings maps the command-line argument to the actual pgParameter. 9 | // This also acts as a whitelist as far as what's configurable via flyctl and 10 | // can be expanded on as needed. 11 | var pgSettings = map[string]string{ 12 | "wal-level": "wal_level", 13 | "max-wal-senders": "max_wal_senders", 14 | "max-replication-slots": "max_replication_slots", 15 | "max-connections": "max_connections", 16 | "work-mem": "work_mem", 17 | "maintenance-work-mem": "maintenance_work_mem", 18 | "shared-buffers": "shared_buffers", 19 | "log-statement": "log_statement", 20 | "log-min-duration-statement": "log_min_duration_statement", 21 | "shared-preload-libraries": "shared_preload_libraries", 22 | } 23 | 24 | func newConfig() (cmd *cobra.Command) { 25 | const ( 26 | short = "Show and manage Postgres configuration." 27 | long = short + "\n" 28 | ) 29 | 30 | cmd = command.New("config", short, long, nil) 31 | 32 | cmd.AddCommand( 33 | newConfigShow(), 34 | newConfigUpdate(), 35 | ) 36 | 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /internal/command/postgres/postgres_test.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | fly "github.com/superfly/fly-go" 8 | ) 9 | 10 | func TestIsFlex(t *testing.T) { 11 | assert.False(t, IsFlex(nil)) 12 | assert.False(t, IsFlex(&fly.Machine{})) 13 | assert.False(t, IsFlex(&fly.Machine{ 14 | ImageRef: fly.MachineImageRef{ 15 | Labels: map[string]string{ 16 | "fly.pg-manager": "stolon", 17 | }, 18 | }, 19 | })) 20 | assert.True(t, IsFlex(&fly.Machine{ 21 | ImageRef: fly.MachineImageRef{ 22 | Labels: map[string]string{ 23 | "fly.pg-manager": "repmgr", 24 | }, 25 | }, 26 | })) 27 | } 28 | -------------------------------------------------------------------------------- /internal/command/redis/attach.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/superfly/flyctl/gql" 8 | "github.com/superfly/flyctl/internal/flyutil" 9 | "github.com/superfly/flyctl/iostreams" 10 | ) 11 | 12 | func AttachDatabase(ctx context.Context, db *gql.AddOn, appName string) (err error) { 13 | client := flyutil.ClientFromContext(ctx) 14 | io := iostreams.FromContext(ctx) 15 | s := map[string]string{} 16 | s["REDIS_URL"] = db.PublicUrl 17 | 18 | _, err = client.SetSecrets(ctx, appName, s) 19 | 20 | if err != nil { 21 | fmt.Fprintf(io.Out, "\nCould not attach Redis database %s to app %s\n", db.Name, appName) 22 | } else { 23 | fmt.Fprintf(io.Out, "\nRedis database %s is set on %s as the REDIS_URL environment variable\n", db.Name, appName) 24 | } 25 | 26 | return err 27 | } 28 | -------------------------------------------------------------------------------- /internal/command/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/superfly/flyctl/gql" 9 | "github.com/superfly/flyctl/internal/command" 10 | "github.com/superfly/flyctl/internal/flyutil" 11 | ) 12 | 13 | // TODO: make internal once the open command has been deprecated 14 | func New() (cmd *cobra.Command) { 15 | const ( 16 | long = `Launch and manage Redis databases managed by Upstash.com` 17 | short = long 18 | ) 19 | 20 | cmd = command.New("redis", short, long, nil) 21 | 22 | cmd.AddCommand( 23 | newCreate(), 24 | newList(), 25 | newDestroy(), 26 | newStatus(), 27 | newPlans(), 28 | newUpdate(), 29 | newConnect(), 30 | newDashboard(), 31 | newReset(), 32 | newProxy(), 33 | ) 34 | 35 | return cmd 36 | } 37 | 38 | func GetExcludedRegions(ctx context.Context) (excludedRegions []string, err error) { 39 | client := flyutil.ClientFromContext(ctx).GenqClient() 40 | 41 | response, err := gql.GetAddOnProvider(ctx, client, "upstash_redis") 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | for _, region := range response.AddOnProvider.ExcludedRegions { 47 | excludedRegions = append(excludedRegions, region.Code) 48 | } 49 | 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /internal/command/registry/auth.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/superfly/flyctl/gql" 8 | "github.com/superfly/flyctl/internal/flyutil" 9 | ) 10 | 11 | func makeToken(ctx context.Context, name, orgID, expiry, profile string, options *gql.LimitedAccessTokenOptions) (*gql.CreateLimitedAccessTokenResponse, error) { 12 | apiClient := flyutil.ClientFromContext(ctx) 13 | resp, err := gql.CreateLimitedAccessToken( 14 | ctx, 15 | apiClient.GenqClient(), 16 | name, 17 | orgID, 18 | profile, 19 | options, 20 | expiry, 21 | ) 22 | if err != nil { 23 | return nil, fmt.Errorf("failed creating token: %w", err) 24 | } 25 | return resp, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/command/registry/command.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | const ( 11 | usage = "registry" 12 | short = "Operate on registry images [experimental]" 13 | long = "Scan registry images for an SBOM or vulnerabilities. These commands\n" + 14 | "are experimental and subject to change." 15 | ) 16 | cmd := command.New(usage, short, long, nil) 17 | cmd.Hidden = true 18 | 19 | cmd.AddCommand( 20 | newFiles(), 21 | newSbom(), 22 | newVulns(), 23 | newVulnSummary(), 24 | ) 25 | 26 | return cmd 27 | } 28 | -------------------------------------------------------------------------------- /internal/command/releases/releases.go: -------------------------------------------------------------------------------- 1 | package releases 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command/apps" 7 | ) 8 | 9 | // TODO: deprecate 10 | func New() *cobra.Command { 11 | return apps.NewReleases() 12 | } 13 | -------------------------------------------------------------------------------- /internal/command/resume/resume.go: -------------------------------------------------------------------------------- 1 | package resume 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | resume := command.New("resume ", "", "", nil, command.RequireSession) 11 | resume.Hidden = true 12 | resume.Deprecated = "use `fly scale count` instead" 13 | return resume 14 | } 15 | -------------------------------------------------------------------------------- /internal/command/scale/memory.go: -------------------------------------------------------------------------------- 1 | package scale 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/docker/go-units" 7 | "github.com/spf13/cobra" 8 | "github.com/superfly/flyctl/helpers" 9 | "github.com/superfly/flyctl/internal/command" 10 | "github.com/superfly/flyctl/internal/flag" 11 | ) 12 | 13 | func newScaleMemory() *cobra.Command { 14 | const ( 15 | short = "Set VM memory" 16 | long = `Set VM memory to a number of megabytes` 17 | ) 18 | cmd := command.New("memory [memoryMB]", short, long, runScaleMemory, 19 | command.RequireSession, 20 | command.RequireAppName, 21 | ) 22 | cmd.Args = cobra.ExactArgs(1) 23 | flag.Add(cmd, 24 | flag.App(), 25 | flag.AppConfig(), 26 | flag.ProcessGroup("The process group to apply the VM size to"), 27 | ) 28 | return cmd 29 | } 30 | 31 | func runScaleMemory(ctx context.Context) error { 32 | group := flag.GetProcessGroup(ctx) 33 | 34 | memoryMB, err := helpers.ParseSize(flag.FirstArg(ctx), units.RAMInBytes, units.MiB) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return scaleVertically(ctx, group, "", memoryMB) 40 | } 41 | -------------------------------------------------------------------------------- /internal/command/scale/scale.go: -------------------------------------------------------------------------------- 1 | package scale 2 | 3 | import ( 4 | "github.com/superfly/flyctl/internal/command" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | const ( 11 | short = "Scale app resources" 12 | long = `Scale application resources` 13 | ) 14 | cmd := command.New("scale", short, long, nil) 15 | cmd.AddCommand( 16 | newScaleVm(), 17 | newScaleMemory(), 18 | newScaleShow(), 19 | newScaleCount(), 20 | ) 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /internal/command/scale/show.go: -------------------------------------------------------------------------------- 1 | package scale 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | "github.com/superfly/flyctl/internal/flag" 7 | ) 8 | 9 | func newScaleShow() *cobra.Command { 10 | const ( 11 | short = "Show current resources" 12 | long = `Show current VM size and counts` 13 | ) 14 | cmd := command.New("show", short, long, runMachinesScaleShow, 15 | command.RequireSession, 16 | command.RequireAppName, 17 | ) 18 | cmd.Args = cobra.NoArgs 19 | flag.Add(cmd, 20 | flag.App(), 21 | flag.AppConfig(), 22 | flag.JSONOutput(), 23 | ) 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /internal/command/scale/show_machines_test.go: -------------------------------------------------------------------------------- 1 | package scale 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | fly "github.com/superfly/fly-go" 8 | ) 9 | 10 | func Test_formatRegions(t *testing.T) { 11 | assert.Equal(t, 12 | formatRegions([]*fly.Machine{ 13 | {Region: "fra"}, 14 | {Region: "fra"}, 15 | {Region: "fra"}, 16 | {Region: "scl"}, 17 | {Region: "scl"}, 18 | {Region: "mia"}, 19 | }), 20 | "fra(3),mia,scl(2)", 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /internal/command/services/services.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | const ( 11 | long = `Shows information about the services of the application.` 12 | short = `Show the application's services` 13 | ) 14 | 15 | services := command.New("services", short, long, nil) 16 | 17 | services.AddCommand( 18 | newList(), 19 | ) 20 | 21 | return services 22 | } 23 | -------------------------------------------------------------------------------- /internal/command/settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() *cobra.Command { 9 | cmd := command.New("settings", "Manage flyctl settings", "", nil) 10 | 11 | cmd.AddCommand( 12 | newAnalytics(), 13 | newAutoUpdate(), 14 | newSynthetics(), 15 | ) 16 | 17 | return cmd 18 | } 19 | -------------------------------------------------------------------------------- /internal/command/ssh/console_stub.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package ssh 4 | 5 | // Windows consoles need a bit of magic to correctly handle ANSI escape 6 | // sequences. Stub these out for non-Windows platforms. 7 | 8 | func setupConsole() (uint32, uint32, uint32, error) { 9 | return 0, 0, 0, nil 10 | } 11 | 12 | func cleanupConsole(_currentStdin uint32, _currentStdout uint32, _currentStderr uint32) error { 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/command/ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | // New initializes and returns a new apps Command. 9 | func New() *cobra.Command { 10 | const ( 11 | long = `Use SSH to log into or run commands on Machines` 12 | short = long 13 | ) 14 | 15 | cmd := command.New("ssh", short, long, nil) 16 | 17 | cmd.AddCommand( 18 | newConsole(), 19 | newIssue(), 20 | newLog(), 21 | NewSFTP(), 22 | ) 23 | 24 | return cmd 25 | } 26 | -------------------------------------------------------------------------------- /internal/command/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command/extensions/tigris" 6 | ) 7 | 8 | func New() (cmd *cobra.Command) { 9 | return tigris.New() 10 | } 11 | -------------------------------------------------------------------------------- /internal/command/suspend/suspend.go: -------------------------------------------------------------------------------- 1 | package suspend 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | suspend := command.New("suspend [APPNAME]", "", "", nil, command.RequireSession) 11 | suspend.Hidden = true 12 | suspend.Deprecated = "use `fly scale count` instead" 13 | return suspend 14 | } 15 | -------------------------------------------------------------------------------- /internal/command/synthetics/synthetics.go: -------------------------------------------------------------------------------- 1 | package synthetics 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/superfly/flyctl/internal/command" 8 | "github.com/superfly/flyctl/internal/metrics/synthetics" 9 | ) 10 | 11 | func New() *cobra.Command { 12 | const ( 13 | short = "Synthetic monitoring" 14 | long = `Synthetic monitoring management.` 15 | ) 16 | cmd := command.New("synthetics", short, long, nil) 17 | cmd.AddCommand( 18 | newAgent(), 19 | ) 20 | return cmd 21 | } 22 | 23 | func newAgent() *cobra.Command { 24 | const ( 25 | short = "Runs the Synthetics agent" 26 | long = "Runs the Synthetics agent in the foreground." 27 | ) 28 | cmd := command.New("agent", short, long, runAgent, 29 | command.RequireSession, 30 | ) 31 | cmd.Args = cobra.NoArgs 32 | return cmd 33 | } 34 | 35 | func runAgent(ctx context.Context) (err error) { 36 | err = synthetics.RunAgent(ctx) 37 | if err != nil { 38 | return err 39 | } 40 | <-ctx.Done() 41 | return ctx.Err() 42 | } 43 | -------------------------------------------------------------------------------- /internal/command/tokens/revoke.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/superfly/flyctl/internal/command" 10 | "github.com/superfly/flyctl/internal/flag" 11 | "github.com/superfly/flyctl/internal/flyutil" 12 | ) 13 | 14 | func newRevoke() *cobra.Command { 15 | const ( 16 | short = "Revoke tokens" 17 | long = "Revoke one or more tokens." 18 | usage = "revoke [flags] ID ID ..." 19 | ) 20 | 21 | cmd := command.New(usage, short, long, runRevoke, 22 | command.RequireSession, 23 | ) 24 | 25 | return cmd 26 | } 27 | 28 | func runRevoke(ctx context.Context) (err error) { 29 | apiClient := flyutil.ClientFromContext(ctx) 30 | 31 | numRevoked := 0 32 | 33 | args := flag.Args(ctx) 34 | if len(args) == 0 { 35 | return fmt.Errorf("no token IDs; please provide IDs as positional arguments") 36 | } 37 | 38 | for _, id := range args { 39 | err := apiClient.RevokeLimitedAccessToken(ctx, id) 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "failed to revoke token %s: %s\n", id, err) 42 | continue 43 | } 44 | fmt.Printf("Revoked %s\n", id) 45 | numRevoked += 1 46 | } 47 | 48 | fmt.Printf("%d tokens revoked\n", numRevoked) 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/command/tokens/tokens.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() *cobra.Command { 9 | const ( 10 | short = "Manage Fly.io API tokens" 11 | long = "Manage Fly.io API tokens" 12 | usage = "tokens" 13 | ) 14 | 15 | cmd := command.New(usage, short, long, nil) 16 | 17 | hiddenDeploy := newDeploy() 18 | hiddenDeploy.Hidden = true 19 | 20 | hiddenOrg := newOrg() 21 | hiddenOrg.Hidden = true 22 | 23 | cmd.AddCommand( 24 | newCreate(), 25 | newList(), 26 | newRevoke(), 27 | newAttenuate(), 28 | newDebug(), 29 | new3P(), 30 | hiddenDeploy, 31 | hiddenOrg, 32 | ) 33 | 34 | return cmd 35 | } 36 | -------------------------------------------------------------------------------- /internal/command/volumes/lsvd/lsvd.go: -------------------------------------------------------------------------------- 1 | package lsvd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/superfly/flyctl/internal/command" 6 | ) 7 | 8 | func New() *cobra.Command { 9 | const help = "Manage log-structured virtual disks (LSVD) on an app" 10 | cmd := command.New("lsvd", help, help, nil) 11 | cmd.Hidden = true 12 | cmd.AddCommand(newSetup()) 13 | return cmd 14 | } 15 | -------------------------------------------------------------------------------- /internal/command/volumes/snapshots/snapshots.go: -------------------------------------------------------------------------------- 1 | package snapshots 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/superfly/flyctl/internal/command" 7 | ) 8 | 9 | func New() *cobra.Command { 10 | const ( 11 | short = "Manage volume snapshots." 12 | 13 | long = short + " A snapshot is a point-in-time copy of a volume. Snapshots can be used to create new volumes or restore a volume to a previous state." 14 | 15 | usage = "snapshots" 16 | ) 17 | 18 | snapshots := command.New(usage, short, long, nil, 19 | command.RequireSession, 20 | ) 21 | 22 | snapshots.Aliases = []string{"snapshot", "snaps"} 23 | 24 | snapshots.AddCommand( 25 | newList(), 26 | newCreate(), 27 | ) 28 | 29 | return snapshots 30 | } 31 | -------------------------------------------------------------------------------- /internal/command_context/context.go: -------------------------------------------------------------------------------- 1 | package command_context 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type contextKey struct{} 10 | 11 | // NewContext derives a context that carries cmd from ctx. 12 | func NewContext(ctx context.Context, cmd *cobra.Command) context.Context { 13 | return context.WithValue(ctx, contextKey{}, cmd) 14 | } 15 | 16 | // FromContext returns the Command ctx carries. It panics in case ctx carries 17 | // no Command. 18 | func FromContext(ctx context.Context) *cobra.Command { 19 | return ctx.Value(contextKey{}).(*cobra.Command) 20 | } 21 | -------------------------------------------------------------------------------- /internal/config/context.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/superfly/fly-go/tokens" 7 | ) 8 | 9 | type contextKey struct{} 10 | 11 | // NewContext derives a Context that carries the given Config from ctx. 12 | func NewContext(ctx context.Context, cfg *Config) context.Context { 13 | return context.WithValue(ctx, contextKey{}, cfg) 14 | } 15 | 16 | // FromContext returns the Config ctx carries. It panics in case ctx carries 17 | // no Config. 18 | func FromContext(ctx context.Context) *Config { 19 | return ctx.Value(contextKey{}).(*Config) 20 | } 21 | 22 | func Tokens(ctx context.Context) *tokens.Tokens { 23 | return FromContext(ctx).Tokens 24 | } 25 | -------------------------------------------------------------------------------- /internal/config/context_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFromContextPanics(t *testing.T) { 11 | assert.Panics(t, func() { _ = FromContext(context.Background()) }) 12 | } 13 | 14 | func TestNewContext(t *testing.T) { 15 | exp := new(Config) 16 | 17 | ctx := NewContext(context.Background(), exp) 18 | assert.Same(t, exp, FromContext(ctx)) 19 | } 20 | -------------------------------------------------------------------------------- /internal/flag/flagctx/helpers.go: -------------------------------------------------------------------------------- 1 | package flagctx 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/pflag" 7 | ) 8 | 9 | // This is a hack to allow constructing a flag context from within completion, 10 | // a dependency of flag. 11 | 12 | type contextKey struct{} 13 | 14 | // NewContext derives a context that carries fs from ctx. 15 | func NewContext(ctx context.Context, fs *pflag.FlagSet) context.Context { 16 | return context.WithValue(ctx, contextKey{}, fs) 17 | } 18 | 19 | // FromContext returns the FlagSet ctx carries. It panics in case ctx carries 20 | // no FlagSet. 21 | func FromContext(ctx context.Context) *pflag.FlagSet { 22 | return ctx.Value(contextKey{}).(*pflag.FlagSet) 23 | } 24 | -------------------------------------------------------------------------------- /internal/flyutil/flyutil.go: -------------------------------------------------------------------------------- 1 | package flyutil 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/superfly/fly-go" 7 | "github.com/superfly/flyctl/internal/buildinfo" 8 | "github.com/superfly/flyctl/internal/logger" 9 | ) 10 | 11 | func NewClientFromOptions(ctx context.Context, opts fly.ClientOptions) *fly.Client { 12 | if opts.Name == "" { 13 | opts.Name = buildinfo.Name() 14 | } 15 | if opts.Version == "" { 16 | opts.Version = buildinfo.Version().String() 17 | } 18 | if v := logger.MaybeFromContext(ctx); v != nil && opts.Logger == nil { 19 | opts.Logger = v 20 | } 21 | return fly.NewClientFromOptions(opts) 22 | } 23 | -------------------------------------------------------------------------------- /internal/future/future.go: -------------------------------------------------------------------------------- 1 | package future 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | type Future[T any] struct { 9 | mu sync.RWMutex 10 | val T 11 | err error 12 | } 13 | 14 | func (fut *Future[T]) Get() (T, error) { 15 | fut.mu.RLock() 16 | defer fut.mu.RUnlock() 17 | 18 | return fut.val, fut.err 19 | } 20 | 21 | // Spawns `fn` on a new goroutine and returns future which resolves on 22 | // completion. 23 | func Spawn[T any](fn func() (T, error)) *Future[T] { 24 | // allocate future and lock it immediately, we pass implied ownership of 25 | // this lock to the spawned goroutine 26 | fut := new(Future[T]) 27 | fut.mu.Lock() 28 | 29 | // spawn goroutine to call fn and update future when done 30 | go func() { 31 | defer func() { 32 | // if we panicked, set future's error field and rethrow 33 | if err := recover(); err != nil { 34 | fut.err = fmt.Errorf("panic: %v", err) 35 | panic(err) 36 | } 37 | 38 | fut.mu.Unlock() 39 | }() 40 | 41 | fut.val, fut.err = fn() 42 | }() 43 | 44 | return fut 45 | } 46 | 47 | func Ready[T any](val T) *Future[T] { 48 | return &Future[T]{val: val} 49 | } 50 | -------------------------------------------------------------------------------- /internal/httptracing/har.go: -------------------------------------------------------------------------------- 1 | package httptracing 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/haileys/go-harlog" 6 | "github.com/superfly/flyctl/terminal" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | type harOpt struct { 12 | Path string 13 | Container *harlog.HARContainer 14 | } 15 | 16 | var har *harOpt 17 | 18 | func Init() { 19 | if path := os.Getenv("FLYCTL_OUTPUT_HAR"); path != "" { 20 | har = &harOpt{ 21 | Path: path, 22 | Container: harlog.NewHARContainer(), 23 | } 24 | } 25 | } 26 | 27 | func Finish() { 28 | if har == nil { 29 | return 30 | } 31 | 32 | harJson, err := json.MarshalIndent(har.Container, "", " ") 33 | if err != nil { 34 | terminal.Warnf("error serializing HAR: %v\n", err) 35 | return 36 | } 37 | 38 | err = os.WriteFile(har.Path, harJson, 0644) 39 | if err != nil { 40 | terminal.Warnf("error writing HAR: %v\n", err) 41 | } 42 | } 43 | 44 | func NewTransport(transport http.RoundTripper) http.RoundTripper { 45 | if har == nil { 46 | return transport 47 | } 48 | 49 | return &harlog.Transport{ 50 | Transport: transport, 51 | Container: har.Container, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/incidents/incidents.go: -------------------------------------------------------------------------------- 1 | package incidents 2 | 3 | import ( 4 | "github.com/superfly/flyctl/internal/cmdutil" 5 | "github.com/superfly/flyctl/internal/env" 6 | "os" 7 | ) 8 | 9 | // Check for incidents 10 | func Check() bool { 11 | switch { 12 | case env.IsTruthy("FLY_INCIDENTS_CHECK"): 13 | return true 14 | case env.IsTruthy("FLY_NO_INCIDENTS_CHECK"): 15 | return false 16 | case !cmdutil.IsTerminal(os.Stdout), !cmdutil.IsTerminal(os.Stderr): 17 | return false 18 | default: 19 | return true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/logger/context.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import "context" 4 | 5 | type contextKey struct{} 6 | 7 | // NewContext derives a context that carries logger from ctx. 8 | func NewContext(ctx context.Context, logger *Logger) context.Context { 9 | return context.WithValue(ctx, contextKey{}, logger) 10 | } 11 | 12 | // FromContext returns the Logger ctx carries. It panics in case ctx carries 13 | // no Logger. 14 | func FromContext(ctx context.Context) *Logger { 15 | return ctx.Value(contextKey{}).(*Logger) 16 | } 17 | 18 | func MaybeFromContext(ctx context.Context) (l *Logger) { 19 | if v := ctx.Value(contextKey{}); v != nil { 20 | l = v.(*Logger) 21 | } 22 | 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /internal/logger/context_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFromContextPanics(t *testing.T) { 11 | assert.Panics(t, func() { _ = FromContext(context.Background()) }) 12 | } 13 | 14 | func TestNewContext(t *testing.T) { 15 | exp := new(Logger) 16 | 17 | ctx := NewContext(context.Background(), exp) 18 | assert.Same(t, exp, FromContext(ctx)) 19 | } 20 | 21 | func TestMaybeFromContextDoesNotPanic(t *testing.T) { 22 | assert.Nil(t, MaybeFromContext(context.Background())) 23 | } 24 | 25 | func TestMaybeFromContext(t *testing.T) { 26 | exp := new(Logger) 27 | 28 | ctx := NewContext(context.Background(), exp) 29 | assert.Same(t, exp, MaybeFromContext(ctx)) 30 | 31 | assert.Nil(t, MaybeFromContext(context.Background())) 32 | } 33 | -------------------------------------------------------------------------------- /internal/logger/file_logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | type logFile struct { 12 | file *os.File 13 | writer *bufio.Writer 14 | lock sync.Mutex 15 | destroyed bool 16 | } 17 | 18 | var ( 19 | logfileAlreadyClosedError = errors.New("logfile already closed") 20 | logfileAlreadyInitializedError = errors.New("logfile already initialized") 21 | ) 22 | 23 | func (l *logFile) WriteLog(_ Level, line string) { 24 | fmt.Fprint(l, line) 25 | } 26 | 27 | func (l *logFile) UseAnsi() bool { 28 | return false 29 | } 30 | 31 | func (l *logFile) Write(p []byte) (n int, err error) { 32 | l.lock.Lock() 33 | defer l.lock.Unlock() 34 | if l.destroyed { 35 | return 0, logfileAlreadyClosedError 36 | } 37 | return l.writer.Write(p) 38 | } 39 | 40 | func (l *logFile) Close() error { 41 | l.lock.Lock() 42 | defer l.lock.Unlock() 43 | if l.destroyed { 44 | return logfileAlreadyClosedError 45 | } 46 | l.destroyed = true 47 | if err := l.writer.Flush(); err != nil { 48 | return err 49 | } 50 | return l.file.Close() 51 | } 52 | 53 | func (l *logFile) Level() Level { 54 | return NoLogLevel 55 | } 56 | -------------------------------------------------------------------------------- /internal/logger/global_logfile.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bufio" 5 | 6 | "github.com/superfly/flyctl/internal/logger/logfile" 7 | ) 8 | 9 | var globalLogFile = logFile{ 10 | destroyed: true, 11 | } 12 | 13 | func InitLogFile() error { 14 | if !globalLogFile.destroyed { 15 | return logfileAlreadyInitializedError 16 | } 17 | rawFile, err := logfile.CreateLogFile() 18 | globalLogFile = logFile{ 19 | file: rawFile, 20 | writer: bufio.NewWriter(rawFile), 21 | destroyed: false, 22 | } 23 | return err 24 | } 25 | 26 | func CloseLogFile() error { 27 | if globalLogFile.destroyed { 28 | return logfileAlreadyClosedError 29 | } 30 | defer func() { 31 | globalLogFile.writer = nil 32 | globalLogFile.file = nil 33 | globalLogFile.destroyed = true 34 | }() 35 | 36 | return globalLogFile.Close() 37 | } 38 | -------------------------------------------------------------------------------- /internal/logger/logfile/name.go: -------------------------------------------------------------------------------- 1 | package logfile 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const ( 11 | timeSpec = time.RFC3339 12 | keepLogs = 10 13 | ) 14 | 15 | func formatLogName(fileTime time.Time) string { 16 | return fmt.Sprintf("flyctl-%s.log", fileTime.Format(timeSpec)) 17 | } 18 | func parseLogName(fileName string) (time.Time, error) { 19 | date := strings.TrimSuffix(strings.TrimPrefix(fileName, "flyctl-"), filepath.Ext(fileName)) 20 | return time.Parse(timeSpec, date) 21 | } 22 | -------------------------------------------------------------------------------- /internal/logger/split_logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import "github.com/superfly/flyctl/internal/cmdutil" 4 | 5 | type SplitLogger struct { 6 | terminal logSink 7 | file logSink 8 | } 9 | 10 | func (l *SplitLogger) WriteLog(level Level, line string) { 11 | if l.terminal != nil { 12 | l.terminal.WriteLog(level, line) 13 | } 14 | if l.file != nil { 15 | sanitized := line 16 | if !l.file.UseAnsi() && l.terminal != nil && l.terminal.UseAnsi() { 17 | sanitized = cmdutil.StripANSI(line) 18 | } 19 | l.file.WriteLog(level, sanitized) 20 | } 21 | } 22 | 23 | func (l *SplitLogger) UseAnsi() bool { 24 | if l.terminal != nil { 25 | return l.terminal.UseAnsi() 26 | } 27 | if l.file != nil { 28 | return l.file.UseAnsi() 29 | } 30 | return true 31 | } 32 | 33 | func (l *SplitLogger) Level() Level { 34 | if l.terminal != nil { 35 | return l.terminal.Level() 36 | } 37 | if l.file != nil { 38 | return l.file.Level() 39 | } 40 | return NoLogLevel 41 | } 42 | -------------------------------------------------------------------------------- /internal/logger/writer_logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type WriterLogger struct { 9 | out io.Writer 10 | level Level 11 | isTerm bool 12 | } 13 | 14 | func (l *WriterLogger) WriteLog(level Level, line string) { 15 | if level < l.level { 16 | return 17 | } 18 | fmt.Fprint(l.out, line) 19 | } 20 | 21 | func (l *WriterLogger) UseAnsi() bool { 22 | return l.isTerm 23 | } 24 | 25 | func (l *WriterLogger) Level() Level { 26 | return l.level 27 | } 28 | -------------------------------------------------------------------------------- /internal/machine/query.go: -------------------------------------------------------------------------------- 1 | package machine 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/samber/lo" 7 | fly "github.com/superfly/fly-go" 8 | "github.com/superfly/flyctl/internal/flapsutil" 9 | ) 10 | 11 | func ListActive(ctx context.Context) ([]*fly.Machine, error) { 12 | flapsClient := flapsutil.ClientFromContext(ctx) 13 | 14 | machines, err := flapsClient.List(ctx, "") 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | machines = lo.Filter(machines, func(m *fly.Machine, _ int) bool { 20 | return m.Config != nil && m.IsActive() && !m.IsReleaseCommandMachine() && !m.IsFlyAppsConsole() 21 | }) 22 | 23 | return machines, nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/machine/restart.go: -------------------------------------------------------------------------------- 1 | package machine 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | fly "github.com/superfly/fly-go" 9 | "github.com/superfly/flyctl/internal/flapsutil" 10 | "github.com/superfly/flyctl/internal/watch" 11 | "github.com/superfly/flyctl/iostreams" 12 | ) 13 | 14 | func Restart(ctx context.Context, m *fly.Machine, input *fly.RestartMachineInput, nonce string) error { 15 | var ( 16 | flapsClient = flapsutil.ClientFromContext(ctx) 17 | io = iostreams.FromContext(ctx) 18 | colorize = io.ColorScheme() 19 | ) 20 | 21 | fmt.Fprintf(io.Out, "Restarting machine %s\n", colorize.Bold(m.ID)) 22 | input.ID = m.ID 23 | if err := flapsClient.Restart(ctx, *input, nonce); err != nil { 24 | return fmt.Errorf("could not stop machine %s: %w", input.ID, err) 25 | } 26 | 27 | if err := WaitForStartOrStop(ctx, &fly.Machine{ID: input.ID}, "start", time.Minute*5); err != nil { 28 | return err 29 | } 30 | 31 | if !input.SkipHealthChecks { 32 | if err := watch.MachinesChecks(ctx, []*fly.Machine{m}); err != nil { 33 | return fmt.Errorf("failed to wait for health checks to pass: %w", err) 34 | } 35 | } 36 | fmt.Fprintf(io.Out, "Machine %s restarted successfully!\n", colorize.Bold(m.ID)) 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/metrics/api.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "sync" 7 | 8 | "github.com/superfly/flyctl/internal/buildinfo" 9 | "github.com/superfly/flyctl/internal/config" 10 | ) 11 | 12 | var ( 13 | Enabled = true 14 | done sync.WaitGroup 15 | ) 16 | 17 | type metricsMessage struct { 18 | Metric string `json:"m"` 19 | Payload json.RawMessage `json:"p"` 20 | } 21 | 22 | func rawSend(parentCtx context.Context, metricSlug string, payload json.RawMessage) { 23 | if !shouldSendMetrics(parentCtx) { 24 | return 25 | } 26 | 27 | message := metricsMessage{ 28 | Metric: metricSlug, 29 | Payload: payload, 30 | } 31 | 32 | queueMetric(message) 33 | } 34 | 35 | func shouldSendMetrics(ctx context.Context) bool { 36 | if !Enabled { 37 | return false 38 | } 39 | 40 | cfg := config.FromContext(ctx) 41 | 42 | if !cfg.SendMetrics { 43 | return false 44 | } 45 | 46 | // never send metrics to the production collector from dev builds 47 | if buildinfo.IsDev() && cfg.MetricsBaseURLIsProduction() { 48 | return false 49 | } 50 | 51 | return true 52 | } 53 | 54 | func FlushPending() { 55 | if !Enabled { 56 | return 57 | } 58 | 59 | done.Wait() 60 | } 61 | -------------------------------------------------------------------------------- /internal/metrics/synthetics/signals_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package synthetics 4 | 5 | import ( 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func signalChannel(c chan os.Signal) error { 12 | signal.Notify(c, syscall.SIGUSR1) 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/metrics/synthetics/signals_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package synthetics 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func signalChannel(c chan os.Signal) error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /internal/prompt/prompt_test.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "testing/quick" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestIsNonInteractive(t *testing.T) { 13 | cases := []struct { 14 | err error 15 | exp bool 16 | }{ 17 | {assert.AnError, false}, 18 | {fmt.Errorf("wrapped: %w", assert.AnError), false}, 19 | {ErrNonInteractive, true}, 20 | {fmt.Errorf("wrapped: %w", ErrNonInteractive), true}, 21 | {NonInteractiveError("some error"), true}, 22 | } 23 | 24 | for i, kase := range cases { 25 | assert.Equal(t, kase.exp, IsNonInteractive(kase.err), "case: %d", i) 26 | } 27 | } 28 | 29 | func TestNonInteractiveError(t *testing.T) { 30 | fn := func(exp string) bool { 31 | return NonInteractiveError(exp).Error() == exp 32 | } 33 | require.NoError(t, quick.Check(fn, nil)) 34 | } 35 | -------------------------------------------------------------------------------- /internal/release/meta.go: -------------------------------------------------------------------------------- 1 | package release 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/superfly/flyctl/internal/version" 7 | ) 8 | 9 | type Meta struct { 10 | Version *version.Version `json:"version"` 11 | Channel string `json:"channel"` 12 | Commit string `json:"commit"` 13 | CommitTime time.Time `json:"commit_time"` 14 | Tag string `json:"tag"` 15 | Branch string `json:"branch"` 16 | Dirty bool `json:"dirty"` 17 | Ref string `json:"ref"` 18 | PreviousVersion *version.Version `json:"previous_version"` 19 | PreviousTag string `json:"previous_tag"` 20 | } 21 | -------------------------------------------------------------------------------- /internal/set/set_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "golang.org/x/exp/slices" 8 | ) 9 | 10 | func TestSet(t *testing.T) { 11 | 12 | var mySet Set[string] 13 | 14 | assert.False(t, mySet.HasAny("hello", "world")) 15 | 16 | mySet.Set("hello", "world") 17 | 18 | assert.True(t, mySet.Has("hello")) 19 | assert.True(t, mySet.Has("world")) 20 | assert.False(t, mySet.Has("foo")) 21 | 22 | assert.True(t, mySet.HasAny("hello", "world")) 23 | assert.True(t, mySet.HasAny("hello", "world", "foo")) 24 | 25 | assert.True(t, mySet.HasAll("hello", "world")) 26 | assert.False(t, mySet.HasAll("hello", "world", "foo")) 27 | 28 | hwSorted := []string{"hello", "world"} 29 | slices.Sort(hwSorted) 30 | valuesSorted := mySet.Values() 31 | slices.Sort(valuesSorted) 32 | assert.Equal(t, hwSorted, valuesSorted) 33 | 34 | other := mySet.Copy() 35 | assert.True(t, other.Has("hello")) 36 | other.Set("foo") 37 | assert.False(t, mySet.Has("foo")) 38 | 39 | assert.Equal(t, 3, other.Len()) 40 | 41 | mySet.Clear() 42 | 43 | assert.False(t, mySet.HasAny("hello", "world")) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /internal/sort/sort.go: -------------------------------------------------------------------------------- 1 | // Package sort implements common sorting functions. 2 | package sort 3 | 4 | import ( 5 | "sort" 6 | 7 | fly "github.com/superfly/fly-go" 8 | ) 9 | 10 | // OrganizationsByTypeAndName sorts orgs by their type and name. 11 | func OrganizationsByTypeAndName(orgs []fly.Organization) { 12 | sort.Slice(orgs, func(i, j int) bool { 13 | return orgs[i].Type < orgs[j].Type || orgs[i].Name < orgs[j].Name 14 | }) 15 | } 16 | 17 | // RegionsByNameAndCode sorts regions by their name and code. 18 | func RegionsByNameAndCode(regions []fly.Region) { 19 | sort.Slice(regions, func(i, j int) bool { 20 | return regions[i].Name < regions[j].Name && 21 | regions[i].Code < regions[j].Code 22 | }) 23 | } 24 | 25 | // VMSizesBySize sorts VM sizes by their name. 26 | func VMSizesBySize(sizes []fly.VMSize) { 27 | sort.Slice(sizes, func(i, j int) bool { 28 | return sizes[i].CPUCores < sizes[j].CPUCores 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /internal/state/state_test.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestStrings(t *testing.T) { 12 | cases := map[string]struct { 13 | getter func(context.Context) string 14 | setter func(context.Context, string) context.Context 15 | }{ 16 | "Hostname": {Hostname, WithHostname}, 17 | "WorkingDirectory": {WorkingDirectory, WithWorkingDirectory}, 18 | "ConfigDirectory": {ConfigDirectory, WithConfigDirectory}, 19 | } 20 | 21 | for name := range cases { 22 | kase := cases[name] 23 | 24 | t.Run(fmt.Sprintf("%sPanics", name), func(t *testing.T) { 25 | assert.Panics(t, func() { _ = kase.getter(context.Background()) }) 26 | }) 27 | 28 | t.Run(fmt.Sprint(name), func(t *testing.T) { 29 | const exp = "expectation" 30 | 31 | ctx := kase.setter(context.Background(), exp) 32 | assert.Equal(t, exp, kase.getter(ctx)) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/statuslogger/interface.go: -------------------------------------------------------------------------------- 1 | package statuslogger 2 | 3 | type ResumeFn func() 4 | 5 | type StatusLogger interface { 6 | // Destroy destroys the logger. 7 | // If clear is true, it will remove the status lines from the terminal. 8 | // Otherwise, it will leave them in place with a clear divider. 9 | Destroy(clear bool) 10 | // Line returns a StatusLine for the given line number. 11 | Line(idx int) StatusLine 12 | // Pause clears the status lines and prevents redraw until the returned resume function is called. 13 | // This allows you to write multiple lines to the terminal without overlapping the status area. 14 | Pause() ResumeFn 15 | } 16 | 17 | type StatusLine interface { 18 | Log(s string) 19 | Logf(format string, args ...interface{}) 20 | LogStatus(s Status, str string) 21 | LogfStatus(s Status, format string, args ...interface{}) 22 | Failed(e error) 23 | // Private because it won't redraw on non-interactive loggers. 24 | // For outside use, use LogStatus or LogfStatus. 25 | setStatus(s Status) 26 | } 27 | -------------------------------------------------------------------------------- /internal/statuslogger/shared.go: -------------------------------------------------------------------------------- 1 | package statuslogger 2 | 3 | import "fmt" 4 | 5 | type Status int 6 | 7 | const ( 8 | StatusNone Status = iota 9 | StatusRunning 10 | StatusSuccess 11 | StatusFailure 12 | ) 13 | 14 | const ( 15 | glyphNone = " " 16 | glyphRunning = ">" 17 | glyphSuccess = "✔" 18 | glyphFailure = "✖" 19 | ) 20 | 21 | var glyphsRunning = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} 22 | 23 | func (status Status) charFor(frame int) string { 24 | switch status { 25 | case StatusNone: 26 | return glyphNone 27 | case StatusRunning: 28 | if frame == -1 { 29 | return glyphRunning 30 | } 31 | return glyphsRunning[frame] 32 | case StatusSuccess: 33 | return glyphSuccess 34 | case StatusFailure: 35 | return glyphFailure 36 | default: 37 | return "?" 38 | } 39 | } 40 | 41 | func formatIndex(n, total int) string { 42 | pad := 0 43 | for i := total; i != 0; i /= 10 { 44 | pad++ 45 | } 46 | return fmt.Sprintf("[%0*d/%d]", pad, n+1, total) 47 | } 48 | -------------------------------------------------------------------------------- /internal/task/task_test.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFromContextPanics(t *testing.T) { 11 | assert.Panics(t, func() { _ = FromContext(context.Background()) }) 12 | } 13 | 14 | func TestClient(t *testing.T) { 15 | exp := new(manager) 16 | 17 | ctx := WithContext(context.Background(), exp) 18 | assert.Same(t, exp, FromContext(ctx)) 19 | } 20 | -------------------------------------------------------------------------------- /internal/tracing/transport.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.opentelemetry.io/otel" 7 | "go.opentelemetry.io/otel/propagation" 8 | ) 9 | 10 | func NewTransport(inner http.RoundTripper) http.RoundTripper { 11 | return &InstrumentedTransport{ 12 | inner: inner, 13 | } 14 | } 15 | 16 | type InstrumentedTransport struct { 17 | inner http.RoundTripper 18 | } 19 | 20 | func (t *InstrumentedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 21 | gp := otel.GetTextMapPropagator() 22 | req = req.Clone(req.Context()) 23 | 24 | gp.Inject(req.Context(), propagation.HeaderCarrier(req.Header)) 25 | 26 | resp, err := t.inner.RoundTrip(req) 27 | if err != nil { 28 | return resp, err 29 | } 30 | 31 | return resp, err 32 | } 33 | -------------------------------------------------------------------------------- /internal/uiexutil/client.go: -------------------------------------------------------------------------------- 1 | package uiexutil 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/superfly/flyctl/internal/uiex" 7 | ) 8 | 9 | type Client interface { 10 | ListManagedClusters(ctx context.Context, orgSlug string) (uiex.ListManagedClustersResponse, error) 11 | GetManagedCluster(ctx context.Context, orgSlug string, id string) (uiex.GetManagedClusterResponse, error) 12 | GetManagedClusterById(ctx context.Context, id string) (uiex.GetManagedClusterResponse, error) 13 | CreateUser(ctx context.Context, id string, input uiex.CreateUserInput) (uiex.CreateUserResponse, error) 14 | CreateCluster(ctx context.Context, input uiex.CreateClusterInput) (uiex.CreateClusterResponse, error) 15 | } 16 | 17 | type contextKey struct{} 18 | 19 | var clientContextKey = &contextKey{} 20 | 21 | // NewContextWithClient derives a Context that carries c from ctx. 22 | func NewContextWithClient(ctx context.Context, c Client) context.Context { 23 | return context.WithValue(ctx, clientContextKey, c) 24 | } 25 | 26 | // ClientFromContext returns the Client ctx carries. 27 | func ClientFromContext(ctx context.Context) Client { 28 | c, _ := ctx.Value(clientContextKey).(Client) 29 | return c 30 | } 31 | -------------------------------------------------------------------------------- /internal/uiexutil/uiexutil.go: -------------------------------------------------------------------------------- 1 | package uiexutil 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/superfly/flyctl/internal/config" 7 | "github.com/superfly/flyctl/internal/logger" 8 | "github.com/superfly/flyctl/internal/uiex" 9 | ) 10 | 11 | func NewClientWithOptions(ctx context.Context, opts uiex.NewClientOpts) (*uiex.Client, error) { 12 | if opts.Tokens == nil { 13 | opts.Tokens = config.Tokens(ctx) 14 | } 15 | 16 | if v := logger.MaybeFromContext(ctx); v != nil && opts.Logger == nil { 17 | opts.Logger = v 18 | } 19 | return uiex.NewWithOptions(ctx, opts) 20 | } 21 | -------------------------------------------------------------------------------- /internal/update/memoize.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import "sync" 4 | 5 | type memoize[T any] struct { 6 | val T 7 | err error 8 | done bool 9 | 10 | lock sync.Mutex 11 | } 12 | 13 | func (m *memoize[T]) Get(fn func() (T, error)) (T, error) { 14 | m.lock.Lock() 15 | defer m.lock.Unlock() 16 | 17 | if m.done { 18 | return m.val, m.err 19 | } 20 | 21 | m.val, m.err = fn() 22 | m.done = true 23 | 24 | return m.val, m.err 25 | } 26 | -------------------------------------------------------------------------------- /iostreams/context.go: -------------------------------------------------------------------------------- 1 | package iostreams 2 | 3 | import "context" 4 | 5 | type contextKey struct{} 6 | 7 | // NewContext derives a context that carries io from ctx. 8 | func NewContext(ctx context.Context, io *IOStreams) context.Context { 9 | return context.WithValue(ctx, contextKey{}, io) 10 | } 11 | 12 | // FromContext returns the IOStreams ctx carries. It panics in case ctx carries 13 | // no IOStreams. 14 | func FromContext(ctx context.Context) *IOStreams { 15 | return ctx.Value(contextKey{}).(*IOStreams) 16 | } 17 | -------------------------------------------------------------------------------- /iostreams/context_test.go: -------------------------------------------------------------------------------- 1 | package iostreams 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFromContextPanics(t *testing.T) { 11 | assert.Panics(t, func() { _ = FromContext(context.Background()) }) 12 | } 13 | 14 | func TestNewContext(t *testing.T) { 15 | exp := new(IOStreams) 16 | 17 | ctx := NewContext(context.Background(), exp) 18 | assert.Same(t, exp, FromContext(ctx)) 19 | } 20 | -------------------------------------------------------------------------------- /ip/ip.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | func IsV6(addr string) bool { 9 | addr = strings.Trim(addr, "[]") 10 | ip := net.ParseIP(addr) 11 | return ip != nil && ip.To16() != nil 12 | } 13 | -------------------------------------------------------------------------------- /logs/entry.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | type LogEntry struct { 4 | Level string `json:"level"` 5 | Instance string `json:"instance"` 6 | Message string `json:"message"` 7 | Region string `json:"region"` 8 | Timestamp string `json:"timestamp"` 9 | Meta Meta `json:"meta"` 10 | } 11 | 12 | type Meta struct { 13 | Instance string 14 | Region string 15 | Event struct { 16 | Provider string 17 | } 18 | HTTP struct { 19 | Request struct { 20 | ID string 21 | Method string 22 | Version string 23 | } 24 | Response struct { 25 | StatusCode int `json:"status_code"` 26 | } 27 | } 28 | Error struct { 29 | Code int 30 | Message string 31 | } 32 | URL struct { 33 | Full string 34 | } 35 | } 36 | 37 | type natsLog struct { 38 | Event struct { 39 | Provider string `json:"provider"` 40 | } `json:"event"` 41 | Fly struct { 42 | App struct { 43 | Instance string `json:"instance"` 44 | Name string `json:"name"` 45 | } `json:"app"` 46 | Region string `json:"region"` 47 | } `json:"fly"` 48 | Host string `json:"host"` 49 | Log struct { 50 | Level string `json:"level"` 51 | } `json:"log"` 52 | Message string `json:"message"` 53 | Timestamp string `json:"timestamp"` 54 | } 55 | -------------------------------------------------------------------------------- /logs/logs.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | fly "github.com/superfly/fly-go" 10 | "github.com/superfly/flyctl/internal/wireguard" 11 | ) 12 | 13 | type LogOptions struct { 14 | W io.Writer 15 | 16 | MaxBackoff time.Duration 17 | AppName string 18 | VMID string 19 | RegionCode string 20 | NoTail bool 21 | } 22 | 23 | type WebClient interface { 24 | GetAppBasic(ctx context.Context, appName string) (*fly.AppBasic, error) 25 | GetAppLogs(ctx context.Context, appName, token, region, instanceID string) (entries []fly.LogEntry, nextToken string, err error) 26 | wireguard.WebClient 27 | } 28 | 29 | func (opts *LogOptions) toNatsSubject() (subject string) { 30 | subject = fmt.Sprintf("logs.%s", opts.AppName) 31 | 32 | add := func(what string) { 33 | if what == "" { 34 | what = "*" 35 | } 36 | 37 | subject = fmt.Sprintf("%s.%s", subject, what) 38 | } 39 | 40 | add(opts.RegionCode) 41 | add(opts.VMID) 42 | 43 | return 44 | } 45 | 46 | type LogStream interface { 47 | Err() error 48 | Stream(ctx context.Context, opts *LogOptions) <-chan LogEntry 49 | } 50 | -------------------------------------------------------------------------------- /scanner/bridgetown.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import "github.com/pkg/errors" 4 | 5 | func configureBridgetown(sourceDir string, _ *ScannerConfig) (*SourceInfo, error) { 6 | if !checksPass(sourceDir, dirContains("Gemfile", "bridgetown")) { 7 | return nil, nil 8 | } 9 | 10 | s := &SourceInfo{ 11 | Family: "Bridgetown", 12 | Port: 4000, 13 | Statics: []Static{ 14 | { 15 | GuestPath: "/app/output", 16 | UrlPrefix: "/", 17 | }, 18 | }, 19 | } 20 | 21 | rubyVersion, err := extractRubyVersion("Gemfile.lock", "Gemfile", ".ruby_version") 22 | if err != nil { 23 | return nil, errors.Wrap(err, "failure extracting Ruby version") 24 | } 25 | 26 | vars := make(map[string]interface{}) 27 | vars["rubyVersion"] = rubyVersion 28 | s.Files = templatesExecute("templates/bridgetown", vars) 29 | 30 | return s, nil 31 | } 32 | -------------------------------------------------------------------------------- /scanner/deno.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | func configureDeno(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { 4 | if !checksPass( 5 | sourceDir, 6 | // default config files: https://deno.land/manual@v1.35.2/getting_started/configuration_file 7 | fileExists("deno.json", "deno.jsonc"), 8 | // deno.land and denopkg.com imports 9 | dirContains("*.ts", "\"https?://deno\\.land/.*\"", "\"https?://denopkg\\.com/.*\""), 10 | ) { 11 | return nil, nil 12 | } 13 | 14 | s := &SourceInfo{ 15 | Files: templates("templates/deno"), 16 | Family: "Deno", 17 | Port: 8080, 18 | Processes: map[string]string{ 19 | "app": "run --allow-net ./example.ts", 20 | }, 21 | Env: map[string]string{ 22 | "PORT": "8080", 23 | }, 24 | } 25 | 26 | return s, nil 27 | } 28 | -------------------------------------------------------------------------------- /scanner/elixir.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/superfly/flyctl/helpers" 7 | ) 8 | 9 | func configureElixir(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { 10 | if !helpers.FileExists(filepath.Join(sourceDir, "mix.exs")) { 11 | return nil, nil 12 | } 13 | 14 | s := &SourceInfo{ 15 | Builder: "heroku/buildpacks:20", 16 | Buildpacks: []string{"https://cnb-shim.herokuapp.com/v1/hashnuke/elixir"}, 17 | Family: "Elixir", 18 | Port: 8080, 19 | Env: map[string]string{ 20 | "PORT": "8080", 21 | }, 22 | } 23 | 24 | return s, nil 25 | } 26 | -------------------------------------------------------------------------------- /scanner/flask.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import "fmt" 4 | 5 | func configureFlask(sourceDir string, _ *ScannerConfig) (*SourceInfo, error) { 6 | // require "Flask" to be in requirements.txt 7 | if !checksPass(sourceDir, dirContains("requirements.txt", "Flask")) { 8 | return nil, nil 9 | } 10 | 11 | // require "app.py" or "wsgi.py" to be in the root directory 12 | if !checksPass(sourceDir, fileExists("app.py", "wsgi.py")) { 13 | return nil, nil 14 | } 15 | 16 | vars := make(map[string]interface{}) 17 | 18 | // Extract Python version 19 | // TODO: support pinned versions 20 | pythonFullVersion, _, err := extractPythonVersion() 21 | if err != nil { 22 | return nil, err 23 | } else if pythonFullVersion == "" { 24 | return nil, fmt.Errorf("could not find Python version") 25 | } 26 | vars["pythonVersion"] = pythonFullVersion 27 | 28 | // Generate a simple Dockerfile 29 | s := &SourceInfo{ 30 | Files: templatesExecute("templates/flask", vars), 31 | Family: "Flask", 32 | Port: 8080, 33 | SkipDeploy: true, 34 | DeployDocs: `We have generated a simple Dockerfile for you. Modify it to fit your needs and run "fly deploy" to deploy your application.`, 35 | } 36 | 37 | return s, nil 38 | } 39 | -------------------------------------------------------------------------------- /scanner/github.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | func github_actions(sourceDir string, actions *GitHubActionsStruct) { 9 | // first check to see if the source directory is a git repo that uses github, 10 | // if so, set secrets and deploy to true 11 | actions.Secrets = checksPass(sourceDir+"/.git", dirContains("config", "github")) 12 | actions.Deploy = actions.Secrets 13 | 14 | // See if the source directory is set up to use github actions, if so set deploy to true 15 | if !actions.Deploy { 16 | info, err := os.Stat(path.Join(sourceDir, ".github")) 17 | actions.Deploy = (err == nil && info.IsDir()) 18 | } 19 | 20 | // if deploy is true, add the github actions templates to the files list 21 | if actions.Deploy { 22 | actions.Files = templates("templates/github") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /scanner/gitignore.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "io/fs" 5 | "path/filepath" 6 | ) 7 | 8 | func FindGitignores(root string) []string { 9 | gitignores := make([]string, 0) 10 | filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { 11 | if err != nil { 12 | return err 13 | } 14 | if info.IsDir() { 15 | return nil 16 | } 17 | if m, err := filepath.Match(".gitignore", filepath.Base(path)); err != nil { 18 | return err 19 | } else if m { 20 | gitignores = append(gitignores, path) 21 | } 22 | return nil 23 | }) 24 | return gitignores 25 | } 26 | -------------------------------------------------------------------------------- /scanner/nextjs.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | func configureNextJs(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { 4 | if !checksPass(sourceDir, fileExists("next.config.js")) && !checksPass(sourceDir, dirContains("package.json", "\"next\"")) { 5 | return nil, nil 6 | } 7 | 8 | env := map[string]string{ 9 | "PORT": "8080", 10 | } 11 | 12 | s := &SourceInfo{ 13 | Family: "NextJS", 14 | Port: 8080, 15 | SkipDatabase: true, 16 | Env: env, 17 | } 18 | 19 | s.Files = templates("templates/nextjs") 20 | 21 | s.BuildArgs = map[string]string{ 22 | "NEXT_PUBLIC_EXAMPLE": "Value goes here", 23 | } 24 | 25 | return s, nil 26 | } 27 | -------------------------------------------------------------------------------- /scanner/nuxtjs.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | func configureNuxt(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { 4 | if !checksPass(sourceDir, fileExists("nuxt.config.ts")) { 5 | return nil, nil 6 | } 7 | 8 | env := map[string]string{ 9 | "PORT": "8080", 10 | } 11 | 12 | s := &SourceInfo{ 13 | Family: "NuxtJS", 14 | Port: 8080, 15 | SkipDatabase: true, 16 | Env: env, 17 | } 18 | 19 | s.Files = templates("templates/nuxtjs") 20 | 21 | return s, nil 22 | } 23 | -------------------------------------------------------------------------------- /scanner/redwood.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | func configureRedwood(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { 4 | if !checksPass(sourceDir, fileExists("redwood.toml")) { 5 | return nil, nil 6 | } 7 | 8 | s := &SourceInfo{ 9 | Family: "RedwoodJS", 10 | Files: templates("templates/redwood"), 11 | Port: 8910, 12 | ReleaseCmd: ".fly/release.sh", 13 | } 14 | 15 | s.Env = map[string]string{ 16 | "PORT": "8910", 17 | // Telemetry gravely incrases memory usage, and isn't required 18 | "REDWOOD_DISABLE_TELEMETRY": "1", 19 | } 20 | 21 | if checksPass(sourceDir+"/api/db", dirContains("*.prisma", "sqlite")) { 22 | s.Env["MIGRATE_ON_BOOT"] = "true" 23 | s.Env["DATABASE_URL"] = "file://data/sqlite.db" 24 | s.Volumes = []Volume{ 25 | { 26 | Source: "data", 27 | Destination: "/data", 28 | }, 29 | } 30 | s.Notice = "\nThis deployment will run an SQLite on a single dedicated volume. The app can't scale beyond a single instance. Look into 'fly postgres' for a more robust production database that supports scaling up. \n" 31 | } 32 | 33 | return s, nil 34 | } 35 | -------------------------------------------------------------------------------- /scanner/rust.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | func configureRust(sourceDir string, _ *ScannerConfig) (*SourceInfo, error) { 4 | if !checksPass(sourceDir, fileExists("Cargo.toml", "Cargo.lock")) { 5 | return nil, nil 6 | } 7 | 8 | cargoData, err := readTomlFile("Cargo.toml") 9 | if err != nil { 10 | return nil, err 11 | } 12 | 13 | deps := cargoData["dependencies"].(map[string]interface{}) 14 | family := "Rust" 15 | env := map[string]string{ 16 | "PORT": "8080", 17 | } 18 | 19 | if _, ok := deps["rocket"]; ok { 20 | family = "Rocket" 21 | env["ROCKET_PORT"] = "8080" 22 | env["ROCKET_ADDRESS"] = "0.0.0.0" 23 | } else if _, ok := deps["actix-web"]; ok { 24 | family = "Actix Web" 25 | } else if _, ok := deps["warp"]; ok { 26 | family = "Warp" 27 | } else if _, ok := deps["axum"]; ok { 28 | family = "Axum" 29 | } else if _, ok := deps["poem"]; ok { 30 | family = "Poem" 31 | } 32 | 33 | vars := make(map[string]interface{}) 34 | vars["appName"] = cargoData["package"].(map[string]interface{})["name"].(string) 35 | 36 | s := &SourceInfo{ 37 | Files: templatesExecute("templates/rust", vars), 38 | Family: family, 39 | Port: 8080, 40 | Env: env, 41 | SkipDatabase: true, 42 | } 43 | return s, nil 44 | } 45 | -------------------------------------------------------------------------------- /scanner/static.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/superfly/flyctl/helpers" 7 | ) 8 | 9 | func configureStatic(sourceDir string, config *ScannerConfig) (*SourceInfo, error) { 10 | // No index.html detected, move on 11 | if !helpers.FileExists(filepath.Join(sourceDir, "index.html")) { 12 | return nil, nil 13 | } 14 | 15 | s := &SourceInfo{ 16 | Family: "Static", 17 | Port: 8080, 18 | Files: templates("templates/static"), 19 | } 20 | 21 | return s, nil 22 | } 23 | -------------------------------------------------------------------------------- /scanner/templates/bridgetown/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION={{ .rubyVersion }} 2 | FROM ruby:$RUBY_VERSION-slim as base 3 | 4 | ENV VOLTA_HOME=/usr/local 5 | 6 | RUN apt-get update &&\ 7 | apt-get install --yes build-essential git curl 8 | 9 | RUN curl https://get.volta.sh | bash &&\ 10 | volta install node@lts yarn@latest 11 | 12 | WORKDIR /app 13 | 14 | FROM base as gems 15 | COPY Gemfile* . 16 | RUN bundle install 17 | 18 | FROM base 19 | COPY . . 20 | COPY --from=base $VOLTA_HOME/bin $VOLTA_HOME/bin 21 | COPY --from=base $VOLTA_HOME/tools $VOLTA_HOME/tools 22 | COPY --from=base /app /app 23 | COPY --from=gems /usr/local/bundle /usr/local/bundle 24 | 25 | RUN yarn install 26 | RUN bundle exec bridgetown frontend:build 27 | 28 | EXPOSE 4000 29 | CMD bundle exec bridgetown start 30 | -------------------------------------------------------------------------------- /scanner/templates/deno/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | -------------------------------------------------------------------------------- /scanner/templates/deno/Dockerfile: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/denoland/deno_docker/blob/main/alpine.dockerfile 2 | 3 | ARG DENO_VERSION=1.14.0 4 | ARG BIN_IMAGE=denoland/deno:bin-${DENO_VERSION} 5 | FROM ${BIN_IMAGE} AS bin 6 | 7 | FROM frolvlad/alpine-glibc:alpine-3.13 8 | 9 | RUN apk --no-cache add ca-certificates 10 | 11 | RUN addgroup --gid 1000 deno \ 12 | && adduser --uid 1000 --disabled-password deno --ingroup deno \ 13 | && mkdir /deno-dir/ \ 14 | && chown deno:deno /deno-dir/ 15 | 16 | ENV DENO_DIR /deno-dir/ 17 | ENV DENO_INSTALL_ROOT /usr/local 18 | 19 | ARG DENO_VERSION 20 | ENV DENO_VERSION=${DENO_VERSION} 21 | COPY --from=bin /deno /bin/deno 22 | 23 | WORKDIR /deno-dir 24 | COPY . . 25 | 26 | ENTRYPOINT ["/bin/deno"] 27 | CMD ["run", "--allow-net", "https://deno.land/std/examples/echo_server.ts"] 28 | -------------------------------------------------------------------------------- /scanner/templates/deno/example.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | get, 4 | post, 5 | redirect, 6 | contentType, 7 | } from "https://denopkg.com/syumai/dinatra/mod.ts"; 8 | 9 | const greeting = "

Hello From Deno on Fly!

"; 10 | 11 | app( 12 | get("/", () => greeting), 13 | get("/:id", ({ params }) => greeting + `
and hello to ${params.id}`), 14 | ); 15 | -------------------------------------------------------------------------------- /scanner/templates/django/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | .git/ 3 | *.sqlite3 4 | {{ if .venv -}} 5 | {{ .venvdir }} 6 | {{- end -}} 7 | -------------------------------------------------------------------------------- /scanner/templates/dotnet/.dockerignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/bin/ 3 | **/obj/ 4 | **/out/ 5 | 6 | # files 7 | fly.toml 8 | Dockerfile* 9 | **/*.md 10 | -------------------------------------------------------------------------------- /scanner/templates/dotnet/Dockerfile: -------------------------------------------------------------------------------- 1 | # Adjust DOTNET_OS_VERSION as desired 2 | ARG DOTNET_OS_VERSION="-alpine" 3 | ARG DOTNET_SDK_VERSION={{ .dotnetSdkVersion }} 4 | 5 | FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_SDK_VERSION}${DOTNET_OS_VERSION} AS build 6 | WORKDIR /src 7 | 8 | # copy everything 9 | COPY . ./ 10 | # restore as distinct layers 11 | RUN dotnet restore 12 | # build and publish a release 13 | RUN dotnet publish -c Release -o /app 14 | 15 | # final stage/image 16 | FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_SDK_VERSION} 17 | ENV ASPNETCORE_URLS http://+:8080 18 | ENV ASPNETCORE_ENVIRONMENT Production 19 | EXPOSE 8080 20 | WORKDIR /app 21 | COPY --from=build /app . 22 | ENTRYPOINT [ "dotnet", "{{ .dotnetAppName }}.dll" ] 23 | -------------------------------------------------------------------------------- /scanner/templates/flask/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG PYTHON_VERSION={{ .pythonVersion }} 4 | 5 | FROM python:${PYTHON_VERSION}-slim 6 | 7 | LABEL fly_launch_runtime="flask" 8 | 9 | WORKDIR /code 10 | 11 | COPY requirements.txt requirements.txt 12 | RUN pip3 install -r requirements.txt 13 | 14 | COPY . . 15 | 16 | EXPOSE 8080 17 | 18 | CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0", "--port=8080"] 19 | -------------------------------------------------------------------------------- /scanner/templates/github/.github/workflows/fly-deploy.yml: -------------------------------------------------------------------------------- 1 | # See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ 2 | 3 | name: Fly Deploy 4 | on: 5 | push: 6 | branches: 7 | - main 8 | jobs: 9 | deploy: 10 | name: Deploy app 11 | runs-on: ubuntu-latest 12 | concurrency: deploy-group # optional: ensure only one action runs at a time 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: superfly/flyctl-actions/setup-flyctl@master 16 | - run: flyctl deploy --remote-only 17 | env: 18 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 19 | -------------------------------------------------------------------------------- /scanner/templates/go/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1 2 | FROM golang:${GO_VERSION}-bookworm as builder 3 | 4 | WORKDIR /usr/src/app 5 | COPY go.mod go.sum ./ 6 | RUN go mod download && go mod verify 7 | COPY . . 8 | RUN go build -v -o /run-app . 9 | 10 | 11 | FROM debian:bookworm 12 | 13 | COPY --from=builder /run-app /usr/local/bin/ 14 | CMD ["run-app"] 15 | -------------------------------------------------------------------------------- /scanner/templates/laravel/.dockerignore: -------------------------------------------------------------------------------- 1 | # excludes from the docker image/build 2 | 3 | # 1. Ignore Laravel-specific files we don't need 4 | bootstrap/cache/* 5 | storage/framework/cache/* 6 | storage/framework/sessions/* 7 | storage/framework/views/* 8 | storage/logs/* 9 | *.env* 10 | .rr.yml 11 | rr 12 | frankenphp 13 | vendor 14 | 15 | # 2. Ignore common files/directories we don't need 16 | fly.toml 17 | .vscode 18 | .idea 19 | **/*node_modules 20 | **.git 21 | **.gitignore 22 | **.gitattributes 23 | **.sass-cache 24 | **/*~ 25 | **/*.log 26 | **/.DS_Store 27 | **/Thumbs.db 28 | public/hot 29 | -------------------------------------------------------------------------------- /scanner/templates/laravel/.fly/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Run user scripts, if they exist 4 | for f in /var/www/html/.fly/scripts/*.sh; do 5 | # Bail out this loop if any script exits with non-zero status code 6 | bash "$f" || break 7 | done 8 | chown -R www-data:www-data /var/www/html 9 | 10 | if [ $# -gt 0 ]; then 11 | # If we passed a command, run it as root 12 | exec "$@" 13 | else 14 | exec supervisord -c /etc/supervisor/supervisord.conf 15 | fi 16 | -------------------------------------------------------------------------------- /scanner/templates/laravel/.fly/scripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/scanner/templates/laravel/.fly/scripts/.gitkeep -------------------------------------------------------------------------------- /scanner/templates/laravel/.fly/scripts/caches.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | /usr/bin/php /var/www/html/artisan config:cache --no-ansi -q 4 | /usr/bin/php /var/www/html/artisan route:cache --no-ansi -q 5 | /usr/bin/php /var/www/html/artisan view:cache --no-ansi -q 6 | -------------------------------------------------------------------------------- /scanner/templates/lucky/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | -------------------------------------------------------------------------------- /scanner/templates/nextjs/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | Dockerfile 3 | .dockerignore 4 | node_modules 5 | npm-debug.log 6 | README.md 7 | .next 8 | .git 9 | -------------------------------------------------------------------------------- /scanner/templates/nextjs/Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:16-alpine AS builder 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN apk add --no-cache libc6-compat 5 | WORKDIR /app 6 | COPY . . 7 | RUN yarn install --frozen-lockfile 8 | 9 | # If using npm with a `package-lock.json` comment out above and use below instead 10 | # RUN npm ci 11 | 12 | ENV NEXT_TELEMETRY_DISABLED 1 13 | 14 | # Add `ARG` instructions below if you need `NEXT_PUBLIC_` variables 15 | # then put the value on your fly.toml 16 | # Example: 17 | # ARG NEXT_PUBLIC_EXAMPLE="value here" 18 | 19 | RUN yarn build 20 | 21 | # If using npm comment out above and use below instead 22 | # RUN npm run build 23 | 24 | # Production image, copy all the files and run next 25 | FROM node:16-alpine AS runner 26 | WORKDIR /app 27 | 28 | ENV NODE_ENV production 29 | ENV NEXT_TELEMETRY_DISABLED 1 30 | 31 | RUN addgroup --system --gid 1001 nodejs 32 | RUN adduser --system --uid 1001 nextjs 33 | 34 | COPY --from=builder /app ./ 35 | 36 | USER nextjs 37 | 38 | CMD ["yarn", "start"] 39 | 40 | # If using npm comment out above and use below instead 41 | # CMD ["npm", "run", "start"] 42 | -------------------------------------------------------------------------------- /scanner/templates/node/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | Dockerfile 3 | .dockerignore 4 | node_modules 5 | .git 6 | {{ if .remix -}} 7 | *.log 8 | .DS_Store 9 | .env 10 | /.cache 11 | /public/build 12 | /build 13 | {{ end -}} 14 | -------------------------------------------------------------------------------- /scanner/templates/node/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # If running the web server then migrate existing database 4 | if [ "${*}" == "{{ .packager }} run start" ]; then 5 | npx prisma migrate deploy 6 | fi 7 | 8 | exec "${@}" 9 | -------------------------------------------------------------------------------- /scanner/templates/nuxtjs/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | node_modules 3 | .nuxt 4 | -------------------------------------------------------------------------------- /scanner/templates/nuxtjs/Dockerfile: -------------------------------------------------------------------------------- 1 | # Source: https://nuxtjs.org/deployments/koyeb#dockerize-your-application 2 | FROM node:lts as builder 3 | 4 | WORKDIR /app 5 | 6 | COPY . . 7 | 8 | RUN yarn install \ 9 | --prefer-offline \ 10 | --frozen-lockfile \ 11 | --non-interactive \ 12 | --production=false 13 | 14 | RUN yarn build 15 | 16 | FROM node:lts 17 | 18 | WORKDIR /app 19 | 20 | COPY --from=builder /app . 21 | 22 | ENV HOST 0.0.0.0 23 | ENV PORT 8080 24 | 25 | # Source: https://nuxt.com/docs/getting-started/deployment#entry-point 26 | CMD ["node", ".output/server/index.mjs"] 27 | -------------------------------------------------------------------------------- /scanner/templates/phoenix/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | assets/node_modules/ 3 | deps/ 4 | -------------------------------------------------------------------------------- /scanner/templates/python-docker/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | .git/ 3 | __pycache__/ 4 | .envrc 5 | .venv/ 6 | -------------------------------------------------------------------------------- /scanner/templates/python-docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:{{ .pyVersion }} AS builder 2 | 3 | ENV PYTHONUNBUFFERED=1 \ 4 | PYTHONDONTWRITEBYTECODE=1 5 | WORKDIR /app 6 | 7 | {{ if .pipenv -}} 8 | ENV PIPENV_VENV_IN_PROJECT=1 \ 9 | PIPENV_CUSTOM_VENV_NAME=.venv 10 | RUN pip install pipenv 11 | COPY Pipfile Pipfile.lock ./ 12 | RUN pipenv install 13 | {{ else if .poetry -}} 14 | RUN pip install poetry 15 | RUN poetry config virtualenvs.in-project true 16 | COPY pyproject.toml poetry.lock ./ 17 | RUN poetry install 18 | {{ else if .pep621 -}} 19 | RUN python -m venv .venv 20 | COPY pyproject.toml ./ 21 | RUN .venv/bin/pip install . 22 | {{ else if .pip }} 23 | RUN python -m venv .venv 24 | COPY requirements.txt ./ 25 | RUN .venv/bin/pip install -r requirements.txt 26 | {{ end -}} 27 | 28 | FROM python:{{ .pyVersion }}-slim 29 | WORKDIR /app 30 | COPY --from=builder /app/.venv .venv/ 31 | COPY . . 32 | {{ if .flask -}} 33 | CMD ["/app/.venv/bin/flask", "run", "--host=0.0.0.0", "--port=8080"] 34 | {{ else if .fastapi -}} 35 | CMD ["/app/.venv/bin/fastapi", "run"] 36 | {{ else if .streamlit -}} 37 | CMD ["/app/.venv/bin/streamlit", "run", "{{ .entrypoint }}"] 38 | {{ end -}} 39 | -------------------------------------------------------------------------------- /scanner/templates/python/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | .venv 3 | -------------------------------------------------------------------------------- /scanner/templates/python/Procfile: -------------------------------------------------------------------------------- 1 | # TODO: Modify this Procfile to fit your needs 2 | web: gunicorn app:app 3 | -------------------------------------------------------------------------------- /scanner/templates/redwood/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | node_modules 3 | -------------------------------------------------------------------------------- /scanner/templates/redwood/.fly/migrate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | # This command pushes us over 256MB of RAM at release time 6 | # yarn rw prisma migrate deploy 7 | 8 | # This alternative command uses less memory 9 | npx prisma migrate deploy --schema /app/api/db/schema.prisma 10 | -------------------------------------------------------------------------------- /scanner/templates/redwood/.fly/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | if [ ! -n $MIGRATE_ON_BOOT ]; then 6 | $(dirname $0)/migrate.sh 7 | fi 8 | -------------------------------------------------------------------------------- /scanner/templates/redwood/.fly/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | if [ -n $MIGRATE_ON_BOOT ]; then 6 | $(dirname $0)/migrate.sh 7 | fi 8 | 9 | npx rw-server --port ${PORT} $@ 10 | -------------------------------------------------------------------------------- /scanner/templates/redwood/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=node:16.13.0-alpine 2 | FROM ${BASE_IMAGE} as base 3 | 4 | RUN mkdir /app 5 | WORKDIR /app 6 | 7 | # Required for building the api and web distributions 8 | ENV NODE_ENV development 9 | 10 | FROM base as dependencies 11 | 12 | COPY .yarn .yarn 13 | COPY .yarnrc.yml .yarnrc.yml 14 | COPY package.json package.json 15 | COPY web/package.json web/package.json 16 | COPY api/package.json api/package.json 17 | COPY yarn.lock yarn.lock 18 | 19 | RUN --mount=type=cache,target=/root/.yarn/berry/cache \ 20 | --mount=type=cache,target=/root/.cache yarn install --immutable 21 | 22 | COPY redwood.toml . 23 | COPY graphql.config.js . 24 | 25 | FROM dependencies as web_build 26 | 27 | COPY web web 28 | RUN yarn rw build web 29 | 30 | FROM dependencies as api_build 31 | 32 | COPY api api 33 | RUN yarn rw build api 34 | 35 | FROM dependencies 36 | 37 | ENV NODE_ENV production 38 | 39 | COPY --from=web_build /app/web/dist /app/web/dist 40 | COPY --from=api_build /app/api /app/api 41 | COPY --from=api_build /app/node_modules/.prisma /app/node_modules/.prisma 42 | 43 | COPY .fly .fly 44 | 45 | ENTRYPOINT ["sh"] 46 | CMD [".fly/start.sh"] 47 | -------------------------------------------------------------------------------- /scanner/templates/ruby/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION={{ .rubyVersion }} 2 | FROM ruby:$RUBY_VERSION-slim as base 3 | 4 | # Rack app lives here 5 | WORKDIR /app 6 | 7 | # Update gems and bundler 8 | RUN gem update --system --no-document && \ 9 | gem install -N bundler 10 | 11 | 12 | # Throw-away build stage to reduce size of final image 13 | FROM base as build 14 | 15 | # Install packages needed to build gems 16 | RUN apt-get update -qq && \ 17 | apt-get install --no-install-recommends -y build-essential 18 | 19 | # Install application gems 20 | COPY Gemfile* . 21 | RUN bundle install 22 | 23 | 24 | # Final stage for app image 25 | FROM base 26 | 27 | # Run and own the application files as a non-root user for security 28 | RUN useradd ruby --home /app --shell /bin/bash 29 | USER ruby:ruby 30 | 31 | # Copy built artifacts: gems, application 32 | COPY --from=build /usr/local/bundle /usr/local/bundle 33 | COPY --from=build --chown=ruby:ruby /app /app 34 | 35 | # Copy application code 36 | COPY --chown=ruby:ruby . . 37 | 38 | # Start the server 39 | EXPOSE 8080 40 | CMD ["bundle", "exec", "rackup", "--host", "0.0.0.0", "--port", "8080"] 41 | -------------------------------------------------------------------------------- /scanner/templates/rust/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | .git/ 3 | -------------------------------------------------------------------------------- /scanner/templates/rust/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef 2 | WORKDIR /app 3 | 4 | FROM chef AS planner 5 | COPY . . 6 | RUN cargo chef prepare --recipe-path recipe.json 7 | 8 | FROM chef AS builder 9 | COPY --from=planner /app/recipe.json recipe.json 10 | # Build dependencies - this is the caching Docker layer! 11 | RUN cargo chef cook --release --recipe-path recipe.json 12 | # Build application 13 | COPY . . 14 | RUN cargo build --release --bin {{ .appName }} 15 | 16 | # We do not need the Rust toolchain to run the binary! 17 | FROM debian:bookworm-slim AS runtime 18 | WORKDIR /app 19 | COPY --from=builder /app/target/release/{{ .appName }} /usr/local/bin 20 | ENTRYPOINT ["/usr/local/bin/{{ .appName }}"] 21 | -------------------------------------------------------------------------------- /scanner/templates/static/.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | -------------------------------------------------------------------------------- /scanner/templates/static/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pierrezemb/gostatic 2 | COPY . /srv/http/ 3 | CMD ["-port","8080","-https-promote", "-enable-logging"] 4 | -------------------------------------------------------------------------------- /scripts/build-dfly: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | docker build -t flyctl -f Dockerfile.dev . 6 | -------------------------------------------------------------------------------- /scripts/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | current_tag=$(git -c "versionsort.prereleasesuffix=-pre" tag --points-at HEAD --sort -v:refname | head -n1) 4 | if [ -z "$current_tag" ]; then 5 | current_tag=$(git describe --tags --abbrev=0) 6 | fi 7 | 8 | >&2 echo "current tag: $current_tag" 9 | 10 | # if the current tag is a prerelease, get the previous tag, otherwise get the previous non-prerelease tag 11 | if [[ $current_tag =~ pre ]]; then 12 | previous_tag=$(git describe --match "v[0-9]*" --abbrev=0 HEAD^) 13 | else 14 | previous_tag=$(git describe --match "v[0-9]*" --exclude "*-pre-*" --abbrev=0 HEAD^) 15 | fi 16 | 17 | >&2 echo "previous tag: $previous_tag" 18 | 19 | # only include go files in the changelog 20 | git log --oneline --no-merges --no-decorate $previous_tag..HEAD -- '*.go' '**/*.go' 21 | -------------------------------------------------------------------------------- /scripts/delete_preflight_apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | prefix="${1:-}" 5 | 6 | if [ "${FLY_PREFLIGHT_TEST_FLY_ORG}" = "" ] ; then 7 | echo "error: ensure FLY_PREFLIGHT_TEST_FLY_ORG env var is set" 8 | exit 1 9 | fi 10 | for app in $(flyctl apps list --json | jq -r '.[] | select(.Organization.Slug == "'${FLY_PREFLIGHT_TEST_FLY_ORG}'") | .Name') 11 | do 12 | if [[ -n "$prefix" && ! "$app" =~ ^$prefix ]]; then 13 | continue 14 | fi 15 | echo "Destroy $app" 16 | flyctl apps destroy --yes "${app}" 17 | done 18 | -------------------------------------------------------------------------------- /scripts/dev_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | ORIGIN=${ORIGIN:-origin} 6 | 7 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | previous_version="$("$dir"/../scripts/version.sh -s)" 9 | 10 | if [[ $(git status --porcelain) != "" ]]; then 11 | echo "Error: repo is dirty. Run git status, clean repo and try again." 12 | exit 1 13 | elif [[ $(git status --porcelain -b | grep -e "ahead" -e "behind") != "" ]]; then 14 | echo "Error: repo has unpushed commits. Push commits to remote and try again." 15 | exit 1 16 | fi 17 | 18 | revision=$(git rev-parse --short HEAD) 19 | branch=$(git rev-parse --abbrev-ref HEAD) 20 | version="v${previous_version}-dev-${branch/\//-}-${revision}" 21 | echo "Publishing development release: $version" 22 | 23 | read -p "Are you sure? " -n 1 -r 24 | echo 25 | if [[ $REPLY =~ ^[Yy]$ ]] 26 | then 27 | git tag -m "release ${version}" -a "$version" && git push "${ORIGIN}" tag "$version" 28 | echo "done" 29 | fi 30 | -------------------------------------------------------------------------------- /scripts/dfly: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | docker run --workdir /app -e FLY_API_TOKEN=$(fly auth token) -v $(PWD):/app -it --rm flyctl $@ 5 | -------------------------------------------------------------------------------- /scripts/force_bump_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | ORIGIN=${ORIGIN:-origin} 6 | 7 | bump=${1:-patch} 8 | 9 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 10 | 11 | previous_version="$("$dir"/../scripts/version.sh -s)" 12 | 13 | prerelversion=$("$dir"/../scripts/semver get prerel "$previous_version") 14 | if [[ $prerelversion == "" ]]; then 15 | new_version=$("$dir"/../scripts/semver bump "$bump" "$previous_version") 16 | else 17 | new_version=${previous_version//-$prerelversion/} 18 | fi 19 | 20 | new_version="v$new_version" 21 | 22 | echo "Bumping version from v${previous_version} to ${new_version}" 23 | 24 | git tag -m "release ${new_version}" -a "$new_version" && git push "${ORIGIN}" tag "$new_version" 25 | -------------------------------------------------------------------------------- /scripts/generate_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Clear out old docs 4 | rm out/*.md 5 | 6 | echo "Running doc/main.go" 7 | go run doc/main.go 8 | 9 | echo "Cleaning up output" 10 | 11 | # test for GNU sed. Hat tip: https://stackoverflow.com/a/65497543 12 | if sed --version >/dev/null 2>&1; then 13 | sed -i 's/```/~~~/g' out/*.md 14 | else 15 | sed -i "" -e 's/```/~~~/g' out/*.md 16 | fi 17 | 18 | if [ "$1" ] 19 | then 20 | echo "rsync to $1" 21 | rsync out/ $1 --delete -r -v 22 | fi 23 | -------------------------------------------------------------------------------- /scripts/preflight.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -euo pipefail 3 | 4 | ref= 5 | total= 6 | index= 7 | out= 8 | 9 | while getopts r:t:i:o: name 10 | do 11 | case "$name" in 12 | r) 13 | ref="$OPTARG" 14 | ;; 15 | t) 16 | total="$OPTARG" 17 | ;; 18 | i) 19 | index="$OPTARG" 20 | ;; 21 | o) 22 | out="$OPTARG" 23 | ;; 24 | ?) 25 | printf "Usage: %s: [-r REF] [-t TOTAL] [-i INDEX] [-o FILE]\n" $0 26 | exit 2 27 | ;; 28 | esac 29 | done 30 | 31 | shift $(($OPTIND - 1)) 32 | 33 | test_opts= 34 | if [[ "$ref" != "refs/heads/master" ]]; then 35 | test_opts=-short 36 | fi 37 | 38 | test_log="$(mktemp)" 39 | function finish { 40 | rm "$test_log" 41 | } 42 | trap finish EXIT 43 | 44 | set +e 45 | 46 | gotesplit \ 47 | -total "$total" \ 48 | -index "$index" \ 49 | github.com/superfly/flyctl/test/preflight/... \ 50 | -- --tags=integration -v -timeout=10m $test_opts | tee "$test_log" 51 | test_status=$? 52 | 53 | set -e 54 | 55 | if [[ -n "$out" ]]; then 56 | awk '/^--- FAIL:/{ printf("%s ", $3) }' "$test_log" >> "$out" 57 | echo >> "$out" 58 | fi 59 | 60 | exit $test_status 61 | -------------------------------------------------------------------------------- /scripts/publish_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | BRANCH=flyctl-docs_$1 4 | scripts/generate_docs.sh docs/flyctl/cmd 5 | 6 | cd docs 7 | git config --global user.email "joshua@fly.io" 8 | git config --global user.name "Fly.io CI" 9 | git checkout -b $BRANCH 10 | git add flyctl/cmd 11 | git diff --cached --quiet 12 | 13 | if [ $? -gt 0 ]; then 14 | git commit -a -m "[flyctl-bot] Update docs from flyctl" 15 | git push -f --set-upstream origin HEAD:$BRANCH 16 | gh pr create -t "[flybot] Fly CLI docs update" -b "Fly CLI docs update" -B main -H $BRANCH -r jsierles 17 | gh pr merge --delete-branch --squash 18 | fi 19 | -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | ORIGIN=${ORIGIN:-origin} 2 | 3 | version=$(git fetch --tags "${ORIGIN}" &>/dev/null | git -c "versionsort.prereleasesuffix=-pre" tag -l --sort=version:refname | grep -e ^v0 |grep -v dev | tail -n1 | cut -c 2-) 4 | 5 | echo "$version" 6 | -------------------------------------------------------------------------------- /scripts/yank_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | ORIGIN=${ORIGIN:-origin} 6 | 7 | TAG="v$1" 8 | 9 | echo "Yanking version $TAG" 10 | 11 | localtag () { 12 | if [[ $(git tag -l $TAG) ]]; then 13 | echo 1 14 | else 15 | echo 0 16 | fi 17 | } 18 | 19 | remotetag () { 20 | if [[ $(git ls-remote $ORIGIN --refs refs/tags/$TAG) ]]; then 21 | echo 1 22 | else 23 | echo 0 24 | fi 25 | } 26 | 27 | prompt () { 28 | read -p "Are you sure? " -n 1 -r 29 | if [[ $REPLY =~ ^[Yy]$ ]]; then 30 | echo 1 31 | else 32 | echo 0 33 | fi 34 | } 35 | 36 | LOCAL_EXISTS=$(localtag) 37 | REMOTE_EXISTS=$(remotetag) 38 | 39 | if [[ $LOCAL_EXISTS != 1 && $REMOTE_EXISTS != 1 ]]; then 40 | echo "no tag found" 41 | exit 1 42 | fi 43 | 44 | if [[ $(prompt) != 1 ]]; then 45 | exit 1 46 | fi 47 | 48 | if [[ $LOCAL_EXISTS == 1 ]]; then 49 | echo "deleting local tag" 50 | git tag -d "$TAG" 51 | echo "done" 52 | fi 53 | 54 | if [[ $REMOTE_EXISTS == 1 ]]; then 55 | echo "deleting remote tag" 56 | git push $ORIGIN :refs/tags/$TAG 57 | echo "done" 58 | fi 59 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with (import (fetchTarball https://github.com/nixos/nixpkgs/archive/db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b.tar.gz) {}); 2 | 3 | let 4 | 5 | basePackages = [ 6 | go_1_21 7 | gnumake 8 | gnused 9 | ]; 10 | 11 | # inputs = basePackages 12 | # ++ lib.optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [ 13 | # Security 14 | # ]); 15 | 16 | in mkShell { 17 | buildInputs = basePackages; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ssh/terminal_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package ssh 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/superfly/flyctl/terminal" 13 | "golang.org/x/crypto/ssh" 14 | "golang.org/x/term" 15 | ) 16 | 17 | func (s *SessionIO) getAndWatchSize(ctx context.Context, sess *ssh.Session) (int, int, error) { 18 | fd, ok := getFd(s.Stdin) 19 | if !ok { 20 | return 0, 0, errors.New("could not get console handle") 21 | } 22 | 23 | width, height, err := term.GetSize(fd) 24 | if err != nil { 25 | return 0, 0, err 26 | } 27 | 28 | go func() { 29 | if err := watchWindowSize(ctx, fd, sess); err != nil { 30 | terminal.Debugf("Error watching window size: %s\n", err) 31 | } 32 | }() 33 | 34 | return width, height, nil 35 | } 36 | 37 | func watchWindowSize(ctx context.Context, fd int, sess *ssh.Session) error { 38 | sigc := make(chan os.Signal, 1) 39 | signal.Notify(sigc, syscall.SIGWINCH) 40 | 41 | for { 42 | select { 43 | case <-sigc: 44 | case <-ctx.Done(): 45 | return nil 46 | } 47 | 48 | width, height, err := term.GetSize(fd) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if err := sess.WindowChange(height, width); err != nil { 54 | return err 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/preflight/fixtures/deploy-node/.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /node_modules 3 | .dockerignore 4 | .env 5 | Dockerfile 6 | fly.toml 7 | -------------------------------------------------------------------------------- /test/preflight/fixtures/deploy-node/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1 2 | 3 | # Adjust NODE_VERSION as desired 4 | ARG NODE_VERSION=21.6.2 5 | FROM node:${NODE_VERSION}-slim as base 6 | 7 | LABEL fly_launch_runtime="Node.js" 8 | 9 | # Node.js app lives here 10 | WORKDIR /app 11 | 12 | # Set production environment 13 | ENV NODE_ENV="production" 14 | 15 | 16 | # Throw-away build stage to reduce size of final image 17 | FROM base as build 18 | 19 | # Install packages needed to build node modules 20 | RUN apt-get update -qq && \ 21 | apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 22 | 23 | # Install node modules 24 | COPY --link package-lock.json package.json ./ 25 | RUN npm ci 26 | 27 | # Copy application code 28 | COPY --link . . 29 | 30 | 31 | # Final stage for app image 32 | FROM base 33 | 34 | # Copy built application 35 | COPY --from=build /app /app 36 | 37 | # Start the server by default, this can be overwritten at runtime 38 | EXPOSE 3000 39 | CMD [ "npm", "run", "start" ] 40 | -------------------------------------------------------------------------------- /test/preflight/fixtures/deploy-node/fly.toml: -------------------------------------------------------------------------------- 1 | app = "{{apps.0}}" 2 | primary_region = '{{region}}' 3 | 4 | [build] 5 | dockerfile = 'Dockerfile' 6 | 7 | [deploy] 8 | release_command = "sleep 2" 9 | 10 | [env] 11 | TEST_ID = "{{test.id}}" 12 | 13 | [http_service] 14 | internal_port = 8080 15 | force_https = true 16 | auto_stop_machines = "stop" 17 | auto_start_machines = true 18 | min_machines_running = 0 19 | processes = ['app'] 20 | 21 | [[http_service.checks]] 22 | grace_period = "5s" 23 | interval = "20s" 24 | method = "GET" 25 | timeout = "5s" 26 | path = "/" 27 | 28 | [[vm]] 29 | memory = '1gb' 30 | cpu_kind = 'shared' 31 | cpus = 1 32 | -------------------------------------------------------------------------------- /test/preflight/fixtures/deploy-node/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | http.createServer((request, response) => { 4 | response.writeHead(200, 5 | { 6 | 'Content-Type': 'text/plain' 7 | } 8 | ); 9 | 10 | // prints environment variable value 11 | response.write(`Hello, World! ${process.env["TEST_ID"]}\n`); 12 | response.end(); 13 | 14 | }).listen(8080); 15 | -------------------------------------------------------------------------------- /test/preflight/fixtures/deploy-node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-node", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hello-node", 9 | "version": "1.0.0" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/preflight/fixtures/deploy-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-node", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node index.js" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/preflight/fixtures/deploy-node/somefile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfly/flyctl/7a200d0547f773b3892a907959878b278d04f107/test/preflight/fixtures/deploy-node/somefile -------------------------------------------------------------------------------- /test/preflight/fixtures/example-buildpack/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # "rack" must be explicitly specified. 6 | # https://devcenter.heroku.com/articles/rack 7 | gem "rack" 8 | 9 | # While the doc above doesn't mention that, Ruby no longer have a HTTP server 10 | # by default. Use puma since webrick is not actively maintained. 11 | # https://bugs.ruby-lang.org/issues/17303 12 | gem "puma" 13 | 14 | gem "sinatra" 15 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example-buildpack/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | mustermann (2.0.2) 5 | ruby2_keywords (~> 0.0.1) 6 | nio4r (2.7.4) 7 | puma (6.5.0) 8 | nio4r (~> 2.0) 9 | rack (2.2.13) 10 | rack-protection (2.2.3) 11 | rack 12 | ruby2_keywords (0.0.5) 13 | sinatra (2.2.3) 14 | mustermann (~> 2.0) 15 | rack (~> 2.2) 16 | rack-protection (= 2.2.3) 17 | tilt (~> 2.0) 18 | tilt (2.1.0) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | puma 25 | rack 26 | sinatra 27 | 28 | BUNDLED WITH 29 | 2.6.1 30 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example-buildpack/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | 3 | set :bind, '0.0.0.0' 4 | set :port, 8080 5 | 6 | get '/' do 7 | "Hello World!\n" 8 | end 9 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example-buildpack/config.ru: -------------------------------------------------------------------------------- 1 | require './app' 2 | run Sinatra::Application 3 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example-buildpack/fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for flyctl-example-buildpack on 2022-03-22T13:01:44-06:00 2 | 3 | app = "flyctl-example-buildpack" 4 | 5 | 6 | [build] 7 | builder = "heroku/builder:24" 8 | 9 | [deploy] 10 | release_command = "sh release.sh" 11 | 12 | [env] 13 | PORT = "8080" 14 | 15 | [[services]] 16 | internal_port = 8080 17 | protocol = "tcp" 18 | 19 | [services.concurrency] 20 | hard_limit = 25 21 | soft_limit = 20 22 | 23 | [[services.ports]] 24 | handlers = ["http"] 25 | port = "80" 26 | 27 | [[services.ports]] 28 | handlers = ["tls", "http"] 29 | port = "443" 30 | 31 | [[services.tcp_checks]] 32 | interval = 10000 33 | timeout = 2000 34 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example-buildpack/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | for i in `seq 1 10` 4 | do 5 | echo "sleeping... $i" 6 | sleep 1 7 | done 8 | 9 | # exit 123 10 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | RUN sed -i 's/ listen 80;/ listen 8080;/g' /etc/nginx/conf.d/default.conf 4 | 5 | ARG SOURCE_DIR=. 6 | ARG APP_DIR=/usr/share/nginx/html 7 | ARG ENTRY_FILE=index.html 8 | 9 | COPY release.sh /release.sh 10 | COPY ${SOURCE_DIR} ${APP_DIR} 11 | 12 | ENV NGINX_PORT=8080 13 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example/fly.toml: -------------------------------------------------------------------------------- 1 | app = "damp-fire-3810" 2 | 3 | [deploy] 4 | release_command = "sh /release.sh" 5 | 6 | [env] 7 | LOG_LEVEL = "debug" 8 | ROOTFS_BUILD_DIR = "/rootfsdata" 9 | QUEUES = "bubblegum" 10 | 11 | [[services]] 12 | internal_port = 8080 13 | protocol = "tcp" 14 | 15 | [services.concurrency] 16 | hard_limit = 70 17 | soft_limit = 50 18 | 19 | [[services.http_checks]] 20 | interval = 10000 21 | method = "get" 22 | path = "/" 23 | protocol = "http" 24 | timeout = 2000 25 | tls_skip_verify = false 26 | 27 | [services.http_checks.headers] 28 | 29 | [[services.ports]] 30 | handlers = [ "tls", "http" ] 31 | port = "443" 32 | 33 | [[services.ports]] 34 | handlers = [ "http" ] 35 | port = "80" 36 | 37 | [[services.tcp_checks]] 38 | interval = 10000 39 | timeout = 2000 40 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example/index.html: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/preflight/fixtures/example/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | for i in `seq 1 10` 4 | do 5 | echo "sleeping... $i" 6 | sleep 1 7 | done 8 | 9 | # exit 123 10 | -------------------------------------------------------------------------------- /test/preflight/fixtures/launch-laravel/.gitignore: -------------------------------------------------------------------------------- 1 | flyctl 2 | -------------------------------------------------------------------------------- /test/preflight/fixtures/launch-laravel/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM laravelsail/php81-composer:latest 2 | 3 | ADD flyctl /usr/local/bin 4 | 5 | WORKDIR /app 6 | 7 | RUN composer create-project laravel/laravel /app/testflight 8 | 9 | WORKDIR /app/testflight 10 | 11 | CMD ["flyctl", "launch", "--build-only"] 12 | # CMD ["php", "artisan", "serve", "--host", "0.0.0.0", "--port", "80"] 13 | -------------------------------------------------------------------------------- /test/preflight/fly_console_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package preflight 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "github.com/superfly/flyctl/test/preflight/testlib" 13 | ) 14 | 15 | func TestFlyConsole(t *testing.T) { 16 | f := testlib.NewTestEnvFromEnv(t) 17 | appName := f.CreateRandomAppMachines() 18 | targetOutput := "console test in " + appName 19 | 20 | f.WriteFlyToml(` 21 | app = "%s" 22 | console_command = "/bin/echo '%s'" 23 | 24 | [build] 25 | image = "nginx" 26 | 27 | [processes] 28 | app = "/bin/sleep inf" 29 | `, 30 | appName, targetOutput, 31 | ) 32 | 33 | f.Fly("deploy --ha=false") 34 | 35 | result := f.Fly("console") 36 | output := result.StdOutString() 37 | require.Contains(f, output, targetOutput) 38 | 39 | // Give time for the machine to be destroyed. 40 | require.EventuallyWithT(t, func(c *assert.CollectT) { 41 | ml := f.MachinesList(appName) 42 | assert.Equal(c, 1, len(ml)) 43 | }, 10*time.Second, 1*time.Second) 44 | } 45 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/Khan/genqlient" 7 | ) 8 | -------------------------------------------------------------------------------- /tools/distribute/flypkgs/errors.go: -------------------------------------------------------------------------------- 1 | package flypkgs 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | type ErrorResponse struct { 10 | Code int 11 | Message string `json:"error"` 12 | Messages []string `json:"errors"` 13 | } 14 | 15 | func (e ErrorResponse) Error() string { 16 | var sb strings.Builder 17 | sb.WriteString(fmt.Sprintf("API error: %d\n", e.Code)) 18 | for _, msg := range e.Messages { 19 | sb.WriteString(fmt.Sprintf(" - %s\n", msg)) 20 | } 21 | 22 | return sb.String() 23 | } 24 | 25 | func IsNotFoundErr(err error) bool { 26 | if err == nil { 27 | return false 28 | } 29 | 30 | if e, ok := err.(ErrorResponse); ok { 31 | return e.Code == http.StatusNotFound 32 | } 33 | 34 | return false 35 | } 36 | 37 | func IsConflictError(err error) bool { 38 | if err == nil { 39 | return false 40 | } 41 | 42 | if e, ok := err.(ErrorResponse); ok { 43 | return e.Code == http.StatusConflict 44 | } 45 | 46 | return false 47 | } 48 | -------------------------------------------------------------------------------- /wg/signals_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package wg 4 | 5 | import ( 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func signalChannel(c chan os.Signal) error { 12 | signal.Notify(c, syscall.SIGUSR1) 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /wg/signals_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package wg 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func signalChannel(c chan os.Signal) error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /winbuild.ps1: -------------------------------------------------------------------------------- 1 | # winbuild 2 | go build -o bin/flyctl.exe . 3 | --------------------------------------------------------------------------------