Skip to content

Commit

Permalink
Merge pull request #6 from row-of-code/main
Browse files Browse the repository at this point in the history
Restrict deployment to single server configuration
  • Loading branch information
yarlson authored Jan 18, 2025
2 parents ba4b1b0 + 56e6153 commit 708eae4
Show file tree
Hide file tree
Showing 24 changed files with 150 additions and 256 deletions.
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# FTL (Faster Than Light) Deployment

FTL is a lightweight deployment tool designed to simplify cloud deployments without the complexity of traditional CI/CD pipelines or container orchestration platforms. It provides automated, zero-downtime deployments to various cloud providers through a single YAML configuration file.
FTL is a lightweight deployment tool designed to simplify cloud deployments without the complexity of traditional CI/CD pipelines or container orchestration platforms. It provides automated, zero-downtime deployments through a single YAML configuration file.

For comprehensive documentation, visit [https://ftl-deploy.org](https://ftl-deploy.org)

Expand Down Expand Up @@ -62,14 +62,14 @@ project:
domain: my-project.example.com
email: [email protected]

servers:
- host: my-project.example.com
port: 22
user: my-project
ssh_key: ~/.ssh/id_rsa
server:
host: my-project.example.com
port: 22
user: my-project
ssh_key: ~/.ssh/id_rsa

services:
- name: my-app
- name: web
image: my-app:latest
port: 80
health_check:
Expand Down Expand Up @@ -160,9 +160,6 @@ ftl logs my-app -n 150
```bash
# Create tunnels for all dependencies
ftl tunnels
# Connect to specific server
ftl tunnels --server my-project.example.com
```

## Development
Expand Down
19 changes: 4 additions & 15 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import (

var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy your application to configured servers",
Long: `Deploy your application to all servers defined in ftl.yaml.
Short: "Deploy your application to configured server",
Long: `Deploy your application to the server defined in ftl.yaml.
This command handles the entire deployment process, ensuring
zero-downtime updates of your services.`,
Run: runDeploy,
Expand All @@ -39,14 +39,13 @@ func runDeploy(cmd *cobra.Command, args []string) {
sm := console.NewSpinnerManager()
sm.Start()

if err := deployToServers(cfg, sm); err != nil {
if err := deployToServer(cfg.Project.Name, cfg, cfg.Server, sm); err != nil {
sm.Stop()
console.Error("Deployment failed")
console.Error("Deployment failed:", err)
return
}

sm.Stop()

console.Success("Deployment completed successfully")
}

Expand All @@ -64,16 +63,6 @@ func parseConfig(filename string) (*config.Config, error) {
return cfg, nil
}

func deployToServers(cfg *config.Config, sm *console.SpinnerManager) error {
for _, server := range cfg.Servers {
if err := deployToServer(cfg.Project.Name, cfg, server, sm); err != nil {
return fmt.Errorf("failed to deploy to server %s: %w", server.Host, err)
}
}

return nil
}

func deployToServer(project string, cfg *config.Config, server config.Server, sm *console.SpinnerManager) error {
hostname := server.Host

Expand Down
35 changes: 14 additions & 21 deletions cmd/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ var (
// logsCmd represents the logs command
var logsCmd = &cobra.Command{
Use: "logs [service]",
Short: "Fetch logs from remote deployments",
Long: `Fetch logs from the specified service running on remote servers.
Short: "Fetch logs from remote deployment",
Long: `Fetch logs from the specified service running on remote server.
If no service is specified, logs from all services will be fetched.
Use the -f flag to stream logs in real-time.`,
Args: cobra.MaximumNArgs(1),
Expand Down Expand Up @@ -49,43 +49,36 @@ func runLogs(cmd *cobra.Command, args []string) {
return
}

if err := getLogsFromServers(cfg, serviceName, follow, tail); err != nil {
if err := getLogs(cfg, serviceName, follow, tail); err != nil {
console.Error("Failed to fetch logs:", err)
return
}
}

func getLogsFromServers(cfg *config.Config, serviceName string, follow bool, tail int) error {
func getLogs(cfg *config.Config, serviceName string, follow bool, tail int) error {
services := []string{}

if serviceName != "" {
services = append(services, serviceName)
} else {
// Collect all service names from the config
for _, service := range cfg.Services {
services = append(services, service.Name)
}
}

for _, server := range cfg.Servers {
console.Info(fmt.Sprintf("Fetching logs from server %s...", server.Host))
console.Info(fmt.Sprintf("Fetching logs from server %s...", cfg.Server.Host))

runner, err := connectToServer(server)
if err != nil {
console.Error(fmt.Sprintf("Failed to connect to server %s: %v", server.Host, err))
continue
}
defer runner.Close()

logger := logs.NewLogger(runner)
runner, err := connectToServer(cfg.Server)
if err != nil {
return fmt.Errorf("failed to connect to server %s: %v", cfg.Server.Host, err)
}
defer runner.Close()

ctx := context.Background()
err = logger.FetchLogs(ctx, cfg.Project.Name, services, follow, tail)
logger := logs.NewLogger(runner)
ctx := context.Background()

if err != nil {
console.Error(fmt.Sprintf("Failed to fetch logs from server %s: %v", server.Host, err))
continue
}
if err := logger.FetchLogs(ctx, cfg.Project.Name, services, follow, tail); err != nil {
return fmt.Errorf("failed to fetch logs from server %s: %v", cfg.Server.Host, err)
}

return nil
Expand Down
10 changes: 7 additions & 3 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (

var setupCmd = &cobra.Command{
Use: "setup",
Short: "Prepare servers for deployment",
Long: `Setup configures servers defined in ftl.yaml for deployment.
Short: "Prepare server for deployment",
Long: `Setup configures server defined in ftl.yaml for deployment.
Run this once for each new server before deploying your application.`,
Run: runSetup,
}
Expand Down Expand Up @@ -66,7 +66,11 @@ func runSetup(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()

if err := server.SetupServers(ctx, cfg, dockerCreds, newUserPassword, sm); err != nil {
if err := server.Setup(ctx, cfg, server.DockerCredentials{
Username: dockerCreds.Username,
Password: dockerCreds.Password,
}, newUserPassword, sm); err != nil {
sm.Stop()
console.Error("Setup failed:", err)
return
}
Expand Down
33 changes: 3 additions & 30 deletions cmd/tunnels.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import (
"syscall"
"time"

"github.com/yarlson/ftl/pkg/config"

"github.com/spf13/cobra"

"github.com/yarlson/ftl/pkg/config"
"github.com/yarlson/ftl/pkg/console"
"github.com/yarlson/ftl/pkg/ssh"
)
Expand All @@ -27,8 +26,6 @@ forwarding local ports to remote ports.`,

func init() {
rootCmd.AddCommand(tunnelsCmd)

tunnelsCmd.Flags().StringP("server", "s", "", "Server name or index to connect to")
}

func runTunnels(cmd *cobra.Command, args []string) {
Expand All @@ -44,15 +41,6 @@ func runTunnels(cmd *cobra.Command, args []string) {
return
}

serverName, _ := cmd.Flags().GetString("server")
serverConfig, err := selectServer(cfg, serverName)
if err != nil {
spinner.ErrorWithMessagef("Server selection failed: %v", err)
return
}

user := serverConfig.User

tunnels, err := collectDependencyTunnels(cfg)
if err != nil {
spinner.ErrorWithMessagef("Failed to collect dependencies: %v", err)
Expand All @@ -73,9 +61,9 @@ func runTunnels(cmd *cobra.Command, args []string) {
wg.Add(1)
go func(t TunnelConfig) {
defer wg.Done()
err := ssh.CreateSSHTunnel(ctx, serverConfig.Host, serverConfig.Port, user, serverConfig.SSHKey, t.LocalPort, t.RemoteAddr)
err := ssh.CreateSSHTunnel(ctx, cfg.Server.Host, cfg.Server.Port, cfg.Server.User, cfg.Server.SSHKey, t.LocalPort, t.RemoteAddr)
if err != nil {
errorChan <- fmt.Errorf("Tunnel %s -> %s failed: %v", t.LocalPort, t.RemoteAddr, err)
errorChan <- fmt.Errorf("tunnel %s -> %s failed: %v", t.LocalPort, t.RemoteAddr, err)
}
}(tunnel)
}
Expand Down Expand Up @@ -106,21 +94,6 @@ func runTunnels(cmd *cobra.Command, args []string) {
time.Sleep(1 * time.Second)
}

func selectServer(cfg *config.Config, serverName string) (config.Server, error) {
if serverName != "" {
for _, srv := range cfg.Servers {
if srv.Host == serverName || srv.User == serverName {
return srv, nil
}
}
return config.Server{}, fmt.Errorf("server not found in configuration: %s", serverName)
} else if len(cfg.Servers) == 1 {
return cfg.Servers[0], nil
} else {
return config.Server{}, fmt.Errorf("multiple servers defined. Please specify a server using the --server flag")
}
}

type TunnelConfig struct {
LocalPort string
RemoteAddr string
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

type Config struct {
Project Project `yaml:"project" validate:"required"`
Servers []Server `yaml:"servers" validate:"required,dive"`
Server Server `yaml:"server" validate:"required"`
Services []Service `yaml:"services" validate:"required,dive"`
Dependencies []Dependency `yaml:"dependencies" validate:"dive"`
Volumes []string `yaml:"volumes" validate:"dive"`
Expand Down
15 changes: 5 additions & 10 deletions pkg/config/sample/ftl.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,11 @@ project:
domain: my-project.example.com
email: [email protected]

servers:
- host: my-project.example.com
port: 22
user: my-project
ssh_key: ~/.ssh/id_rsa

- host: 127.0.0.1
port: 22
user: my-project
ssh_key: ~/.ssh/id_rsa
server:
host: my-project.example.com
port: 22
user: my-project
ssh_key: ~/.ssh/id_rsa

services:
- name: my-app
Expand Down
36 changes: 7 additions & 29 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"path/filepath"
"strings"
"sync"

gossh "golang.org/x/crypto/ssh"

Expand All @@ -22,35 +21,14 @@ type DockerCredentials struct {
Password string
}

// SetupServers performs the setup on all servers concurrently.
func SetupServers(ctx context.Context, cfg *config.Config, dockerCreds DockerCredentials, newUserPassword string, sm *console.SpinnerManager) error {
var wg sync.WaitGroup
errChan := make(chan error, len(cfg.Servers))

for _, server := range cfg.Servers {
wg.Add(1)
go func(server config.Server) {
defer wg.Done()

if err := setupServer(ctx, server, dockerCreds, newUserPassword, sm); err != nil {
errChan <- fmt.Errorf("[%s] Setup failed: %w", server.Host, err)
return
}
}(server)
}

wg.Wait()
close(errChan)

var errs []error
for err := range errChan {
errs = append(errs, err)
// Setup performs the server setup
func Setup(ctx context.Context, cfg *config.Config, dockerCreds DockerCredentials, newUserPassword string, sm *console.SpinnerManager) error {
spinner := sm.AddSpinner("setup", fmt.Sprintf("[%s] Setting up server", cfg.Server.Host))
if err := setupServer(ctx, cfg.Server, dockerCreds, newUserPassword, sm); err != nil {
spinner.ErrorWithMessagef("Setup failed: %v", err)
return fmt.Errorf("[%s] Setup failed: %w", cfg.Server.Host, err)
}

if len(errs) > 0 {
return fmt.Errorf("errors occurred during server setup: %v", errs)
}

spinner.Complete()
return nil
}

Expand Down
37 changes: 17 additions & 20 deletions schemas/ftl-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,23 @@
}
}
},
"servers": {
"type": "array",
"items": {
"type": "object",
"required": ["host", "port", "user", "ssh_key"],
"properties": {
"host": {
"type": "string",
"format": "hostname-or-ip"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"user": { "type": "string" },
"ssh_key": {
"type": "string",
"format": "file-path"
}
"server": {
"type": "object",
"required": ["host", "port", "user", "ssh_key"],
"properties": {
"host": {
"type": "string",
"format": "hostname-or-ip"
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"user": { "type": "string" },
"ssh_key": {
"type": "string",
"format": "file-path"
}
}
},
Expand Down
12 changes: 6 additions & 6 deletions www/docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ FTL uses a single YAML configuration file (`ftl.yaml`) to define your entire dep
The `ftl.yaml` file is organized into these main sections:

- **Project**: Basic project information like name, domain, and contact details
- **Servers**: Target server specifications and SSH connection details
- **Server**: Target server specifications and SSH connection details
- **Services**: Your application services that will be deployed
- **Dependencies**: Supporting services like databases and caches
- **Volumes**: Persistent storage definitions
Expand All @@ -25,11 +25,11 @@ project:
domain: my-project.example.com
email: [email protected]

servers:
- host: my-project.example.com
port: 22
user: deploy
ssh_key: ~/.ssh/id_rsa
server:
host: my-project.example.com
port: 22
user: deploy
ssh_key: ~/.ssh/id_rsa

services:
- name: web
Expand Down
Loading

0 comments on commit 708eae4

Please sign in to comment.