Skip to content

Commit

Permalink
Add docker like koyeb compose functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
pawelbeza committed May 7, 2024
1 parent 941e46a commit 3403fd9
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 0 deletions.
21 changes: 21 additions & 0 deletions examples/koyeb-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: test-compose-service
services:
compose-go-service:
git:
repository: github.com/koyeb/example-golang
branch: main
ports:
- port: 8000
regions: ['par']
scalings:
- scopes: ['region:par']
min: 1
max: 1
instance_types:
- scopes: ['region:par']
type: 'nano'
envs:
- scopes: ['region:par']
key: 'PORT'
value: '8000'

4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ require (

require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/briandowns/spinner v1.23.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
Expand All @@ -38,6 +40,8 @@ require (
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
Expand Down Expand Up @@ -71,6 +73,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
Expand Down Expand Up @@ -180,6 +184,12 @@ github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamh
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
Expand Down Expand Up @@ -374,6 +384,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -396,6 +407,8 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
221 changes: 221 additions & 0 deletions pkg/koyeb/compose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package koyeb

import (
"fmt"
"os"
"slices"
"time"

"github.com/briandowns/spinner"
"github.com/ghodss/yaml"
"github.com/koyeb/koyeb-api-client-go/api/v1/koyeb"
"github.com/koyeb/koyeb-cli/pkg/koyeb/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func NewComposeCmd() *cobra.Command {
return &cobra.Command{
Use: "compose KOYEB_COMPOSE_FILE_PATH",
Short: "Create koyeb resources read from koyeb-compose.yaml file",
Example: `
# Init koyeb-compose.yaml file
$> echo 'apps:
- name: example-app
services:
- name: example-service1
image: nginx:latest
ports:
- 80:80
- name: example-service2
path: github.com/koyeb/golang-example-app
branch: main
ports:
- 8080:8080
depends_on:
- example-service1' > koyeb-compose.yaml
# Apply compose file
$> koyeb compose koyeb-compose.yaml
`,
Args: cobra.ExactArgs(1),
RunE: WithCLIContext(func(ctx *CLIContext, cmd *cobra.Command, args []string) error {
composeFile, err := parseComposeFile(args[0])
if err != nil {
return err
}

// TODO (pawel) validate compose file
// TODO (pawel) better error handling and tips how to fix the errors

return NewKoyebComposeHandler().Compose(ctx, composeFile)
}),
}
}

func parseComposeFile(path string) (*KoyebCompose, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}

var config *KoyebCompose
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}

return config, nil
}

type KoyebCompose struct {
koyeb.CreateApp `yaml:",inline"`
Services map[string]KoyebComposeService `yaml:"services"`
}

type KoyebComposeService struct {
koyeb.DeploymentDefinition `yaml:",inline"`
DependsOn []string `json:"depends_on"`
}

type KoyebComposeHandler struct{}

func NewKoyebComposeHandler() *KoyebComposeHandler {
return &KoyebComposeHandler{}
}

func (h *KoyebComposeHandler) Compose(ctx *CLIContext, koyebCompose *KoyebCompose) error {
appName := *koyebCompose.Name
appId, err := h.CreateAppIfNotExists(ctx, appName)
if err != nil {
return err
}

for serviceName, serviceDetails := range koyebCompose.Services {
serviceDefinition := &serviceDetails.DeploymentDefinition
serviceDefinition.Name = koyeb.PtrString(serviceName)

deploymentId, err := h.UpdateService(ctx, appId, appName, serviceDefinition)
if err != nil {
return err
}

err = h.MonitorService(ctx, deploymentId, serviceName)
if err != nil {
return err
}

}

return nil
}

func (h *KoyebComposeHandler) isMonitoringEndState(status koyeb.DeploymentStatus) bool {
return slices.Contains([]koyeb.DeploymentStatus{
koyeb.DEPLOYMENTSTATUS_HEALTHY,
koyeb.DEPLOYMENTSTATUS_DEGRADED,
koyeb.DEPLOYMENTSTATUS_UNHEALTHY,
koyeb.DEPLOYMENTSTATUS_CANCELED,
koyeb.DEPLOYMENTSTATUS_STOPPED,
koyeb.DEPLOYMENTSTATUS_ERROR,
}, status)
}

func (h *KoyebComposeHandler) MonitorService(ctx *CLIContext, deploymentId, serviceName string) error {
s := spinner.New(spinner.CharSets[21], 100*time.Millisecond, spinner.WithColor("green"))
s.Start()
defer s.Stop()

previousStatus := koyeb.DeploymentStatus("")
// it's dumb as it's busy waiting but for now we don't support streaming events
for {
resDeployment, resp, err := ctx.Client.DeploymentsApi.GetDeployment(ctx.Context, deploymentId).Execute()
if err != nil {
return errors.NewCLIErrorFromAPIError(
"Error while fetching deployment status",
err,
resp,
)
}

currentStatus := resDeployment.Deployment.GetStatus()
if previousStatus != currentStatus {
previousStatus = currentStatus
s.Suffix = fmt.Sprintf(" Deploying service %s: %s", serviceName, currentStatus)
}

if h.isMonitoringEndState(currentStatus) {
break
}

time.Sleep(5 * time.Second)
}

if previousStatus == koyeb.DEPLOYMENTSTATUS_HEALTHY {
log.Infof("\nSucccessfully deployed %v ✅", serviceName)
} else {
log.Errorf("\nFailed to deploy %v deployment status: %v ❌", serviceName, previousStatus)
return fmt.Errorf("failed to deploy %v", serviceName)
}

return nil
}

// Creates app if not exists and returns app id and error if any
func (h *KoyebComposeHandler) CreateAppIfNotExists(ctx *CLIContext, appName string) (string, error) {
appId, err := NewAppHandler().ResolveAppArgs(ctx, appName)
if err == nil {
return appId, nil
}

// if we fail to fetch the app id then failover to app creation
createApp := koyeb.CreateApp{Name: &appName}
resApp, resp, err := ctx.Client.AppsApi.CreateApp(ctx.Context).App(createApp).Execute()
if err != nil {
return "", errors.NewCLIErrorFromAPIError(
fmt.Sprintf("Error while creating the application `%s`", appName),
err,
resp,
)
}

return resApp.App.GetId(), nil
}

// Updates service or creates one if not exists
// returns deployment id
func (h *KoyebComposeHandler) UpdateService(ctx *CLIContext, appId, appName string, deploymentDefinition *koyeb.DeploymentDefinition) (string, error) {
fullServiceName := fmt.Sprintf("%s/%s", appName, *deploymentDefinition.Name)
serviceId, err := NewServiceHandler().ResolveServiceArgs(ctx, fullServiceName)
if err != nil {
// if we fail to fetch the service id then failover to service creation
createService := &koyeb.CreateService{
AppId: &appId,
Definition: deploymentDefinition,
}

resService, resp, err := ctx.Client.ServicesApi.CreateService(ctx.Context).Service(*createService).Execute()
if err != nil {
return "", errors.NewCLIErrorFromAPIError(
"Error while creating the service",
err,
resp,
)
}

return *resService.Service.LatestDeploymentId, nil
}

updateService := &koyeb.UpdateService{
Definition: deploymentDefinition,
}
resService, resp, err := ctx.Client.ServicesApi.UpdateService(ctx.Context, serviceId).Service(*updateService).Execute()
if err != nil {
return "", errors.NewCLIErrorFromAPIError(
"Error while creating the service",
err,
resp,
)
}

return *resService.Service.LatestDeploymentId, nil
}
1 change: 1 addition & 0 deletions pkg/koyeb/koyeb.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func GetRootCommand() *cobra.Command {
rootCmd.AddCommand(NewRegionalDeploymentCmd())
rootCmd.AddCommand(NewDatabaseCmd())
rootCmd.AddCommand(NewMetricsCmd())
rootCmd.AddCommand(NewComposeCmd())
return rootCmd
}

Expand Down

0 comments on commit 3403fd9

Please sign in to comment.