Skip to content

Commit

Permalink
Update readme and activate preview command
Browse files Browse the repository at this point in the history
  • Loading branch information
MightyMoud committed Sep 17, 2024
1 parent 5982d50 commit b99b61b
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 13 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,28 @@ This command will also do a couple of things behind the scenes. You can check th
</details>

### Deploy a preview environment
Sidekick also allows you to deploy preview envs at aney point from your application. Preview envs are attached to your commit hash and require a clean git tree before you can initiate them.
<div align="center" >
<img width="500px" src="/demo/imgs/preview.png">
</div>
Sidekick also allows you to deploy preview envs at any point from your application. Preview envs are attached to your commit hash and require a clean git tree before you can initiate them.
Once you have a clean git tree, you can run the following command to deploy a preview environment:

```bash
sidekick deploy preview
```


<details>
<summary>What does Sidekick do when I run this command</summary>

* Build your docker image locally for linux
* Tag the new image with the short checksum of your git commit
* Compare your latest env file checksum for changes from last time you deployed your application.
* If your env file has changed, sidekick will re encrypt it and replace the encrypte.env file on your server.
* Add a new folder inside your app folder called "preview" where Sidekick will store and manage all your preview deployments
* Deploy a new version of your app reachable on a short hash based subdomain
</details>

This feature is still in development - I will add it to the next release when I'm happy with the way it works.


## Inspiration
Expand Down
251 changes: 251 additions & 0 deletions cmd/compose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"time"

"github.com/mightymoud/sidekick/utils"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)

// composeCmd represents the compose command
var composeCmd = &cobra.Command{
Use: "compose",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
if configErr := utils.ViperInit(); configErr != nil {
pterm.Error.Println("Sidekick config not found - Run sidekick init")
os.Exit(1)
}

// keyAddSshCommand := exec.Command("sh", "-s", "-", viper.Get("serverAddress").(string))
// keyAddSshCommand.Stdin = strings.NewReader(utils.SshKeysScript)
// if sshAddErr := keyAddSshCommand.Run(); sshAddErr != nil {
// panic(sshAddErr)
// }

if utils.FileExists("./docker-compose.yaml") {
pterm.Info.Println("Docker compose file detected - scanning file for details")
} else {
pterm.Error.Println("No Docker compose files found in current directory.")
os.Exit(1)
}
pterm.Info.Println("Analyzing Compose file...")
res, err := os.ReadFile("./docker-compose.yaml")
if err != nil {
pterm.Error.Println("Unable to process your compose file")
}
var compose utils.DockerComposeFile
err = yaml.Unmarshal(res, &compose)
if err != nil {
log.Fatalf("error unmarshalling yaml: %v", err)
}

composeServices := []string{}
for name := range compose.Services {
composeServices = append(composeServices, name)
}
httpServiceName, _ := pterm.DefaultInteractiveSelect.WithOptions(composeServices).WithDefaultText("Please select which service in your docker compose file will recieve http requests").Show()

otherServices := []string{}
for _, v := range composeServices {
if v != httpServiceName {
otherServices = append(otherServices, v)
}
}

// attempt to get a port from dockerfile
appPort := ""
appPortTextInput := pterm.DefaultInteractiveTextInput.WithDefaultValue(appPort)
appPortTextInput.DefaultText = "Please enter the port at which the app receives requests"
appPort, _ = appPortTextInput.Show()
if appPort == "" {
pterm.Error.Println("You you have to enter a port to accept requests")
os.Exit(0)
}

appName := ""
appNameTextInput := pterm.DefaultInteractiveTextInput
appNameTextInput.DefaultText = "Please enter your app url friendly app name"
appName, _ = appNameTextInput.Show()
if appName == "" || strings.Contains(appName, " ") {
pterm.Error.Println("You have to enter url friendly app name")
os.Exit(0)
}

appDomain := ""
appDomainTextInput := pterm.DefaultInteractiveTextInput.WithDefaultValue(fmt.Sprintf("%s.%s.sslip.io", appName, viper.Get("serverAddress").(string)))
appDomainTextInput.DefaultText = "Please enter the domain to point the app to"
appDomain, _ = appDomainTextInput.Show()

envFileName := ""
envFileNameTextInput := pterm.DefaultInteractiveTextInput.WithDefaultValue(".env")
envFileNameTextInput.DefaultText = "Please enter which env file you would like to load"
envFileName, _ = envFileNameTextInput.Show()

hasEnvFile := false
envVariables := []string{}
dockerEnvProperty := []string{}
envFileChecksum := ""
if utils.FileExists(fmt.Sprintf("./%s", envFileName)) {
hasEnvFile = true
pterm.Info.Printfln("Env file detected - Loading env vars from %s", envFileName)
res := utils.HandleEnvFile(envFileName, envVariables, dockerEnvProperty, &envFileChecksum)
fmt.Println(res)
fmt.Println(dockerEnvProperty)
defer os.Remove("encrypted.env")
} else {
pterm.Info.Println("No env file detected - Skipping env parsing")
}
// make a docker service
imageName := fmt.Sprintf("%s/%s", viper.Get("dockerUsername").(string), appName)
currentService := compose.Services[httpServiceName]
currentService.Image = imageName
currentService.Labels = []string{
"traefik.enable=true",
fmt.Sprintf("traefik.http.routers.%s.rule=Host(`%s`)", appName, appDomain),
fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port=%s", appName, appPort),
fmt.Sprintf("traefik.http.routers.%s.tls=true", appName),
fmt.Sprintf("traefik.http.routers.%s.tls.certresolver=default", appName),
"traefik.docker.network=sidekick",
}
currentService.Environment = dockerEnvProperty
currentService.Networks = []string{
"sidekick",
}
currentService.DependsOn = otherServices
compose.Services[httpServiceName] = currentService

compose.Networks =
map[string]utils.DockerNetwork{
"sidekick": {
External: true,
},
}

dockerComposeFile, err := yaml.Marshal(&compose)
if err != nil {
fmt.Printf("Error marshalling YAML: %v\n", err)
return
}
err = os.WriteFile("docker-compose.yaml", dockerComposeFile, 0644)
if err != nil {
fmt.Printf("Error writing file: %v\n", err)
return
}
os.Exit(2)

multi := pterm.DefaultMultiPrinter
launchPb, _ := pterm.DefaultProgressbar.WithTotal(3).WithWriter(multi.NewWriter()).Start("Booting up app on VPS")
loginSpinner, _ := utils.GetSpinner().WithWriter(multi.NewWriter()).Start("Logging into VPS")
dockerBuildSpinner, _ := utils.GetSpinner().WithWriter(multi.NewWriter()).Start("Preparing docker image")
setupSpinner, _ := utils.GetSpinner().WithWriter(multi.NewWriter()).Start("Setting up application")

multi.Start()

loginSpinner.Sequence = []string{"▀ ", " ▀", " ▄", "▄ "}
sshClient, err := utils.Login(viper.Get("serverAddress").(string), "sidekick")
if err != nil {
loginSpinner.Fail("Something went wrong logging in to your VPS")
panic(err)
}
loginSpinner.Success("Logged in successfully!")
launchPb.Increment()

dockerBuildSpinner.Sequence = []string{"▀ ", " ▀", " ▄", "▄ "}
cwd, _ := os.Getwd()
dockerBuildCommd := exec.Command("sh", "-s", "-", appName, viper.Get("dockerUsername").(string), cwd)
dockerBuildCommd.Stdin = strings.NewReader(utils.DockerHandleScript)
// better handle of errors -> Push it to another writer aside from os.stderr and then flush it when it panics
if dockerBuildErr := dockerBuildCommd.Run(); dockerBuildErr != nil {
log.Fatalln("Failed to run docker")
os.Exit(1)
}
dockerBuildSpinner.Success("Successfully built and pushed docker image")
launchPb.Increment()

setupSpinner.Sequence = []string{"▀ ", " ▀", " ▄", "▄ "}
_, sessionErr := utils.RunCommand(sshClient, fmt.Sprintf("mkdir %s", appName))
if sessionErr != nil {
panic(sessionErr)
}
rsync := exec.Command("rsync", "docker-compose.yaml", fmt.Sprintf("%s@%s:%s", "sidekick", viper.Get("serverAddress").(string), fmt.Sprintf("./%s", appName)))
rsync.Run()
if hasEnvFile {
encryptSync := exec.Command("rsync", "encrypted.env", fmt.Sprintf("%s@%s:%s", "sidekick", viper.Get("serverAddress").(string), fmt.Sprintf("./%s", appName)))
encryptSync.Run()

_, sessionErr1 := utils.RunCommand(sshClient, fmt.Sprintf(`cd %s && sops exec-env encrypted.env 'docker compose -p sidekick up -d'`, appName))
if sessionErr1 != nil {
fmt.Println("something went wrong")
}
} else {
_, sessionErr1 := utils.RunCommand(sshClient, fmt.Sprintf(`cd %s && docker compose -p sidekick up -d`, appName))
if sessionErr1 != nil {
panic(sessionErr1)
}
}

portNumber, err := strconv.ParseUint(appPort, 0, 64)
if err != nil {
panic(err)
}
envConfig := utils.SidekickAppEnvConfig{}
if hasEnvFile {
envConfig.File = envFileName
envConfig.Hash = envFileChecksum
}
// save app config in same folder
sidekickAppConfig := utils.SidekickAppConfig{
Name: appName,
Version: "V1",
Image: fmt.Sprintf("%s/%s", viper.Get("dockerUsername"), appName),
Port: portNumber,
Url: appDomain,
CreatedAt: time.Now().Format(time.UnixDate),
Env: envConfig,
}
ymlData, err := yaml.Marshal(&sidekickAppConfig)
os.WriteFile("./sidekick.yml", ymlData, 0644)
launchPb.Increment()

setupSpinner.Success("🙌 App setup successfully 🙌")
multi.Stop()

pterm.Println()
pterm.Info.Printfln("😎 Access your app at: https://%s", appDomain)
pterm.Println()

},
}

func init() {
rootCmd.AddCommand(composeCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// composeCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// composeCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
11 changes: 6 additions & 5 deletions cmd/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"os"
"os/exec"
"strings"
"time"

"github.com/mightymoud/sidekick/utils"
"github.com/pterm/pterm"
Expand Down Expand Up @@ -169,9 +170,9 @@ to quickly create a Cobra application.`,
}
}
previewEnvConfig := utils.SidekickPreview{
Name: serviceName,
Url: fmt.Sprintf("https://%s", previewURL),
Image: imageName,
Url: fmt.Sprintf("https://%s", previewURL),
Image: imageName,
CreatedAt: time.Now().Format(time.UnixDate),
}
appConfig.PreviewEnvs = map[string]utils.SidekickPreview{
deployHash: previewEnvConfig,
Expand All @@ -184,14 +185,14 @@ to quickly create a Cobra application.`,
multi.Stop()

pterm.Println()
pterm.Info.Printfln("😎 Access your app at: https://%s.%s", deployHash, appConfig.Url)
pterm.Info.Printfln("😎 Access your preview app at: https://%s.%s", deployHash, appConfig.Url)
pterm.Println()

},
}

func init() {

deployCmd.AddCommand(previewCmd)
// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
Expand Down
Binary file added demo/imgs/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion utils/scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var DockerHandleScript = `
projectFolder=$3
tag=${4:-"latest"}
docker build --cache-from=$dockerUsername/$appName:$tag --tag $appName --platform linux/amd64 $projectFolder
docker build --cache-from=$dockerUsername/$appName:latest --tag $appName --platform linux/amd64 $projectFolder
docker tag $appName $dockerUsername/$appName:$tag
Expand Down
16 changes: 11 additions & 5 deletions utils/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ package utils
type DockerService struct {
Image string `yaml:"image"`
Command string `yaml:"command,omitempty"`
Restart string `yaml:"restart,omitempty"`
Ports []string `yaml:"ports,omitempty"`
Volumes []string `yaml:"volumes,omitempty"`
Labels []string `yaml:"labels,omitempty"`
Networks []string `yaml:"networks,omitempty"`
Environment []string `yaml:"environment,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
}

type DockerNetwork struct {
Expand All @@ -30,18 +32,22 @@ type DockerNetwork struct {

type DockerComposeFile struct {
Services map[string]DockerService `yaml:"services"`
Networks map[string]DockerNetwork `yaml:"networks"`
Networks map[string]DockerNetwork `yaml:"networks,omitempty"`
Volumes map[string]DockerVolume `yaml:"volumes,omitempty"`
}

type DockerVolume struct {
Driver string `yaml:"driver,omitempty"`
}
type SidekickAppEnvConfig struct {
File string `yaml:"file"`
Hash string `yaml:"hash"`
}

type SidekickPreview struct {
Name string `yaml:"name"`
Url string `yaml:"url"`
Image string `yaml:"image"`
Url string `yaml:"url"`
Image string `yaml:"image"`
CreatedAt string `yaml:"createdAt"`
}

type SidekickAppConfig struct {
Expand All @@ -52,5 +58,5 @@ type SidekickAppConfig struct {
Port uint64 `yaml:"port"`
CreatedAt string `yaml:"createdAt"`
Env SidekickAppEnvConfig `yaml:"env,omitempty"`
PreviewEnvs map[string]SidekickPreview `yaml:"previewEnvs"`
PreviewEnvs map[string]SidekickPreview `yaml:"previewEnvs,omitempty"`
}

0 comments on commit b99b61b

Please sign in to comment.